nexus-shared 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -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/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. package/src/Styles/Nexus.Tree.css +41 -1
@@ -0,0 +1,378 @@
1
+ import { EInputTypes } from "../Interfaces/FormInterfaces.js";
2
+ import { isApiEndpoint } from "./ApiRoutes.js";
3
+ import { formatInputValue, holdsValue, isBooleanInput, isEmptyInputValue, isRowsInput } from "./InputParamsHelpers.js";
4
+ /*
5
+ * The pure half of a record's page of cards (`RecordCards`, Components/Viewers/RecordCards.tsx): how a value reads in a
6
+ * card, what an answer writes into the data, where a card's items are, its limit, an action's state, a popup's steps,
7
+ * and the sources the page reads. Pure, so Node tests run it.
8
+ */
9
+ /* eslint-disable @typescript-eslint/no-explicit-any */
10
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11
+ /** A fields card with its data's type: `fieldsCard<Profile>({ ... })`. */
12
+ export const fieldsCard = (card) => card;
13
+ /** An items card with its data's and its items' types: `itemsCard<ContactList, Contact>({ ... })`. */
14
+ export const itemsCard = (card) => card;
15
+ /** A card of the page's own, with its data's type. */
16
+ export const contentCard = (card) => card;
17
+ /** Which of the three a card is. */
18
+ export function recordCardKind(card) {
19
+ if ("fields" in card && Array.isArray(card.fields))
20
+ return "fields";
21
+ if ("render" in card && typeof card.render === "function")
22
+ return "content";
23
+ return "items";
24
+ }
25
+ /* Values */
26
+ /** A field given as its input alone, or with its own label and look: always the second. */
27
+ export function recordField(entry) {
28
+ return "input" in entry && isRecord(entry.input) && "type" in entry.input ? entry : { input: entry };
29
+ }
30
+ const isPlain = (value) => value === null || ["string", "number", "boolean"].includes(typeof value);
31
+ /**
32
+ * What a popup's form opens on, of the record or the item it works on: the values its inputs name that an input can hold
33
+ * (text, a number, a yes or no, a list of those; rows for a rows input - Tabular, Group). A field of another shape - the
34
+ * code record of a contact, when the step's box is also called `code` - is left out, so the box opens empty rather than
35
+ * on "[object Object]". Rows are kept whole: an Edit that opened with no rows would send none and wipe them.
36
+ */
37
+ export function recordOpenedValues(given, inputs) {
38
+ const opened = {};
39
+ if (!isRecord(given))
40
+ return opened;
41
+ for (const input of inputs) {
42
+ const value = given[input.name];
43
+ if (value === undefined)
44
+ continue;
45
+ if (isPlain(value) || (Array.isArray(value) && value.every(isPlain)))
46
+ opened[input.name] = value;
47
+ else if (isRowsInput(input) && Array.isArray(value) && value.every(row => isRecord(row)))
48
+ opened[input.name] = value;
49
+ }
50
+ return opened;
51
+ }
52
+ /** A fields card's Edit inputs: the fields' own, without those only shown (`edit: false`) or that hold no value. */
53
+ export function recordFieldInputs(card) {
54
+ return card.fields
55
+ .map(recordField)
56
+ .filter(field => field.edit !== false && holdsValue(field.input))
57
+ .map(field => field.input);
58
+ }
59
+ const LONG_TEXT = new Set([EInputTypes.TextArea]);
60
+ const TEXT_TYPES = new Set([EInputTypes.Text, EInputTypes.Email, EInputTypes.Search, EInputTypes.Tel, EInputTypes.Url, EInputTypes.TextArea]);
61
+ /** Whether a field takes the card's whole width when it does not say: long text, chips and editors do. */
62
+ export function recordFieldWide(field) {
63
+ if (field.wide !== undefined)
64
+ return field.wide;
65
+ const type = field.input.type;
66
+ return field.look === "lines" || field.look === "chips" || LONG_TEXT.has(type) || type === EInputTypes.Markdown || type === EInputTypes.RichText || type === EInputTypes.CheckBoxGroup;
67
+ }
68
+ /** What a field holds while its source loads, so the card is as tall as it will be: a line, two lines, or a chip's row. */
69
+ export function recordFieldHeld(field) {
70
+ const type = field.input.type;
71
+ const multiple = field.input.multiple === true;
72
+ if (field.look === "chips" || type === EInputTypes.CheckBoxGroup || (type === EInputTypes.Dropdown && multiple))
73
+ return "chips";
74
+ if (field.look === "lines" || (field.look !== "text" && (LONG_TEXT.has(type) || type === EInputTypes.Markdown || type === EInputTypes.RichText)))
75
+ return "lines";
76
+ return "line";
77
+ }
78
+ /** How many lines a value held as lines holds while its source loads: the field's `heldLines`, else two. */
79
+ export function recordHeldLines(field) {
80
+ const lines = field.heldLines;
81
+ return typeof lines === "number" && Number.isFinite(lines) && lines >= 1 ? Math.min(Math.floor(lines), 12) : 2;
82
+ }
83
+ /**
84
+ * A value as a card draws it, from the input that edits it: a choice by its label, a date in the person's format, a
85
+ * number with its format, a yes or a no, a list as chips (`look: "chips"`, a multiple choice, a group of checkboxes), long
86
+ * text on its own lines, Markdown and rich text as such, a password never. Empty when there is nothing to say.
87
+ */
88
+ export function recordValueView(params, value, options = {}) {
89
+ const { look, preferences } = options;
90
+ if (!holdsValue(params))
91
+ return { kind: "empty" };
92
+ if (isBooleanInput(params))
93
+ return value === true ? { kind: "text", text: "Yes" } : value === false ? { kind: "text", text: "No" } : { kind: "empty" };
94
+ if (isEmptyInputValue(value))
95
+ return { kind: "empty" };
96
+ if (params.type === EInputTypes.Password)
97
+ return { kind: "text", text: "••••••••" };
98
+ const multiple = params.type === EInputTypes.CheckBoxGroup || (params.type === EInputTypes.Dropdown && Array.isArray(value));
99
+ if (multiple) {
100
+ const items = (Array.isArray(value) ? value : [value]).map(item => formatInputValue(params, [item], { preferences })).filter(Boolean);
101
+ return items.length ? { kind: "chips", items } : { kind: "empty" };
102
+ }
103
+ if (look === "chips") {
104
+ const items = (Array.isArray(value) ? value.map(String) : String(value).split(","))
105
+ .map(item => item.trim())
106
+ .filter(Boolean);
107
+ return items.length ? { kind: "chips", items } : { kind: "empty" };
108
+ }
109
+ if (params.type === EInputTypes.Markdown)
110
+ return { kind: "markdown", text: String(value) };
111
+ if (params.type === EInputTypes.RichText)
112
+ return { kind: "rich", html: String(value) };
113
+ if (TEXT_TYPES.has(params.type)) {
114
+ const text = String(value).trim();
115
+ return look === "lines" || (look !== "text" && LONG_TEXT.has(params.type)) ? { kind: "lines", text } : { kind: "text", text };
116
+ }
117
+ const text = formatInputValue(params, value, { preferences });
118
+ return text ? { kind: look === "lines" ? "lines" : "text", text } : { kind: "empty" };
119
+ }
120
+ /* Items */
121
+ /** A card's items: the field it names, what its function answers, or the data itself when that is the list. */
122
+ export function recordItems(data, items) {
123
+ if (typeof items === "function")
124
+ return items(data) ?? [];
125
+ const list = typeof items === "string" ? (isRecord(data) ? data[items] : undefined) : data;
126
+ return Array.isArray(list) ? list : [];
127
+ }
128
+ /** An item's key, as text. */
129
+ export function recordItemKey(item, itemKey) {
130
+ const key = typeof itemKey === "function" ? itemKey(item) : isRecord(item) ? item[itemKey] : undefined;
131
+ return key === undefined || key === null ? "" : String(key);
132
+ }
133
+ /** The data with these items in place of its own, or `null` when the items come from a function (which cannot be set). */
134
+ export function recordWithItems(data, items, next) {
135
+ if (typeof items === "function")
136
+ return null;
137
+ if (typeof items === "string")
138
+ return { ...(isRecord(data) ? data : {}), [items]: next };
139
+ return [...next];
140
+ }
141
+ /** The most a card's list may hold, by its `limit`: a number, a field of the data, or a function. `null` for no limit. */
142
+ export function recordLimit(data, limit) {
143
+ const value = typeof limit === "function" ? limit(data) : typeof limit === "string" ? (isRecord(data) ? data[limit] : undefined) : limit;
144
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
145
+ }
146
+ /** "3 of 5", or "3" with no limit. */
147
+ export const recordCount = (count, limit) => (limit === null ? String(count) : `${count} of ${limit}`);
148
+ /** Why Add is held at the limit. */
149
+ export function recordLimitReason(limit, reason) {
150
+ if (typeof reason === "function")
151
+ return reason(limit);
152
+ return reason ?? `The most there may be is ${limit}. Remove one first.`;
153
+ }
154
+ const merged = (data, patch) => (isRecord(data) && isRecord(patch) ? { ...data, ...patch } : patch);
155
+ /**
156
+ * What an answer writes into the card's data, as `writes` says (`RecordWrites`). Everything merges over the data as held
157
+ * now; a list (`"item"`, `"remove"`) is written back only where the card names its field or the data is the list, else
158
+ * the source is read again.
159
+ */
160
+ export function recordWrite(request) {
161
+ const { writes = "answer", result, data, values, fallback, items, itemKey, item } = request;
162
+ if (typeof writes === "function") {
163
+ const patch = writes(result, data, values);
164
+ return patch === null || patch === undefined ? { kind: "none" } : { kind: "write", data: merged(data, patch) };
165
+ }
166
+ if (Array.isArray(writes)) {
167
+ if (!isRecord(result))
168
+ return { kind: "none" };
169
+ const patch = {};
170
+ for (const field of writes)
171
+ if (result[field] !== undefined)
172
+ patch[field] = result[field];
173
+ return Object.keys(patch).length ? { kind: "write", data: merged(data, patch) } : { kind: "none" };
174
+ }
175
+ switch (writes) {
176
+ case "none":
177
+ return { kind: "none" };
178
+ case "reload":
179
+ return { kind: "reload" };
180
+ case "values":
181
+ return isRecord(values) ? { kind: "write", data: merged(data, values) } : { kind: "none" };
182
+ case "item":
183
+ case "remove": {
184
+ if (!itemKey)
185
+ return { kind: "reload" };
186
+ const list = recordItems(data, items);
187
+ const key = recordItemKey(writes === "item" ? result : item, itemKey);
188
+ if (!key)
189
+ return { kind: "reload" };
190
+ const next = writes === "remove"
191
+ ? list.filter(one => recordItemKey(one, itemKey) !== key)
192
+ : list.some(one => recordItemKey(one, itemKey) === key)
193
+ ? list.map(one => (recordItemKey(one, itemKey) === key ? result : one))
194
+ : [...list, result];
195
+ const written = recordWithItems(data, items, next);
196
+ return written === null ? { kind: "reload" } : { kind: "write", data: written };
197
+ }
198
+ default: {
199
+ if (isRecord(result))
200
+ return { kind: "write", data: merged(data, result) };
201
+ if (Array.isArray(result) && Array.isArray(data))
202
+ return { kind: "write", data: result };
203
+ if (fallback === "values" && isRecord(values))
204
+ return { kind: "write", data: merged(data, values) };
205
+ return { kind: "reload" };
206
+ }
207
+ }
208
+ }
209
+ /** The endpoint an action calls first, when it is one: the action shows only to a person who may call it. */
210
+ export function recordActionEndpoint(action) {
211
+ const submit = action.flow ? (recordSteps(action.flow)[0]?.submit ?? action.flow.submit) : action.confirm?.submit;
212
+ return isApiEndpoint(submit) ? submit : undefined;
213
+ }
214
+ /** What a heading's button says while its card's data is read. */
215
+ export const RECORD_STILL_READ = "Still being read.";
216
+ /**
217
+ * An action's state: hidden on a read-only card, by its own rule, or to a person who may not call its endpoint; held with
218
+ * its reason; else ready. `args` is `null` while the data is still read: the rules typed on the data are not asked then,
219
+ * and the action is held ("Still being read.") or, with `loading: "hidden"`, not drawn yet.
220
+ */
221
+ export function recordActionState(action, args, { readOnly, can }) {
222
+ if (readOnly)
223
+ return { kind: "hidden" };
224
+ const endpoint = recordActionEndpoint(action);
225
+ if (args === null) {
226
+ if (endpoint && !can(endpoint))
227
+ return { kind: "hidden" };
228
+ return action.loading === "hidden" ? { kind: "hidden" } : { kind: "held", reason: RECORD_STILL_READ };
229
+ }
230
+ if (action.hidden?.(...args))
231
+ return { kind: "hidden" };
232
+ if (endpoint && !can(endpoint))
233
+ return { kind: "hidden" };
234
+ const reason = action.held?.(...args);
235
+ return reason ? { kind: "held", reason } : { kind: "ready" };
236
+ }
237
+ /**
238
+ * A card's calls on their way, by what each works on (an item's key, or "card"): one added as it starts, that one alone
239
+ * taken away as it ends, so a call that ends first never frees a row whose own call still runs.
240
+ */
241
+ export function recordBusyKeys(keys, key, on) {
242
+ const now = keys ?? [];
243
+ if (on)
244
+ return [...now, key];
245
+ const at = now.indexOf(key);
246
+ return at < 0 ? now : [...now.slice(0, at), ...now.slice(at + 1)];
247
+ }
248
+ /** An action's accessible name with what it acts on: "Edit: Personal details", "Verify: ram@example.com". */
249
+ export const recordActionName = (label, of) => (of ? `${label}: ${of}` : label);
250
+ /** A popup's steps, each with its id (its place, "1", when it names none): the flow itself when it has no `steps`. */
251
+ export function recordSteps(flow) {
252
+ const steps = flow.steps?.length ? flow.steps : [flow];
253
+ return steps.map((step, index) => ({ ...step, id: step.id ?? String(index + 1) }));
254
+ }
255
+ /** The step a popup opens on: `start` when it names a step there is, else the first. */
256
+ export function recordStartStep(flow, context) {
257
+ const steps = recordSteps(flow);
258
+ const asked = typeof flow.start === "function" ? flow.start(context) : flow.start;
259
+ return steps.find(step => step.id === asked)?.id ?? steps[0]?.id ?? "1";
260
+ }
261
+ /** The status line a step's bar keeps: the full one for a form of several inputs, the quiet one for a step of one or two. */
262
+ export const recordStatusLine = (inputs) => (inputs > 2 ? "full" : "errors");
263
+ /** The default rule for a session that ended: a 401 that names no input (a wrong password names its box). */
264
+ export const recordSessionEnded = (answer) => answer.status === 401 && Object.keys(answer.errors ?? {}).length === 0;
265
+ /** Whole seconds until a moment (an ISO text or ms), never below zero. */
266
+ export function secondsUntil(moment, now = Date.now()) {
267
+ if (moment === null || moment === undefined || moment === "")
268
+ return 0;
269
+ const at = typeof moment === "number" ? moment : Date.parse(moment);
270
+ return Number.isFinite(at) ? Math.max(0, Math.ceil((at - now) / 1000)) : 0;
271
+ }
272
+ const isSourceParams = (source) => isRecord(source) && "read" in source;
273
+ /**
274
+ * The page's sources: each read once (and again on Try again or a step's `reload`), written by the answers. A read that
275
+ * fails before anything was read leaves the source failed; one that fails while its data shows keeps the data and only
276
+ * answers the failure. A newer read wins over an older one still on its way. `end` marks the session ended: the page
277
+ * shows its words in place of the cards and nothing more is read.
278
+ */
279
+ export class RecordSources {
280
+ states;
281
+ controllers = new Map();
282
+ listeners = new Set();
283
+ endedAnswer = null;
284
+ sources;
285
+ reader;
286
+ constructor(sources, reader) {
287
+ this.sources = sources;
288
+ this.reader = reader;
289
+ const states = {};
290
+ for (const [name, source] of Object.entries(sources))
291
+ states[name] = { status: "loading", data: isSourceParams(source) ? (source.held ?? null) : null };
292
+ this.states = states;
293
+ }
294
+ /** The names, in the page's order: the first is every card's default. */
295
+ get names() {
296
+ return Object.keys(this.sources);
297
+ }
298
+ subscribe = (listener) => {
299
+ this.listeners.add(listener);
300
+ return () => {
301
+ this.listeners.delete(listener);
302
+ };
303
+ };
304
+ /** Every source's state; a new object after each change, for `useSyncExternalStore`. */
305
+ snapshot = () => this.states;
306
+ get(name) {
307
+ return this.states[name] ?? { status: "ready", data: null };
308
+ }
309
+ /** The answer that found the session ended, once one has. */
310
+ get ended() {
311
+ return this.endedAnswer;
312
+ }
313
+ set(name, state) {
314
+ this.states = { ...this.states, [name]: state };
315
+ for (const listener of this.listeners)
316
+ listener();
317
+ }
318
+ /** Reads every source not read yet (none read, or stopped on its way). */
319
+ start() {
320
+ for (const name of this.names) {
321
+ const state = this.get(name);
322
+ if (state.status === "loading" && !this.controllers.has(name))
323
+ void this.read(name);
324
+ }
325
+ }
326
+ /** Stops every read on its way; `start` reads them again. */
327
+ stop() {
328
+ for (const controller of this.controllers.values())
329
+ controller.abort();
330
+ this.controllers.clear();
331
+ }
332
+ /**
333
+ * Reads a source (again): resolves with the answer. A failed source goes back to loading while it runs; one that shows
334
+ * its data keeps it. An answer that a newer read or `stop` overtook changes nothing and resolves aborted.
335
+ */
336
+ async read(name) {
337
+ const source = this.sources[name];
338
+ if (!source || this.endedAnswer)
339
+ return { ok: false, aborted: true };
340
+ this.controllers.get(name)?.abort();
341
+ const controller = new AbortController();
342
+ this.controllers.set(name, controller);
343
+ const before = this.get(name);
344
+ if (before.status === "failed")
345
+ this.set(name, { status: "loading", data: before.data });
346
+ // A read overtaken or stopped settles at once, even when the page's own function never looks at its signal.
347
+ const stopped = new Promise(resolve => controller.signal.addEventListener("abort", () => resolve({ ok: false, aborted: true }), { once: true }));
348
+ const reading = this.reader(isSourceParams(source) ? source.read : source, controller.signal).catch((error) => (controller.signal.aborted ? { ok: false, aborted: true } : { ok: false, message: error instanceof Error ? error.message : "The server did not answer." }));
349
+ const answer = await Promise.race([reading, stopped]);
350
+ if (controller.signal.aborted || this.controllers.get(name) !== controller)
351
+ return { ...answer, ok: false, aborted: true };
352
+ this.controllers.delete(name);
353
+ if (this.endedAnswer)
354
+ return answer;
355
+ const now = this.get(name);
356
+ if (answer.ok)
357
+ this.set(name, { status: "ready", data: answer.result ?? null });
358
+ else if (now.status !== "ready")
359
+ this.set(name, { status: "failed", data: now.data, answer });
360
+ return answer;
361
+ }
362
+ /** Writes a source's data: every card on it follows. */
363
+ write(name, data) {
364
+ if (!(name in this.sources) || this.endedAnswer)
365
+ return;
366
+ this.set(name, { status: "ready", data });
367
+ }
368
+ /** The session ended: nothing more is read, and the page shows its words. */
369
+ end(answer) {
370
+ if (this.endedAnswer)
371
+ return;
372
+ this.endedAnswer = answer;
373
+ this.stop();
374
+ this.states = { ...this.states };
375
+ for (const listener of this.listeners)
376
+ listener();
377
+ }
378
+ }
@@ -20,8 +20,20 @@ export declare class RequestCache<T> {
20
20
  * const page = cache.get(`regions|${parentId}|0`, signal => api.post(children, { parentId, skipCount: 0, maxCount: 50 }, { signal }));
21
21
  */
22
22
  get(key: string, load: (signal: AbortSignal) => T | PromiseLike<T>, lifetime?: number): T | Promise<T>;
23
- /** Keeps an answer under `key`, as if it had just been loaded. */
24
- set(key: string, value: T): void;
23
+ /**
24
+ * Keeps an answer under `key`, as if it had just been loaded - or as if loaded at `at`, for an answer written over
25
+ * one that was kept: a change written in does not make what was kept any newer.
26
+ */
27
+ set(key: string, value: T, at?: number): void;
28
+ /**
29
+ * The fresh answers kept under keys that start with `prefix`, with their keys and when they were loaded. A request
30
+ * on its way is not an answer yet, and an aged answer is not handed out.
31
+ */
32
+ entries(prefix?: string, lifetime?: number): {
33
+ key: string;
34
+ value: T;
35
+ at: number;
36
+ }[];
25
37
  private keep;
26
38
  /**
27
39
  * Drops the answers - kept or on their way - under every key that starts with `prefix`, or that `test` passes, or
@@ -77,9 +77,28 @@ export class RequestCache {
77
77
  this.kept.set(key, loading);
78
78
  return loading.promise;
79
79
  }
80
- /** Keeps an answer under `key`, as if it had just been loaded. */
81
- set(key, value) {
82
- this.keep(key, value, this.lifetime);
80
+ /**
81
+ * Keeps an answer under `key`, as if it had just been loaded - or as if loaded at `at`, for an answer written over
82
+ * one that was kept: a change written in does not make what was kept any newer.
83
+ */
84
+ set(key, value, at) {
85
+ if (at === undefined)
86
+ this.keep(key, value, this.lifetime);
87
+ else if (this.lifetime > 0)
88
+ this.kept.set(key, { state: "ready", value, at });
89
+ }
90
+ /**
91
+ * The fresh answers kept under keys that start with `prefix`, with their keys and when they were loaded. A request
92
+ * on its way is not an answer yet, and an aged answer is not handed out.
93
+ */
94
+ entries(prefix = "", lifetime = this.lifetime) {
95
+ const list = [];
96
+ const now = this.now();
97
+ for (const [key, entry] of this.kept) {
98
+ if (entry.state === "ready" && key.startsWith(prefix) && now - entry.at < lifetime)
99
+ list.push({ key, value: entry.value, at: entry.at });
100
+ }
101
+ return list;
83
102
  }
84
103
  keep(key, value, lifetime) {
85
104
  if (lifetime > 0)
@@ -1,4 +1,5 @@
1
- import type { InputParams, InputValidationContext, RowsInputProps, RowValue } from "../Interfaces/FormInterfaces.ts";
1
+ import { type InputParams, type InputValidationContext, type RowsInputProps, type RowValue } from "../Interfaces/FormInterfaces.ts";
2
+ import { type InputMemoryStorage } from "./InputMemory.ts";
2
3
  export type RowsStoreOptions<TRow extends RowValue = RowValue> = Pick<RowsInputProps<TRow>, "inputs" | "required" | "min" | "max" | "unique" | "validate" | "itemLabel" | "itemLabelPlural" | "rowKey" | "newRow" | "onRowChange" | "deriveRows"> & {
3
4
  /** The row's name when there is no `itemLabel`: "Row" in a table, "Item" in sections. */
4
5
  fallbackItem?: string;
@@ -47,6 +48,10 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
47
48
  private showAll;
48
49
  private touched;
49
50
  private removed;
51
+ private memory;
52
+ private added;
53
+ private asking;
54
+ private memoryRound;
50
55
  private revisions;
51
56
  private versions;
52
57
  private rowListeners;
@@ -103,10 +108,38 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
103
108
  configure(options: RowsStoreOptions<TRow>, recheck?: boolean): void;
104
109
  /** Sets a value in a row. `input` is a change the user made in the row's input: it runs `onRowChange`. */
105
110
  set(key: string, name: string, value: unknown, source?: "input" | "code"): void;
111
+ /**
112
+ * Values a person set in a row through another of its inputs - one worked out from them and typed in (`compute.solve`)
113
+ * - as one change. Their cells show them at once, as values from code do, and they are taken as the person's own:
114
+ * `onRowChange` runs for each, the row's inputs narrowed by them lose what they picked, and the rows' value listeners
115
+ * hear of it (a parent form's unsaved-changes state, its Save and its leave guard). A name worked out itself is not
116
+ * taken. Answers whether anything changed.
117
+ */
118
+ setFromPerson(key: string, values: Partial<TRow>): boolean;
119
+ /**
120
+ * Draws every row again, values untouched: the form around the rows changed a value their cells follow
121
+ * (`narrowedBy` `from: "form"`), so each cell is narrowed afresh as it draws.
122
+ */
123
+ redrawRows(): void;
106
124
  /** Marks a value as left by the user: a repeated value shows its message from now on. */
107
125
  touch(key: string, name: string): void;
108
- /** Adds a row with the inputs' defaults, `newRow`, and `values`, at `index` or at the end. Returns its key. */
126
+ /**
127
+ * Adds a row with the inputs' defaults, `newRow`, and `values`, at `index` or at the end. Returns its key. A remembered
128
+ * input (`remember`) that neither `newRow` nor `values` names with a value opens on the value kept for it, while its
129
+ * input offers it; a picker's opens once the picker answers, unless the person picked one first.
130
+ */
109
131
  add(values?: Partial<TRow>, index?: number): string;
132
+ /**
133
+ * The storage remembered row inputs read and keep (`InputLayoutParams.remember`), given once the rows are drawn in the
134
+ * browser: rows added from then on open on the kept values. The rows already there keep theirs.
135
+ */
136
+ attachMemory(storage: InputMemoryStorage | null): void;
137
+ /**
138
+ * A save of the form holding the rows: each remembered input keeps its value in the row added last that is still there
139
+ * (an empty value forgets), under that row's `per` value and where its `when` holds on that row. No row added since the
140
+ * rows were loaded: nothing is kept, since no new choice was made.
141
+ */
142
+ rememberAdded(): void;
110
143
  /** Adds a copy of a row after it, without its id and with its radios off. Returns the copy's key. */
111
144
  duplicate(key: string): string | null;
112
145
  /**
@@ -138,9 +171,35 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
138
171
  private normalizeRow;
139
172
  private load;
140
173
  private afterStructure;
174
+ /**
175
+ * Opens a new row's remembered inputs on their kept values, where the row is not added with one: the company's
176
+ * `preferred`, else the person's last choice, while the input offers it (`rememberedOpeningNow`, `when` and `per` read
177
+ * in the row). A kept choice no longer offered is forgotten. Answers the pickers that must be asked first.
178
+ */
179
+ private openRemembered;
180
+ /** Stops asking pickers about remembered values: their answers no longer open anything. */
181
+ private stopAskingPickers;
182
+ /**
183
+ * Asks each picker whether it still offers the remembered value of a new row's input (`findRememberedOpening`), judged
184
+ * on the row as it was added: an answer forgets a choice no longer offered (under that row's `per` value), and opens an
185
+ * offered one while the row is there, the person has not picked in that cell since, and the row's values it was judged
186
+ * on (`rememberDependencies`) are unchanged.
187
+ */
188
+ private askPickers;
141
189
  /** Drops what the store knew about a removed row. */
142
190
  private forget;
191
+ /**
192
+ * Writes one value. `input` was typed in its own cell; `code` comes from code (its cell mounts again with it); `person`
193
+ * is a person's change made through another cell (`setFromPerson`): written as code writes, followed as typing is.
194
+ */
143
195
  private write;
196
+ /**
197
+ * After a person changed a row's value: the row's inputs narrowed by it (`narrowedBy`, a line's Tags by its Item) lose
198
+ * what they picked among its old options, down the row's chain, and the row draws again so each is narrowed afresh;
199
+ * a row input that `dependsOn` it (a Tags box whose most picks are the row's quantity, through `cellParams`) draws
200
+ * again too, its rule asked again.
201
+ */
202
+ private follow;
144
203
  /** Turns a radio off in every row but this one. */
145
204
  private radioOn;
146
205
  /** Checks one value against its input's rules. */