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.
Files changed (163) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +220 -14
  3. package/dist/Client.Index.d.ts +13 -0
  4. package/dist/Client.Index.js +13 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  22. package/dist/Components/Documents/TabButtons.js +5 -4
  23. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  24. package/dist/Components/Domains/DomainCard.js +182 -0
  25. package/dist/Components/Forms/ApiForm.js +5 -1
  26. package/dist/Components/Forms/Crud.js +72 -24
  27. package/dist/Components/Forms/CrudActions.d.ts +5 -4
  28. package/dist/Components/Forms/CrudActions.js +42 -12
  29. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  30. package/dist/Components/Forms/CrudHistory.js +14 -6
  31. package/dist/Components/Forms/CrudParts.d.ts +11 -4
  32. package/dist/Components/Forms/CrudParts.js +76 -10
  33. package/dist/Components/Forms/CrudRowForms.js +65 -5
  34. package/dist/Components/Forms/CrudTree.js +7 -7
  35. package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
  36. package/dist/Components/Forms/CrudWorkspace.js +19 -11
  37. package/dist/Components/Forms/Form.d.ts +3 -2
  38. package/dist/Components/Forms/Form.js +33 -26
  39. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  40. package/dist/Components/Forms/FormQuestion.js +28 -0
  41. package/dist/Components/Forms/SubmitForm.js +2 -15
  42. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  43. package/dist/Components/Inputs/ComputedInput.js +179 -0
  44. package/dist/Components/Inputs/Dropdown.js +4 -2
  45. package/dist/Components/Inputs/GroupForm.js +3 -2
  46. package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
  47. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  48. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  49. package/dist/Components/Inputs/PictureInput.js +8 -3
  50. package/dist/Components/Inputs/RowsInput.d.ts +14 -1
  51. package/dist/Components/Inputs/RowsInput.js +78 -5
  52. package/dist/Components/Inputs/TabularForm.js +16 -3
  53. package/dist/Components/Inputs/TreeView.d.ts +24 -1
  54. package/dist/Components/Inputs/TreeView.js +84 -7
  55. package/dist/Components/Layouts/Popup.d.ts +22 -2
  56. package/dist/Components/Layouts/Popup.js +108 -6
  57. package/dist/Components/Messages/MailClient.d.ts +50 -0
  58. package/dist/Components/Messages/MailClient.js +410 -0
  59. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  60. package/dist/Components/Messages/MailColumns.js +92 -0
  61. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  62. package/dist/Components/Messages/MailCompose.js +154 -0
  63. package/dist/Components/Messages/MailReader.d.ts +43 -0
  64. package/dist/Components/Messages/MailReader.js +207 -0
  65. package/dist/Components/Messages/MailThread.d.ts +71 -0
  66. package/dist/Components/Messages/MailThread.js +185 -0
  67. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  68. package/dist/Components/Messages/MessageBell.js +76 -0
  69. package/dist/Components/Viewers/DataTable.js +7 -5
  70. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  71. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  72. package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
  73. package/dist/Components/Viewers/DataTableParts.js +2 -2
  74. package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
  75. package/dist/Components/Viewers/OwnerFiles.js +142 -31
  76. package/dist/Helpers/ApiModules.js +4 -0
  77. package/dist/Helpers/AuditEntries.d.ts +50 -0
  78. package/dist/Helpers/AuditEntries.js +96 -0
  79. package/dist/Helpers/ChatBackend.d.ts +8 -1
  80. package/dist/Helpers/ChatBackend.js +18 -0
  81. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  82. package/dist/Helpers/ChatHelpers.js +4 -1
  83. package/dist/Helpers/CrudColumns.d.ts +44 -6
  84. package/dist/Helpers/CrudColumns.js +63 -13
  85. package/dist/Helpers/CrudHelpers.d.ts +36 -1
  86. package/dist/Helpers/CrudHelpers.js +61 -0
  87. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  88. package/dist/Helpers/CrudPickerPlan.js +46 -0
  89. package/dist/Helpers/CrudRowForms.d.ts +18 -1
  90. package/dist/Helpers/CrudRowForms.js +35 -2
  91. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  92. package/dist/Helpers/DashboardHelpers.js +305 -0
  93. package/dist/Helpers/DomainCards.d.ts +140 -0
  94. package/dist/Helpers/DomainCards.js +370 -0
  95. package/dist/Helpers/FileAdapter.d.ts +82 -2
  96. package/dist/Helpers/FileAdapter.js +72 -1
  97. package/dist/Helpers/FormStore.d.ts +11 -1
  98. package/dist/Helpers/FormStore.js +66 -19
  99. package/dist/Helpers/InputConditions.d.ts +6 -0
  100. package/dist/Helpers/InputConditions.js +24 -0
  101. package/dist/Helpers/InputMemory.js +40 -30
  102. package/dist/Helpers/InputNarrowing.d.ts +38 -6
  103. package/dist/Helpers/InputNarrowing.js +102 -21
  104. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  105. package/dist/Helpers/MailClientHelpers.js +211 -0
  106. package/dist/Helpers/MessageBackend.d.ts +152 -0
  107. package/dist/Helpers/MessageBackend.js +329 -0
  108. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  109. package/dist/Helpers/MessageHelpers.js +573 -0
  110. package/dist/Helpers/MessageUnread.d.ts +95 -0
  111. package/dist/Helpers/MessageUnread.js +245 -0
  112. package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
  113. package/dist/Helpers/NexusDriveAdapter.js +32 -3
  114. package/dist/Helpers/NumberHelpers.d.ts +61 -0
  115. package/dist/Helpers/NumberHelpers.js +124 -8
  116. package/dist/Helpers/PagedPicker.d.ts +42 -0
  117. package/dist/Helpers/PagedPicker.js +85 -0
  118. package/dist/Helpers/RowsStore.d.ts +24 -0
  119. package/dist/Helpers/RowsStore.js +79 -10
  120. package/dist/Helpers/TableHelpers.d.ts +15 -2
  121. package/dist/Helpers/TableHelpers.js +28 -1
  122. package/dist/Helpers/TableStore.d.ts +7 -3
  123. package/dist/Helpers/TableStore.js +9 -4
  124. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  125. package/dist/Helpers/ToolbarFit.js +30 -0
  126. package/dist/Helpers/TreeStore.d.ts +75 -6
  127. package/dist/Helpers/TreeStore.js +182 -18
  128. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  129. package/dist/Interfaces/ApiInterfaces.js +10 -1
  130. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  131. package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
  132. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  133. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  134. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  135. package/dist/Interfaces/DomainInterfaces.js +1 -0
  136. package/dist/Interfaces/FileInterfaces.d.ts +21 -0
  137. package/dist/Interfaces/FileInterfaces.js +2 -0
  138. package/dist/Interfaces/FormInterfaces.d.ts +78 -11
  139. package/dist/Interfaces/InputInterfaces.d.ts +12 -5
  140. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  141. package/dist/Interfaces/MailInterfaces.js +1 -0
  142. package/dist/Interfaces/TableInterfaces.d.ts +28 -2
  143. package/dist/Services/JoinSources.d.ts +47 -6
  144. package/dist/Services/JoinSources.js +68 -33
  145. package/dist/Services/MessageApi.d.ts +18 -0
  146. package/dist/Services/MessageApi.js +64 -0
  147. package/dist/Shared.Index.d.ts +12 -0
  148. package/dist/Shared.Index.js +12 -0
  149. package/package.json +7 -1
  150. package/src/Styles/Nexus.Button.css +29 -0
  151. package/src/Styles/Nexus.Chat.css +114 -22
  152. package/src/Styles/Nexus.Crud.css +119 -28
  153. package/src/Styles/Nexus.Dashboard.css +763 -0
  154. package/src/Styles/Nexus.Domain.css +384 -0
  155. package/src/Styles/Nexus.Editor.css +20 -0
  156. package/src/Styles/Nexus.Files.css +27 -0
  157. package/src/Styles/Nexus.Index.css +3 -0
  158. package/src/Styles/Nexus.Input.css +31 -0
  159. package/src/Styles/Nexus.Mail.css +1062 -0
  160. package/src/Styles/Nexus.Popup.css +58 -0
  161. package/src/Styles/Nexus.Table.css +1458 -1449
  162. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  163. 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 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.
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 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.
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
- export function CrudDetails({ row, columns, load, render, currency }) {
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 => (_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))) }));
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("h3", { className: "nx-crud-details__heading", children: run.group }), list(run.columns)] }, `${index}:${run.group ?? ""}`)))
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 () => panel.style.removeProperty("--nx-crud-tab-footer-size");
176
+ return stop;
111
177
  const observer = new ResizeObserver(write);
