nexus-shared 3.1.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 (234) hide show
  1. package/CHANGELOG.md +2268 -446
  2. package/LICENSE +21 -21
  3. package/README.md +1705 -1084
  4. package/dist/Client.Index.d.ts +17 -0
  5. package/dist/Client.Index.js +17 -0
  6. package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
  7. package/dist/Components/Documents/DocumentFrame.js +83 -0
  8. package/dist/Components/Documents/Menu.d.ts +4 -0
  9. package/dist/Components/Documents/Menu.js +40 -24
  10. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  11. package/dist/Components/Documents/PrintDocument.js +62 -0
  12. package/dist/Components/Documents/Tabs.d.ts +59 -0
  13. package/dist/Components/Documents/Tabs.js +70 -0
  14. package/dist/Components/Forms/ApiForm.js +19 -2
  15. package/dist/Components/Forms/Crud.d.ts +2 -2
  16. package/dist/Components/Forms/Crud.js +871 -377
  17. package/dist/Components/Forms/CrudActions.d.ts +136 -0
  18. package/dist/Components/Forms/CrudActions.js +184 -0
  19. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  20. package/dist/Components/Forms/CrudHistory.js +140 -0
  21. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  22. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  23. package/dist/Components/Forms/CrudParts.d.ts +30 -2
  24. package/dist/Components/Forms/CrudParts.js +56 -5
  25. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  26. package/dist/Components/Forms/CrudRowForms.js +114 -0
  27. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  28. package/dist/Components/Forms/CrudScopes.js +76 -0
  29. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  30. package/dist/Components/Forms/CrudTree.js +363 -0
  31. package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
  32. package/dist/Components/Forms/CrudWorkspace.js +169 -0
  33. package/dist/Components/Forms/ExcelImport.js +3 -3
  34. package/dist/Components/Forms/Form.d.ts +4 -4
  35. package/dist/Components/Forms/Form.js +125 -16
  36. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  37. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  38. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  39. package/dist/Components/Forms/RecordFlows.js +282 -0
  40. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  41. package/dist/Components/Forms/SubmitForm.js +195 -10
  42. package/dist/Components/Inputs/CheckBox.js +1 -1
  43. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  44. package/dist/Components/Inputs/ChoiceField.js +28 -2
  45. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  46. package/dist/Components/Inputs/CodeBox.js +316 -0
  47. package/dist/Components/Inputs/DatePicker.js +4 -1
  48. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  49. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  50. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  51. package/dist/Components/Inputs/Dropdown.js +8 -5
  52. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  53. package/dist/Components/Inputs/DropdownParts.js +16 -4
  54. package/dist/Components/Inputs/Field.d.ts +7 -1
  55. package/dist/Components/Inputs/Field.js +2 -2
  56. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  57. package/dist/Components/Inputs/GroupForm.js +10 -11
  58. package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
  59. package/dist/Components/Inputs/InputRenderer.js +39 -31
  60. package/dist/Components/Inputs/NumberBox.js +4 -2
  61. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  62. package/dist/Components/Inputs/PictureInput.js +70 -0
  63. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  64. package/dist/Components/Inputs/RadioGroup.js +8 -3
  65. package/dist/Components/Inputs/RowsInput.d.ts +1 -1
  66. package/dist/Components/Inputs/RowsInput.js +18 -3
  67. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  68. package/dist/Components/Inputs/TabularForm.js +29 -29
  69. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  70. package/dist/Components/Inputs/TextArea.js +14 -2
  71. package/dist/Components/Inputs/TextBox.js +4 -1
  72. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  73. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  74. package/dist/Components/Inputs/TimePicker.js +4 -1
  75. package/dist/Components/Inputs/TreeView.d.ts +107 -6
  76. package/dist/Components/Inputs/TreeView.js +513 -30
  77. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  78. package/dist/Components/Layouts/MessageBox.js +56 -7
  79. package/dist/Components/Layouts/Popup.d.ts +13 -6
  80. package/dist/Components/Layouts/Popup.js +37 -9
  81. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  82. package/dist/Components/Layouts/PopupDock.js +2 -1
  83. package/dist/Components/Layouts/PopupHost.js +5 -4
  84. package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
  85. package/dist/Components/Layouts/ThemePicker.js +16 -4
  86. package/dist/Components/Viewers/DataTable.js +205 -61
  87. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  88. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  89. package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
  90. package/dist/Components/Viewers/DataTableParts.js +62 -21
  91. package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
  92. package/dist/Components/Viewers/OwnerFiles.js +589 -0
  93. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  94. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  95. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  96. package/dist/Components/Viewers/RecordCards.js +509 -0
  97. package/dist/Helpers/ApiModules.d.ts +3 -34
  98. package/dist/Helpers/ApiModules.js +3 -3
  99. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  100. package/dist/Helpers/ApiRoutes.js +31 -27
  101. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  102. package/dist/Helpers/ArrayHelpers.js +2 -0
  103. package/dist/Helpers/BusyHooks.d.ts +6 -0
  104. package/dist/Helpers/BusyHooks.js +39 -0
  105. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  106. package/dist/Helpers/CodeHelpers.js +332 -0
  107. package/dist/Helpers/CrudBackend.d.ts +114 -6
  108. package/dist/Helpers/CrudBackend.js +214 -11
  109. package/dist/Helpers/CrudColumns.d.ts +98 -0
  110. package/dist/Helpers/CrudColumns.js +160 -0
  111. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  112. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  113. package/dist/Helpers/CrudHelpers.d.ts +130 -6
  114. package/dist/Helpers/CrudHelpers.js +304 -12
  115. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  116. package/dist/Helpers/CrudNarrowPick.js +157 -0
  117. package/dist/Helpers/CrudRowForms.d.ts +15 -0
  118. package/dist/Helpers/CrudRowForms.js +43 -0
  119. package/dist/Helpers/CrudScopes.d.ts +32 -0
  120. package/dist/Helpers/CrudScopes.js +79 -0
  121. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  122. package/dist/Helpers/DropdownHelpers.js +6 -2
  123. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  124. package/dist/Helpers/ExcelHelpers.js +12 -11
  125. package/dist/Helpers/FileAdapter.d.ts +78 -0
  126. package/dist/Helpers/FileAdapter.js +102 -0
  127. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  128. package/dist/Helpers/FilePrimaries.js +138 -0
  129. package/dist/Helpers/FormConfirm.d.ts +32 -0
  130. package/dist/Helpers/FormConfirm.js +40 -0
  131. package/dist/Helpers/FormStore.d.ts +90 -5
  132. package/dist/Helpers/FormStore.js +318 -22
  133. package/dist/Helpers/InputConditions.d.ts +50 -0
  134. package/dist/Helpers/InputConditions.js +168 -0
  135. package/dist/Helpers/InputMemory.d.ts +106 -0
  136. package/dist/Helpers/InputMemory.js +385 -0
  137. package/dist/Helpers/InputNarrowing.d.ts +18 -0
  138. package/dist/Helpers/InputNarrowing.js +67 -0
  139. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  140. package/dist/Helpers/InputParamsHelpers.js +162 -71
  141. package/dist/Helpers/MessageBuilder.js +2 -0
  142. package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
  143. package/dist/Helpers/NexusDriveAdapter.js +207 -0
  144. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  145. package/dist/Helpers/NexusHelpers.js +1 -0
  146. package/dist/Helpers/NumberHelpers.d.ts +95 -0
  147. package/dist/Helpers/NumberHelpers.js +126 -0
  148. package/dist/Helpers/Permissions.js +2 -1
  149. package/dist/Helpers/QrCode.d.ts +26 -0
  150. package/dist/Helpers/QrCode.js +463 -0
  151. package/dist/Helpers/RecordCards.d.ts +205 -0
  152. package/dist/Helpers/RecordCards.js +378 -0
  153. package/dist/Helpers/RequestCache.d.ts +47 -0
  154. package/dist/Helpers/RequestCache.js +132 -0
  155. package/dist/Helpers/RowsStore.d.ts +37 -2
  156. package/dist/Helpers/RowsStore.js +128 -10
  157. package/dist/Helpers/ServerPreview.d.ts +69 -0
  158. package/dist/Helpers/ServerPreview.js +99 -0
  159. package/dist/Helpers/TableColumns.js +4 -3
  160. package/dist/Helpers/TableExport.d.ts +7 -1
  161. package/dist/Helpers/TableExport.js +21 -15
  162. package/dist/Helpers/TableFilters.d.ts +1 -1
  163. package/dist/Helpers/TableFilters.js +24 -23
  164. package/dist/Helpers/TableHelpers.d.ts +24 -3
  165. package/dist/Helpers/TableHelpers.js +58 -12
  166. package/dist/Helpers/TableJoins.d.ts +83 -0
  167. package/dist/Helpers/TableJoins.js +284 -0
  168. package/dist/Helpers/TableStore.d.ts +26 -1
  169. package/dist/Helpers/TableStore.js +73 -7
  170. package/dist/Helpers/TreeStore.d.ts +306 -11
  171. package/dist/Helpers/TreeStore.js +1048 -90
  172. package/dist/Helpers/UserPreferences.d.ts +8 -0
  173. package/dist/Helpers/UserPreferences.js +17 -0
  174. package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
  175. package/dist/Interfaces/FileInterfaces.d.ts +162 -0
  176. package/dist/Interfaces/FileInterfaces.js +4 -0
  177. package/dist/Interfaces/FormInterfaces.d.ts +393 -29
  178. package/dist/Interfaces/FormInterfaces.js +79 -1
  179. package/dist/Interfaces/InputInterfaces.d.ts +39 -3
  180. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  181. package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
  182. package/dist/Interfaces/PreferenceInterfaces.js +2 -5
  183. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  184. package/dist/Interfaces/RecordInterfaces.js +1 -0
  185. package/dist/Interfaces/TableInterfaces.d.ts +160 -5
  186. package/dist/Services/ApiProxy.d.ts +36 -1
  187. package/dist/Services/ApiProxy.js +182 -52
  188. package/dist/Services/ApiProxyRules.d.ts +203 -0
  189. package/dist/Services/ApiProxyRules.js +426 -0
  190. package/dist/Services/ExcelApi.js +5 -0
  191. package/dist/Services/FileApi.d.ts +13 -0
  192. package/dist/Services/FileApi.js +29 -0
  193. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  194. package/dist/Services/ImageEditorLoader.js +51 -0
  195. package/dist/Services/JoinSources.d.ts +80 -0
  196. package/dist/Services/JoinSources.js +150 -0
  197. package/dist/Services/ModuleImport.d.ts +7 -0
  198. package/dist/Services/ModuleImport.js +22 -0
  199. package/dist/Services/ServerApi.d.ts +13 -2
  200. package/dist/Services/ServerApi.js +8 -3
  201. package/dist/Shared.Index.d.ts +20 -0
  202. package/dist/Shared.Index.js +20 -0
  203. package/package.json +92 -85
  204. package/src/Styles/Nexus.Avatar.css +80 -79
  205. package/src/Styles/Nexus.Base.css +117 -117
  206. package/src/Styles/Nexus.Button.css +186 -180
  207. package/src/Styles/Nexus.Calendar.css +273 -273
  208. package/src/Styles/Nexus.Chat.css +1443 -1443
  209. package/src/Styles/Nexus.Choice.css +359 -343
  210. package/src/Styles/Nexus.Crud.css +542 -168
  211. package/src/Styles/Nexus.DateTime.css +81 -81
  212. package/src/Styles/Nexus.Dropdown.css +733 -725
  213. package/src/Styles/Nexus.Editor.css +295 -295
  214. package/src/Styles/Nexus.Excel.css +389 -389
  215. package/src/Styles/Nexus.Files.css +410 -0
  216. package/src/Styles/Nexus.Form.css +282 -279
  217. package/src/Styles/Nexus.Index.css +41 -37
  218. package/src/Styles/Nexus.Input.css +238 -4
  219. package/src/Styles/Nexus.Markdown.css +237 -237
  220. package/src/Styles/Nexus.Menu.css +246 -237
  221. package/src/Styles/Nexus.Popup.css +800 -724
  222. package/src/Styles/Nexus.Print.css +288 -0
  223. package/src/Styles/Nexus.QrCode.css +12 -0
  224. package/src/Styles/Nexus.Record.css +430 -0
  225. package/src/Styles/Nexus.Rows.css +637 -637
  226. package/src/Styles/Nexus.Slider.css +276 -276
  227. package/src/Styles/Nexus.Tab.Buttons.css +497 -432
  228. package/src/Styles/Nexus.Table.css +98 -35
  229. package/src/Styles/Nexus.Theme.Picker.css +59 -3
  230. package/src/Styles/Nexus.Themes.css +200 -194
  231. package/src/Styles/Nexus.Time.css +144 -144
  232. package/src/Styles/Nexus.Toast.css +263 -263
  233. package/src/Styles/Nexus.Tokens.css +94 -89
  234. package/src/Styles/Nexus.Tree.css +530 -431
