nexus-shared 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +1564 -0
  2. package/README.md +1705 -1123
  3. package/dist/Client.Index.d.ts +14 -0
  4. package/dist/Client.Index.js +14 -0
  5. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  6. package/dist/Components/Documents/DocumentFrame.js +83 -0
  7. package/dist/Components/Documents/Menu.d.ts +4 -0
  8. package/dist/Components/Documents/Menu.js +40 -24
  9. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  10. package/dist/Components/Documents/PrintDocument.js +62 -0
  11. package/dist/Components/Documents/Tabs.d.ts +59 -0
  12. package/dist/Components/Documents/Tabs.js +70 -0
  13. package/dist/Components/Forms/ApiForm.js +19 -2
  14. package/dist/Components/Forms/Crud.d.ts +2 -2
  15. package/dist/Components/Forms/Crud.js +576 -243
  16. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  17. package/dist/Components/Forms/CrudActions.js +184 -0
  18. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  19. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  21. package/dist/Components/Forms/CrudParts.js +55 -5
  22. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  23. package/dist/Components/Forms/CrudRowForms.js +114 -0
  24. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  25. package/dist/Components/Forms/CrudScopes.js +76 -0
  26. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  27. package/dist/Components/Forms/CrudTree.js +363 -0
  28. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  29. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  30. package/dist/Components/Forms/ExcelImport.js +3 -3
  31. package/dist/Components/Forms/Form.d.ts +4 -4
  32. package/dist/Components/Forms/Form.js +123 -17
  33. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  34. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  35. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  36. package/dist/Components/Forms/RecordFlows.js +282 -0
  37. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  38. package/dist/Components/Forms/SubmitForm.js +195 -10
  39. package/dist/Components/Inputs/CheckBox.js +1 -1
  40. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  41. package/dist/Components/Inputs/ChoiceField.js +28 -2
  42. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  43. package/dist/Components/Inputs/Dropdown.js +5 -5
  44. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  45. package/dist/Components/Inputs/DropdownParts.js +16 -4
  46. package/dist/Components/Inputs/Field.d.ts +7 -1
  47. package/dist/Components/Inputs/Field.js +2 -2
  48. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  49. package/dist/Components/Inputs/GroupForm.js +10 -11
  50. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  51. package/dist/Components/Inputs/InputRenderer.js +37 -32
  52. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  53. package/dist/Components/Inputs/PictureInput.js +70 -0
  54. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  55. package/dist/Components/Inputs/RadioGroup.js +8 -3
  56. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  57. package/dist/Components/Inputs/RowsInput.js +18 -3
  58. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  59. package/dist/Components/Inputs/TabularForm.js +29 -29
  60. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  61. package/dist/Components/Inputs/TextArea.js +11 -2
  62. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  63. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  64. package/dist/Components/Inputs/TreeView.d.ts +53 -2
  65. package/dist/Components/Inputs/TreeView.js +147 -13
  66. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  67. package/dist/Components/Layouts/MessageBox.js +56 -7
  68. package/dist/Components/Layouts/Popup.d.ts +13 -6
  69. package/dist/Components/Layouts/Popup.js +37 -9
  70. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  71. package/dist/Components/Layouts/PopupDock.js +2 -1
  72. package/dist/Components/Layouts/PopupHost.js +5 -4
  73. package/dist/Components/Viewers/DataTable.js +150 -48
  74. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  75. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  76. package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
  77. package/dist/Components/Viewers/DataTableParts.js +26 -14
  78. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  79. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  80. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  81. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  82. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  83. package/dist/Components/Viewers/RecordCards.js +509 -0
  84. package/dist/Helpers/BusyHooks.d.ts +6 -0
  85. package/dist/Helpers/BusyHooks.js +39 -0
  86. package/dist/Helpers/CrudBackend.d.ts +20 -2
  87. package/dist/Helpers/CrudBackend.js +46 -2
  88. package/dist/Helpers/CrudColumns.d.ts +53 -6
  89. package/dist/Helpers/CrudColumns.js +98 -7
  90. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  91. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  92. package/dist/Helpers/CrudHelpers.d.ts +81 -2
  93. package/dist/Helpers/CrudHelpers.js +195 -0
  94. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  95. package/dist/Helpers/CrudNarrowPick.js +157 -0
  96. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  97. package/dist/Helpers/CrudRowForms.js +43 -0
  98. package/dist/Helpers/CrudScopes.d.ts +32 -0
  99. package/dist/Helpers/CrudScopes.js +79 -0
  100. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  101. package/dist/Helpers/DropdownHelpers.js +6 -2
  102. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  103. package/dist/Helpers/ExcelHelpers.js +12 -11
  104. package/dist/Helpers/FileAdapter.d.ts +78 -0
  105. package/dist/Helpers/FileAdapter.js +102 -0
  106. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  107. package/dist/Helpers/FilePrimaries.js +138 -0
  108. package/dist/Helpers/FormConfirm.d.ts +32 -0
  109. package/dist/Helpers/FormConfirm.js +40 -0
  110. package/dist/Helpers/FormStore.d.ts +90 -5
  111. package/dist/Helpers/FormStore.js +318 -22
  112. package/dist/Helpers/InputConditions.d.ts +50 -0
  113. package/dist/Helpers/InputConditions.js +168 -0
  114. package/dist/Helpers/InputMemory.d.ts +106 -0
  115. package/dist/Helpers/InputMemory.js +385 -0
  116. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  117. package/dist/Helpers/InputNarrowing.js +67 -0
  118. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  119. package/dist/Helpers/InputParamsHelpers.js +157 -74
  120. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  121. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  122. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  123. package/dist/Helpers/NumberHelpers.js +126 -0
  124. package/dist/Helpers/Permissions.js +2 -1
  125. package/dist/Helpers/QrCode.d.ts +26 -0
  126. package/dist/Helpers/QrCode.js +463 -0
  127. package/dist/Helpers/RecordCards.d.ts +205 -0
  128. package/dist/Helpers/RecordCards.js +378 -0
  129. package/dist/Helpers/RequestCache.d.ts +14 -2
  130. package/dist/Helpers/RequestCache.js +22 -3
  131. package/dist/Helpers/RowsStore.d.ts +37 -2
  132. package/dist/Helpers/RowsStore.js +128 -10
  133. package/dist/Helpers/ServerPreview.d.ts +69 -0
  134. package/dist/Helpers/ServerPreview.js +99 -0
  135. package/dist/Helpers/TableColumns.js +4 -3
  136. package/dist/Helpers/TableExport.d.ts +7 -1
  137. package/dist/Helpers/TableExport.js +9 -3
  138. package/dist/Helpers/TableFilters.d.ts +1 -1
  139. package/dist/Helpers/TableFilters.js +24 -23
  140. package/dist/Helpers/TableHelpers.d.ts +23 -2
  141. package/dist/Helpers/TableHelpers.js +51 -10
  142. package/dist/Helpers/TableJoins.d.ts +13 -0
  143. package/dist/Helpers/TableJoins.js +34 -0
  144. package/dist/Helpers/TableStore.d.ts +21 -1
  145. package/dist/Helpers/TableStore.js +64 -7
  146. package/dist/Helpers/TreeStore.d.ts +81 -0
  147. package/dist/Helpers/TreeStore.js +365 -0
  148. package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
  149. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  150. package/dist/Interfaces/FileInterfaces.js +4 -0
  151. package/dist/Interfaces/FormInterfaces.d.ts +391 -30
  152. package/dist/Interfaces/FormInterfaces.js +79 -1
  153. package/dist/Interfaces/InputInterfaces.d.ts +15 -3
  154. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  155. package/dist/Interfaces/RecordInterfaces.js +1 -0
  156. package/dist/Interfaces/TableInterfaces.d.ts +74 -1
  157. package/dist/Services/ExcelApi.js +5 -0
  158. package/dist/Services/FileApi.d.ts +13 -0
  159. package/dist/Services/FileApi.js +29 -0
  160. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  161. package/dist/Services/ImageEditorLoader.js +51 -0
  162. package/dist/Services/JoinSources.d.ts +37 -5
  163. package/dist/Services/JoinSources.js +90 -14
  164. package/dist/Services/ModuleImport.d.ts +7 -0
  165. package/dist/Services/ModuleImport.js +22 -0
  166. package/dist/Shared.Index.d.ts +16 -0
  167. package/dist/Shared.Index.js +16 -0
  168. package/package.json +7 -2
  169. package/src/Styles/Nexus.Avatar.css +6 -5
  170. package/src/Styles/Nexus.Button.css +10 -4
  171. package/src/Styles/Nexus.Choice.css +16 -0
  172. package/src/Styles/Nexus.Crud.css +383 -9
  173. package/src/Styles/Nexus.Dropdown.css +8 -0
  174. package/src/Styles/Nexus.Files.css +410 -0
  175. package/src/Styles/Nexus.Form.css +5 -2
  176. package/src/Styles/Nexus.Index.css +4 -0
  177. package/src/Styles/Nexus.Input.css +42 -0
  178. package/src/Styles/Nexus.Menu.css +10 -1
  179. package/src/Styles/Nexus.Popup.css +77 -1
  180. package/src/Styles/Nexus.Print.css +288 -0
  181. package/src/Styles/Nexus.QrCode.css +12 -0
  182. package/src/Styles/Nexus.Record.css +430 -0
  183. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  184. package/src/Styles/Nexus.Table.css +67 -5
  185. package/src/Styles/Nexus.Themes.css +12 -6
  186. package/src/Styles/Nexus.Tokens.css +8 -3
  187. package/src/Styles/Nexus.Tree.css +31 -1
