--- title: 'Custom Export Scripts (`collection.meta.exports`)' --- # Custom Export Scripts (`collection.meta.exports`) Custom exports are ES modules that run **client-side** in the ExportDialog. One module may serve several exports — `ctx.exportName` tells them apart. ## Config ```yaml # collections/.yml meta: exports: - name: bestellreport label: { de: "Bestellreport", en: "Order report" } file: "_/assets/exports/reports.mjs" # _/... project-relative (recommended); # also: /_/... (alias), /assets/... (host-absolute), https://..., data:, blob:, { eval } filename: "bestellreport-{date}.xlsx" # optional, {date} → YYYY-MM-DD ``` No `select` — the script loads via `db.find` / `db.loadAll` itself (server syntax, see below). Server schema: `tibi-types/schemas/config/collection-meta.schema.json` (`meta.exports`, `additionalProperties: false` — `name` + `file` required). In the ExportDialog, collections with `meta.exports` show two tabs: **Predefined** (custom scripts, active by default — no format/field selection) and **Generic** (the standard JSON/CSV/XLSX export). Without `meta.exports` the dialog is unchanged (generic only, no tabs). ## Module contract ```js // _/assets/exports/reports.mjs export default async function (ctx) { if (ctx.exportName === "bestellreport") { /* … */ } return { workbook: ctx.xlsx.utils.book_new(), // OR blob | text | bytes (exactly one) filename: "report-{date}.xlsx", // {date} supported contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", } } ``` Return: `{ filename, contentType }` + exactly one payload — `workbook` (XLSX workbook, needs Excel contentType), `blob`, `text` (CSV/JSON/TXT), `bytes` (`Uint8Array`/`ArrayBuffer`, e.g. PDF). Filenames are sanitized (`/:*?"<>|` → `_`). See `frontend/src/lib/utils/custom-export-result.ts`. ## `ctx` (mirrors tibi-server hook context where sensible) | Key | Notes | |---|---| | `api`, `project`, `collection` | GetMap-shaped JSON copies (safe to read, mutating is pointless) | | `namespace`, `$project`, `$namespace`, `$method="export"`, `context` (self) | like `jsData` | | `export` / `exportName` | own `meta.exports` entry / its `name` | | `view` | current list state: `search`, `searchMode`, `sortField`, `sortOrder`, `manualSort`, `fieldFilters`, `subNavFilter`, `filter` (merged) | | `db.find(coll, opts)` | server syntax: `{ filter, select, selector{…:1}, sort["-a","b"], offset, limit, lookup }`. Only `sort[0]` is used (REST limit). | | `db.count(coll, { filter })` | server syntax | | `db.loadAll(coll, opts, onProgress?)` | client helper: count → 500-batches (retry-once) + abort + progress. Or loop `find` manually. | | `xlsx` | gebundelte `xlsx-js-style`-Lib (SheetJS + Zell-Styles, siehe unten) — kein Eigen-Import nötig | | `helpers` | `deepGet`, `formatValue`, `csvCell` (formula-injection guard), `csvWithBom` (Excel umlauts) | | `lang`, `languageCodes`, `defaultLanguage` | i18n | | `progress(done, total, label?)` | dialog progress bar + label | | `signal` | `AbortSignal` — "Abbrechen" button; `fetch` throws `AbortError` | No `$auth`: requests run as the logged-in admin (`fetchWithAuth`), the server enforces permissions. Read-only: no create/update/delete. ## Full example (multi-sheet Excel) ```js export default async function (ctx) { ctx.progress(0, 0, "Lade Bestellungen…") const orders = await ctx.db.loadAll( "orders", { select: ["orderNo", "totals"], filter: ctx.view.filter }, (done, total) => ctx.progress(done, total, "Lade Bestellungen…") ) const wb = ctx.xlsx.utils.book_new() ctx.xlsx.utils.book_append_sheet( wb, ctx.xlsx.utils.aoa_to_sheet([["Order", "Sum"], ...orders.map((o) => [o.orderNo, o.totals?.grandTotal ?? ""])]), "Übersicht" ) const lines = orders.flatMap((o) => (o.items ?? []).map((it) => [o.orderNo, it.sku, it.qty])) ctx.xlsx.utils.book_append_sheet(wb, ctx.xlsx.utils.aoa_to_sheet([["Order", "SKU", "Qty"], ...lines]), "Positionen") return { workbook: wb, filename: "bestellreport-{date}.xlsx", contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } } ``` Demo: `frontend/assets/mock/demo-exports/reports.mjs` (mock `pages` collection). Types: `frontend/src/lib/utils/custom-export.types.ts` (single source of truth). ## Styling (`cell.s` — nur via `xlsx-js-style` möglich) `ctx.xlsx` ist `xlsx-js-style` (SheetJS-API + schreibbare Zell-Styles). Unterstützte `cell.s`-Keys: `fill`, `font`, `alignment`, `border`, `numFmt` (Farben als ARGB, z.B. `"FF2F5496"`). ```js const HEADER_FILL = "FF2F5496" function styleHeaderRow(ws, colCount) { for (let c = 0; c < colCount; c++) { const addr = ctx.xlsx.utils.encode_cell({ r: 0, c }) if (ws[addr]) ws[addr].s = { fill: { patternType: "solid", fgColor: { rgb: HEADER_FILL } }, font: { bold: true, color: { rgb: "FFFFFFFF" } }, } } } const ws = ctx.xlsx.utils.aoa_to_sheet([["Order", "Sum"], ...orders.map((o) => [o.orderNo, o.totals?.grandTotal ?? ""])]) styleHeaderRow(ws, 2) ``` Spaltenbreiten + Autofilter gehen ebenfalls ohne Zusatz-Lib: `ws["!cols"] = [{ wch: 18 }, { wch: 14 }]` und `ws["!autofilter"] = { ref: "A1:B10" }`. Hinweis für Tests: `XLSX.read(buf, { cellStyles: true })` — ohne die Option werden Styles beim Lesen ignoriert.