nexus-shared 4.0.0 → 4.1.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 +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { iconAlertCircle, iconArrowBackUp, iconPlus, InlineIcon } from "nexus-icons";
|
|
4
|
-
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
4
|
+
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
6
|
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
7
7
|
import { browserInputMemory, FORM_SAVED_EVENT } from "../../Helpers/InputMemory.js";
|
|
8
|
+
import { narrowingValue, narrowInputParamsBy } from "../../Helpers/InputNarrowing.js";
|
|
8
9
|
import { inputFieldName } from "../../Helpers/InputParamsHelpers.js";
|
|
9
10
|
import { RowsStore } from "../../Helpers/RowsStore.js";
|
|
10
11
|
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
11
12
|
import { Button } from "../Documents/Button.js";
|
|
13
|
+
import { ComputedInput } from "./ComputedInput.js";
|
|
12
14
|
import { hasContent } from "./Field.js";
|
|
13
15
|
import { isFirstInvalid } from "./InputField.js";
|
|
14
16
|
import { renderInput } from "./InputRenderer.js";
|
|
@@ -194,9 +196,43 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
194
196
|
const row = store.row(key);
|
|
195
197
|
return row ? ask(row, name, index) : undefined;
|
|
196
198
|
}, [store, rowLock]);
|
|
199
|
+
// The form's values the rows follow (`narrowedBy` `from: "form"`): when one changes, every row draws again, narrowed afresh.
|
|
200
|
+
const formKey = formValuesKey(props.formValues);
|
|
201
|
+
const shownFormKey = useRef(formKey);
|
|
202
|
+
useLayoutEffect(() => {
|
|
203
|
+
if (shownFormKey.current === formKey)
|
|
204
|
+
return;
|
|
205
|
+
shownFormKey.current = formKey;
|
|
206
|
+
store.redrawRows();
|
|
207
|
+
}, [store, formKey]);
|
|
208
|
+
// Row inputs worked out from others (`compute`) read the rows' `computeWith` as they draw: a new one draws every row.
|
|
209
|
+
const computeWith = useCallback(() => latest.current.props.computeWith, []);
|
|
210
|
+
const shownComputeWith = useRef(props.computeWith);
|
|
211
|
+
useLayoutEffect(() => {
|
|
212
|
+
if (Object.is(shownComputeWith.current, props.computeWith))
|
|
213
|
+
return;
|
|
214
|
+
shownComputeWith.current = props.computeWith;
|
|
215
|
+
if (inputs.some(params => params.compute))
|
|
216
|
+
store.redrawRows();
|
|
217
|
+
}, [store, inputs, props.computeWith]);
|
|
197
218
|
// A cell's own params, merged once per answer: the same answer gives the same object, so the cell does not draw again.
|
|
198
219
|
const merged = useRef(new WeakMap());
|
|
199
220
|
const cellParams = useCallback((key, params, index) => {
|
|
221
|
+
const own = ownCellParams(key, params, index);
|
|
222
|
+
// Options narrowed by the row's own values and the form's (`narrowedBy`): read as the row draws, which it does when
|
|
223
|
+
// one of them changes.
|
|
224
|
+
if (!params.narrowedBy)
|
|
225
|
+
return own;
|
|
226
|
+
const row = store.row(key);
|
|
227
|
+
const outer = latest.current.props.formValues;
|
|
228
|
+
return narrowInputParamsBy(own, narrowing => {
|
|
229
|
+
if (narrowing.from === "form")
|
|
230
|
+
return outer?.[narrowing.name] ?? { value: undefined };
|
|
231
|
+
return { value: row?.[narrowing.name], label: latest.current.props.inputs.find(input => input.name === narrowing.name)?.label };
|
|
232
|
+
});
|
|
233
|
+
}, [store]);
|
|
234
|
+
/** The column's params, or the row's own over them (`cellParams`). */
|
|
235
|
+
function ownCellParams(key, params, index) {
|
|
200
236
|
const ask = latest.current.props.cellParams;
|
|
201
237
|
if (!ask)
|
|
202
238
|
return params;
|
|
@@ -216,8 +252,14 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
216
252
|
byColumn.set(params, result);
|
|
217
253
|
}
|
|
218
254
|
return result;
|
|
219
|
-
}
|
|
220
|
-
return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams };
|
|
255
|
+
}
|
|
256
|
+
return { store, snapshot, proxyRef, onInvalid, canAdd, actions, cellLock, rowLock, cellParams, computeWith };
|
|
257
|
+
}
|
|
258
|
+
/** The form values rows follow, as text that changes only when one of them does (a new object with the same values does not). */
|
|
259
|
+
function formValuesKey(values) {
|
|
260
|
+
if (!values)
|
|
261
|
+
return "";
|
|
262
|
+
return JSON.stringify(Object.keys(values).sort().map(name => [name, narrowingValue(values[name]?.value)]));
|
|
221
263
|
}
|
|
222
264
|
function addButtonOf(proxy) {
|
|
223
265
|
return proxy?.closest(".nx-rows")?.querySelector(":scope > .nx-rows__footer .nx-rows__add:not(:disabled)") ?? null;
|
|
@@ -243,17 +285,23 @@ export function rowName(store, index) {
|
|
|
243
285
|
export function labelText(params) {
|
|
244
286
|
return typeof params.label === "string" ? params.label : (inputFieldName(params) ?? params.name);
|
|
245
287
|
}
|
|
288
|
+
/** A cell's control id: the rows' id, the row's key, and the input's name. */
|
|
289
|
+
const cellId = (idPrefix, rowKey, name) => `${idPrefix}-${rowKey.replace(/[^A-Za-z0-9_-]/g, "_")}-${name}`;
|
|
246
290
|
/**
|
|
247
291
|
* One input of one row. It starts from the store's value when it mounts and writes every change back, so typing renders
|
|
248
292
|
* nothing; a value set from code mounts it again (its key holds the value's revision). In a table the column header is
|
|
249
293
|
* the label, so the input is named "City, row 2" for screen readers and its messages still say "City".
|
|
250
294
|
*/
|
|
251
|
-
export const RowCell = memo(function RowCell(
|
|
295
|
+
export const RowCell = memo(function RowCell(props) {
|
|
296
|
+
const { store, rowKey, params, index, error, layout, idPrefix, size, design, readOnly, disabled, lock } = props;
|
|
252
297
|
const [initial] = useState(() => store.get(rowKey, params.name));
|
|
253
298
|
const onValueChange = useCallback((next) => store.set(rowKey, params.name, next, "input"), [store, rowKey, params.name]);
|
|
299
|
+
// Worked out from the row's other inputs: no value of its own, drawn from the row as it is now.
|
|
300
|
+
if (params.compute)
|
|
301
|
+
return _jsx(RowComputedCell, { ...props });
|
|
254
302
|
const needsName = params.type === EInputTypes.RadioGroup || params.type === EInputTypes.CheckBoxGroup;
|
|
255
303
|
const overrides = {
|
|
256
|
-
id:
|
|
304
|
+
id: cellId(idPrefix, rowKey, params.name),
|
|
257
305
|
// Nothing goes into a parent form by itself: the rows go in whole, under the rows input's name.
|
|
258
306
|
name: needsName ? `${ROW_INPUT_NAME}${idPrefix}:${rowKey}:${params.name}` : undefined,
|
|
259
307
|
defaultValue: initial,
|
|
@@ -285,6 +333,31 @@ export const RowCell = memo(function RowCell({ store, rowKey, params, index, err
|
|
|
285
333
|
}
|
|
286
334
|
return renderInput(params, overrides);
|
|
287
335
|
});
|
|
336
|
+
/**
|
|
337
|
+
* A row input worked out from the row's others (`compute`): `ComputedInput` reading the row and the rows' `computeWith`,
|
|
338
|
+
* drawn again whenever the row changes, and writing a typed-in figure's answer as the person's change of the row
|
|
339
|
+
* (`RowsStore.setFromPerson`). In a table it has no label and no line under it: the note goes under the rows.
|
|
340
|
+
*/
|
|
341
|
+
function RowComputedCell({ store, rowKey, params, index, layout, idPrefix, size, design, readOnly, disabled, lock, cells }) {
|
|
342
|
+
useRowVersion(store, rowKey);
|
|
343
|
+
const at = useRef(index);
|
|
344
|
+
at.current = index;
|
|
345
|
+
const read = useCallback(() => (store.row(rowKey) ?? {}), [store, rowKey]);
|
|
346
|
+
const write = useCallback((values) => void store.setFromPerson(rowKey, values), [store, rowKey]);
|
|
347
|
+
// The row's other inputs as this row has them: its own params (`cellParams`), and read-only where the cell is locked.
|
|
348
|
+
const inputParams = useCallback((name) => {
|
|
349
|
+
const column = store.inputs.find(params => params.name === name);
|
|
350
|
+
if (!column || !cells)
|
|
351
|
+
return column;
|
|
352
|
+
const own = cells.cellParams(rowKey, column, at.current);
|
|
353
|
+
return cells.cellLock(rowKey, name, at.current) ? { ...own, readOnly: true } : own;
|
|
354
|
+
}, [store, rowKey, cells]);
|
|
355
|
+
const sourceId = useCallback((name) => cellId(idPrefix, rowKey, name), [idPrefix, rowKey]);
|
|
356
|
+
const tabular = layout === "tabular";
|
|
357
|
+
const say = cells?.onNote;
|
|
358
|
+
const onNote = useMemo(() => (tabular && say ? (note) => say(rowKey, params.name, note) : undefined), [tabular, say, rowKey, params.name]);
|
|
359
|
+
return (_jsx(ComputedInput, { params: params, id: cellId(idPrefix, rowKey, params.name), read: read, withValue: cells?.computeWith?.(), write: write, inputParams: inputParams, sourceId: sourceId, size: params.size ?? size, design: design, readOnly: readOnly || Boolean(lock), readOnlyMessage: typeof lock === "string" ? lock : undefined, disabled: disabled, cell: tabular ? { "aria-label": `${labelText(params)}, ${rowName(store, index)}` } : undefined, onNote: onNote }));
|
|
360
|
+
}
|
|
288
361
|
/** The legend of the rows, with the required mark and, when read-only, a lock. */
|
|
289
362
|
export function RowsLegend({ id, label, required, lock }) {
|
|
290
363
|
if (!hasContent(label))
|
|
@@ -84,7 +84,7 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
|
|
|
84
84
|
return;
|
|
85
85
|
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
86
86
|
}
|
|
87
|
-
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
|
|
87
|
+
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index), cells: context }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
|
|
88
88
|
});
|
|
89
89
|
/**
|
|
90
90
|
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
@@ -135,6 +135,15 @@ export function TabularForm(props) {
|
|
|
135
135
|
const pageRows = Math.ceil((typeof maxHeight === "number" ? maxHeight : 600) / rowHeight);
|
|
136
136
|
const [range, setRange] = useState({ start: 0, end: pageRows + OVERSCAN });
|
|
137
137
|
const [status, setStatus] = useState(null);
|
|
138
|
+
/** The note of the figure typed into a worked-out cell last (`compute.solve`), until it no longer applies. */
|
|
139
|
+
const [note, setNote] = useState(null);
|
|
140
|
+
const onNote = useCallback((key, name, next) => {
|
|
141
|
+
setNote(current => {
|
|
142
|
+
if (next)
|
|
143
|
+
return current && current.key === key && current.name === name && current.text === next.text && current.refused === next.refused ? current : { ...next, key, name };
|
|
144
|
+
return current && current.key === key && current.name === name ? null : current;
|
|
145
|
+
});
|
|
146
|
+
}, []);
|
|
138
147
|
const pendingRef = useRef(null);
|
|
139
148
|
const start = Math.min(range.start, count);
|
|
140
149
|
const end = Math.min(count, Math.max(range.end, start + 1));
|
|
@@ -311,7 +320,7 @@ export function TabularForm(props) {
|
|
|
311
320
|
layoutRef.current?.focusRow(key, name);
|
|
312
321
|
}
|
|
313
322
|
}
|
|
314
|
-
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked]);
|
|
323
|
+
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked, computeWith: rows.computeWith, onNote }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked, rows.computeWith, onNote]);
|
|
315
324
|
/**
|
|
316
325
|
* Which columns are generated for **every** row on the page. The lock is then said once, on the column header,
|
|
317
326
|
* instead of on each of its cells: eighteen locks down one column is eighteen times the ink for one fact. The cells
|
|
@@ -369,6 +378,10 @@ export function TabularForm(props) {
|
|
|
369
378
|
const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, actionsWidth ?? "0px"].join(" + ")})`;
|
|
370
379
|
const tableStyle = { "--nx-tabular-columns": template, "--nx-tabular-num": numbered ? numberWidth : "0px", minWidth };
|
|
371
380
|
const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
|
|
381
|
+
// A typed-in figure's note, named with its row, says more than a hint; a message of the cell in focus says more still.
|
|
382
|
+
const noteRow = note ? store.indexOf(note.key) : -1;
|
|
383
|
+
const said = note && noteRow >= 0 ? { text: `${store.names.title} ${noteRow + 1}: ${note.text}`, error: note.refused } : null;
|
|
384
|
+
const footerStatus = status?.error ? status : (said ?? status);
|
|
372
385
|
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "data-actions-at": atEnd ? "end" : undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
|
|
373
386
|
// A focused cell scrolls clear of the actions fixed at the end, as it does of the number at the start.
|
|
374
387
|
style: { maxHeight: viewportMax ?? cssLength(maxHeight), scrollPaddingRight: actionsWidth ?? undefined }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
|
|
@@ -376,5 +389,5 @@ export function TabularForm(props) {
|
|
|
376
389
|
const locked = lockedColumns.get(params.name);
|
|
377
390
|
const reason = typeof locked === "string" ? locked : locked ? "Read-only" : undefined;
|
|
378
391
|
return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: reason ?? (typeof columnHint === "string" ? columnHint : undefined), "data-nx-locked": locked ? "" : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && !locked && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), locked && _jsx(InlineIcon, { icon: iconLock, className: "nx-tabular__head-lock", "aria-hidden": "true" })] }, params.name));
|
|
379
|
-
}), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status:
|
|
392
|
+
}), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: footerStatus, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
380
393
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, type Ref } from "react";
|
|
2
|
-
import { type TreeChangeRules, type TreeChildrenPage, type TreeChildrenRequest, type TreeItem, type TreeItemChange, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
|
|
2
|
+
import { type TreeChangeRules, type TreeChildrenPage, type TreeChildrenRequest, type TreeFilter, type TreeItem, type TreeItemChange, type TreeValue, type TreeValueMode } from "../../Helpers/TreeStore.ts";
|
|
3
3
|
import type { TableRowAction } from "../../Interfaces/TableInterfaces.ts";
|
|
4
4
|
import type { Size } from "../../Interfaces/TypeInterfaces.ts";
|
|
5
5
|
export type TreeAction = "create" | "update" | "delete";
|
|
@@ -204,6 +204,29 @@ export interface TreeViewProps<TItem extends TreeItem = TreeItem> {
|
|
|
204
204
|
* where it is. Not on a `checkable` tree, whose rows are checked instead.
|
|
205
205
|
*/
|
|
206
206
|
selectedValue?: TreeValue | null;
|
|
207
|
+
/**
|
|
208
|
+
* Small tabs above the rows that narrow them, such as All actions · View only · Add and edit · Trash and delete on a
|
|
209
|
+
* permission tree. A filter's `match` is asked of every item without children (one without `match` shows all); the
|
|
210
|
+
* tree shows the leaves it takes and the parents that hold one. While it is on, checking a parent - or Select all -
|
|
211
|
+
* checks or unchecks only the leaves shown, a parent's box and count read only them, and a leaf the filter hides keeps
|
|
212
|
+
* its check exactly as it was. The value is the same whatever tab is on: switching tabs never changes it, and never
|
|
213
|
+
* makes a form dirty. The first filter is on at the start, unless `defaultFilter` names another.
|
|
214
|
+
*/
|
|
215
|
+
filters?: readonly TreeFilter<TItem>[];
|
|
216
|
+
/** With `filters`: the id of the filter on at the start. Default the first. */
|
|
217
|
+
defaultFilter?: string;
|
|
218
|
+
/**
|
|
219
|
+
* With `filters`: keeps the tab chosen in this browser, per `key` and `viewer` (the signed-in person), and opens it
|
|
220
|
+
* next time; nothing is kept without a viewer. Read after the first draw, as every kept choice is.
|
|
221
|
+
*/
|
|
222
|
+
rememberFilter?: {
|
|
223
|
+
key: string;
|
|
224
|
+
viewer?: string | null;
|
|
225
|
+
};
|
|
226
|
+
/** With `filters`: after a tab is chosen, its id. */
|
|
227
|
+
onFilterChange?: (id: string) => void;
|
|
228
|
+
/** With `filters`: what a screen reader calls the tabs. Default "Show". */
|
|
229
|
+
filtersLabel?: string;
|
|
207
230
|
/** The longest name the editor takes. Default 100. */
|
|
208
231
|
maxLabelLength?: number;
|
|
209
232
|
/** A click on a row without a checkbox, or Enter on it. Without it, a click expands or collapses a parent. */
|
|
@@ -4,10 +4,12 @@ import { Icon, iconAlertCircle, iconCheck, iconChevronRight, iconChevronsDown, i
|
|
|
4
4
|
import { isValidElement, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState, } from "react";
|
|
5
5
|
import { afterAnimation } from "../../Helpers/AnimationHelpers.js";
|
|
6
6
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
7
|
+
import { browserInputMemory } from "../../Helpers/InputMemory.js";
|
|
7
8
|
import { isPromiseLike, REQUEST_CACHE_LIFETIME } from "../../Helpers/RequestCache.js";
|
|
8
|
-
import { expireTreePages, subscribeTreeChanges, TREE_DEFAULT_KEYS, treeKey, treePageKey, treePages, TreeStore, treeStatusKey, } from "../../Helpers/TreeStore.js";
|
|
9
|
+
import { expireTreePages, shownTreeFilter, subscribeTreeChanges, TREE_DEFAULT_KEYS, treeKey, treePageKey, treePages, TreeStore, treeFilterStorageKey, treeStatusKey, } from "../../Helpers/TreeStore.js";
|
|
9
10
|
import { Button } from "../Documents/Button.js";
|
|
10
11
|
import { ContextMenu } from "../Documents/Menu.js";
|
|
12
|
+
import { TabButtons } from "../Documents/TabButtons.js";
|
|
11
13
|
import { messageBox } from "../Layouts/MessageBox.js";
|
|
12
14
|
import { CheckBox } from "./CheckBox.js";
|
|
13
15
|
import { hasContent } from "./Field.js";
|
|
@@ -56,7 +58,7 @@ const sameItems = (a, b) => a === b || (a.length === b.length && a.every((item,
|
|
|
56
58
|
* loadChildren={({ parent, skip, take, signal }) => loadRegions(parent, skip, take, signal)} />
|
|
57
59
|
*/
|
|
58
60
|
export function TreeView(props) {
|
|
59
|
-
const { items: givenItems, loadChildren, childPageSize = CHILD_PAGE_SIZE, hasChildrenKey, cacheKey, cacheLifetime = REQUEST_CACHE_LIFETIME, partialMessage = "Its items are not all loaded: open it and load them all to check it as a whole.", valueKey = TREE_DEFAULT_KEYS.value, parentKey = TREE_DEFAULT_KEYS.parent, labelKey = TREE_DEFAULT_KEYS.label, descriptionKey = "description", iconKey = "icon", isItemDisabled, lockedMessage = "You do not have permission to change this item.", label, hint, error, size = "md", checkable = false, cascade = true, valueMode = "leaves", value, defaultValue, onValueChange, name, form, counts = true, defaultExpanded, creatable = false, editable = false, deletable = false, canChange, onCreate, onUpdate, onDelete, onItemsChange, confirmDelete = true, rowActions, add, selectedValue, maxLabelLength = 100, onItemClick, loading = false, emptyMessage = "No items", toolbar = true, height, maxHeight = 420, fill = false, readOnly = false, readOnlyMessage = "Read-only: this tree cannot be changed.", disabled = false, id, className, style, "aria-label": ariaLabel, ref, } = props;
|
|
61
|
+
const { items: givenItems, loadChildren, childPageSize = CHILD_PAGE_SIZE, hasChildrenKey, cacheKey, cacheLifetime = REQUEST_CACHE_LIFETIME, partialMessage = "Its items are not all loaded: open it and load them all to check it as a whole.", valueKey = TREE_DEFAULT_KEYS.value, parentKey = TREE_DEFAULT_KEYS.parent, labelKey = TREE_DEFAULT_KEYS.label, descriptionKey = "description", iconKey = "icon", isItemDisabled, lockedMessage = "You do not have permission to change this item.", label, hint, error, size = "md", checkable = false, cascade = true, valueMode = "leaves", value, defaultValue, onValueChange, name, form, counts = true, defaultExpanded, creatable = false, editable = false, deletable = false, canChange, onCreate, onUpdate, onDelete, onItemsChange, confirmDelete = true, rowActions, add, selectedValue, filters, defaultFilter, rememberFilter, onFilterChange, filtersLabel = "Show", maxLabelLength = 100, onItemClick, loading = false, emptyMessage = "No items", toolbar = true, height, maxHeight = 420, fill = false, readOnly = false, readOnlyMessage = "Read-only: this tree cannot be changed.", disabled = false, id, className, style, "aria-label": ariaLabel, ref, } = props;
|
|
60
62
|
const generatedId = useId();
|
|
61
63
|
const treeId = id ?? generatedId;
|
|
62
64
|
const labelId = `${treeId}-label`;
|
|
@@ -75,6 +77,14 @@ export function TreeView(props) {
|
|
|
75
77
|
const holderRef = useRef(null);
|
|
76
78
|
// Whether the user (or the handle) changed the checks since the items came, so new items keep them.
|
|
77
79
|
const touchedRef = useRef(false);
|
|
80
|
+
// The filter on: the tab chosen (or kept), else the default, else the first.
|
|
81
|
+
const [chosenFilter, setChosenFilter] = useState(null);
|
|
82
|
+
const activeFilter = shownTreeFilter(filters, chosenFilter, defaultFilter);
|
|
83
|
+
const activeFilterId = activeFilter?.match ? activeFilter.id : null;
|
|
84
|
+
// The store asks the filter through this, so a page passing a new arrow function each draw counts nothing again.
|
|
85
|
+
const matchRef = useRef(undefined);
|
|
86
|
+
matchRef.current = activeFilter?.match;
|
|
87
|
+
const storeMatch = (id) => (id === null ? null : item => (matchRef.current ? matchRef.current(item) : true));
|
|
78
88
|
if (holderRef.current?.config !== config) {
|
|
79
89
|
// A new rule (another value mode, say) keeps what was checked and expanded.
|
|
80
90
|
const previous = holderRef.current?.store;
|
|
@@ -87,6 +97,7 @@ export function TreeView(props) {
|
|
|
87
97
|
lazy,
|
|
88
98
|
lazyRoots,
|
|
89
99
|
hasChildrenKey,
|
|
100
|
+
filter: storeMatch(activeFilterId),
|
|
90
101
|
isItemDisabled: item => (disabledRef.current ? disabledRef.current(item) : read(item, "disabled") === true || read(item, "isDisabled") === true),
|
|
91
102
|
}, value ?? (previous && touchedRef.current ? previous.value() : defaultValue));
|
|
92
103
|
if (previous)
|
|
@@ -97,10 +108,15 @@ export function TreeView(props) {
|
|
|
97
108
|
store.expandAll(defaultExpanded);
|
|
98
109
|
else if (defaultExpanded)
|
|
99
110
|
store.expandValues(defaultExpanded);
|
|
100
|
-
holderRef.current = { config, store, items, value };
|
|
111
|
+
holderRef.current = { config, store, items, value, filterId: activeFilterId };
|
|
101
112
|
}
|
|
102
113
|
const holder = holderRef.current;
|
|
103
114
|
const store = holder.store;
|
|
115
|
+
// Another filter (a kept one read, the filters changed): rows again, nothing checked or unchecked.
|
|
116
|
+
if (holder.filterId !== activeFilterId) {
|
|
117
|
+
store.setFilter(storeMatch(activeFilterId));
|
|
118
|
+
holder.filterId = activeFilterId;
|
|
119
|
+
}
|
|
104
120
|
if (!sameItems(holder.items, items)) {
|
|
105
121
|
store.setItems(items, value ?? (touchedRef.current ? store.value() : defaultValue));
|
|
106
122
|
holder.items = items;
|
|
@@ -358,6 +374,59 @@ export function TreeView(props) {
|
|
|
358
374
|
function onPanelFocus() {
|
|
359
375
|
hadFocusRef.current = true;
|
|
360
376
|
}
|
|
377
|
+
// Filters: the tab kept in this browser is read after the first draw, as the server drew none; a tab chosen before then wins.
|
|
378
|
+
const filterStorageKey = filters && filters.length > 0 ? treeFilterStorageKey(rememberFilter) : null;
|
|
379
|
+
const filterTouchedRef = useRef(false);
|
|
380
|
+
useLayoutEffect(() => {
|
|
381
|
+
if (!filterStorageKey)
|
|
382
|
+
return;
|
|
383
|
+
let kept = null;
|
|
384
|
+
try {
|
|
385
|
+
kept = browserInputMemory()?.getItem(filterStorageKey) ?? null;
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
kept = null;
|
|
389
|
+
}
|
|
390
|
+
if (kept !== null && !filterTouchedRef.current)
|
|
391
|
+
setChosenFilter(kept);
|
|
392
|
+
}, [filterStorageKey]);
|
|
393
|
+
/** A tab chosen: rows again for its filter, focus kept on a row still shown, and the choice kept. Nothing is checked or unchecked. */
|
|
394
|
+
function chooseFilter(id) {
|
|
395
|
+
const next = shownTreeFilter(filters, id, defaultFilter);
|
|
396
|
+
if (!next)
|
|
397
|
+
return;
|
|
398
|
+
filterTouchedRef.current = true;
|
|
399
|
+
setChosenFilter(next.id);
|
|
400
|
+
if (filterStorageKey) {
|
|
401
|
+
try {
|
|
402
|
+
browserInputMemory()?.setItem(filterStorageKey, next.id);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// Storage switched off or full: the choice lasts until the page goes.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
matchRef.current = next.match;
|
|
409
|
+
const nextId = next.match ? next.id : null;
|
|
410
|
+
if (holder.filterId !== nextId) {
|
|
411
|
+
store.setFilter(storeMatch(nextId));
|
|
412
|
+
holder.filterId = nextId;
|
|
413
|
+
}
|
|
414
|
+
setMotion(null);
|
|
415
|
+
// A focused row the filter hides hands its place to its nearest shown parent, or to the first row.
|
|
416
|
+
const focused = focusKey === null ? undefined : store.find(focusKey);
|
|
417
|
+
if (focused && !store.isVisible(focused)) {
|
|
418
|
+
let up = focused.parent;
|
|
419
|
+
while (up && !store.isVisible(up))
|
|
420
|
+
up = up.parent;
|
|
421
|
+
setFocusKey((up ?? store.rows[0])?.key ?? null);
|
|
422
|
+
}
|
|
423
|
+
noteRowRef.current = null;
|
|
424
|
+
readOnlyNotice.hide();
|
|
425
|
+
lockedNotice.hide();
|
|
426
|
+
partialNotice.hide();
|
|
427
|
+
onFilterChange?.(next.id);
|
|
428
|
+
bump();
|
|
429
|
+
}
|
|
361
430
|
/**
|
|
362
431
|
* Changes made elsewhere, written in (the handle's `apply`). The focused row keeps focus; when it leaves the page or
|
|
363
432
|
* moves in it, focus goes to the viewport first, while the DOM is whole, and comes back to it - or, when the item
|
|
@@ -1262,6 +1331,8 @@ export function TreeView(props) {
|
|
|
1262
1331
|
const rendered = new Set();
|
|
1263
1332
|
/** How many siblings a row has: all of them on the server when the loader said, loaded or not. */
|
|
1264
1333
|
function setSize(node) {
|
|
1334
|
+
if (store.filtered)
|
|
1335
|
+
return store.shownChildCount(node.parent);
|
|
1265
1336
|
const loaded = node.parent ? node.parent.children.length : store.roots.length;
|
|
1266
1337
|
const total = store.pagesOf(node.parent)?.total;
|
|
1267
1338
|
return total === undefined ? loaded : Math.max(total, loaded);
|
|
@@ -1329,7 +1400,7 @@ export function TreeView(props) {
|
|
|
1329
1400
|
return (_jsxs("div", { role: ghost ? undefined : "treeitem", className: cx("nx-tree__row", locked && "nx-tree__row--locked", edit && "nx-tree__row--editing", node.key === selectedKey && "nx-tree__row--selected"), style: { "--nx-tree-depth": node.depth }, "data-key": ghost ? undefined : node.key, "data-uid": ghost ? undefined : node.uid, "data-readonly": locked || undefined, tabIndex: ghost ? undefined : !disabled && tabbable === node ? 0 : -1, "aria-level": ghost ? undefined : node.depth + 1, "aria-setsize": ghost ? undefined : setSize(node), "aria-posinset": ghost ? undefined : node.position + 1, "aria-expanded": ghost || !expandable ? undefined : node.expanded, "aria-checked": ghost || !state ? undefined : state === "mixed" ? "mixed" : state === "checked", "aria-selected": !ghost && node.key === selectedKey ? true : undefined, "aria-disabled": ghost ? undefined : locked || undefined, "aria-busy": busy || childrenLoading || undefined, "aria-labelledby": ghost || edit ? undefined : labelElementId, "aria-label": edit ? (edit.mode === "create" ? "New item" : `Renaming ${node.label}`) : undefined, "aria-describedby": ghost || edit ? undefined : descriptionElementId, children: [_jsx("span", { className: "nx-tree__toggle", "aria-hidden": "true", children: expandable && _jsx(InlineIcon, { icon: iconChevronRight }) }), checkable && (_jsx("span", { className: cx("nx-tree__check nx-choice", `nx-choice--${size}`), "aria-hidden": "true", children: _jsx("input", { type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", tabIndex: -1, checked: state === "checked", disabled: disabled, readOnly: true, ref: input => {
|
|
1330
1401
|
if (input)
|
|
1331
1402
|
input.indeterminate = state === "mixed";
|
|
1332
|
-
} }) })), icon && (_jsx("span", { className: "nx-tree__icon", "aria-hidden": "true", children: icon })), edit ? (_jsx(TreeEditor, { initial: editTextRef.current, select: edit.mode === "update", maxLength: maxLabelLength, saving: saving, label: edit.mode === "create" ? "Name of the new item" : `New name for ${node.label}`, invalid: message !== null, describedBy: message !== null ? messageId : undefined, onText: text => (editTextRef.current = text), onCommit: via => void commitEdit(via), onCancel: cancelEdit }, node.uid)) : (_jsxs("span", { className: "nx-tree__text", children: [_jsx("span", { id: ghost ? undefined : labelElementId, className: "nx-tree__label", title: node.label, children: node.label || "(no name)" }), hasDescription && (_jsx("span", { id: ghost ? undefined : descriptionElementId, className: "nx-tree__description", children: String(description) }))] })), showCounts && hasChildren && !edit && (_jsxs("span", { className: "nx-tree__count", "aria-hidden": "true", children: [node.
|
|
1403
|
+
} }) })), icon && (_jsx("span", { className: "nx-tree__icon", "aria-hidden": "true", children: icon })), edit ? (_jsx(TreeEditor, { initial: editTextRef.current, select: edit.mode === "update", maxLength: maxLabelLength, saving: saving, label: edit.mode === "create" ? "Name of the new item" : `New name for ${node.label}`, invalid: message !== null, describedBy: message !== null ? messageId : undefined, onText: text => (editTextRef.current = text), onCommit: via => void commitEdit(via), onCancel: cancelEdit }, node.uid)) : (_jsxs("span", { className: "nx-tree__text", children: [_jsx("span", { id: ghost ? undefined : labelElementId, className: "nx-tree__label", title: node.label, children: node.label || "(no name)" }), hasDescription && (_jsx("span", { id: ghost ? undefined : descriptionElementId, className: "nx-tree__description", children: String(description) }))] })), showCounts && hasChildren && !edit && (_jsxs("span", { className: "nx-tree__count", "aria-hidden": "true", children: [node.shownChecked.toLocaleString(), "/", node.shownLeaves.toLocaleString(), node.partial && "+"] })), busy && !edit && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin nx-tree__busy", "aria-hidden": "true" }), locked && !edit && (_jsx("span", { className: "nx-tree__lock", title: lockedMessage, "aria-hidden": "true", children: _jsx(InlineIcon, { icon: iconLock }) })), actions.length > 0 && _jsx("span", { className: "nx-tree__actions", children: actions })] }, node.uid));
|
|
1333
1404
|
}
|
|
1334
1405
|
const end = Math.min(range.end, total);
|
|
1335
1406
|
const start = Math.min(range.start, end);
|
|
@@ -1362,6 +1433,8 @@ export function TreeView(props) {
|
|
|
1362
1433
|
}
|
|
1363
1434
|
const allState = checkable ? store.allState() : null;
|
|
1364
1435
|
const tally = store.counts();
|
|
1436
|
+
// Under a filter the toolbar counts what is shown, and everything after it.
|
|
1437
|
+
const shownTally = store.filtered ? store.shownCounts() : null;
|
|
1365
1438
|
const hasParents = store.roots.some(root => root.children.length > 0 || root.expanded);
|
|
1366
1439
|
const hasLabel = hasContent(label);
|
|
1367
1440
|
const shownMessage = message ?? (hasContent(error) ? error : null);
|
|
@@ -1370,11 +1443,15 @@ export function TreeView(props) {
|
|
|
1370
1443
|
const rootWaiting = store.size === 0 && rootPages !== null && (rootPages.status === "idle" || rootPages.status === "loading");
|
|
1371
1444
|
const waiting = loading || rootWaiting;
|
|
1372
1445
|
const empty = total === 0 || rootWaiting;
|
|
1373
|
-
return (_jsxs("div", { className: cx("nx-field nx-field--standard", `nx-field--${size}`, "nx-tree-field", fill && "nx-tree-field--fill", className), style: style, "data-invalid": shownMessage ? true : undefined, "data-readonly": readOnly || undefined, "data-disabled": disabled || undefined, children: [hasLabel && (_jsxs("span", { id: labelId, className: "nx-field__label", children: [label, readOnlyNotice.lock] })), _jsxs("div", { ref: panelRef, className: cx("nx-tree", `nx-tree--${size}`), onFocus: onPanelFocus, onBlur: onPanelBlur, children: [
|
|
1446
|
+
return (_jsxs("div", { className: cx("nx-field nx-field--standard", `nx-field--${size}`, "nx-tree-field", fill && "nx-tree-field--fill", className), style: style, "data-invalid": shownMessage ? true : undefined, "data-readonly": readOnly || undefined, "data-disabled": disabled || undefined, children: [hasLabel && (_jsxs("span", { id: labelId, className: "nx-field__label", children: [label, readOnlyNotice.lock] })), _jsxs("div", { ref: panelRef, className: cx("nx-tree", `nx-tree--${size}`), onFocus: onPanelFocus, onBlur: onPanelBlur, children: [activeFilter && filters && (_jsx("div", { className: "nx-tree__filters", children: _jsx(TabButtons, { size: "sm", wrap: true, options: filters.map(filter => ({ value: filter.id, label: filter.label, title: filter.title, disabled })), value: activeFilter.id, onChange: chooseFilter,
|
|
1447
|
+
// An id no form has: the tabs are no value of the form around the tree.
|
|
1448
|
+
form: `${treeId}-filters`, "aria-label": filtersLabel }) })), toolbar && (_jsxs("div", { className: "nx-tree__toolbar", children: [checkable && (_jsx(CheckBox, { size: "sm", className: "nx-tree__all", label: "Select all", checked: allState === "checked", indeterminate: allState === "mixed", disabled: disabled || allState === null || empty, readOnly: readOnly, readOnlyMessage: readOnlyMessage, onValueChange: toggleAll })), _jsxs("span", { className: "nx-tree__summary", "aria-live": "polite", children: [(loading || rootPages?.status === "loading") && _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin", "aria-hidden": "true" }), waiting && empty
|
|
1374
1449
|
? "Loading…"
|
|
1375
1450
|
: checkable
|
|
1376
|
-
?
|
|
1377
|
-
|
|
1451
|
+
? shownTally
|
|
1452
|
+
? `${shownTally.checked.toLocaleString()} of ${shownTally.total.toLocaleString()} selected · ${tally.checked.toLocaleString()} of ${tally.total.toLocaleString()} in all`
|
|
1453
|
+
: `${tally.checked.toLocaleString()} of ${tally.total.toLocaleString()} selected`
|
|
1454
|
+
: `${store.size.toLocaleString()} ${store.size === 1 ? "item" : "items"}${lazy ? " loaded" : ""}`, !hasLabel && readOnlyNotice.lock] }), _jsxs("span", { className: "nx-tree__tools", children: [_jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconChevronsDown }), "aria-label": "Expand all", title: "Expand all", disabled: !hasParents, onClick: expandAll }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconChevronsUp }), "aria-label": "Collapse all", title: "Collapse all", disabled: !hasParents, onClick: collapseAll }), creatable && !blocked && (!canChange || canChange("create", null)) && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconPlus }), onMouseDown: keepEditorFocus, onClick: () => startCreate(null), children: "Add" })), add && !blocked && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconPlus }), "aria-disabled": add.disabled || undefined, title: add.description, onMouseDown: keepEditorFocus, onKeyDown: addNotice.onKeyDown, onClick: event => (add.disabled ? addNotice.show(event.currentTarget) : add.onClick()), children: add.label }))] })] })), _jsx(ContextMenu, { items: contextItems, onSelect: onContextSelect, disabled: contextItems.length === 0, "aria-label": "Item actions", children: _jsx("div", { ref: viewportRef, className: "nx-tree__viewport", style: fill ? undefined : { height, maxHeight: height === undefined ? maxHeight : undefined }, tabIndex: !disabled && !empty && !(tabbable && rendered.has(tabbable.key)) ? 0 : -1, onScroll: event => syncRange(event.currentTarget), onFocus: onViewportFocus, onPointerDown: onViewportPointerDown, onClick: onViewportClick, onKeyDown: onViewportKeyDown, children: empty ? (waiting ? (_jsx("div", { className: "nx-tree__list", "aria-hidden": "true", children: SKELETON_WIDTHS.map((width, index) => (_jsxs("div", { className: "nx-tree__row nx-tree__row--skeleton", style: { "--nx-tree-depth": index % 3 === 0 ? 0 : 1 }, children: [_jsx("span", { className: "nx-tree__toggle" }), _jsx("span", { className: "nx-tree__skeleton", style: { width } })] }, index))) })) : (_jsx("p", { className: "nx-tree__empty", children: store.filtered && store.size > 0 && activeFilter ? `No items in "${activeFilter.label}".` : emptyMessage }))) : (_jsx("div", { role: "tree", className: "nx-tree__list", "aria-labelledby": hasLabel ? labelId : undefined, "aria-label": hasLabel ? undefined : (ariaLabel ?? "Tree"), "aria-describedby": shownMessage || hasContent(hint) ? messageId : undefined, "aria-disabled": disabled || undefined, "aria-readonly": readOnly || undefined, "aria-busy": waiting || undefined,
|
|
1378
1455
|
// As tall as all of its rows at every moment of a commit, not only once it has finished. A scroll that
|
|
1379
1456
|
// moves the window makes React take the rows that leave it out before it puts the new ones in and sets
|
|
1380
1457
|
// this padding, and a layout in between - Chrome lays the page out inside the removal of a focused row,
|
|
@@ -40,6 +40,11 @@ export interface PopupProps {
|
|
|
40
40
|
variant?: PopupVariant;
|
|
41
41
|
/** Width: `xs` 360, `sm` 440, `md` 560 (default), `lg` 720, `xl` 960, or `full`. */
|
|
42
42
|
size?: PopupSize;
|
|
43
|
+
/**
|
|
44
|
+
* Height, pixels or a CSS length: the body scrolls inside it. Default: as tall as the content, up to the window. A
|
|
45
|
+
* docked window is `32rem` tall by default, never taller than the window.
|
|
46
|
+
*/
|
|
47
|
+
height?: number | string;
|
|
43
48
|
/** Buttons at the end, right-aligned. */
|
|
44
49
|
footer?: ReactNode;
|
|
45
50
|
/** Shows the close button. Default true. */
|
|
@@ -67,13 +72,28 @@ export interface PopupProps {
|
|
|
67
72
|
reopen?: PopupReopen;
|
|
68
73
|
/** The window state, when the page controls it; pair it with `onWindowStateChange`. */
|
|
69
74
|
windowState?: PopupWindowState;
|
|
75
|
+
/**
|
|
76
|
+
* A window that leaves the page usable: it opens without a backdrop at the bottom right, beside the other docked
|
|
77
|
+
* windows (the first on the right, each next one to its left; on a screen too narrow for two, over each other), the
|
|
78
|
+
* one last used on top. Esc inside it closes it, Tab leaves it for the page. A mail client's chat popups and compose
|
|
79
|
+
* window are docked. Minimize and maximize work as on any popup. Default false: a popup locks the page.
|
|
80
|
+
*/
|
|
81
|
+
docked?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* A drawer: the window's whole height at its end edge (the right, in a left-to-right page), its width from `size`
|
|
84
|
+
* (all of it on a phone). Modal as any popup; it neither moves nor maximizes. A board's Add widget list is one.
|
|
85
|
+
*/
|
|
86
|
+
drawer?: boolean;
|
|
70
87
|
/** The window state each time the popup opens, when the page does not control it. Default `normal`. */
|
|
71
88
|
defaultWindowState?: Exclude<PopupWindowState, "minimized">;
|
|
72
89
|
/** When the buttons, a double-click on the title bar, a drag, or the minimized bar change the window state. */
|
|
73
90
|
onWindowStateChange?: (state: PopupWindowState) => void;
|
|
74
91
|
/** A body without padding, for tables and lists that reach the edges. */
|
|
75
92
|
flush?: boolean;
|
|
76
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* A selector for the element that takes focus when the popup opens. Default: an element of the body or footer marked
|
|
95
|
+
* `data-nx-focus` (with `tabIndex={-1}` when it is no control, such as a heading), else the first control there.
|
|
96
|
+
*/
|
|
77
97
|
initialFocus?: string;
|
|
78
98
|
/** Name for screen readers when there is no title. */
|
|
79
99
|
"aria-label"?: string;
|
|
@@ -96,4 +116,4 @@ export interface PopupProps {
|
|
|
96
116
|
* @example
|
|
97
117
|
* <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
|
|
98
118
|
*/
|
|
99
|
-
export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable, movable, reopen, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }: PopupProps): import("react").JSX.Element;
|
|
119
|
+
export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, height, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable: maximizableProp, movable: movableProp, reopen, docked, drawer, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }: PopupProps): import("react").JSX.Element | null;
|