@@ -1,19 +1,20 @@
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
- * shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned),
4
- * and the cache of lists kept while the page is open - including how a change is written into the lists kept, so that
5
- * a record saved or marked costs no request on any list.
4
+ * shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned
5
+ * or made favourites), a record's marks, and the cache of lists kept while the page is open - including how a change
6
+ * is written into the lists kept, so that a record saved or marked costs no request on any list.
6
7
  */
7
8
  /** Every list, in the order the CRUD shows them. */
8
9
  export const CRUD_MODES = ["normal", "flag", "trash", "deleted"];
9
- const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "trash"];
10
- const ACTIVE_MASS_ACTIONS = ["flag", "pin", "description", "trash"];
11
- /** The built-in actions on one record, per list, in their menu order. */
10
+ const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "favorite", "history", "trash"];
11
+ const ACTIVE_MASS_ACTIONS = ["flag", "pin", "favorite", "description", "trash"];
12
+ /** The built-in actions on one record, per list, in their menu order. A record's history is on every list. */
12
13
  export const CRUD_ROW_ACTIONS = {
13
14
  normal: ACTIVE_ROW_ACTIONS,
14
15
  flag: ACTIVE_ROW_ACTIONS,
15
- trash: ["view", "recover", "delete"],
16
- deleted: ["view", "trashFromDelete"],
16
+ trash: ["view", "history", "recover", "delete"],
17
+ deleted: ["view", "history", "trashFromDelete"],
17
18
  };
