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,13 +1,16 @@
1
1
  import type { CSSProperties, ReactNode, Ref } from "react";
2
2
  import type { PopupSize } from "../Components/Layouts/Popup.tsx";
3
3
  import type { PopupHandle } from "../Components/Layouts/PopupHost.tsx";
4
+ import type { OwnerFilesParams } from "../Components/Viewers/OwnerFiles.tsx";
4
5
  import type { FormStore } from "../Helpers/FormStore.ts";
5
6
  import type { CrudBackend } from "../Helpers/CrudBackend.ts";
6
7
  import type { TableStore } from "../Helpers/TableStore.ts";
7
- import type { ApiEndpoint } from "./ApiInterfaces.ts";
8
- import type { FormProps, FormValues, InputParams } from "./FormInterfaces.ts";
9
- import type { InputDesign } from "./InputInterfaces.ts";
8
+ import type { TreeChangeRules, TreeValue } from "../Helpers/TreeStore.ts";
9
+ import type { ApiEndpoint, ApiModuleValue } from "./ApiInterfaces.ts";
10
+ import type { FormConfirmParams, FormProps, FormValues, InputParams } from "./FormInterfaces.ts";
11
+ import type { DropdownPage, DropdownPageRequest, InputDesign, OptionValue } from "./InputInterfaces.ts";
10
12
  import type { DataTableProps, TableColumn, TableMassAction, TablePaging, TableRowAction } from "./TableInterfaces.ts";
13
+ import type { Tone } from "./TypeInterfaces.ts";
11
14
  /**
12
15
  * The lists of a controller: every active record (`normal`), the flagged ones, the trash, and the deleted ones
13
16
  * (which can go back to the trash). The records the user pinned or made favourites are not lists of their own: they
@@ -120,6 +123,11 @@ export interface CrudFormOptions<TRow extends FormValues = FormValues> {
120
123
  action: "create" | "edit";
121
124
  row: TRow | null;
122
125
  }) => unknown;
126
+ /**
127
+ * Add and Edit ask before they save, showing what will be saved in full (`FormConfirmParams`): `body` is what
128
+ * `transform` made. Default: they save at once.
129
+ */
130
+ confirm?: FormConfirmParams<TRow>;
123
131
  /** The form's own rule over its values. */
124
132
  validate?: FormProps<TRow>["validate"];
125
133
  /** Derived values: return values to set after a change. */
