nexus-shared 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +220 -14
  3. package/dist/Client.Index.d.ts +13 -0
  4. package/dist/Client.Index.js +13 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  22. package/dist/Components/Documents/TabButtons.js +5 -4
  23. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  24. package/dist/Components/Domains/DomainCard.js +182 -0
  25. package/dist/Components/Forms/ApiForm.js +5 -1
  26. package/dist/Components/Forms/Crud.js +72 -24
  27. package/dist/Components/Forms/CrudActions.d.ts +5 -4
  28. package/dist/Components/Forms/CrudActions.js +42 -12
  29. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  30. package/dist/Components/Forms/CrudHistory.js +14 -6
  31. package/dist/Components/Forms/CrudParts.d.ts +11 -4
  32. package/dist/Components/Forms/CrudParts.js +76 -10
  33. package/dist/Components/Forms/CrudRowForms.js +65 -5
  34. package/dist/Components/Forms/CrudTree.js +7 -7
  35. package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
  36. package/dist/Components/Forms/CrudWorkspace.js +19 -11
  37. package/dist/Components/Forms/Form.d.ts +3 -2
  38. package/dist/Components/Forms/Form.js +33 -26
  39. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  40. package/dist/Components/Forms/FormQuestion.js +28 -0
  41. package/dist/Components/Forms/SubmitForm.js +2 -15
  42. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  43. package/dist/Components/Inputs/ComputedInput.js +179 -0
  44. package/dist/Components/Inputs/Dropdown.js +4 -2
  45. package/dist/Components/Inputs/GroupForm.js +3 -2
  46. package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
  47. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  48. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  49. package/dist/Components/Inputs/PictureInput.js +8 -3
  50. package/dist/Components/Inputs/RowsInput.d.ts +14 -1
  51. package/dist/Components/Inputs/RowsInput.js +78 -5
  52. package/dist/Components/Inputs/TabularForm.js +16 -3
  53. package/dist/Components/Inputs/TreeView.d.ts +24 -1
  54. package/dist/Components/Inputs/TreeView.js +84 -7
  55. package/dist/Components/Layouts/Popup.d.ts +22 -2
  56. package/dist/Components/Layouts/Popup.js +108 -6
  57. package/dist/Components/Messages/MailClient.d.ts +50 -0
  58. package/dist/Components/Messages/MailClient.js +410 -0
  59. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  60. package/dist/Components/Messages/MailColumns.js +92 -0
  61. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  62. package/dist/Components/Messages/MailCompose.js +154 -0
  63. package/dist/Components/Messages/MailReader.d.ts +43 -0
  64. package/dist/Components/Messages/MailReader.js +207 -0
  65. package/dist/Components/Messages/MailThread.d.ts +71 -0
  66. package/dist/Components/Messages/MailThread.js +185 -0
  67. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  68. package/dist/Components/Messages/MessageBell.js +76 -0
  69. package/dist/Components/Viewers/DataTable.js +7 -5
  70. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  71. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  72. package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
  73. package/dist/Components/Viewers/DataTableParts.js +2 -2
  74. package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
  75. package/dist/Components/Viewers/OwnerFiles.js +142 -31
  76. package/dist/Helpers/ApiModules.js +4 -0
  77. package/dist/Helpers/AuditEntries.d.ts +50 -0
  78. package/dist/Helpers/AuditEntries.js +96 -0
  79. package/dist/Helpers/ChatBackend.d.ts +8 -1
  80. package/dist/Helpers/ChatBackend.js +18 -0
  81. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  82. package/dist/Helpers/ChatHelpers.js +4 -1
  83. package/dist/Helpers/CrudColumns.d.ts +44 -6
  84. package/dist/Helpers/CrudColumns.js +63 -13
  85. package/dist/Helpers/CrudHelpers.d.ts +36 -1
  86. package/dist/Helpers/CrudHelpers.js +61 -0
  87. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  88. package/dist/Helpers/CrudPickerPlan.js +46 -0
  89. package/dist/Helpers/CrudRowForms.d.ts +18 -1
  90. package/dist/Helpers/CrudRowForms.js +35 -2
  91. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  92. package/dist/Helpers/DashboardHelpers.js +305 -0
  93. package/dist/Helpers/DomainCards.d.ts +140 -0
  94. package/dist/Helpers/DomainCards.js +370 -0
  95. package/dist/Helpers/FileAdapter.d.ts +82 -2
  96. package/dist/Helpers/FileAdapter.js +72 -1
  97. package/dist/Helpers/FormStore.d.ts +11 -1
  98. package/dist/Helpers/FormStore.js +66 -19
  99. package/dist/Helpers/InputConditions.d.ts +6 -0
  100. package/dist/Helpers/InputConditions.js +24 -0
  101. package/dist/Helpers/InputMemory.js +40 -30
  102. package/dist/Helpers/InputNarrowing.d.ts +38 -6
  103. package/dist/Helpers/InputNarrowing.js +102 -21
  104. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  105. package/dist/Helpers/MailClientHelpers.js +211 -0
  106. package/dist/Helpers/MessageBackend.d.ts +152 -0
  107. package/dist/Helpers/MessageBackend.js +329 -0
  108. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  109. package/dist/Helpers/MessageHelpers.js +573 -0
  110. package/dist/Helpers/MessageUnread.d.ts +95 -0
  111. package/dist/Helpers/MessageUnread.js +245 -0
  112. package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
  113. package/dist/Helpers/NexusDriveAdapter.js +32 -3
  114. package/dist/Helpers/NumberHelpers.d.ts +61 -0
  115. package/dist/Helpers/NumberHelpers.js +124 -8
  116. package/dist/Helpers/PagedPicker.d.ts +42 -0
  117. package/dist/Helpers/PagedPicker.js +85 -0
  118. package/dist/Helpers/RowsStore.d.ts +24 -0
  119. package/dist/Helpers/RowsStore.js +79 -10
  120. package/dist/Helpers/TableHelpers.d.ts +15 -2
  121. package/dist/Helpers/TableHelpers.js +28 -1
  122. package/dist/Helpers/TableStore.d.ts +7 -3
  123. package/dist/Helpers/TableStore.js +9 -4
  124. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  125. package/dist/Helpers/ToolbarFit.js +30 -0
  126. package/dist/Helpers/TreeStore.d.ts +75 -6
  127. package/dist/Helpers/TreeStore.js +182 -18
  128. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  129. package/dist/Interfaces/ApiInterfaces.js +10 -1
  130. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  131. package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
  132. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  133. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  134. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  135. package/dist/Interfaces/DomainInterfaces.js +1 -0
  136. package/dist/Interfaces/FileInterfaces.d.ts +21 -0
  137. package/dist/Interfaces/FileInterfaces.js +2 -0
  138. package/dist/Interfaces/FormInterfaces.d.ts +78 -11
  139. package/dist/Interfaces/InputInterfaces.d.ts +12 -5
  140. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  141. package/dist/Interfaces/MailInterfaces.js +1 -0
  142. package/dist/Interfaces/TableInterfaces.d.ts +28 -2
  143. package/dist/Services/JoinSources.d.ts +47 -6
  144. package/dist/Services/JoinSources.js +68 -33
  145. package/dist/Services/MessageApi.d.ts +18 -0
  146. package/dist/Services/MessageApi.js +64 -0
  147. package/dist/Shared.Index.d.ts +12 -0
  148. package/dist/Shared.Index.js +12 -0
  149. package/package.json +7 -1
  150. package/src/Styles/Nexus.Button.css +29 -0
  151. package/src/Styles/Nexus.Chat.css +114 -22
  152. package/src/Styles/Nexus.Crud.css +119 -28
  153. package/src/Styles/Nexus.Dashboard.css +763 -0
  154. package/src/Styles/Nexus.Domain.css +384 -0
  155. package/src/Styles/Nexus.Editor.css +20 -0
  156. package/src/Styles/Nexus.Files.css +27 -0
  157. package/src/Styles/Nexus.Index.css +3 -0
  158. package/src/Styles/Nexus.Input.css +31 -0
  159. package/src/Styles/Nexus.Mail.css +1062 -0
  160. package/src/Styles/Nexus.Popup.css +58 -0
  161. package/src/Styles/Nexus.Table.css +1458 -1449
  162. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  163. package/src/Styles/Nexus.Tree.css +10 -0