18
19
  /** The built-in actions on selected records, per list. */
19
20
  export const CRUD_MASS_ACTIONS = {
@@ -56,14 +57,31 @@ export function crudMassActions(mode, allowed, trashless = false) {
56
57
  export function crudListEndpoint(api, mode, paging) {
57
58
  return paging === "server" ? api.pages?.[mode] : api.lists?.[mode];
58
59
  }
59
- /** The endpoint of a row action. `view` needs none of its own: it shows the row (and calls `details` when there is one). */
60
+ /**
61
+ * The endpoint of a row action. `view` needs none of its own: it shows the row (and calls `details` when there is one).
62
+ * Pin and Favourite both set the record's marks through `bookmark`; a backend without it may still pin, through `pin`.
63
+ */
60
64
  export function crudRowEndpoint(api, action) {
61
65
  if (action === "view")
62
66
  return api.details;
63
67
  if (action === "edit")
64
68
  return api.update;
69
+ if (action === "favorite")
70
+ return api.bookmark;
71
+ if (action === "pin")
72
+ return api.bookmark ?? api.pin;
73
+ if (action === "history")
74
+ return api.audit;
65
75
  return api[action];
66
76
  }
77
+ /** The endpoint of an action on many records: Pin and Favourite through `mass.bookmark`, or Pin through `mass.pin`. */
78
+ export function crudMassEndpoint(api, action) {
79
+ if (action === "favorite")
80
+ return api.mass?.bookmark;
81
+ if (action === "pin")
82
+ return api.mass?.bookmark ?? api.mass?.pin;
83
+ return api.mass?.[action];
84
+ }
67
85
  /** The lists a CRUD can offer: asked for, with an endpoint, and allowed. */
68
86
  export function availableModes(api, paging, wanted, can) {
69
87
  return CRUD_MODES.filter(mode => wanted.includes(mode)).filter(mode => {
@@ -91,13 +109,13 @@ export function actionAllowed(action, api, wanted, can) {
91
109
  export function massActionAllowed(action, api, wanted, can) {
92
110
  if (!wanted.includes(action))
93
111
  return false;
94
- const endpoint = api.mass?.[action];
112
+ const endpoint = crudMassEndpoint(api, action);
95
113
  return Boolean(endpoint) && can(endpoint);
96
114
  }
97
115
  /**
98
116
  * Whether a record leaves the list shown after an action. Trash, recover, delete, and moving back to the trash take it
99
- * to another list, and unflagging leaves the flagged list. Pinning and unpinning never do: a pinned
100
- * record stays in the list it is in. `markOn` is the record's new flag.
117
+ * to another list, and unflagging leaves the flagged list. A pin or a favourite never does: a marked record stays in
118
+ * the list it is in. `markOn` is the record's new flag.
101
119
  */
102
120
  export function leavesList(action, mode, markOn) {
103
121
  switch (action) {
@@ -112,6 +130,200 @@ export function leavesList(action, mode, markOn) {
112
130
  return false;
113
131
  }
114
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
+ }
115
327
  /** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
116
328
  export function crudMessageAction(action, markOn) {
117
329
  switch (action) {
@@ -119,6 +331,8 @@ export function crudMessageAction(action, markOn) {
119
331
  return markOn === false ? "unflag" : "flag";
120
332
  case "pin":
121
333
  return markOn === false ? "unpin" : "pin";
334
+ case "favorite":
335
+ return markOn === false ? "unfavorite" : "favorite";
122
336
  case "trashFromDelete":
123
337
  return "trash";
124
338
  case "description":
@@ -126,6 +340,8 @@ export function crudMessageAction(action, markOn) {
126
340
  case "edit":
127
341
  case "view":
128
342
  return "save";
343
+ case "history":
344
+ return "load";
129
345
  default:
130
346
  return action;
131
347
  }
@@ -153,6 +369,82 @@ export function pinRoom(pinned, max = getCrudSettings().maxPins) {
153
369
  return Number.POSITIVE_INFINITY;
154
370
  return Math.max(0, max - pinned);
155
371
  }
372
+ /* This user's marks on a record */
373
+ /** Every mark, in the order their sections show above the list: Pinned, then Favourites. */
374
+ export const CRUD_MARKS = ["pin", "favorite"];
375
+ /**
376
+ * Each mark's bit in a record's set of marks, as the Nexus backend's `[Flags] EBookmarkTypes` numbers them. A set may
377
+ * hold bits this package does not know - a backend's next mark is its next bit - and a change here keeps them.
378
+ */
379
+ export const CRUD_MARK_BITS = { pin: 1, favorite: 2 };
380
+ /** Whether a set of marks holds this mark. */
381
+ export function hasMark(marks, mark) {
382
+ return (marks & CRUD_MARK_BITS[mark]) !== 0;
383
+ }
384
+ /** The set of marks with this one put on or taken off, and every other bit as it was. */
385
+ export function withMark(marks, mark, on) {
386
+ const bit = CRUD_MARK_BITS[mark];
387
+ return on ? marks | bit : marks & ~bit;
388
+ }
389
+ /** Most records a user may give a mark: `maxPins`, and for favourites `maxFavorites`, else the same as the pins. */
390
+ export function markLimit(mark, settings = getCrudSettings()) {
391
+ return mark === "pin" ? settings.maxPins : (settings.maxFavorites ?? settings.maxPins);
392
+ }
393
+ /**
394
+ * Which ways a selection can go for one mark, from each selected record's marks: on, when one of them lacks it, and
395
+ * off, when one of them has it. A mixed selection can go both ways, and each is offered on its own (the owner,
396
+ * 2026-10-07): no action that would change nothing.
397
+ */
398
+ export function markDirections(marks, mark) {
399
+ return { on: marks.some(set => !hasMark(set, mark)), off: marks.some(set => hasMark(set, mark)) };
400
+ }
401
+ /** The marked records as one listing answered them, in its order: `marksOf` reads each record's marks. */
402
+ export function markedRecords(rows, marksOf, keyOf) {
403
+ const marks = new Map();
404
+ for (const row of rows) {
405
+ const set = marksOf(row);
406
+ if (set)
407
+ marks.set(keyOf(row), set);
408
+ }
409
+ const sections = {};
410
+ for (const mark of CRUD_MARKS)
411
+ sections[mark] = rows.filter(row => hasMark(marks.get(keyOf(row)) ?? 0, mark));
412
+ return { marks, rows: sections };
413
+ }
414
+ /**
415
+ * The marked records after a change, with no request. `stored` is each changed record's new set of marks (`0` for
416
+ * none). A record in `joining` - by default every record in `stored` - that now has a mark goes on top of that mark's
417
+ * section; one that lost a mark leaves it; every copy of a record in `rows` is replaced. With `joining` empty only the
418
+ * copies change: a flag or a save never moves a record into a section.
419
+ */
420
+ export function patchMarkedRecords(current, rows, stored, keyOf, joining = new Set(stored.keys())) {
421
+ const marks = new Map(current.marks);
422
+ for (const [key, set] of stored) {
423
+ if (set)
424
+ marks.set(key, set);
425
+ else
426
+ marks.delete(key);
427
+ }
428
+ const changed = new Map(rows.map(row => [keyOf(row), row]));
429
+ const has = (row, mark) => hasMark(marks.get(keyOf(row)) ?? 0, mark);
430
+ const sections = {};
431
+ for (const mark of CRUD_MARKS) {
432
+ const kept = current.rows[mark].map(row => changed.get(keyOf(row)) ?? row).filter(row => has(row, mark));
433
+ const held = new Set(kept.map(keyOf));
434
+ sections[mark] = [...rows.filter(row => joining.has(keyOf(row)) && has(row, mark) && !held.has(keyOf(row))), ...kept];
435
+ }
436
+ return { marks, rows: sections };
437
+ }
438
+ /** The marked records with these records out of every section - they left the active lists - keeping their marks. */
439
+ export function dropMarkedRows(current, keys, keyOf) {
440
+ const gone = new Set(keys);
441
+ if (!CRUD_MARKS.some(mark => current.rows[mark].some(row => gone.has(keyOf(row)))))
442
+ return current;
443
+ const sections = {};
444
+ for (const mark of CRUD_MARKS)
445
+ sections[mark] = current.rows[mark].filter(row => !gone.has(keyOf(row)));
446
+ return { marks: current.marks, rows: sections };
447
+ }
156
448
  /** Default time a CRUD keeps a list: 5 minutes. */
157
449
  export const CRUD_CACHE_LIFETIME = 5 * 60 * 1000;
158
450
  /** Lists kept while a page is open, by key, each for a lifetime. Plain values; nothing is written to storage. */
@@ -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;