5.3 KiB
title
| 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
# collections/<name>.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
// _/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)
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").
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.