Initial: Tibi-Doku auf Starlight-Basis (Nova-Theme, Docker-Stack, Sync-Skripte)

This commit is contained in:
apairon committed 2026-10-16 11:43:04 +00:00
commit 58502aa3c8
78 files changed
+22860

No files matched your search

@@ -0,0 +1,121 @@
---
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/<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
```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.