nexus-shared 3.2.0 → 4.0.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/CHANGELOG.md +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -17,17 +17,20 @@ function popupPosition(dialog) {
|
|
|
17
17
|
}
|
|
18
18
|
// Each popup's opener and the popup that held it, so focus can find its way back through popups that have gone.
|
|
19
19
|
const openers = new WeakMap();
|
|
20
|
+
// How each popup puts the focus inside itself (its `initialFocus`), for a popup closed over it that left the focus nowhere.
|
|
21
|
+
const homes = new WeakMap();
|
|
20
22
|
/**
|
|
21
|
-
* A modal window on the native <dialog>, with the buttons of a desktop window:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* A modal window on the native <dialog>, with the buttons of a desktop window: maximize and close, and minimize when
|
|
24
|
+
* it asks (`minimizable`: a CRUD page's Add and Edit). It sits in the top layer above everything, dims and locks the
|
|
25
|
+
* page behind it, traps Tab, closes with Esc or a click outside, and returns focus to where it came from. The title
|
|
26
|
+
* bar moves it; a double-click there maximizes it. Minimized, it
|
|
24
27
|
* waits as a bar at the bottom right with its content kept, while the page can be used. The body scrolls while the
|
|
25
28
|
* title and the footer stay. The content mounts while the popup is open, so a form inside starts fresh every time.
|
|
26
29
|
*
|
|
27
30
|
* @example
|
|
28
31
|
* <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
|
|
29
32
|
*/
|
|
30
|
-
export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, className, children, }) {
|
|
33
|
+
export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }) {
|
|
31
34
|
const id = useId();
|
|
32
35
|
const dialogRef = useRef(null);
|
|
33
36
|
// Content stays mounted through the exit animation, and while minimized.
|
|
@@ -41,7 +44,8 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
41
44
|
const maximized = windowState === "maximized";
|
|
42
45
|
const dockReady = useDockReady();
|
|
43
46
|
const hosted = useContext(HostedPopupContext);
|
|
44
|
-
|
|
47
|
+
// Only a popup that asks: a CRUD page's Add and Edit, and its row forms.
|
|
48
|
+
const canMinimize = minimizable === true && dockReady;
|
|
45
49
|
const downOnBackdropRef = useRef(false);
|
|
46
50
|
// The element that opened the popup, where focus goes back when the browser has nowhere better.
|
|
47
51
|
const openerRef = useRef(null);
|
|
@@ -65,19 +69,22 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
65
69
|
onWindowStateChange?.(next);
|
|
66
70
|
}
|
|
67
71
|
// The latest props and callbacks, for the dock bar and the drag, which outlive a render.
|
|
68
|
-
const latest = useRef({ windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState });
|
|
72
|
+
const latest = useRef({ windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState });
|
|
69
73
|
useLayoutEffect(() => {
|
|
70
|
-
latest.current = { windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState };
|
|
74
|
+
latest.current = { windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState };
|
|
71
75
|
if (!minimized)
|
|
72
76
|
restoreToRef.current = windowState;
|
|
73
77
|
});
|
|
74
78
|
function dock() {
|
|
75
79
|
const current = latest.current;
|
|
76
80
|
dockedRef.current = true;
|
|
81
|
+
const label = typeof current.title === "string" ? current.title : current.ariaLabel;
|
|
77
82
|
dockWindow({
|
|
78
83
|
id: dockId,
|
|
79
84
|
title: current.title ?? current.ariaLabel ?? "Window",
|
|
80
|
-
|
|
85
|
+
// Under the title, so two bars of one kind (two Edit forms, two Adds) read apart: the record, the place.
|
|
86
|
+
description: current.description,
|
|
87
|
+
label: label && typeof current.description === "string" ? `${label} · ${current.description}` : label,
|
|
81
88
|
icon: current.icon,
|
|
82
89
|
restore: () => latest.current.changeWindowState(restoreToRef.current),
|
|
83
90
|
close: current.closable ? () => latest.current.onClose("close") : undefined,
|
|
@@ -102,11 +109,26 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
102
109
|
link = openers.get(link.within);
|
|
103
110
|
if (link?.opener?.isConnected)
|
|
104
111
|
link.opener.focus({ preventScroll: true });
|
|
112
|
+
// An opener that cannot take the focus - hidden in a menu that closed, disabled while its action runs - or none at
|
|
113
|
+
// all never leaves it on the page while another popup is still open: the popup now on top takes it, where it puts
|
|
114
|
+
// it on opening (a record's workspace: its chosen tab).
|
|
115
|
+
const now = document.activeElement;
|
|
116
|
+
if (now && now !== document.body && !dialog.contains(now))
|
|
117
|
+
return;
|
|
118
|
+
const below = Array.from(document.querySelectorAll("dialog.nx-popup")).filter(other => other !== dialog && other.open);
|
|
119
|
+
const top = below[below.length - 1];
|
|
120
|
+
if (top)
|
|
121
|
+
(homes.get(top) ?? (() => top.focus()))();
|
|
105
122
|
}
|
|
106
123
|
function focusInside(dialog) {
|
|
107
124
|
const target = initialFocus ? dialog.querySelector(initialFocus) : dialog.querySelector(`:is(.nx-popup__body, .nx-popup__foot) :is(${FOCUSABLE})`);
|
|
108
125
|
(target ?? dialog).focus();
|
|
109
126
|
}
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
const dialog = dialogRef.current;
|
|
129
|
+
if (dialog)
|
|
130
|
+
homes.set(dialog, () => focusInside(dialog));
|
|
131
|
+
});
|
|
110
132
|
function showDialog(dialog) {
|
|
111
133
|
// The first modal locks the page; the space of its scrollbar is kept as padding, so nothing shifts.
|
|
112
134
|
if (!document.querySelector(".nx-popup:modal"))
|
|
@@ -317,6 +339,12 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
317
339
|
function onPointerDown(event) {
|
|
318
340
|
downOnBackdropRef.current = event.target === event.currentTarget;
|
|
319
341
|
}
|
|
342
|
+
// A press outside a popup that stays open through it (`closeOnBackdrop: false`) leaves the focus where it was, as
|
|
343
|
+
// the press does nothing: a question's Confirm keeps it, so Enter still answers, and a field keeps its caret.
|
|
344
|
+
function onMouseDown(event) {
|
|
345
|
+
if (event.target === event.currentTarget && !closeOnBackdrop)
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
}
|
|
320
348
|
function onClick(event) {
|
|
321
349
|
const onBackdrop = event.target === event.currentTarget && downOnBackdropRef.current;
|
|
322
350
|
downOnBackdropRef.current = false;
|
|
@@ -369,5 +397,5 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
369
397
|
const titleId = `${id}-title`;
|
|
370
398
|
const descriptionId = `${id}-description`;
|
|
371
399
|
const hasControls = canMinimize || maximizable || closable;
|
|
372
|
-
return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": description ? descriptionId : undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
|
|
400
|
+
return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": cx(description ? descriptionId : undefined, ariaDescribedBy) || undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onMouseDown: onMouseDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
|
|
373
401
|
}
|
|
@@ -3,7 +3,9 @@ import { type ReactNode } from "react";
|
|
|
3
3
|
export interface DockedWindow {
|
|
4
4
|
id: string;
|
|
5
5
|
title: ReactNode;
|
|
6
|
-
/**
|
|
6
|
+
/** A second line under the title: the record or the place a window is about, so two of one kind read apart. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Plain text of the title, and of the description, for the tooltip. */
|
|
7
9
|
label?: string;
|
|
8
10
|
/** The window's icon; a window icon when left out. */
|
|
9
11
|
icon?: ReactNode;
|
|
@@ -113,9 +113,10 @@ export function PopupDock() {
|
|
|
113
113
|
function DockBar({ item, onRestore }) {
|
|
114
114
|
const id = useId();
|
|
115
115
|
const titleId = `${id}-title`;
|
|
116
|
+
const descriptionId = `${id}-description`;
|
|
116
117
|
const closeId = `${id}-close`;
|
|
117
118
|
return (_jsxs("li", { className: "nx-dock__item", children: [_jsxs("button", { type: "button", className: "nx-dock__window", title: item.label ? `Restore: ${item.label}` : "Restore", onClick: () => {
|
|
118
119
|
onRestore();
|
|
119
120
|
item.restore();
|
|
120
|
-
}, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
|
|
121
|
+
}, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsxs("span", { className: "nx-dock__text", children: [_jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), item.description ? (_jsx("span", { id: descriptionId, className: "nx-dock__description", children: item.description })) : null] }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": item.description ? `${closeId} ${titleId} ${descriptionId}` : `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
|
|
121
122
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useSyncExternalStore } from "react";
|
|
4
4
|
import { Popup } from "./Popup.js";
|
|
5
5
|
import { HostedPopupContext } from "./PopupContext.js";
|
|
6
|
+
import { PageLeaveGuardContext } from "../Forms/FormLeaveGuard.js";
|
|
6
7
|
import { PopupDock } from "./PopupDock.js";
|
|
7
8
|
import { withPopupRule } from "./PopupRules.js";
|
|
8
9
|
const EMPTY = [];
|
|
@@ -156,8 +157,8 @@ export function PopupHost() {
|
|
|
156
157
|
for (const entry of list)
|
|
157
158
|
rendered.add(entry.id);
|
|
158
159
|
});
|
|
159
|
-
return (_jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
return (_jsx(PageLeaveGuardContext, { value: null, children: _jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
|
|
161
|
+
const { content, footer, onClose: _onClose, beforeClose: _beforeClose, onWindowStateChange: _onWindowStateChange, key: _key, ...rest } = entry.options;
|
|
162
|
+
return (_jsx(Popup, { ...rest, open: entry.open, windowState: entry.windowState, onWindowStateChange: state => setWindowState(entry.id, state), footer: typeof footer === "function" ? footer(entry.handle) : footer, onClose: reason => requestClose(entry.id, reason), onClosed: () => removeEntry(entry.id), children: typeof content === "function" ? content(entry.handle) : content }, entry.id));
|
|
163
|
+
}), _jsx(PopupDock, {})] }) }));
|
|
163
164
|
}
|
|
@@ -5,16 +5,18 @@ import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, us
|
|
|
5
5
|
import { flushSync } from "react-dom";
|
|
6
6
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
7
7
|
import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
|
+
import { canCall, permissionsVersion, subscribePermissions } from "../../Helpers/Permissions.js";
|
|
8
9
|
import { fitColumnWidth, headLeafCells, startColumnDrag, startColumnResize } from "../../Helpers/TableColumns.js";
|
|
9
10
|
import { exportTableCsv, printTable } from "../../Helpers/TableExport.js";
|
|
10
11
|
import { asFilterInput, filterNames, filterRequest } from "../../Helpers/TableFilters.js";
|
|
11
12
|
import { joinKeys, joinSourceName, tableJoins } from "../../Helpers/TableJoins.js";
|
|
12
|
-
import { canPinColumn, columnAlign, columnValue, cssLength, DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, formatCellText, headBands, MIN_COLUMN_WIDTH, orderColumns, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, viewColumns, } from "../../Helpers/TableHelpers.js";
|
|
13
|
+
import { canPinColumn, columnAlign, columnValue, cssLength, DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, formatCellText, headBands, MIN_COLUMN_WIDTH, orderColumns, releasedPins, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, viewColumns, } from "../../Helpers/TableHelpers.js";
|
|
13
14
|
import { TableStore } from "../../Helpers/TableStore.js";
|
|
14
15
|
import { readUserPreferences, useNumberLocale, useUserPreferences } from "../../Helpers/UserPreferences.js";
|
|
15
16
|
import { Button } from "../Documents/Button.js";
|
|
16
17
|
import { Menu } from "../Documents/Menu.js";
|
|
17
18
|
import { SplitButton } from "../Documents/SplitButton.js";
|
|
19
|
+
import { useReadOnlyNotice } from "../Inputs/ReadOnlyNotice.js";
|
|
18
20
|
import { TextBox } from "../Inputs/TextBox.js";
|
|
19
21
|
import { toast } from "../Layouts/Toaster.js";
|
|
20
22
|
import { downloadExcelTemplate, exportExcel } from "../../Services/ExcelApi.js";
|
|
@@ -41,8 +43,10 @@ export function useTable() {
|
|
|
41
43
|
const STATE_PREFIX = "nx-table:";
|
|
42
44
|
/** The key of the section `pinnedRows` makes. */
|
|
43
45
|
const PINS_SECTION = "pins";
|
|
44
|
-
/**
|
|
45
|
-
const
|
|
46
|
+
/** A version that never changes: the lookups' for a table with no `join` column, the permissions' for one with no sheet. */
|
|
47
|
+
const NO_VERSION = () => 0;
|
|
48
|
+
/** The page size of a table read whole (`pager="none"`): every row on the first page. */
|
|
49
|
+
const WHOLE_PAGE = Number.MAX_SAFE_INTEGER;
|
|
46
50
|
function readSaved(key) {
|
|
47
51
|
if (!key || typeof localStorage === "undefined")
|
|
48
52
|
return null;
|
|
@@ -77,6 +81,9 @@ function usePerRow(make) {
|
|
|
77
81
|
return value;
|
|
78
82
|
}, [cache, make]);
|
|
79
83
|
}
|
|
84
|
+
/** No pinned column scrolls: the table is wide enough for them all. */
|
|
85
|
+
const NONE_RELEASED = new Set();
|
|
86
|
+
const sameKeys = (a, b) => a.size === b.size && [...a].every(key => b.has(key));
|
|
80
87
|
const sameNumbers = (a, b) => {
|
|
81
88
|
const keys = Object.keys(a);
|
|
82
89
|
return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
|
|
@@ -99,17 +106,24 @@ const sameNumbers = (a, b) => {
|
|
|
99
106
|
* />
|
|
100
107
|
*/
|
|
101
108
|
export function DataTable(props) {
|
|
102
|
-
const { columns, rows: givenRows, load, loadKey, rowKey = "id", rowLabel, table: givenTable, searchable = true, searchPlaceholder = "Search…", toolbar, actions, add, columnsMenu = true, selectable = false, maxSelection = DEFAULT_MAX_SELECTION, onSelectionChange, massActions, selectBy, rowActions, onRowOpen, openOnClick: ownOpenOnClick, sections: ownSections, rowMarks, pinnedRows, pinnedLoading = false, pinnedLabel = "Pinned", pinnedMax, onReload, reorderable = true, resizable = true, pinnable = true, moreActions, exportable = true, printable = true, exportName, onImport, excel, pageSize: ownPageSize, pageSizes = DEFAULT_PAGE_SIZES, defaultSort = null, stateKey, emptyMessage = "No records yet.", emptyAction, maxHeight, fill = false, size = "md", id, className, style, ref, } = props;
|
|
109
|
+
const { columns, rows: givenRows, load, loadKey, rowKey = "id", rowLabel, table: givenTable, searchable = true, searchPlaceholder = "Search…", caption, toolbar, actions, add, columnsMenu = true, selectable = false, maxSelection = DEFAULT_MAX_SELECTION, onSelectionChange, massActions, selectBy, rowActions, onRowOpen, openOnClick: ownOpenOnClick, onPick, sections: ownSections, rowMarks, pinnedRows, pinnedLoading = false, pinnedLabel = "Pinned", pinnedMax, onReload, reorderable = true, resizable = true, pinnable = true, moreActions, exportable = true, printable = true, exportName, onImport, excel, pageSize: ownPageSize, pageSizes = DEFAULT_PAGE_SIZES, defaultSort = null, stateKey, currency, emptyMessage = "No records yet.", emptyAction, maxHeight, fill = false, size = "md", id, className, style, ref, } = props;
|
|
103
110
|
const paging = givenRows ? "client" : (props.paging ?? "client");
|
|
111
|
+
// A server's pages cannot all be shown at once: such a table keeps its paging in the footer instead.
|
|
112
|
+
const pager = props.pager === "none" && paging === "server" ? "footer" : (props.pager ?? "bar");
|
|
113
|
+
const whole = pager === "none";
|
|
104
114
|
const reloadable = props.reloadable ?? Boolean(load);
|
|
105
115
|
const searchDelay = props.searchDelay ?? (paging === "server" ? 300 : 0);
|
|
106
|
-
// A table with checkboxes numbers its rows: the number becomes the checkbox as soon as the row is under the pointer.
|
|
107
|
-
|
|
116
|
+
// A table with checkboxes numbers its rows: the number becomes the checkbox as soon as the row is under the pointer. So
|
|
117
|
+
// does one whose rows are picked, whose first column holds each row's radio button.
|
|
118
|
+
const pickable = Boolean(onPick);
|
|
119
|
+
const numbered = props.numbered ?? (selectable || pickable);
|
|
108
120
|
// The person's choices in the quick preference panel, once made, go over what the table names itself.
|
|
109
121
|
const preferences = useUserPreferences();
|
|
110
122
|
const openOnClick = preferences.rowClick ?? ownOpenOnClick ?? false;
|
|
111
|
-
/** The page size the table opens with: the person's default when this table offers it, else the table's own. */
|
|
123
|
+
/** The page size the table opens with: every row for a table read whole, the person's default when this table offers it, else the table's own. */
|
|
112
124
|
const openingPageSize = () => {
|
|
125
|
+
if (whole)
|
|
126
|
+
return WHOLE_PAGE;
|
|
113
127
|
const preferred = readUserPreferences().pageSize;
|
|
114
128
|
return preferred !== null && pageSizes.includes(preferred) ? preferred : (ownPageSize ?? DEFAULT_PAGE_SIZE);
|
|
115
129
|
};
|
|
@@ -117,20 +131,24 @@ export function DataTable(props) {
|
|
|
117
131
|
const table = givenTable ?? own;
|
|
118
132
|
// Prepared once for the store and the panel: the same params without what would make them filter on their own.
|
|
119
133
|
const filterInputs = useMemo(() => props.filterInputs?.map(asFilterInput), [props.filterInputs]);
|
|
120
|
-
table.configure({ columns, rowKey, rows: givenRows, load, paging, maxSelection, onSelectionChange, filterInputs });
|
|
134
|
+
table.configure({ columns, rowKey, rows: givenRows, load, paging, maxSelection, onSelectionChange, onPickChange: onPick, filterInputs });
|
|
121
135
|
useImperativeHandle(ref, () => table, [table]);
|
|
122
136
|
const state = useSyncExternalStore(table.subscribe, table.getState, table.getState);
|
|
123
137
|
// The columns whose values are other records', looked up by key (`join`). Only a table with one listens to the page's
|
|
124
138
|
// lookups, and it draws again - its rows too, through `cellOptions` - each time names come in.
|
|
125
139
|
const joins = useMemo(() => columns.flatMap(column => (column.join ? [column.join] : [])), [columns]);
|
|
126
140
|
const subscribeJoins = useCallback((listener) => (joins.length ? tableJoins.subscribe(listener) : () => { }), [joins.length]);
|
|
127
|
-
const joinsVersion = useSyncExternalStore(subscribeJoins, joins.length ? tableJoins.getVersion :
|
|
141
|
+
const joinsVersion = useSyncExternalStore(subscribeJoins, joins.length ? tableJoins.getVersion : NO_VERSION, NO_VERSION);
|
|
142
|
+
// A spreadsheet's items ask the app's permissions, as a CRUD's actions do, so only a table with one listens for them.
|
|
143
|
+
const hasSheet = Boolean(excel);
|
|
144
|
+
const subscribeSheet = useCallback((listener) => (hasSheet ? subscribePermissions(listener) : () => { }), [hasSheet]);
|
|
145
|
+
useSyncExternalStore(subscribeSheet, hasSheet ? permissionsVersion : NO_VERSION, NO_VERSION);
|
|
128
146
|
const dates = useDatePreferences();
|
|
129
147
|
const timeZone = useBrowserTimeZone();
|
|
130
148
|
const numbers = useNumberLocale();
|
|
131
149
|
// `joinsVersion` is read by no cell option: a new object is what tells the memoized rows that their names came in.
|
|
132
150
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
133
|
-
const cellOptions = useMemo(() => ({ preferences: dates, timeZone: timeZone ?? undefined, locale: numbers }), [dates, timeZone, numbers, joinsVersion]);
|
|
151
|
+
const cellOptions = useMemo(() => ({ preferences: dates, timeZone: timeZone ?? undefined, locale: numbers, currency }), [dates, timeZone, numbers, currency, joinsVersion]);
|
|
134
152
|
const tableId = useId();
|
|
135
153
|
// The search box is uncontrolled: a new key clears it when a new list starts over.
|
|
136
154
|
const [searchGeneration, setSearchGeneration] = useState(0);
|
|
@@ -147,7 +165,7 @@ export function DataTable(props) {
|
|
|
147
165
|
started.current = true;
|
|
148
166
|
const saved = readSaved(stateKey);
|
|
149
167
|
table.restore({
|
|
150
|
-
pageSize: saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
|
|
168
|
+
pageSize: !whole && saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
|
|
151
169
|
sort: saved?.sort !== undefined ? saved.sort : defaultSort,
|
|
152
170
|
hidden: saved?.hidden ?? columns.filter(column => column.hidden).map(column => column.key),
|
|
153
171
|
order: saved?.order,
|
|
@@ -173,6 +191,26 @@ export function DataTable(props) {
|
|
|
173
191
|
if (started.current && givenRows)
|
|
174
192
|
table.setRows(givenRows);
|
|
175
193
|
}, [table, givenRows]);
|
|
194
|
+
// Another table's settings behind the same table (a CRUD's scope keeps its own): read as on the first draw, before the
|
|
195
|
+
// rows its new list loads, and the settings on screen are not written under the new key (`restoredKey` is what says
|
|
196
|
+
// the store already holds that key's settings).
|
|
197
|
+
const restoredKey = useRef(stateKey);
|
|
198
|
+
useLayoutEffect(() => {
|
|
199
|
+
if (!started.current || restoredKey.current === stateKey)
|
|
200
|
+
return;
|
|
201
|
+
restoredKey.current = stateKey;
|
|
202
|
+
const saved = readSaved(stateKey);
|
|
203
|
+
table.restore({
|
|
204
|
+
pageSize: !whole && saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : openingPageSize(),
|
|
205
|
+
sort: saved?.sort !== undefined ? saved.sort : (defaultSort ?? null),
|
|
206
|
+
hidden: saved?.hidden ?? columns.filter(column => column.hidden).map(column => column.key),
|
|
207
|
+
order: saved?.order ?? [],
|
|
208
|
+
widths: saved?.widths ?? {},
|
|
209
|
+
pinned: saved?.pinned ?? {},
|
|
210
|
+
}, { notify: true });
|
|
211
|
+
setOpenSections(new Set(saved?.openSections ?? (saved?.pinsOpen ? [PINS_SECTION] : [])));
|
|
212
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
213
|
+
}, [table, stateKey]);
|
|
176
214
|
// Another list behind the same table: start over from the first page, with no search and no selection.
|
|
177
215
|
useLayoutEffect(() => {
|
|
178
216
|
if (loadedKey.current === loadKey)
|
|
@@ -183,20 +221,24 @@ export function DataTable(props) {
|
|
|
183
221
|
void table.reload({ reset: true });
|
|
184
222
|
}, [table, loadKey]);
|
|
185
223
|
useEffect(() => () => clearTimeout(searchTimer.current), []);
|
|
186
|
-
// Saved settings follow the user's choices.
|
|
224
|
+
// Saved settings follow the user's choices. What is written is the store's state now, never this draw's: the first
|
|
225
|
+
// draw, and the draw that took another key's settings, still hold the settings from before the restore, and writing
|
|
226
|
+
// those would put the opening defaults (or another key's settings) under the key.
|
|
187
227
|
const hiddenList = useMemo(() => [...state.hidden].sort(), [state.hidden]);
|
|
188
228
|
useEffect(() => {
|
|
189
229
|
if (!stateKey || !started.current)
|
|
190
230
|
return;
|
|
231
|
+
const now = table.getState();
|
|
191
232
|
writeSaved(stateKey, {
|
|
192
233
|
// Only a size the person picked here: one equal to the default is not kept, so a new default still reaches it.
|
|
193
|
-
pageSize:
|
|
194
|
-
sort:
|
|
195
|
-
hidden:
|
|
196
|
-
order:
|
|
197
|
-
widths: Object.keys(
|
|
198
|
-
pinned: Object.keys(
|
|
234
|
+
pageSize: now.query.pageSize === openingPageSize() ? undefined : now.query.pageSize,
|
|
235
|
+
sort: now.query.sort,
|
|
236
|
+
hidden: [...now.hidden].sort(),
|
|
237
|
+
order: now.order.length ? [...now.order] : undefined,
|
|
238
|
+
widths: Object.keys(now.widths).length ? now.widths : undefined,
|
|
239
|
+
pinned: Object.keys(now.pinned).length ? now.pinned : undefined,
|
|
199
240
|
});
|
|
241
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
200
242
|
}, [stateKey, state.query.pageSize, state.query.sort, hiddenList, state.order, state.widths, state.pinned]);
|
|
201
243
|
// A new default page size from the panel reaches an open table where no size was picked. Only a change counts: on the
|
|
202
244
|
// first effects the save above still holds the store's opening size and has not written the restored one yet, so a
|
|
@@ -207,7 +249,7 @@ export function DataTable(props) {
|
|
|
207
249
|
if (preferredSize.current === preferences.pageSize)
|
|
208
250
|
return;
|
|
209
251
|
preferredSize.current = preferences.pageSize;
|
|
210
|
-
if (!started.current || readSaved(stateKey)?.pageSize)
|
|
252
|
+
if (whole || !started.current || readSaved(stateKey)?.pageSize)
|
|
211
253
|
return;
|
|
212
254
|
table.setPageSize(openingPageSize());
|
|
213
255
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -233,7 +275,7 @@ export function DataTable(props) {
|
|
|
233
275
|
if (rowLabel)
|
|
234
276
|
return rowLabel(row);
|
|
235
277
|
const first = columns[0];
|
|
236
|
-
const text = first ? formatCellText(first, columnValue(first, row), cellOptions) : "";
|
|
278
|
+
const text = first ? formatCellText(first, columnValue(first, row), cellOptions, row) : "";
|
|
237
279
|
return text || `row ${table.keyOf(row)}`;
|
|
238
280
|
}, [rowLabel, columns, cellOptions, table]);
|
|
239
281
|
const actionsOf = usePerRow(rowActions);
|
|
@@ -260,6 +302,10 @@ export function DataTable(props) {
|
|
|
260
302
|
void result.finally(() => table.setBusy([key], false)).catch(() => undefined);
|
|
261
303
|
}
|
|
262
304
|
}, [table]);
|
|
305
|
+
// The picked row (`onPick`): a click on a row, or its radio button, picks it through the store, which follows it.
|
|
306
|
+
const pickRow = useCallback((row) => table.pick(row), [table]);
|
|
307
|
+
const pickName = `${tableId}pick`;
|
|
308
|
+
const pickedKey = state.picked ? table.keyOf(state.picked) : null;
|
|
263
309
|
const onSearch = (text) => {
|
|
264
310
|
clearTimeout(searchTimer.current);
|
|
265
311
|
if (searchDelay > 0 && text.trim())
|
|
@@ -280,9 +326,11 @@ export function DataTable(props) {
|
|
|
280
326
|
const headRef = useRef(null);
|
|
281
327
|
const colRefs = useMemo(() => new Map(), []);
|
|
282
328
|
const [offsets, setOffsets] = useState({});
|
|
329
|
+
// Pinned columns that scroll for now: on a narrow table the ones past `PINNED_SHARE` of its width give way.
|
|
330
|
+
const [released, setReleased] = useState(NONE_RELEASED);
|
|
283
331
|
const sized = Object.keys(state.widths).length > 0;
|
|
284
|
-
const views = useMemo(() => viewColumns(visible, { order: state.order, pinned: state.pinned, widths: state.widths, offsets, reorderable, resizable }), [visible, state.order, state.pinned, state.widths, offsets, reorderable, resizable]);
|
|
285
|
-
const indexOn = selectable || numbered;
|
|
332
|
+
const views = useMemo(() => viewColumns(visible, { order: state.order, pinned: state.pinned, widths: state.widths, offsets, released, reorderable, resizable }), [visible, state.order, state.pinned, state.widths, offsets, released, reorderable, resizable]);
|
|
333
|
+
const indexOn = selectable || numbered || pickable;
|
|
286
334
|
// The bands above the headings. With no group anywhere the header is the one row it always was.
|
|
287
335
|
const bands = useMemo(() => headBands(views), [views]);
|
|
288
336
|
const grouped = bands.some(band => band.group !== null);
|
|
@@ -327,8 +375,36 @@ export function DataTable(props) {
|
|
|
327
375
|
// The bar of the selected rows starts after the checkbox column, whatever width it took.
|
|
328
376
|
root?.style.setProperty("--nx-table-index-w", `${Math.round(left)}px`);
|
|
329
377
|
setOffsets(previous => (sameNumbers(previous, next) ? previous : next));
|
|
378
|
+
settlePins.current();
|
|
330
379
|
});
|
|
380
|
+
// Which pinned columns give way: measured from the headings after every render and whenever the table's width
|
|
381
|
+
// changes. Staying or scrolling changes no width, so it settles at once.
|
|
382
|
+
const settlePins = useRef(() => undefined);
|
|
383
|
+
settlePins.current = () => {
|
|
384
|
+
const scroller = scrollRef.current;
|
|
385
|
+
const cells = headLeafCells(headRef.current);
|
|
386
|
+
if (!scroller || cells.length === 0)
|
|
387
|
+
return;
|
|
388
|
+
const sides = new Map(views.map(view => [view.column.key, view.side]));
|
|
389
|
+
let frameWidth = 0;
|
|
390
|
+
const left = [];
|
|
391
|
+
const right = [];
|
|
392
|
+
for (const cell of cells) {
|
|
393
|
+
const key = cell.getAttribute("data-col");
|
|
394
|
+
const width = cell.getBoundingClientRect().width;
|
|
395
|
+
if (key === TABLE_INDEX_COLUMN || key === TABLE_ACTIONS_COLUMN)
|
|
396
|
+
frameWidth += width;
|
|
397
|
+
else if (key && sides.get(key) === "left")
|
|
398
|
+
left.push([key, width]);
|
|
399
|
+
else if (key && sides.get(key) === "right")
|
|
400
|
+
right.unshift([key, width]);
|
|
401
|
+
}
|
|
402
|
+
const next = left.length + right.length === 0 ? NONE_RELEASED : releasedPins(scroller.clientWidth, frameWidth, left, right);
|
|
403
|
+
setReleased(previous => (sameKeys(previous, next) ? previous : next));
|
|
404
|
+
};
|
|
331
405
|
// Shadows at the pinned columns while the others scroll under them: set on the frame, with no render.
|
|
406
|
+
// The width the rows can show goes on the frame too, so a message across a wide table (no rows, a hint, a failed
|
|
407
|
+
// load) stays inside what is on screen instead of centring on the whole scroll width.
|
|
332
408
|
useLayoutEffect(() => {
|
|
333
409
|
const scroller = scrollRef.current;
|
|
334
410
|
const frameElement = scroller?.parentElement;
|
|
@@ -339,9 +415,14 @@ export function DataTable(props) {
|
|
|
339
415
|
frameElement.toggleAttribute("data-scroll-start", left > 1);
|
|
340
416
|
frameElement.toggleAttribute("data-scroll-end", left < scroller.scrollWidth - scroller.clientWidth - 1);
|
|
341
417
|
};
|
|
342
|
-
|
|
418
|
+
const measure = () => {
|
|
419
|
+
frameElement.style.setProperty("--nx-table-view-w", `${scroller.clientWidth}px`);
|
|
420
|
+
update();
|
|
421
|
+
settlePins.current();
|
|
422
|
+
};
|
|
423
|
+
measure();
|
|
343
424
|
scroller.addEventListener("scroll", update, { passive: true });
|
|
344
|
-
const observer = new ResizeObserver(
|
|
425
|
+
const observer = new ResizeObserver(measure);
|
|
345
426
|
observer.observe(scroller);
|
|
346
427
|
if (scroller.firstElementChild)
|
|
347
428
|
observer.observe(scroller.firstElementChild);
|
|
@@ -467,6 +548,10 @@ export function DataTable(props) {
|
|
|
467
548
|
const barRef = useRef(null);
|
|
468
549
|
const reloadRef = useRef(null);
|
|
469
550
|
const [reloadHidden, setReloadHidden] = useState(false);
|
|
551
|
+
// An Add that cannot add yet (`add.disabled` with a `description`) says why on a press, under it.
|
|
552
|
+
const addNotice = useReadOnlyNotice(Boolean(add?.disabled && add.description), barRef, add?.description, { anchor: target => target.closest(".nx-btn"), typing: false, lock: "label" });
|
|
553
|
+
// The spreadsheet's other ways to add sit under the Add's arrow only while it can add.
|
|
554
|
+
const addable = Boolean(add && !add.disabled);
|
|
470
555
|
const addPart = Boolean(add || actions);
|
|
471
556
|
const listsPart = Boolean(toolbar);
|
|
472
557
|
useLayoutEffect(() => {
|
|
@@ -500,23 +585,35 @@ export function DataTable(props) {
|
|
|
500
585
|
void table.reload({ quiet: true });
|
|
501
586
|
};
|
|
502
587
|
// A spreadsheet: the template to fill, the file to bring back, and an export the server writes from what is on
|
|
503
|
-
// screen. With an Add, they are other ways to add, under its arrow; without one, they are in the options menu.
|
|
588
|
+
// screen. With an Add, they are other ways to add, under its arrow; without one, they are in the options menu. Each is
|
|
589
|
+
// offered only to a person the app's permissions let call every endpoint it needs, the settings included.
|
|
590
|
+
const sheet = excel?.endpoints;
|
|
591
|
+
const mayUseSheet = (...endpoints) => [sheet.settings, ...endpoints].every(endpoint => canCall(endpoint));
|
|
504
592
|
const sheetItems = excel
|
|
505
593
|
? [
|
|
506
|
-
{ id: "excel-template", label: "Download template", description: `A spreadsheet to fill in`, icon: _jsx(InlineIcon, { icon: iconDownload }),
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
594
|
+
...(mayUseSheet(sheet.template) ? [{ id: "excel-template", label: "Download template", description: `A spreadsheet to fill in`, icon: _jsx(InlineIcon, { icon: iconDownload }), onSelect: () => void downloadExcelTemplate(excel.endpoints, { params: excel.params }) }] : []),
|
|
595
|
+
...(mayUseSheet(sheet.verify, sheet.import)
|
|
596
|
+
? [
|
|
597
|
+
{ id: "excel-import", label: "Import from a spreadsheet", icon: _jsx(InlineIcon, { icon: iconFileImport }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported }) },
|
|
598
|
+
// A bulk insert is an Add of many rows, so it is offered only beside an Add that can add: a CRUD shows its Add
|
|
599
|
+
// only to a person who may create, and nobody types fifty rows to be refused at the end.
|
|
600
|
+
...(excel.bulk === false || !addable
|
|
601
|
+
? []
|
|
602
|
+
: [{ id: "excel-bulk", label: "Bulk insert", description: "Type the rows here, with no file", icon: _jsx(InlineIcon, { icon: iconRowInsertBottom }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported, mode: "bulk" }) }]),
|
|
603
|
+
]
|
|
604
|
+
: []),
|
|
605
|
+
...(sheet.export && mayUseSheet(sheet.export)
|
|
514
606
|
? [{ id: "excel-export", label: "Export to Excel", description: "Everything the search and filters leave, from the server", icon: _jsx(InlineIcon, { icon: iconFileExport }), onSelect: () => void exportExcel(excel.endpoints, exportQuery()) }]
|
|
515
607
|
: []),
|
|
516
|
-
]
|
|
608
|
+
].map((item, index) => (index === 0 ? { ...item, separator: true } : item))
|
|
517
609
|
: [];
|
|
518
610
|
// In the bar it is `block`: as wide as its own label there, and the whole line once the Add section takes one.
|
|
519
|
-
const addButton = (block = false) => add && (_jsx(Button, { variant: "primary", size: "sm", block: block, icon: _jsx(InlineIcon, { icon: iconPlus }),
|
|
611
|
+
const addButton = (block = false) => add && (_jsx(Button, { variant: "primary", size: "sm", block: block, icon: _jsx(InlineIcon, { icon: iconPlus }), "aria-disabled": add.disabled || undefined, title: add.description, onKeyDown: addNotice.onKeyDown, onClick: event => {
|
|
612
|
+
if (!add.disabled)
|
|
613
|
+
add.onClick();
|
|
614
|
+
else if (!add.onDisabledPress?.())
|
|
615
|
+
addNotice.show(event.currentTarget);
|
|
616
|
+
}, children: add.label }));
|
|
520
617
|
const optionItems = [
|
|
521
618
|
...(filterInputs?.length
|
|
522
619
|
? [{ id: "filters", label: "Filters", description: filtersOn.join(", ") || undefined, icon: _jsx(InlineIcon, { icon: iconFilter }), onSelect: () => filtersRef.current?.open() }]
|
|
@@ -531,7 +628,7 @@ export function DataTable(props) {
|
|
|
531
628
|
: []),
|
|
532
629
|
// The sheet's own Import replaces the page's `onImport` hook.
|
|
533
630
|
...(excel
|
|
534
|
-
?
|
|
631
|
+
? addable
|
|
535
632
|
? []
|
|
536
633
|
: sheetItems
|
|
537
634
|
: onImport
|
|
@@ -545,7 +642,9 @@ export function DataTable(props) {
|
|
|
545
642
|
...(reloadable && reloadHidden ? [{ id: "reload", label: "Reload", icon: _jsx(InlineIcon, { icon: iconRefresh }), separator: true, onSelect: reload }] : []),
|
|
546
643
|
];
|
|
547
644
|
const optionEntries = menuEntries(optionItems);
|
|
548
|
-
|
|
645
|
+
// Rows per page: inside the search box, in the bar without one, or beside the pager (`pager`); none for a table read whole.
|
|
646
|
+
const pageSizePicker = whole ? null : _jsx(TablePageSize, { pageSize: query.pageSize, pageSizes: pageSizes, compact: searchable && pager === "bar", onChange: value => table.setPageSize(value) });
|
|
647
|
+
const barPageSize = pager === "bar" ? pageSizePicker : null;
|
|
549
648
|
let body;
|
|
550
649
|
if (status === "loading") {
|
|
551
650
|
body = _jsx(TableSkeletonRows, { count: Math.min(query.pageSize, 8), cells: cellCount, frame: frame, sized: sized });
|
|
@@ -576,7 +675,7 @@ export function DataTable(props) {
|
|
|
576
675
|
body = rows.map((row, index) => {
|
|
577
676
|
const key = pageKeys[index];
|
|
578
677
|
const selected = state.selected.has(key);
|
|
579
|
-
return (_jsx(TableRowView, { row: row, rowKey: key, number: state.offset + index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: rowMarks?.(row), frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick }, key || index));
|
|
678
|
+
return (_jsx(TableRowView, { row: row, rowKey: key, number: state.offset + index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: rowMarks?.(row), frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick, onPick: pickable ? pickRow : undefined, picked: key === pickedKey, pickName: pickable ? pickName : undefined }, key || index));
|
|
580
679
|
});
|
|
581
680
|
}
|
|
582
681
|
/* The sections above the page: the records pinned, the favourites, each folded open or shut, remembered on this device */
|
|
@@ -598,7 +697,9 @@ export function DataTable(props) {
|
|
|
598
697
|
const key = table.keyOf(row);
|
|
599
698
|
const selected = state.selected.has(key);
|
|
600
699
|
const marks = rowMarks?.(row);
|
|
601
|
-
return (_jsx(TableRowView, { row: row, rowKey: key, number: index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: marks?.length ? marks : own, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick
|
|
700
|
+
return (_jsx(TableRowView, { row: row, rowKey: key, number: index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, marks: marks?.length ? marks : own, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick,
|
|
701
|
+
// A click picks a section's row too; its radio button is the one in the list below, where the arrow keys move.
|
|
702
|
+
onPick: pickable ? pickRow : undefined, picked: key === pickedKey }, key || index));
|
|
602
703
|
});
|
|
603
704
|
};
|
|
604
705
|
const selectedRows = selecting ? [...state.selected.values()] : [];
|
|
@@ -619,8 +720,8 @@ export function DataTable(props) {
|
|
|
619
720
|
const mass = selectedRows.length ? (typeof massActions === "function" ? massActions(selectedRows) : (massActions ?? [])) : [];
|
|
620
721
|
// A column's own heading, wherever it stands: across both rows when no band is above it, in the row under its band when one is.
|
|
621
722
|
const headingCell = (view, rowSpan) => (_jsx(HeaderCell, { view: view, sized: sized, rowSpan: rowSpan, sort: query.sort, disabled: status !== "ready" || rows.length < 2, covered: selectedRows.length > 0, canPin: canPinColumn(view.column, { pinnable }), canHide: view.column.hideable !== false && visible.length > 1, neighbours: moveTargets(views, view.column.key), onSort: direction => table.setSort(direction ? { key: view.column.key, direction } : null), onPin: side => table.setPinned(view.column.key, side), onMove: beforeKey => table.moveColumn(view.column.key, beforeKey), onFit: () => fitColumn(view.column.key), onHide: () => table.setHidden(view.column.key, true), onPointerDown: event => onColumnPointerDown(event, view), onResizerDown: event => onResizerPointerDown(event, view.column.key) }, view.column.key));
|
|
622
|
-
return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-table", `nx-table--${size}`, fill && "nx-table--fill", className), style: style, "data-head-rows": headRows, "data-status": status, "data-refreshing": refreshing || undefined, "data-selecting": selecting || undefined, "data-sized": sized || undefined, children: [_jsxs("div", { ref: barRef, className: "nx-table__bar", children: [addPart && (_jsxs("div", { className: "nx-table__bar-add", children: [add &&
|
|
623
|
-
(sheetItems.length > 0 ? (_jsx(SplitButton, { fixed: true, block: true, size: "sm", menuLabel: "More ways to add", options: [{ value: "add", label: add.label, icon: _jsx(InlineIcon, { icon: iconPlus }) }, ...sheetItems.map(item => ({ value: item.id, label: item.label, icon: item.icon, description: item.description }))], onAction: value => (value === "add" ? add.onClick() : sheetItems.find(item => item.id === value)?.onSelect()) })) : (addButton(true))), actions] })), toolbar && _jsx("div", { className: "nx-table__bar-start", children: toolbar }), _jsxs("div", { className: "nx-table__bar-end", children: [filterInputs?.length ? (_jsx(TableFiltersPanel, { ref: filtersRef, anchor: optionsRef, inputs: filterInputs, value: query.filters, delay: searchDelay, matches: state.matches, total: state.total, onChange: values => table.setFilters(values) })) : null, hasColumnsPanel && (_jsx(TableColumnsPanel, { ref: columnsRef, anchor: optionsRef, columns: ordered, hidden: state.hidden, pinned: state.pinned, reorderable: reorderable, size: "sm", changed: table.hasColumnChanges(), onToggle: (key, hide) => table.setHidden(key, hide), onOrder: keys => table.setOrder(keys), onReset: () => table.resetColumns() })), reloadable && (_jsx(Button, { ref: reloadRef, size: "sm", variant: "ghost", className: "nx-table__reload", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": "Reload", title: "Reload", disabled: status === "loading", onClick: reload })), optionEntries.length > 0 && (_jsx(Menu, { items: optionEntries, size: "sm", align: "end", "aria-label": "More options", onSelect: value => optionItems.find(action => action.id === value)?.onSelect(), children: _jsx(Button, { ref: optionsRef, size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), dot: filtersOn.length > 0 && "primary", "aria-label": "More options", title: filtersOn.length ? `More options · ${filtersOn.length} ${filtersOn.length === 1 ? "filter" : "filters"} on` : "More options", "data-table-options": true }) })), !searchable &&
|
|
723
|
+
return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-table", `nx-table--${size}`, fill && "nx-table--fill", className), style: style, "data-head-rows": headRows, "data-status": status, "data-refreshing": refreshing || undefined, "data-selecting": selecting || undefined, "data-sized": sized || undefined, children: [caption && _jsx("div", { className: "nx-table__caption", children: caption }), _jsxs("div", { ref: barRef, className: "nx-table__bar", children: [addPart && (_jsxs("div", { className: "nx-table__bar-add", children: [add &&
|
|
724
|
+
(addable && sheetItems.length > 0 ? (_jsx(SplitButton, { fixed: true, block: true, size: "sm", menuLabel: "More ways to add", options: [{ value: "add", label: add.label, icon: _jsx(InlineIcon, { icon: iconPlus }) }, ...sheetItems.map(item => ({ value: item.id, label: item.label, icon: item.icon, description: item.description }))], onAction: value => (value === "add" ? add.onClick() : sheetItems.find(item => item.id === value)?.onSelect()) })) : (addButton(true))), actions] })), toolbar && _jsx("div", { className: "nx-table__bar-start", children: toolbar }), _jsxs("div", { className: "nx-table__bar-end", children: [filterInputs?.length ? (_jsx(TableFiltersPanel, { ref: filtersRef, anchor: optionsRef, inputs: filterInputs, value: query.filters, delay: searchDelay, matches: state.matches, total: state.total, onChange: values => table.setFilters(values) })) : null, hasColumnsPanel && (_jsx(TableColumnsPanel, { ref: columnsRef, anchor: optionsRef, columns: ordered, hidden: state.hidden, pinned: state.pinned, reorderable: reorderable, size: "sm", changed: table.hasColumnChanges(), onToggle: (key, hide) => table.setHidden(key, hide), onOrder: keys => table.setOrder(keys), onReset: () => table.resetColumns() })), reloadable && (_jsx(Button, { ref: reloadRef, size: "sm", variant: "ghost", className: "nx-table__reload", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": "Reload", title: "Reload", disabled: status === "loading", onClick: reload })), optionEntries.length > 0 && (_jsx(Menu, { items: optionEntries, size: "sm", align: "end", "aria-label": "More options", onSelect: value => optionItems.find(action => action.id === value)?.onSelect(), children: _jsx(Button, { ref: optionsRef, size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), dot: filtersOn.length > 0 && "primary", "aria-label": "More options", title: filtersOn.length ? `More options · ${filtersOn.length} ${filtersOn.length === 1 ? "filter" : "filters"} on` : "More options", "data-table-options": true }) })), !searchable && barPageSize] }), searchable && (_jsx(TextBox, { ref: searchRef, type: "search", size: "sm", className: "nx-table__search", actionsStart: barPageSize, icon: _jsx(InlineIcon, { icon: iconSearch }), placeholder: searchPlaceholder, "aria-label": searchPlaceholder.replace(/…$/, ""), "aria-controls": tableId, defaultValue: query.search, clearable: true, onValueChange: onSearch, onKeyDown: event => {
|
|
624
725
|
if (event.key === "Enter") {
|
|
625
726
|
clearTimeout(searchTimer.current);
|
|
626
727
|
table.setSearch(event.currentTarget.value);
|
|
@@ -632,13 +733,13 @@ export function DataTable(props) {
|
|
|
632
733
|
} }) })) })), bands.map(band => band.group === null ? (headingCell(band.columns[0], headRows)) : (_jsx(GroupHeading, { band: band }, `group:${band.group}:${band.columns[0].column.key}`))), sized && _jsx("td", { className: "nx-table__heading nx-table__cell--filler", rowSpan: headRows }), frame.actions && (_jsx("th", { scope: "col", className: "nx-table__heading nx-table__cell--actions", rowSpan: headRows, "data-col": TABLE_ACTIONS_COLUMN, "data-pin": "right", "data-pin-edge": frame.actions.edge || undefined, style: { right: frame.actions.offset }, children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }), grouped && _jsx("tr", { className: "nx-table__head nx-table__head--leaves", children: bands.filter(band => band.group !== null).flatMap(band => band.columns.map(view => headingCell(view))) })] }), sections.map(section => {
|
|
633
734
|
const open = openSections.has(section.key);
|
|
634
735
|
return (_jsxs("tbody", { className: "nx-table__section", "data-section": section.key, "data-open": open || undefined, children: [_jsx(TableSectionHead, { span: span, label: section.label, icon: section.icon, count: section.rows.length, max: section.max, loading: Boolean(section.loading), open: open, onToggle: () => toggleSection(section.key) }), open && sectionRows(section)] }, section.key));
|
|
635
|
-
}), _jsx("tbody", { children: body })] }) })] }), _jsx(TableFooter, { state: state, disabled: refreshing, onPage: page => table.setPage(page) })] }));
|
|
736
|
+
}), _jsx("tbody", { children: body })] }) })] }), _jsx(TableFooter, { state: state, disabled: refreshing, whole: whole, pageSize: pager === "footer" ? pageSizePicker : null, onPage: page => table.setPage(page) }), addNotice.notice] }));
|
|
636
737
|
}
|
|
637
738
|
/** Where "Move left" and "Move right" put a column: the key it goes before, or null for the end of its side. */
|
|
638
739
|
function moveTargets(views, key) {
|
|
639
740
|
// A column moves among the columns on its own side, past the ones that stay put.
|
|
640
|
-
const
|
|
641
|
-
const group = views.filter(view => view.
|
|
741
|
+
const side = views.find(view => view.column.key === key)?.side ?? null;
|
|
742
|
+
const group = views.filter(view => view.side === side && view.movable);
|
|
642
743
|
const at = group.findIndex(view => view.column.key === key);
|
|
643
744
|
if (at < 0)
|
|
644
745
|
return { left: undefined, right: undefined };
|
|
@@ -679,11 +780,12 @@ function HeaderCell({ view, sized, rowSpan, sort, disabled, covered, canPin, can
|
|
|
679
780
|
if (canPin) {
|
|
680
781
|
if (entries.length)
|
|
681
782
|
entries.push({ separator: true });
|
|
682
|
-
|
|
783
|
+
// The side chosen, also while a narrow table lets it scroll.
|
|
784
|
+
if (view.side !== "left")
|
|
683
785
|
entries.push({ value: "pin-left", label: "Pin left", icon: _jsx(InlineIcon, { icon: iconArrowBarToLeft }) });
|
|
684
|
-
if (view.
|
|
786
|
+
if (view.side !== "right")
|
|
685
787
|
entries.push({ value: "pin-right", label: "Pin right", icon: _jsx(InlineIcon, { icon: iconArrowBarToRight }) });
|
|
686
|
-
if (view.
|
|
788
|
+
if (view.side)
|
|
687
789
|
entries.push({ value: "unpin", label: "Unpin", icon: _jsx(InlineIcon, { icon: iconPinnedOff }) });
|
|
688
790
|
}
|
|
689
791
|
if (view.movable && (neighbours.left !== undefined || neighbours.right !== undefined)) {
|
|
@@ -721,7 +823,7 @@ function HeaderCell({ view, sized, rowSpan, sort, disabled, covered, canPin, can
|
|
|
721
823
|
onHide();
|
|
722
824
|
}
|
|
723
825
|
const label = _jsx("span", { className: "nx-table__heading-label", children: column.label });
|
|
724
|
-
return (_jsxs("th", { scope: "col", className: cx("nx-table__heading", `nx-table__cell--${align}`), rowSpan: rowSpan, "data-col": column.key, "data-pin": view.pin ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, "data-movable": view.movable || undefined, style: columnStyle(view, sized), "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined, onPointerDown: onPointerDown, children: [_jsxs("div", { className: "nx-table__heading-inner", children: [sortable ? (_jsxs("button", { type: "button", className: "nx-table__sort", "data-sorted": direction ?? undefined, disabled: disabled && !direction, tabIndex: covered ? -1 : undefined, onClick: () => onSort(direction === "asc" ? "desc" : direction === "desc" ? null : "asc"), title: `Sort by ${column.label}`, children: [label, _jsx(InlineIcon, { icon: direction === "asc" ? iconArrowUp : direction === "desc" ? iconArrowDown : iconArrowsSort, className: "nx-table__sort-icon" })] })) : (label), entries.length > 0 && (_jsx(Menu, { items: entries, size: "sm", align: "end", "aria-label": `${column.label}: column`, onSelect: choose, children: _jsx(Button, { variant: "ghost", size: "sm", className: "nx-table__col-menu", icon: _jsx(InlineIcon, { icon: iconChevronDown }), tabIndex: covered ? -1 : undefined, "aria-label": `${column.label}: column options`, title: "Column options" }) }))] }), view.resizable && (_jsx("span", { className: "nx-table__resizer", "aria-hidden": "true", "data-resize": column.key, title: `Drag to resize ${column.label}, double-click to fit`, onPointerDown: onResizerDown, onDoubleClick: onFit }))] }));
|
|
826
|
+
return (_jsxs("th", { scope: "col", className: cx("nx-table__heading", `nx-table__cell--${align}`), rowSpan: rowSpan, "data-col": column.key, "data-pin": view.pin ?? undefined, "data-pin-side": view.side ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, "data-movable": view.movable || undefined, style: columnStyle(view, sized), "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined, onPointerDown: onPointerDown, children: [_jsxs("div", { className: "nx-table__heading-inner", children: [sortable ? (_jsxs("button", { type: "button", className: "nx-table__sort", "data-sorted": direction ?? undefined, disabled: disabled && !direction, tabIndex: covered ? -1 : undefined, onClick: () => onSort(direction === "asc" ? "desc" : direction === "desc" ? null : "asc"), title: `Sort by ${column.label}`, children: [label, _jsx(InlineIcon, { icon: direction === "asc" ? iconArrowUp : direction === "desc" ? iconArrowDown : iconArrowsSort, className: "nx-table__sort-icon" })] })) : (label), entries.length > 0 && (_jsx(Menu, { items: entries, size: "sm", align: "end", "aria-label": `${column.label}: column`, onSelect: choose, children: _jsx(Button, { variant: "ghost", size: "sm", className: "nx-table__col-menu", icon: _jsx(InlineIcon, { icon: iconChevronDown }), tabIndex: covered ? -1 : undefined, "aria-label": `${column.label}: column options`, title: "Column options" }) }))] }), view.resizable && (_jsx("span", { className: "nx-table__resizer", "aria-hidden": "true", "data-resize": column.key, title: `Drag to resize ${column.label}, double-click to fit`, onPointerDown: onResizerDown, onDoubleClick: onFit }))] }));
|
|
725
827
|
}
|
|
726
828
|
/** The steps a bar is squeezed into when its parts do not fit, each one keeping the steps before it. */
|
|
727
829
|
const BAR_SQUEEZES = ["search", "search reload", "search reload add"];
|