@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 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 DataTable<T, K extends string | number>({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, persistence, }: DataTableProps<T, K>): React.ReactElement;
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 {};
@@ -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 lbl = isDate ? (timeOf(raw) == null ? '' : formatDate(raw)) : groupLabelOf(raw);
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 DataTable({ listId, rows, columns, rowKey, onRowClick, selection, initialPrefs, footer, persistence, }) {
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 prefs.columnOrder
219
+ return effectiveOrder
161
220
  .map((id) => byId.get(id))
162
- .filter((c) => Boolean(c) && prefs.columnVisibility[c.id] !== false);
163
- }, [columns, prefs.columnOrder, prefs.columnVisibility]);
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 = [...prefs.columnOrder];
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(prefs.columnOrder, sourceId, targetId, side);
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[col.id] !== false;
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.20.0",
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",