@efa-one/sdk 1.20.0 → 1.21.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 +12 -0
- package/frontend/ui/DataTable.d.ts +5 -1
- package/frontend/ui/DataTable.js +70 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,11 @@ const columns: ColumnDef<Item>[] = [
|
|
|
105
105
|
<DataTable listId="items.list" rows={items} columns={columns} rowKey={(r) => r.id} persistence={viewPrefs} />
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
Columns added later show up in views users saved before (at their natural position) — no
|
|
109
|
+
need to bump `DEFAULT_VIEW_VERSION` and reset everyone's view. Grouped lists open collapsed;
|
|
110
|
+
pass `groupsDefaultExpanded` (e.g. with `initialPrefs={{ groupBy: ['category'] }}`) to start
|
|
111
|
+
with every group expanded while keeping groups the user collapses closed.
|
|
112
|
+
|
|
108
113
|
## Build your first app
|
|
109
114
|
|
|
110
115
|
This repo ships a ready-to-copy app scaffold under [`template/`](./template) — the fastest
|
|
@@ -211,6 +216,13 @@ chronologically, groups by day and lets the text filter match the displayed form
|
|
|
211
216
|
the app's job: use `formatDate`/`formatDateTime`, never `toLocaleDateString` (that is
|
|
212
217
|
where `7.10.2026` comes from). No backend or kernel change.
|
|
213
218
|
|
|
219
|
+
**1.21.0 — new columns in old views, groups that start open.** A column added to
|
|
220
|
+
`columns` after a user saved their view now appears in that view at its natural position
|
|
221
|
+
(and stays hidden if it has `defaultVisible: false`), and can be dragged like any other —
|
|
222
|
+
before, it was missing until the user reset the view. New prop `groupsDefaultExpanded`:
|
|
223
|
+
groups start expanded; groups the user collapses stay collapsed. No backend or kernel
|
|
224
|
+
change.
|
|
225
|
+
|
|
214
226
|
## License
|
|
215
227
|
|
|
216
228
|
[Apache-2.0](./LICENSE) — permissive with a patent grant, so customers and partners
|
|
@@ -56,9 +56,12 @@ interface DataTableProps<T, K extends string | number> {
|
|
|
56
56
|
selection?: DataTableSelectionProps<K>;
|
|
57
57
|
initialPrefs?: Partial<ViewPrefs>;
|
|
58
58
|
footer?: React.ReactNode;
|
|
59
|
+
groupsDefaultExpanded?: boolean;
|
|
59
60
|
persistence?: ViewPreferencesAdapter;
|
|
60
61
|
}
|
|
61
62
|
export declare function defaultPrefsFor<T>(columns: ColumnDef<T>[], overrides?: Partial<ViewPrefs>): ViewPrefs;
|
|
63
|
+
export declare function reconcileOrder<T>(columns: ColumnDef<T>[], order: string[]): string[];
|
|
64
|
+
export declare function isColumnVisible<T>(column: ColumnDef<T>, visibility: Record<string, boolean>): boolean;
|
|
62
65
|
export declare function formatCellValue<T>(col: ColumnDef<T>, row: T): string;
|
|
63
66
|
export declare const MIN_COLUMN_WIDTH = 60;
|
|
64
67
|
export declare const MAX_COLUMN_WIDTH = 1600;
|
|
@@ -76,5 +79,6 @@ interface RenderItem<T> {
|
|
|
76
79
|
row?: T;
|
|
77
80
|
}
|
|
78
81
|
export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string>): RenderItem<T>[];
|
|
79
|
-
export declare function
|
|
82
|
+
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;
|
|
80
84
|
export {};
|
package/frontend/ui/DataTable.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import * as DropdownMenu from './DropdownMenu.js';
|
|
4
4
|
import { ChevronDown, ChevronUp, ChevronRight, Settings, EyeOff, Filter, RotateCcw, ArrowUp, ArrowDown, Layers, X, GripVertical } from 'lucide-react';
|
|
5
5
|
import { Button } from './Button.js';
|
|
@@ -23,6 +23,21 @@ export function defaultPrefsFor(columns, overrides) {
|
|
|
23
23
|
...(overrides ?? {}),
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
export function reconcileOrder(columns, order) {
|
|
27
|
+
const known = new Set(columns.map((c) => c.id));
|
|
28
|
+
const result = order.filter((id) => known.has(id));
|
|
29
|
+
const present = new Set(result);
|
|
30
|
+
columns.forEach((c, idx) => {
|
|
31
|
+
if (present.has(c.id))
|
|
32
|
+
return;
|
|
33
|
+
result.splice(Math.min(idx, result.length), 0, c.id);
|
|
34
|
+
present.add(c.id);
|
|
35
|
+
});
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
export function isColumnVisible(column, visibility) {
|
|
39
|
+
return visibility[column.id] ?? column.defaultVisible !== false;
|
|
40
|
+
}
|
|
26
41
|
function isDateColumn(col, value) {
|
|
27
42
|
return col.type === 'date' || col.type === 'datetime' || (col.type == null && value instanceof Date);
|
|
28
43
|
}
|
|
@@ -76,6 +91,12 @@ export function groupLabelOf(value) {
|
|
|
76
91
|
const s = String(value).trim();
|
|
77
92
|
return s;
|
|
78
93
|
}
|
|
94
|
+
function groupBucketLabel(col, raw) {
|
|
95
|
+
const lbl = isDateColumn(col, raw)
|
|
96
|
+
? (timeOf(raw) == null ? '' : formatDate(raw))
|
|
97
|
+
: groupLabelOf(raw);
|
|
98
|
+
return lbl || EMPTY_GROUP_LABEL;
|
|
99
|
+
}
|
|
79
100
|
export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
|
|
80
101
|
if (groupBy.length === 0)
|
|
81
102
|
return rows.map((row) => ({ kind: 'row', row }));
|
|
@@ -98,8 +119,7 @@ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
|
|
|
98
119
|
const isDate = isDateColumn(col, raw);
|
|
99
120
|
if (isDate)
|
|
100
121
|
chronological = true;
|
|
101
|
-
const
|
|
102
|
-
const bucketLabel = lbl || EMPTY_GROUP_LABEL;
|
|
122
|
+
const bucketLabel = groupBucketLabel(col, raw);
|
|
103
123
|
const arr = buckets.get(bucketLabel) ?? [];
|
|
104
124
|
arr.push(r);
|
|
105
125
|
buckets.set(bucketLabel, arr);
|
|
@@ -134,7 +154,31 @@ export function buildRenderItems(rows, groupBy, columnsById, expandedGroups) {
|
|
|
134
154
|
recurse(rows, 0, '');
|
|
135
155
|
return out;
|
|
136
156
|
}
|
|
137
|
-
export function
|
|
157
|
+
export function collectAllGroupKeys(rows, groupBy, columnsById) {
|
|
158
|
+
const keys = [];
|
|
159
|
+
const recurse = (subset, depth, parentKey) => {
|
|
160
|
+
if (depth >= groupBy.length)
|
|
161
|
+
return;
|
|
162
|
+
const col = columnsById.get(groupBy[depth]);
|
|
163
|
+
if (!col)
|
|
164
|
+
return;
|
|
165
|
+
const buckets = new Map();
|
|
166
|
+
for (const r of subset) {
|
|
167
|
+
const lbl = groupBucketLabel(col, col.accessor(r));
|
|
168
|
+
const arr = buckets.get(lbl) ?? [];
|
|
169
|
+
arr.push(r);
|
|
170
|
+
buckets.set(lbl, arr);
|
|
171
|
+
}
|
|
172
|
+
for (const [label, childRows] of buckets) {
|
|
173
|
+
const key = parentKey ? `${parentKey}${GROUP_KEY_SEP}${label}` : label;
|
|
174
|
+
keys.push(key);
|
|
175
|
+
recurse(childRows, depth + 1, key);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
recurse(rows, 0, '');
|
|
179
|
+
return keys;
|
|
180
|
+
}
|
|
181
|
+
export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, groupsDefaultExpanded, persistence, }) {
|
|
138
182
|
const defaultPrefs = useMemo(() => defaultPrefsFor(columns, initialPrefs), [columns, initialPrefs]);
|
|
139
183
|
const [prefs, setPrefs, reset] = useViewPreferences(listId, defaultPrefs, persistence);
|
|
140
184
|
const isMobile = useIsMobile();
|
|
@@ -155,12 +199,27 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
155
199
|
const [dragColumnId, setDragColumnId] = useState(null);
|
|
156
200
|
const [dropTarget, setDropTarget] = useState(null);
|
|
157
201
|
const containerRef = useRef(null);
|
|
202
|
+
const effectiveOrder = useMemo(() => reconcileOrder(columns, prefs.columnOrder), [columns, prefs.columnOrder]);
|
|
203
|
+
const seenGroupKeysRef = useRef(new Set());
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (!groupsDefaultExpanded || groupBy.length === 0)
|
|
206
|
+
return;
|
|
207
|
+
const fresh = collectAllGroupKeys(rows, groupBy, columnsById).filter((k) => !seenGroupKeysRef.current.has(k));
|
|
208
|
+
if (fresh.length === 0)
|
|
209
|
+
return;
|
|
210
|
+
fresh.forEach((k) => seenGroupKeysRef.current.add(k));
|
|
211
|
+
setExpandedGroups((prev) => {
|
|
212
|
+
const next = new Set(prev);
|
|
213
|
+
fresh.forEach((k) => next.add(k));
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
}, [groupsDefaultExpanded, rows, groupBy, columnsById]);
|
|
158
217
|
const visibleColumns = useMemo(() => {
|
|
159
218
|
const byId = new Map(columns.map((c) => [c.id, c]));
|
|
160
|
-
return
|
|
219
|
+
return effectiveOrder
|
|
161
220
|
.map((id) => byId.get(id))
|
|
162
|
-
.filter((c) => Boolean(c) && prefs.columnVisibility
|
|
163
|
-
}, [columns,
|
|
221
|
+
.filter((c) => Boolean(c) && isColumnVisible(c, prefs.columnVisibility));
|
|
222
|
+
}, [columns, effectiveOrder, prefs.columnVisibility]);
|
|
164
223
|
const sortedFilteredRows = useMemo(() => {
|
|
165
224
|
let out = rows;
|
|
166
225
|
for (const col of columns) {
|
|
@@ -251,7 +310,7 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
251
310
|
});
|
|
252
311
|
};
|
|
253
312
|
const moveColumn = (columnId, dir) => {
|
|
254
|
-
const order = [...
|
|
313
|
+
const order = [...effectiveOrder];
|
|
255
314
|
const idx = order.indexOf(columnId);
|
|
256
315
|
if (idx < 0)
|
|
257
316
|
return;
|
|
@@ -262,7 +321,7 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
262
321
|
setPrefs({ ...prefs, columnOrder: order });
|
|
263
322
|
};
|
|
264
323
|
const dropColumn = (sourceId, targetId, side) => {
|
|
265
|
-
const order = reorderColumns(
|
|
324
|
+
const order = reorderColumns(effectiveOrder, sourceId, targetId, side);
|
|
266
325
|
if (order.join(GROUP_KEY_SEP) !== prefs.columnOrder.join(GROUP_KEY_SEP)) {
|
|
267
326
|
setPrefs({ ...prefs, columnOrder: order });
|
|
268
327
|
}
|
|
@@ -515,7 +574,7 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
|
|
|
515
574
|
const [open, setOpen] = useState(false);
|
|
516
575
|
const orderedColumns = useMemo(() => {
|
|
517
576
|
const byId = new Map(columns.map((c) => [c.id, c]));
|
|
518
|
-
return prefs.columnOrder.map((id) => byId.get(id)).filter(Boolean);
|
|
577
|
+
return reconcileOrder(columns, prefs.columnOrder).map((id) => byId.get(id)).filter(Boolean);
|
|
519
578
|
}, [columns, prefs.columnOrder]);
|
|
520
579
|
return (_jsxs(DropdownMenu.Root, { open: open, onOpenChange: setOpen, children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsx("button", { type: "button", className: "p-2.5 sm:p-1 hover:bg-[var(--color-surface)] text-[var(--color-text-muted)] justify-self-end", title: "Spalten konfigurieren", style: { borderRadius: 'var(--border-radius-md)' }, children: _jsx(Settings, { className: "w-4 h-4" }) }) }), _jsxs(DropdownMenu.Content, { className: "min-w-[260px] p-2", children: [_jsx("div", { className: "text-xs font-semibold mb-2 text-[var(--color-text-muted)] uppercase", children: "Gruppieren nach" }), groupBy.length === 0 ? (_jsx("div", { className: "text-xs text-[var(--color-text-muted)] italic px-1 mb-2", children: "Keine Gruppierung aktiv." })) : (_jsxs("div", { className: "mb-2 space-y-1", onClick: (e) => e.stopPropagation(), children: [groupBy.map((id, idx) => {
|
|
521
580
|
const col = columns.find((c) => c.id === id);
|
|
@@ -526,7 +585,7 @@ function ColumnInventoryButton({ columns, prefs, setColumnVisible, moveColumn, r
|
|
|
526
585
|
addGroupBy(v);
|
|
527
586
|
e.target.value = '';
|
|
528
587
|
}, 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) => {
|
|
529
|
-
const visible = prefs.columnVisibility
|
|
588
|
+
const visible = isColumnVisible(col, prefs.columnVisibility);
|
|
530
589
|
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));
|
|
531
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"] }) })] })] }));
|
|
532
591
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@efa-one/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.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",
|