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
@@ -1,5 +1,6 @@
1
1
  import { deleteFormDraft, readFormDraft, writeFormDraft } from "./FormDrafts.js";
2
2
  import { isComputedInput } from "./InputConditions.js";
3
+ import { formNarrowingNames, inputNarrowingsOf, isEmptyNarrowed, narrowedFollowers } from "./InputNarrowing.js";
3
4
  import { findRememberedOpening, forgetInputMemory, isEmptyInputValue, isPickerInput, readInputMemory, rememberDependencies, rememberedOpeningNow, writeInputMemory } from "./InputMemory.js";
4
5
  import { holdsValue, initialInputValue, inputValuesEqual, isRowsInput, keepsTyping, normalizeInputValue, validateInput } from "./InputParamsHelpers.js";
5
6
  const INITIAL_STATUS = {
@@ -376,6 +377,21 @@ export class FormStore {
376
377
  setValues(values) {
377
378
  this.change(Object.entries(values), "code");
378
379
  }
380
+ /**
381
+ * Values a person set through another input - one worked out from them and typed in (`compute.solve`) - as one change.
382
+ * Their inputs show them at once, as values from code do, and they are taken as the person's own: the form's
383
+ * `onFieldChange` runs for them, and inputs narrowed by them lose what they picked. Nothing is taken while the values
384
+ * load. Answers whether anything changed.
385
+ */
386
+ setFromPerson(values) {
387
+ if (this.status.loading) {
388
+ this.refusedWhileLoading = true;
389
+ return false;
390
+ }
391
+ const before = this.values;
392
+ this.change(Object.entries(values), "person");
393
+ return this.values !== before;
394
+ }
379
395
  /**
380
396
  * A value the user typed or picked, from the input rendered with `stamp`, with what a choice input reported beside it
381
397
  * (`item`: a dropdown's option, kept for `pickedItem`). An input that was replaced since (by a value from code, a load,
@@ -812,7 +828,11 @@ export class FormStore {
812
828
  this.changedNames.add(name);
813
829
  }
814
830
  }
815
- change(entries, source) {
831
+ change(entries, changedBy) {
832
+ // A person's change through another input (`setFromPerson`) is written as code writes (its inputs mount again with
833
+ // it) and followed as typing is.
834
+ const source = changedBy === "code" ? "code" : "input";
835
+ const writtenAs = changedBy === "input" ? "input" : "code";
816
836
  const crossBefore = this.crossErrors;
817
837
  const shown = new Map();
818
838
  const remounted = new Set();
@@ -850,7 +870,7 @@ export class FormStore {
850
870
  written.push(name);
851
871
  };
852
872
  for (const [name, value] of entries)
853
- write(name, value, source);
873
+ write(name, value, writtenAs);
854
874
  if (!written.length)
855
875
  return;
856
876
  // Values derived from the ones the user changed, such as an amount from a quantity and a rate.
@@ -869,20 +889,37 @@ export class FormStore {
869
889
  // chain too: a Country change clears the Region, and with it the District picked among that Region's (each input
870
890
  // once, whatever order the form lists them in; one the person set in this same change keeps its value).
871
891
  if (source === "input") {
872
- const changed = new Set(written);
873
- let cleared = true;
874
- while (cleared) {
875
- cleared = false;
876
- for (const params of this.options.inputs) {
877
- const other = params.narrowedBy?.name;
878
- if (!other || !changed.has(other) || changed.has(params.name))
879
- continue;
880
- changed.add(params.name);
881
- cleared = true;
882
- const kept = this.values[params.name];
883
- if (kept !== undefined && kept !== null && kept !== "" && !(Array.isArray(kept) && kept.length === 0))
884
- write(params.name, null, "code");
885
- }
892
+ const followers = narrowedFollowers(this.options.inputs, written, "form");
893
+ for (const name of followers) {
894
+ if (!isEmptyNarrowed(this.values[name]))
895
+ write(name, null, "code");
896
+ }
897
+ // Rows whose inputs follow a form value (`from: "form"`, a line's Tags of the form's Store): every row loses what
898
+ // it picked among the old value's options, and down the row's own chain.
899
+ const changedNow = new Set([...written, ...followers]);
900
+ for (const params of this.options.inputs) {
901
+ if (!isRowsInput(params))
902
+ continue;
903
+ const rowInputs = params.inputs ?? [];
904
+ if (!formNarrowingNames(rowInputs).some(name => changedNow.has(name)))
905
+ continue;
906
+ const clear = narrowedFollowers(rowInputs, changedNow, "rows-of-form");
907
+ const rows = this.values[params.name];
908
+ if (!clear.length || !Array.isArray(rows))
909
+ continue;
910
+ let touched = false;
911
+ const next = rows.map(row => {
912
+ if (row === null || typeof row !== "object")
913
+ return row;
914
+ const filled = clear.filter(name => !isEmptyNarrowed(row[name]));
915
+ if (!filled.length)
916
+ return row;
917
+ touched = true;
918
+ // Each cleared as its input holds nothing: an empty list for a picker of several.
919
+ return { ...row, ...Object.fromEntries(filled.map(name => [name, emptyRowValue(rowInputs, name)])) };
920
+ });
921
+ if (touched)
922
+ write(params.name, next, "code");
886
923
  }
887
924
  }
888
925
  // Inputs that follow a changed one (`dependsOn`) draw again with their params read afresh, and their rule is asked
@@ -900,20 +937,25 @@ export class FormStore {
900
937
  if (remounted.has(name) || followers.has(name) || unasked.includes(name) || this.shownError(name) !== before)
901
938
  this.bump(name);
902
939
  this.patchStatus({ changed: this.changedNames.size > 0, message: this.formMessage(), problems: this.problemList() });
903
- this.emit(source === "input" ? written[0] : written.length === 1 ? written[0] : null);
940
+ this.emit(changedBy === "input" ? written[0] : written.length === 1 ? written[0] : null);
904
941
  // A change after a cancel is worth keeping again.
905
942
  if (source === "input")
906
943
  this.draftDropped = false;
907
944
  this.scheduleDraft();
908
945
  }
909
- /** The inputs whose `dependsOn` (or `narrowedBy`) names any of `names`. */
946
+ /**
947
+ * The inputs whose `dependsOn` (or `narrowedBy`) names any of `names`, and the rows inputs whose row inputs are narrowed
948
+ * by one of them `from: "form"`: their rows draw again with the form's value as it is now.
949
+ */
910
950
  followersOf(names) {
911
951
  const found = new Set();
912
952
  if (!names.length)
913
953
  return found;
914
954
  const changed = new Set(names);
915
955
  for (const params of this.options.inputs) {
916
- if (params.dependsOn?.some(name => changed.has(name)) || (params.narrowedBy?.name !== undefined && changed.has(params.narrowedBy.name)))
956
+ if (params.dependsOn?.some(name => changed.has(name)) || inputNarrowingsOf(params).some(narrowing => changed.has(narrowing.name)))
957
+ found.add(params.name);
958
+ else if (isRowsInput(params) && formNarrowingNames(params.inputs ?? []).some(name => changed.has(name)))
917
959
  found.add(params.name);
918
960
  }
919
961
  return found;
@@ -1066,3 +1108,8 @@ export class FormStore {
1066
1108
  this.patchStatus({ restored: { savedAt: draft.savedAt, names: entries.map(([name]) => name), conflicts }, draftSavedAt: draft.savedAt });
1067
1109
  }
1068
1110
  }
1111
+ /** A row input's value when nothing is picked: as the rows hold it (an empty list for a picker of several). */
1112
+ function emptyRowValue(rowInputs, name) {
1113
+ const params = rowInputs.find(input => input.name === name);
1114
+ return params ? normalizeInputValue(params, null) : null;
1115
+ }
@@ -35,6 +35,12 @@ export declare function spanFor(spanWhen: InputSpanWhen | undefined, read: (name
35
35
  * @example withInputConditions(inputs, name => store.get(name))
36
36
  */
37
37
  export declare function withInputConditions(inputs: readonly InputParams[], read: (name: string) => unknown): readonly InputParams[];
38
+ /**
39
+ * The inputs with each worked-out one's `dependsOn` joined by the inputs it is worked out from (`compute.from`), so what
40
+ * follows `dependsOn` - a row's cells (`RowsStore`), as a form's do through `withInputConditions` - draws it again when
41
+ * one of them changes. The same array when none is worked out from another, and the same copy for the same array.
42
+ */
43
+ export declare function withComputeSources(inputs: readonly InputParams[]): readonly InputParams[];
38
44
  /** Whether the input is worked out from others (`compute`): it holds no value of the form's and is never typed in. */
39
45
  export declare function isComputedInput(params: InputParams | undefined): boolean;
40
46
  /**
@@ -129,6 +129,30 @@ export function withInputConditions(inputs, read) {
129
129
  });
130
130
  }
131
131
  /* Inputs worked out from others (`compute`) */
132
+ const joinedSources = new WeakMap();
133
+ /**
134
+ * The inputs with each worked-out one's `dependsOn` joined by the inputs it is worked out from (`compute.from`), so what
135
+ * follows `dependsOn` - a row's cells (`RowsStore`), as a form's do through `withInputConditions` - draws it again when
136
+ * one of them changes. The same array when none is worked out from another, and the same copy for the same array.
137
+ */
138
+ export function withComputeSources(inputs) {
139
+ if (!inputs.some(params => params.compute?.from.length))
140
+ return inputs;
141
+ let joined = joinedSources.get(inputs);
142
+ if (!joined) {
143
+ joined = inputs.map(params => {
144
+ if (!params.compute?.from.length)
145
+ return params;
146
+ // Descriptors, never a spread: a getter (`get readOnly()`) is still read as the input draws.
147
+ const copy = Object.defineProperties({}, Object.getOwnPropertyDescriptors(params));
148
+ const follows = [...new Set([...(params.dependsOn ?? []), ...params.compute.from])];
149
+ Object.defineProperty(copy, "dependsOn", { value: follows, enumerable: true, configurable: true, writable: true });
150
+ return copy;
151
+ });
152
+ joinedSources.set(inputs, joined);
153
+ }
154
+ return joined;
155
+ }
132
156
  /** Whether the input is worked out from others (`compute`): it holds no value of the form's and is never typed in. */
133
157
  export function isComputedInput(params) {
134
158
  return Boolean(params?.compute);
@@ -1,7 +1,7 @@
1
1
  import { EInputTypes } from "../Interfaces/FormInterfaces.js";
2
2
  import { isDisabledItem, isInactiveItem, optionKey, readField } from "./DropdownHelpers.js";
3
3
  import { conditionsHold, conditionsOf } from "./InputConditions.js";
4
- import { narrowInputParams, narrowingValue } from "./InputNarrowing.js";
4
+ import { inputNarrowingsOf, narrowInputParamsBy, narrowingsOf, narrowingValue } from "./InputNarrowing.js";
5
5
  import { emptyInputValue, inputValuesEqual } from "./InputParamsHelpers.js";
6
6
  /*
7
7
  * An input's last saved value, kept in this browser per viewer (`InputLayoutParams.remember`): what a form opens an
@@ -20,8 +20,8 @@ const readNothing = () => undefined;
20
20
  */
21
21
  export function rememberDependencies(params) {
22
22
  const names = new Set();
23
- if (params.narrowedBy?.name !== undefined)
24
- names.add(params.narrowedBy.name);
23
+ for (const narrowing of inputNarrowingsOf(params))
24
+ names.add(narrowing.name);
25
25
  if (params.remember?.per)
26
26
  names.add(params.remember.per);
27
27
  for (const condition of conditionsOf(params.remember?.when))
@@ -145,15 +145,16 @@ export function offeredInputValue(params, value) {
145
145
  export function isPickerInput(params) {
146
146
  return params.type === EInputTypes.Dropdown && !listedOptions(params) && typeof params.loadOptions === "function";
147
147
  }
148
- /** The narrowing an input's options follow among these values (`narrowedBy`), or null when they follow none. */
149
- function narrowingAmong(params, read) {
150
- const narrowing = params.narrowedBy;
151
- if (!narrowing)
152
- return null;
153
- const fixed = narrowing.name === undefined;
154
- const value = narrowingValue(fixed ? narrowing.value : read(narrowing.name));
155
- return { key: narrowing.key ?? narrowing.name ?? "", value, orUnset: narrowing.orUnset === true };
148
+ /** The narrowings an input's options follow among these values (`narrowedBy`, one or several): none when they follow none. */
149
+ function narrowingsAmong(params, read) {
150
+ return narrowingsOf(params).map(narrowing => {
151
+ const fixed = narrowing.name === undefined;
152
+ const value = narrowingValue(fixed ? narrowing.value : read(narrowing.name));
153
+ return { key: narrowing.key ?? narrowing.name ?? "", value, orUnset: narrowing.orUnset === true };
154
+ });
156
155
  }
156
+ /** The input's params narrowed among these values, as the form narrows them as it draws. */
157
+ const narrowedAmong = (params, read) => narrowInputParamsBy(params, narrowing => ({ value: read(narrowing.name) }));
157
158
  const GONE = { status: "gone" };
158
159
  const UNKNOWN = { status: "unknown" };
159
160
  /** A value as its keys: a list's items, or the one value. */
@@ -182,15 +183,20 @@ function pickable(params, record) {
182
183
  function offersRecord(params, record, read) {
183
184
  if (!pickable(params, record))
184
185
  return false;
185
- const narrowing = narrowingAmong(params, read);
186
- if (!narrowing)
187
- return true;
188
- if (narrowing.value === null && !narrowing.orUnset)
189
- return false;
190
- if (!(narrowing.key in record))
191
- return undefined;
192
- const field = record[narrowing.key];
193
- return (narrowing.value !== null && String(field) === String(narrowing.value)) || (narrowing.orUnset && (field === null || field === undefined || field === ""));
186
+ // Inside every narrowing: false at the first it falls outside, undefined when one cannot be told from the record.
187
+ let told = true;
188
+ for (const narrowing of narrowingsAmong(params, read)) {
189
+ if (narrowing.value === null && !narrowing.orUnset)
190
+ return false;
191
+ if (!(narrowing.key in record)) {
192
+ told = undefined;
193
+ continue;
194
+ }
195
+ const field = record[narrowing.key];
196
+ if (!((narrowing.value !== null && String(field) === String(narrowing.value)) || (narrowing.orUnset && (field === null || field === undefined || field === ""))))
197
+ return false;
198
+ }
199
+ return told;
194
200
  }
195
201
  /** The records among `items` by key, each judged: true offered, false not, undefined not to be told from the record. */
196
202
  function judge(params, items, keys, read) {
@@ -212,11 +218,10 @@ export function keptStandingNow(params, value, read = readNothing) {
212
218
  if (isEmptyInputValue(params, value))
213
219
  return GONE;
214
220
  // Nothing to narrow by yet (the other input is empty): judged once it has a value, so neither opened nor forgotten.
215
- const narrowing = narrowingAmong(params, read);
216
- if (narrowing && narrowing.value === null && !narrowing.orUnset)
221
+ if (narrowingsAmong(params, read).some(narrowing => narrowing.value === null && !narrowing.orUnset))
217
222
  return UNKNOWN;
218
223
  if (!isPickerInput(params)) {
219
- const offered = offeredInputValue(narrowInputParams(params, params.narrowedBy?.name === undefined ? undefined : read(params.narrowedBy.name)), value);
224
+ const offered = offeredInputValue(narrowedAmong(params, read), value);
220
225
  return offered === undefined ? GONE : { status: "offered", value: offered };
221
226
  }
222
227
  const peek = params.peekOption;
@@ -230,7 +235,10 @@ export function keptStandingNow(params, value, read = readNothing) {
230
235
  const offered = offeredPart(value, new Set(keys.filter(key => judged.get(key) === true)));
231
236
  return offered === undefined ? GONE : { status: "offered", value: offered };
232
237
  }
233
- /** The page size a kept value is looked for in, when the picker cannot name a record by its key. */
238
+ /**
239
+ * The page size a kept value is looked for in, when the picker cannot name a record by its key and gives no `pageSize`
240
+ * of its own: the box's default.
241
+ */
234
242
  const FIND_PAGE_SIZE = 50;
235
243
  /**
236
244
  * Pages looked through for a kept value. A key not seen in them is no longer offered only when the list ended there; a
@@ -239,22 +247,24 @@ const FIND_PAGE_SIZE = 50;
239
247
  const FIND_PAGES = 20;
240
248
  /**
241
249
  * The keys the picker's own pages (narrowed as the form narrows them) offer, read until each key showed or they end.
242
- * A page ends the list as the box itself reads one: `hasMore: false`, an empty page, or (no `hasMore`) fewer items than
243
- * the page size asked - the box would offer nothing past it either.
250
+ * The pages are asked in the box's own size (its `pageSize`), and one ends the list as the box itself reads it:
251
+ * `hasMore: false`, an empty page, or (no `hasMore`) fewer items than that size - the box would offer nothing past it
252
+ * either. So a loader that pages by the size it is asked, or by the box's, ends where the box's list ends.
244
253
  */
245
254
  async function offeredInPages(params, keys, read, signal) {
246
- const narrowed = narrowInputParams(params, params.narrowedBy?.name === undefined ? undefined : read(params.narrowedBy.name));
255
+ const narrowed = narrowedAmong(params, read);
247
256
  const load = narrowed.loadOptions;
248
257
  const offered = new Set();
249
258
  if (!load)
250
259
  return { offered, ended: true };
251
260
  const valueKey = narrowed.valueKey ?? "value";
261
+ const size = typeof narrowed.pageSize === "number" && narrowed.pageSize >= 1 ? Math.floor(narrowed.pageSize) : FIND_PAGE_SIZE;
252
262
  const wanted = new Set(keys);
253
263
  for (let page = 1; page <= FIND_PAGES && wanted.size; page++) {
254
264
  if (signal.aborted)
255
265
  break;
256
- const skip = (page - 1) * FIND_PAGE_SIZE;
257
- const answer = await load({ search: "", page, skip, pageSize: FIND_PAGE_SIZE, signal });
266
+ const skip = (page - 1) * size;
267
+ const answer = await load({ search: "", page, skip, pageSize: size, signal });
258
268
  for (const item of answer.items) {
259
269
  const key = optionKey(item[valueKey]);
260
270
  if (!wanted.has(key))
@@ -264,7 +274,7 @@ async function offeredInPages(params, keys, read, signal) {
264
274
  if (pickable(params, item))
265
275
  offered.add(key);
266
276
  }
267
- const more = answer.hasMore ?? answer.items.length >= FIND_PAGE_SIZE;
277
+ const more = answer.hasMore ?? answer.items.length >= size;
268
278
  if (!more || answer.items.length === 0)
269
279
  return { offered, ended: true };
270
280
  }
@@ -1,18 +1,50 @@
1
- import type { InputNarrowing, InputParams } from "../Interfaces/FormInterfaces.ts";
1
+ import type { InputNarrowing, InputNarrowingByInput, InputParams } from "../Interfaces/FormInterfaces.ts";
2
2
  import type { OptionValue } from "../Interfaces/InputInterfaces.ts";
3
3
  /** The value that narrows, or null while the other input has none: an empty text, an empty list and `false` count as none. */
4
4
  export declare function narrowingValue(value: unknown): OptionValue | null;
5
+ /** Every narrowing of an input's options: none, one, or several at once (a list). */
6
+ export declare function narrowingsOf(params: Pick<InputParams, "narrowedBy">): readonly InputNarrowing[];
7
+ /** The narrowings that follow another input (not a fixed value). */
8
+ export declare function inputNarrowingsOf(params: Pick<InputParams, "narrowedBy">): readonly InputNarrowingByInput[];
5
9
  /** What a narrowed box says while the other input is empty: "Pick a group first". */
6
10
  export declare function narrowingPlaceholder(narrowing: InputNarrowing, otherLabel: unknown): string;
7
11
  /** A narrowing by a fixed value (`{ key, value }`), not by another input. */
8
12
  export declare function isFixedNarrowing(narrowing: InputNarrowing): boolean;
13
+ /** What a narrowing by another input reads: that input's value now, and its label for "Pick a store first". */
14
+ export type NarrowingRead = (narrowing: InputNarrowingByInput) => {
15
+ value: unknown;
16
+ label?: unknown;
17
+ };
9
18
  /**
10
- * An input's params narrowed by the other input's value as it is now (or by the fixed `value` the narrowing gives): static `options` filtered to the records whose
11
- * `key` field holds it; `loadOptions` asked with `request.narrow` (crudPicker reads it: from the server when its
12
- * endpoint takes the parent, else from the list it keeps, narrowed in the browser), and a `loadKey` of its own per
13
- * value, so the list loads afresh when the other value changes. While the other input is empty, no option is offered
14
- * and the box says what to pick first. Params without `narrowedBy` are answered as they are.
19
+ * An input's params narrowed by the other inputs' values as they are now (and by the fixed values its narrowings give):
20
+ * static `options` filtered to the records whose fields hold every value; `loadOptions` asked with `request.narrow` (the
21
+ * first) and, for several, `request.narrows` (every one) - crudPicker reads them: from the server when its endpoint takes
22
+ * them, else from the list it keeps, narrowed in the browser - and a `loadKey` of its own per values, so the list loads
23
+ * afresh when any changes. While one is empty no option is offered and the box says what to pick first. Params without
24
+ * `narrowedBy` are answered as they are.
25
+ *
26
+ * @example narrowInputParamsBy(tagsParams, narrowing => ({ value: narrowing.from === "form" ? form.get(narrowing.name) : row[narrowing.name] }))
27
+ */
28
+ export declare function narrowInputParamsBy(params: InputParams, read: NarrowingRead): InputParams;
29
+ /**
30
+ * An input's params narrowed by one other input's value as it is now (or by the fixed `value` its narrowing gives), as
31
+ * `narrowInputParamsBy` does: every narrowing by an input reads `otherValue`. For several other inputs, use that.
15
32
  *
16
33
  * @example narrowInputParams(packageParams, form.get("subscriptionGroupId"), "Group")
17
34
  */
18
35
  export declare function narrowInputParams(params: InputParams, otherValue: unknown, otherLabel?: unknown): InputParams;
36
+ /** Whether a narrowed input holds nothing to clear: no value, an empty text or an empty list. */
37
+ export declare function isEmptyNarrowed(value: unknown): boolean;
38
+ /** The inputs of the form around rows that the rows' inputs narrow by (`from: "form"`): the document's Store. */
39
+ export declare function formNarrowingNames(rowInputs: readonly InputParams[]): string[];
40
+ /**
41
+ * The inputs whose value a person's change of `changed` clears: those narrowed by one of them, and down the chain (a
42
+ * Country clears the Region, and with it the District picked among that Region's), each once, in the inputs' order;
43
+ * one in `changed` itself keeps its value. Where the change is:
44
+ *
45
+ * - `form`: in a form, its inputs' narrowings by name;
46
+ * - `row`: in a row, the row's own (`from` left out, or `"row"`);
47
+ * - `rows-of-form`: in every row, after a change of the form around them - first the row inputs narrowed `from: "form"`
48
+ * by one of `changed`, then down the row's own chain.
49
+ */
50
+ export declare function narrowedFollowers(inputs: readonly InputParams[], changed: Iterable<string>, where: "form" | "row" | "rows-of-form"): string[];
@@ -1,7 +1,8 @@
1
1
  /*
2
2
  * Options narrowed by another input's value (`narrowedBy`): a Package list of the Group picked, a Sub-region list of the
3
- * Country picked. The form reads the other value as it draws and hands the input its params narrowed by it (this file,
4
- * pure, so Node tests run it); the form's store clears the input's value when the other one changes (FormStore).
3
+ * Country picked, a line's asset Tags of the line's Item in the form's Store. The form (or the rows) reads the other
4
+ * values as it draws and hands the input its params narrowed by them (this file, pure, so Node tests run it); the form's
5
+ * store, and the rows' for a row's own, clear a value when a person changes one it follows (`narrowedFollowers`).
5
6
  */
6
7
  /** The value that narrows, or null while the other input has none: an empty text, an empty list and `false` count as none. */
7
8
  export function narrowingValue(value) {
@@ -13,6 +14,17 @@ export function narrowingValue(value) {
13
14
  return value;
14
15
  return null;
15
16
  }
17
+ /** Every narrowing of an input's options: none, one, or several at once (a list). */
18
+ export function narrowingsOf(params) {
19
+ const given = params.narrowedBy;
20
+ if (!given)
21
+ return [];
22
+ return Array.isArray(given) ? given : [given];
23
+ }
24
+ /** The narrowings that follow another input (not a fixed value). */
25
+ export function inputNarrowingsOf(params) {
26
+ return narrowingsOf(params).filter((narrowing) => narrowing.name !== undefined);
27
+ }
16
28
  /** What a narrowed box says while the other input is empty: "Pick a group first". */
17
29
  export function narrowingPlaceholder(narrowing, otherLabel) {
18
30
  if (narrowing.placeholder)
@@ -28,40 +40,109 @@ export function isFixedNarrowing(narrowing) {
28
40
  return narrowing.name === undefined;
29
41
  }
30
42
  /**
31
- * An input's params narrowed by the other input's value as it is now (or by the fixed `value` the narrowing gives): static `options` filtered to the records whose
32
- * `key` field holds it; `loadOptions` asked with `request.narrow` (crudPicker reads it: from the server when its
33
- * endpoint takes the parent, else from the list it keeps, narrowed in the browser), and a `loadKey` of its own per
34
- * value, so the list loads afresh when the other value changes. While the other input is empty, no option is offered
35
- * and the box says what to pick first. Params without `narrowedBy` are answered as they are.
43
+ * An input's params narrowed by the other inputs' values as they are now (and by the fixed values its narrowings give):
44
+ * static `options` filtered to the records whose fields hold every value; `loadOptions` asked with `request.narrow` (the
45
+ * first) and, for several, `request.narrows` (every one) - crudPicker reads them: from the server when its endpoint takes
46
+ * them, else from the list it keeps, narrowed in the browser - and a `loadKey` of its own per values, so the list loads
47
+ * afresh when any changes. While one is empty no option is offered and the box says what to pick first. Params without
48
+ * `narrowedBy` are answered as they are.
36
49
  *
37
- * @example narrowInputParams(packageParams, form.get("subscriptionGroupId"), "Group")
50
+ * @example narrowInputParamsBy(tagsParams, narrowing => ({ value: narrowing.from === "form" ? form.get(narrowing.name) : row[narrowing.name] }))
38
51
  */
39
- export function narrowInputParams(params, otherValue, otherLabel) {
40
- const narrowing = params.narrowedBy;
41
- if (!narrowing)
52
+ export function narrowInputParamsBy(params, read) {
53
+ const narrowings = narrowingsOf(params);
54
+ if (narrowings.length === 0)
42
55
  return params;
43
- const fixed = narrowing.name === undefined;
44
- const key = narrowing.key ?? narrowing.name ?? "";
45
- const value = narrowingValue(fixed ? narrowing.value : otherValue);
46
- const orUnset = narrowing.orUnset === true;
56
+ const parts = narrowings.map(narrowing => {
57
+ const other = narrowing.name === undefined ? null : read(narrowing);
58
+ return {
59
+ narrowing,
60
+ key: narrowing.key ?? narrowing.name ?? "",
61
+ value: narrowingValue(other === null ? narrowing.value : other.value),
62
+ orUnset: narrowing.orUnset === true,
63
+ label: other?.label,
64
+ };
65
+ });
47
66
  const keeps = (item) => {
48
67
  if (item === null || typeof item !== "object")
49
68
  return false;
50
- const field = item[key];
51
- return (value !== null && String(field) === String(value)) || (orUnset && (field === null || field === undefined || field === ""));
69
+ return parts.every(({ key, value, orUnset }) => {
70
+ const field = item[key];
71
+ return (value !== null && String(field) === String(value)) || (orUnset && (field === null || field === undefined || field === ""));
72
+ });
52
73
  };
53
74
  const own = params;
54
- const narrowed = { ...own, loadKey: `${own.loadKey === undefined ? "" : String(own.loadKey)}|${narrowing.name ?? key}=${value === null ? "" : String(value)}` };
55
- const placeholder = value === null ? narrowingPlaceholder(narrowing, otherLabel) : "";
75
+ const keyed = parts.map(part => `|${part.narrowing.name ?? part.key}=${part.value === null ? "" : String(part.value)}`).join("");
76
+ const narrowed = { ...own, loadKey: `${own.loadKey === undefined ? "" : String(own.loadKey)}${keyed}` };
77
+ // The first narrowing with nothing to narrow by says what to pick first.
78
+ const waiting = parts.find(part => part.value === null);
79
+ const placeholder = waiting ? narrowingPlaceholder(waiting.narrowing, waiting.label) : "";
56
80
  if (placeholder)
57
81
  narrowed.placeholder = placeholder;
58
82
  // Nothing to narrow by offers nothing, unless the options with no value in the field are offered too (`orUnset`).
59
- const none = value === null && !orUnset;
83
+ const none = parts.some(part => part.value === null && !part.orUnset);
60
84
  if (Array.isArray(own.options))
61
85
  narrowed.options = none ? [] : own.options.filter(keeps);
62
86
  const load = own.loadOptions;
63
87
  if (load) {
64
- narrowed.loadOptions = (request) => (none ? Promise.resolve(EMPTY_PAGE) : load({ ...request, narrow: { key, value, ...(orUnset && { orUnset }) } }));
88
+ const asked = parts.map(({ key, value, orUnset }) => ({ key, value, ...(orUnset && { orUnset }) }));
89
+ narrowed.loadOptions = (request) => (none ? Promise.resolve(EMPTY_PAGE) : load({ ...request, narrow: asked[0], ...(asked.length > 1 && { narrows: asked }) }));
65
90
  }
66
91
  return narrowed;
67
92
  }
93
+ /**
94
+ * An input's params narrowed by one other input's value as it is now (or by the fixed `value` its narrowing gives), as
95
+ * `narrowInputParamsBy` does: every narrowing by an input reads `otherValue`. For several other inputs, use that.
96
+ *
97
+ * @example narrowInputParams(packageParams, form.get("subscriptionGroupId"), "Group")
98
+ */
99
+ export function narrowInputParams(params, otherValue, otherLabel) {
100
+ return narrowInputParamsBy(params, () => ({ value: otherValue, label: otherLabel }));
101
+ }
102
+ /** Whether a narrowed input holds nothing to clear: no value, an empty text or an empty list. */
103
+ export function isEmptyNarrowed(value) {
104
+ return value === undefined || value === null || value === "" || (Array.isArray(value) && value.length === 0);
105
+ }
106
+ /** The inputs of the form around rows that the rows' inputs narrow by (`from: "form"`): the document's Store. */
107
+ export function formNarrowingNames(rowInputs) {
108
+ const names = new Set();
109
+ for (const params of rowInputs)
110
+ for (const narrowing of inputNarrowingsOf(params))
111
+ if (narrowing.from === "form")
112
+ names.add(narrowing.name);
113
+ return [...names];
114
+ }
115
+ /**
116
+ * The inputs whose value a person's change of `changed` clears: those narrowed by one of them, and down the chain (a
117
+ * Country clears the Region, and with it the District picked among that Region's), each once, in the inputs' order;
118
+ * one in `changed` itself keeps its value. Where the change is:
119
+ *
120
+ * - `form`: in a form, its inputs' narrowings by name;
121
+ * - `row`: in a row, the row's own (`from` left out, or `"row"`);
122
+ * - `rows-of-form`: in every row, after a change of the form around them - first the row inputs narrowed `from: "form"`
123
+ * by one of `changed`, then down the row's own chain.
124
+ */
125
+ export function narrowedFollowers(inputs, changed, where) {
126
+ const fromForm = where === "rows-of-form" ? new Set(changed) : new Set();
127
+ const done = where === "rows-of-form" ? new Set() : new Set(changed);
128
+ const followers = [];
129
+ let grew = true;
130
+ while (grew) {
131
+ grew = false;
132
+ for (const params of inputs) {
133
+ if (done.has(params.name))
134
+ continue;
135
+ const follows = inputNarrowingsOf(params).some(narrowing => {
136
+ if (where === "form")
137
+ return done.has(narrowing.name);
138
+ return narrowing.from === "form" ? fromForm.has(narrowing.name) : done.has(narrowing.name);
139
+ });
140
+ if (!follows)
141
+ continue;
142
+ done.add(params.name);
143
+ followers.push(params.name);
144
+ grew = true;
145
+ }
146
+ }
147
+ return followers;
148
+ }