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
@@ -1,3 +1,6 @@
1
+ import { inputShown, isComputedInput } from "./InputConditions.js";
2
+ import { holdsValue } from "./InputParamsHelpers.js";
3
+ import { readPath } from "./TableHelpers.js";
1
4
  /*
2
5
  * The rules of the CRUD component, with no React: which lists and actions a controller offers, which actions each list
3
6
  * shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned
@@ -129,6 +132,259 @@ export function leavesList(action, mode, markOn) {
129
132
  return false;
130
133
  }
131
134
  }
135
+ /**
136
+ * A CRUD change as changes to a tree that shows the same records (`CrudTreePlacement`): a record in the active list is
137
+ * put under its parent - added, changed or moved - and one out of it (in the trash, deleted) leaves the tree. Where it
138
+ * was comes from `before`: under its parent while it was active, out of the tree when it was not or is new, and not
139
+ * known otherwise (the tree then looks for it). A record without a status counts as active. Pins and favourites are a
140
+ * user's own and change no tree.
141
+ */
142
+ export function crudTreeChanges(change, place, backend) {
143
+ if (change.action === "pin" || change.action === "favorite")
144
+ return [];
145
+ const valueKey = place.rules.valueKey ?? "id";
146
+ const inTree = (row) => backend.listMembership(row, "normal") !== false;
147
+ const list = [];
148
+ change.rows.forEach((row, index) => {
149
+ const before = change.before?.[index];
150
+ const was = change.action === "create" ? false : before === undefined ? undefined : inTree(before) ? place.parentOf(before) : false;
151
+ if (inTree(row))
152
+ list.push({ type: "put", item: place.itemOf(row), parent: place.parentOf(row), before: was });
153
+ else if (was !== false)
154
+ list.push({ type: "remove", value: place.itemOf(row)[valueKey], parent: was });
155
+ });
156
+ return list;
157
+ }
158
+ /**
159
+ * The records whose child count a change moved, among `rows` (a list's rows), through a tree placement: what the tree's
160
+ * parents follow (`crudTreeChanges`) written into the parents' own rows. A record added or back under a parent counts one
161
+ * more there; one moved counts one fewer where it was and one more where it went; one trashed or deleted one fewer. Each
162
+ * parent row comes back with what `recordOf` reads from its item with the count moved written over it (so `recordOf`
163
+ * must carry the item's count back into the record); a row whose count did not move, or a count that is not a number, is
164
+ * left out.
165
+ */
166
+ export function crudParentCounts(treeChanges, rows, place) {
167
+ const countKey = place.rules.hasChildrenKey;
168
+ if (!countKey || treeChanges.length === 0)
169
+ return [];
170
+ const valueKey = place.rules.valueKey ?? "id";
171
+ const deltas = new Map();
172
+ const move = (parent, delta) => {
173
+ if (parent === null || parent === undefined)
174
+ return;
175
+ deltas.set(String(parent), (deltas.get(String(parent)) ?? 0) + delta);
176
+ };
177
+ for (const change of treeChanges) {
178
+ if (change.type === "remove")
179
+ move(change.parent, -1);
180
+ else if (change.before === false)
181
+ move(change.parent, 1);
182
+ else if (change.before !== undefined && String(change.before) !== String(change.parent)) {
183
+ move(change.before, -1);
184
+ move(change.parent, 1);
185
+ }
186
+ }
187
+ if (![...deltas.values()].some(Boolean))
188
+ return [];
189
+ const counted = [];
190
+ for (const row of rows) {
191
+ const item = place.itemOf(row);
192
+ const delta = deltas.get(String(item[valueKey]));
193
+ const count = item[countKey];
194
+ if (!delta || typeof count !== "number")
195
+ continue;
196
+ const record = place.recordOf({ ...item, [countKey]: Math.max(0, count + delta) });
197
+ // Over the row the list holds, so a placement whose item carries only some of its fields loses none of them.
198
+ if (record)
199
+ counted.push({ ...row, ...record });
200
+ }
201
+ return counted;
202
+ }
203
+ /**
204
+ * A saved record with the child count a tree placement keeps for it (`rules.hasChildrenKey`): a new record's own, as
205
+ * `itemOf` gives it (none yet), and an edited one's from the row the page holds (`held`) - its form's copy is as old as
206
+ * the form, and a child added since counts. Written over the record through `recordOf`, which carries an item's count
207
+ * back; a placement without a count leaves the record as it is.
208
+ */
209
+ export function crudSavedCount(record, held, place) {
210
+ const countKey = place.rules.hasChildrenKey;
211
+ if (!countKey)
212
+ return record;
213
+ let item = place.itemOf(record);
214
+ if (held) {
215
+ const count = place.itemOf(held)[countKey];
216
+ if (count !== undefined)
217
+ item = { ...item, [countKey]: count };
218
+ }
219
+ const counted = place.recordOf(item);
220
+ return counted ? { ...record, ...counted } : record;
221
+ }
222
+ /**
223
+ * The names the database joins into a record (`crudColumns.dbJoin` columns with `from`), written again after a save that
224
+ * answered only what was sent - with no call. For each such column: a key the save kept as `before` had it keeps
225
+ * `before`'s name and status; an emptied key empties both; a new or changed key takes the name and the status of the
226
+ * record `find(from, key)` gives - the one picked in the form, or one the page holds. Where it gives none, a new record
227
+ * keeps the name its save answered, if any, and an edited one drops the old record's name and status rather than show
228
+ * them for the new key: either way the record is then read back once (`crudJoinsMissing`). Names and statuses are written
229
+ * as top-level fields of the record.
230
+ */
231
+ export function crudJoinedNames(record, before, columns, find) {
232
+ const row = { ...record };
233
+ for (const column of columns) {
234
+ const join = column.dbJoin;
235
+ if (!join?.from)
236
+ continue;
237
+ const write = (name, status) => {
238
+ row[column.key] = name ?? null;
239
+ if (join.status)
240
+ row[join.status] = status ?? null;
241
+ };
242
+ const key = readPath(record, join.from);
243
+ if (key === undefined || key === null || key === "") {
244
+ write(null, null);
245
+ continue;
246
+ }
247
+ const held = before ? readPath(before, join.from) : undefined;
248
+ if (held !== undefined && held !== null && String(held) === String(key)) {
249
+ write(readPath(before, column.key), join.status ? readPath(before, join.status) : null);
250
+ continue;
251
+ }
252
+ const found = find(join.from, String(key));
253
+ if (found) {
254
+ write(found.name, found.status);
255
+ }
256
+ else if (before && readPath(record, column.key) === readPath(before, column.key)) {
257
+ // The name the record had is the old key's.
258
+ delete row[column.key];
259
+ if (join.status)
260
+ delete row[join.status];
261
+ }
262
+ }
263
+ return row;
264
+ }
265
+ /**
266
+ * Whether a saved record still lacks a name the database joins into it, which is the one case a CRUD reads it back
267
+ * (`details`, once): a `crudColumns.dbJoin` column whose name the record does not hold while its key is set - no record
268
+ * was known for a new key (`crudJoinedNames`) - or one that names no key field (`from`) on a new record, whose save
269
+ * answered no name. An edited record keeps the name it had on a column without `from`: whether its key changed cannot
270
+ * be told.
271
+ */
272
+ export function crudJoinsMissing(record, before, columns) {
273
+ for (const column of columns) {
274
+ const join = column.dbJoin;
275
+ if (!join || readPath(record, column.key) !== undefined)
276
+ continue;
277
+ if (!join.from) {
278
+ if (!before)
279
+ return true;
280
+ continue;
281
+ }
282
+ const key = readPath(record, join.from);
283
+ if (key !== undefined && key !== null && key !== "")
284
+ return true;
285
+ }
286
+ return false;
287
+ }
288
+ /** Two answers with the same fields and the same values in each (`Object.is`), or the same value; lists compared item by item. */
289
+ export function sameShallow(a, b) {
290
+ if (Object.is(a, b))
291
+ return true;
292
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
293
+ return false;
294
+ if (Array.isArray(a) !== Array.isArray(b))
295
+ return false;
296
+ const aKeys = Object.keys(a);
297
+ const bKeys = Object.keys(b);
298
+ if (aKeys.length !== bKeys.length)
299
+ return false;
300
+ return aKeys.every(key => Object.prototype.hasOwnProperty.call(b, key) && Object.is(a[key], b[key]));
301
+ }
302
+ /**
303
+ * A Crud form's `computeWith` (`CrudFormOptions.computeWith`) for the values as they are now: `previous` itself when the
304
+ * rule answers the same fields and values (so the form redraws nothing), the new answer otherwise. A rule that throws
305
+ * keeps `previous` and says why in the console. No rule: `undefined`.
306
+ */
307
+ export function crudComputeWith(rule, values, context, previous) {
308
+ if (!rule)
309
+ return undefined;
310
+ try {
311
+ const next = rule(values, context);
312
+ return sameShallow(previous, next) ? previous : next;
313
+ }
314
+ catch (error) {
315
+ console.error("A Crud form's computeWith failed; its last answer is kept.", error);
316
+ return previous;
317
+ }
318
+ }
319
+ /** A record of fields, as a JSON body is: not a list, a `FormData` or anything else with a prototype of its own. */
320
+ const isPlainRecord = (value) => {
321
+ if (typeof value !== "object" || value === null || Array.isArray(value))
322
+ return false;
323
+ const prototype = Object.getPrototypeOf(value);
324
+ return prototype === Object.prototype || prototype === null;
325
+ };
326
+ /** The inputs `showWhen` hides for these values: each one that holds a value of the form's (not a picture, nor one worked out from others). */
327
+ export function crudHiddenOnSave(inputs, values) {
328
+ return inputs.filter(params => params.showWhen && holdsValue(params) && !isComputedInput(params) && !inputShown(params.showWhen, name => values[name])).map(params => params.name);
329
+ }
330
+ /**
331
+ * The record a save leaves, as the server now holds it, with no call: the row the form was opened on (none for a new
332
+ * record), then what was **sent** - the body `transform` made, when it is a record, over the form's values, so a field it
333
+ * sends as null is empty on the row and one it reshapes shows as sent - then the answer. An input `showWhen` hid when the
334
+ * form was saved is one the form says does not apply, so the row holds it only as the server does: as the answer gives
335
+ * it, when the answer is a record (a field it leaves out has none, as a Nexus answer leaves out its nulls), else as the
336
+ * body gives it (a field the body leaves out has none). Never the form's own copy: that copy kept a hidden value on the
337
+ * row after a save, though the server held none, until the list was read again.
338
+ */
339
+ export function crudSavedRecord(row, sent, answered) {
340
+ const body = isPlainRecord(sent.body) ? sent.body : null;
341
+ const record = { ...row, ...sent.values, ...body, ...answered };
342
+ const holder = answered ?? body ?? sent.values;
343
+ for (const name of sent.hidden)
344
+ record[name] = holder[name] ?? null;
345
+ return record;
346
+ }
347
+ /**
348
+ * Where an Add or Edit form with fixed fields stands, as text for its popup's key and its draft's: the fixed fields'
349
+ * names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
350
+ * parent never comes back inside another, a fixed form never stands in for a free one, and a draft written before the
351
+ * record moved is not brought back over its new place.
352
+ */
353
+ export function crudFormPlace(fixed) {
354
+ if (!fixed)
355
+ return "";
356
+ const names = Object.keys(fixed).sort();
357
+ if (names.length === 0)
358
+ return "";
359
+ return `@${names.map(name => `${name}=${String(fixed[name] ?? "")}`).join("&")}`;
360
+ }
361
+ /**
362
+ * A `Crud` `narrow` read once: the field, the picked record's value (its `valueKey` field, else its `key` field) and its
363
+ * name (its `labelKey` field, else the value). `null` without a narrowing; a value of `null` while nothing is picked, or
364
+ * the picked record holds no value.
365
+ */
366
+ export function crudNarrowing(narrow) {
367
+ if (!narrow)
368
+ return null;
369
+ const picked = narrow.picked;
370
+ const raw = picked ? readPath(picked, narrow.valueKey ?? narrow.key) : undefined;
371
+ if (raw === undefined || raw === null || raw === "")
372
+ return { key: narrow.key, value: null, raw: null, label: "" };
373
+ const name = narrow.labelKey && picked ? readPath(picked, narrow.labelKey) : undefined;
374
+ return { key: narrow.key, value: String(raw), raw, label: name === undefined || name === null || name === "" ? String(raw) : String(name) };
375
+ }
376
+ /**
377
+ * Whether a record belongs in a narrowed list: its field holds the picked record's value (compared as text, so 7 and "7"
378
+ * are one). Every record does without a narrowing, and none while nothing is picked.
379
+ */
380
+ export function crudInNarrowing(row, narrowing) {
381
+ if (!narrowing)
382
+ return true;
383
+ if (narrowing.value === null)
384
+ return false;
385
+ const held = readPath(row, narrowing.key);
386
+ return held !== undefined && held !== null && String(held) === narrowing.value;
387
+ }
132
388
  /** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
133
389
  export function crudMessageAction(action, markOn) {
134
390
  switch (action) {
@@ -0,0 +1,83 @@
1
+ import type { CrudNarrowPickerSource } from "../Interfaces/CrudInterfaces.ts";
2
+ type Picked = Readonly<Record<string, unknown>>;
3
+ /** "Pick a group", "Pick an area": the box's text while nothing is picked. */
4
+ export declare function narrowPickerPlaceholder(label: string): string;
5
+ /** What a narrowed list says while no record is picked, on its Add, and on its Add form's fixed field. */
6
+ export interface NarrowPickWords {
7
+ /** The empty list: "Pick a group in the Group box to see its amenities." */
8
+ empty: string;
9
+ /** Add, switched off: "Pick a group in the Group box to add a new amenity for it." */
10
+ add: string;
11
+ /** The fixed field of the Add form, given the picked record's name. */
12
+ fixed: (pickedName: string) => string;
13
+ }
14
+ /**
15
+ * The words of a narrowed list, by where its record is picked: in the CRUD's own box (`narrowPicker`, its label given),
16
+ * or in another list on the page (no label). A page's `emptyMessage`, `addMessage` and `fixedMessage` replace them.
17
+ *
18
+ * @example narrowPickWords("Group", "amenity", "amenities").add // "Pick a group in the Group box to add a new amenity for it."
19
+ */
20
+ export declare function narrowPickWords(pickerLabel: string | undefined, subject: string, plural: string): NarrowPickWords;
21
+ /**
22
+ * Where a `narrowPicker`'s pick is kept: one place per page key and viewer. No place without a viewer: a pick is never
23
+ * shared by everyone on a browser, so a page that cannot say who is looking (not signed in yet) remembers nothing.
24
+ */
25
+ export declare function narrowPickerStorageKey(remember: {
26
+ key: string;
27
+ viewer?: string | null;
28
+ }): string | null;
29
+ /** The kept pick's key, or null. Only the record's key is kept: never its name or anything else of it. */
30
+ export declare function readNarrowPick(storage: Pick<Storage, "getItem">, key: string): string | null;
31
+ /** Keeps the pick's key, or forgets it (null). A browser that keeps nothing asks for the pick again next time. */
32
+ export declare function writeNarrowPick(storage: Pick<Storage, "setItem" | "removeItem">, key: string, value: string | null): void;
33
+ /**
34
+ * The record a kept pick names, as the box offers it now, or null when it no longer does (gone, inactive). Asked by its
35
+ * key (`resolveOptions`, one record) when the picker can; else the box's own pages are read, at a normal page size,
36
+ * until it shows up or they end.
37
+ */
38
+ export declare function findNarrowPick(picker: CrudNarrowPickerSource, value: string, valueKey: string, signal: AbortSignal): Promise<Picked | null>;
39
+ /**
40
+ * The one record the box offers, or null when it offers none or more than one: a box with a single record to pick picks
41
+ * it by itself. Offered as the box offers it: an inactive record (a person still invited, say) is in the list's answer
42
+ * but never offered, and a key is counted once. Read at the box's own page size, the pages the box would ask for, and
43
+ * only until a second record shows.
44
+ */
45
+ export declare function findLoneNarrowPick(picker: CrudNarrowPickerSource, valueKey: string, signal: AbortSignal): Promise<Picked | null>;
46
+ /** How a narrowing box opens: on the pick this browser kept, on the one record it offers, or asking for a pick. */
47
+ export interface NarrowPickStart {
48
+ /** The record it opens on, or null: the box asks for a pick. */
49
+ picked: Picked | null;
50
+ /** `kept`: the pick this browser kept for the viewer. `lone`: the only record the box offers, picked by itself. */
51
+ from: "kept" | "lone" | null;
52
+ /**
53
+ * What to keep in this browser now: the record's key (a lone record is kept as a person's pick is), or null to forget
54
+ * a kept pick the box no longer offers. Undefined: leave what is kept as it is (nothing could be read just now).
55
+ */
56
+ keep?: string | null;
57
+ }
58
+ /**
59
+ * Settles a narrowing box as its page opens: the kept pick while the box still offers it (`findNarrowPick`), else the
60
+ * one record the box offers (`findLoneNarrowPick`), else none. Never rejects: a read that fails counts as nothing found,
61
+ * and a kept pick that could not be checked is left kept for next time.
62
+ */
63
+ export declare function startNarrowPick(picker: CrudNarrowPickerSource, kept: string | null, valueKey: string, signal: AbortSignal): Promise<NarrowPickStart>;
64
+ /** The page around a narrowing box at the moment it could take the cursor, as its page opens. */
65
+ export interface NarrowCursorMoment {
66
+ /** The person has pressed a key or a pointer on the page since it opened: they are already at work. */
67
+ used: boolean;
68
+ /** Focus is on something other than the page itself. */
69
+ focusElsewhere: boolean;
70
+ /** A popup (a dialog) or a panel (a menu, a list) is open. */
71
+ popupOpen: boolean;
72
+ /** A touch screen with no mouse: a cursor in the box would raise the on-screen keyboard over the list. */
73
+ touchOnly: boolean;
74
+ /** The whole box is on screen: taking the cursor never scrolls the page. */
75
+ inView: boolean;
76
+ }
77
+ /**
78
+ * Whether a narrowing box that asks for a pick takes the cursor as its page opens: only on a page nobody has started
79
+ * using, with nothing open over it, where the box is on screen and a cursor brings no on-screen keyboard. Otherwise it is
80
+ * only drawn as wanted, and the list's button takes the person to it.
81
+ */
82
+ export declare function narrowBoxTakesCursor(moment: NarrowCursorMoment): boolean;
83
+ export {};
@@ -0,0 +1,157 @@
1
+ /** "a group", "an area". */
2
+ function withArticle(noun) {
3
+ return `${/^[aeiou]/i.test(noun) ? "an" : "a"} ${noun}`;
4
+ }
5
+ /** "Pick a group", "Pick an area": the box's text while nothing is picked. */
6
+ export function narrowPickerPlaceholder(label) {
7
+ return `Pick ${withArticle(label.trim().toLocaleLowerCase())}`;
8
+ }
9
+ /**
10
+ * The words of a narrowed list, by where its record is picked: in the CRUD's own box (`narrowPicker`, its label given),
11
+ * or in another list on the page (no label). A page's `emptyMessage`, `addMessage` and `fixedMessage` replace them.
12
+ *
13
+ * @example narrowPickWords("Group", "amenity", "amenities").add // "Pick a group in the Group box to add a new amenity for it."
14
+ */
15
+ export function narrowPickWords(pickerLabel, subject, plural) {
16
+ const label = pickerLabel?.trim();
17
+ if (!label) {
18
+ return {
19
+ empty: `Select a row in the other list to see its ${plural}.`,
20
+ add: `Select a row in the other list to add a new ${subject} for it.`,
21
+ fixed: name => `Added for ${name}: pick another row in the other list to add elsewhere.`,
22
+ };
23
+ }
24
+ const noun = label.toLocaleLowerCase();
25
+ return {
26
+ empty: `Pick ${withArticle(noun)} in the ${label} box to see its ${plural}.`,
27
+ add: `Pick ${withArticle(noun)} in the ${label} box to add a new ${subject} for it.`,
28
+ fixed: name => `Added for ${name}: pick another ${noun} in the ${label} box to add elsewhere.`,
29
+ };
30
+ }
31
+ /**
32
+ * Where a `narrowPicker`'s pick is kept: one place per page key and viewer. No place without a viewer: a pick is never
33
+ * shared by everyone on a browser, so a page that cannot say who is looking (not signed in yet) remembers nothing.
34
+ */
35
+ export function narrowPickerStorageKey(remember) {
36
+ const viewer = remember.viewer?.trim();
37
+ return remember.key && viewer ? `nx-crud-narrow:${remember.key}:${viewer}` : null;
38
+ }
39
+ /** The kept pick's key, or null. Only the record's key is kept: never its name or anything else of it. */
40
+ export function readNarrowPick(storage, key) {
41
+ try {
42
+ const text = storage.getItem(key);
43
+ if (!text)
44
+ return null;
45
+ const saved = JSON.parse(text);
46
+ return typeof saved.value === "string" && saved.value ? saved.value : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /** Keeps the pick's key, or forgets it (null). A browser that keeps nothing asks for the pick again next time. */
53
+ export function writeNarrowPick(storage, key, value) {
54
+ try {
55
+ if (value)
56
+ storage.setItem(key, JSON.stringify({ value }));
57
+ else
58
+ storage.removeItem(key);
59
+ }
60
+ catch {
61
+ // Kept nowhere: the pick is asked for again.
62
+ }
63
+ }
64
+ /** The page size a kept pick is looked for in, when the picker cannot name a record by its key. */
65
+ const FIND_PAGE_SIZE = 50;
66
+ /** Pages looked through before the kept pick counts as no longer offered. */
67
+ const FIND_PAGES = 20;
68
+ /**
69
+ * The record a kept pick names, as the box offers it now, or null when it no longer does (gone, inactive). Asked by its
70
+ * key (`resolveOptions`, one record) when the picker can; else the box's own pages are read, at a normal page size,
71
+ * until it shows up or they end.
72
+ */
73
+ export async function findNarrowPick(picker, value, valueKey, signal) {
74
+ const offered = (row) => String(row[valueKey]) === value && !picker.isOptionInactive?.(row);
75
+ if (picker.resolveOptions) {
76
+ const rows = await picker.resolveOptions([value]);
77
+ return rows.find(offered) ?? null;
78
+ }
79
+ for (let page = 1; page <= FIND_PAGES; page++) {
80
+ if (signal.aborted)
81
+ return null;
82
+ const skip = (page - 1) * FIND_PAGE_SIZE;
83
+ const answer = await picker.loadOptions({ search: "", page, skip, pageSize: FIND_PAGE_SIZE, signal });
84
+ const row = answer.items.find(offered);
85
+ if (row)
86
+ return row;
87
+ if (!answer.hasMore || answer.items.length === 0)
88
+ return null;
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * The one record the box offers, or null when it offers none or more than one: a box with a single record to pick picks
94
+ * it by itself. Offered as the box offers it: an inactive record (a person still invited, say) is in the list's answer
95
+ * but never offered, and a key is counted once. Read at the box's own page size, the pages the box would ask for, and
96
+ * only until a second record shows.
97
+ */
98
+ export async function findLoneNarrowPick(picker, valueKey, signal) {
99
+ let lone = null;
100
+ for (let page = 1; page <= FIND_PAGES; page++) {
101
+ if (signal.aborted)
102
+ return null;
103
+ const skip = (page - 1) * FIND_PAGE_SIZE;
104
+ const answer = await picker.loadOptions({ search: "", page, skip, pageSize: FIND_PAGE_SIZE, signal });
105
+ for (const row of answer.items) {
106
+ if (picker.isOptionInactive?.(row))
107
+ continue;
108
+ if (!lone)
109
+ lone = row;
110
+ else if (String(row[valueKey]) !== String(lone[valueKey]))
111
+ return null;
112
+ }
113
+ const more = answer.hasMore ?? answer.items.length >= FIND_PAGE_SIZE;
114
+ if (!more || answer.items.length === 0)
115
+ return lone;
116
+ }
117
+ // Pages that never end with one record offered in them: not a box worth deciding for the person.
118
+ return null;
119
+ }
120
+ /**
121
+ * Settles a narrowing box as its page opens: the kept pick while the box still offers it (`findNarrowPick`), else the
122
+ * one record the box offers (`findLoneNarrowPick`), else none. Never rejects: a read that fails counts as nothing found,
123
+ * and a kept pick that could not be checked is left kept for next time.
124
+ */
125
+ export async function startNarrowPick(picker, kept, valueKey, signal) {
126
+ let keep;
127
+ if (kept) {
128
+ try {
129
+ const row = await findNarrowPick(picker, kept, valueKey, signal);
130
+ if (row)
131
+ return { picked: row, from: "kept", keep: kept };
132
+ keep = null;
133
+ }
134
+ catch {
135
+ // Not read just now: kept for the next visit, and the box is asked about as on a first one.
136
+ }
137
+ }
138
+ if (signal.aborted)
139
+ return { picked: null, from: null };
140
+ try {
141
+ const lone = await findLoneNarrowPick(picker, valueKey, signal);
142
+ if (lone)
143
+ return { picked: lone, from: "lone", keep: String(lone[valueKey]) };
144
+ }
145
+ catch {
146
+ // The box's records could not be read: it asks for a pick, and its own list says what failed when it opens.
147
+ }
148
+ return keep === undefined ? { picked: null, from: null } : { picked: null, from: null, keep };
149
+ }
150
+ /**
151
+ * Whether a narrowing box that asks for a pick takes the cursor as its page opens: only on a page nobody has started
152
+ * using, with nothing open over it, where the box is on screen and a cursor brings no on-screen keyboard. Otherwise it is
153
+ * only drawn as wanted, and the list's button takes the person to it.
154
+ */
155
+ export function narrowBoxTakesCursor(moment) {
156
+ return !moment.used && !moment.focusElsewhere && !moment.popupOpen && !moment.touchOnly && moment.inView;
157
+ }
@@ -0,0 +1,58 @@
1
+ import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
2
+ import type { DropdownNarrow } from "../Interfaces/InputInterfaces.ts";
3
+ import type { TableJoinSource, TableJoinedRecord } from "../Interfaces/TableInterfaces.ts";
4
+ import type { CrudBackend } from "./CrudBackend.ts";
5
+ /** The controller's doors a picker may use: its picker's list and its lookup by keys, either of which it may lack. */
6
+ export interface CrudPickerDoors {
7
+ activeListing?: ApiEndpoint;
8
+ findListing?: ApiEndpoint;
9
+ }
10
+ /** The options that decide the plan (`CrudPickerOptions`). */
11
+ export interface CrudPickerPlanOptions {
12
+ list?: ApiEndpoint;
13
+ valueKey?: string;
14
+ find?: ApiEndpoint | TableJoinSource;
15
+ usable?: (record: TableJoinedRecord) => boolean;
16
+ }
17
+ /** Where a picker's options come from and which of them it offers. */
18
+ export interface CrudPickerPlan {
19
+ /** The listing read once and kept for the page: `list`, else the controller's `activeListing`. */
20
+ list: ApiEndpoint;
21
+ /**
22
+ * Where a value the list does not offer is looked up, and the field its records are filed by: the picker's `find`, else
23
+ * the controller's `findListing` while the values are the records' keys, else nowhere (`null`).
24
+ */
25
+ lookup: {
26
+ source: ApiEndpoint | TableJoinSource;
27
+ key: string;
28
+ } | null;
29
+ /** Whether a record is offered, and shown untagged when a form holds it: `usable`, else the backend's `isPickable`. */
30
+ usable: (record: TableJoinedRecord) => boolean;
31
+ }
32
+ /** The names a listing's path takes (`StockBalance/Units/{storeId}/{itemId}`: storeId and itemId). */
33
+ export declare function pathValueNames(list: Pick<ApiEndpoint, "path">): string[];
34
+ /** How a picker asks for its options narrowed by several values at once (`crudPickerNarrowAsk`). */
35
+ export interface CrudPickerNarrowAsk {
36
+ /** The values that go into the listing's path. */
37
+ params: Record<string, string>;
38
+ /** The values that go as query parameters. */
39
+ query: Record<string, string>;
40
+ /** The narrowings the browser applies to what the server answers, by the records' own fields. */
41
+ filters: DropdownNarrow[];
42
+ /** Where the answer is kept: the listing's name with the values the server was asked for. */
43
+ name: string;
44
+ }
45
+ /**
46
+ * How a picker asks for its options narrowed by these values: each in the listing's path when the path names it
47
+ * (`{storeId}`), as a query parameter when `narrowQuery` names it, else filtered in the browser by the records' own field
48
+ * (`orUnset` there too). `null` when a value the path needs is empty: there is nothing to ask for.
49
+ */
50
+ export declare function crudPickerNarrowAsk(list: Pick<ApiEndpoint, "path">, listName: string, narrows: readonly DropdownNarrow[], narrowQuery?: string | readonly string[]): CrudPickerNarrowAsk | null;
51
+ /** Whether a record fits a narrowing the browser applies: its field holds the value, or is empty with `orUnset`. */
52
+ export declare function fitsNarrow(record: Readonly<Record<string, unknown>>, { key, value, orUnset }: DropdownNarrow): boolean;
53
+ /**
54
+ * The plan for a picker over this controller. A controller with no `activeListing` needs a `list`; one with no
55
+ * `findListing` looks nothing up (a value its list does not offer is named from what the form holds, or not found),
56
+ * unless the picker has its own `find`.
57
+ */
58
+ export declare function crudPickerPlan(controller: CrudPickerDoors, { list, valueKey, find, usable }: CrudPickerPlanOptions, backend: CrudBackend): CrudPickerPlan;
@@ -0,0 +1,46 @@
1
+ /** The names a listing's path takes (`StockBalance/Units/{storeId}/{itemId}`: storeId and itemId). */
2
+ export function pathValueNames(list) {
3
+ return [...list.path.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g)].map(match => match[1]);
4
+ }
5
+ /**
6
+ * How a picker asks for its options narrowed by these values: each in the listing's path when the path names it
7
+ * (`{storeId}`), as a query parameter when `narrowQuery` names it, else filtered in the browser by the records' own field
8
+ * (`orUnset` there too). `null` when a value the path needs is empty: there is nothing to ask for.
9
+ */
10
+ export function crudPickerNarrowAsk(list, listName, narrows, narrowQuery) {
11
+ const inPath = new Set(pathValueNames(list));
12
+ const asQuery = new Set(typeof narrowQuery === "string" ? [narrowQuery] : (narrowQuery ?? []));
13
+ const params = {};
14
+ const query = {};
15
+ const filters = [];
16
+ for (const narrow of narrows) {
17
+ if (inPath.has(narrow.key)) {
18
+ if (narrow.value === null)
19
+ return null;
20
+ params[narrow.key] = String(narrow.value);
21
+ }
22
+ else if (asQuery.has(narrow.key) && narrow.value !== null && !narrow.orUnset)
23
+ query[narrow.key] = String(narrow.value);
24
+ else
25
+ filters.push(narrow);
26
+ }
27
+ const asked = [...Object.entries(params), ...Object.entries(query)].sort(([a], [b]) => a.localeCompare(b));
28
+ return { params, query, filters, name: asked.length ? `${listName}?${asked.map(([key, value]) => `${key}=${value}`).join("&")}` : listName };
29
+ }
30
+ /** Whether a record fits a narrowing the browser applies: its field holds the value, or is empty with `orUnset`. */
31
+ export function fitsNarrow(record, { key, value, orUnset }) {
32
+ const field = record[key];
33
+ return (value !== null && String(field) === String(value)) || (orUnset === true && (field === null || field === undefined || field === ""));
34
+ }
35
+ /**
36
+ * The plan for a picker over this controller. A controller with no `activeListing` needs a `list`; one with no
37
+ * `findListing` looks nothing up (a value its list does not offer is named from what the form holds, or not found),
38
+ * unless the picker has its own `find`.
39
+ */
40
+ export function crudPickerPlan(controller, { list, valueKey = "id", find, usable }, backend) {
41
+ const listing = list ?? controller.activeListing;
42
+ if (!listing)
43
+ throw new Error("crudPicker: the controller has no activeListing; name the listing to read with `list`.");
44
+ const lookup = find ? { source: find, key: valueKey } : valueKey === "id" && controller.findListing ? { source: controller.findListing, key: "id" } : null;
45
+ return { list: listing, lookup, usable: usable ?? (record => backend.isPickable(record)) };
46
+ }
@@ -0,0 +1,32 @@
1
+ import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
2
+ import type { CrudMode, CrudRowForm } from "../Interfaces/CrudInterfaces.ts";
3
+ import type { FormConfirmParams, FormValues } from "../Interfaces/FormInterfaces.ts";
4
+ import { type FormConfirmQuestion } from "./FormConfirm.ts";
5
+ /**
6
+ * Whether a row form is offered on a record: on its lists (default the lists that offer Edit), to a person who may call
7
+ * its `submit` and, when it is an endpoint, its `load`, and by the page's own `when`.
8
+ */
9
+ export declare function crudRowFormOffered<TRow extends FormValues>(form: CrudRowForm<TRow, FormValues>, row: TRow, mode: CrudMode, can: (endpoint: ApiEndpoint) => boolean): boolean;
10
+ /**
11
+ * What a save changes on the record, as `writes` says: the answer's fields it names (each one the answer carries; an
12
+ * answer that is not a record changes nothing), or what its function answers. Never the record as the form was opened
13
+ * on: the CRUD merges this over the record as it holds it when the answer lands, so a change made meanwhile (an Edit
14
+ * while the popup was minimized) stays.
15
+ */
16
+ export declare function crudRowFormWrites<TRow extends FormValues, TValues extends FormValues>(writes: CrudRowForm<TRow, TValues>["writes"], result: unknown, row: TRow, values: TValues): Partial<TRow> | null;
17
+ /**
18
+ * Whether a row form is its question alone: it gives no inputs. Choosing it asks (its `confirm`, else "{label} {name}?")
19
+ * and sends on Confirm, with no popup form.
20
+ */
21
+ export declare function crudRowFormAsksOnly<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>): boolean;
22
+ /**
23
+ * The question a row form asks for this record: its `confirm` as given, or made from the record and what `load` read;
24
+ * for a question alone that gives none, "{label} {name}?" (`name` is the record's, as its row is named). `null`: a form
25
+ * that saves without asking.
26
+ */
27
+ export declare function crudRowFormConfirm<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>, row: TRow, loaded: TValues | null, name: string): FormConfirmParams<TValues> | null;
28
+ /**
29
+ * The question settled for these values and this body (`formConfirmQuestion`). A question alone has no form to go back
30
+ * to: its buttons are the action's own word (`submitLabel`, else `label`) and "Cancel", unless the page names them.
31
+ */
32
+ export declare function crudRowFormQuestion<TRow extends FormValues, TValues extends FormValues>(form: CrudRowForm<TRow, TValues>, confirm: FormConfirmParams<TValues>, values: TValues, body: unknown): FormConfirmQuestion;