@@ -149,7 +157,7 @@ export interface CrudDetailsOptions<TRow extends FormValues = FormValues> {
149
157
  columns?: readonly TableColumn<TRow>[];
150
158
  /** Draws the details instead. */
151
159
  render?: (row: TRow) => ReactNode;
152
- /** Default `md`. */
160
+ /** Default `md`; `full` with `tabs`. */
153
161
  size?: PopupSize;
154
162
  /**
155
163
  * A panel beside the details, in the same popup: what the record generates, a chart, a preview. It is laid out as
@@ -157,6 +165,13 @@ export interface CrudDetailsOptions<TRow extends FormValues = FormValues> {
157
165
  * beside it are a short list of fields. Pair it with a wide `size` (`xl` or `full`).
158
166
  */
159
167
  aside?: ReactNode | ((context: CrudDetailsAsideContext<TRow>) => ReactNode);
168
+ /**
169
+ * Where `aside` goes: `"beside"` the fields where the popup is 1000px wide or more (stacked under them below that),
170
+ * or `"below"` them at every width - the fields across the whole width, then the panel across the whole width under
171
+ * them, as tall as it needs, scrolled with them. Default `"below"` in a record's workspace (`tabs` or `profile`), whose
172
+ * Details tab reads as the record first, then what belongs to it (its notes); `"beside"` in a plain details popup.
173
+ */
174
+ asidePlace?: "beside" | "below";
160
175
  /**
161
176
  * What the popup is called, as a noun: the row's menu offers "View {label}" and the popup's title is
162
177
  * "{Subject} {label}". Default `details`. A popup whose panel is the point says so: `schema`, `preview`.
@@ -164,6 +179,44 @@ export interface CrudDetailsOptions<TRow extends FormValues = FormValues> {
164
179
  label?: string;
165
180
  /** Buttons of the page's own in the footer, before Edit and Close: "View history". */
166
181
  actions?: readonly CrudDetailsAction<TRow>[];
182
+ /**
183
+ * A record's workspace: the popup opens wide (`full`, unless `size` says otherwise) with a column on the left - the
184
+ * record's `profile` (picture, name, a line, tags) over a list of tabs - and the chosen tab on the right, scrolling
185
+ * inside itself. Each tab is a component of the page's own (a form that records something, a list, documents),
186
+ * handed the record and a `refresh()` that reads the record again and writes it into every list the CRUD keeps, so a
187
+ * stay recorded in a tab changes the row's visits and dates with no list read again. Only the chosen tab is drawn. A
188
+ * `SubmitForm` or `ApiForm` in a tab with changes asks "Discard your changes?" before another tab is chosen or the
189
+ * popup closes (Close, Edit, its X, Esc, its bar on the dock).
190
+ *
191
+ * The built-in tab `details` (the fields, with `aside` beside them) comes first. To move it, list `{ id: "details" }`
192
+ * where it goes (a `label` or `icon` there renames it); to leave it out, give that entry `hidden: () => true`; a
193
+ * `render` there replaces it. Below 720px of popup width (a phone) the profile is one row and the tabs one strip.
194
+ *
195
+ * @example
196
+ * details={{
197
+ * profile: row => ({ picture: { ...GUEST_PICTURE, ownerId: String(row.id) }, title: row.fullName, subtitle: row.email }),
198
+ * tabs: [
199
+ * { id: "stays", label: "Stays", icon: <InlineIcon icon={iconCalendar} />, endpoint: STAYS.create, render: ({ row, refresh }) => <StayForm guest={row} onSaved={refresh} /> },
200
+ * { id: "bills", label: "Bills", icon: <InlineIcon icon={iconReceipt} />, render: ({ row }) => <Bills guestId={row.id} /> },
201
+ * ],
202
+ * remember: { key: "guest-tab", viewer },
203
+ * }}
204
+ */
205
+ tabs?: readonly CrudDetailsTab<TRow>[];
206
+ /**
207
+ * The record's picture, name, a line under it, its tags and a few of its fields (`fields`, left out of the Details
208
+ * tab), at the top of the left column (with or without `tabs`), from the record as it is now: the row, then the
209
+ * `details` endpoint's record, then each `refresh()`. Or anything to draw there instead.
210
+ */
211
+ profile?: (row: TRow) => CrudDetailsProfile<TRow> | ReactNode;
212
+ /**
213
+ * Keeps the tab last chosen in this browser, per viewer (`nx-crud-tab:{key}:{viewer}`), and opens on it next time
214
+ * while it is still offered to the person; otherwise the first tab. Without a viewer nothing is kept.
215
+ */
216
+ remember?: {
217
+ key: string;
218
+ viewer?: string | null;
219
+ };
167
220
  }
168
221
  /**
169
222
  * A button in the details popup's footer. One that names the endpoint it calls shows only to a user who may call
@@ -181,15 +234,355 @@ export interface CrudDetailsAction<TRow extends FormValues = FormValues> {
181
234
  /** On a click, with the record and the popup (close it, or leave it open beside what this opens). */
182
235
  onSelect: (row: TRow, popup: PopupHandle) => void;
183
236
  }
