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
@@ -72,9 +72,10 @@ function toNumber(value) {
72
72
  }
73
73
  /**
74
74
  * A value as the cell writes it, in the column's format: "1,250.00", "Rs. 4,500.00", "12.5%", the date and time in the
75
- * user's settings, "Yes" or "No", an option's label. Empty values give "".
75
+ * user's settings, "Yes" or "No", an option's label. Empty values give "". `row` is the record, for a currency the
76
+ * column reads from it (`currency: row => row.currencyText`).
76
77
  */
77
- export function formatCellText(column, value, options = {}) {
78
+ export function formatCellText(column, value, options = {}, row) {
78
79
  const option = columnOption(column, value);
79
80
  if (option)
80
81
  return option.label;
@@ -90,7 +91,8 @@ export function formatCellText(column, value, options = {}) {
90
91
  return String(value);
91
92
  const decimals = column.decimals ?? (column.format === "money" ? 2 : undefined);
92
93
  const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true, locale: options.locale });
93
- return `${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
94
+ const currency = column.format === "money" ? columnCurrency(column, options, row) : "";
95
+ return `${currency ? currency + " " : ""}${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
94
96
  }
95
97
  case "date":
96
98
  return formatDate(value, options.preferences) || String(value);
@@ -104,6 +106,11 @@ export function formatCellText(column, value, options = {}) {
104
106
  return `${prefix}${typeof value === "object" ? JSON.stringify(value) : String(value)}${suffix}`;
105
107
  }
106
108
  }
109
+ /** A money column's currency: its own (a text, or read from the row), else the table's; "" for none. */
110
+ export function columnCurrency(column, options = {}, row) {
111
+ const own = typeof column.currency === "function" ? (row ? column.currency(row) : undefined) : column.currency;
112
+ return (own ?? options.currency ?? "").trim();
113
+ }
107
114
  /** Text the search looks in: the column's `text`, an option's label, or the value as text (dates as ISO text). */