@@ -0,0 +1,85 @@
1
+ import { isAbortError } from "./ApiResponses.js";
2
+ import { itemValue, optionKey } from "./DropdownHelpers.js";
3
+ /** The items each picker has seen, by key, kept beside its params so the params stay a dropdown's alone. */
4
+ const seenBy = new WeakMap();
5
+ /**
6
+ * A dropdown over a server's search, paged (`load` takes the dropdown's own request), that names every value it holds:
7
+ * from the pages it has shown, the items it was handed, the server's lookup, or the server's first pages.
8
+ *
9
+ * @example
10
+ * const people = pagedPicker(request => mail.people(request), { isInactive: person => person.isActive === false });
11
+ * const inputs = [{ type: EInputTypes.Dropdown, name: "to", label: "To", multiple: true, required: true, ...people }];
12
+ */
13
+ export function pagedPicker(load, options = {}) {
14
+ const { valueKey = "id", labelKey = "name", descriptionKey, isInactive, find, findPages = 4, findPageSize = 50 } = options;
15
+ const seen = new Map();
16
+ const hold = (items) => {
17
+ for (const item of items) {
18
+ const value = itemValue(item, valueKey);
19
+ if (value !== undefined)
20
+ seen.set(optionKey(value), item);
21
+ }
22
+ };
23
+ /** Searches the server's first pages for keys nothing has named, stopping at the list's end or when all are found. */
24
+ async function searchPages(missing) {
25
+ const controller = new AbortController();
26
+ for (let page = 1; page <= findPages && missing.size > 0; page++) {
27
+ const answer = await load({ search: "", page, pageSize: findPageSize, skip: (page - 1) * findPageSize, signal: controller.signal });
28
+ hold(answer.items);
29
+ for (const item of answer.items)
30
+ missing.delete(optionKey(itemValue(item, valueKey)));
31
+ const ended = answer.hasMore === false || answer.items.length === 0 || (answer.hasMore === undefined && answer.items.length < findPageSize);
32
+ if (ended)
33
+ return;
34
+ }
35
+ }
36
+ const params = {
37
+ async loadOptions(request) {
38
+ const answer = await load(request);
39
+ hold(answer.items);
40
+ return answer;
41
+ },
42
+ async resolveOptions(values) {
43
+ const keys = values.map(optionKey);
44
+ const missing = new Set(keys.filter(key => !seen.has(key)));
45
+ try {
46
+ if (missing.size > 0 && find) {
47
+ const found = await find([...missing]);
48
+ hold(found);
49
+ for (const item of found)
50
+ missing.delete(optionKey(itemValue(item, valueKey)));
51
+ }
52
+ if (missing.size > 0)
53
+ await searchPages(missing);
54
+ }
55
+ catch (error) {
56
+ // A value the dropdown cannot name shows as "Not found"; a failed lookup is not worth more than that, but the
57
+ // dropdown's own abort still ends the call.
58
+ if (isAbortError(error))
59
+ throw error;
60
+ }
61
+ return keys.map(key => seen.get(key)).filter((item) => item !== undefined);
62
+ },
63
+ peekOption: key => seen.get(key),
64
+ valueKey,
65
+ labelKey,
66
+ ...(descriptionKey && { descriptionKey }),
67
+ isOptionInactive: item => isInactive?.(item) === true,
68
+ };
69
+ seenBy.set(params, seen);
70
+ return params;
71
+ }
72
+ /**
73
+ * Hands a picker items the page already holds, so a form that opens with their values names them at once, with no call:
74
+ * the people a new message goes to by default.
75
+ */
76
+ export function holdPickerItems(picker, items) {
77
+ const seen = seenBy.get(picker);
78
+ if (!seen)
79
+ return;
80
+ for (const item of items) {
81
+ const value = itemValue(item, picker.valueKey);
82
+ if (value !== undefined)
83
+ seen.set(optionKey(value), item);
84
+ }
85
+ }
@@ -108,6 +108,19 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
108
108
  configure(options: RowsStoreOptions<TRow>, recheck?: boolean): void;