237
+ /** A record's main picture in a details popup's profile, by the params the picture column and input take. */
238
+ export interface CrudDetailsPicture extends Omit<OwnerFilesParams<ApiModuleValue>, "onChange"> {
239
+ /** Whose picture it is: without one, their initials stand in. Default: the profile's `title`, when it is text. */
240
+ name?: string | null;
241
+ }
242
+ /** The top of a details popup's left column: who the record is. */
243
+ export interface CrudDetailsProfile<TRow extends FormValues = FormValues> {
244
+ /** The owner's main picture by its Drive params (a click opens the pictures), or anything to draw in its place. */
245
+ picture?: CrudDetailsPicture | ReactNode;
246
+ /** The record's name. */
247
+ title: ReactNode;
248
+ /** A line under the name: a number, a phone. */
249
+ subtitle?: ReactNode;
250
+ /** Short tags under it, a status with its tone: `{ label: "Due", tone: "warning" }`. */
251
+ tags?: readonly {
252
+ label: ReactNode;
253
+ tone?: Tone;
254
+ }[];
255
+ /**
256
+ * A few of the record's fields under the tags, each as label and value, the value as the table draws it: its format
257
+ * in the person's settings (a date in their date format), an option's tone, the column's `tag` after it. A column given
258
+ * whole, or a key of the details' `columns` (the table's, unless the details name their own). A field with no value
259
+ * for the record is left out. The built-in Details tab leaves out every key named here, so no field shows twice.
260
+ *
261
+ * @example
262
+ * fields: [EXPIRY_COLUMN, "inactiveDate"]
263
+ */
264
+ fields?: readonly (TableColumn<TRow> | string)[];
265
+ }
266
+ /** What a details tab is handed. */
267
+ export interface CrudDetailsTabContext<TRow extends FormValues = FormValues> {
268
+ /** The record as the list has it now: after a `refresh()`, the record as it was read again. */
269
+ row: TRow;
270
+ /**
271
+ * The record, in the `FormStore` the details' `aside` is handed: the row at once, then the `details` endpoint's record
272
+ * when it lands, then each `refresh()`'s.
273
+ */
274
+ form: FormStore<TRow>;
275
+ /** The popup: close it, or open another over it. */
276
+ popup: PopupHandle;
277
+ /** The tab shown: this one. */
278
+ tab: string;
279
+ /** Shows another tab ("Take a payment" in the details opening the cash counter). Ignored for a tab not offered. */
280
+ select: (tab: string) => void;
281
+ /**
282
+ * Reads the record again from the `details` endpoint (without one, keeps the row) and writes it into the table and
283
+ * every list and tree the CRUD keeps, as a save's answer is: never a list read again. Answers the record, or `null`
284
+ * when it could not be read. Call it after a tab changed the record on the server: it lands even when the popup was
285
+ * closed before the record came back.
286
+ */
287
+ refresh: () => Promise<TRow | null>;
288
+ }
289
+ /** A tab of a details popup (`CrudDetailsOptions.tabs`). */
290
+ export interface CrudDetailsTab<TRow extends FormValues = FormValues> {
291
+ /** Unique in the popup; what `select()` and `remember` keep. `details` is the built-in tab. */
292
+ id: string;
293
+ /** What the tab is called. Needed for every tab but `details` (whose default is "Details"). */
294
+ label?: string;
295
+ icon?: ReactNode;
296
+ /** What it calls: the tab shows only to a person who may call it, by the rule of `CrudDetailsAction`. */
297
+ endpoint?: ApiEndpoint;
298
+ /** Hidden for this record, by a rule of the page's own. */
299
+ hidden?: (row: TRow) => boolean;
300
+ /** A count or short tag at the end of the tab: `3`. */
301
+ badge?: ReactNode | ((row: TRow) => ReactNode);
302
+ /** The tab's content, drawn only while it is the chosen tab. Without one on `details`, the built-in fields. */
303
+ render?: (context: CrudDetailsTabContext<TRow>) => ReactNode;
304
+ }
184
305
  /** What changed, for `onChange`. */