108
115
  export function searchTextOf(column, row) {
109
116
  if (column.text)
@@ -256,18 +263,52 @@ export function moveColumnKey(keys, key, beforeKey) {
256
263
  rest.splice(at, 0, key);
257
264
  return rest;
258
265
  }
266
+ /**
267
+ * The most of a table's width on screen that the columns staying in place may take: the numbers, the checkboxes and the
268
+ * actions, which always stay, and the pinned columns. Past it the pinned ones give way (`releasedPins`), so a phone
269
+ * keeps at least 40% of the table for the columns that scroll.
270
+ */
271
+ export const PINNED_SHARE = 0.6;
272
+ /**
273
+ * Which pinned columns scroll for now, the table being narrow: the pinned ones stay from the outer edge in - the left
274
+ * side first - while they and the columns that always stay (`frame`: the numbers and checkboxes, the actions; their
275
+ * width in pixels) take at most `share` of `view`, the width on screen; every one past that scrolls with the rest,
276
+ * and so do the ones inside it on the same side. `left` and `right`: each side's pinned columns with their widths,
277
+ * from the outer edge in. A table not laid out (`view` 0, in a closed tab) releases none.
278
+ */
279
+ export function releasedPins(view, frame, left, right, share = PINNED_SHARE) {
280
+ const released = new Set();
281
+ if (!(view > 0))
282
+ return released;
283
+ const room = view * share;
284
+ let used = frame;
285
+ for (const side of [left, right]) {
286
+ let open = true;
287
+ for (const [key, width] of side) {
288
+ if (open && used + width <= room + 0.5)
289
+ used += width;
290
+ else {
291
+ open = false;
292
+ released.add(key);
293
+ }
294
+ }
295
+ }
296
+ return released;
297
+ }
259
298
  /**
260
299
  * The columns as the table draws them: in the user's order, with the pinned ones moved to their side (keeping their
261
300
  * order among themselves), each with its width, its distance from that edge, and what the user may do with it.
262
301
  */
263
302
  export function viewColumns(columns, options = {}) {
264
- const { order, pinned = {}, widths = {}, offsets = {}, reorderable = true, resizable = true } = options;
303
+ const { order, pinned = {}, widths = {}, offsets = {}, released, reorderable = true, resizable = true } = options;
265
304
  const groups = { left: [], middle: [], right: [] };
266
305
  for (const column of orderColumns(columns, order)) {
267
- const pin = pinned[column.key] ?? column.pinned ?? null;
268
- groups[pin === "left" ? "left" : pin === "right" ? "right" : "middle"].push({
306
+ const side = pinned[column.key] ?? column.pinned ?? null;
307
+ groups[side === "left" ? "left" : side === "right" ? "right" : "middle"].push({
269
308
  column,
270
- pin,
309
+ // A pinned column the table is too narrow for keeps its side and scrolls with the rest.
310
+ pin: side && !released?.has(column.key) ? side : null,
311
+ side,
271
312
  offset: offsets[column.key] ?? 0,
272
313
  edge: false,
273
314
  resizable: resizable && column.resizable !== false,
@@ -275,11 +316,11 @@ export function viewColumns(columns, options = {}) {
275
316
  width: widths[column.key],
276
317
  });
277
318
  }
278
- // The shadow that says columns scroll under a pinned side belongs to its last column.
279
- const last = groups.left[groups.left.length - 1];
319
+ // The shadow that says columns scroll under a pinned side belongs to its last column that stays.
320
+ const last = groups.left.filter(view => view.pin).at(-1);
280
321
  if (last)
281
322
  last.edge = true;
282
- const first = groups.right[0];
323
+ const first = groups.right.find(view => view.pin);
283
324
  if (first)
284
325
  first.edge = true;
285
326
  return [...groups.left, ...groups.middle, ...groups.right];
@@ -29,6 +29,12 @@ export declare class TableJoins {
29
29
  * in - or that it is missing, on its way, or could not be read. A key nobody has asked for yet reads as on its way.
30
30
  */
31
31
  peek(name: string, key: string): JoinedState;
32
+ /**
33
+ * The record kept for `key` while its answer is fresh - within `lifetime`, and not marked old since (`expire`: a save
34
+ * there, a Reload) - at once and with no call; `undefined` otherwise. What names a value from a form's first draw
35
+ * without standing in for a record that may have changed since.
36
+ */
37
+ peekFresh(name: string, key: string, lifetime?: number): TableJoinedRecord | undefined;
32
38
  /**
33
39
  * Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
34
40
  * the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
@@ -36,6 +42,11 @@ export declare class TableJoins {
36
42
  request(source: TableJoinSource, keys: Iterable<string>): void;
37
43
  private queue;
38
44
  private send;
45
+ /**
46
+ * Records the page already holds, kept as if a lookup of `source` had just answered them: a form opened on a record
47
+ * whose parent the page shows - a tree's row - names that parent with no call.
48
+ */
49
+ remember(source: TableJoinSource, records: readonly TableJoinedRecord[]): void;
39
50
  /**
40
51
  * The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
41
52
  * calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
@@ -45,6 +56,8 @@ export declare class TableJoins {
45
56
  * A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
46
57
  * the same call. A list that failed is not kept, so the next ask tries again.
47
58
  */
59
+ /** The list kept under `name` while it is fresh, without loading it; `undefined` when there is none yet, or it aged. */
60
+ peekList(name: string, lifetime?: number): readonly TableJoinedRecord[] | undefined;
48
61
  list(name: string, load: (signal: AbortSignal) => Promise<readonly TableJoinedRecord[]>, lifetime?: number): Promise<readonly TableJoinedRecord[]>;
49
62
  /**
50
63
  * Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
@@ -65,6 +65,15 @@ export class TableJoins {
65
65
  return FAILED;
66
66
  return LOADING;
67
67
  }
68
+ /**
69
+ * The record kept for `key` while its answer is fresh - within `lifetime`, and not marked old since (`expire`: a save
70
+ * there, a Reload) - at once and with no call; `undefined` otherwise. What names a value from a form's first draw
71
+ * without standing in for a record that may have changed since.
72
+ */
73
+ peekFresh(name, key, lifetime = JOIN_LIFETIME) {
74
+ const entry = this.sources.get(name)?.entries.get(key);
75
+ return entry?.record && Date.now() - entry.at < lifetime ? entry.record : undefined;
76
+ }
68
77
  /**
69
78
  * Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
70
79
  * the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
@@ -136,6 +145,26 @@ export class TableJoins {
136
145
  this.notify();
137
146
  }
138
147
  }
148
+ /**
149
+ * Records the page already holds, kept as if a lookup of `source` had just answered them: a form opened on a record
150
+ * whose parent the page shows - a tree's row - names that parent with no call.
151
+ */
152
+ remember(source, records) {
153
+ const cache = this.cacheOf(source.name);
154
+ const keyField = source.key ?? "id";
155
+ const at = Date.now();
156
+ let changed = false;
157
+ for (const record of records) {
158
+ const key = record[keyField];
159
+ if (key === undefined || key === null || key === "")
160
+ continue;
161
+ cache.entries.set(String(key), { record, at });
162
+ cache.failedAt.delete(String(key));
163
+ changed = true;
164
+ }
165
+ if (changed)
166
+ this.notify();
167
+ }
139
168
  /**
140
169
  * The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
141
170
  * calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
@@ -157,6 +186,11 @@ export class TableJoins {
157
186
  * A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
158
187
  * the same call. A list that failed is not kept, so the next ask tries again.
159
188
  */
189
+ /** The list kept under `name` while it is fresh, without loading it; `undefined` when there is none yet, or it aged. */
190
+ peekList(name, lifetime = JOIN_LIFETIME) {
191
+ const kept = this.lists.get(name);
192
+ return kept?.rows && Date.now() - kept.at < lifetime ? kept.rows : undefined;
193
+ }
160
194
  list(name, load, lifetime = JOIN_LIFETIME) {
161
195
  const kept = this.lists.get(name);
162
196
  if (kept?.pending)
@@ -10,6 +10,8 @@ export interface TableStoreOptions<TRow extends TableRow = TableRow> {
10
10
  paging: TablePaging;
11
11
  maxSelection?: number;
12
12
  onSelectionChange?: (rows: TRow[]) => void;
13
+ /** After the picked row changes: another row, its newest copy, or none (`DataTable` `onPick`). */
14
+ onPickChange?: (row: TRow | null) => void;
13
15
  /** The advanced filter's inputs, as the table prepared them (`asFilterInput`). */
14
16
  filterInputs?: readonly InputParams[];
15
17
  }
@@ -63,7 +65,10 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
63
65
  /** Takes the component's props. Tells nobody: the component calls it while rendering. */
64
66
  configure(options: TableStoreOptions<TRow>): void;
65
67
  private set;
66
- /** Puts back what was saved on this device (page size, sort, columns), before the first load. Tells nobody. */
68
+ /**
69
+ * Puts back what was saved on this device (page size, sort, columns): before the first load, telling nobody, or, with
70
+ * `notify`, over a table already shown (another key's settings), its rows shown again in them on the first page.
71
+ */
67
72
  restore(saved: {
68
73
  pageSize?: number;
69
74
  sort?: TableSort | null;
@@ -71,6 +76,8 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
71
76
  order?: readonly string[];
72
77
  widths?: Readonly<Record<string, number>>;
73
78
  pinned?: Readonly<Record<string, TablePinSide>>;
79
+ }, options?: {
80
+ notify?: boolean;
74
81
  }): void;
75
82
  /** Shows these rows: every row there is. The table searches, sorts, and pages them. */
76
83
  setRows(rows: readonly TRow[]): void;
@@ -126,6 +133,19 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
126
133
  private refreshSelected;
127
134
  /** A client table forgets selected rows that are gone from its rows. */
128
135
  private dropMissingSelection;
136
+ /**
137
+ * Picks a row (`DataTable` `onPick`), or none: the one row whose own records the page shows beside the table. It
138
+ * follows the record from then on - its newest copy after an upsert, none once it is removed or gone after a load -
139
+ * and every change is told to `onPickChange`.
140
+ */
141
+ pick(row: TRow | null): void;
142
+ /**
143
+ * The picked row as these rows hold it: its newest copy, or none when they are every row there is (`whole`, a client
144
+ * table) and it is not among them. A server page without it keeps it, since it may be on another page.
145
+ */
146
+ private followPicked;
147
+ /** Tells the page when a change left another copy of the picked row, or none. */
148
+ private pickedChanged;
129
149
  setBusy(keys: readonly TableKey[], on: boolean): void;
130
150
  flash(keys: readonly TableKey[]): void;
131
151
  setHidden(key: string, hidden: boolean): void;
@@ -19,6 +19,7 @@ function initialState(pageSize) {
19
19
  matches: 0,
20
20
  query: { search: "", sort: null, page: 1, pageSize },
21
21
  selected: EMPTY_MAP,
22
+ picked: null,
22
23
  busy: EMPTY_SET,
23
24
  flashed: EMPTY_SET,
24
25
  hidden: EMPTY_SET,
@@ -103,13 +104,18 @@ export class TableStore {
103
104
  if (changes.selected && changes.selected !== previous.selected)
104
105
  this.options.onSelectionChange?.(this.getSelected());
105
106
  }
106
- /** Puts back what was saved on this device (page size, sort, columns), before the first load. Tells nobody. */
107
- restore(saved) {
107
+ /**
108
+ * Puts back what was saved on this device (page size, sort, columns): before the first load, telling nobody, or, with
109
+ * `notify`, over a table already shown (another key's settings), its rows shown again in them on the first page.
110
+ */
111
+ restore(saved, options = {}) {
108
112
  const query = { ...this.state.query };
109
113
  if (saved.pageSize && saved.pageSize > 0)
110
114
  query.pageSize = saved.pageSize;
111
115
  if (saved.sort !== undefined)
112
116
  query.sort = saved.sort;
117
+ if (options.notify)
118
+ query.page = 1;
113
119
  this.state = {
114
120
  ...this.state,
115
121
  query,
@@ -118,6 +124,8 @@ export class TableStore {
118
124
  widths: saved.widths ?? this.state.widths,
119
125
  pinned: saved.pinned ?? this.state.pinned,
120
126
  };
127
+ if (options.notify)
128
+ this.view();
121
129
  }
122
130
  /* Rows given by the page */
123
131
  /** Shows these rows: every row there is. The table searches, sorts, and pages them. */
@@ -127,7 +135,9 @@ export class TableStore {
127
135
  this.given = rows;
128
136
  this.all = [...rows];
129
137
  this.dropMissingSelection();
130
- this.view({ status: "ready", error: null, refreshing: false });
138
+ const picked = this.state.picked;
139
+ this.view({ status: "ready", error: null, refreshing: false, picked: this.followPicked(this.all, true) });
140
+ this.pickedChanged(picked);
131
141
  }
132
142
  /* Loading */
133
143
  /**
@@ -151,6 +161,9 @@ export class TableStore {
151
161
  this.state = { ...this.state, query: { ...this.state.query, search: "", page: 1 } };
152
162
  if (this.state.selected.size)
153
163
  this.set({ selected: EMPTY_MAP });
164
+ // Another list: the row picked in the one before is not in it.
165
+ if (this.state.picked)
166
+ this.pick(null);
154
167
  }
155
168
  const keepRows = quiet ?? (this.state.status === "ready" && !reset);
156
169
  const query = this.state.query;
@@ -197,6 +210,7 @@ export class TableStore {
197
210
  void this.reload({ quiet: true });
198
211
  return;
199
212
  }
213
+ const picked = this.state.picked;
200
214
  this.set({
201
215
  status: "ready",
202
216
  refreshing: false,
@@ -207,13 +221,17 @@ export class TableStore {
207
221
  offset: (query.page - 1) * query.pageSize,
208
222
  query,
209
223
  selected: this.refreshSelected(page.rows),
224
+ picked: this.followPicked(page.rows, false),
210
225
  });
226
+ this.pickedChanged(picked);
211
227
  return;
212
228
  }
213
229
  this.all = page.rows;
214
230
  this.given = null;
215
231
  this.dropMissingSelection();
216
- this.view({ status: "ready", refreshing: false, error: null });
232
+ const picked = this.state.picked;
233
+ this.view({ status: "ready", refreshing: false, error: null, picked: this.followPicked(this.all, true) });
234
+ this.pickedChanged(picked);
217
235
  }
218
236
  fail(error, seq) {
219
237
  if (seq !== this.seq)
@@ -301,6 +319,9 @@ export class TableStore {
301
319
  */
302
320
  upsert(row, { at = "start", flash = true } = {}) {
303
321
  const key = this.keyOf(row);
322
+ // The picked record's new copy is the one picked now.
323
+ const before = this.state.picked;
324
+ const picked = before && key && this.keyOf(before) === key ? row : before;
304
325
  if (this.options.paging === "server") {
305
326
  const index = this.state.rows.findIndex(candidate => this.keyOf(candidate) === key);
306
327
  const rows = [...this.state.rows];
@@ -315,7 +336,7 @@ export class TableStore {
315
336
  total++;
316
337
  matches++;
317
338
  }
318
- this.set({ rows, total, matches, selected: this.replaceSelected(key, row), status: "ready" });
339
+ this.set({ rows, total, matches, selected: this.replaceSelected(key, row), picked, status: "ready" });
319
340
  }
320
341
  else {
321
342
  const index = this.all.findIndex(candidate => this.keyOf(candidate) === key);
@@ -326,8 +347,9 @@ export class TableStore {
326
347
  else
327
348
  this.all.push(row);
328
349
  this.all = [...this.all];
329
- this.view({ selected: this.replaceSelected(key, row), status: "ready" });
350
+ this.view({ selected: this.replaceSelected(key, row), picked, status: "ready" });
330
351
  }
352
+ this.pickedChanged(before);
331
353
  if (flash && key)
332
354
  this.flash([key]);
333
355
  }
@@ -355,6 +377,9 @@ export class TableStore {
355
377
  for (const key of gone)
356
378
  selected.delete(key);
357
379
  const busy = new Set([...this.state.busy].filter(key => !gone.has(key)));
380
+ // A picked record that left the list is no longer picked.
381
+ const before = this.state.picked;
382
+ const picked = before && gone.has(this.keyOf(before)) ? null : before;
358
383
  if (this.options.paging === "server") {
359
384
  const rows = this.state.rows.filter(row => !gone.has(this.keyOf(row)));
360
385
  const removed = this.state.rows.length - rows.length;
@@ -363,15 +388,18 @@ export class TableStore {
363
388
  total: Math.max(0, this.state.total - removed),
364
389
  matches: Math.max(0, this.state.matches - removed),
365
390
  selected: selected.size === this.state.selected.size ? this.state.selected : selected,
391
+ picked,
366
392
  busy,
367
393
  });
394
+ this.pickedChanged(before);
368
395
  // A short wait lets several removals (a mass action's rows) share one load.
369
396
  clearTimeout(this.reloadTimer);
370
397
  this.reloadTimer = setTimeout(() => void this.reload({ quiet: true }), 60);
371
398
  return;
372
399
  }
373
400
  this.all = this.all.filter(row => !gone.has(this.keyOf(row)));
374
- this.view({ selected: selected.size === this.state.selected.size ? this.state.selected : selected, busy });
401
+ this.view({ selected: selected.size === this.state.selected.size ? this.state.selected : selected, picked, busy });
402
+ this.pickedChanged(before);
375
403
  }
376
404
  /* Selection */
377
405
  /** Selects or unselects rows, up to `maxSelection`. Returns how many rows it could not select. */
@@ -440,6 +468,35 @@ export class TableStore {
440
468
  this.state = { ...this.state, selected: selected.size ? selected : EMPTY_MAP };
441
469
  this.options.onSelectionChange?.(this.getSelected());
442
470
  }
471
+ /* The picked row */
472
+ /**
473
+ * Picks a row (`DataTable` `onPick`), or none: the one row whose own records the page shows beside the table. It
474
+ * follows the record from then on - its newest copy after an upsert, none once it is removed or gone after a load -
475
+ * and every change is told to `onPickChange`.
476
+ */
477
+ pick(row) {
478
+ const next = row !== null && this.keyOf(row) !== "" ? row : null;
479
+ if (next === this.state.picked)
480
+ return;
481
+ this.set({ picked: next });
482
+ this.options.onPickChange?.(next);
483
+ }
484
+ /**
485
+ * The picked row as these rows hold it: its newest copy, or none when they are every row there is (`whole`, a client
486
+ * table) and it is not among them. A server page without it keeps it, since it may be on another page.
487
+ */
488
+ followPicked(rows, whole) {
489
+ const picked = this.state.picked;
490
+ if (!picked)
491
+ return null;
492
+ const key = this.keyOf(picked);
493
+ return rows.find(row => this.keyOf(row) === key) ?? (whole ? null : picked);
494
+ }
495
+ /** Tells the page when a change left another copy of the picked row, or none. */
496
+ pickedChanged(before) {
497
+ if (this.state.picked !== before)
498
+ this.options.onPickChange?.(this.state.picked);
499
+ }
443
500
  /* Rows with an action running, and rows to highlight */
444
501
  setBusy(keys, on) {
445
502
  const busy = new Set(this.state.busy);
@@ -78,6 +78,40 @@ export interface TreeChildrenPage<TItem extends TreeItem = TreeItem> {
78
78
  /** Whether another page follows. Default: `total` says so, or the page was full. */
79
79
  hasMore?: boolean;
80
80
  }
81
+ /**
82
+ * A change to one item of a tree's source made somewhere else - a form saved, a record moved to the trash - written into
83
+ * the pages kept for the tree (`writeTreeChanges`) and into every tree on screen that shows them, so nothing is read
84
+ * from the server again for it.
85
+ */
86
+ export type TreeItemChange<TItem extends TreeItem = TreeItem> = {
87
+ /** Added, changed, or moved: the item as it is now, under the item whose value is `parent` (`null`: the top level). */
88
+ type: "put";
89
+ item: TItem;
90
+ parent: TreeValue | null;
91
+ /**
92
+ * Where it was: its parent's value (`null`: the top level), or `false` when it was not in the tree at all - just
93
+ * added, or back from the trash. Left out when not known: a tree that holds the item knows, and the kept pages are
94
+ * searched; an item found nowhere then changes no count.
95
+ */
96
+ before?: TreeValue | null | false;
97
+ } | {
98
+ /** Out of the tree - moved to the trash, deleted: the item's value, and its parent's when known. */
99
+ type: "remove";
100
+ value: TreeValue;
101
+ parent?: TreeValue | null;
102
+ };
103
+ /** How a tree reads its items, for changes written from outside it: the same keys the tree is given. */
104
+ export interface TreeChangeRules<TItem extends TreeItem = TreeItem> {
105
+ /** The item field that holds its value: the tree's `valueKey`. Default `id`. */
106
+ valueKey?: string;
107
+ /** The tree's `hasChildrenKey`: a parent's count there goes up and down with its children, and its chevron with it. */
108
+ hasChildrenKey?: string;
109
+ /**
110
+ * The order of siblings, as the server sorts them, so an item added or changed goes where a fresh read would put it.
111
+ * Without it an added item goes after the children loaded, and a changed one stays where it was.
112
+ */
113
+ compare?: (a: TItem, b: TItem) => number;
114
+ }
81
115
  /** Children's pages kept for every lazy tree on the page that names a `cacheKey`, for 5 minutes: see `RequestCache`. */
82
116
  export declare const treePages: RequestCache<TreeChildrenPage<object>>;
83
117
  /** The start of every key a tree keeps its pages under, or one parent's pages (`null`: the top level). */
@@ -90,6 +124,24 @@ export declare function treePageKey(cacheKey: string, parent: TreeValue | null,
90
124
  * its `refresh()` loads that again.
91
125
  */
92
126
  export declare function expireTreePages(cacheKey: string, parent?: TreeValue | null | readonly (TreeValue | null)[]): number;
127
+ /**
128
+ * Writes changes made elsewhere into the pages kept for a tree's source, and into every tree on screen that keeps its
129
+ * pages under `cacheKey` - so a record saved in a form, moved, or sent to the trash shows in the tree at once, and
130
+ * leaving the page and coming back reads nothing again for it. Items go where the parent's children are known (see
131
+ * `TreeStore.placeItem`); a parent whose children were never loaded only counts one more or one fewer, and its first page
132
+ * comes from the server when it is opened, as it would have anyway.
133
+ */
134
+ export declare function writeTreeChanges<TItem extends TreeItem>(cacheKey: string, changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>): void;
135
+ type TreeChangeListener = (changes: readonly TreeItemChange[], rules: TreeChangeRules) => void;
136
+ /** Hears the changes written for `cacheKey`: what a tree on screen does to write them into itself. Returns the way out. */
137
+ export declare function subscribeTreeChanges(cacheKey: string, listener: TreeChangeListener): () => void;
138
+ /**
139
+ * The kept pages under `cacheKey` with the changes written in (what `writeTreeChanges` does to `treePages`). A parent's
140
+ * pages are read as one list from its first child, the changes applied to it, and the list written back in pages of the
141
+ * same size, each with the parent's total - so a tree reading them again, page by page, asks the server for exactly the
142
+ * children it does not have. A page still on its way for a changed parent is dropped: its answer may predate the change.
143
+ */
144
+ export declare function patchTreePages<TItem extends TreeItem>(cache: RequestCache<TreeChildrenPage>, cacheKey: string, changes: readonly TreeItemChange<TItem>[], rules?: TreeChangeRules<TItem>): void;
93
145
  /** A has-children field read as the tree reads it: a boolean, or a count above 0; `undefined` when it says nothing. */
94
146
  export declare function treeHasChildren(value: unknown): boolean | undefined;
95
147
  /** The key of the status row under a parent (`null`: the top level), as `TreeNode.key` holds it. */
@@ -342,6 +394,34 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
342
394
  }): void;
343
395
  /** Removes a node and everything under it. Returns the removed nodes in tree order. */
344
396
  remove(node: TreeNode<TItem>): TreeNode<TItem>[];
397
+ /**
398
+ * Writes an item where a change made elsewhere left it (see `TreeItemChange`): in place under the same parent (moved
399
+ * among its siblings by `compare`), under its new parent when it moved - with what is loaded under it - and in for the
400
+ * first time when it is new or back. Only where the parent's children are known: given, loaded, or known to be none.
401
+ * Under a parent never opened it waits for that parent's first page, and one that sorts after the loaded children while
402
+ * more are to come waits for its page. A lazy parent's `loaded` and `total` follow, so its next page skips the right
403
+ * number, and so does the count in `hasChildrenKey` on the parent's own item, which its chevron goes by. Returns the
404
+ * item's node where it is now, or `undefined` when this tree does not show it.
405
+ */
406
+ placeItem(item: TItem, parentValue: TreeValue | null, options?: {
407
+ before?: TreeValue | null | false;
408
+ compare?: (a: TItem, b: TItem) => number;
409
+ }): TreeNode<TItem> | undefined;
410
+ /**
411
+ * Takes out an item a change made elsewhere took out of the tree - moved to the trash, deleted - with everything under
412
+ * it. Its parent counts one fewer (its pages' `loaded` and `total`, the count on its item) and loses its chevron when
413
+ * none are left. An item not loaded here still lowers its parent's counts when `parentValue` names the parent.
414
+ * Returns the nodes taken out.
415
+ */
416
+ removeItem(value: TreeValue, parentValue?: TreeValue | null): TreeNode<TItem>[];
417
+ /** Whether a parent's children are known here: given in the items, loaded (all or the first pages), or known to be none. */
418
+ private listsChildren;
419
+ /**
420
+ * One child more or fewer under a parent (`null`: the top level): its pages' total, and the count its own item
421
+ * carries in `hasChildrenKey` - a new copy of the item. A parent never opened reads its children from the new count,
422
+ * so one that has none left loses its chevron; one whose children were loaded knows already.
423
+ */
424
+ private countChild;
345
425
  private createNode;
346
426
  /** A lazy node's pages before anything is asked: none to ask for when its item says it has no children. */
347
427
  private newPages;
@@ -400,3 +480,4 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
400
480
  }
401
481
  /** Nodes in tree order: each node, then its children. No recursion, so even a chain 100,000 levels deep works. */
402
482
  export declare function preOrder<TItem extends TreeItem>(nodes: readonly TreeNode<TItem>[]): TreeNode<TItem>[];
483
+ export {};