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.
- package/CHANGELOG.md +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- 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,
|
|
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,
|
|
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
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
24
|
-
names.add(
|
|
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
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
243
|
-
*
|
|
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 =
|
|
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) *
|
|
257
|
-
const answer = await load({ search: "", page, skip, pageSize:
|
|
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 >=
|
|
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
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and the box says what to pick first. Params without
|
|
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
|
|
4
|
-
* pure, so Node tests run it); the form's
|
|
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
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* and the box says what to pick first. Params without
|
|
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
|
|
50
|
+
* @example narrowInputParamsBy(tagsParams, narrowing => ({ value: narrowing.from === "form" ? form.get(narrowing.name) : row[narrowing.name] }))
|
|
38
51
|
*/
|
|
39
|
-
export function
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
52
|
+
export function narrowInputParamsBy(params, read) {
|
|
53
|
+
const narrowings = narrowingsOf(params);
|
|
54
|
+
if (narrowings.length === 0)
|
|
42
55
|
return params;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
55
|
-
const
|
|
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
|
-
|
|
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
|
+
}
|