@efa-one/sdk 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/frontend/ui/DataTable.d.ts +10 -2
- package/frontend/ui/DataTable.js +64 -7
- package/frontend/ui/index.d.ts +1 -0
- package/frontend/ui/index.js +1 -0
- package/frontend/ui/tableExport.d.ts +22 -0
- package/frontend/ui/tableExport.js +254 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ sub-paths — this keeps `express` out of the frontend and `react` out of the ba
|
|
|
14
14
|
|---|---|---|
|
|
15
15
|
| `@efa-one/sdk/backend` | Node/Express | Auth + token exchange (`requireAuth`, `createExchangeRouter`, `requireInternalOrAuth` …), health router, service discovery + gateway client (`serviceClient`, `resolveService`), clients for audit/reporting/mail/notifications, permission resolution/registration (`getUserPermissions`, `registerPermissions`, `checkPermission`), capability registry (`registerApiMetadata`), backend i18n |
|
|
16
16
|
| `@efa-one/sdk/frontend` | Browser/React | postMessage IPC (`registerAppInfo`, `sendAtStart`, `navigateToApp`, `notifyRouteChange` …), react-i18next factory (`initI18n`), `DevHeader` |
|
|
17
|
-
| `@efa-one/sdk/frontend/ui` | Browser/React | The efa-one design-system kit: `Button`, `Input`, `Badge`, `Alert`, `Dialog`, `DropdownMenu`, `Tooltip`, `EmptyState`, `Skeleton`, `RecordDialog`, and the **`DataTable`** (sortable/filterable/groupable list with draggable, resizable columns
|
|
17
|
+
| `@efa-one/sdk/frontend/ui` | Browser/React | The efa-one design-system kit: `Button`, `Input`, `Badge`, `Alert`, `Dialog`, `DropdownMenu`, `Tooltip`, `EmptyState`, `Skeleton`, `RecordDialog`, and the **`DataTable`** (sortable/filterable/groupable list with draggable, resizable columns, a per-user persisted view and CSV/Excel export of the current view; reflows to stacked cards below 640px). Also exports `useIsMobile` — the platform-wide 640px breakpoint hook. Ship its companion CSS once: `import '@efa-one/sdk/frontend/ui/styles.css'` |
|
|
18
18
|
| `@efa-one/sdk/frontend/format` | Browser | Platform-wide date/time display — `formatDate` (`01.09.2026`), `formatDateTime` (`01.09.2026, 11:05`), `formatTime`, `formatFileStamp`, `localeForLanguage`. Always use these instead of `toLocaleDateString`/`toLocaleString`: without an options object those render `1.9.2026`, and without a locale argument they follow the browser language (`9/1/2026`) |
|
|
19
19
|
| `@efa-one/sdk/frontend/viewPreferences` | Browser/React | Persistence seam for `DataTable` — `createViewPreferencesClient()` (standard `/api/view-preferences/:listId` adapter) + `useViewPreferences()` |
|
|
20
20
|
|
|
@@ -223,6 +223,22 @@ before, it was missing until the user reset the view. New prop `groupsDefaultExp
|
|
|
223
223
|
groups start expanded; groups the user collapses stay collapsed. No backend or kernel
|
|
224
224
|
change.
|
|
225
225
|
|
|
226
|
+
**1.22.0 — export the current view.** The DataTable's gear menu now has „Excel" and
|
|
227
|
+
„CSV" buttons. Both export exactly what the user sees: filtered and sorted rows (in
|
|
228
|
+
group order when grouped, collapsed groups included), visible columns in their order.
|
|
229
|
+
CSV uses `;` as separator, `,` as decimal mark and a UTF-8 BOM, so German Excel opens
|
|
230
|
+
it by double-click; text that starts like a formula is prefixed with `'`. The .xlsx is
|
|
231
|
+
written without a dependency (stored ZIP): bold, light-grey header row with thin
|
|
232
|
+
borders, frozen, with autofilter; numbers and dates are real Excel values
|
|
233
|
+
(`dd.mm.yyyy`). Values come from `accessor`, not from `cell`; multi-select filter
|
|
234
|
+
codes are exported as their labels. New `ColumnDef` fields `exportValue` (own export
|
|
235
|
+
value) and `exportable: false` (leave out, e.g. action columns), new props
|
|
236
|
+
`exportFileName` (default: `listId`, plus a timestamp) and `exportable={false}` (hide
|
|
237
|
+
the export, e.g. without an export permission). `toCsv`/`toXlsx`/`downloadFile` are
|
|
238
|
+
exported for app-side use. CSV is CSV: Excel turns `01067` into `1067` and long digit
|
|
239
|
+
strings into `1,23E+18`, and shows the `'` in front of `+49 …` — point users to the
|
|
240
|
+
Excel export for such data. No backend or kernel change.
|
|
241
|
+
|
|
226
242
|
## License
|
|
227
243
|
|
|
228
244
|
[Apache-2.0](./LICENSE) — permissive with a patent grant, so customers and partners
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { type ViewPreferencesAdapter } from '../viewPreferences.js';
|
|
3
|
+
import { type ExportCell, type ExportTable } from './tableExport.js';
|
|
3
4
|
export type FilterDef = {
|
|
4
5
|
type: 'text';
|
|
5
6
|
} | {
|
|
@@ -23,6 +24,8 @@ export interface ColumnDef<T> {
|
|
|
23
24
|
sortable?: boolean;
|
|
24
25
|
width?: string;
|
|
25
26
|
defaultVisible?: boolean;
|
|
27
|
+
exportValue?: (row: T) => string | number | boolean | Date | null | undefined;
|
|
28
|
+
exportable?: boolean;
|
|
26
29
|
}
|
|
27
30
|
interface FilterValue {
|
|
28
31
|
text?: string;
|
|
@@ -58,11 +61,16 @@ interface DataTableProps<T, K extends string | number> {
|
|
|
58
61
|
footer?: React.ReactNode;
|
|
59
62
|
groupsDefaultExpanded?: boolean;
|
|
60
63
|
persistence?: ViewPreferencesAdapter;
|
|
64
|
+
exportable?: boolean;
|
|
65
|
+
exportFileName?: string;
|
|
61
66
|
}
|
|
62
67
|
export declare function defaultPrefsFor<T>(columns: ColumnDef<T>[], overrides?: Partial<ViewPrefs>): ViewPrefs;
|
|
63
68
|
export declare function reconcileOrder<T>(columns: ColumnDef<T>[], order: string[]): string[];
|
|
64
69
|
export declare function isColumnVisible<T>(column: ColumnDef<T>, visibility: Record<string, boolean>): boolean;
|
|
65
70
|
export declare function formatCellValue<T>(col: ColumnDef<T>, row: T): string;
|
|
71
|
+
export declare function exportCellOf<T>(col: ColumnDef<T>, row: T): ExportCell;
|
|
72
|
+
export declare function buildExportTable<T>(rows: T[], visibleColumns: ColumnDef<T>[]): ExportTable;
|
|
73
|
+
export declare function exportFileBase(name: string, now?: Date): string;
|
|
66
74
|
export declare const MIN_COLUMN_WIDTH = 60;
|
|
67
75
|
export declare const MAX_COLUMN_WIDTH = 1600;
|
|
68
76
|
export declare function clampColumnWidth(px: number): number;
|
|
@@ -78,7 +86,7 @@ interface RenderItem<T> {
|
|
|
78
86
|
expanded?: boolean;
|
|
79
87
|
row?: T;
|
|
80
88
|
}
|
|
81
|
-
export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string>): RenderItem<T>[];
|
|
89
|
+
export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string> | 'all'): RenderItem<T>[];
|
|
82
90
|
export declare function collectAllGroupKeys<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>): string[];
|
|
83
|
-
export declare function DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, }: DataTableProps<T, K>): React.ReactElement;
|
|
91
|
+
export declare function DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, exportable, exportFileName, }: DataTableProps<T, K>): React.ReactElement;
|
|
84
92
|
export {};
|
package/frontend/ui/DataTable.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import * as DropdownMenu from './DropdownMenu.js';
|
|
4
|
-
import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X, GripVertical } from 'lucide-react';
|
|
4
|
+
import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X, GripVertical, Download } from 'lucide-react';
|
|
5
5
|
import { Button } from './Button.js';
|
|
6
6
|
import { useViewPreferences } from '../viewPreferences.js';
|
|
7
7
|
import { useIsMobile } from '../useIsMobile.js';
|
|
8
|
-
import { formatDate, formatDateTime } from '../format.js';
|
|
8
|
+
import { formatDate, formatDateTime, formatFileStamp } from '../format.js';
|
|
9
|
+
import { downloadFile, toCsv, toXlsx, trimChar, XLSX_MIME } from './tableExport.js';
|
|
9
10
|
const GROUP_KEY_SEP = '\x1f';
|
|
10
11
|
const EMPTY_GROUP_LABEL = '– ohne –';
|
|
11
12
|
export const DEFAULT_VIEW_VERSION = 2;
|
|
@@ -54,6 +55,47 @@ export function formatCellValue(col, row) {
|
|
|
54
55
|
}
|
|
55
56
|
return String(v ?? '');
|
|
56
57
|
}
|
|
58
|
+
export function exportCellOf(col, row) {
|
|
59
|
+
const v = col.exportValue ? col.exportValue(row) : col.accessor(row);
|
|
60
|
+
if (v == null || v === '')
|
|
61
|
+
return null;
|
|
62
|
+
if (isDateColumn(col, v)) {
|
|
63
|
+
const t = timeOf(v);
|
|
64
|
+
if (t != null)
|
|
65
|
+
return { date: new Date(t), withTime: col.type === 'datetime' };
|
|
66
|
+
return col.exportValue ? String(v) : null;
|
|
67
|
+
}
|
|
68
|
+
const f = col.exportValue ? undefined : col.filter;
|
|
69
|
+
if (typeof v === 'boolean') {
|
|
70
|
+
if (f?.type === 'boolean')
|
|
71
|
+
return v ? (f.labelTrue ?? 'Ja') : (f.labelFalse ?? 'Nein');
|
|
72
|
+
return v ? 'Ja' : 'Nein';
|
|
73
|
+
}
|
|
74
|
+
if (typeof v === 'number') {
|
|
75
|
+
if (!Number.isFinite(v))
|
|
76
|
+
return null;
|
|
77
|
+
const opt = f?.type === 'multi-select' ? f.options.find((o) => o.value === String(v)) : undefined;
|
|
78
|
+
return opt ? opt.label : v;
|
|
79
|
+
}
|
|
80
|
+
const labelOf = (x) => {
|
|
81
|
+
const s = String(x);
|
|
82
|
+
return f?.type === 'multi-select' ? (f.options.find((o) => o.value === s)?.label ?? s) : s;
|
|
83
|
+
};
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
return v.map(labelOf).join(', ');
|
|
86
|
+
return labelOf(v);
|
|
87
|
+
}
|
|
88
|
+
export function buildExportTable(rows, visibleColumns) {
|
|
89
|
+
const cols = visibleColumns.filter((c) => c.exportable !== false);
|
|
90
|
+
return {
|
|
91
|
+
headers: cols.map((c) => c.label),
|
|
92
|
+
rows: rows.map((r) => cols.map((c) => exportCellOf(c, r))),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function exportFileBase(name, now = new Date()) {
|
|
96
|
+
const base = trimChar(name.trim().replace(/[\\/:*?"<>|\s]+/g, '_'), '_') || 'export';
|
|
97
|
+
return `${base}_${formatFileStamp(now)}`;
|
|
98
|
+
}
|
|
57
99
|
export const MIN_COLUMN_WIDTH = 60;
|
|
58
100
|
export const MAX_COLUMN_WIDTH = 1600;
|
|
59
101
|
const KEYBOARD_RESIZE_STEP = 16;
|
|
@@ -138,7 +180,7 @@ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
|
|
|
138
180
|
for (const label of labels) {
|
|
139
181
|
const childRows = buckets.get(label);
|
|
140
182
|
const key = parentKey ? `${parentKey}${GROUP_KEY_SEP}${label}` : label;
|
|
141
|
-
const expanded = expandedGroups.has(key);
|
|
183
|
+
const expanded = expandedGroups === 'all' || expandedGroups.has(key);
|
|
142
184
|
out.push({
|
|
143
185
|
kind: 'group',
|
|
144
186
|
key,
|
|
@@ -178,7 +220,7 @@ export function collectAllGroupKeys(rows, groupBy, columnsById) {
|
|
|
178
220
|
recurse(rows, 0, '');
|
|
179
221
|
return keys;
|
|
180
222
|
}
|
|
181
|
-
export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, }) {
|
|
223
|
+
export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, exportable = true, exportFileName, }) {
|
|
182
224
|
const defaultPrefs = useMemo(() => defaultPrefsFor(columns, initialPrefs), [columns, initialPrefs]);
|
|
183
225
|
const [prefs, setPrefs, reset] = useViewPreferences(listId, defaultPrefs, persistence);
|
|
184
226
|
const isMobile = useIsMobile();
|
|
@@ -375,7 +417,22 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
375
417
|
}
|
|
376
418
|
setPrefs({ ...prefs, filters });
|
|
377
419
|
};
|
|
378
|
-
const
|
|
420
|
+
const runExport = (format) => {
|
|
421
|
+
const exportRows = buildRenderItems(sortedFilteredRows, groupBy, columnsById, 'all')
|
|
422
|
+
.filter((i) => i.kind === 'row')
|
|
423
|
+
.map((i) => i.row);
|
|
424
|
+
const table = buildExportTable(exportRows, visibleColumns);
|
|
425
|
+
const base = exportFileBase(exportFileName ?? listId);
|
|
426
|
+
if (format === 'csv')
|
|
427
|
+
downloadFile(toCsv(table), `${base}.csv`, 'text/csv;charset=utf-8');
|
|
428
|
+
else
|
|
429
|
+
downloadFile(toXlsx(table, exportFileName ?? 'Export'), `${base}.xlsx`, XLSX_MIME);
|
|
430
|
+
};
|
|
431
|
+
const gearButton = (_jsx(ColumnInventoryButton, { exportInfo: exportable ? {
|
|
432
|
+
rowCount: sortedFilteredRows.length,
|
|
433
|
+
hasColumns: visibleColumns.some((c) => c.exportable !== false),
|
|
434
|
+
run: runExport,
|
|
435
|
+
} : undefined, columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy }));
|
|
379
436
|
return (_jsxs("div", { ref: containerRef, className: "border border-[var(--color-border)] overflow-x-auto", style: { '--dt-cols': gridTemplate }, children: [selection && selection.selected.size > 0 && (_jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-sm border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [_jsxs("span", { children: [selection.selected.size, " ausgew\u00E4hlt"] }), _jsx("button", { type: "button", className: "text-xs underline text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)]", onClick: () => selection.onChange(new Set()), children: "Auswahl l\u00F6schen" }), _jsx("div", { className: "flex-1" }), selection.bulkActions] })), isMobile ? (_jsxs("div", { className: "flex items-center gap-3 px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { background: 'var(--color-surface-raised)' }, children: [selection && (_jsxs("label", { className: "flex items-center gap-2 py-2 pr-2 cursor-pointer", children: [_jsx("input", { type: "checkbox", className: "w-5 h-5", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" }), _jsx("span", { className: "text-[var(--color-text-muted)]", children: "Alle" })] })), _jsx("div", { className: "flex-1" }), gearButton] })) : (_jsxs("div", { className: "grid items-center min-w-min px-3 py-2 text-xs font-medium border-b border-[var(--color-border)]", style: { gridTemplateColumns: 'var(--dt-cols)', background: 'var(--color-surface-raised)' }, children: [selection && (_jsx("input", { type: "checkbox", checked: allSelected, onChange: toggleAll, "aria-label": "Alle ausw\u00E4hlen" })), visibleColumns.map((col) => {
|
|
380
437
|
const activeSort = prefs.sort?.columnId === col.id ? prefs.sort.direction : null;
|
|
381
438
|
const filterValue = prefs.filters[col.id];
|
|
@@ -569,7 +626,7 @@ function FilterEditor({ column, value, onChange, }) {
|
|
|
569
626
|
}
|
|
570
627
|
return null;
|
|
571
628
|
}
|
|
572
|
-
function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, reset, groupBy, addGroupBy, removeGroupBy, moveGroupBy, clearGroupBy, }) {
|
|
629
|
+
function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, reset, groupBy, addGroupBy, removeGroupBy, moveGroupBy, clearGroupBy, exportInfo, }) {
|
|
573
630
|
const groupableCandidates = columns.filter((c) => !groupBy.includes(c.id));
|
|
574
631
|
const [open, setOpen] = useState(false);
|
|
575
632
|
const orderedColumns = useMemo(() => {
|
|
@@ -587,5 +644,5 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
|
|
|
587
644
|
}, className: "flex-1 px-1 py-0.5 text-xs border border-[var(--color-border)] bg-[var(--color-surface)]", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("option", { value: "", children: "\u2013 w\u00E4hlen \u2013" }), groupableCandidates.map((c) => (_jsx("option", { value: c.id, children: c.label }, c.id)))] })] })), _jsx("div", { className: "border-t border-[var(--color-border)] mb-2" }), _jsx("div", { className: "text-xs font-semibold mb-2 text-[var(--color-text-muted)] uppercase", children: "Spalten" }), orderedColumns.map((col, idx) => {
|
|
588
645
|
const visible = isColumnVisible(col, prefs.columnVisibility);
|
|
589
646
|
return (_jsxs("div", { className: "flex items-center gap-2 px-1 py-1 text-sm", onClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", checked: visible, onChange: () => setColumnVisible(col.id, !visible), "aria-label": `Spalte ${col.label} ${visible ? 'ausblenden' : 'einblenden'}` }), _jsx("span", { className: "flex-1 text-xs", children: col.label }), _jsx("button", { type: "button", disabled: idx === 0, onClick: () => moveColumn(col.id, -1), className: "p-2 sm:p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach oben", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowUp, { className: "w-3 h-3" }) }), _jsx("button", { type: "button", disabled: idx === orderedColumns.length - 1, onClick: () => moveColumn(col.id, 1), className: "p-2 sm:p-0.5 disabled:opacity-30 hover:bg-[var(--color-surface-raised)]", title: "Nach unten", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(ArrowDown, { className: "w-3 h-3" }) })] }, col.id));
|
|
590
|
-
}), _jsx("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: _jsxs(Button, { variant: "ghost", onClick: () => { reset(); setOpen(false); }, children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Zur\u00FCcksetzen"] }) })] })] }));
|
|
647
|
+
}), exportInfo && (_jsxs("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: [_jsx("div", { className: "text-xs font-semibold mb-1 text-[var(--color-text-muted)] uppercase", children: "Exportieren" }), _jsxs("div", { className: "text-xs text-[var(--color-text-muted)] px-1 mb-1", children: ["Aktuelle Ansicht \u00B7 ", exportInfo.rowCount, " ", exportInfo.rowCount === 1 ? 'Zeile' : 'Zeilen'] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs(Button, { variant: "ghost", disabled: exportInfo.rowCount === 0 || !exportInfo.hasColumns, onClick: () => { exportInfo.run('xlsx'); setOpen(false); }, children: [_jsx(Download, { className: "w-4 h-4" }), "Excel"] }), _jsxs(Button, { variant: "ghost", disabled: exportInfo.rowCount === 0 || !exportInfo.hasColumns, title: "CSV, Trennzeichen Semikolon", onClick: () => { exportInfo.run('csv'); setOpen(false); }, children: [_jsx(Download, { className: "w-4 h-4" }), "CSV"] })] })] })), _jsx("div", { className: "border-t border-[var(--color-border)] mt-2 pt-2", children: _jsxs(Button, { variant: "ghost", onClick: () => { reset(); setOpen(false); }, children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Zur\u00FCcksetzen"] }) })] })] }));
|
|
591
648
|
}
|
package/frontend/ui/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export { Skeleton, SkeletonRow } from './Skeleton.js';
|
|
|
10
10
|
export { RecordDialog, type RecordDialogMode } from './RecordDialog.js';
|
|
11
11
|
export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
|
|
12
12
|
export { DataTable, DEFAULT_VIEW_VERSION, type ColumnDef, type FilterDef, type DataTableSelectionProps, } from './DataTable.js';
|
|
13
|
+
export { toCsv, toXlsx, downloadFile, type ExportCell, type ExportTable } from './tableExport.js';
|
package/frontend/ui/index.js
CHANGED
|
@@ -10,3 +10,4 @@ export { Skeleton, SkeletonRow } from './Skeleton.js';
|
|
|
10
10
|
export { RecordDialog } from './RecordDialog.js';
|
|
11
11
|
export { useIsMobile, MOBILE_QUERY } from '../useIsMobile.js';
|
|
12
12
|
export { DataTable, DEFAULT_VIEW_VERSION, } from './DataTable.js';
|
|
13
|
+
export { toCsv, toXlsx, downloadFile } from './tableExport.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type ExportCell = null | string | number | {
|
|
2
|
+
date: Date;
|
|
3
|
+
withTime: boolean;
|
|
4
|
+
};
|
|
5
|
+
export interface ExportTable {
|
|
6
|
+
headers: string[];
|
|
7
|
+
rows: ExportCell[][];
|
|
8
|
+
}
|
|
9
|
+
export declare const CSV_SEPARATOR = ";";
|
|
10
|
+
export declare function toCsv(table: ExportTable): string;
|
|
11
|
+
export declare function columnLetter(index: number): string;
|
|
12
|
+
export declare function excelSerial(date: Date, withTime: boolean): number;
|
|
13
|
+
export declare function trimChar(s: string, ch: string): string;
|
|
14
|
+
export declare function sanitizeSheetName(name: string): string;
|
|
15
|
+
export declare function toXlsx(table: ExportTable, sheetName?: string): Uint8Array<ArrayBuffer>;
|
|
16
|
+
export declare function crc32(bytes: Uint8Array): number;
|
|
17
|
+
export declare function zipStore(files: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
data: string;
|
|
20
|
+
}>): Uint8Array<ArrayBuffer>;
|
|
21
|
+
export declare const XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
22
|
+
export declare function downloadFile(data: BlobPart, fileName: string, mime: string): void;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { formatDate, formatDateTime } from '../format.js';
|
|
2
|
+
export const CSV_SEPARATOR = ';';
|
|
3
|
+
function neutralizeFormula(text) {
|
|
4
|
+
return /^[=+\-@\t\r]/.test(text) ? `'${text}` : text;
|
|
5
|
+
}
|
|
6
|
+
function csvField(cell) {
|
|
7
|
+
let text;
|
|
8
|
+
if (cell == null)
|
|
9
|
+
text = '';
|
|
10
|
+
else if (typeof cell === 'number')
|
|
11
|
+
text = String(cell).replace('.', ',');
|
|
12
|
+
else if (typeof cell === 'string')
|
|
13
|
+
text = neutralizeFormula(cell);
|
|
14
|
+
else
|
|
15
|
+
text = cell.withTime ? formatDateTime(cell.date) : formatDate(cell.date);
|
|
16
|
+
return /[";\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
17
|
+
}
|
|
18
|
+
export function toCsv(table) {
|
|
19
|
+
const lines = [table.headers.map(csvField), ...table.rows.map((r) => r.map(csvField))];
|
|
20
|
+
return '' + lines.map((l) => l.join(CSV_SEPARATOR)).join('\r\n') + '\r\n';
|
|
21
|
+
}
|
|
22
|
+
const XLSX_MAX_CELL_CHARS = 32767;
|
|
23
|
+
function xmlEscape(text) {
|
|
24
|
+
return text
|
|
25
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
|
26
|
+
.replace(/&/g, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"');
|
|
30
|
+
}
|
|
31
|
+
export function columnLetter(index) {
|
|
32
|
+
let n = index + 1;
|
|
33
|
+
let s = '';
|
|
34
|
+
while (n > 0) {
|
|
35
|
+
const r = (n - 1) % 26;
|
|
36
|
+
s = String.fromCharCode(65 + r) + s;
|
|
37
|
+
n = Math.floor((n - 1) / 26);
|
|
38
|
+
}
|
|
39
|
+
return s;
|
|
40
|
+
}
|
|
41
|
+
export function excelSerial(date, withTime) {
|
|
42
|
+
const localMs = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), withTime ? date.getHours() : 0, withTime ? date.getMinutes() : 0, withTime ? date.getSeconds() : 0);
|
|
43
|
+
return localMs / 86400000 + 25569;
|
|
44
|
+
}
|
|
45
|
+
const STYLE_HEADER = 1;
|
|
46
|
+
const STYLE_DATE = 2;
|
|
47
|
+
const STYLE_DATETIME = 3;
|
|
48
|
+
const STYLES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
49
|
+
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
50
|
+
<numFmts count="2"><numFmt numFmtId="164" formatCode="dd.mm.yyyy"/><numFmt numFmtId="165" formatCode="dd.mm.yyyy hh:mm"/></numFmts>
|
|
51
|
+
<fonts count="2"><font><sz val="11"/><name val="Calibri"/><family val="2"/></font><font><b/><sz val="11"/><name val="Calibri"/><family val="2"/></font></fonts>
|
|
52
|
+
<fills count="3"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill><fill><patternFill patternType="solid"><fgColor rgb="FFE7E7E7"/><bgColor indexed="64"/></patternFill></fill></fills>
|
|
53
|
+
<borders count="2"><border><left/><right/><top/><bottom/><diagonal/></border><border><left style="thin"><color rgb="FFBFBFBF"/></left><right style="thin"><color rgb="FFBFBFBF"/></right><top style="thin"><color rgb="FFBFBFBF"/></top><bottom style="medium"><color rgb="FF808080"/></bottom><diagonal/></border></borders>
|
|
54
|
+
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
|
55
|
+
<cellXfs count="4">
|
|
56
|
+
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
|
|
57
|
+
<xf numFmtId="0" fontId="1" fillId="2" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="center"/></xf>
|
|
58
|
+
<xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
|
|
59
|
+
<xf numFmtId="165" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>
|
|
60
|
+
</cellXfs>
|
|
61
|
+
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
|
|
62
|
+
</styleSheet>`;
|
|
63
|
+
const CONTENT_TYPES_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
64
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
65
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
66
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
67
|
+
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
68
|
+
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
|
69
|
+
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
|
70
|
+
</Types>`;
|
|
71
|
+
const ROOT_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
72
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
73
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
74
|
+
</Relationships>`;
|
|
75
|
+
const WORKBOOK_RELS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
76
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
77
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
|
78
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
79
|
+
</Relationships>`;
|
|
80
|
+
export function trimChar(s, ch) {
|
|
81
|
+
let start = 0;
|
|
82
|
+
let end = s.length;
|
|
83
|
+
while (start < end && s[start] === ch)
|
|
84
|
+
start++;
|
|
85
|
+
while (end > start && s[end - 1] === ch)
|
|
86
|
+
end--;
|
|
87
|
+
return s.slice(start, end);
|
|
88
|
+
}
|
|
89
|
+
export function sanitizeSheetName(name) {
|
|
90
|
+
const cleaned = trimChar(name.replace(/[[\]:*?/\\]/g, ' '), "'").trim().slice(0, 31).trim();
|
|
91
|
+
return cleaned || 'Export';
|
|
92
|
+
}
|
|
93
|
+
function textCell(ref, text, style) {
|
|
94
|
+
const s = style != null ? ` s="${style}"` : '';
|
|
95
|
+
return `<c r="${ref}" t="inlineStr"${s}><is><t xml:space="preserve">${xmlEscape(text.slice(0, XLSX_MAX_CELL_CHARS))}</t></is></c>`;
|
|
96
|
+
}
|
|
97
|
+
function dataCell(ref, cell) {
|
|
98
|
+
if (cell == null)
|
|
99
|
+
return '';
|
|
100
|
+
if (typeof cell === 'number')
|
|
101
|
+
return `<c r="${ref}"><v>${cell}</v></c>`;
|
|
102
|
+
if (typeof cell === 'string')
|
|
103
|
+
return cell === '' ? '' : textCell(ref, cell);
|
|
104
|
+
const serial = excelSerial(cell.date, cell.withTime);
|
|
105
|
+
if (serial < 61)
|
|
106
|
+
return textCell(ref, cell.withTime ? formatDateTime(cell.date) : formatDate(cell.date));
|
|
107
|
+
return `<c r="${ref}" s="${cell.withTime ? STYLE_DATETIME : STYLE_DATE}"><v>${serial}</v></c>`;
|
|
108
|
+
}
|
|
109
|
+
function displayLength(cell) {
|
|
110
|
+
if (cell == null)
|
|
111
|
+
return 0;
|
|
112
|
+
if (typeof cell === 'number')
|
|
113
|
+
return String(cell).length;
|
|
114
|
+
if (typeof cell === 'string')
|
|
115
|
+
return Math.max(...cell.split('\n').map((l) => l.length));
|
|
116
|
+
return cell.withTime ? 16 : 10;
|
|
117
|
+
}
|
|
118
|
+
function sheetXml(table) {
|
|
119
|
+
const colCount = Math.max(table.headers.length, 1);
|
|
120
|
+
const lastCol = columnLetter(colCount - 1);
|
|
121
|
+
const lastRow = table.rows.length + 1;
|
|
122
|
+
const widths = table.headers.map((h, ci) => {
|
|
123
|
+
let max = h.length + 3;
|
|
124
|
+
for (const r of table.rows)
|
|
125
|
+
max = Math.max(max, displayLength(r[ci] ?? null));
|
|
126
|
+
return Math.min(60, Math.max(8, max + 2));
|
|
127
|
+
});
|
|
128
|
+
const header = `<row r="1">${table.headers.map((h, ci) => textCell(`${columnLetter(ci)}1`, h, STYLE_HEADER)).join('')}</row>`;
|
|
129
|
+
const body = table.rows.map((r, ri) => {
|
|
130
|
+
const rowNo = ri + 2;
|
|
131
|
+
return `<row r="${rowNo}">${r.map((c, ci) => dataCell(`${columnLetter(ci)}${rowNo}`, c)).join('')}</row>`;
|
|
132
|
+
}).join('');
|
|
133
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
134
|
+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
135
|
+
<dimension ref="A1:${lastCol}${lastRow}"/>
|
|
136
|
+
<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>
|
|
137
|
+
<sheetFormatPr defaultRowHeight="15"/>
|
|
138
|
+
${widths.length > 0 ? `<cols>${widths.map((w, i) => `<col min="${i + 1}" max="${i + 1}" width="${w}" customWidth="1"/>`).join('')}</cols>` : ''}
|
|
139
|
+
<sheetData>${header}${body}</sheetData>
|
|
140
|
+
${table.headers.length > 0 ? `<autoFilter ref="A1:${lastCol}${lastRow}"/>` : ''}
|
|
141
|
+
</worksheet>`;
|
|
142
|
+
}
|
|
143
|
+
function workbookXml(sheetName, table) {
|
|
144
|
+
const lastCol = columnLetter(Math.max(table.headers.length, 1) - 1);
|
|
145
|
+
const quoted = `'${sheetName.replace(/'/g, "''")}'`;
|
|
146
|
+
const filterName = table.headers.length > 0
|
|
147
|
+
? `<definedNames><definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">${xmlEscape(quoted)}!$A$1:$${lastCol}$${table.rows.length + 1}</definedName></definedNames>`
|
|
148
|
+
: '';
|
|
149
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
150
|
+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
151
|
+
<sheets><sheet name="${xmlEscape(sheetName)}" sheetId="1" r:id="rId1"/></sheets>
|
|
152
|
+
${filterName}
|
|
153
|
+
</workbook>`;
|
|
154
|
+
}
|
|
155
|
+
export function toXlsx(table, sheetName = 'Export') {
|
|
156
|
+
const name = sanitizeSheetName(sheetName);
|
|
157
|
+
return zipStore([
|
|
158
|
+
{ name: '[Content_Types].xml', data: CONTENT_TYPES_XML },
|
|
159
|
+
{ name: '_rels/.rels', data: ROOT_RELS_XML },
|
|
160
|
+
{ name: 'xl/workbook.xml', data: workbookXml(name, table) },
|
|
161
|
+
{ name: 'xl/_rels/workbook.xml.rels', data: WORKBOOK_RELS_XML },
|
|
162
|
+
{ name: 'xl/styles.xml', data: STYLES_XML },
|
|
163
|
+
{ name: 'xl/worksheets/sheet1.xml', data: sheetXml(table) },
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
let crcTable = null;
|
|
167
|
+
export function crc32(bytes) {
|
|
168
|
+
if (!crcTable) {
|
|
169
|
+
crcTable = new Uint32Array(256);
|
|
170
|
+
for (let n = 0; n < 256; n++) {
|
|
171
|
+
let c = n;
|
|
172
|
+
for (let k = 0; k < 8; k++)
|
|
173
|
+
c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1;
|
|
174
|
+
crcTable[n] = c >>> 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
let crc = 0xFFFFFFFF;
|
|
178
|
+
for (let i = 0; i < bytes.length; i++)
|
|
179
|
+
crc = crcTable[(crc ^ bytes[i]) & 0xFF] ^ (crc >>> 8);
|
|
180
|
+
return (crc ^ 0xFFFFFFFF) >>> 0;
|
|
181
|
+
}
|
|
182
|
+
export function zipStore(files) {
|
|
183
|
+
const enc = new TextEncoder();
|
|
184
|
+
const dosTime = 0;
|
|
185
|
+
const dosDate = ((2026 - 1980) << 9) | (1 << 5) | 1;
|
|
186
|
+
const locals = [];
|
|
187
|
+
const centrals = [];
|
|
188
|
+
let offset = 0;
|
|
189
|
+
for (const f of files) {
|
|
190
|
+
const name = enc.encode(f.name);
|
|
191
|
+
const data = enc.encode(f.data);
|
|
192
|
+
const crc = crc32(data);
|
|
193
|
+
const local = new Uint8Array(30 + name.length + data.length);
|
|
194
|
+
const lv = new DataView(local.buffer);
|
|
195
|
+
lv.setUint32(0, 0x04034B50, true);
|
|
196
|
+
lv.setUint16(4, 20, true);
|
|
197
|
+
lv.setUint16(6, 0x0800, true);
|
|
198
|
+
lv.setUint16(8, 0, true);
|
|
199
|
+
lv.setUint16(10, dosTime, true);
|
|
200
|
+
lv.setUint16(12, dosDate, true);
|
|
201
|
+
lv.setUint32(14, crc, true);
|
|
202
|
+
lv.setUint32(18, data.length, true);
|
|
203
|
+
lv.setUint32(22, data.length, true);
|
|
204
|
+
lv.setUint16(26, name.length, true);
|
|
205
|
+
lv.setUint16(28, 0, true);
|
|
206
|
+
local.set(name, 30);
|
|
207
|
+
local.set(data, 30 + name.length);
|
|
208
|
+
locals.push(local);
|
|
209
|
+
const central = new Uint8Array(46 + name.length);
|
|
210
|
+
const cv = new DataView(central.buffer);
|
|
211
|
+
cv.setUint32(0, 0x02014B50, true);
|
|
212
|
+
cv.setUint16(4, 20, true);
|
|
213
|
+
cv.setUint16(6, 20, true);
|
|
214
|
+
cv.setUint16(8, 0x0800, true);
|
|
215
|
+
cv.setUint16(10, 0, true);
|
|
216
|
+
cv.setUint16(12, dosTime, true);
|
|
217
|
+
cv.setUint16(14, dosDate, true);
|
|
218
|
+
cv.setUint32(16, crc, true);
|
|
219
|
+
cv.setUint32(20, data.length, true);
|
|
220
|
+
cv.setUint32(24, data.length, true);
|
|
221
|
+
cv.setUint16(28, name.length, true);
|
|
222
|
+
cv.setUint32(42, offset, true);
|
|
223
|
+
central.set(name, 46);
|
|
224
|
+
centrals.push(central);
|
|
225
|
+
offset += local.length;
|
|
226
|
+
}
|
|
227
|
+
const centralSize = centrals.reduce((s, c) => s + c.length, 0);
|
|
228
|
+
const end = new Uint8Array(22);
|
|
229
|
+
const ev = new DataView(end.buffer);
|
|
230
|
+
ev.setUint32(0, 0x06054B50, true);
|
|
231
|
+
ev.setUint16(8, files.length, true);
|
|
232
|
+
ev.setUint16(10, files.length, true);
|
|
233
|
+
ev.setUint32(12, centralSize, true);
|
|
234
|
+
ev.setUint32(16, offset, true);
|
|
235
|
+
const out = new Uint8Array(offset + centralSize + end.length);
|
|
236
|
+
let pos = 0;
|
|
237
|
+
for (const part of [...locals, ...centrals, end]) {
|
|
238
|
+
out.set(part, pos);
|
|
239
|
+
pos += part.length;
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
244
|
+
export function downloadFile(data, fileName, mime) {
|
|
245
|
+
const url = URL.createObjectURL(new Blob([data], { type: mime }));
|
|
246
|
+
const a = document.createElement('a');
|
|
247
|
+
a.href = url;
|
|
248
|
+
a.download = fileName;
|
|
249
|
+
a.style.display = 'none';
|
|
250
|
+
document.body.appendChild(a);
|
|
251
|
+
a.click();
|
|
252
|
+
a.remove();
|
|
253
|
+
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
254
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@efa-one/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Offizielles SDK für efa-one-Apps: Auth/Session, Service-Discovery, Gateway-Client, Audit/Reporting/Mail/Notifications, Permission- und Capability-Registrierung sowie das Frontend-IPC-/i18n-Toolkit.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"eslint-plugin-react-hooks": "^7.1.1",
|
|
123
123
|
"express": "^4.19.2",
|
|
124
124
|
"i18next": "^23.11.0",
|
|
125
|
-
"i18next-http-backend": "^
|
|
125
|
+
"i18next-http-backend": "^4.0.2",
|
|
126
126
|
"jsonwebtoken": "^9.0.2",
|
|
127
127
|
"lucide-react": "^0.577.0",
|
|
128
128
|
"pg": "^8.12.0",
|