nexus-shared 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -5,6 +5,8 @@ import { motionAllowed } from "../../Helpers/AnimationHelpers.js";
|
|
|
5
5
|
import { isAbortError } from "../../Helpers/ApiResponses.js";
|
|
6
6
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
7
7
|
import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
|
+
import { isAwaitedCrudField } from "../../Helpers/CrudDetailsTabs.js";
|
|
9
|
+
import { crudDetailsRuns } from "../../Helpers/FormConfirm.js";
|
|
8
10
|
import { readPath } from "../../Helpers/TableHelpers.js";
|
|
9
11
|
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
10
12
|
import { TableCellValue } from "../Viewers/DataTableParts.js";
|
|
@@ -15,8 +17,12 @@ import { MarkdownView } from "../Viewers/MarkdownView.js";
|
|
|
15
17
|
* not drawn.
|
|
16
18
|
*/
|
|
17
19
|
const usedHeight = (element) => parseFloat(getComputedStyle(element).height);
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A record's fields as label and value, two to a line where there is room, in the formats of the table's columns. Where
|
|
22
|
+
* columns name a `group` (a table's band), each run of neighbouring columns with one group is a section under that
|
|
23
|
+
* heading; columns with no group draw as before, without one.
|
|
24
|
+
*/
|
|
25
|
+
export function CrudDetails({ row, columns, load, render, currency }) {
|
|
20
26
|
const [record, setRecord] = useState(row);
|
|
21
27
|
const [state, setState] = useState(load ? "loading" : "done");
|
|
22
28
|
const rootRef = useRef(null);
|
|
@@ -25,7 +31,7 @@ export function CrudDetails({ row, columns, load, render }) {
|
|
|
25
31
|
const preferences = useDatePreferences();
|
|
26
32
|
const timeZone = useBrowserTimeZone();
|
|
27
33
|
const locale = useNumberLocale();
|
|
28
|
-
const options = useMemo(() => ({ preferences, timeZone: timeZone ?? undefined, locale }), [preferences, timeZone, locale]);
|
|
34
|
+
const options = useMemo(() => ({ preferences, timeZone: timeZone ?? undefined, locale, currency }), [preferences, timeZone, locale, currency]);
|
|
29
35
|
useEffect(() => {
|
|
30
36
|
if (!load)
|
|
31
37
|
return;
|
|
@@ -64,6 +70,50 @@ export function CrudDetails({ row, columns, load, render }) {
|
|
|
64
70
|
}, [record]);
|
|
65
71
|
// A field the list's row does not carry can only be filled by the full record. Columns that draw or work out their
|
|
66
72
|
// own value (`render`, `value`) draw as always, because only they know what they read.
|
|
67
|
-
const awaited = (column) =>
|
|
68
|
-
|
|
73
|
+
const awaited = (column) => isAwaitedCrudField(column, record, state);
|
|
74
|
+
// Sections only where a column names a group: without one the list is drawn exactly as it always was.
|
|
75
|
+
const runs = useMemo(() => (columns.some(column => column.group) ? crudDetailsRuns(columns) : null), [columns]);
|
|
76
|
+
const list = (fields) => (_jsx("dl", { className: "nx-crud-details__list", children: fields.map(column => (_jsxs("div", { className: cx("nx-crud-details__item", column.wrap && "nx-crud-details__item--wide"), children: [_jsx("dt", { className: "nx-crud-details__label", children: column.label }), _jsx("dd", { className: "nx-crud-details__value", children: awaited(column) ? (state === "failed" ? _jsx("span", { className: "nx-crud-details__missing", children: "Not loaded" }) : null) : column.markdown && !column.render ? (_jsx(MarkdownView, { value: String(readPath(record, column.key) ?? ""), placeholder: `No ${column.label.toLowerCase()}` })) : (_jsx(TableCellValue, { column: column, row: record, options: options })) })] }, column.key))) }));
|
|
77
|
+
return (_jsx("div", { ref: rootRef, className: "nx-crud-details", "aria-busy": state === "loading" || undefined, children: render
|
|
78
|
+
? render(record)
|
|
79
|
+
: runs
|
|
80
|
+
? runs.map((run, index) => (_jsxs("div", { className: "nx-crud-details__section", children: [run.group && _jsx("h3", { className: "nx-crud-details__heading", children: run.group }), list(run.columns)] }, `${index}:${run.group ?? ""}`)))
|
|
81
|
+
: list(columns) }));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The footer of a tab in a record's workspace (`CrudDetailsOptions.tabs`): one bar, its words at the start and its
|
|
85
|
+
* buttons at the end, put last in the tab. While the tab scrolls it stays at the bottom of the workspace's panel, so a
|
|
86
|
+
* long form's Save never falls below the window; at the end of the tab it sits in its place. It is drawn on the panel's
|
|
87
|
+
* own surface under a hairline, across the panel's whole width, with the popup footer's room, and on a phone its words
|
|
88
|
+
* and its buttons wrap as whole groups. Outside a workspace it is a plain bar where it stands.
|
|
89
|
+
*
|
|
90
|
+
* A field the keyboard moves to is scrolled into view above it, never under it.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* <ApiForm form={form} buttons={false} api={STAYS.create} inputs={STAY_INPUTS} />
|
|
94
|
+
* <CrudTabFooter>
|
|
95
|
+
* <p role="status" aria-live="polite">3 nights in a double room · To pay Rs. 10,500.00</p>
|
|
96
|
+
* <FormButtons form={form} submitLabel="Save" />
|
|
97
|
+
* </CrudTabFooter>
|
|
98
|
+
*/
|
|
99
|
+
export function CrudTabFooter({ children, className }) {
|
|
100
|
+
const ref = useRef(null);
|
|
101
|
+
// Its height is the room the panel keeps at its foot when the focus scrolls a field into view (`scroll-padding`).
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
|
+
const bar = ref.current;
|
|
104
|
+
const panel = bar?.closest(".nx-crud-workspace__panel");
|
|
105
|
+
if (!bar || !panel)
|
|
106
|
+
return;
|
|
107
|
+
const write = () => panel.style.setProperty("--nx-crud-tab-footer-size", `${bar.getBoundingClientRect().height}px`);
|
|
108
|
+
write();
|
|
109
|
+
if (typeof ResizeObserver === "undefined")
|
|
110
|
+
return () => panel.style.removeProperty("--nx-crud-tab-footer-size");
|
|
111
|
+
const observer = new ResizeObserver(write);
|
|
112
|
+
observer.observe(bar);
|
|
113
|
+
return () => {
|
|
114
|
+
observer.disconnect();
|
|
115
|
+
panel.style.removeProperty("--nx-crud-tab-footer-size");
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
return (_jsx("div", { ref: ref, className: cx("nx-crud-tab-footer", className), children: children }));
|
|
69
119
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CrudRowForm } from "../../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { FormValues } from "../../Interfaces/FormInterfaces.ts";
|
|
3
|
+
/** What `openCrudRowForm` is opened with. */
|
|
4
|
+
export interface CrudRowFormRequest<TRow extends FormValues = FormValues, TValues extends FormValues = FormValues> {
|
|
5
|
+
form: CrudRowForm<TRow, TValues>;
|
|
6
|
+
/** The record, as the list has it when the action was chosen. */
|
|
7
|
+
row: TRow;
|
|
8
|
+
/** The record's key as text: one popup per record and row form. */
|
|
9
|
+
key: string;
|
|
10
|
+
/** What the endpoints are called with in `{id}`: the form's `target`, else the record's key. */
|
|
11
|
+
id: string | number;
|
|
12
|
+
/** The record's key field, sent beside `id` as the details are. */
|
|
13
|
+
rowKey: string;
|
|
14
|
+
/** Tells one CRUD's popups from another's. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** The record's name, under the popup's title. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** After a save: the answer and the values sent (before `transform`). The popup closes itself. */
|
|
19
|
+
onSaved: (result: unknown, values: TValues) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Opens a row form's popup: one per record and row form, brought back if it is open. Its values are read first when it
|
|
23
|
+
* has a `load` - and until they are, Save is out of reach: a read that fails (an answer that is not ok, or no record)
|
|
24
|
+
* shows "Could not load the values" with Retry and no Save, so an empty form can never be saved over the record.
|
|
25
|
+
*/
|
|
26
|
+
export declare function openCrudRowForm<TRow extends FormValues, TValues extends FormValues>(request: CrudRowFormRequest<TRow, TValues>): void;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { unwrapApiResult } from "../../Helpers/ApiResponses.js";
|
|
5
|
+
import { isApiEndpoint } from "../../Helpers/ApiRoutes.js";
|
|
6
|
+
import { FormStore } from "../../Helpers/FormStore.js";
|
|
7
|
+
import { api as browserApi } from "../../Services/BrowserApi.js";
|
|
8
|
+
import { openPopup } from "../Layouts/PopupHost.js";
|
|
9
|
+
import { popupRuleFor } from "../Layouts/PopupRules.js";
|
|
10
|
+
import { ApiForm } from "./ApiForm.js";
|
|
11
|
+
import { confirmDiscard, FormButtons } from "./SubmitForm.js";
|
|
12
|
+
/*
|
|
13
|
+
* A row action that opens a form of its own for the record, in a popup (`Crud` `rowForms`): its inputs, the values it
|
|
14
|
+
* opens on (`load`), where a save goes (`submit`, `transform`), and the answer handed back so the CRUD writes what it
|
|
15
|
+
* names into the record (`writes`, Helpers/CrudRowForms.ts). The CRUD decides where it is offered and does the writing.
|
|
16
|
+
*/
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
const isPromise = (value) => typeof value?.then === "function";
|
|
19
|
+
/** One popup's load state, shared by the form (which loads) and the footer (whose Save waits for it). */
|
|
20
|
+
function rowFormLoad(initial) {
|
|
21
|
+
let state = initial;
|
|
22
|
+
const listeners = new Set();
|
|
23
|
+
return {
|
|
24
|
+
get: () => state,
|
|
25
|
+
set(next) {
|
|
26
|
+
if (next === state)
|
|
27
|
+
return;
|
|
28
|
+
state = next;
|
|
29
|
+
for (const listener of listeners)
|
|
30
|
+
listener();
|
|
31
|
+
},
|
|
32
|
+
subscribe(listener) {
|
|
33
|
+
listeners.add(listener);
|
|
34
|
+
return () => {
|
|
35
|
+
listeners.delete(listener);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Opens a row form's popup: one per record and row form, brought back if it is open. Its values are read first when it
|
|
42
|
+
* has a `load` - and until they are, Save is out of reach: a read that fails (an answer that is not ok, or no record)
|
|
43
|
+
* shows "Could not load the values" with Retry and no Save, so an empty form can never be saved over the record.
|
|
44
|
+
*/
|
|
45
|
+
export function openCrudRowForm(request) {
|
|
46
|
+
const { form: params, key, name, label } = request;
|
|
47
|
+
const form = new FormStore();
|
|
48
|
+
const status = rowFormLoad(params.load ? "loading" : "done");
|
|
49
|
+
const popupKey = `${name}-${params.id}-${key}`;
|
|
50
|
+
openPopup({
|
|
51
|
+
key: popupKey,
|
|
52
|
+
reopen: "restore",
|
|
53
|
+
title: params.title ?? params.label,
|
|
54
|
+
description: label,
|
|
55
|
+
icon: params.icon,
|
|
56
|
+
size: params.size ?? "md",
|
|
57
|
+
closeOnBackdrop: false,
|
|
58
|
+
// A CRUD page's forms can be put aside on the dock, as its Add and Edit; a page's rule for the key may say no.
|
|
59
|
+
minimizable: popupRuleFor(popupKey)?.minimizable ?? true,
|
|
60
|
+
beforeClose: () => confirmDiscard(form),
|
|
61
|
+
content: popup => (_jsx(CrudRowFormBody, { request: request, form: form, status: status, onSaved: (result, values) => {
|
|
62
|
+
request.onSaved(result, values);
|
|
63
|
+
popup.close();
|
|
64
|
+
} })),
|
|
65
|
+
footer: popup => _jsx(CrudRowFormFooter, { form: form, status: status, submitLabel: params.submitLabel, onCancel: () => popup.close() }),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** The footer: the form's bar, its Save out of reach while the values could not be read. */
|
|
69
|
+
function CrudRowFormFooter({ form, status, submitLabel, onCancel }) {
|
|
70
|
+
const state = useSyncExternalStore(status.subscribe, status.get, status.get);
|
|
71
|
+
return _jsx(FormButtons, { form: form, submitLabel: submitLabel ?? "Save", readOnly: state === "failed", onCancel: onCancel });
|
|
72
|
+
}
|
|
73
|
+
/** The popup's form: the values read once (and again on Retry), the inputs from the record and what was read. */
|
|
74
|
+
function CrudRowFormBody({ request, form, status, onSaved, }) {
|
|
75
|
+
const { form: params, row, id, rowKey, label } = request;
|
|
76
|
+
const [loaded, setLoaded] = useState(null);
|
|
77
|
+
// The inputs: given, or made from the record and what was read - at once, or when a promise of them lands.
|
|
78
|
+
const made = useMemo(() => (typeof params.inputs === "function" ? params.inputs(row, loaded) : params.inputs), [params, row, loaded]);
|
|
79
|
+
const [later, setLater] = useState(null);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!isPromise(made))
|
|
82
|
+
return;
|
|
83
|
+
let live = true;
|
|
84
|
+
made.then(inputs => live && setLater(inputs), () => live && setLater([]));
|
|
85
|
+
return () => {
|
|
86
|
+
live = false;
|
|
87
|
+
};
|
|
88
|
+
}, [made]);
|
|
89
|
+
const inputs = isPromise(made) ? (later ?? []) : made;
|
|
90
|
+
const load = params.load;
|
|
91
|
+
const loadValue = load
|
|
92
|
+
? (signal) => {
|
|
93
|
+
status.set("loading");
|
|
94
|
+
const read = isApiEndpoint(load)
|
|
95
|
+
? browserApi.get(load, { params: { id, [rowKey]: id }, signal, toast: false }).then(unwrapApiResult)
|
|
96
|
+
: load(row, signal);
|
|
97
|
+
return Promise.resolve(read)
|
|
98
|
+
.then(values => {
|
|
99
|
+
// A read that answers no record is a failure: the form would otherwise open empty and save that.
|
|
100
|
+
if (!isRecord(values))
|
|
101
|
+
throw new Error("The values could not be read.");
|
|
102
|
+
setLoaded(values);
|
|
103
|
+
status.set("done");
|
|
104
|
+
return values;
|
|
105
|
+
})
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
if (!signal.aborted)
|
|
108
|
+
status.set("failed");
|
|
109
|
+
throw error;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
: undefined;
|
|
113
|
+
return (_jsx(ApiForm, { form: form, inputs: inputs, buttons: false, defaultValue: load ? undefined : row, loadValue: loadValue, api: params.submit, transform: params.transform ? values => params.transform(values, row, loaded) : undefined, "aria-label": `${params.title ?? params.label}: ${label}`, onSuccess: (result, values) => onSaved(result, values) }));
|
|
114
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CrudScope, CrudScopes } from "../../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { FormValues } from "../../Interfaces/FormInterfaces.ts";
|
|
3
|
+
/** Where a CRUD's scopes stand. */
|
|
4
|
+
export interface CrudScopeState<TRow extends FormValues = FormValues> {
|
|
5
|
+
/** The scope shown: `null` for the CRUD's own list. */
|
|
6
|
+
scope: CrudScope<TRow> | null;
|
|
7
|
+
/** The scopes offered now, in their order. */
|
|
8
|
+
offered: CrudScope<TRow>[];
|
|
9
|
+
/** While the kept choice is not read yet (the first draw, with `remember`): the CRUD asks for no list. */
|
|
10
|
+
waiting: boolean;
|
|
11
|
+
/** Shows a scope (`null`: the CRUD's own list) and keeps it as the person's choice. */
|
|
12
|
+
choose: (scope: string | null) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The scope a CRUD shows. The choice kept in this browser is read after the first draw (a server draw has no storage, and
|
|
16
|
+
* the browser's first draw must match it), and before the CRUD asks for a list; one the CRUD does not offer now (the
|
|
17
|
+
* person's rights changed) opens the CRUD's own list and stays kept for when it is offered again.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useCrudScopes<TRow extends FormValues>(scopes: CrudScopes<TRow> | undefined, callable: (scope: CrudScope<TRow>) => boolean): CrudScopeState<TRow>;
|
|
20
|
+
/**
|
|
21
|
+
* The toolbar's control for the scopes offered: one is a switch (on shows it), two or more are tabs with the CRUD's own
|
|
22
|
+
* list first. Each says its `description` as its tooltip. Nothing while no other list is offered.
|
|
23
|
+
*/
|
|
24
|
+
export declare function CrudScopeControl<TRow extends FormValues>({ scopes, state, plural }: {
|
|
25
|
+
scopes: CrudScopes<TRow> | undefined;
|
|
26
|
+
state: CrudScopeState<TRow>;
|
|
27
|
+
plural: string;
|
|
28
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { crudScopeStorageKey, offeredCrudScopes, readCrudScope, shownCrudScope, writeCrudScope } from "../../Helpers/CrudScopes.js";
|
|
5
|
+
import { TabButtons } from "../Documents/TabButtons.js";
|
|
6
|
+
import { Switch } from "../Inputs/CheckBox.js";
|
|
7
|
+
/*
|
|
8
|
+
* A CRUD whose list can be widened (`Crud` `scopes`): which list shows, the person's choice kept in this browser per
|
|
9
|
+
* viewer, and the switch (or the tabs) in the toolbar that picks it. The rules are in Helpers/CrudScopes.ts.
|
|
10
|
+
*/
|
|
11
|
+
/** This browser's local storage, or nothing where it cannot be reached (a server draw, storage switched off). */
|
|
12
|
+
function browserStorage() {
|
|
13
|
+
try {
|
|
14
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The scope a CRUD shows. The choice kept in this browser is read after the first draw (a server draw has no storage, and
|
|
22
|
+
* the browser's first draw must match it), and before the CRUD asks for a list; one the CRUD does not offer now (the
|
|
23
|
+
* person's rights changed) opens the CRUD's own list and stays kept for when it is offered again.
|
|
24
|
+
*/
|
|
25
|
+
export function useCrudScopes(scopes, callable) {
|
|
26
|
+
const storageKey = crudScopeStorageKey(scopes?.remember);
|
|
27
|
+
const [chosen, setChosen] = useState(null);
|
|
28
|
+
const [readKey, setReadKey] = useState(null);
|
|
29
|
+
// A choice made before the kept one was read is the person's: reading never replaces it.
|
|
30
|
+
const touched = useRef(false);
|
|
31
|
+
const latest = useRef(scopes);
|
|
32
|
+
latest.current = scopes;
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
if (!storageKey)
|
|
35
|
+
return;
|
|
36
|
+
const kept = readCrudScope(browserStorage(), storageKey);
|
|
37
|
+
if (!touched.current && kept !== undefined)
|
|
38
|
+
setChosen(kept);
|
|
39
|
+
setReadKey(storageKey);
|
|
40
|
+
}, [storageKey]);
|
|
41
|
+
const waiting = storageKey !== null && readKey !== storageKey;
|
|
42
|
+
const offered = offeredCrudScopes(scopes, callable);
|
|
43
|
+
const scope = waiting ? null : shownCrudScope(offered, chosen);
|
|
44
|
+
// The page hears of every change of the list shown: a pick, a kept choice opened, a scope no longer offered.
|
|
45
|
+
const shownId = scope?.id ?? null;
|
|
46
|
+
const told = useRef(null);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (told.current === shownId)
|
|
49
|
+
return;
|
|
50
|
+
told.current = shownId;
|
|
51
|
+
latest.current?.onChange?.(shownId);
|
|
52
|
+
}, [shownId]);
|
|
53
|
+
const choose = (next) => {
|
|
54
|
+
touched.current = true;
|
|
55
|
+
setChosen(next);
|
|
56
|
+
writeCrudScope(browserStorage(), storageKey, next);
|
|
57
|
+
};
|
|
58
|
+
return { scope, offered, waiting, choose };
|
|
59
|
+
}
|
|
60
|
+
/** The tab of the CRUD's own list among the scopes' tabs. */
|
|
61
|
+
const OWN_TAB = "";
|
|
62
|
+
/**
|
|
63
|
+
* The toolbar's control for the scopes offered: one is a switch (on shows it), two or more are tabs with the CRUD's own
|
|
64
|
+
* list first. Each says its `description` as its tooltip. Nothing while no other list is offered.
|
|
65
|
+
*/
|
|
66
|
+
export function CrudScopeControl({ scopes, state, plural }) {
|
|
67
|
+
const { offered, scope, choose } = state;
|
|
68
|
+
if (!scopes || offered.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (offered.length === 1) {
|
|
71
|
+
const only = offered[0];
|
|
72
|
+
return _jsx(Switch, { size: "sm", label: only.label, title: only.description, checked: scope?.id === only.id, onValueChange: on => choose(on ? only.id : null) });
|
|
73
|
+
}
|
|
74
|
+
const options = [{ value: OWN_TAB, label: scopes.label ?? "Own", title: scopes.description }, ...offered.map(option => ({ value: option.id, label: option.label, title: option.description }))];
|
|
75
|
+
return _jsx(TabButtons, { size: "sm", options: options, value: scope?.id ?? OWN_TAB, onChange: value => choose(value === OWN_TAB ? null : value), "aria-label": `${plural[0]?.toUpperCase() ?? ""}${plural.slice(1)} to show` });
|
|
76
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type ReactNode, type Ref } from "react";
|
|
2
|
+
import { type CrudBackend } from "../../Helpers/CrudBackend.ts";
|
|
3
|
+
import { type TreeItem } from "../../Helpers/TreeStore.ts";
|
|
4
|
+
import type { CrudActionName, CrudChange, CrudEndpoints, CrudFormOptions, CrudInputs, CrudTreePlacement } from "../../Interfaces/CrudInterfaces.ts";
|
|
5
|
+
import type { FormValues } from "../../Interfaces/FormInterfaces.ts";
|
|
6
|
+
import type { TableColumn } from "../../Interfaces/TableInterfaces.ts";
|
|
7
|
+
import { type TreeViewHandle, type TreeViewProps } from "../Inputs/TreeView.tsx";
|
|
8
|
+
/** What the panel beside the tree is handed (`details`). */
|
|
9
|
+
export interface CrudTreeDetails<TRow extends FormValues = FormValues> {
|
|
10
|
+
/** The record picked in the tree as it is now - after an edit, after the trash; `null` before a pick. */
|
|
11
|
+
row: TRow | null;
|
|
12
|
+
/** Its fields, as the details popup draws them; `null` before a pick. */
|
|
13
|
+
body: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Buttons for what its list offers: Add New under it, Edit, Move to trash and History for an active record; Recover
|
|
16
|
+
* and Delete permanently once it is in the trash; Move back to trash once deleted. `null` before a pick.
|
|
17
|
+
*/
|
|
18
|
+
actions: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
/** Props of `CrudTree`. */
|
|
21
|
+
export interface CrudTreeProps<TRow extends FormValues = FormValues, TItem extends TreeItem = TreeItem> {
|
|
22
|
+
/** The controller's endpoints, as the data table's `Crud` is given them. */
|
|
23
|
+
api: CrudEndpoints;
|
|
24
|
+
/** The Add and Edit forms' inputs: the data table's own, so a record is added the same way from either. */
|
|
25
|
+
inputs: CrudInputs<TRow>;
|
|
26
|
+
/** The fields the panel shows: the data table's columns. */
|
|
27
|
+
columns: readonly TableColumn<TRow>[];
|
|
28
|
+
/** Where the records sit in the tree, and the tree's keys and cache: the same placement a `Crud`'s `trees` names. */
|
|
29
|
+
place: CrudTreePlacement<TRow, TItem>;
|
|
30
|
+
/** The tree: how its levels load, its name for screen readers, `fill`, its look. Its keys and its cache come from `place`. */
|
|
31
|
+
tree: Omit<TreeViewProps<TItem>, "valueKey" | "hasChildrenKey" | "cacheKey" | "rowActions" | "add" | "onItemClick" | "selectedValue" | "ref" | "checkable" | "creatable" | "editable" | "deletable"> & {
|
|
32
|
+
loadChildren: NonNullable<TreeViewProps<TItem>["loadChildren"]>;
|
|
33
|
+
};
|
|
34
|
+
/** What one record is: "region". Default: the endpoints' subject. */
|
|
35
|
+
subject?: string;
|
|
36
|
+
subjectPlural?: string;
|
|
37
|
+
/** Tells popups apart. Default: the subject - so the tree's forms are the data table's. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** The field that tells records apart. Default `id`. */
|
|
40
|
+
rowKey?: string;
|
|
41
|
+
/** Names a record in titles and questions. Default: the first column's text. */
|
|
42
|
+
rowLabel?: (row: TRow) => string;
|
|
43
|
+
/** The Add and Edit forms: the data table's own `form`. */
|
|
44
|
+
form?: CrudFormOptions<TRow>;
|
|
45
|
+
/** The built-in actions to offer, of create, edit, history, trash, recover, delete and trashFromDelete. Default all. */
|
|
46
|
+
actions?: readonly CrudActionName[];
|
|
47
|
+
backend?: CrudBackend;
|
|
48
|
+
/** After every change made here. */
|
|
49
|
+
onChange?: (change: CrudChange<TRow>) => void;
|
|
50
|
+
/** The panel beside the tree, drawn by the page: the picked record, its fields, and its buttons. */
|
|
51
|
+
details?: (details: CrudTreeDetails<TRow>) => ReactNode;
|
|
52
|
+
ref?: Ref<CrudTreeHandle<TRow, TItem>>;
|
|
53
|
+
}
|
|
54
|
+
/** What `ref` gives on a `CrudTree`. */
|
|
55
|
+
export interface CrudTreeHandle<TRow extends FormValues = FormValues, TItem extends TreeItem = TreeItem> {
|
|
56
|
+
/** The tree's own handle: `refresh` for a Reload, `expand`, `reveal`. */
|
|
57
|
+
readonly tree: TreeViewHandle<TItem> | null;
|
|
58
|
+
/**
|
|
59
|
+
* Opens the Add form under an item, or with the new record's values alone. With `place.placeKeys` it always adds inside
|
|
60
|
+
* an item - the selected row when none is given, and nothing while none is - its place fixed in the form.
|
|
61
|
+
*/
|
|
62
|
+
create: (parent?: TItem | null, values?: Partial<TRow>) => void;
|
|
63
|
+
/** Opens a record's Edit form. */
|
|
64
|
+
edit: (row: TRow) => void;
|
|
65
|
+
/** Shows a record in the panel and selects its row, or neither. */
|
|
66
|
+
pick: (row: TRow | null) => void;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A controller's records as a tree, with Add New, Edit, Move to trash, Recover, Delete and History where they are shown:
|
|
70
|
+
* an icon button on each row for Add New under it, Edit and Move to trash (Insert, F2 and Delete on a focused row, all
|
|
71
|
+
* three in its context menu), Add New in the tree's toolbar, and every action of the picked record on the panel beside
|
|
72
|
+
* the tree. The forms and the calls are the data table's; a change is written into the tree and its kept pages, so no
|
|
73
|
+
* level is read again for it.
|
|
74
|
+
*
|
|
75
|
+
* A click or Enter on a row selects it - marked in the tree, and staying so while focus, the keys and the chevrons move
|
|
76
|
+
* on - and a record's row shows that record in the panel; a row that is not a record (a country above its regions)
|
|
77
|
+
* empties the panel and opens.
|
|
78
|
+
*
|
|
79
|
+
* With `place.placeKeys` the tree fixes where a record sits: every Add goes inside a row - the toolbar's inside the
|
|
80
|
+
* selected row, the one the panel shows, and saying why it cannot until there is one - and the Add and Edit forms show
|
|
81
|
+
* those fields read-only, saying why.
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* <AdminSplit>
|
|
85
|
+
* <CrudTree api={REGIONS} inputs={REGION_INPUTS} columns={REGION_COLUMNS} place={REGION_TREE} form={REGION_FORM}
|
|
86
|
+
* tree={{ loadChildren, fill: true, "aria-label": "Regions" }}
|
|
87
|
+
* details={({ row, body, actions }) => <AdminPanel title={row?.name ?? "Details"} action={actions}>{body}</AdminPanel>} />
|
|
88
|
+
* </AdminSplit>
|
|
89
|
+
*/
|
|
90
|
+
export declare function CrudTree<TRow extends FormValues = FormValues, TItem extends TreeItem = TreeItem>(props: CrudTreeProps<TRow, TItem>): import("react").JSX.Element;
|