@efa-one/sdk 1.24.0 → 1.25.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/Alert.d.ts +4 -1
- package/frontend/ui/Alert.js +13 -2
- package/frontend/ui/DataTable.d.ts +1 -0
- package/frontend/ui/DataTable.js +80 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -269,6 +269,18 @@ Excel export for such data. No backend or kernel change.
|
|
|
269
269
|
- After the bump every dialog without `size` is 768 px wide. Give short confirmations
|
|
270
270
|
("Wirklich löschen?") `size="sm"`.
|
|
271
271
|
|
|
272
|
+
**1.25.0 — dismissible banners, top scrollbar.** No kernel or backend change.
|
|
273
|
+
- Every `Alert` shows a close „×" top right by default. With `onDismiss` nothing
|
|
274
|
+
changes. Without it the banner hides itself and shows again when its text changes.
|
|
275
|
+
`dismissible={false}` removes the „×" for notices that must stay.
|
|
276
|
+
- Messages from app state (errors, results) need `onDismiss`: without it, a closed
|
|
277
|
+
banner stays hidden when the app sets the same text again in one handler or when
|
|
278
|
+
the content is JSX. After the bump, add `onDismiss` (and `onErrorDismiss` on
|
|
279
|
+
`RecordDialog`) wherever such banners lack it.
|
|
280
|
+
- `DataTable`: when the grid is wider than its container, a second horizontal
|
|
281
|
+
scrollbar sits above the table and scrolls in sync with the bottom one. Not on
|
|
282
|
+
mobile. The border moved to an outer wrapper.
|
|
283
|
+
|
|
272
284
|
**Next minor (planned 1.23.0, not yet released) — exchange, cookie and S2S hardening.**
|
|
273
285
|
Needs a kernel with efaware/efa_kernel#106 and #107 first (gateway exchange token
|
|
274
286
|
`gw: 'app'`, provenance `jti`, cookie filter). Then:
|
package/frontend/ui/Alert.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ interface AlertProps {
|
|
|
4
4
|
variant?: AlertVariant;
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
onDismiss?: () => void;
|
|
7
|
+
dismissible?: boolean;
|
|
7
8
|
className?: string;
|
|
8
9
|
}
|
|
9
|
-
export declare function
|
|
10
|
+
export declare function alertContentKey(children: React.ReactNode): string | null;
|
|
11
|
+
export declare function isAlertHidden(dismissedKey: string | null | undefined, contentKey: string | null): boolean;
|
|
12
|
+
export declare function Alert({ variant, children, onDismiss, dismissible, className }: AlertProps): React.JSX.Element | null;
|
|
10
13
|
export {};
|
package/frontend/ui/Alert.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
2
3
|
import { X } from 'lucide-react';
|
|
3
|
-
export function
|
|
4
|
-
return
|
|
4
|
+
export function alertContentKey(children) {
|
|
5
|
+
return typeof children === 'string' || typeof children === 'number' ? String(children) : null;
|
|
6
|
+
}
|
|
7
|
+
export function isAlertHidden(dismissedKey, contentKey) {
|
|
8
|
+
return dismissedKey !== undefined && dismissedKey === contentKey;
|
|
9
|
+
}
|
|
10
|
+
export function Alert({ variant = 'error', children, onDismiss, dismissible = true, className = '' }) {
|
|
11
|
+
const [dismissedKey, setDismissedKey] = useState(undefined);
|
|
12
|
+
const contentKey = alertContentKey(children);
|
|
13
|
+
if (!onDismiss && isAlertHidden(dismissedKey, contentKey))
|
|
14
|
+
return null;
|
|
15
|
+
return (_jsxs("div", { role: "alert", className: `shrink-0 flex items-start gap-2 border rounded-md px-4 py-3 text-sm font-medium alert-${variant} ${className}`, children: [_jsx("span", { className: "flex-1 min-w-0", children: children }), dismissible && (_jsx("button", { type: "button", onClick: onDismiss ?? (() => setDismissedKey(contentKey)), "aria-label": "Meldung schlie\u00DFen", className: "shrink-0 -mr-1 p-0.5 rounded hover:bg-current/10 transition-colors", children: _jsx(X, { className: "w-4 h-4" }) }))] }));
|
|
5
16
|
}
|
|
@@ -89,4 +89,5 @@ interface RenderItem<T> {
|
|
|
89
89
|
export declare function buildRenderItems<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>, expandedGroups: Set<string> | 'all'): RenderItem<T>[];
|
|
90
90
|
export declare function collectAllGroupKeys<T>(rows: T[], groupBy: string[], columnsById: Map<string, ColumnDef<T>>): string[];
|
|
91
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;
|
|
92
|
+
export declare function topScrollbarWidth(gridWidth: number, viewportWidth: number): number | null;
|
|
92
93
|
export {};
|
package/frontend/ui/DataTable.js
CHANGED
|
@@ -241,6 +241,7 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
241
241
|
const [dragColumnId, setDragColumnId] = useState(null);
|
|
242
242
|
const [dropTarget, setDropTarget] = useState(null);
|
|
243
243
|
const containerRef = useRef(null);
|
|
244
|
+
const headerRef = useRef(null);
|
|
244
245
|
const effectiveOrder = useMemo(() => reconcileOrder(columns, prefs.columnOrder), [columns, prefs.columnOrder]);
|
|
245
246
|
const seenGroupKeysRef = useRef(new Set());
|
|
246
247
|
useEffect(() => {
|
|
@@ -433,42 +434,85 @@ export function DataTable({ listId, rows, columns, rowKey, onRowClick, selection
|
|
|
433
434
|
hasColumns: visibleColumns.some((c) => c.exportable !== false),
|
|
434
435
|
run: runExport,
|
|
435
436
|
} : undefined, columns: columns, prefs: prefs, setColumnVisible: setColumnVisible, moveColumn: moveColumn, reset: reset, groupBy: groupBy, addGroupBy: addGroupBy, removeGroupBy: removeGroupBy, moveGroupBy: moveGroupBy, clearGroupBy: clearGroupBy }));
|
|
436
|
-
return (_jsxs("div", {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
437
|
+
return (_jsxs("div", { className: "border border-[var(--color-border)]", children: [_jsx(TopScrollbar, { targetRef: containerRef, measureRef: headerRef, enabled: !isMobile }), _jsxs("div", { ref: containerRef, className: "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 aufheben" }), _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", { ref: headerRef, 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) => {
|
|
438
|
+
const activeSort = prefs.sort?.columnId === col.id ? prefs.sort.direction : null;
|
|
439
|
+
const filterValue = prefs.filters[col.id];
|
|
440
|
+
const isFiltered = filterValue != null;
|
|
441
|
+
return (_jsx(HeaderCell, { columnId: col.id, label: col.label, dragColumnId: dragColumnId, dropSide: dropTarget?.id === col.id ? dropTarget.side : null, onDragStart: () => setDragColumnId(col.id), onDragEnd: () => { setDragColumnId(null); setDropTarget(null); }, onDragOverSide: (side) => {
|
|
442
|
+
if (dropTarget?.id !== col.id || dropTarget.side !== side)
|
|
443
|
+
setDropTarget({ id: col.id, side });
|
|
444
|
+
}, onDrop: (sourceId, side) => {
|
|
445
|
+
dropColumn(sourceId, col.id, side);
|
|
446
|
+
setDragColumnId(null);
|
|
447
|
+
setDropTarget(null);
|
|
448
|
+
}, onResizePreview: (px) => previewColumnWidth(col.id, px), onResizeCommit: (px) => setColumnWidth(col.id, px), onResizeCancel: () => containerRef.current?.style.setProperty('--dt-cols', gridTemplate), hasCustomWidth: columnWidths[col.id] != null, children: _jsxs(DropdownMenu.Root, { children: [_jsx(DropdownMenu.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex items-center gap-1 min-w-0 px-1 py-0.5 hover:bg-[var(--color-surface)] text-left", style: { borderRadius: 'var(--border-radius-md)' }, children: [_jsx("span", { className: "min-w-0 truncate", children: col.label }), activeSort === 'asc' && _jsx(ChevronUp, { className: "w-3 h-3" }), activeSort === 'desc' && _jsx(ChevronDown, { className: "w-3 h-3" }), isFiltered && _jsx(Filter, { className: "w-3 h-3 signal-text-primary" })] }) }), _jsxs(DropdownMenu.Content, { className: "min-w-[220px] p-1", children: [col.sortable !== false && (_jsxs(_Fragment, { children: [_jsxs(DropdownMenu.Item, { onSelect: () => setSort(col.id, 'asc'), children: [_jsx(ArrowUp, { className: "w-4 h-4" }), "Sortieren A\u2192Z"] }), _jsxs(DropdownMenu.Item, { onSelect: () => setSort(col.id, 'desc'), children: [_jsx(ArrowDown, { className: "w-4 h-4" }), "Sortieren Z\u2192A"] }), activeSort && (_jsx(DropdownMenu.Item, { onSelect: clearSort, children: "Sortierung zur\u00FCcksetzen" })), _jsx(DropdownMenu.Separator, {})] })), groupBy.includes(col.id) ? (_jsxs(DropdownMenu.Item, { onSelect: () => removeGroupBy(col.id), children: [_jsx(Layers, { className: "w-4 h-4" }), "Gruppierung aufheben"] })) : (_jsxs(DropdownMenu.Item, { onSelect: () => addGroupBy(col.id), children: [_jsx(Layers, { className: "w-4 h-4" }), "Nach dieser Spalte gruppieren"] })), _jsx(DropdownMenu.Separator, {}), col.filter && (_jsx(FilterEditor, { column: col, value: filterValue, onChange: (v) => setFilter(col.id, v) })), columnWidths[col.id] != null && (_jsxs(DropdownMenu.Item, { onSelect: () => setColumnWidth(col.id, null), children: [_jsx(RotateCcw, { className: "w-4 h-4" }), "Standardbreite"] })), _jsxs(DropdownMenu.Item, { onSelect: () => setColumnVisible(col.id, false), variant: "danger", children: [_jsx(EyeOff, { className: "w-4 h-4" }), "Spalte ausblenden"] })] })] }) }, col.id));
|
|
449
|
+
}), gearButton] })), sortedFilteredRows.length === 0 ? (_jsx("div", { className: "px-3 py-8 text-center text-sm text-[var(--color-text-muted)]", children: "Keine Eintr\u00E4ge" })) : (buildRenderItems(sortedFilteredRows, groupBy, columnsById, expandedGroups).map((item, idx) => {
|
|
450
|
+
if (item.kind === 'group') {
|
|
451
|
+
const level = item.level ?? 0;
|
|
452
|
+
const groupColLabel = columnsById.get(groupBy[level])?.label ?? '';
|
|
453
|
+
return (_jsx("div", { className: "px-3 py-1.5 text-xs border-b border-[var(--color-border)] cursor-pointer select-none hover:bg-[var(--color-surface-raised)]", style: {
|
|
454
|
+
background: 'var(--color-surface-raised)',
|
|
455
|
+
paddingLeft: 12 + level * 18,
|
|
456
|
+
}, onClick: () => toggleGroup(item.key), title: item.expanded ? 'Gruppe einklappen' : 'Gruppe ausklappen', children: _jsxs("div", { className: "flex items-center gap-2", children: [item.expanded
|
|
457
|
+
? _jsx(ChevronDown, { className: "w-3.5 h-3.5 text-[var(--color-text-muted)]" })
|
|
458
|
+
: _jsx(ChevronRight, { className: "w-3.5 h-3.5 text-[var(--color-text-muted)]" }), _jsx("span", { className: "text-[var(--color-text-muted)] uppercase text-[10px]", children: groupColLabel }), _jsx("span", { className: "font-medium text-[var(--color-text-primary)]", children: item.label }), _jsxs("span", { className: "text-[var(--color-text-muted)]", children: ["(", item.count, ")"] })] }) }, `g:${item.key}`));
|
|
459
|
+
}
|
|
460
|
+
const row = item.row;
|
|
461
|
+
const key = rowKey(row);
|
|
462
|
+
const isChecked = selection?.selected.has(key) ?? false;
|
|
463
|
+
const rowIndent = groupBy.length > 0 ? 12 + groupBy.length * 18 : undefined;
|
|
464
|
+
if (isMobile) {
|
|
465
|
+
return (_jsxs("div", { className: `flex flex-col gap-1.5 px-3 py-3 border-b border-[var(--color-border)] ${onRowClick ? 'cursor-pointer hover:bg-[var(--color-surface-raised)]' : ''}`, style: { background: isChecked ? 'rgba(99,102,241,0.07)' : undefined, paddingLeft: rowIndent }, onClick: () => onRowClick?.(row), children: [selection && (_jsxs("label", { className: "flex items-center gap-2 self-start py-1 pr-2 cursor-pointer", onClick: (e) => e.stopPropagation(), children: [_jsx("input", { type: "checkbox", className: "w-5 h-5", checked: isChecked, onChange: () => toggleOne(key), "aria-label": "Zeile ausw\u00E4hlen" }), _jsx("span", { className: "text-xs text-[var(--color-text-muted)]", children: "Ausw\u00E4hlen" })] })), visibleColumns.map((col) => (_jsxs("div", { className: "flex flex-col gap-0.5", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: col.label }), _jsx("div", { className: "min-w-0 break-words text-sm", children: col.cell ? col.cell(row) : formatCellValue(col, row) })] }, col.id)))] }, `r:${String(key)}:${idx}`));
|
|
466
|
+
}
|
|
467
|
+
return (_jsxs("div", { className: `grid items-center min-w-min px-3 py-2 text-sm border-b border-[var(--color-border)] ${onRowClick ? 'cursor-pointer hover:bg-[var(--color-surface-raised)]' : ''}`, style: {
|
|
468
|
+
gridTemplateColumns: 'var(--dt-cols)',
|
|
469
|
+
background: isChecked ? 'rgba(99,102,241,0.07)' : undefined,
|
|
470
|
+
paddingLeft: rowIndent,
|
|
471
|
+
}, onClick: () => onRowClick?.(row), children: [selection && (_jsx("input", { type: "checkbox", checked: isChecked, onChange: () => toggleOne(key), onClick: (e) => e.stopPropagation(), "aria-label": "Zeile ausw\u00E4hlen" })), visibleColumns.map((col) => (_jsx("div", { className: "min-w-0 truncate", children: col.cell ? col.cell(row) : formatCellValue(col, row) }, col.id))), _jsx("div", {})] }, `r:${String(key)}:${idx}`));
|
|
472
|
+
})), footer && (_jsx("div", { className: "px-3 py-2 text-xs text-[var(--color-text-muted)] border-t border-[var(--color-border)]", children: footer }))] })] }));
|
|
473
|
+
}
|
|
474
|
+
export function topScrollbarWidth(gridWidth, viewportWidth) {
|
|
475
|
+
return gridWidth > viewportWidth + 1 ? gridWidth : null;
|
|
476
|
+
}
|
|
477
|
+
function syncScroll(from, to) {
|
|
478
|
+
if (Math.abs(to.scrollLeft - from.scrollLeft) > 1)
|
|
479
|
+
to.scrollLeft = from.scrollLeft;
|
|
480
|
+
}
|
|
481
|
+
function TopScrollbar({ targetRef, measureRef, enabled }) {
|
|
482
|
+
const barRef = useRef(null);
|
|
483
|
+
const [width, setWidth] = useState(null);
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
const target = targetRef.current;
|
|
486
|
+
const measured = measureRef.current;
|
|
487
|
+
if (!enabled || !target || !measured || typeof ResizeObserver === 'undefined') {
|
|
488
|
+
setWidth(null);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const update = () => setWidth(topScrollbarWidth(target.scrollWidth, target.clientWidth));
|
|
492
|
+
const observer = new ResizeObserver(update);
|
|
493
|
+
observer.observe(target);
|
|
494
|
+
observer.observe(measured);
|
|
495
|
+
update();
|
|
496
|
+
const follow = () => {
|
|
497
|
+
if (barRef.current)
|
|
498
|
+
syncScroll(target, barRef.current);
|
|
499
|
+
};
|
|
500
|
+
target.addEventListener('scroll', follow, { passive: true });
|
|
501
|
+
return () => {
|
|
502
|
+
observer.disconnect();
|
|
503
|
+
target.removeEventListener('scroll', follow);
|
|
504
|
+
};
|
|
505
|
+
}, [enabled, targetRef, measureRef]);
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
if (barRef.current && targetRef.current)
|
|
508
|
+
barRef.current.scrollLeft = targetRef.current.scrollLeft;
|
|
509
|
+
}, [width, targetRef]);
|
|
510
|
+
if (width == null)
|
|
511
|
+
return null;
|
|
512
|
+
return (_jsx("div", { ref: barRef, "aria-hidden": "true", tabIndex: -1, style: { overflowX: 'auto', overflowY: 'hidden', minHeight: 12 }, onScroll: () => {
|
|
513
|
+
if (targetRef.current && barRef.current)
|
|
514
|
+
syncScroll(barRef.current, targetRef.current);
|
|
515
|
+
}, children: _jsx("div", { style: { width, height: 1 } }) }));
|
|
472
516
|
}
|
|
473
517
|
const DRAG_MIME = 'application/x-efa-datatable-column';
|
|
474
518
|
function HeaderCell({ columnId, label, dragColumnId, dropSide, hasCustomWidth, onDragStart, onDragEnd, onDragOverSide, onDrop, onResizePreview, onResizeCommit, onResizeCancel, children, }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@efa-one/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.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",
|