@@ -1,3 +1,4 @@
1
+ import { readPath } from "./TableHelpers.js";
1
2
  /*
2
3
  * The rules of the CRUD component, with no React: which lists and actions a controller offers, which actions each list
3
4
  * shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned
@@ -129,6 +130,200 @@ export function leavesList(action, mode, markOn) {
129
130
  return false;
130
131
  }
131
132
  }
133
+ /**
134
+ * A CRUD change as changes to a tree that shows the same records (`CrudTreePlacement`): a record in the active list is
135
+ * put under its parent - added, changed or moved - and one out of it (in the trash, deleted) leaves the tree. Where it
136
+ * was comes from `before`: under its parent while it was active, out of the tree when it was not or is new, and not
137
+ * known otherwise (the tree then looks for it). A record without a status counts as active. Pins and favourites are a
138
+ * user's own and change no tree.
139
+ */
140
+ export function crudTreeChanges(change, place, backend) {
141
+ if (change.action === "pin" || change.action === "favorite")
142
+ return [];
143
+ const valueKey = place.rules.valueKey ?? "id";
144
+ const inTree = (row) => backend.listMembership(row, "normal") !== false;
145
+ const list = [];
146
+ change.rows.forEach((row, index) => {
147
+ const before = change.before?.[index];
148
+ const was = change.action === "create" ? false : before === undefined ? undefined : inTree(before) ? place.parentOf(before) : false;
149
+ if (inTree(row))
150
+ list.push({ type: "put", item: place.itemOf(row), parent: place.parentOf(row), before: was });
151
+ else if (was !== false)
152
+ list.push({ type: "remove", value: place.itemOf(row)[valueKey], parent: was });
153
+ });
154
+ return list;
155
+ }
156
+ /**
157
+ * The records whose child count a change moved, among `rows` (a list's rows), through a tree placement: what the tree's
158
+ * parents follow (`crudTreeChanges`) written into the parents' own rows. A record added or back under a parent counts one
159
+ * more there; one moved counts one fewer where it was and one more where it went; one trashed or deleted one fewer. Each
160
+ * parent row comes back with what `recordOf` reads from its item with the count moved written over it (so `recordOf`
161
+ * 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
162
+ * left out.
163
+ */
164
+ export function crudParentCounts(treeChanges, rows, place) {
165
+ const countKey = place.rules.hasChildrenKey;
166
+ if (!countKey || treeChanges.length === 0)
167
+ return [];
168
+ const valueKey = place.rules.valueKey ?? "id";
169
+ const deltas = new Map();
170
+ const move = (parent, delta) => {
171
+ if (parent === null || parent === undefined)
172
+ return;
173
+ deltas.set(String(parent), (deltas.get(String(parent)) ?? 0) + delta);
174
+ };
175
+ for (const change of treeChanges) {
176
+ if (change.type === "remove")
177
+ move(change.parent, -1);
178
+ else if (change.before === false)
179
+ move(change.parent, 1);
180
+ else if (change.before !== undefined && String(change.before) !== String(change.parent)) {
181
+ move(change.before, -1);
182
+ move(change.parent, 1);
183
+ }
184
+ }
185
+ if (![...deltas.values()].some(Boolean))
186
+ return [];
187
+ const counted = [];
188
+ for (const row of rows) {
189
+ const item = place.itemOf(row);
190
+ const delta = deltas.get(String(item[valueKey]));
191
+ const count = item[countKey];
192
+ if (!delta || typeof count !== "number")
193
+ continue;
194
+ const record = place.recordOf({ ...item, [countKey]: Math.max(0, count + delta) });
195
+ // Over the row the list holds, so a placement whose item carries only some of its fields loses none of them.
196
+ if (record)
197
+ counted.push({ ...row, ...record });
198
+ }
199
+ return counted;
200
+ }
201
+ /**
202
+ * A saved record with the child count a tree placement keeps for it (`rules.hasChildrenKey`): a new record's own, as
203
+ * `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
204
+ * the form, and a child added since counts. Written over the record through `recordOf`, which carries an item's count
205
+ * back; a placement without a count leaves the record as it is.
206
+ */
207
+ export function crudSavedCount(record, held, place) {
208
+ const countKey = place.rules.hasChildrenKey;
209
+ if (!countKey)
210
+ return record;
211
+ let item = place.itemOf(record);
212
+ if (held) {
213
+ const count = place.itemOf(held)[countKey];
214
+ if (count !== undefined)
215
+ item = { ...item, [countKey]: count };
216
+ }
217
+ const counted = place.recordOf(item);
218
+ return counted ? { ...record, ...counted } : record;
219
+ }
220
+ /**
221
+ * The names the database joins into a record (`crudColumns.dbJoin` columns with `from`), written again after a save that
222
+ * answered only what was sent - with no call. For each such column: a key the save kept as `before` had it keeps
223
+ * `before`'s name and status; an emptied key empties both; a new or changed key takes the name and the status of the
224
+ * record `find(from, key)` gives - the one picked in the form, or one the page holds. Where it gives none, a new record
225
+ * keeps the name its save answered, if any, and an edited one drops the old record's name and status rather than show
226
+ * them for the new key: either way the record is then read back once (`crudJoinsMissing`). Names and statuses are written
227
+ * as top-level fields of the record.
228
+ */
229
+ export function crudJoinedNames(record, before, columns, find) {
230
+ const row = { ...record };
231
+ for (const column of columns) {
232
+ const join = column.dbJoin;
233
+ if (!join?.from)
234
+ continue;
235
+ const write = (name, status) => {
236
+ row[column.key] = name ?? null;
237
+ if (join.status)
238
+ row[join.status] = status ?? null;
239
+ };
240
+ const key = readPath(record, join.from);
241
+ if (key === undefined || key === null || key === "") {
242
+ write(null, null);
243
+ continue;
244
+ }
245
+ const held = before ? readPath(before, join.from) : undefined;
246
+ if (held !== undefined && held !== null && String(held) === String(key)) {
247
+ write(readPath(before, column.key), join.status ? readPath(before, join.status) : null);
248
+ continue;
249
+ }
250
+ const found = find(join.from, String(key));
251
+ if (found) {
252
+ write(found.name, found.status);
253
+ }
254
+ else if (before && readPath(record, column.key) === readPath(before, column.key)) {
255
+ // The name the record had is the old key's.
256
+ delete row[column.key];
257
+ if (join.status)
258
+ delete row[join.status];
259
+ }
260
+ }
261
+ return row;
262
+ }
263
+ /**
264
+ * Whether a saved record still lacks a name the database joins into it, which is the one case a CRUD reads it back
265
+ * (`details`, once): a `crudColumns.dbJoin` column whose name the record does not hold while its key is set - no record
266
+ * was known for a new key (`crudJoinedNames`) - or one that names no key field (`from`) on a new record, whose save
267
+ * answered no name. An edited record keeps the name it had on a column without `from`: whether its key changed cannot
268
+ * be told.
269
+ */
270
+ export function crudJoinsMissing(record, before, columns) {
271
+ for (const column of columns) {
272
+ const join = column.dbJoin;
273
+ if (!join || readPath(record, column.key) !== undefined)
274
+ continue;
275
+ if (!join.from) {
276
+ if (!before)
277
+ return true;
278
+ continue;
279
+ }
280
+ const key = readPath(record, join.from);
281
+ if (key !== undefined && key !== null && key !== "")
282
+ return true;
283
+ }
284
+ return false;
285
+ }
286
+ /**
287
+ * Where an Add or Edit form with fixed fields stands, as text for its popup's key and its draft's: the fixed fields'
288
+ * names and values, in name order (`"@countryId=7&parentId="`), or "" when nothing is fixed. So a form opened inside one
289
+ * parent never comes back inside another, a fixed form never stands in for a free one, and a draft written before the
290
+ * record moved is not brought back over its new place.
291
+ */
292
+ export function crudFormPlace(fixed) {
293
+ if (!fixed)
294
+ return "";
295
+ const names = Object.keys(fixed).sort();
296
+ if (names.length === 0)
297
+ return "";
298
+ return `@${names.map(name => `${name}=${String(fixed[name] ?? "")}`).join("&")}`;
299
+ }
300
+ /**
301
+ * A `Crud` `narrow` read once: the field, the picked record's value (its `valueKey` field, else its `key` field) and its
302
+ * name (its `labelKey` field, else the value). `null` without a narrowing; a value of `null` while nothing is picked, or
303
+ * the picked record holds no value.
304
+ */
305
+ export function crudNarrowing(narrow) {
306
+ if (!narrow)
307
+ return null;
308
+ const picked = narrow.picked;
309
+ const raw = picked ? readPath(picked, narrow.valueKey ?? narrow.key) : undefined;
310
+ if (raw === undefined || raw === null || raw === "")
311
+ return { key: narrow.key, value: null, raw: null, label: "" };
312
+ const name = narrow.labelKey && picked ? readPath(picked, narrow.labelKey) : undefined;
313
+ return { key: narrow.key, value: String(raw), raw, label: name === undefined || name === null || name === "" ? String(raw) : String(name) };
314
+ }
315
+ /**
316
+ * Whether a record belongs in a narrowed list: its field holds the picked record's value (compared as text, so 7 and "7"
317
+ * are one). Every record does without a narrowing, and none while nothing is picked.
318
+ */
319
+ export function crudInNarrowing(row, narrowing) {
320
+ if (!narrowing)
321
+ return true;
322
+ if (narrowing.value === null)
323
+ return false;
324
+ const held = readPath(row, narrowing.key);
325
+ return held !== undefined && held !== null && String(held) === narrowing.value;
326
+ }
132
327
  /** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
133
328
  export function crudMessageAction(action, markOn) {
134
329
  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,15 @@
1
+ import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
2
+ import type { CrudMode, CrudRowForm } from "../Interfaces/CrudInterfaces.ts";
3
+ import type { FormValues } from "../Interfaces/FormInterfaces.ts";
4
+ /**
5
+ * Whether a row form is offered on a record: on its lists (default the lists that offer Edit), to a person who may call
6
+ * its `submit` and, when it is an endpoint, its `load`, and by the page's own `when`.
7
+ */
8
+ export declare function crudRowFormOffered<TRow extends FormValues>(form: CrudRowForm<TRow, FormValues>, row: TRow, mode: CrudMode, can: (endpoint: ApiEndpoint) => boolean): boolean;
9
+ /**
10
+ * What a save changes on the record, as `writes` says: the answer's fields it names (each one the answer carries; an
11
+ * answer that is not a record changes nothing), or what its function answers. Never the record as the form was opened
12
+ * on: the CRUD merges this over the record as it holds it when the answer lands, so a change made meanwhile (an Edit
13
+ * while the popup was minimized) stays.
14
+ */
15
+ export declare function crudRowFormWrites<TRow extends FormValues, TValues extends FormValues>(writes: CrudRowForm<TRow, TValues>["writes"], result: unknown, row: TRow, values: TValues): Partial<TRow> | null;
@@ -0,0 +1,43 @@
1
+ import { CRUD_ROW_ACTIONS } from "./CrudHelpers.js";
2
+ import { isApiEndpoint } from "./ApiRoutes.js";
3
+ /*
4
+ * The pure half of a row action that opens a form of its own for the record (`Crud` `rowForms`,
5
+ * Components/Forms/CrudRowForms.tsx): where it is offered, and what of a save's answer is written back into the record.
6
+ * Pure, so Node tests run it.
7
+ */
8
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
+ /**
10
+ * Whether a row form is offered on a record: on its lists (default the lists that offer Edit), to a person who may call
11
+ * its `submit` and, when it is an endpoint, its `load`, and by the page's own `when`.
12
+ */
13
+ export function crudRowFormOffered(form, row, mode, can) {
14
+ const modes = form.modes ?? Object.keys(CRUD_ROW_ACTIONS).filter(list => CRUD_ROW_ACTIONS[list].includes("edit"));
15
+ if (!modes.includes(mode))
16
+ return false;
17
+ if (!can(form.submit))
18
+ return false;
19
+ if (isApiEndpoint(form.load) && !can(form.load))
20
+ return false;
21
+ return form.when ? form.when(row, mode) : true;
22
+ }
23
+ /**
24
+ * What a save changes on the record, as `writes` says: the answer's fields it names (each one the answer carries; an
25
+ * answer that is not a record changes nothing), or what its function answers. Never the record as the form was opened
26
+ * on: the CRUD merges this over the record as it holds it when the answer lands, so a change made meanwhile (an Edit
27
+ * while the popup was minimized) stays.
28
+ */
29
+ export function crudRowFormWrites(writes, result, row, values) {
30
+ if (!writes)
31
+ return null;
32
+ if (typeof writes === "function") {
33
+ const patch = writes(result, row, values);
34
+ return patch && Object.keys(patch).length ? patch : null;
35
+ }
36
+ if (!isRecord(result))
37
+ return null;
38
+ const patch = {};
39
+ for (const field of writes)
40
+ if (result[field] !== undefined)
41
+ patch[field] = result[field];
42
+ return Object.keys(patch).length ? patch : null;
43
+ }
@@ -0,0 +1,32 @@
1
+ import type { CrudProps, CrudScope, CrudScopes } from "../Interfaces/CrudInterfaces.ts";
2
+ import type { FormValues } from "../Interfaces/FormInterfaces.ts";
3
+ /**
4
+ * The scopes offered now: those whose `when` (the page's own rule) allows them and whose lists the person may call
5
+ * (`callable`, the CRUD's permission test on the scope's endpoints). None while the permissions are not known.
6
+ */
7
+ export declare function offeredCrudScopes<TRow extends FormValues>(scopes: CrudScopes<TRow> | undefined, callable: (scope: CrudScope<TRow>) => boolean): CrudScope<TRow>[];
8
+ /** The scope shown: the one chosen while it is offered, else the CRUD's own list (`null`). */
9
+ export declare function shownCrudScope<TRow extends FormValues>(offered: readonly CrudScope<TRow>[], chosen: string | null): CrudScope<TRow> | null;
10
+ /**
11
+ * The CRUD's props on a scope: its endpoints, and its columns, inputs, forms, table settings and name where it gives its
12
+ * own (`form` and `table` over the CRUD's, field by field). The CRUD's own list is the props as they are given.
13
+ */
14
+ export declare function crudScopeProps<TRow extends FormValues>(given: CrudProps<TRow>, scope: CrudScope<TRow> | null): CrudProps<TRow>;
15
+ /**
16
+ * Where a scope keeps its table's settings: the scope's own `table.stateKey`, else the CRUD's key and the scope's id, so
17
+ * each list keeps the columns shown, their order and widths, its sort and its page size apart (a Branch column hidden
18
+ * on one branch's list and shown on every branch's). The CRUD's own list keeps the CRUD's key.
19
+ */
20
+ export declare function crudScopeStateKey<TRow extends FormValues>(ownKey: string, scope: CrudScope<TRow> | null): string;
21
+ /** Where this browser keeps the choice: `nx-crud-scope:{key}:{viewer}`; nothing without a viewer. */
22
+ export declare function crudScopeStorageKey(remember: {
23
+ key: string;
24
+ viewer?: string | null;
25
+ } | undefined): string | null;
26
+ /**
27
+ * The scope kept: its id, `null` for the CRUD's own list (kept as nothing), `undefined` when nothing could be read (no
28
+ * storage, no key). A kept scope the CRUD no longer offers stays kept and opens the CRUD's own list until it is offered.
29
+ */
30
+ export declare function readCrudScope(storage: Pick<Storage, "getItem"> | null, key: string | null): string | null | undefined;
31
+ /** Keeps the choice; the CRUD's own list is kept as nothing. A browser that keeps nothing opens the CRUD's own list. */
32
+ export declare function writeCrudScope(storage: Pick<Storage, "setItem" | "removeItem"> | null, key: string | null, scope: string | null): void;
@@ -0,0 +1,79 @@
1
+ /*
2
+ * The pure half of a CRUD whose list can be widened (`Crud` `scopes`, Components/Forms/CrudScopes.tsx): which lists are
3
+ * offered, which one shows, the props it gives the CRUD, and where this browser keeps the choice. Pure, so Node tests
4
+ * run it.
5
+ */
6
+ /**
7
+ * The scopes offered now: those whose `when` (the page's own rule) allows them and whose lists the person may call
8
+ * (`callable`, the CRUD's permission test on the scope's endpoints). None while the permissions are not known.
9
+ */
10
+ export function offeredCrudScopes(scopes, callable) {
11
+ if (!scopes)
12
+ return [];
13
+ return scopes.options.filter(scope => (scope.when ? scope.when() : true) && callable(scope));
14
+ }
15
+ /** The scope shown: the one chosen while it is offered, else the CRUD's own list (`null`). */
16
+ export function shownCrudScope(offered, chosen) {
17
+ return chosen === null ? null : (offered.find(scope => scope.id === chosen) ?? null);
18
+ }
19
+ /**
20
+ * The CRUD's props on a scope: its endpoints, and its columns, inputs, forms, table settings and name where it gives its
21
+ * own (`form` and `table` over the CRUD's, field by field). The CRUD's own list is the props as they are given.
22
+ */
23
+ export function crudScopeProps(given, scope) {
24
+ if (!scope)
25
+ return given;
26
+ return {
27
+ ...given,
28
+ api: scope.api,
29
+ columns: scope.columns ?? given.columns,
30
+ inputs: scope.inputs ?? given.inputs,
31
+ form: scope.form ? { ...given.form, ...scope.form } : given.form,
32
+ table: scope.table ? { ...given.table, ...scope.table } : given.table,
33
+ "aria-label": scope["aria-label"] ?? given["aria-label"],
34
+ };
35
+ }
36
+ /**
37
+ * Where a scope keeps its table's settings: the scope's own `table.stateKey`, else the CRUD's key and the scope's id, so
38
+ * each list keeps the columns shown, their order and widths, its sort and its page size apart (a Branch column hidden
39
+ * on one branch's list and shown on every branch's). The CRUD's own list keeps the CRUD's key.
40
+ */
41
+ export function crudScopeStateKey(ownKey, scope) {
42
+ if (!scope)
43
+ return ownKey;
44
+ return scope.table?.stateKey ?? `${ownKey}:${scope.id}`;
45
+ }
46
+ /** Where this browser keeps the choice: `nx-crud-scope:{key}:{viewer}`; nothing without a viewer. */
47
+ export function crudScopeStorageKey(remember) {
48
+ const viewer = remember?.viewer?.trim();
49
+ return remember?.key && viewer ? `nx-crud-scope:${remember.key}:${viewer}` : null;
50
+ }
51
+ /**
52
+ * The scope kept: its id, `null` for the CRUD's own list (kept as nothing), `undefined` when nothing could be read (no
53
+ * storage, no key). A kept scope the CRUD no longer offers stays kept and opens the CRUD's own list until it is offered.
54
+ */
55
+ export function readCrudScope(storage, key) {
56
+ if (!storage || !key)
57
+ return undefined;
58
+ try {
59
+ const kept = storage.getItem(key);
60
+ return kept ? kept : null;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /** Keeps the choice; the CRUD's own list is kept as nothing. A browser that keeps nothing opens the CRUD's own list. */
67
+ export function writeCrudScope(storage, key, scope) {
68
+ if (!storage || !key)
69
+ return;
70
+ try {
71
+ if (scope)
72
+ storage.setItem(key, scope);
73
+ else
74
+ storage.removeItem(key);
75
+ }
76
+ catch {
77
+ // Not kept: the CRUD opens its own list next time.
78
+ }
79
+ }
@@ -45,8 +45,10 @@ export declare function filterItems<TItem extends DropdownItem>(items: readonly
45
45
  export declare function defaultSearchKeys(labelKey: string, descriptionKey: string, valueKey: string, columns?: readonly DropdownColumn<never>[]): string[];
46
46
  /** Whether a column draws a picture: an avatar, an image, or an icon. The first one also leads the value in the box. */
47
47
  export declare function isMediaColumn(column: Pick<DropdownColumn<never>, "type">): boolean;
48
- /** A column's grid track: its `width`, or `auto` for pictures and codes and `minmax(0, 1fr)` for text. */
48
+ /** A column's grid track: its `width`, or `auto` for pictures, codes and money and `minmax(0, 1fr)` for text. */
49
49
  export declare function columnTrack(column: Pick<DropdownColumn<never>, "type" | "width">): string;
50
+ /** Where a column's cells and heading sit: its own `align`, else at the end for money (figures line up), else at the start. */
51
+ export declare function dropdownColumnAlign(column: Pick<DropdownColumn<never>, "type" | "align">): "start" | "center" | "end";
50
52
  /**
51
53
  * Up to two initials of a name, from its first and last words: "Ram Bahadur Sharma" → "RS", "admin" → "A",
52
54
  * "sita.rai" → "SR". Letters keep their script, so "राम शर्मा" → "रश".
@@ -150,11 +150,15 @@ export function defaultSearchKeys(labelKey, descriptionKey, valueKey, columns) {
150
150
  export function isMediaColumn(column) {
151
151
  return column.type === "avatar" || column.type === "image" || column.type === "icon";
152
152
  }
153
- /** A column's grid track: its `width`, or `auto` for pictures and codes and `minmax(0, 1fr)` for text. */
153
+ /** A column's grid track: its `width`, or `auto` for pictures, codes and money and `minmax(0, 1fr)` for text. */
154
154
  export function columnTrack(column) {
155
155
  if (column.width)
156
156
  return column.width;
157
- return isMediaColumn(column) || column.type === "code" ? "auto" : "minmax(0, 1fr)";
157
+ return isMediaColumn(column) || column.type === "code" || column.type === "money" ? "auto" : "minmax(0, 1fr)";
158
+ }
159
+ /** Where a column's cells and heading sit: its own `align`, else at the end for money (figures line up), else at the start. */
160
+ export function dropdownColumnAlign(column) {
161
+ return column.align ?? (column.type === "money" ? "end" : "start");
158
162
  }
159
163
  /**
160
164
  * Up to two initials of a name, from its first and last words: "Ram Bahadur Sharma" → "RS", "admin" → "A",
@@ -1,4 +1,4 @@
1
- import type { InputParams } from "../Interfaces/FormInterfaces.ts";
1
+ import { type InputParams } from "../Interfaces/FormInterfaces.ts";
2
2
  import type { ExcelCellProblem, ExcelColumnInfo, ExcelDownload, ExcelReviewRow, ExcelSheetRow } from "../Interfaces/ExcelInterfaces.ts";
3
3
  /** What a spreadsheet file is called in a file picker. */
4
4
  export declare const EXCEL_FILE_ACCEPT = ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";