Initial: Tibi-Doku auf Starlight-Basis (Nova-Theme, Docker-Stack, Sync-Skripte)
This commit is contained in:
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.
|
||||
Reference in new issue
Block a user