nexus-shared 4.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
|
@@ -16,13 +16,20 @@ export interface CrudDetailsProps<TRow extends FormValues = FormValues> {
|
|
|
16
16
|
render?: (row: TRow) => ReactNode;
|
|
17
17
|
/** The currency of a `money` field without its own, as the table writes it (`DataTableProps.currency`). */
|
|
18
18
|
currency?: string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Drawn at the end of a section's heading, by the section's `group`: a section's own Edit button
|
|
21
|
+
* (`CrudDetailsOptions.sections`). Nothing for a section it answers nothing for.
|
|
22
|
+
*/
|
|
23
|
+
headingEnd?: (group: string) => ReactNode;
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
21
|
-
* A record's fields
|
|
22
|
-
* columns name a `group` (a table's band), each run of
|
|
23
|
-
*
|
|
26
|
+
* A record's fields, each a label and its value in a box: two pairs to a line where the details are wide, one where they
|
|
27
|
+
* are not, in the formats of the table's columns. Where columns name a `group` (a table's band), each run of
|
|
28
|
+
* neighbouring columns with one group is a section under a numbered heading ("01. Contact"); columns with no group come
|
|
29
|
+
* without one. A `wrap` column takes the whole line, and a `table` column whose value is a list of rows is a small table
|
|
30
|
+
* across it ("None" for an empty list).
|
|
24
31
|
*/
|
|
25
|
-
export declare function CrudDetails<TRow extends FormValues = FormValues>({ row, columns, load, render, currency }: CrudDetailsProps<TRow>): import("react").JSX.Element;
|
|
32
|
+
export declare function CrudDetails<TRow extends FormValues = FormValues>({ row, columns, load, render, currency, headingEnd }: CrudDetailsProps<TRow>): import("react").JSX.Element;
|
|
26
33
|
export interface CrudTabFooterProps {
|
|
27
34
|
/** Its words first (a `<p>`: what saving does), then the buttons (`FormButtons`, or `Button`s). */
|
|
28
35
|
children: ReactNode;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { motionAllowed } from "../../Helpers/AnimationHelpers.js";
|
|
5
5
|
import { isAbortError } from "../../Helpers/ApiResponses.js";
|
|
@@ -7,7 +7,7 @@ import { cx } from "../../Helpers/ClassHelpers.js";
|
|
|
7
7
|
import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
8
|
import { isAwaitedCrudField } from "../../Helpers/CrudDetailsTabs.js";
|
|
9
9
|
import { crudDetailsRuns } from "../../Helpers/FormConfirm.js";
|
|
10
|
-
import { readPath } from "../../Helpers/TableHelpers.js";
|
|
10
|
+
import { columnValue, formatCellText, readPath } from "../../Helpers/TableHelpers.js";
|
|
11
11
|
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
12
12
|
import { TableCellValue } from "../Viewers/DataTableParts.js";
|
|
13
13
|
import { MarkdownView } from "../Viewers/MarkdownView.js";
|
|
@@ -17,13 +17,46 @@ import { MarkdownView } from "../Viewers/MarkdownView.js";
|
|
|
17
17
|
* not drawn.
|
|
18
18
|
*/
|
|
19
19
|
const usedHeight = (element) => parseFloat(getComputedStyle(element).height);
|
|
20
|
+
/** A section's number before its heading: "01. ", "02. ", in the order the sections come. */
|
|
21
|
+
const sectionNumber = (index) => `${String(index + 1).padStart(2, "0")}. `;
|
|
22
|
+
/** A section's numbered heading, and what the page puts at its end (its Edit button) beside it on the same line. */
|
|
23
|
+
function CrudDetailsHeading({ number, group, end }) {
|
|
24
|
+
const heading = (_jsxs("h3", { className: "nx-crud-details__heading", children: [sectionNumber(number), group] }));
|
|
25
|
+
if (end === null || end === undefined || end === false)
|
|
26
|
+
return heading;
|
|
27
|
+
return (_jsxs("div", { className: "nx-crud-details__head", children: [heading, end] }));
|
|
28
|
+
}
|
|
29
|
+
/** One cell of a field's small table: the table's look of the value, but a yes or no in words (not the table's tick). */
|
|
30
|
+
function CrudDetailsCell({ column, row, options }) {
|
|
31
|
+
if (column.format === "boolean" && !column.options && !column.render)
|
|
32
|
+
return _jsx(_Fragment, { children: formatCellText(column, columnValue(column, row), options, row) });
|
|
33
|
+
return _jsx(TableCellValue, { column: column, row: row, options: options });
|
|
34
|
+
}
|
|
20
35
|
/**
|
|
21
|
-
* A
|
|
22
|
-
*
|
|
23
|
-
*
|
|
36
|
+
* A field whose value is a list of rows (`column.table`), as a small read-only table: a header row, each row's number
|
|
37
|
+
* first, every cell in its column's format (an option's tone as its tag, a yes or no in words). The Markdown view's
|
|
38
|
+
* table look, across the whole line, scrolling sideways inside itself when its columns are wider than the line.
|
|
24
39
|
*/
|
|
25
|
-
|
|
40
|
+
function CrudDetailsTable({ label, columns, rows, options }) {
|
|
41
|
+
return (_jsx("div", { className: "nx-markdown nx-crud-details__rows", children: _jsxs("table", { "aria-label": label, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "#" }), columns.map(column => (_jsx("th", { scope: "col", children: column.label }, column.key)))] }) }), _jsx("tbody", { children: rows.map((one, index) => (_jsxs("tr", { children: [_jsx("td", { children: index + 1 }), columns.map(column => (_jsx("td", { children: _jsx(CrudDetailsCell, { column: column, row: one, options: options }) }, column.key)))] }, index))) })] }) }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A record's fields, each a label and its value in a box: two pairs to a line where the details are wide, one where they
|
|
45
|
+
* are not, in the formats of the table's columns. Where columns name a `group` (a table's band), each run of
|
|
46
|
+
* neighbouring columns with one group is a section under a numbered heading ("01. Contact"); columns with no group come
|
|
47
|
+
* without one. A `wrap` column takes the whole line, and a `table` column whose value is a list of rows is a small table
|
|
48
|
+
* across it ("None" for an empty list).
|
|
49
|
+
*/
|
|
50
|
+
export function CrudDetails({ row, columns, load, render, currency, headingEnd }) {
|
|
26
51
|
const [record, setRecord] = useState(row);
|
|
52
|
+
// A record read again (a workspace's `refresh()`) is drawn in place, so what has the focus (a section's Edit) keeps it.
|
|
53
|
+
const given = useRef(row);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (row === given.current)
|
|
56
|
+
return;
|
|
57
|
+
given.current = row;
|
|
58
|
+
setRecord(current => ({ ...current, ...row }));
|
|
59
|
+
}, [row]);
|
|
27
60
|
const [state, setState] = useState(load ? "loading" : "done");
|
|
28
61
|
const rootRef = useRef(null);
|
|
29
62
|
// The details' height just before the full record went in, kept only when the change may be animated.
|
|
@@ -73,11 +106,20 @@ export function CrudDetails({ row, columns, load, render, currency }) {
|
|
|
73
106
|
const awaited = (column) => isAwaitedCrudField(column, record, state);
|
|
74
107
|
// Sections only where a column names a group: without one the list is drawn exactly as it always was.
|
|
75
108
|
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 =>
|
|
109
|
+
const list = (fields) => (_jsx("dl", { className: "nx-crud-details__list", children: fields.map(column => {
|
|
110
|
+
const waiting = awaited(column);
|
|
111
|
+
const rows = column.table && !waiting ? columnValue(column, record) : undefined;
|
|
112
|
+
if (column.table && Array.isArray(rows) && rows.length > 0) {
|
|
113
|
+
return (_jsxs("div", { className: "nx-crud-details__item nx-crud-details__item--table", children: [_jsx("dt", { className: "nx-crud-details__label", children: column.label }), _jsx("dd", { className: "nx-crud-details__table", children: _jsx(CrudDetailsTable, { label: column.label, columns: column.table, rows: rows.filter(one => typeof one === "object" && one !== null), options: options }) })] }, column.key));
|
|
114
|
+
}
|
|
115
|
+
return (_jsxs("div", { className: cx("nx-crud-details__item", (column.wrap || column.markdown) && "nx-crud-details__item--wide"), children: [_jsx("dt", { className: "nx-crud-details__label", children: column.label }), _jsx("dd", { className: "nx-crud-details__value", children: waiting ? (state === "failed" ? _jsx("span", { className: "nx-crud-details__missing", children: "Not loaded" }) : null) : column.table ? (_jsx("span", { className: "nx-crud-details__missing", children: "None" })) : 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));
|
|
116
|
+
}) }));
|
|
117
|
+
// Only the sections with a heading are numbered; a run of columns with no group has none.
|
|
118
|
+
let numbered = 0;
|
|
77
119
|
return (_jsx("div", { ref: rootRef, className: "nx-crud-details", "aria-busy": state === "loading" || undefined, children: render
|
|
78
120
|
? render(record)
|
|
79
121
|
: runs
|
|
80
|
-
? runs.map((run, index) => (_jsxs("div", { className: "nx-crud-details__section", children: [run.group && _jsx(
|
|
122
|
+
? runs.map((run, index) => (_jsxs("div", { className: "nx-crud-details__section", children: [run.group && _jsx(CrudDetailsHeading, { number: numbered++, group: run.group, end: headingEnd?.(run.group) }), list(run.columns)] }, `${index}:${run.group ?? ""}`)))
|
|
81
123
|
: list(columns) }));
|
|
82
124
|
}
|
|
83
125
|
/**
|
|
@@ -106,13 +148,37 @@ export function CrudTabFooter({ children, className }) {
|
|
|
106
148
|
return;
|
|
107
149
|
const write = () => panel.style.setProperty("--nx-crud-tab-footer-size", `${bar.getBoundingClientRect().height}px`);
|
|
108
150
|
write();
|
|
151
|
+
// Chrome scrolls a text box's caret into view, not the box: a text area whose first line is clear of the bar keeps
|
|
152
|
+
// its other lines under it. Once the browser has scrolled, the rest of the field is brought up above the bar, never
|
|
153
|
+
// so far that its top leaves the panel.
|
|
154
|
+
let frame = 0;
|
|
155
|
+
const onFocus = (event) => {
|
|
156
|
+
const field = event.target;
|
|
157
|
+
if (!(field instanceof HTMLElement) || bar.contains(field))
|
|
158
|
+
return;
|
|
159
|
+
cancelAnimationFrame(frame);
|
|
160
|
+
frame = requestAnimationFrame(() => {
|
|
161
|
+
const box = field.getBoundingClientRect();
|
|
162
|
+
const under = box.bottom + 4 - bar.getBoundingClientRect().top;
|
|
163
|
+
const room = box.top - (panel.getBoundingClientRect().top + 4);
|
|
164
|
+
const by = Math.min(under, room);
|
|
165
|
+
if (by > 0.5)
|
|
166
|
+
panel.scrollBy({ top: by });
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
panel.addEventListener("focusin", onFocus);
|
|
170
|
+
const stop = () => {
|
|
171
|
+
cancelAnimationFrame(frame);
|
|
172
|
+
panel.removeEventListener("focusin", onFocus);
|
|
173
|
+
panel.style.removeProperty("--nx-crud-tab-footer-size");
|
|
174
|
+
};
|
|
109
175
|
if (typeof ResizeObserver === "undefined")
|
|
110
|
-
return
|
|
176
|
+
return stop;
|
|
111
177
|
const observer = new ResizeObserver(write);
|
|
112
178
|
observer.observe(bar);
|
|
113
179
|
return () => {
|
|
114
180
|
observer.disconnect();
|
|
115
|
-
|
|
181
|
+
stop();
|
|
116
182
|
};
|
|
117
183
|
}, []);
|
|
118
184
|
return (_jsx("div", { ref: ref, className: cx("nx-crud-tab-footer", className), children: children }));
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
-
import { unwrapApiResult } from "../../Helpers/ApiResponses.js";
|
|
4
|
+
import { errorMessage, unwrapApiResult } from "../../Helpers/ApiResponses.js";
|
|
5
5
|
import { isApiEndpoint } from "../../Helpers/ApiRoutes.js";
|
|
6
|
+
import { formConfirmAsks } from "../../Helpers/FormConfirm.js";
|
|
7
|
+
import { crudRowFormAsksOnly, crudRowFormConfirm, crudRowFormQuestion } from "../../Helpers/CrudRowForms.js";
|
|
6
8
|
import { FormStore } from "../../Helpers/FormStore.js";
|
|
7
9
|
import { api as browserApi } from "../../Services/BrowserApi.js";
|
|
8
10
|
import { openPopup } from "../Layouts/PopupHost.js";
|
|
9
11
|
import { popupRuleFor } from "../Layouts/PopupRules.js";
|
|
12
|
+
import { toast } from "../Layouts/Toaster.js";
|
|
10
13
|
import { ApiForm } from "./ApiForm.js";
|
|
14
|
+
import { askFormQuestion } from "./FormQuestion.js";
|
|
11
15
|
import { confirmDiscard, FormButtons } from "./SubmitForm.js";
|
|
12
16
|
/*
|
|
13
17
|
* 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
|
|
15
|
-
* names into the record (`writes`, Helpers/CrudRowForms.ts).
|
|
18
|
+
* opens on (`load`), the question before a save (`confirm`), where a save goes (`submit`, `transform`), and the answer
|
|
19
|
+
* handed back so the CRUD writes what it names into the record (`writes`, Helpers/CrudRowForms.ts). One with no inputs
|
|
20
|
+
* is its question alone, with no popup. The CRUD decides where it is offered and does the writing.
|
|
16
21
|
*/
|
|
17
22
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
23
|
const isPromise = (value) => typeof value?.then === "function";
|
|
@@ -44,6 +49,10 @@ function rowFormLoad(initial) {
|
|
|
44
49
|
*/
|
|
45
50
|
export function openCrudRowForm(request) {
|
|
46
51
|
const { form: params, key, name, label } = request;
|
|
52
|
+
if (crudRowFormAsksOnly(params)) {
|
|
53
|
+
void askCrudRowForm(request);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
47
56
|
const form = new FormStore();
|
|
48
57
|
const status = rowFormLoad(params.load ? "loading" : "done");
|
|
49
58
|
const popupKey = `${name}-${params.id}-${key}`;
|
|
@@ -65,6 +74,55 @@ export function openCrudRowForm(request) {
|
|
|
65
74
|
footer: popup => _jsx(CrudRowFormFooter, { form: form, status: status, submitLabel: params.submitLabel, onCancel: () => popup.close() }),
|
|
66
75
|
});
|
|
67
76
|
}
|
|
77
|
+
/** The questions alone on their way, by popup key: choosing one again while it asks or sends does nothing. */
|
|
78
|
+
const asking = new Set();
|
|
79
|
+
/**
|
|
80
|
+
* A row form with no inputs: what `load` read (else the record as the list has it) through `transform`, asked about
|
|
81
|
+
* (`confirm`, else "{label} {name}?"), then sent. A read or a save that fails shows the API's message as a toast, and a
|
|
82
|
+
* refusal writes nothing; Cancel, Esc or the close button send nothing. One at a time per record and action.
|
|
83
|
+
*/
|
|
84
|
+
async function askCrudRowForm(request) {
|
|
85
|
+
const { form: params, row, id, rowKey, key, name, label } = request;
|
|
86
|
+
const busy = `${name}-${params.id}-${key}`;
|
|
87
|
+
if (asking.has(busy))
|
|
88
|
+
return;
|
|
89
|
+
asking.add(busy);
|
|
90
|
+
try {
|
|
91
|
+
let loaded = null;
|
|
92
|
+
const load = params.load;
|
|
93
|
+
if (load) {
|
|
94
|
+
if (isApiEndpoint(load)) {
|
|
95
|
+
// The API's own toast says why a read failed; an answer with no record says it here.
|
|
96
|
+
const answer = await browserApi.get(load, { params: { id, [rowKey]: id } });
|
|
97
|
+
if (!answer.ok)
|
|
98
|
+
return;
|
|
99
|
+
loaded = isRecord(answer.result) ? answer.result : null;
|
|
100
|
+
}
|
|
101
|
+
else
|
|
102
|
+
loaded = ((await load(row, new AbortController().signal)) ?? null);
|
|
103
|
+
if (!isRecord(loaded)) {
|
|
104
|
+
toast.error(`The values of ${label} could not be read.`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const values = (loaded ?? row);
|
|
109
|
+
const body = params.transform ? params.transform(values, row, loaded) : values;
|
|
110
|
+
const confirm = crudRowFormConfirm(params, row, loaded, label);
|
|
111
|
+
if (confirm && formConfirmAsks(confirm, values) && !(await askFormQuestion(crudRowFormQuestion(params, confirm, values, body))))
|
|
112
|
+
return;
|
|
113
|
+
// The API's toasts say it was done, or why not; the record is named in them.
|
|
114
|
+
const answer = await browserApi.send(params.submit, { body, params: { id, [rowKey]: id }, subject: label });
|
|
115
|
+
if (answer.ok)
|
|
116
|
+
request.onSaved(answer.result, values);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// A load, a transform or a rule of the question that throws: said, and nothing sent.
|
|
120
|
+
toast.error(errorMessage(error));
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
asking.delete(busy);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
68
126
|
/** The footer: the form's bar, its Save out of reach while the values could not be read. */
|
|
69
127
|
function CrudRowFormFooter({ form, status, submitLabel, onCancel }) {
|
|
70
128
|
const state = useSyncExternalStore(status.subscribe, status.get, status.get);
|
|
@@ -75,7 +133,9 @@ function CrudRowFormBody({ request, form, status, onSaved, }) {
|
|
|
75
133
|
const { form: params, row, id, rowKey, label } = request;
|
|
76
134
|
const [loaded, setLoaded] = useState(null);
|
|
77
135
|
// 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]);
|
|
136
|
+
const made = useMemo(() => (typeof params.inputs === "function" ? params.inputs(row, loaded) : (params.inputs ?? [])), [params, row, loaded]);
|
|
137
|
+
// The question before a save, for this record and what was read: the form asks it after its checks.
|
|
138
|
+
const confirm = useMemo(() => crudRowFormConfirm(params, row, loaded, label) ?? undefined, [params, row, loaded, label]);
|
|
79
139
|
const [later, setLater] = useState(null);
|
|
80
140
|
useEffect(() => {
|
|
81
141
|
if (!isPromise(made))
|
|
@@ -110,5 +170,5 @@ function CrudRowFormBody({ request, form, status, onSaved, }) {
|
|
|
110
170
|
});
|
|
111
171
|
}
|
|
112
172
|
: 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) }));
|
|
173
|
+
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, confirm: confirm, "aria-label": `${params.title ?? params.label}: ${label}`, onSuccess: (result, values) => onSaved(result, values) }));
|
|
114
174
|
}
|
|
@@ -3,10 +3,10 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { unwrapApiResult } from "../../Helpers/ApiResponses.js";
|
|
5
5
|
import { getCrudBackend } from "../../Helpers/CrudBackend.js";
|
|
6
|
-
import { actionAllowed, crudJoinedNames, crudJoinsMissing, crudRowActions, crudSavedCount, crudTreeChanges } from "../../Helpers/CrudHelpers.js";
|
|
6
|
+
import { actionAllowed, crudJoinedNames, crudJoinsMissing, crudRowActions, crudSavedCount, crudSavedRecord, crudTreeChanges } from "../../Helpers/CrudHelpers.js";
|
|
7
7
|
import { pluralize } from "../../Helpers/MessageBuilder.js";
|
|
8
8
|
import { getPermissions, permissionsVersion, subscribePermissions } from "../../Helpers/Permissions.js";
|
|
9
|
-
import { columnValue, formatCellText, readPath } from "../../Helpers/TableHelpers.js";
|
|
9
|
+
import { columnValue, formatCellText, namingColumn, readPath } from "../../Helpers/TableHelpers.js";
|
|
10
10
|
import { tableJoins } from "../../Helpers/TableJoins.js";
|
|
11
11
|
import { TREE_DEFAULT_KEYS, writeTreeChanges } from "../../Helpers/TreeStore.js";
|
|
12
12
|
import { api as browserApi } from "../../Services/BrowserApi.js";
|
|
@@ -92,7 +92,7 @@ export function CrudTree(props) {
|
|
|
92
92
|
const custom = latest.current.rowLabel;
|
|
93
93
|
if (custom)
|
|
94
94
|
return custom(row);
|
|
95
|
-
const first = latest.current.columns
|
|
95
|
+
const first = namingColumn(latest.current.columns);
|
|
96
96
|
const text = first ? formatCellText(first, columnValue(first, row), {}, row) : "";
|
|
97
97
|
return text || `${capitalize(subject)} ${keyOf(row)}`;
|
|
98
98
|
}
|
|
@@ -177,7 +177,7 @@ export function CrudTree(props) {
|
|
|
177
177
|
rowKey,
|
|
178
178
|
// Says where, in the title bar and on the dock: two Adds minimized inside two rows read apart.
|
|
179
179
|
label: inside ? `Inside ${itemLabel(inside)}` : undefined,
|
|
180
|
-
onSaved: (result,
|
|
180
|
+
onSaved: (result, sent, joined) => void afterSave(null, result, sent, joined),
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
/** Opens a record's Edit form; with `placeKeys` its place stays as it is, fixed, and moves only where it is offered. */
|
|
@@ -197,7 +197,7 @@ export function CrudTree(props) {
|
|
|
197
197
|
rowKey,
|
|
198
198
|
key: keyOf(row),
|
|
199
199
|
label: labelOf(row),
|
|
200
|
-
onSaved: (result,
|
|
200
|
+
onSaved: (result, sent, joined) => void afterSave(row, result, sent, joined),
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
@@ -207,10 +207,10 @@ export function CrudTree(props) {
|
|
|
207
207
|
* the tree counts it (`crudSavedCount`: a new one has no children, an edited one the count the tree keeps now). An
|
|
208
208
|
* edited record is finished from the row as the tree holds it now, not as the form opened it.
|
|
209
209
|
*/
|
|
210
|
-
async function afterSave(row, result,
|
|
210
|
+
async function afterSave(row, result, sent, joined) {
|
|
211
211
|
const action = row ? "edit" : "create";
|
|
212
212
|
const answered = backend.answeredRow(result);
|
|
213
|
-
let record =
|
|
213
|
+
let record = crudSavedRecord(row, sent, answered);
|
|
214
214
|
if (!answered && !readPath(record, rowKey)) {
|
|
215
215
|
const given = backend.answeredKey(result);
|
|
216
216
|
if (given !== null && !rowKey.includes("."))
|
|
@@ -18,8 +18,11 @@ export interface CrudWorkspaceProps<TRow extends FormValues = FormValues> {
|
|
|
18
18
|
columns: readonly TableColumn<TRow>[];
|
|
19
19
|
/** The currency of a `money` field without its own, as the table writes it. */
|
|
20
20
|
currency?: string | null;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The built-in details: the fields, with the `aside` beside them, less the fields the profile shows (`fields`), given
|
|
23
|
+
* the workspace's `refresh` (a section's own Edit reads the record again with it once saved).
|
|
24
|
+
*/
|
|
25
|
+
details: (without: readonly (TableColumn<TRow> | string)[] | undefined, refresh: () => Promise<TRow | null>) => ReactNode;
|
|
23
26
|
/** Starts reading the record from the details endpoint, once: called when the workspace is first drawn. */
|
|
24
27
|
load?: () => void;
|
|
25
28
|
/**
|
|
@@ -38,6 +41,11 @@ export interface CrudWorkspaceProps<TRow extends FormValues = FormValues> {
|
|
|
38
41
|
* closing, ask "Discard your changes?" while one has changes.
|
|
39
42
|
*/
|
|
40
43
|
guard: FormLeaveGuard;
|
|
44
|
+
/**
|
|
45
|
+
* Told the chosen tab when the workspace opens and on every switch: the Crud shows the popup's footer (View history,
|
|
46
|
+
* Edit, Close) only while the built-in Details tab is chosen.
|
|
47
|
+
*/
|
|
48
|
+
onTab?: (tab: string) => void;
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
51
|
* Asks "Discard your changes?" when a form in a place that is about to go away (a `FormLeaveGuard`: a tab of a record's
|
|
@@ -48,16 +56,18 @@ export declare function confirmLeaving(guard: FormLeaveGuard): Promise<boolean>;
|
|
|
48
56
|
/**
|
|
49
57
|
* A record's picture where it is shown large (a workspace's profile, a record page's hero): by its Drive params, a button
|
|
50
58
|
* that opens the pictures window, the record's initials standing in; or what the page draws in its place. `size` is a
|
|
51
|
-
* CSS length. Read-only shows the pictures with no way to change them.
|
|
59
|
+
* CSS length. Read-only shows the pictures with no way to change them. `round` draws it as a circle whatever the
|
|
60
|
+
* picture's own shape (a workspace's profile); the pictures window keeps the picture's shape and aspect.
|
|
52
61
|
*/
|
|
53
|
-
export declare function ProfilePicture({ picture, title, size, readOnly }: {
|
|
62
|
+
export declare function ProfilePicture({ picture, title, size, readOnly, round }: {
|
|
54
63
|
picture: CrudDetailsProfile["picture"];
|
|
55
64
|
title: ReactNode;
|
|
56
65
|
size: string;
|
|
57
66
|
readOnly?: boolean;
|
|
67
|
+
round?: boolean;
|
|
58
68
|
}): ReactNode;
|
|
59
69
|
/**
|
|
60
70
|
* The workspace: profile and tabs on the left, the chosen tab on the right. Only the chosen tab is drawn; a switch
|
|
61
71
|
* keeps the focus on the tabs, or puts it on the chosen tab when the panel that held it went away.
|
|
62
72
|
*/
|
|
63
|
-
export declare function CrudWorkspace<TRow extends FormValues>({ options, form, current, popup, columns, currency, details, load, read, refresh, subject, guard }: CrudWorkspaceProps<TRow>): import("react").JSX.Element;
|
|
73
|
+
export declare function CrudWorkspace<TRow extends FormValues>({ options, form, current, popup, columns, currency, details, load, read, refresh, subject, guard, onTab }: CrudWorkspaceProps<TRow>): import("react").JSX.Element;
|
|
@@ -45,15 +45,16 @@ export async function confirmLeaving(guard) {
|
|
|
45
45
|
/**
|
|
46
46
|
* A record's picture where it is shown large (a workspace's profile, a record page's hero): by its Drive params, a button
|
|
47
47
|
* that opens the pictures window, the record's initials standing in; or what the page draws in its place. `size` is a
|
|
48
|
-
* CSS length. Read-only shows the pictures with no way to change them.
|
|
48
|
+
* CSS length. Read-only shows the pictures with no way to change them. `round` draws it as a circle whatever the
|
|
49
|
+
* picture's own shape (a workspace's profile); the pictures window keeps the picture's shape and aspect.
|
|
49
50
|
*/
|
|
50
|
-
export function ProfilePicture({ picture, title, size, readOnly = false }) {
|
|
51
|
+
export function ProfilePicture({ picture, title, size, readOnly = false, round = false }) {
|
|
51
52
|
if (!isCrudPicture(picture))
|
|
52
53
|
return picture ?? null;
|
|
53
54
|
const named = typeof title === "string" ? title : undefined;
|
|
54
55
|
const { name, module, category, ownerId, aspect, shape, ...gallery } = picture;
|
|
55
56
|
const owner = { module, category, ownerId };
|
|
56
|
-
return (_jsx(OwnerPicture, { owner: owner, size: size, aspect: aspect, shape: shape, name: name ?? named ?? null, label: named ? `${named}: pictures` : "Pictures", onClick: () => openPictureGallery({ ...gallery, ...owner, aspect, shape, title: gallery.title ?? named, readOnly: gallery.readOnly || readOnly }) }));
|
|
57
|
+
return (_jsx(OwnerPicture, { owner: owner, size: size, aspect: round ? 1 : aspect, shape: round ? "circle" : shape, name: name ?? named ?? null, label: named ? `${named}: pictures` : "Pictures", onClick: () => openPictureGallery({ ...gallery, ...owner, aspect, shape, title: gallery.title ?? named, readOnly: gallery.readOnly || readOnly }) }));
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
60
|
* A few of the record's fields under the profile's tags (`profile.fields`): label and value, the value drawn as the
|
|
@@ -68,19 +69,19 @@ function CrudProfileFields({ fields, row, currency, read }) {
|
|
|
68
69
|
return (_jsx("dl", { className: "nx-crud-profile__fields", "aria-busy": (awaiting && read === "loading") || undefined, children: fields.map(column => (_jsxs("div", { className: "nx-crud-profile__field", children: [_jsx("dt", { className: "nx-crud-profile__label", children: column.label }), " ", _jsx("dd", { className: "nx-crud-profile__value", children: !isAwaitedCrudField(column, row, read) ? (_jsx(TableCellValue, { column: column, row: row, options: options })) : read === "failed" ? (_jsx("span", { className: "nx-crud-details__missing", children: "Not loaded" })) : (_jsx("span", { className: "nx-table__bone nx-record__bone nx-crud-profile__bone", "aria-hidden": "true" })) })] }, column.key))) }));
|
|
69
70
|
}
|
|
70
71
|
/**
|
|
71
|
-
* The profile at the top of the column: the picture (a button that opens the pictures), the name, a line,
|
|
72
|
-
* few of the record's fields.
|
|
72
|
+
* The profile at the top of the column: the picture in a circle (a button that opens the pictures), the name, a line,
|
|
73
|
+
* tags, and a few of the record's fields. A profile of a picture and a name alone is those two, in the middle.
|
|
73
74
|
*/
|
|
74
75
|
function CrudProfile({ profile, row, columns, currency, read }) {
|
|
75
76
|
const { picture, title, subtitle, tags } = profile;
|
|
76
77
|
const fields = crudProfileFields(profile.fields, columns, row, read);
|
|
77
|
-
return (_jsxs("div", { className: "nx-crud-profile", children: [picture !== undefined && picture !== null && picture !== false && (_jsx("div", { className: "nx-crud-profile__picture", children: _jsx(ProfilePicture, { picture: picture, title: title, size: "var(--nx-crud-profile-picture)" }) })), _jsxs("div", { className: "nx-crud-profile__text", children: [_jsx("p", { className: "nx-crud-profile__title", children: title }), subtitle !== undefined && subtitle !== null && subtitle !== "" && _jsx("p", { className: "nx-crud-profile__subtitle", children: subtitle }), tags && tags.length > 0 && (_jsx("p", { className: "nx-crud-profile__tags", children: tags.map((tag, index) => (_jsx("span", { className: "nx-table__tag", "data-tone": tag.tone, children: tag.label }, index))) })), fields.length > 0 && _jsx(CrudProfileFields, { fields: fields, row: row, currency: currency, read: read })] })] }));
|
|
78
|
+
return (_jsxs("div", { className: "nx-crud-profile", children: [picture !== undefined && picture !== null && picture !== false && (_jsx("div", { className: "nx-crud-profile__picture", children: _jsx(ProfilePicture, { picture: picture, title: title, size: "var(--nx-crud-profile-picture)", round: true }) })), _jsxs("div", { className: "nx-crud-profile__text", children: [_jsx("p", { className: "nx-crud-profile__title", children: title }), subtitle !== undefined && subtitle !== null && subtitle !== "" && _jsx("p", { className: "nx-crud-profile__subtitle", children: subtitle }), tags && tags.length > 0 && (_jsx("p", { className: "nx-crud-profile__tags", children: tags.map((tag, index) => (_jsx("span", { className: "nx-table__tag", "data-tone": tag.tone, children: tag.label }, index))) })), fields.length > 0 && _jsx(CrudProfileFields, { fields: fields, row: row, currency: currency, read: read })] })] }));
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
81
|
* The workspace: profile and tabs on the left, the chosen tab on the right. Only the chosen tab is drawn; a switch
|
|
81
82
|
* keeps the focus on the tabs, or puts it on the chosen tab when the panel that held it went away.
|
|
82
83
|
*/
|
|
83
|
-
export function CrudWorkspace({ options, form, current, popup, columns, currency, details, load, read, refresh, subject, guard }) {
|
|
84
|
+
export function CrudWorkspace({ options, form, current, popup, columns, currency, details, load, read, refresh, subject, guard, onTab }) {
|
|
84
85
|
const id = useId();
|
|
85
86
|
const recordRead = useSyncExternalStore(read?.subscribe ?? subscribeNothing, read?.get ?? readDone, read?.get ?? readDone);
|
|
86
87
|
// The record as it is now: the profile and the tabs' rules follow it.
|
|
@@ -92,8 +93,9 @@ export function CrudWorkspace({ options, form, current, popup, columns, currency
|
|
|
92
93
|
const storage = useMemo(() => crudTabStorageKey(options.remember), [options.remember?.key, options.remember?.viewer]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
93
94
|
const [asked, setAsked] = useState(() => readCrudTab(browserStorage(), storage));
|
|
94
95
|
const chosen = openingCrudTab(offered, asked) ?? CRUD_DETAILS_TAB;
|
|
95
|
-
// Bumped by each refresh, so the built-in details are drawn again from the record read again
|
|
96
|
-
|
|
96
|
+
// Bumped by each refresh, so the built-in details are drawn again from the record read again (in place: what has the
|
|
97
|
+
// focus keeps it).
|
|
98
|
+
const [, setVersion] = useState(0);
|
|
97
99
|
const rootRef = useRef(null);
|
|
98
100
|
// Set when a switch took the panel that held the focus: the chosen tab takes it once drawn.
|
|
99
101
|
const refocus = useRef(false);
|
|
@@ -110,10 +112,16 @@ export function CrudWorkspace({ options, form, current, popup, columns, currency
|
|
|
110
112
|
}
|
|
111
113
|
const active = document.activeElement;
|
|
112
114
|
const panel = rootRef.current?.querySelector(".nx-crud-workspace__panel");
|
|
113
|
-
|
|
115
|
+
// The focus in the panel, or in the popup's footer, which only the Details tab has: the chosen tab takes it.
|
|
116
|
+
refocus.current = !active || active === document.body || Boolean(panel?.contains(active)) || Boolean(active.closest(".nx-popup__foot"));
|
|
114
117
|
setAsked(tab);
|
|
115
118
|
writeCrudTab(browserStorage(), storage, tab);
|
|
116
119
|
}
|
|
120
|
+
// Before the paint, so the footer of a tab that does not have one is never drawn.
|
|
121
|
+
useLayoutEffect(() => {
|
|
122
|
+
onTab?.(chosen);
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
}, [chosen]);
|
|
117
125
|
useLayoutEffect(() => {
|
|
118
126
|
if (!refocus.current)
|
|
119
127
|
return;
|
|
@@ -160,7 +168,7 @@ export function CrudWorkspace({ options, form, current, popup, columns, currency
|
|
|
160
168
|
const row = current();
|
|
161
169
|
let body;
|
|
162
170
|
if (!tab || tab.builtIn)
|
|
163
|
-
body = _jsx("div", { children: details(profileFields) }
|
|
171
|
+
body = _jsx("div", { children: details(profileFields, refreshed) });
|
|
164
172
|
else
|
|
165
173
|
body = tab.render?.({ row, form, popup, tab: tab.id, select: next => void select(next), refresh: refreshed });
|
|
166
174
|
const shownProfile = profile !== undefined && profile !== null && profile !== false;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FormStore, type AnyFormStore, type FormStatus } from "../../Helpers/FormStore.ts";
|
|
2
2
|
import type { DatePreferences } from "../../Interfaces/DateInterfaces.ts";
|
|
3
|
-
import { type FormProps, type FormValues
|
|
3
|
+
import { type FormProps, type FormValues } from "../../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import { formInputLabel } from "../Inputs/ComputedInput.tsx";
|
|
4
5
|
/**
|
|
5
6
|
* A form store for the page to hold: pass it to a form as `form`, then read, watch, set, and submit through it.
|
|
6
7
|
*
|
|
@@ -19,7 +20,7 @@ export declare function useFormStatus(form: AnyFormStore): FormStatus;
|
|
|
19
20
|
/** The form's own store, or the one given. */
|
|
20
21
|
export declare function useFormStore<TValues extends FormValues>(given: FormStore<TValues> | undefined): FormStore<TValues>;
|
|
21
22
|
/** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
|
|
22
|
-
export
|
|
23
|
+
export { formInputLabel };
|
|
23
24
|
/** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
|
|
24
25
|
export declare function namesToShow<T>(names: readonly T[], most?: number): {
|
|
25
26
|
shown: readonly T[];
|
|
@@ -4,17 +4,17 @@ import { Fragment } from "react";
|
|
|
4
4
|
import { iconAlertCircle, iconHistory, iconX, InlineIcon } from "nexus-icons";
|
|
5
5
|
import { memo, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
|
|
6
6
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
7
|
-
import { useInputDesign
|
|
7
|
+
import { useInputDesign } from "../../Helpers/UserPreferences.js";
|
|
8
8
|
import { formatDate, toIsoDate, usesDevanagari } from "../../Helpers/DateHelpers.js";
|
|
9
9
|
import { useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
10
10
|
import { FormStore } from "../../Helpers/FormStore.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { withInputConditions } from "../../Helpers/InputConditions.js";
|
|
12
|
+
import { formNarrowingNames, narrowInputParamsBy } from "../../Helpers/InputNarrowing.js";
|
|
13
|
+
import { isBooleanInput, isRowsInput } from "../../Helpers/InputParamsHelpers.js";
|
|
14
14
|
import { formatTime } from "../../Helpers/TimeHelpers.js";
|
|
15
15
|
import { EInputTypes } from "../../Interfaces/FormInterfaces.js";
|
|
16
16
|
import { Button } from "../Documents/Button.js";
|
|
17
|
-
import {
|
|
17
|
+
import { ComputedInput, formInputLabel } from "../Inputs/ComputedInput.js";
|
|
18
18
|
import { isFirstInvalid } from "../Inputs/InputField.js";
|
|
19
19
|
import { renderInput } from "../Inputs/InputRenderer.js";
|
|
20
20
|
import { firstFocusable } from "../Inputs/RowsInput.js";
|
|
@@ -27,6 +27,9 @@ import { firstFocusable } from "../Inputs/RowsInput.js";
|
|
|
27
27
|
/** What takes focus in an input's cell: a control to type in or pick, before any button of its toolbar. */
|
|
28
28
|
const TYPING_TARGETS = 'input:not([type="hidden"]):not(.nx-visually-hidden):not(:disabled), textarea:not(.nx-visually-hidden):not(:disabled), select:not(:disabled), [contenteditable="true"]';
|
|
29
29
|
const INVALID_CONTROLS = ":is(input, textarea, select):invalid";
|
|
30
|
+
const NO_INPUTS = [];
|
|
31
|
+
const NO_SUBSCRIBE = () => () => { };
|
|
32
|
+
const NO_VERSION = () => 0;
|
|
30
33
|
/** This browser's local storage, or nothing where it cannot be reached (storage switched off). */
|
|
31
34
|
function browserStorage() {
|
|
32
35
|
try {
|
|
@@ -72,11 +75,7 @@ export function useFormStore(given) {
|
|
|
72
75
|
}
|
|
73
76
|
/* Text */
|
|
74
77
|
/** An input's name in the form's messages: its `fieldName`, its label when it is text, or its name. */
|
|
75
|
-
export
|
|
76
|
-
if (!params)
|
|
77
|
-
return name;
|
|
78
|
-
return inputFieldName(params) ?? (typeof params.label === "string" ? params.label : params.name);
|
|
79
|
-
}
|
|
78
|
+
export { formInputLabel };
|
|
80
79
|
/** The names a list shows: all of them up to one more than `most` ("and 1 more" says less than the name), else the first `most`. */
|
|
81
80
|
export function namesToShow(names, most = 3) {
|
|
82
81
|
return names.length <= most + 1 ? { shown: names, more: 0 } : { shown: names.slice(0, most), more: names.length - most };
|
|
@@ -165,29 +164,37 @@ function FieldInput({ store, params, stamp, error, context, cell, heldFocus }) {
|
|
|
165
164
|
// Options another input narrows (`narrowedBy`): read with its value as it is now; the cell draws again when it changes.
|
|
166
165
|
// Copied afresh on every draw, never kept: the copy reads the params' getters, and one that `dependsOn` follows
|
|
167
166
|
// (readOnly, hint, label, options; span is read by the cell) must answer for the values of this draw. The box keeps its loaded pages by `loadKey`.
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
const shown = params.narrowedBy ? narrowInputParamsBy(params, narrowing => ({ value: store.get(narrowing.name), label: store.inputParams(narrowing.name)?.label })) : params;
|
|
168
|
+
// Rows whose inputs follow this form's values (`narrowedBy` `from: "form"`): handed them as they are now; the rows input
|
|
169
|
+
// draws again when one changes (it follows them as `dependsOn` would).
|
|
170
|
+
const rowInputs = isRowsInput(params) ? (params.inputs ?? []) : NO_INPUTS;
|
|
171
|
+
if (rowInputs.length) {
|
|
172
|
+
const names = formNarrowingNames(rowInputs);
|
|
173
|
+
if (names.length)
|
|
174
|
+
overrides.formValues = Object.fromEntries(names.map(name => [name, { value: store.get(name), label: store.inputParams(name)?.label }]));
|
|
175
|
+
}
|
|
176
|
+
// Rows with inputs worked out from others (`compute`) get the form's `computeWith`, unless their params name their own;
|
|
177
|
+
// the rows input draws again when the page passes a new one.
|
|
178
|
+
const handsCompute = rowInputs.some(input => input.compute) && !("computeWith" in params);
|
|
179
|
+
const source = context.computeWith;
|
|
180
|
+
useSyncExternalStore(handsCompute ? source.subscribe : NO_SUBSCRIBE, handsCompute ? source.getVersion : NO_VERSION, handsCompute ? source.getVersion : NO_VERSION);
|
|
181
|
+
if (handsCompute)
|
|
182
|
+
overrides.computeWith = source.value;
|
|
171
183
|
return renderInput(shown, overrides);
|
|
172
184
|
}
|
|
173
185
|
/**
|
|
174
|
-
* An input worked out from others (`compute`)
|
|
175
|
-
*
|
|
176
|
-
* no lock, no note, no steppers and no Tab stop. Blank while the values load, and for a value that is no figure.
|
|
186
|
+
* An input worked out from others (`compute`) in a form: `ComputedInput` reading the form's values and its `computeWith`
|
|
187
|
+
* (it alone draws again when the page passes a new one), and writing a typed-in figure's answer as the person's change.
|
|
177
188
|
*/
|
|
178
189
|
function ComputedField({ store, params, context }) {
|
|
179
190
|
const source = context.computeWith;
|
|
180
191
|
useSyncExternalStore(source.subscribe, source.getVersion, source.getVersion);
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
const disabled = context.disabled || Boolean(own.disabled);
|
|
188
|
-
// The inputs it is worked out from, as an output names them.
|
|
189
|
-
const from = params.compute?.from.map(name => `${context.idPrefix}-${name}`).join(" ");
|
|
190
|
-
return (_jsx(Field, { controlId: controlId, messageId: messageId, label: params.label, hint: own.hint, design: design, size: own.size ?? context.size, icon: own.icon, iconEnd: own.iconEnd, prefix: own.prefix, suffix: own.suffix, readOnly: true, disabled: disabled, raiseLabel: true, className: own.className, children: _jsx("output", { id: controlId, htmlFor: from || undefined, className: cx("nx-field__control", "nx-field__control--output", params.type === EInputTypes.Number && "nx-field__control--number"), "aria-live": "polite", "aria-describedby": own.hint ? messageId : undefined, "aria-disabled": disabled || undefined, children: _jsx("span", { className: "nx-field__output-text", children: text }) }) }));
|
|
192
|
+
const { idPrefix } = context;
|
|
193
|
+
const read = useCallback(() => store.getValue(), [store]);
|
|
194
|
+
const write = useCallback((values) => void store.setFromPerson(values), [store]);
|
|
195
|
+
const inputParams = useCallback((name) => store.inputParams(name), [store]);
|
|
196
|
+
const sourceId = useCallback((name) => `${idPrefix}-${name}`, [idPrefix]);
|
|
197
|
+
return (_jsx(ComputedInput, { params: params, id: `${idPrefix}-${params.name}`, read: read, withValue: source.value, write: write, inputParams: inputParams, sourceId: sourceId, size: context.size, design: context.design, loading: context.loading, readOnly: context.readOnly, disabled: context.disabled }));
|
|
191
198
|
}
|
|
192
199
|
/** The control an input mounted again takes the focus back in: its box, a list's combobox, a choice, an editor. */
|
|
193
200
|
const REFOCUSABLE = '[role="combobox"]:not([disabled]), input:not([type="hidden"]):not([disabled]):not([tabindex="-1"]), textarea:not([disabled]), select:not([disabled]), [contenteditable="true"], button:not([disabled]):not([tabindex="-1"])';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FormConfirmQuestion } from "../../Helpers/FormConfirm.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Asks a settled question (`formConfirmQuestion`): a question box with what will be saved drawn as a record's details,
|
|
4
|
+
* Confirm holding the focus, a press outside it changing nothing. Resolves whether to go on; an abort answers no.
|
|
5
|
+
*/
|
|
6
|
+
export declare function askFormQuestion(question: FormConfirmQuestion, signal?: AbortSignal): Promise<boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { messageBox } from "../Layouts/MessageBox.js";
|
|
4
|
+
import { CrudDetails } from "./CrudParts.js";
|
|
5
|
+
/*
|
|
6
|
+
* The question asked before a save, drawn once for every place that asks it: a form's `confirm` (SubmitForm) and a row
|
|
7
|
+
* form's, including a row action that is its question alone (CrudRowForms.tsx). Not exported from the package: a page
|
|
8
|
+
* asks through those params.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Asks a settled question (`formConfirmQuestion`): a question box with what will be saved drawn as a record's details,
|
|
12
|
+
* Confirm holding the focus, a press outside it changing nothing. Resolves whether to go on; an abort answers no.
|
|
13
|
+
*/
|
|
14
|
+
export function askFormQuestion(question, signal) {
|
|
15
|
+
const details = question.details;
|
|
16
|
+
return messageBox.confirm({
|
|
17
|
+
tone: "question",
|
|
18
|
+
closable: true,
|
|
19
|
+
closeOnBackdrop: false,
|
|
20
|
+
signal,
|
|
21
|
+
size: question.size,
|
|
22
|
+
title: question.title,
|
|
23
|
+
message: question.message,
|
|
24
|
+
details: details ? _jsx(CrudDetails, { row: details.row, columns: details.columns, currency: details.currency }) : null,
|
|
25
|
+
confirmLabel: question.confirmLabel,
|
|
26
|
+
cancelLabel: question.cancelLabel,
|
|
27
|
+
});
|
|
28
|
+
}
|