109
109
  /** Sets a value in a row. `input` is a change the user made in the row's input: it runs `onRowChange`. */
110
110
  set(key: string, name: string, value: unknown, source?: "input" | "code"): void;
111
+ /**
112
+ * Values a person set in a row through another of its inputs - one worked out from them and typed in (`compute.solve`)
113
+ * - as one change. Their cells show them at once, as values from code do, and they are taken as the person's own:
114
+ * `onRowChange` runs for each, the row's inputs narrowed by them lose what they picked, and the rows' value listeners
115
+ * hear of it (a parent form's unsaved-changes state, its Save and its leave guard). A name worked out itself is not
116
+ * taken. Answers whether anything changed.
117
+ */
118
+ setFromPerson(key: string, values: Partial<TRow>): boolean;
119
+ /**
120
+ * Draws every row again, values untouched: the form around the rows changed a value their cells follow
121
+ * (`narrowedBy` `from: "form"`), so each cell is narrowed afresh as it draws.
122
+ */
123
+ redrawRows(): void;
111
124
  /** Marks a value as left by the user: a repeated value shows its message from now on. */
112
125
  touch(key: string, name: string): void;
113
126
  /**
@@ -175,7 +188,18 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
175
188
  private askPickers;
176
189
  /** Drops what the store knew about a removed row. */
177
190
  private forget;
191
+ /**
192
+ * Writes one value. `input` was typed in its own cell; `code` comes from code (its cell mounts again with it); `person`
193
+ * is a person's change made through another cell (`setFromPerson`): written as code writes, followed as typing is.
194
+ */
178
195
  private write;
196
+ /**
197
+ * After a person changed a row's value: the row's inputs narrowed by it (`narrowedBy`, a line's Tags by its Item) lose
198
+ * what they picked among its old options, down the row's chain, and the row draws again so each is narrowed afresh;
199
+ * a row input that `dependsOn` it (a Tags box whose most picks are the row's quantity, through `cellParams`) draws
200
+ * again too, its rule asked again.
201
+ */
202
+ private follow;
179
203
  /** Turns a radio off in every row but this one. */
180
204
  private radioOn;
181
205
  /** Checks one value against its input's rules. */
@@ -1,6 +1,13 @@
1
1
  import { EInputTypes } from "../Interfaces/FormInterfaces.js";
2
+ import { isComputedInput, withComputeSources } from "./InputConditions.js";
2
3
  import { findRememberedOpening, forgetInputMemory, isEmptyInputValue, isPickerInput, lastAddedRowKey, readInputMemory, rememberDependencies, rememberedOpeningNow, writeInputMemory, } from "./InputMemory.js";
4
+ import { isEmptyNarrowed, narrowedFollowers } from "./InputNarrowing.js";
3
5
  import { duplicateMessageFor, holdsValue, initialInputValue, inputValuesEqual, normalizeInputValue, rowsItemNames, rowsMessage, uniqueKeyOf, validateInput, } from "./InputParamsHelpers.js";