185
306
  export interface CrudChange<TRow extends FormValues = FormValues> {
186
307
  action: CrudActionName;
187
308
  /** The records as they are now; for actions that took them out of the list, as they were. */
188
309
  rows: TRow[];
189
310
  mode: CrudMode;
311
+ /** The same records before the change, in the order of `rows`, where the CRUD held them; `undefined` for a new one. */
312
+ before?: (TRow | undefined)[];
313
+ }
314
+ /**
315
+ * Where a controller's records sit in a tree: for a `CrudTree` of them, and for a `Crud` on another page whose changes
316
+ * that tree must follow (`trees`). One definition for both, so a change made on either page is written where the tree
317
+ * keeps its pages, and nothing is read again for it.
318
+ */
319
+ export interface CrudTreePlacement<TRow extends FormValues = FormValues, TItem extends object = object> {
320
+ /** The tree's `cacheKey`: where its pages are kept, and what the trees on screen that show them listen on. */
321
+ cacheKey: string;
322
+ /** The tree's value and has-children keys, and the order of siblings (`compare`) as the server sorts them. */
323
+ rules: TreeChangeRules<TItem>;
324
+ /** The tree item that shows a record. */
325
+ itemOf: (row: TRow) => TItem;
326
+ /** The value of the item a record sits under (`null`: the top level). */
327
+ parentOf: (row: TRow) => TreeValue | null;
328
+ /**
329
+ * The record an item shows, with the item's count (`rules.hasChildrenKey`) written back into it - a save counts a
330
+ * record's children, and its parents' on a data table, through it - or `null` for an item that is not one of these
331
+ * records (a country above its regions).
332
+ *
333
+ * @example item => (item.region ? { ...item.region, childCount: item.childCount } : null)
334
+ */
335
+ recordOf: (item: TItem) => TRow | null;
336
+ /** A new record's values when it is added under an item: the parent's key, its country. */
337
+ childValues?: (parent: TItem) => Partial<TRow>;
338
+ /**
339
+ * The fields that say where a record sits (its parent, its country). Given, a `CrudTree` fixes them in every form it
340
+ * opens - read-only, saying why: an Add inside an item takes them from `childValues` (a field it leaves out is fixed
341
+ * empty), an Edit keeps the record's own - and every Add goes inside an item: the toolbar's Add New inside the selected
342
+ * row. A record then moves only where these fields are offered, such as a data table's form. Without them a tree's forms
343
+ * leave every field free.
344
+ */
345
+ placeKeys?: readonly string[];
346
+ }
347
+ /**
348
+ * A list narrowed to the records of one record picked in another list - a person's places beside the people, a hotel's
349
+ * rooms beside the hotels - which the page feeds from that list's `onPick`. Every behaviour is the `Crud`'s: the page
350
+ * passes the picked record and names the fields.
351
+ *
352
+ * @example
353
+ * const [person, setPerson] = useState<CompanyUser | null>(null);
354
+ * <AdminSplit>
355
+ * <Crud<CompanyUser> api={COMPANY_USERS} ... onPick={setPerson} />
356
+ * <Crud<BranchUser> api={BRANCH_USERS} ... narrow={{ picked: person, key: "userId", labelKey: "fullname" }} />
357
+ * </AdminSplit>
358
+ */
359
+ export interface CrudNarrow<TPicked extends FormValues = FormValues> {
360
+ /** The record picked in the other list, or `null` while none is: the list then shows nothing, and Add waits. */
361
+ picked: TPicked | null;
362
+ /**
363
+ * The field of these records that names the picked one (`"userId"`): the list shows the records whose field holds its
364
+ * value, and a new record is given that value, fixed. A server-paged list sends it as a filter of that name.
365
+ */
366
+ key: string;
367
+ /** The picked record's field holding that value. Default `key`. */
368
+ valueKey?: string;
369
+ /** The picked record's field that names it in words: "Places of: Ram Thapa". Default the value itself. */
370
+ labelKey?: string;
371
+ /**
372
+ * Why nothing can be added for the picked record, or nothing when something can: Add stays in the bar, switched off,
373
+ * and says this on a press. Default: the backend's rule (`CrudBackend.isPickable`), so a record a picker would not
374
+ * offer takes no new records - in the Nexus bands one pending (30-39), in the trash or deleted.
375
+ *
376
+ * @example addLock: person => (getCrudBackend().isPickable(person) ? null : `${person.fullname} has not joined yet.`)
377
+ */
378
+ addLock?: (picked: TPicked) => string | null | undefined;
379
+ /**
380
+ * What the list says while no record is picked. Default "Select a row in the other list to see its {plural}.", or with
381
+ * a `narrowPicker` "Pick a {label} in the {Label} box to see its {plural}."
382
+ */
383
+ emptyMessage?: string;
384
+ /**
385
+ * Why Add is switched off while no record is picked, on its tooltip and on a press. Default "Select a row in the other
386
+ * list to add a new {subject} for it.", or with a `narrowPicker` "Pick a {label} in the {Label} box to add a new {subject} for it."
387
+ */
388
+ addMessage?: string;
389
+ /**
390
+ * What the Add form's fixed field says, given the picked record's name. Default "Added for {name}: pick another row in
391
+ * the other list to add elsewhere.", or with a `narrowPicker` "Added for {name}: pick another {label} in the {Label} box to add elsewhere."
392
+ *
393
+ * @example fixedMessage: name => `Added to ${name}. Close and pick another group to add elsewhere.`
394
+ */
395
+ fixedMessage?: (pickedName: string) => string;
396
+ }
397
+ /** The box a `narrowPicker` draws: a picker's params, as `crudPicker` gives them. */
398
+ export interface CrudNarrowPickerSource {
399
+ loadOptions: (request: DropdownPageRequest) => Promise<DropdownPage<Readonly<Record<string, unknown>>>>;
400
+ resolveOptions?: (values: OptionValue[]) => Promise<readonly Readonly<Record<string, unknown>>[]>;
401
+ peekOption?: (key: string, aged?: boolean) => Readonly<Record<string, unknown>> | undefined;
402
+ valueKey?: string;
403
+ labelKey?: string;
404
+ descriptionKey?: string;
405
+ isOptionInactive?: (item: Readonly<Record<string, unknown>>) => boolean;
406
+ }
407
+ /**
408
+ * A list narrowed by a record picked in a box in the CRUD's own toolbar (`Crud` `narrowPicker`): the picker's params, the
409
+ * box's words, and where this browser remembers the pick. Every field of `CrudNarrow` but `picked`, which the CRUD keeps.
410
+ *
411
+ * As its page opens the box settles - a spinner in it, the list waiting - on the remembered pick while it is offered, or
412
+ * else on the one record the box offers (picked by itself, and remembered as a person's pick is). With neither it asks:
413
+ * required, drawn with a field's invalid edge, the list's `emptyMessage` read with it, the cursor in it unless the person
414
+ * is already at work on the page, a popup is open, it is below the fold or the screen is touch-only; the empty list has a
415
+ * button to it, and Add pressed meanwhile sends the cursor there (nudged, with Add's `addMessage` read) instead of a note.
416
+ *
417
+ * @example
418
+ * narrowPicker={{
419
+ * picker: crudPicker(GROUPS, { labelKey: "groupName" }),
420
+ * label: "Group",
421
+ * key: "subscriptionGroupId",
422
+ * labelKey: "groupName",
423
+ * emptyMessage: "Pick a group to see its subscribers.",
424
+ * remember: { key: "subscribers-group", viewer },
425
+ * onPick: group => setGroupId(group?.id ?? null),
426
+ * }}
427
+ */
428
+ export interface CrudNarrowPicker<TPicked extends FormValues = FormValues> extends Omit<CrudNarrow<TPicked>, "picked"> {
429
+ /** The box's options: `crudPicker(GROUPS, { labelKey: "groupName" })`. Its value field is the narrowing's `valueKey`. */
430
+ picker: CrudNarrowPickerSource;
431
+ /** What the box picks, for a screen reader and its placeholder: "Group" says "Pick a group". */
432
+ label: string;
433
+ /** The box's text while nothing is picked. Default "Pick a {label}". */
434
+ placeholder?: string;
435
+ /**
436
+ * Remembers the pick in this browser (local storage), under `key` and the viewer: pass who is looking (a short hash of
437
+ * the person and the company the session works in), so a PC shared by two people, or one person in two companies,
438
+ * never opens on another's pick; without a viewer nothing is remembered. Opened again only while the box still offers
439
+ * it (a record trashed since is asked for again), looked up by its key alone (`resolveOptions` when the picker has it).
440
+ * Only the record's key is stored: never its name.
441
+ */
442
+ remember?: {
443
+ key: string;
444
+ viewer?: string | null;
445
+ };
446
+ /** After a pick, a remembered one opened, or the pick cleared: the page's inputs or filters can follow it. */
447
+ onPick?: (picked: TPicked | null) => void;
448
+ }
449
+ /**
450
+ * Another list a CRUD can show in place of its own (`Crud` `scopes`): the same kind of records, widened - every branch's
451
+ * places beside the branch signed in at - through endpoints of their own, and whatever else differs on it. The CRUD's own
452
+ * props are its own list; a field left out here is the CRUD's.
453
+ *
454
+ * @example
455
+ * { id: "all", label: "All branches", description: "Every branch's people", api: ALL_BRANCH_USERS, columns: EVERY_BRANCH_COLUMNS, inputs: everyBranchInputs }
456
+ */
457
+ export interface CrudScope<TRow extends FormValues = FormValues> {
458
+ /** Unique among the scopes: what `remember` keeps, `onChange` names and `CrudHandle.scope` answers. */
459
+ id: string;
460
+ /** What the toolbar's switch says ("All branches"), or this scope's tab beside two or more. */
461
+ label: string;
462
+ /** The switch's (or tab's) tooltip, read after the label by a screen reader: "Every branch's people". */
463
+ description?: string;
464
+ /** Its endpoints: `MODULES.controller(NexusModule.Enterprise, "AllBranchUser", "place")`. */
465
+ api: CrudEndpoints;
466
+ /**
467
+ * Whether it is offered, by a rule of the page's own. It is offered only to a person who may call one of its lists
468
+ * anyway (the rule every list follows), again whenever their rights change; a scope kept for a person who may no longer
469
+ * see it opens the CRUD's own list.
470
+ */
471
+ when?: () => boolean;
472
+ /** The table's columns on this list, when they differ: the Branch column shown. */
473
+ columns?: readonly TableColumn<TRow>[];
474
+ /** The Add and Edit forms' inputs on this list, when they differ: a branch to pick. */
475
+ inputs?: CrudInputs<TRow>;
476
+ /** The forms' options on this list, over the CRUD's own `form`, field by field: other `newValues`. */
477
+ form?: CrudFormOptions<TRow>;
478
+ /**
479
+ * The table's settings on this list, over the CRUD's own `table`, field by field. Each list keeps its own columns,
480
+ * widths, sort and page size: its `stateKey` is the CRUD's (or `crud:{name}`) and `:{id}` unless given here.
481
+ */
482
+ table?: CrudTableOptions<TRow>;
483
+ /** The table's name for screen readers on this list. */
484
+ "aria-label"?: string;
485
+ }
486
+ /**
487
+ * The lists a CRUD can be widened to, and how the person picks one (`Crud` `scopes`): with one, a switch in the toolbar,
488
+ * after Add ("All branches": on shows that list); with two or more, tabs, the CRUD's own list first. Changing it reads the
489
+ * new list on its endpoints; the CRUD stays (its toolbar keeps the focus, popups stay open), and each list keeps what it
490
+ * read while the page is open, so going back shows it at once.
491
+ *
492
+ * @example
493
+ * scopes={{ options: [{ id: "all", label: "All branches", api: ALL_BRANCH_USERS }], remember: { key: "branch-users-scope", viewer } }}
494
+ */
495
+ export interface CrudScopes<TRow extends FormValues = FormValues> {
496
+ /** The other lists, in the order their tabs show. */
497
+ options: readonly CrudScope<TRow>[];
498
+ /** The CRUD's own list's tab beside two scopes or more: "This branch". Default "Own". */
499
+ label?: string;
500
+ /** That tab's tooltip. */
501
+ description?: string;
502
+ /**
503
+ * Keeps the choice in this browser (`nx-crud-scope:{key}:{viewer}`), so the next visit opens the same list for the same
504
+ * viewer while it is offered to them: pass who is looking (a short hash of the person and the company), so a PC shared
505
+ * by two people never opens on the other's choice. Without a viewer nothing is kept.
506
+ */
507
+ remember?: {
508
+ key: string;
509
+ viewer?: string | null;
510
+ };
511
+ /** After the list shown changed - a pick, a kept choice opened, a scope no longer offered: its id, `null` for the CRUD's own. */
512
+ onChange?: (scope: string | null) => void;
513
+ }
514
+ /**
515
+ * A row action that opens a form of its own for the record, in a popup (`Crud` `rowForms`): a record's permission set
516
+ * beside its Edit. The page gives its inputs, where its values are read and where a save goes, and which of the answer's
517
+ * fields change the record; the CRUD offers it, opens it (one popup per record, brought back if open), and writes those
518
+ * fields into the record wherever it holds it - the table, every list kept, the pinned sections - with no list read
519
+ * again.
520
+ *
521
+ * @example
522
+ * {
523
+ * id: "permissions", label: "Permissions", icon: <InlineIcon icon={iconLock} />, inline: true, modes: ["normal"], size: "lg",
524
+ * load: BRANCH_USER_PERMISSIONS.details, submit: BRANCH_USER_PERMISSIONS.update,
525
+ * inputs: (row, loaded) => permissionSetInputs("user", tree, loaded),
526
+ * transform: (values, row) => ({ branchUserId: row.id, permissionKeys: values.permissionKeys ?? [], description: values.description }),
527
+ * writes: ["assignedPermissionCount"],
528
+ * }
529
+ */
530
+ export interface CrudRowForm<TRow extends FormValues = FormValues, TValues extends FormValues = FormValues> {
531
+ /** Unique among the record's actions. */
532
+ id: string;
533
+ /** The action's name in the row's menu (and on its icon button), and the popup's title unless `title` says otherwise. */
534
+ label: string;
535
+ /** A line under the label in the row's menu. */
536
+ description?: string;
537
+ icon?: ReactNode;
538
+ /** An icon button on the row, beside Edit, rather than a line of its menu. Needs an `icon`. */
539
+ inline?: boolean;
540
+ /** The lists that offer it. Default the lists that offer Edit (the active and the flagged). */
541
+ modes?: readonly CrudMode[];
542
+ /**
543
+ * Offered on this record, by a rule of the page's own. It is offered only to a person who may call `submit` and, when
544
+ * it is an endpoint, `load` - again whenever their rights change.
545
+ */
546
+ when?: (row: TRow, mode: CrudMode) => boolean;
547
+ /** Why it cannot run on this record now: listed switched off with this sentence, as `rowActionLock` does. */
548
+ lock?: (row: TRow) => string | null | undefined;
549
+ /** What the endpoints are called with in `{id}` (the record the form is about, by its token). Default the record's key. */
550
+ target?: (row: TRow) => string | number;
551
+ /** The form's inputs, or a function of the record and of what `load` read (`null` until it has, and without one); it may answer a promise. */
552
+ inputs: readonly InputParams[] | ((row: TRow, loaded: TValues | null) => readonly InputParams[] | Promise<readonly InputParams[]>);
553
+ /**
554
+ * Where the form's values are read: an endpoint called with the record's key (`{id}`, as the details are), or a
555
+ * function. Until they are read the form waits, and a read that fails - an answer that is not ok, or no record - shows
556
+ * "Could not load the values" with Retry and no Save, so an empty form is never saved over the record. A function must
557
+ * reject (throw) for a failure. Without it, the form opens on the record as the list has it.
558
+ */
559
+ load?: ApiEndpoint | ((row: TRow, signal: AbortSignal) => Promise<TValues | null | undefined>);
560
+ /** Where a save goes. */
561
+ submit: ApiEndpoint;
562
+ /** What a save sends, from the values, the record and what was read. Default the values. */
563
+ transform?: (values: TValues, row: TRow, loaded: TValues | null) => unknown;
564
+ /**
565
+ * What the answer changes on the record: the answer's fields by name (`["assignedPermissionCount"]`, each one it
566
+ * carries), or a function of the answer. Only those fields and the record's key are written, over the record as the
567
+ * CRUD holds it when the answer lands, so a change made meanwhile (the same record edited while this popup was
568
+ * minimized) stays. Without it nothing is written.
569
+ */
570
+ writes?: readonly string[] | ((result: unknown, row: TRow, values: TValues) => Partial<TRow> | null | undefined);
571
+ /** The popup's title. Default the `label`. */
572
+ title?: string;
573
+ /** The popup's width. Default `md`. */
574
+ size?: PopupSize;
575
+ /** The save button's label. Default "Save". */
576
+ submitLabel?: string;
577
+ /** After a save: the answer, the record as it is now, and the values sent (before `transform`). */
578
+ onSaved?: (result: unknown, row: TRow, values: TValues) => void;
190
579
  }