112
178
  observer.observe(bar);
113
179
  return () => {
114
180
  observer.disconnect();
115
- panel.style.removeProperty("--nx-crud-tab-footer-size");
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 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.
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[0];
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, saved, joined) => void afterSave(null, result, saved, joined),
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, saved, joined) => void afterSave(row, result, saved, joined),
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, saved, joined) {
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 = { ...row, ...saved, ...answered };
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
- /** The built-in details: the fields, with the `aside` beside them, less the fields the profile shows (`fields`). */
22
- details: (without: readonly (TableColumn<TRow> | string)[] | undefined) => ReactNode;
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, tags, and a
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
- const [version, setVersion] = useState(0);
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
- refocus.current = !active || active === document.body || Boolean(panel?.contains(active));
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) }, `details:${version}`);
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, type InputParams } from "../../Interfaces/FormInterfaces.ts";
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 declare function formInputLabel(params: InputParams | undefined, name?: string): string;
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, useNumberLocale } from "../../Helpers/UserPreferences.js";
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 { computedText, computedValue, withInputConditions } from "../../Helpers/InputConditions.js";
12
- import { narrowInputParams } from "../../Helpers/InputNarrowing.js";
13
- import { inputFieldName, isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
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 { Field } from "../Inputs/Field.js";
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 function formInputLabel(params, name = "") {
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 narrowing = params.narrowedBy;
169
- const other = narrowing?.name;
170
- const shown = narrowing ? narrowInputParams(params, other === undefined ? undefined : store.get(other), other === undefined ? undefined : store.inputParams(other)?.label) : params;
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`): the field's frame (label, hint, prefix, suffix, size, design) around an
175
- * `<output>` that says the value as it is now, in the person's number format, on the read-only tint - never typed in, so
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 locale = useNumberLocale();
182
- const own = params;
183
- const design = useInputDesign(own.design ?? context.design);
184
- const controlId = `${context.idPrefix}-${params.name}`;
185
- const messageId = `${controlId}-message`;
186
- const text = context.loading ? "" : computedText(params, computedValue(params, store.getValue(), source.value), locale);
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
+ }