nexus-shared 3.2.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 +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -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/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/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -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 +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -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 +182 -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/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- 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 +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -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 +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- 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 +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- 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/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 +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -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/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -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/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- 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/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 +158 -0
- package/dist/Helpers/FileAdapter.js +173 -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 +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- 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 +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -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 +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- 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 +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -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 +539 -6
- 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 +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- 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 +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -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 +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -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, TREE_DEFAULT_KEYS, 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, maxLabelLength = 100, onItemClick, loading = false, emptyMessage = "No items", toolbar = true, height, maxHeight = 420, 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;
|
|
@@ -142,6 +158,8 @@ export function TreeView(props) {
|
|
|
142
158
|
const pendingScrollRef = useRef(null);
|
|
143
159
|
const pendingFocusRef = useRef(null);
|
|
144
160
|
const typeaheadRef = useRef({ text: "", at: 0 });
|
|
161
|
+
// An item the handle was asked to reveal before it was loaded: revealed when the page that brings it comes.
|
|
162
|
+
const pendingRevealRef = useRef(null);
|
|
145
163
|
const blocked = readOnly || disabled;
|
|
146
164
|
const noticeAnchor = (target) => {
|
|
147
165
|
const row = target.closest(".nx-tree__row");
|
|
@@ -150,6 +168,10 @@ export function TreeView(props) {
|
|
|
150
168
|
const readOnlyNotice = useReadOnlyNotice(readOnly && !disabled, viewportRef, readOnlyMessage, { anchor: noticeAnchor, typing: false, lock: "label" });
|
|
151
169
|
const lockedNotice = useReadOnlyNotice(!blocked, viewportRef, lockedMessage, { anchor: noticeAnchor, typing: false, lock: "label" });
|
|
152
170
|
const partialNotice = useReadOnlyNotice(!blocked && lazy && checkable && cascade, viewportRef, partialMessage, { anchor: noticeAnchor, typing: false, lock: "label" });
|
|
171
|
+
// Why the toolbar's own button cannot be pressed yet, under it, on a press.
|
|
172
|
+
const addNotice = useReadOnlyNotice(Boolean(add?.disabled && add.description), viewportRef, add?.description, { anchor: target => target.closest(".nx-btn"), align: "end", typing: false, lock: "label" });
|
|
173
|
+
// The row shown as selected, as a node key.
|
|
174
|
+
const selectedKey = checkable ? null : treeKey(selectedValue);
|
|
153
175
|
// The row a note points at: when it leaves the page (scrolled out of the window), the note goes too.
|
|
154
176
|
const noteRowRef = useRef(null);
|
|
155
177
|
function showNote(notice, target) {
|
|
@@ -319,6 +341,13 @@ export function TreeView(props) {
|
|
|
319
341
|
});
|
|
320
342
|
};
|
|
321
343
|
}, []);
|
|
344
|
+
// Changes to the source written elsewhere (`writeTreeChanges`) are written into this tree too.
|
|
345
|
+
const applyRef = useRef(() => undefined);
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!lazy || !cacheKey)
|
|
348
|
+
return;
|
|
349
|
+
return subscribeTreeChanges(cacheKey, (changes, rules) => applyRef.current(changes, rules));
|
|
350
|
+
}, [lazy, cacheKey]);
|
|
322
351
|
// Open parents with rows in view that need pages: the top level at first, a parent a refresh made stale, one whose
|
|
323
352
|
// request a refresh forgot. A stale parent out of view waits until it scrolls in, and a closed one until it opens; a
|
|
324
353
|
// failed one waits for Retry, so a server that is down is not asked again and again.
|
|
@@ -345,6 +374,141 @@ export function TreeView(props) {
|
|
|
345
374
|
function onPanelFocus() {
|
|
346
375
|
hadFocusRef.current = true;
|
|
347
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
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Changes made elsewhere, written in (the handle's `apply`). The focused row keeps focus; when it leaves the page or
|
|
432
|
+
* moves in it, focus goes to the viewport first, while the DOM is whole, and comes back to it - or, when the item
|
|
433
|
+
* left, to the row now in its place - in the layout effect, as when the window moves (see `releaseFocusLeaving`).
|
|
434
|
+
*/
|
|
435
|
+
function applyChanges(changes, rules = {}) {
|
|
436
|
+
if (changes.length === 0)
|
|
437
|
+
return;
|
|
438
|
+
const viewport = viewportRef.current;
|
|
439
|
+
const active = document.activeElement;
|
|
440
|
+
const held = viewport && active instanceof HTMLElement && active !== viewport && viewport.contains(active) ? nodeOfTarget(active) : undefined;
|
|
441
|
+
const heldIndex = held ? store.indexOf(held) : -1;
|
|
442
|
+
const focused = focusKeyRef.current === null ? undefined : store.find(focusKeyRef.current);
|
|
443
|
+
const focusedIndex = focused ? store.indexOf(focused) : -1;
|
|
444
|
+
const valueBefore = valueSignature();
|
|
445
|
+
for (const change of changes) {
|
|
446
|
+
if (change.type === "put")
|
|
447
|
+
store.placeItem(change.item, change.parent, { before: change.before, compare: rules.compare });
|
|
448
|
+
else
|
|
449
|
+
store.removeItem(change.value, change.parent);
|
|
450
|
+
}
|
|
451
|
+
setMotion(null);
|
|
452
|
+
const stillThere = (node, index) => {
|
|
453
|
+
const found = store.find(node.key);
|
|
454
|
+
if (found && store.isVisible(found))
|
|
455
|
+
return found;
|
|
456
|
+
return store.rows.length > 0 ? store.rows[Math.max(0, Math.min(index, store.rows.length - 1))] : undefined;
|
|
457
|
+
};
|
|
458
|
+
if (held && viewport) {
|
|
459
|
+
const next = stillThere(held, heldIndex);
|
|
460
|
+
if (next !== held || store.indexOf(held) !== heldIndex) {
|
|
461
|
+
restoringRef.current = true;
|
|
462
|
+
viewport.focus({ preventScroll: true });
|
|
463
|
+
restoringRef.current = false;
|
|
464
|
+
if (next)
|
|
465
|
+
focusNode(next);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
else if (focused && !(store.find(focused.key) === focused && store.isVisible(focused))) {
|
|
469
|
+
setFocusKey(stillThere(focused, focusedIndex)?.key ?? null);
|
|
470
|
+
}
|
|
471
|
+
emitValueIfChanged(valueBefore);
|
|
472
|
+
bump();
|
|
473
|
+
}
|
|
474
|
+
applyRef.current = applyChanges;
|
|
475
|
+
/** An item revealed before it was loaded, revealed now if the answer just in brought it. */
|
|
476
|
+
function revealPending() {
|
|
477
|
+
const pending = pendingRevealRef.current;
|
|
478
|
+
const node = pending ? store.get(pending.key) : undefined;
|
|
479
|
+
if (!pending || !node)
|
|
480
|
+
return;
|
|
481
|
+
pendingRevealRef.current = null;
|
|
482
|
+
if (pending.focus) {
|
|
483
|
+
focusNode(node);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (store.reveal(node))
|
|
487
|
+
setMotion(null);
|
|
488
|
+
pendingScrollRef.current = store.indexOf(node);
|
|
489
|
+
}
|
|
490
|
+
/** The page's own actions on a row, where they may be offered. */
|
|
491
|
+
function rowActionsOf(node) {
|
|
492
|
+
if (!rowActions || blocked || node.status || node.draft || node.disabledPath)
|
|
493
|
+
return [];
|
|
494
|
+
return rowActions(node.item);
|
|
495
|
+
}
|
|
496
|
+
/** Runs one of them. The row is busy, its actions waiting, while a promise the action returns runs. */
|
|
497
|
+
function runRowAction(node, action) {
|
|
498
|
+
if (blocked || action.disabled || busyKeys.has(node.key))
|
|
499
|
+
return;
|
|
500
|
+
pendingRevealRef.current = null;
|
|
501
|
+
const result = action.onSelect(node.item);
|
|
502
|
+
if (!isPromiseLike(result))
|
|
503
|
+
return;
|
|
504
|
+
const key = node.key;
|
|
505
|
+
setBusy(key, true);
|
|
506
|
+
const done = () => {
|
|
507
|
+
if (mountedRef.current)
|
|
508
|
+
setBusy(key, false);
|
|
509
|
+
};
|
|
510
|
+
result.then(done, done);
|
|
511
|
+
}
|
|
348
512
|
function onPanelBlur(event) {
|
|
349
513
|
const next = event.relatedTarget;
|
|
350
514
|
if (next instanceof Node && panelRef.current?.contains(next))
|
|
@@ -354,6 +518,7 @@ export function TreeView(props) {
|
|
|
354
518
|
readOnlyNotice.hide();
|
|
355
519
|
lockedNotice.hide();
|
|
356
520
|
partialNotice.hide();
|
|
521
|
+
addNotice.hide();
|
|
357
522
|
return;
|
|
358
523
|
}
|
|
359
524
|
// Focus went nowhere: a click on the page, or a focused row that left the DOM. The first is settled after this frame.
|
|
@@ -526,6 +691,7 @@ export function TreeView(props) {
|
|
|
526
691
|
if (result.change)
|
|
527
692
|
setMotion(openMotion(result.change));
|
|
528
693
|
followStatusFocus(current, result.added);
|
|
694
|
+
revealPending();
|
|
529
695
|
const now = store.pagesOf(current);
|
|
530
696
|
const count = result.added.length;
|
|
531
697
|
const of = now?.total !== undefined ? ` (${now.loaded.toLocaleString()} of ${now.total.toLocaleString()})` : "";
|
|
@@ -575,6 +741,7 @@ export function TreeView(props) {
|
|
|
575
741
|
return;
|
|
576
742
|
setMotion(null);
|
|
577
743
|
followStatusFocus(current, []);
|
|
744
|
+
revealPending();
|
|
578
745
|
setAnnouncement(`Items loaded again${where}.`);
|
|
579
746
|
bump();
|
|
580
747
|
};
|
|
@@ -881,6 +1048,8 @@ export function TreeView(props) {
|
|
|
881
1048
|
}
|
|
882
1049
|
// Pointer and keys
|
|
883
1050
|
function onViewportPointerDown(event) {
|
|
1051
|
+
// The user moves on: an item waiting to be revealed is not brought into view under them later.
|
|
1052
|
+
pendingRevealRef.current = null;
|
|
884
1053
|
// The context menu opens on the row that was pressed, or at the top level on empty space or a status row.
|
|
885
1054
|
const node = nodeOfTarget(event.target);
|
|
886
1055
|
setContextKey(node && !node.status ? node.key : null);
|
|
@@ -922,6 +1091,7 @@ export function TreeView(props) {
|
|
|
922
1091
|
if (event.defaultPrevented || event.target.closest(".nx-tree__edit"))
|
|
923
1092
|
return;
|
|
924
1093
|
setContextKey(undefined);
|
|
1094
|
+
pendingRevealRef.current = null;
|
|
925
1095
|
if (event.key === "Escape" && noteRowRef.current) {
|
|
926
1096
|
noteRowRef.current = null;
|
|
927
1097
|
readOnlyNotice.hide();
|
|
@@ -990,12 +1160,18 @@ export function TreeView(props) {
|
|
|
990
1160
|
setMotion(null);
|
|
991
1161
|
bump();
|
|
992
1162
|
}
|
|
993
|
-
else if (key === "F2")
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1163
|
+
else if (key === "F2" || key === "Delete" || key === "Insert") {
|
|
1164
|
+
// The page's own action for the key, where it gave one; else the tree's own editing.
|
|
1165
|
+
const own = rowActionsOf(node).find(action => action.shortcut === key && !action.disabled);
|
|
1166
|
+
if (own)
|
|
1167
|
+
runRowAction(node, own);
|
|
1168
|
+
else if (key === "F2")
|
|
1169
|
+
startRename(node);
|
|
1170
|
+
else if (key === "Delete")
|
|
1171
|
+
void requestDelete(node);
|
|
1172
|
+
else
|
|
1173
|
+
startCreate(node);
|
|
1174
|
+
}
|
|
999
1175
|
else if (key.length === 1 && !ctrlKey && !metaKey && !altKey && key !== " ") {
|
|
1000
1176
|
const now = performance.now();
|
|
1001
1177
|
const typed = typeaheadRef.current;
|
|
@@ -1041,7 +1217,20 @@ export function TreeView(props) {
|
|
|
1041
1217
|
contextItems.push({ value: "delete", label: "Delete", icon: _jsx(InlineIcon, { icon: iconTrash }), shortcut: "Delete", tone: "danger", disabled: !allowed("delete", contextNode) });
|
|
1042
1218
|
}
|
|
1043
1219
|
}
|
|
1220
|
+
// The page's own actions on the row, after the tree's.
|
|
1221
|
+
const ownActions = contextNode ? rowActionsOf(contextNode) : [];
|
|
1222
|
+
if (ownActions.length > 0 && contextItems.length > 0)
|
|
1223
|
+
contextItems.push({ separator: true });
|
|
1224
|
+
for (const action of ownActions) {
|
|
1225
|
+
contextItems.push({ value: `row:${action.id}`, label: action.label, description: action.description, icon: action.icon, shortcut: action.shortcut, tone: action.tone, disabled: action.disabled });
|
|
1226
|
+
}
|
|
1044
1227
|
function onContextSelect(action) {
|
|
1228
|
+
if (action.startsWith("row:")) {
|
|
1229
|
+
const own = ownActions.find(each => `row:${each.id}` === action);
|
|
1230
|
+
if (own && contextNode)
|
|
1231
|
+
runRowAction(contextNode, own);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1045
1234
|
if (action === "create")
|
|
1046
1235
|
startCreate(contextNode ?? null);
|
|
1047
1236
|
else if (action === "update" && contextNode)
|
|
@@ -1076,8 +1265,12 @@ export function TreeView(props) {
|
|
|
1076
1265
|
},
|
|
1077
1266
|
reveal: (revealValue, focus = true) => {
|
|
1078
1267
|
const node = store.get(revealValue);
|
|
1079
|
-
if (!node)
|
|
1268
|
+
if (!node) {
|
|
1269
|
+
const key = treeKey(revealValue);
|
|
1270
|
+
pendingRevealRef.current = key === null ? null : { key, focus };
|
|
1080
1271
|
return;
|
|
1272
|
+
}
|
|
1273
|
+
pendingRevealRef.current = null;
|
|
1081
1274
|
if (focus) {
|
|
1082
1275
|
focusNode(node);
|
|
1083
1276
|
return;
|
|
@@ -1099,6 +1292,10 @@ export function TreeView(props) {
|
|
|
1099
1292
|
afterCheck();
|
|
1100
1293
|
},
|
|
1101
1294
|
getItems: () => store.items(),
|
|
1295
|
+
getItem: itemValue => {
|
|
1296
|
+
const node = store.get(itemValue);
|
|
1297
|
+
return node && !node.draft ? node.item : undefined;
|
|
1298
|
+
},
|
|
1102
1299
|
startCreate: parentValue => startCreate(parentValue === null ? null : (store.get(parentValue) ?? null)),
|
|
1103
1300
|
startRename: renameValue => {
|
|
1104
1301
|
const node = store.get(renameValue);
|
|
@@ -1126,6 +1323,7 @@ export function TreeView(props) {
|
|
|
1126
1323
|
setMotion(null);
|
|
1127
1324
|
bump();
|
|
1128
1325
|
},
|
|
1326
|
+
apply: (changes, rules) => applyChanges(changes, rules),
|
|
1129
1327
|
}));
|
|
1130
1328
|
// Rendering
|
|
1131
1329
|
const showCounts = checkable && cascade && counts;
|
|
@@ -1133,6 +1331,8 @@ export function TreeView(props) {
|
|
|
1133
1331
|
const rendered = new Set();
|
|
1134
1332
|
/** How many siblings a row has: all of them on the server when the loader said, loaded or not. */
|
|
1135
1333
|
function setSize(node) {
|
|
1334
|
+
if (store.filtered)
|
|
1335
|
+
return store.shownChildCount(node.parent);
|
|
1136
1336
|
const loaded = node.parent ? node.parent.children.length : store.roots.length;
|
|
1137
1337
|
const total = store.pagesOf(node.parent)?.total;
|
|
1138
1338
|
return total === undefined ? loaded : Math.max(total, loaded);
|
|
@@ -1191,11 +1391,16 @@ export function TreeView(props) {
|
|
|
1191
1391
|
if (allowed("delete", node)) {
|
|
1192
1392
|
actions.push(_jsx(Button, { variant: "ghost", size: "sm", hoverTone: "danger", className: "nx-tree__action", icon: _jsx(InlineIcon, { icon: iconTrash }), "aria-label": `Delete ${node.label}`, title: "Delete (Delete)", tabIndex: -1, onMouseDown: keepEditorFocus, onClick: () => void requestDelete(node) }, "delete"));
|
|
1193
1393
|
}
|
|
1394
|
+
for (const action of rowActionsOf(node)) {
|
|
1395
|
+
if (!action.inline)
|
|
1396
|
+
continue;
|
|
1397
|
+
actions.push(_jsx(Button, { variant: "ghost", size: "sm", hoverTone: action.tone, className: "nx-tree__action", icon: action.icon, "aria-label": `${action.label}: ${node.label}`, title: action.shortcut ? `${action.label} (${action.shortcut})` : action.label, tabIndex: -1, disabled: action.disabled, "data-action": action.id, onMouseDown: keepEditorFocus, onClick: () => runRowAction(node, action) }, `row:${action.id}`));
|
|
1398
|
+
}
|
|
1194
1399
|
}
|
|
1195
|
-
return (_jsxs("div", { role: ghost ? undefined : "treeitem", className: cx("nx-tree__row", locked && "nx-tree__row--locked", edit && "nx-tree__row--editing"), 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-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 => {
|
|
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 => {
|
|
1196
1401
|
if (input)
|
|
1197
1402
|
input.indeterminate = state === "mixed";
|
|
1198
|
-
} }) })), 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));
|
|
1199
1404
|
}
|
|
1200
1405
|
const end = Math.min(range.end, total);
|
|
1201
1406
|
const start = Math.min(range.start, end);
|
|
@@ -1228,6 +1433,8 @@ export function TreeView(props) {
|
|
|
1228
1433
|
}
|
|
1229
1434
|
const allState = checkable ? store.allState() : null;
|
|
1230
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;
|
|
1231
1438
|
const hasParents = store.roots.some(root => root.children.length > 0 || root.expanded);
|
|
1232
1439
|
const hasLabel = hasContent(label);
|
|
1233
1440
|
const shownMessage = message ?? (hasContent(error) ? error : null);
|
|
@@ -1236,17 +1443,21 @@ export function TreeView(props) {
|
|
|
1236
1443
|
const rootWaiting = store.size === 0 && rootPages !== null && (rootPages.status === "idle" || rootPages.status === "loading");
|
|
1237
1444
|
const waiting = loading || rootWaiting;
|
|
1238
1445
|
const empty = total === 0 || rootWaiting;
|
|
1239
|
-
return (_jsxs("div", { className: cx("nx-field nx-field--standard", `nx-field--${size}`, "nx-tree-field", 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
|
|
1240
1449
|
? "Loading…"
|
|
1241
1450
|
: checkable
|
|
1242
|
-
?
|
|
1243
|
-
|
|
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,
|
|
1244
1455
|
// As tall as all of its rows at every moment of a commit, not only once it has finished. A scroll that
|
|
1245
1456
|
// moves the window makes React take the rows that leave it out before it puts the new ones in and sets
|
|
1246
1457
|
// this padding, and a layout in between - Chrome lays the page out inside the removal of a focused row,
|
|
1247
1458
|
// and so may any focusout listener that measures - would find the list short of the scroll position and
|
|
1248
1459
|
// clamp it: a jump to the end came back up to 1,120px (nexus.web, 2026-10-09).
|
|
1249
|
-
style: { boxSizing: "border-box", minHeight: 2 * PAD + total * rowHeight, paddingTop: PAD + start * rowHeight, paddingBottom: PAD + bottom }, children: body })) }) })] }), (shownMessage || hasContent(hint)) && (_jsx("div", { className: "nx-field__footer", children: _jsxs("p", { id: messageId, className: cx("nx-field__message", shownMessage && "nx-field__message--error"), role: message ? "alert" : undefined, children: [shownMessage && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: shownMessage ?? hint })] }) })), name && formValues.map(item => _jsx("input", { type: "hidden", name: name, form: form, disabled: disabled, value: String(item) }, String(item))), lazy && (_jsx("span", { className: "nx-visually-hidden", role: "status", "aria-live": "polite", children: announcement })), readOnlyNotice.notice, lockedNotice.notice, partialNotice.notice] }));
|
|
1460
|
+
style: { boxSizing: "border-box", minHeight: 2 * PAD + total * rowHeight, paddingTop: PAD + start * rowHeight, paddingBottom: PAD + bottom }, children: body })) }) })] }), (shownMessage || hasContent(hint)) && (_jsx("div", { className: "nx-field__footer", children: _jsxs("p", { id: messageId, className: cx("nx-field__message", shownMessage && "nx-field__message--error"), role: message ? "alert" : undefined, children: [shownMessage && _jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: shownMessage ?? hint })] }) })), name && formValues.map(item => _jsx("input", { type: "hidden", name: name, form: form, disabled: disabled, value: String(item) }, String(item))), lazy && (_jsx("span", { className: "nx-visually-hidden", role: "status", "aria-live": "polite", children: announcement })), readOnlyNotice.notice, lockedNotice.notice, partialNotice.notice, addNotice.notice] }));
|
|
1250
1461
|
}
|
|
1251
1462
|
/** The name box of a row being added or renamed. Enter saves, Esc cancels, and leaving it saves. */
|
|
1252
1463
|
function TreeEditor({ initial, select, maxLength, saving, label, invalid, describedBy, onText, onCommit, onCancel }) {
|
|
@@ -5,16 +5,29 @@ import type { PopupSize } from "./Popup.tsx";
|
|
|
5
5
|
export type MessageTone = "neutral" | "info" | "success" | "warning" | "danger" | "question";
|
|
6
6
|
export interface MessageBoxOptions {
|
|
7
7
|
title: ReactNode;
|
|
8
|
-
/** Text under the title. */
|
|
8
|
+
/** Text under the title, in the box's muted text. */
|
|
9
9
|
message?: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* What the question is about, in full, after the message and in the normal text colour: a record's details
|
|
12
|
+
* (`<CrudDetails row columns />`), a list. It scrolls with the box's body while the buttons stay in view; give the box a
|
|
13
|
+
* width that suits it (`size: "lg"` or `"xl"`).
|
|
14
|
+
*/
|
|
15
|
+
details?: ReactNode;
|
|
10
16
|
/** Colors the icon: `info`, `success`, `warning`, `danger`, `question`. `neutral` shows no icon. */
|
|
11
17
|
tone?: MessageTone;
|
|
12
18
|
/** Replaces the tone's icon; `null` shows none. */
|
|
13
19
|
icon?: ReactNode | null;
|
|
14
20
|
/** Default `sm`. */
|
|
15
21
|
size?: PopupSize;
|
|
16
|
-
/** The close button
|
|
22
|
+
/** The close button and Esc dismiss the box. Default true. */
|
|
17
23
|
closable?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A click outside dismisses the box. Default: as `closable`. `false` keeps a box with much to read (a question with its
|
|
26
|
+
* `details`) open through a stray click, while Esc and the close button still dismiss it.
|
|
27
|
+
*/
|
|
28
|
+
closeOnBackdrop?: boolean;
|
|
29
|
+
/** Dismisses the box when it aborts, as Esc would: the form or the page that asked has gone. */
|
|
30
|
+
signal?: AbortSignal;
|
|
18
31
|
}
|
|
19
32
|
export interface MessageBoxButton<T extends string = string> {
|
|
20
33
|
value: T;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { iconAlertCircle, iconAlertTriangle, iconCircleCheck, iconHelpCircle, iconInfoCircle, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
5
|
import { Button } from "../Documents/Button.js";
|
|
@@ -14,40 +14,85 @@ const TONE_ICONS = {
|
|
|
14
14
|
question: iconHelpCircle,
|
|
15
15
|
};
|
|
16
16
|
let promptCounter = 0;
|
|
17
|
+
let messageCounter = 0;
|
|
18
|
+
/*
|
|
19
|
+
* A held Enter, or one pressed again at once (a double press), never answers a box. The Enter that opened a question (in
|
|
20
|
+
* a form's box) would otherwise press the Confirm it gave the focus to - by the key's repeat, or by the second tap of a
|
|
21
|
+
* double press - before anything in the box was read. Any Enter within this long of the one before it is held.
|
|
22
|
+
*/
|
|
23
|
+
const HASTY_ENTER_MS = 300;
|
|
24
|
+
let lastEnterAt = Number.NEGATIVE_INFINITY;
|
|
25
|
+
function holdHastyEnter(event) {
|
|
26
|
+
if (event.key !== "Enter")
|
|
27
|
+
return;
|
|
28
|
+
const gap = event.timeStamp - lastEnterAt;
|
|
29
|
+
lastEnterAt = event.timeStamp;
|
|
30
|
+
if (!(event.target instanceof Element) || !event.target.closest(".nx-popup--message"))
|
|
31
|
+
return;
|
|
32
|
+
if (event.repeat || gap < HASTY_ENTER_MS)
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
}
|
|
35
|
+
// Listening from the start: the Enter that opens a box comes before the box.
|
|
36
|
+
if (typeof window !== "undefined")
|
|
37
|
+
window.addEventListener("keydown", holdHastyEnter, true);
|
|
17
38
|
function toneIcon(tone, icon) {
|
|
18
39
|
if (icon !== undefined)
|
|
19
40
|
return icon;
|
|
20
41
|
const data = TONE_ICONS[tone];
|
|
21
42
|
return data ? _jsx(InlineIcon, { icon: data }) : null;
|
|
22
43
|
}
|
|
44
|
+
/** The muted message, then the details in the normal text colour; each with an id the box is described by. */
|
|
45
|
+
function messageBody(message, details, id) {
|
|
46
|
+
return (_jsxs(_Fragment, { children: [message && (_jsx("div", { id: `${id}-text`, className: "nx-message__text", children: message })), details && (_jsx("div", { id: `${id}-details`, className: "nx-message__details", children: details }))] }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What describes a box to a screen reader: its message and its details, read with the title as the focus enters it, so
|
|
50
|
+
* a question's Confirm is never reached before what it confirms was heard.
|
|
51
|
+
*/
|
|
52
|
+
function describedBy(message, details, id) {
|
|
53
|
+
return cx(message ? `${id}-text` : undefined, details ? `${id}-details` : undefined) || undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Dismisses an open box when the signal aborts; nothing is left listening once it has closed. */
|
|
56
|
+
function closeOnAbort(popup, signal) {
|
|
57
|
+
if (!signal)
|
|
58
|
+
return;
|
|
59
|
+
const dismiss = () => popup.close();
|
|
60
|
+
signal.addEventListener("abort", dismiss, { once: true });
|
|
61
|
+
void popup.closed.then(() => signal.removeEventListener("abort", dismiss));
|
|
62
|
+
}
|
|
23
63
|
/**
|
|
24
64
|
* A message box with the given buttons. Resolves with the clicked button's value, or `null` when the box was
|
|
25
65
|
* dismissed with the close button, Esc, or a click outside.
|
|
26
66
|
*/
|
|
27
67
|
export function showMessageBox(options) {
|
|
28
|
-
const { title, message, tone = "neutral", icon, size = "sm", closable = true, buttons } = options;
|
|
68
|
+
const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, buttons } = options;
|
|
69
|
+
if (signal?.aborted)
|
|
70
|
+
return Promise.resolve(null);
|
|
29
71
|
const cautious = tone === "danger" || tone === "warning";
|
|
30
72
|
const explicit = buttons.findIndex(button => button.autoFocus);
|
|
31
73
|
const focusIndex = explicit >= 0 ? explicit : cautious ? 0 : buttons.length - 1;
|
|
32
74
|
let result = null;
|
|
75
|
+
const id = `nx-message-${++messageCounter}`;
|
|
33
76
|
const popup = openPopup({
|
|
34
77
|
title,
|
|
35
78
|
icon: toneIcon(tone, icon),
|
|
36
79
|
size,
|
|
37
80
|
closable,
|
|
38
|
-
closeOnBackdrop
|
|
81
|
+
closeOnBackdrop,
|
|
39
82
|
closeOnEscape: closable,
|
|
40
83
|
variant: "dialog",
|
|
41
84
|
minimizable: false,
|
|
42
85
|
maximizable: false,
|
|
43
86
|
className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
|
|
44
87
|
initialFocus: "[data-nx-focus]",
|
|
45
|
-
|
|
88
|
+
"aria-describedby": describedBy(message, details, id),
|
|
89
|
+
content: message || details ? messageBody(message, details, id) : null,
|
|
46
90
|
footer: handle => buttons.map((button, index) => (_jsx(Button, { variant: button.variant ?? (index === buttons.length - 1 ? (tone === "danger" ? "danger" : "primary") : "secondary"), "data-nx-focus": index === focusIndex || undefined, onClick: () => {
|
|
47
91
|
result = button.value;
|
|
48
92
|
handle.close();
|
|
49
93
|
}, children: button.label }, button.value))),
|
|
50
94
|
});
|
|
95
|
+
closeOnAbort(popup, signal);
|
|
51
96
|
return popup.closed.then(() => result);
|
|
52
97
|
}
|
|
53
98
|
function options(input) {
|
|
@@ -72,7 +117,9 @@ function confirm(input) {
|
|
|
72
117
|
}
|
|
73
118
|
/** A text box with Cancel and OK. Resolves with the text, or `null` when cancelled. */
|
|
74
119
|
function prompt(input) {
|
|
75
|
-
const { title, message, tone = "neutral", icon, size = "sm", closable = true, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
|
|
120
|
+
const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
|
|
121
|
+
if (signal?.aborted)
|
|
122
|
+
return Promise.resolve(null);
|
|
76
123
|
const formId = `nx-prompt-${++promptCounter}`;
|
|
77
124
|
let result = null;
|
|
78
125
|
const popup = openPopup({
|
|
@@ -80,20 +127,22 @@ function prompt(input) {
|
|
|
80
127
|
icon: toneIcon(tone, icon),
|
|
81
128
|
size,
|
|
82
129
|
closable,
|
|
83
|
-
closeOnBackdrop
|
|
130
|
+
closeOnBackdrop,
|
|
84
131
|
closeOnEscape: closable,
|
|
85
132
|
variant: "dialog",
|
|
86
133
|
minimizable: false,
|
|
87
134
|
maximizable: false,
|
|
88
135
|
className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
|
|
89
136
|
initialFocus: `#${formId} input`,
|
|
137
|
+
"aria-describedby": describedBy(message, details, formId),
|
|
90
138
|
content: handle => (_jsxs("form", { id: formId, className: "nx-message__form", onSubmit: (event) => {
|
|
91
139
|
event.preventDefault();
|
|
92
140
|
result = String(new FormData(event.currentTarget).get("value") ?? "");
|
|
93
141
|
handle.close();
|
|
94
|
-
}, children: [message
|
|
142
|
+
}, children: [messageBody(message, details, formId), _jsx(TextBox, { name: "value", label: label, type: inputType, defaultValue: defaultValue, placeholder: placeholder, required: required })] })),
|
|
95
143
|
footer: handle => (_jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => handle.close(), children: cancelLabel ?? "Cancel" }), _jsx(Button, { type: "submit", form: formId, variant: "primary", children: confirmLabel ?? "OK" })] })),
|
|
96
144
|
});
|
|
145
|
+
closeOnAbort(popup, signal);
|
|
97
146
|
return popup.closed.then(() => result);
|
|
98
147
|
}
|
|
99
148
|
/**
|