580
+ /** The actions that change a record, which `rowActionLock` can switch off one by one. */
581
+ export type CrudLockableAction = "edit" | "flag" | "trash" | "recover" | "delete" | "trashFromDelete" | "description";
582
+ /** A record's own locks, by action: the sentence that says why (`rowActionLock`). Nothing, or null, for an action that may run. */
583
+ export type CrudActionLocks = Partial<Record<CrudLockableAction, string | null | undefined>>;
191
584
  /** Table settings a CRUD passes on to its `DataTable`. */
192
- export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "searchDelay" | "filterInputs" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick">;
585
+ export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pager" | "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "currency" | "searchDelay" | "filterInputs" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick">;
193
586
  /** Props of `Crud`. */
194
587
  export interface CrudProps<TRow extends FormValues = FormValues> {
195
588
  /** The endpoints: `MODULES.controller(NexusModule.Reservation, "Guest", "guest")`, or an object of your own. */
@@ -218,8 +611,35 @@ export interface CrudProps<TRow extends FormValues = FormValues> {
218
611
  actions?: readonly CrudActionName[];
219
612
  /** More actions on a row, after the built-in ones. */
220
613
  rowActions?: (row: TRow, mode: CrudMode) => readonly TableRowAction<TRow>[];
614
+ /**
615
+ * Row actions that each open a form of their own for the record, in a popup, after the built-in actions and before
616
+ * `rowActions`: what a record's permission set, its duties or its limits are edited through. Each is offered to a
617
+ * person who may call its endpoints, on the lists that offer Edit unless it says otherwise, and its save writes the
618
+ * fields its answer names into the record wherever the CRUD holds it.
619
+ */
620
+ rowForms?: readonly CrudRowForm<TRow, any>[];
221
621
  /** More actions on selected rows, or `false` for no selection at all. */
222
622
  massActions?: false | ((rows: TRow[], mode: CrudMode) => readonly TableMassAction<TRow>[]);
623
+ /**
624
+ * Why a record may not be changed here, or nothing when it may: one the backend keeps read-only, for everybody (a
625
+ * seeded system record) or for this user. Its Edit, Flag, Move to trash, Recover, Delete permanently and Move back to
626
+ * trash are then listed switched off with this sentence under them, so its menu says why rather than looking short;
627
+ * its details offer no Edit, and the handle's `edit` opens its details instead; a selection that holds it has the
628
+ * mass actions that change records switched off. View, History, Pin and Favourite stay: they change nothing on it.
629
+ * The page's own `rowActions` are the page's to switch off. The backend still decides; this says so first.
630
+ *
631
+ * @example rowLock={row => (row.isLocked ? row.lockReason : null)}
632
+ */
633
+ rowLock?: (row: TRow) => string | null | undefined;
634
+ /**
635
+ * Why single actions may not run on a record, each with its sentence, when the rest may: a branch that is the head office
636
+ * keeps Edit but has no Move to trash. Each named action is listed switched off with its sentence under it, a selection
637
+ * that holds the record has that mass action switched off, and a locked `edit` opens the details instead, as with
638
+ * `rowLock` (which, when it locks the record, locks every action at once). The backend still decides; this says so first.
639
+ *
640
+ * @example rowActionLock={row => (row.isHeadOffice ? { trash: "The head office stays open: make another branch the head office first." } : null)}
641
+ */
642
+ rowActionLock?: (row: TRow) => CrudActionLocks | null | undefined;
223
643
  /** The Add and Edit forms. */
224
644
  form?: CrudFormOptions<TRow>;
225
645
  /** The details popup, or `false` for none. */
@@ -239,6 +659,40 @@ export interface CrudProps<TRow extends FormValues = FormValues> {
239
659
  table?: CrudTableOptions<TRow>;
240
660
  /** After every change the CRUD made: a record added, saved, trashed, deleted. */
241
661
  onChange?: (change: CrudChange<TRow>) => void;
662
+ /**
663
+ * Rows the user picks one of, for a list beside this one that shows the picked record's own (its `narrow`): a click
664
+ * on a row picks it - a double-click still opens its details - and the arrow keys move the pick (`DataTable`
665
+ * `onPick`). Called with the picked record, with its new copy after an edit here, and with `null` once it leaves the
666
+ * list on screen (trashed, deleted, another list shown).
667
+ */
668
+ onPick?: (row: TRow | null) => void;
669
+ /**
670
+ * The list narrowed to the records of one picked in another list, fed from that list's `onPick`: only those records
671
+ * show, under a line naming the picked one ("Places of: Ram Thapa"); Add fixes its value in the new record and waits
672
+ * while none is picked or it may take none (`addLock`); a record saved here lands in the list of the record it names.
673
+ */
674
+ narrow?: CrudNarrow<any>;
675
+ /**
676
+ * The list narrowed by a record picked in a box of its own, in the toolbar: a group first, then that group's
677
+ * subscribers. The CRUD draws the box, keeps the pick (as `narrow` does with one passed in), and, with `remember`,
678
+ * opens it again on the next visit in this browser for the same viewer, while the box still offers it. Use it in place
679
+ * of `narrow`, not beside it.
680
+ */
681
+ narrowPicker?: CrudNarrowPicker<any>;
682
+ /**
683
+ * Other lists the CRUD can show in place of its own - every branch's places beside the branch signed in at - each on
684
+ * endpoints of its own, with its own columns, inputs, form values and table settings where they differ: a switch in
685
+ * the toolbar picks one (tabs for two or more), offered only to a person who may call its lists and passes its `when`,
686
+ * and, with `remember`, kept in this browser per viewer.
687
+ */
688
+ scopes?: CrudScopes<TRow>;
689
+ /**
690
+ * Trees on other pages that show these records (the placement their `CrudTree` is given): every change made here is
691
+ * written into the pages they keep - a record added goes under its parent, an edit is written in place or moved, one
692
+ * sent to the trash leaves - so the tree shows it next time without reading again. A spreadsheet import, which writes
693
+ * records nobody here has seen, drops their pages instead. Pins and favourites change no tree.
694
+ */
695
+ trees?: readonly CrudTreePlacement<TRow, any>[];
242
696
  /**
243
697
  * The conventions of this controller's backend, when they differ from the app's (`defineCrudBackend`): an app whose
244
698
  * pages sit on two backends gives the other one here. Default the app's, else the Nexus backend's.
@@ -259,6 +713,10 @@ export interface CrudHandle<TRow extends FormValues = FormValues> {
259
713
  /** The list shown. */
260
714
  mode: CrudMode;
261
715
  setMode: (mode: CrudMode) => void;
716
+ /** The scope shown (`scopes`): its id, `null` for the CRUD's own list. */
717
+ scope: string | null;
718
+ /** Shows a scope, as the toolbar's switch does (kept with `remember`); one not offered now shows the CRUD's own list. */
719
+ setScope: (scope: string | null) => void;
262
720
  /** Loads the list again, past the cache. A last resort: `apply` writes a change in without a request. */
263
721
  reload: () => Promise<void>;
264
722
  /**
@@ -286,4 +744,6 @@ export interface CrudHandle<TRow extends FormValues = FormValues> {
286
744
  edit: (row: TRow) => void;
287
745
  /** Opens the details popup of a record. */
288
746
  view: (row: TRow) => void;
747
+ /** Opens one of `rowForms` for a record, by its id, wherever it is offered. */
748
+ openRowForm: (id: string, row: TRow) => void;
289
749
  }