Files
tibi-docs/src/content/docs/nova/custom-export-script.md
T

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.