6
+ /** The options with each worked-out input following the inputs it is worked out from (`compute.from` in `dependsOn`). */
7
+ function withSources(options) {
8
+ const inputs = withComputeSources(options.inputs);
9
+ return inputs === options.inputs ? options : { ...options, inputs };
10
+ }
4
11
  export class RowsStore {
5
12
  options;
6
13
  inputByName = new Map();
@@ -38,7 +45,7 @@ export class RowsStore {
38
45
  /** The array last given to value listeners. A controlled parent that passes it back changes nothing. */
39
46
  lastValue = null;
40
47
  constructor(options, rows = []) {
41
- this.options = options;
48
+ this.options = withSources(options);
42
49
  this.indexInputs();
43
50
  this.load(rows);
44
51
  this.initial = this.getValue();
@@ -161,7 +168,7 @@ export class RowsStore {
161
168
  * callbacks (`validate`, `onRowChange`, `newRow`) are swapped, which a parent's render does every time.
162
169
  */
163
170
  configure(options, recheck = true) {
164
- this.options = options;
171
+ this.options = withSources(options);
165
172
  if (!recheck)
166
173
  return;
167
174
  this.indexInputs();
@@ -179,6 +186,30 @@ export class RowsStore {
179
186
  return;
180
187
  this.settle(changed);
181
188
  }
189
+ /**
190
+ * Values a person set in a row through another of its inputs - one worked out from them and typed in (`compute.solve`)
191
+ * - as one change. Their cells show them at once, as values from code do, and they are taken as the person's own:
192
+ * `onRowChange` runs for each, the row's inputs narrowed by them lose what they picked, and the rows' value listeners
193
+ * hear of it (a parent form's unsaved-changes state, its Save and its leave guard). A name worked out itself is not
194
+ * taken. Answers whether anything changed.
195
+ */
196
+ setFromPerson(key, values) {
197
+ const before = this.getValue();
198
+ const changed = new Set();
199
+ for (const [name, value] of Object.entries(values))
200
+ this.write(key, name, value, "person", changed, 0);
201
+ if (this.getValue() === before)
202
+ return false;
203
+ this.settle(changed);
204
+ return true;
205
+ }
206
+ /**
207
+ * Draws every row again, values untouched: the form around the rows changed a value their cells follow
208
+ * (`narrowedBy` `from: "form"`), so each cell is narrowed afresh as it draws.
209
+ */
210
+ redrawRows() {
211
+ this.touchAllRows();
212
+ }
182
213
  /** Marks a value as left by the user: a repeated value shows its message from now on. */
183
214
  touch(key, name) {
184
215
  let names = this.touched.get(key);
@@ -200,7 +231,7 @@ export class RowsStore {
200
231
  const wasEmpty = this.entries.length === 0;
201
232
  const row = {};
202
233
  for (const params of this.options.inputs)
203
- if (holdsValue(params))
234
+ if (holdsValue(params) && !isComputedInput(params))
204
235
  row[params.name] = initialInputValue(params);
205
236
  const { newRow } = this.options;
206
237
  const named = { ...(typeof newRow === "function" ? newRow(position) : newRow), ...values };
@@ -250,7 +281,7 @@ export class RowsStore {
250
281
  return;
251
282
  const read = (name) => row[name];
252
283
  for (const params of this.options.inputs)
253
- if (params.remember && !params.hidden)
284
+ if (params.remember && !params.hidden && !isComputedInput(params))
254
285
  writeInputMemory(this.memory, params, row[params.name], read);
255
286
  }
256
287
  /** Adds a copy of a row after it, without its id and with its radios off. Returns the copy's key. */
@@ -387,6 +418,11 @@ export class RowsStore {
387
418
  normalizeRow(row) {
388
419
  const values = { ...row };
389
420
  for (const params of this.options.inputs) {
421
+ // A worked-out input holds no value: a field the row came with under its name is not the row's to send.
422
+ if (isComputedInput(params)) {
423
+ delete values[params.name];
424
+ continue;
425
+ }
390
426
  // A hidden input's value stays in the data as it was.
391
427
  if (params.hidden)
392
428
  continue;
@@ -437,7 +473,7 @@ export class RowsStore {
437
473
  const read = (name) => row[name];
438
474
  for (const params of this.options.inputs) {
439
475
  const { name } = params;
440
- if (!params.remember || params.hidden || !holdsValue(params))
476
+ if (!params.remember || params.hidden || !holdsValue(params) || isComputedInput(params))
441
477
  continue;
442
478
  if (name in named && !isEmptyInputValue(params, named[name]))
443
479
  continue;
@@ -507,22 +543,31 @@ export class RowsStore {
507
543
  this.revisions.delete(key);
508
544
  this.asking.delete(key);
509
545
  }
546
+ /**
547
+ * Writes one value. `input` was typed in its own cell; `code` comes from code (its cell mounts again with it); `person`
548
+ * is a person's change made through another cell (`setFromPerson`): written as code writes, followed as typing is.
549
+ */
510
550
  write(key, name, value, source, changed, depth) {
511
551
  const index = this.positions.get(key);
512
552
  const entry = index === undefined ? undefined : this.entries[index];
513
553
  if (!entry || index === undefined)
514
554
  return;
515
555
  const params = this.inputByName.get(name);
516
- const next = params && source === "code" ? normalizeInputValue(params, value) : value;
556
+ // A worked-out input holds no value: one set from code (an `onRowChange` or `deriveRows` patch) is not taken.
557
+ if (isComputedInput(params))
558
+ return;
559
+ const asCode = source !== "input";
560
+ const byPerson = source !== "code";
561
+ const next = params && asCode ? normalizeInputValue(params, value) : value;
517
562
  // The person's own pick: a picker's answer about a remembered value no longer replaces it.
518
- if (source === "input")
563
+ if (byPerson)
519
564
  this.asking.get(key)?.delete(name);
520
565
  if (inputValuesEqual(entry.values[name], next))
521
566
  return;
522
567
  const shownBefore = this.shownError(key, name);
523
568
  entry.values = { ...entry.values, [name]: next };
524
569
  this.valueCache = null;
525
- if (source === "code") {
570
+ if (asCode) {
526
571
  // The input shows its value from when it mounted; a new revision mounts it again with this one.
527
572
  let names = this.revisions.get(key);
528
573
  if (!names)
@@ -544,7 +589,7 @@ export class RowsStore {
544
589
  if (this.shownError(key, name) !== shownBefore)
545
590
  changed.add(key);
546
591
  // Values the row derives from the one the user changed, such as an amount from a quantity and a rate.
547
- if (source === "input" && depth === 0 && this.options.onRowChange) {
592
+ if (byPerson && depth === 0 && this.options.onRowChange) {
548
593
  const patch = this.options.onRowChange(entry.values, name, index);
549
594
  if (patch) {
550
595
  for (const [field, fieldValue] of Object.entries(patch)) {
@@ -553,6 +598,30 @@ export class RowsStore {
553
598
  }
554
599
  }
555
600
  }
601
+ if (byPerson && depth === 0)
602
+ this.follow(key, name, changed);
603
+ }
604
+ /**
605
+ * After a person changed a row's value: the row's inputs narrowed by it (`narrowedBy`, a line's Tags by its Item) lose
606
+ * what they picked among its old options, down the row's chain, and the row draws again so each is narrowed afresh;
607
+ * a row input that `dependsOn` it (a Tags box whose most picks are the row's quantity, through `cellParams`) draws
608
+ * again too, its rule asked again.
609
+ */
610
+ follow(key, name, changed) {
611
+ const index = this.positions.get(key);
612
+ const entry = index === undefined ? undefined : this.entries[index];
613
+ if (!entry)
614
+ return;
615
+ const followers = narrowedFollowers(this.options.inputs, [name], "row");
616
+ for (const follower of followers) {
617
+ if (!isEmptyNarrowed(entry.values[follower]))
618
+ this.write(key, follower, null, "code", changed, 1);
619
+ }
620
+ const dependents = this.options.inputs.filter(params => params.dependsOn?.includes(name));
621
+ for (const params of dependents)
622
+ this.checkCell(entry, params);
623
+ if (followers.length || dependents.length)
624
+ changed.add(key);
556
625
  }
557
626
  /** Turns a radio off in every row but this one. */
558
627
  radioOn(entry, name, changed) {
@@ -563,7 +632,7 @@ export class RowsStore {
563
632
  }
564
633
  /** Checks one value against its input's rules. */
565
634
  checkCell(entry, params) {
566
- const message = params.hidden ? null : validateInput(params, entry.values[params.name], this.options.context);
635
+ const message = params.hidden || isComputedInput(params) ? null : validateInput(params, entry.values[params.name], this.options.context);
567
636
  this.setError(this.ruleErrors, entry.key, params.name, message);
568
637
  }
569
638
  setError(map, key, name, message) {
@@ -1,6 +1,6 @@
1
1
  import type { DatePreferences } from "../Interfaces/DateInterfaces.ts";
2
2
  import type { InputParams } from "../Interfaces/FormInterfaces.ts";
3
- import type { TableColumn, TableColumnOption, TableHeadBand, TableKey, TablePage, TablePinSide, TableQuery, TableRow, TableSort, TableViewColumn } from "../Interfaces/TableInterfaces.ts";
3
+ import type { TableColumn, TableColumnOption, TableHeadBand, TableKey, TablePage, TablePinSide, TablePins, TableQuery, TableRow, TableSort, TableViewColumn } from "../Interfaces/TableInterfaces.ts";
4
4
  /** Rows per page when nothing says otherwise. */
5
5
  export declare const DEFAULT_PAGE_SIZE = 10;
6
6
  /** Choices of rows per page. */
@@ -81,6 +81,19 @@ export declare const TABLE_ACTIONS_COLUMN = "#actions";
81
81
  export declare const MIN_COLUMN_WIDTH = 56;
82
82
  /** Whether a key names one of the table's own columns, not a column of the page's. */
83
83
  export declare const isOwnColumn: (key: string) => boolean;
84
+ /**
85
+ * The columns as the table arranges them before the user does: every `lead` column (a picture, a logo) first, the
86
+ * others in the order given. When the page pins other columns on the left, a lead column without a side of its own is
87
+ * pinned there too, so it still comes first. The same array when there is nothing to move.
88
+ */
89
+ export declare function leadColumns<TRow extends TableRow>(columns: readonly TableColumn<TRow>[]): readonly TableColumn<TRow>[];
90
+ /**
91
+ * The column whose text names a row when no `rowLabel` is given ("Delete Kathmandu?"): the first that is not a `lead`
92
+ * column, whose picture has no words.
93
+ */
94
+ export declare function namingColumn<TRow extends TableRow>(columns: readonly TableColumn<TRow>[]): TableColumn<TRow> | undefined;
95
+ /** The side a column is pinned to: the user's choice (`null` when they unpinned it), else its own `pinned`. */
96
+ export declare function columnPin(column: Pick<TableColumn, "key" | "pinned">, pinned: TablePins): TablePinSide | null;
84
97
  /**
85
98
  * The columns in the order the user put them: the keys of `order` first, in that order, then any column it does not
86
99
  * name, where the columns were given. A key of a column that is gone is left out.
@@ -96,7 +109,7 @@ export interface ColumnRights {
96
109
  }
97
110
  export interface ViewColumnsOptions extends ColumnRights {
98
111
  order?: readonly string[];
99
- pinned?: Readonly<Record<string, TablePinSide>>;
112
+ pinned?: TablePins;
100
113
  widths?: Readonly<Record<string, number>>;
101
114
  /** Pixels from the pinned side, measured from the columns before it. */
102
115
  offsets?: Readonly<Record<string, number>>;
@@ -81,6 +81,9 @@ export function formatCellText(column, value, options = {}, row) {
81
81
  return option.label;
82
82
  if (isEmpty(value))
83
83
  return "";
84
+ // A list of rows a record's details draw as a table (`column.table`): elsewhere, how many it holds.
85
+ if (column.table && Array.isArray(value))
86
+ return String(value.length);
84
87
  const { prefix = "", suffix = "" } = column;
85
88
  switch (column.format) {
86
89
  case "number":
@@ -232,6 +235,30 @@ export const TABLE_ACTIONS_COLUMN = "#actions";
232
235
  export const MIN_COLUMN_WIDTH = 56;
233
236
  /** Whether a key names one of the table's own columns, not a column of the page's. */
234
237
  export const isOwnColumn = (key) => key.startsWith("#");
238
+ /**
239
+ * The columns as the table arranges them before the user does: every `lead` column (a picture, a logo) first, the
240
+ * others in the order given. When the page pins other columns on the left, a lead column without a side of its own is
241
+ * pinned there too, so it still comes first. The same array when there is nothing to move.
242
+ */
243
+ export function leadColumns(columns) {
244
+ if (!columns.some(column => column.lead))
245
+ return columns;
246
+ const pinsLeft = columns.some(column => !column.lead && column.pinned === "left");
247
+ const leads = columns.filter(column => column.lead).map(column => (pinsLeft && !column.pinned ? { ...column, pinned: "left" } : column));
248
+ const arranged = [...leads, ...columns.filter(column => !column.lead)];
249
+ return arranged.every((column, index) => column === columns[index]) ? columns : arranged;
250
+ }
251
+ /**
252
+ * The column whose text names a row when no `rowLabel` is given ("Delete Kathmandu?"): the first that is not a `lead`
253
+ * column, whose picture has no words.
254
+ */
255
+ export function namingColumn(columns) {
256
+ return columns.find(column => !column.lead) ?? columns[0];
257
+ }
258
+ /** The side a column is pinned to: the user's choice (`null` when they unpinned it), else its own `pinned`. */
259
+ export function columnPin(column, pinned) {
260
+ return Object.hasOwn(pinned, column.key) ? (pinned[column.key] ?? null) : (column.pinned ?? null);
261
+ }
235
262
  /**
236
263
  * The columns in the order the user put them: the keys of `order` first, in that order, then any column it does not
237
264
  * name, where the columns were given. A key of a column that is gone is left out.
@@ -303,7 +330,7 @@ export function viewColumns(columns, options = {}) {
303
330
  const { order, pinned = {}, widths = {}, offsets = {}, released, reorderable = true, resizable = true } = options;
304
331
  const groups = { left: [], middle: [], right: [] };
305
332
  for (const column of orderColumns(columns, order)) {
306
- const side = pinned[column.key] ?? column.pinned ?? null;
333
+ const side = columnPin(column, pinned);
307
334
  groups[side === "left" ? "left" : side === "right" ? "right" : "middle"].push({
308
335
  column,
309
336
  // A pinned column the table is too narrow for keeps its side and scrolls with the rest.
@@ -1,5 +1,5 @@
1
1
  import type { InputParams } from "../Interfaces/FormInterfaces.ts";
2
- import type { TableColumn, TableKey, TableLoader, TablePaging, TablePinSide, TableRow, TableSort, TableState } from "../Interfaces/TableInterfaces.ts";
2
+ import type { TableColumn, TableKey, TableLoader, TablePaging, TablePinSide, TablePins, TableRow, TableSort, TableState } from "../Interfaces/TableInterfaces.ts";
3
3
  /** What the table component tells its store on every render. */
4
4
  export interface TableStoreOptions<TRow extends TableRow = TableRow> {
5
5
  columns: readonly TableColumn<TRow>[];
@@ -75,7 +75,7 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
75
75
  hidden?: readonly string[];
76
76
  order?: readonly string[];
77
77
  widths?: Readonly<Record<string, number>>;
78
- pinned?: Readonly<Record<string, TablePinSide>>;
78
+ pinned?: TablePins;
79
79
  }, options?: {
80
80
  notify?: boolean;
81
81
  }): void;
@@ -159,7 +159,11 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
159
159
  setWidths(widths: Readonly<Record<string, number>>): void;
160
160
  /** Back to widths that follow the content. */
161
161
  clearWidths(): void;
162
- /** Keeps a column on the left or the right while the others scroll, or lets it scroll again (`null`). */
162
+ /**
163
+ * Keeps a column on the left or the right while the others scroll, or lets it scroll again (`null`). A choice equal
164
+ * to the column's own `pinned` is no change to remember; unpinning a column its params pin is remembered as `null`,
165
+ * which wins over them.
166
+ */
163
167
  setPinned(key: string, side: TablePinSide | null): void;
164
168
  /** Every column back as its params say: the given order, widths from the content, no pins, and the hidden ones. */
165
169
  resetColumns(): void;
@@ -1,6 +1,6 @@
1
1
  import { errorMessage, isAbortError } from "./ApiResponses.js";
2
2
  import { activeFilters } from "./TableFilters.js";
3
- import { DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, MIN_COLUMN_WIDTH, clampPage, moveColumnKey, nextSort, queryRows, readTablePage, rowKeyText } from "./TableHelpers.js";
3
+ import { DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, MIN_COLUMN_WIDTH, clampPage, columnPin, moveColumnKey, nextSort, queryRows, readTablePage, rowKeyText } from "./TableHelpers.js";
4
4
  /** How long a changed or added row stays highlighted. */
5
5
  export const TABLE_FLASH_MS = 1600;
6
6
  const EMPTY_MAP = new Map();
@@ -562,12 +562,17 @@ export class TableStore {
562
562
  return;
563
563
  this.set({ widths: NO_WIDTHS });
564
564
  }
565
- /** Keeps a column on the left or the right while the others scroll, or lets it scroll again (`null`). */
565
+ /**
566
+ * Keeps a column on the left or the right while the others scroll, or lets it scroll again (`null`). A choice equal
567
+ * to the column's own `pinned` is no change to remember; unpinning a column its params pin is remembered as `null`,
568
+ * which wins over them.
569
+ */
566
570
  setPinned(key, side) {
567
- if ((this.state.pinned[key] ?? null) === side)
571
+ const own = this.options.columns.find(column => column.key === key)?.pinned ?? null;
572
+ if (columnPin({ key, pinned: own ?? undefined }, this.state.pinned) === side)
568
573
  return;
569
574
  const pinned = { ...this.state.pinned };
570
- if (side === null)
575
+ if (side === own)
571
576
  delete pinned[key];
572
577
  else
573
578
  pinned[key] = side;
@@ -0,0 +1,23 @@
1
+ /** The sizes a toolbar is laid out with, in pixels. */
2
+ export interface ToolbarSizes {
3
+ /** A button's width. */
4
+ tool: number;
5
+ /** A divider's width with its margins. */
6
+ divider: number;
7
+ /** The gap between two items. */
8
+ gap: number;
9
+ /** The "More" button's width. Default `tool`. */
10
+ more?: number;
11
+ }
12
+ /** What a toolbar of a width shows: its items in order, dividers among them, and the tools left for the menu. */
13
+ export interface ToolbarFit<T extends string> {
14
+ shown: (T | "|")[];
15
+ hidden: T[];
16
+ }
17
+ /**
18
+ * The items of a toolbar that fit `width`, in order, and the rest for its "More" menu. All of them when they fit; else
19
+ * as many as fit beside the "More" button, with no divider left at the end of the row and none in the menu.
20
+ *
21
+ * @example fitToolbar(["bold", "italic", "|", "link"], 70, { tool: 28, divider: 11, gap: 2 }) // { shown: ["bold"], hidden: ["italic", "link"] }
22
+ */
23
+ export declare function fitToolbar<T extends string>(items: readonly (T | "|")[], width: number, sizes: ToolbarSizes): ToolbarFit<T>;
@@ -0,0 +1,30 @@
1
+ /*
2
+ * A toolbar that keeps to one row: which of its buttons fit the room it has, and which go into its "More" menu. Pure,
3
+ * so it is checked without a browser; an editor measures its row and its sizes and draws what this answers.
4
+ */
5
+ /**
6
+ * The items of a toolbar that fit `width`, in order, and the rest for its "More" menu. All of them when they fit; else
7
+ * as many as fit beside the "More" button, with no divider left at the end of the row and none in the menu.
8
+ *
9
+ * @example fitToolbar(["bold", "italic", "|", "link"], 70, { tool: 28, divider: 11, gap: 2 }) // { shown: ["bold"], hidden: ["italic", "link"] }
10
+ */
11
+ export function fitToolbar(items, width, sizes) {
12
+ const size = (item) => (item === "|" ? sizes.divider : sizes.tool);
13
+ const all = items.reduce((total, item, index) => total + size(item) + (index > 0 ? sizes.gap : 0), 0);
14
+ if (all <= width)
15
+ return { shown: [...items], hidden: [] };
16
+ const room = width - (sizes.more ?? sizes.tool) - sizes.gap;
17
+ let used = 0;
18
+ let count = 0;
19
+ for (const item of items) {
20
+ const next = size(item) + (count > 0 ? sizes.gap : 0);
21
+ if (used + next > room)
22
+ break;
23
+ used += next;
24
+ count++;
25
+ }
26
+ const shown = items.slice(0, count);
27
+ while (shown.length > 0 && shown[shown.length - 1] === "|")
28
+ shown.pop();
29
+ return { shown, hidden: items.slice(count).filter((item) => item !== "|") };
30
+ }
@@ -38,7 +38,34 @@ export interface TreeStoreOptions<TItem extends TreeItem = TreeItem> {
38
38
  * than 0). Without it, or when the field is empty, a node counts as having children until its first page answers.
39
39
  */
40
40
  hasChildrenKey?: string;
41
+ /** The leaves shown at the start: see `TreeStore.setFilter`. Default: all. */
42
+ filter?: TreeFilterMatch<TItem> | null;
41
43
  }
44
+ /**
45
+ * Which leaves a filtered tree shows (`TreeStore.setFilter`): asked of every item without children. A parent shows when it
46
+ * holds a leaf the filter shows; one whose children are still to load (a lazy tree) shows too, since they may.
47
+ */
48
+ export type TreeFilterMatch<TItem extends TreeItem = TreeItem> = (item: TItem) => boolean;
49
+ /** One tab of a tree's filters (TreeView `filters`): what it is called, and the leaves it shows. */
50
+ export interface TreeFilter<TItem extends TreeItem = TreeItem> {
51
+ /** Unique among the filters: what `defaultFilter`, the kept choice and `onFilterChange` name. */
52
+ id: string;
53
+ /** The tab's text, e.g. "View only". */
54
+ label: string;
55
+ /** The tab's tooltip. */
56
+ title?: string;
57
+ /** The leaves it shows, asked of every item without children. Left out: all of them (an "All" tab). */
58
+ match?: TreeFilterMatch<TItem>;
59
+ }
60
+ /** Where a tree keeps the filter chosen in this browser: per `key` and `viewer`, nothing without a viewer. */
61
+ export declare function treeFilterStorageKey(remember: {
62
+ key: string;
63
+ viewer?: string | null;
64
+ } | undefined): string | null;
65
+ /** The filter on: the one chosen, else the default, else the first; `undefined` without filters. An id no filter has is passed over. */
66
+ export declare function shownTreeFilter<TFilter extends {
67
+ id: string;
68
+ }>(filters: readonly TFilter[] | undefined, chosen: string | null | undefined, defaultId?: string): TFilter | undefined;
42
69
  /** A row that is not an item, under a parent of a lazy tree: its page is on its way, failed (Retry), or more can be loaded. */
43
70
  export type TreeStatusKind = "loading" | "error" | "more";
44
71
  /** Where a lazy parent's children stand: what was asked of the loader, and what it said. */
@@ -189,6 +216,21 @@ export interface TreeNode<TItem extends TreeItem = TreeItem> {
189
216
  * cannot be checked or unchecked as a whole (see `TreeStore.toggle`), and never shows checked unless the value names it.
190
217
  */
191
218
  partial: boolean;
219
+ /**
220
+ * A row of the filtered tree (`TreeStore.setFilter`): a leaf the filter shows, a parent holding one, a draft, or a lazy
221
+ * item whose children are still to load. Always true without a filter.
222
+ */
223
+ shown: boolean;
224
+ /** Children that are `shown`: all of them without a filter. */
225
+ shownChildren: number;
226
+ /**
227
+ * `leaves`, `checkedLeaves`, `openLeaves` and `openChecked` over the leaves the filter shows - what a box, a count and a
228
+ * check of a parent work on while a filter is on. The same numbers without a filter. The value never reads them.
229
+ */
230
+ shownLeaves: number;
231
+ shownChecked: number;
232
+ shownOpenLeaves: number;
233
+ shownOpenChecked: number;
192
234
  /**
193
235
  * Set only on a status row, a row that is not an item: under its `parent` (`null`: the top level) a page is on its
194
236
  * way, failed, or more can be loaded. A status row is in `rows` only - never in `children`, the value, or the items -
@@ -252,7 +294,23 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
252
294
  private openNodes;
253
295
  private openCheckedNodes;
254
296
  private indexCache;
297
+ /** The leaves shown (`setFilter`); `null`: all. */
298
+ private match;
299
+ /** Goes up whenever a node's counts are worked out again, so what is kept from them knows it is old. */
300
+ private counted;
301
+ private shownRoots;
255
302
  constructor(items?: readonly TItem[], options?: TreeStoreOptions<TItem>, value?: readonly TreeValue[]);
303
+ /** Whether a filter is on (`setFilter`). */
304
+ get filtered(): boolean;
305
+ /**
306
+ * Shows only the leaves `match` takes, and the parents that hold one (`null`: every item). Nothing is checked or
307
+ * unchecked by it, and the value is the same whatever filter is on. While one is on, a parent's box, its count, and a
308
+ * check of it - or of "select all" - work on the leaves shown only: checking a parent checks its shown leaves, and a
309
+ * leaf the filter hides keeps its check exactly as it was. Expanded items stay expanded.
310
+ */
311
+ setFilter(match: TreeFilterMatch<TItem> | null): void;
312
+ /** Children of a parent (`null`: the top level) that are rows of the filtered tree: all of them without a filter. */
313
+ shownChildCount(parent: TreeNode<TItem> | null): number;
256
314
  /** Items in the tree, drafts included. */
257
315
  get size(): number;
258
316
  /** The node of a value. */
@@ -280,11 +338,19 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
280
338
  * unloaded items are not in the value - unless the value names it (`valueMode: "checked"`, where it stands for all).
281
339
  */
282
340
  state(node: TreeNode<TItem>): TreeCheckState;
283
- /** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. */
341
+ /** The state of a parent over all of its leaves, whatever the filter shows. */
342
+ private wholeState;
343
+ private stateOver;
344
+ /** Checked boxes and all boxes: leaves with `cascade`, every item without it. Drafts count. The filter does not narrow it. */
284
345
  counts(): {
285
346
  checked: number;
286
347
  total: number;
287
348
  };
349
+ /** `counts` over what the filter shows: the leaves shown with `cascade`, the rows of items without it. */
350
+ shownCounts(): {
351
+ checked: number;
352
+ total: number;
353
+ };
288
354
  /**
289
355
  * The checked values, in tree order, following `valueMode`. In a lazy tree, values whose items are not loaded yet
290
356
  * (or not settled) follow the others, so a saved value survives until its items come.
@@ -306,16 +372,19 @@ export declare class TreeStore<TItem extends TreeItem = TreeItem> {
306
372
  * silently leave the rest out. The items under it whose own items are all loaded can still be checked one by one.
307
373
  */
308
374
  toggle(node: TreeNode<TItem>): boolean;
309
- /** Checks or unchecks a node; with `cascade`, every item under it that is not locked. Returns whether anything changed. */
375
+ /**
376
+ * Checks or unchecks a node; with `cascade`, every item under it that is not locked - and, while a filter is on, that
377
+ * the filter shows: the hidden ones keep their checks. Returns whether anything changed.
378
+ */
310
379
  setChecked(node: TreeNode<TItem>, checked: boolean): boolean;
311
- /** The state of a "select all" box, over the items that are not locked; `null` when every item is locked. */
380
+ /** The state of a "select all" box, over the items that are not locked (and shown); `null` when every one is locked. */
312
381
  allState(): TreeCheckState | null;
313
382
  /** What a click on a "select all" box does: checks every item that is not locked, or unchecks them all when all are checked. */
314
383
  toggleAll(): boolean;
315
384
  /**
316
- * Checks or unchecks every item that is not locked. Returns whether anything changed. In a lazy tree with `cascade`,
317
- * a node whose items are not all loaded is passed over and the loaded parts under it are changed; values whose
318
- * items are not loaded stay as they are.
385
+ * Checks or unchecks every item that is not locked, and that the filter shows. Returns whether anything changed. In a
386
+ * lazy tree with `cascade`, a node whose items are not all loaded is passed over and the loaded parts under it are
387
+ * changed; values whose items are not loaded stay as they are.
319
388
  */
320
389
  checkAll(checked: boolean): boolean;
321
390
  /** Whether every parent of the node is expanded, so the node is a row. */