nexus-shared 3.2.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 +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -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/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +182 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- 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 +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- 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/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -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/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- 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/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +158 -0
- package/dist/Helpers/FileAdapter.js +173 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- 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 +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -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 +539 -6
- 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 +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { isDisabledItem, isInactiveItem, optionKey, readField } from "./DropdownHelpers.js";
|
|
3
|
+
import { conditionsHold, conditionsOf } from "./InputConditions.js";
|
|
4
|
+
import { inputNarrowingsOf, narrowInputParamsBy, narrowingsOf, narrowingValue } from "./InputNarrowing.js";
|
|
5
|
+
import { emptyInputValue, inputValuesEqual } from "./InputParamsHelpers.js";
|
|
6
|
+
/*
|
|
7
|
+
* An input's last saved value, kept in this browser per viewer (`InputLayoutParams.remember`): what a form opens an
|
|
8
|
+
* input on when its own values leave it empty - the company's preferred value first, then the person's last choice, each
|
|
9
|
+
* only while the input still offers it. A picker (a dropdown whose options come from `loadOptions`) is asked first, by
|
|
10
|
+
* the value's key, whether it still offers it; a kept choice it no longer offers is forgotten. Pure: the storage is
|
|
11
|
+
* passed in, so a test runs on a map.
|
|
12
|
+
*/
|
|
13
|
+
/** Where an input's value is kept: `nx-input:{key}:{viewer}`, or `nx-input:{key}-{per value}:{viewer}` with `per`. */
|
|
14
|
+
export const INPUT_MEMORY_PREFIX = "nx-input:";
|
|
15
|
+
const readNothing = () => undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The other values a remembered input's opening is judged on: the input that narrows its options (`narrowedBy`), the one
|
|
18
|
+
* its value is kept per (`per`), and the ones its `when` names. A picker's late answer opens nothing once any of them
|
|
19
|
+
* changed since it was asked: it answered for values the form no longer holds.
|
|
20
|
+
*/
|
|
21
|
+
export function rememberDependencies(params) {
|
|
22
|
+
const names = new Set();
|
|
23
|
+
for (const narrowing of inputNarrowingsOf(params))
|
|
24
|
+
names.add(narrowing.name);
|
|
25
|
+
if (params.remember?.per)
|
|
26
|
+
names.add(params.remember.per);
|
|
27
|
+
for (const condition of conditionsOf(params.remember?.when))
|
|
28
|
+
names.add(condition.name);
|
|
29
|
+
names.delete(params.name);
|
|
30
|
+
return [...names];
|
|
31
|
+
}
|
|
32
|
+
/** Whether the input remembers among these values: its `when` holds (none: always). */
|
|
33
|
+
export function remembersHere(remember, read = readNothing) {
|
|
34
|
+
if (!remember)
|
|
35
|
+
return false;
|
|
36
|
+
return !remember.when || conditionsHold(remember.when, read);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The storage key of a remembered input, or null when nothing is kept here: no key, no viewer to keep it for, its `when`
|
|
40
|
+
* does not hold, or its `per` input holds no single value. With `per`, the other value ends the key: one value kept per
|
|
41
|
+
* group, `nx-input:subscription-package-{group}:{viewer}`.
|
|
42
|
+
*/
|
|
43
|
+
export function inputMemoryKey(remember, read = readNothing) {
|
|
44
|
+
if (!remember?.key || !remember.viewer || !remembersHere(remember, read))
|
|
45
|
+
return null;
|
|
46
|
+
if (!remember.per)
|
|
47
|
+
return `${INPUT_MEMORY_PREFIX}${remember.key}:${remember.viewer}`;
|
|
48
|
+
const per = narrowingValue(read(remember.per));
|
|
49
|
+
return per === null ? null : `${INPUT_MEMORY_PREFIX}${remember.key}-${String(per)}:${remember.viewer}`;
|
|
50
|
+
}
|
|
51
|
+
/** This browser's local storage, or nothing where it cannot be reached (on a server, storage switched off). */
|
|
52
|
+
export function browserInputMemory() {
|
|
53
|
+
try {
|
|
54
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The event a `SubmitForm` sends its `<form>` once a save went through, before the form resets: the rows inside it keep
|
|
62
|
+
* their remembered values then (`RowsStore.rememberAdded`), as the form's own inputs do.
|
|
63
|
+
*/
|
|
64
|
+
export const FORM_SAVED_EVENT = "nx-form-saved";
|
|
65
|
+
/** The value kept for this input, or undefined: none kept, no viewer, or storage that cannot be read. */
|
|
66
|
+
export function readInputMemory(storage, remember, read = readNothing) {
|
|
67
|
+
const key = inputMemoryKey(remember, read);
|
|
68
|
+
if (!storage || !key)
|
|
69
|
+
return undefined;
|
|
70
|
+
try {
|
|
71
|
+
const text = storage.getItem(key);
|
|
72
|
+
if (!text)
|
|
73
|
+
return undefined;
|
|
74
|
+
const kept = JSON.parse(text);
|
|
75
|
+
return kept && typeof kept === "object" ? kept.value : undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Keeps a saved value for the input; an empty one forgets it. Storage that cannot be written keeps nothing. */
|
|
82
|
+
export function writeInputMemory(storage, params, value, read = readNothing) {
|
|
83
|
+
const key = inputMemoryKey(params.remember, read);
|
|
84
|
+
if (!storage || !key)
|
|
85
|
+
return;
|
|
86
|
+
try {
|
|
87
|
+
if (isEmptyInputValue(params, value))
|
|
88
|
+
storage.removeItem(key);
|
|
89
|
+
else
|
|
90
|
+
storage.setItem(key, JSON.stringify({ value }));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Storage full or blocked: the form works without it.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Forgets the input's kept value: one its list no longer offers. */
|
|
97
|
+
export function forgetInputMemory(storage, remember, read = readNothing) {
|
|
98
|
+
const key = inputMemoryKey(remember, read);
|
|
99
|
+
if (!storage || !key)
|
|
100
|
+
return;
|
|
101
|
+
try {
|
|
102
|
+
storage.removeItem(key);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Blocked: it is asked about again next time.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Whether a value is the input's empty one: nothing, "", an empty list, or a switch that is off. */
|
|
109
|
+
export function isEmptyInputValue(params, value) {
|
|
110
|
+
if (value === undefined || value === null || value === "")
|
|
111
|
+
return true;
|
|
112
|
+
if (Array.isArray(value) && value.length === 0)
|
|
113
|
+
return true;
|
|
114
|
+
return inputValuesEqual(value, emptyInputValue(params));
|
|
115
|
+
}
|
|
116
|
+
/** An input's own list of options, when it holds one: a choice group's, a dropdown's `options` (not a server's pages). */
|
|
117
|
+
function listedOptions(params) {
|
|
118
|
+
const options = params.options;
|
|
119
|
+
return Array.isArray(options) ? options : null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The part of a value the input offers now, or undefined when it offers none of it. An input with a list of its own
|
|
123
|
+
* (radio and checkbox groups, a dropdown's `options`) must list it, neither disabled nor inactive; a list of values
|
|
124
|
+
* keeps the ones it lists. An input whose options come from a server, or that has no options, takes any value here: a
|
|
125
|
+
* picker is asked by `keptStandingNow` / `findKeptStanding`.
|
|
126
|
+
*/
|
|
127
|
+
export function offeredInputValue(params, value) {
|
|
128
|
+
if (isEmptyInputValue(params, value))
|
|
129
|
+
return undefined;
|
|
130
|
+
const options = listedOptions(params);
|
|
131
|
+
if (!options)
|
|
132
|
+
return value;
|
|
133
|
+
const valueKey = params.type === EInputTypes.Dropdown ? (params.valueKey ?? "value") : "value";
|
|
134
|
+
const inactive = params.isOptionInactive;
|
|
135
|
+
const offered = new Set(options
|
|
136
|
+
.filter(item => item && typeof item === "object" && !isDisabledItem(item) && !(inactive ? inactive(item) : isInactiveItem(item)))
|
|
137
|
+
.map(item => optionKey(readField(item, valueKey))));
|
|
138
|
+
if (Array.isArray(value)) {
|
|
139
|
+
const kept = value.filter(one => offered.has(optionKey(one)));
|
|
140
|
+
return kept.length ? kept : undefined;
|
|
141
|
+
}
|
|
142
|
+
return offered.has(optionKey(value)) ? value : undefined;
|
|
143
|
+
}
|
|
144
|
+
/** A dropdown whose options come from `loadOptions` and that has no list of its own: it is asked what it offers. */
|
|
145
|
+
export function isPickerInput(params) {
|
|
146
|
+
return params.type === EInputTypes.Dropdown && !listedOptions(params) && typeof params.loadOptions === "function";
|
|
147
|
+
}
|
|
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
|
+
});
|
|
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) }));
|
|
158
|
+
const GONE = { status: "gone" };
|
|
159
|
+
const UNKNOWN = { status: "unknown" };
|
|
160
|
+
/** A value as its keys: a list's items, or the one value. */
|
|
161
|
+
function keysOf(value) {
|
|
162
|
+
return (Array.isArray(value) ? value : [value]).map(optionKey).filter(key => key !== "");
|
|
163
|
+
}
|
|
164
|
+
/** The part of `value` whose keys are offered, in its own shape: the value, a shorter list, or undefined for none. */
|
|
165
|
+
function offeredPart(value, offered) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
const kept = value.filter(one => offered.has(optionKey(one)));
|
|
168
|
+
return kept.length ? kept : undefined;
|
|
169
|
+
}
|
|
170
|
+
return offered.has(optionKey(value)) ? value : undefined;
|
|
171
|
+
}
|
|
172
|
+
/** A record the picker would let a person pick, its narrowing aside: not disabled, not inactive. */
|
|
173
|
+
function pickable(params, record) {
|
|
174
|
+
const own = params;
|
|
175
|
+
return !isDisabledItem(record) && !(own.isOptionInactive ? own.isOptionInactive(record) : isInactiveItem(record));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether a record the picker answered is one it offers among these values: not disabled, not inactive (its
|
|
179
|
+
* `isOptionInactive`, else `inactive`/`isActive: false`), and inside its narrowing - the record's field holds the other
|
|
180
|
+
* input's value (or is empty, with `orUnset`). Undefined when the record does not carry the narrowing's field: only the
|
|
181
|
+
* picker's narrowed pages can tell then.
|
|
182
|
+
*/
|
|
183
|
+
function offersRecord(params, record, read) {
|
|
184
|
+
if (!pickable(params, record))
|
|
185
|
+
return false;
|
|
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;
|
|
200
|
+
}
|
|
201
|
+
/** The records among `items` by key, each judged: true offered, false not, undefined not to be told from the record. */
|
|
202
|
+
function judge(params, items, keys, read) {
|
|
203
|
+
const valueKey = params.valueKey ?? "value";
|
|
204
|
+
const found = new Map();
|
|
205
|
+
for (const key of keys) {
|
|
206
|
+
const record = items.find(item => optionKey(item[valueKey]) === key);
|
|
207
|
+
if (record)
|
|
208
|
+
found.set(key, offersRecord(params, record, read));
|
|
209
|
+
}
|
|
210
|
+
return found;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* How a value stands with the input at once, with no call: an input's own list (narrowed as its `narrowedBy` says among
|
|
214
|
+
* these values) offers it or not; a picker by what it holds already (`peekOption`, fresh only); any other input takes
|
|
215
|
+
* it. Null when a picker must be asked (`findKeptStanding`).
|
|
216
|
+
*/
|
|
217
|
+
export function keptStandingNow(params, value, read = readNothing) {
|
|
218
|
+
if (isEmptyInputValue(params, value))
|
|
219
|
+
return GONE;
|
|
220
|
+
// Nothing to narrow by yet (the other input is empty): judged once it has a value, so neither opened nor forgotten.
|
|
221
|
+
if (narrowingsAmong(params, read).some(narrowing => narrowing.value === null && !narrowing.orUnset))
|
|
222
|
+
return UNKNOWN;
|
|
223
|
+
if (!isPickerInput(params)) {
|
|
224
|
+
const offered = offeredInputValue(narrowedAmong(params, read), value);
|
|
225
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
226
|
+
}
|
|
227
|
+
const peek = params.peekOption;
|
|
228
|
+
if (!peek)
|
|
229
|
+
return null;
|
|
230
|
+
const keys = keysOf(value);
|
|
231
|
+
const records = keys.map(key => peek(key)).filter((record) => Boolean(record));
|
|
232
|
+
const judged = judge(params, records, keys, read);
|
|
233
|
+
if (keys.some(key => judged.get(key) === undefined))
|
|
234
|
+
return null;
|
|
235
|
+
const offered = offeredPart(value, new Set(keys.filter(key => judged.get(key) === true)));
|
|
236
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
237
|
+
}
|
|
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
|
+
*/
|
|
242
|
+
const FIND_PAGE_SIZE = 50;
|
|
243
|
+
/**
|
|
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
|
|
245
|
+
* longer list leaves it untold (`unknown`: neither opened nor forgotten).
|
|
246
|
+
*/
|
|
247
|
+
const FIND_PAGES = 20;
|
|
248
|
+
/**
|
|
249
|
+
* The keys the picker's own pages (narrowed as the form narrows them) offer, read until each key showed or they end.
|
|
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.
|
|
253
|
+
*/
|
|
254
|
+
async function offeredInPages(params, keys, read, signal) {
|
|
255
|
+
const narrowed = narrowedAmong(params, read);
|
|
256
|
+
const load = narrowed.loadOptions;
|
|
257
|
+
const offered = new Set();
|
|
258
|
+
if (!load)
|
|
259
|
+
return { offered, ended: true };
|
|
260
|
+
const valueKey = narrowed.valueKey ?? "value";
|
|
261
|
+
const size = typeof narrowed.pageSize === "number" && narrowed.pageSize >= 1 ? Math.floor(narrowed.pageSize) : FIND_PAGE_SIZE;
|
|
262
|
+
const wanted = new Set(keys);
|
|
263
|
+
for (let page = 1; page <= FIND_PAGES && wanted.size; page++) {
|
|
264
|
+
if (signal.aborted)
|
|
265
|
+
break;
|
|
266
|
+
const skip = (page - 1) * size;
|
|
267
|
+
const answer = await load({ search: "", page, skip, pageSize: size, signal });
|
|
268
|
+
for (const item of answer.items) {
|
|
269
|
+
const key = optionKey(item[valueKey]);
|
|
270
|
+
if (!wanted.has(key))
|
|
271
|
+
continue;
|
|
272
|
+
wanted.delete(key);
|
|
273
|
+
// The narrowed pages hold only the narrowing's records: one read there is judged on its own state alone.
|
|
274
|
+
if (pickable(params, item))
|
|
275
|
+
offered.add(key);
|
|
276
|
+
}
|
|
277
|
+
const more = answer.hasMore ?? answer.items.length >= size;
|
|
278
|
+
if (!more || answer.items.length === 0)
|
|
279
|
+
return { offered, ended: true };
|
|
280
|
+
}
|
|
281
|
+
// Every key showed (the rest of the list is not needed), or the pages ran out (or the ask stopped) before the end.
|
|
282
|
+
return { offered, ended: wanted.size === 0 };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* How a value stands with the input, asking a picker when it must: by the value's key (`resolveOptions`, as a saved
|
|
286
|
+
* value is named) - a record found is judged on its own state and narrowing field - and, for a key it did not answer or
|
|
287
|
+
* a record without its narrowing's field, through its own pages narrowed as the form narrows them (a picker that names
|
|
288
|
+
* only what it already holds, such as a `crudPicker` with another `valueKey` and no `find`, answers nothing for a list
|
|
289
|
+
* not loaded yet: its pages tell). Never rejects: a picker that cannot be read answers `unknown`.
|
|
290
|
+
*/
|
|
291
|
+
export async function findKeptStanding(params, value, read, signal) {
|
|
292
|
+
const now = keptStandingNow(params, value, read);
|
|
293
|
+
if (now)
|
|
294
|
+
return now;
|
|
295
|
+
const own = params;
|
|
296
|
+
const keys = keysOf(value);
|
|
297
|
+
try {
|
|
298
|
+
let judged = new Map();
|
|
299
|
+
if (own.resolveOptions) {
|
|
300
|
+
const records = await own.resolveOptions(keys);
|
|
301
|
+
if (signal.aborted)
|
|
302
|
+
return UNKNOWN;
|
|
303
|
+
judged = judge(params, records, keys, read);
|
|
304
|
+
}
|
|
305
|
+
const untold = keys.filter(key => judged.get(key) === undefined);
|
|
306
|
+
if (untold.length) {
|
|
307
|
+
if (signal.aborted)
|
|
308
|
+
return UNKNOWN;
|
|
309
|
+
const pages = await offeredInPages(params, untold, read, signal);
|
|
310
|
+
if (signal.aborted)
|
|
311
|
+
return UNKNOWN;
|
|
312
|
+
// A key not seen before the pages ran out may stand further on: not known, so neither opened nor forgotten.
|
|
313
|
+
if (!pages.ended && untold.some(key => !pages.offered.has(key)))
|
|
314
|
+
return UNKNOWN;
|
|
315
|
+
for (const key of untold)
|
|
316
|
+
judged.set(key, pages.offered.has(key));
|
|
317
|
+
}
|
|
318
|
+
const offered = offeredPart(value, new Set(keys.filter(key => judged.get(key) === true)));
|
|
319
|
+
return offered === undefined ? GONE : { status: "offered", value: offered };
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return UNKNOWN;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const NO_OPENING = { value: undefined, forget: false, ask: false };
|
|
326
|
+
/**
|
|
327
|
+
* What a remembered input opens on among these values, at once where it can be told: the company's `preferred` value
|
|
328
|
+
* while the input offers it, else the person's last saved choice while it offers that, else none. `ask` when a picker
|
|
329
|
+
* must be asked first (`findRememberedOpening`). Nothing while its `when` does not hold.
|
|
330
|
+
*/
|
|
331
|
+
export function rememberedOpeningNow(params, stored, read = readNothing) {
|
|
332
|
+
const remember = params.remember;
|
|
333
|
+
if (!remember || !remembersHere(remember, read))
|
|
334
|
+
return NO_OPENING;
|
|
335
|
+
if (!isEmptyInputValue(params, remember.preferred)) {
|
|
336
|
+
const standing = keptStandingNow(params, remember.preferred, read);
|
|
337
|
+
if (!standing)
|
|
338
|
+
return { ...NO_OPENING, ask: true };
|
|
339
|
+
if (standing.status === "offered")
|
|
340
|
+
return { value: standing.value, forget: false, ask: false };
|
|
341
|
+
}
|
|
342
|
+
if (isEmptyInputValue(params, stored))
|
|
343
|
+
return NO_OPENING;
|
|
344
|
+
const standing = keptStandingNow(params, stored, read);
|
|
345
|
+
if (!standing)
|
|
346
|
+
return { ...NO_OPENING, ask: true };
|
|
347
|
+
if (standing.status === "offered")
|
|
348
|
+
return { value: standing.value, forget: false, ask: false };
|
|
349
|
+
return { value: undefined, forget: standing.status === "gone", ask: false };
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The same, asking a picker by the values' keys where it must (`findKeptStanding`). Never rejects: a picker that could
|
|
353
|
+
* not be read opens nothing and forgets nothing.
|
|
354
|
+
*/
|
|
355
|
+
export async function findRememberedOpening(params, stored, read, signal) {
|
|
356
|
+
const remember = params.remember;
|
|
357
|
+
if (!remember || !remembersHere(remember, read))
|
|
358
|
+
return NO_OPENING;
|
|
359
|
+
if (!isEmptyInputValue(params, remember.preferred)) {
|
|
360
|
+
const standing = await findKeptStanding(params, remember.preferred, read, signal);
|
|
361
|
+
if (standing.status === "offered")
|
|
362
|
+
return { value: standing.value, forget: false, ask: false };
|
|
363
|
+
}
|
|
364
|
+
if (isEmptyInputValue(params, stored) || signal.aborted)
|
|
365
|
+
return NO_OPENING;
|
|
366
|
+
const standing = await findKeptStanding(params, stored, read, signal);
|
|
367
|
+
if (standing.status === "offered")
|
|
368
|
+
return { value: standing.value, forget: false, ask: false };
|
|
369
|
+
return { value: undefined, forget: standing.status === "gone", ask: false };
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* What a remembered input opens on when the form's values leave it empty, from its own list alone: the company's
|
|
373
|
+
* `preferred` value while the input offers it, else the person's last saved choice while it offers that, else undefined
|
|
374
|
+
* (the input keeps its own default). A value no longer offered is dropped without a word, never shown as a broken one.
|
|
375
|
+
* A picker's values are taken as they are here; a form asks the picker (`rememberedOpeningNow`).
|
|
376
|
+
*/
|
|
377
|
+
export function rememberedOpening(params, stored) {
|
|
378
|
+
const remember = params.remember;
|
|
379
|
+
if (!remember)
|
|
380
|
+
return undefined;
|
|
381
|
+
const preferred = offeredInputValue(params, remember.preferred);
|
|
382
|
+
if (preferred !== undefined)
|
|
383
|
+
return preferred;
|
|
384
|
+
return offeredInputValue(params, stored);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* The row of a list a save keeps a row input's value from: the one added last that is still there, else none (no row
|
|
388
|
+
* was added, so no new choice was made). Keys in the order they were added.
|
|
389
|
+
*/
|
|
390
|
+
export function lastAddedRowKey(added, present) {
|
|
391
|
+
for (let index = added.length - 1; index >= 0; index--)
|
|
392
|
+
if (present(added[index]))
|
|
393
|
+
return added[index];
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { InputNarrowing, InputNarrowingByInput, InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
2
|
+
import type { OptionValue } from "../Interfaces/InputInterfaces.ts";
|
|
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
|
+
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[];
|
|
9
|
+
/** What a narrowed box says while the other input is empty: "Pick a group first". */
|
|
10
|
+
export declare function narrowingPlaceholder(narrowing: InputNarrowing, otherLabel: unknown): string;
|
|
11
|
+
/** A narrowing by a fixed value (`{ key, value }`), not by another input. */
|
|
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
|
+
};
|
|
18
|
+
/**
|
|
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.
|
|
32
|
+
*
|
|
33
|
+
* @example narrowInputParams(packageParams, form.get("subscriptionGroupId"), "Group")
|
|
34
|
+
*/
|
|
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[];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*
|
|
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, 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`).
|
|
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. */
|
|
8
|
+
export function narrowingValue(value) {
|
|
9
|
+
if (value === null || value === undefined || value === "" || value === false)
|
|
10
|
+
return null;
|
|
11
|
+
if (Array.isArray(value))
|
|
12
|
+
return value.length === 1 ? narrowingValue(value[0]) : null;
|
|
13
|
+
if (typeof value === "string" || typeof value === "number")
|
|
14
|
+
return value;
|
|
15
|
+
return null;
|
|
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
|
+
}
|
|
28
|
+
/** What a narrowed box says while the other input is empty: "Pick a group first". */
|
|
29
|
+
export function narrowingPlaceholder(narrowing, otherLabel) {
|
|
30
|
+
if (narrowing.placeholder)
|
|
31
|
+
return narrowing.placeholder;
|
|
32
|
+
if (narrowing.name === undefined)
|
|
33
|
+
return "";
|
|
34
|
+
const label = typeof otherLabel === "string" && otherLabel.trim() ? otherLabel.trim().toLocaleLowerCase() : narrowing.name;
|
|
35
|
+
return `Pick ${/^[aeiou]/i.test(label) ? "an" : "a"} ${label} first`;
|
|
36
|
+
}
|
|
37
|
+
const EMPTY_PAGE = { items: [], total: 0, hasMore: false };
|
|
38
|
+
/** A narrowing by a fixed value (`{ key, value }`), not by another input. */
|
|
39
|
+
export function isFixedNarrowing(narrowing) {
|
|
40
|
+
return narrowing.name === undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
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.
|
|
49
|
+
*
|
|
50
|
+
* @example narrowInputParamsBy(tagsParams, narrowing => ({ value: narrowing.from === "form" ? form.get(narrowing.name) : row[narrowing.name] }))
|
|
51
|
+
*/
|
|
52
|
+
export function narrowInputParamsBy(params, read) {
|
|
53
|
+
const narrowings = narrowingsOf(params);
|
|
54
|
+
if (narrowings.length === 0)
|
|
55
|
+
return params;
|
|
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
|
+
});
|
|
66
|
+
const keeps = (item) => {
|
|
67
|
+
if (item === null || typeof item !== "object")
|
|
68
|
+
return false;
|
|
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
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const own = params;
|
|
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) : "";
|
|
80
|
+
if (placeholder)
|
|
81
|
+
narrowed.placeholder = placeholder;
|
|
82
|
+
// Nothing to narrow by offers nothing, unless the options with no value in the field are offered too (`orUnset`).
|
|
83
|
+
const none = parts.some(part => part.value === null && !part.orUnset);
|
|
84
|
+
if (Array.isArray(own.options))
|
|
85
|
+
narrowed.options = none ? [] : own.options.filter(keeps);
|
|
86
|
+
const load = own.loadOptions;
|
|
87
|
+
if (load) {
|
|
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 }) }));
|
|
90
|
+
}
|
|
91
|
+
return narrowed;
|
|
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
|
+
}
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InputParams, type InputValidationContext, type RowsInputProps, type RowValue } from "../Interfaces/FormInterfaces.ts";
|
|
2
2
|
/** Keys that place an input in a form; a renderer leaves them out of the component's props. */
|
|
3
3
|
export declare const INPUT_LAYOUT_KEYS: readonly ["span", "width", "grow", "hidden"];
|
|
4
4
|
/** Inputs whose value is `true` or `false`. */
|
|
5
5
|
export declare const isBooleanInput: (params: InputParams) => params is import("../Shared.Index.ts").CheckBoxParams | import("../Shared.Index.ts").SwitchParams | import("../Shared.Index.ts").RadioParams;
|
|
6
|
+
/**
|
|
7
|
+
* The member name of an input type, for a message or a log: `inputTypeName(EInputTypes.RichText)` is "RichText". A number
|
|
8
|
+
* that is no member, such as one read from old data, comes back as its digits.
|
|
9
|
+
*/
|
|
10
|
+
export declare const inputTypeName: (type: number) => string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether an input holds a value of the form. A picture does not: it is the owner's file, changed at once through the
|
|
13
|
+
* file backend, so the form keeps no field for it and a save sends nothing under its name.
|
|
14
|
+
*/
|
|
15
|
+
export declare const holdsValue: (params: InputParams) => params is import("../Shared.Index.ts").TextInputParams | import("../Shared.Index.ts").TextAreaParams | import("../Shared.Index.ts").NumberInputParams | import("../Shared.Index.ts").CodeInputParams | import("../Shared.Index.ts").CheckBoxParams | import("../Shared.Index.ts").SwitchParams | import("../Shared.Index.ts").RadioParams | import("../Shared.Index.ts").CheckBoxGroupParams | import("../Shared.Index.ts").RadioGroupParams | import("../Shared.Index.ts").DropdownParams | import("../Shared.Index.ts").DateParams | import("../Shared.Index.ts").DateRangeParams | import("../Shared.Index.ts").TimeParams | import("../Shared.Index.ts").DateTimeParams | import("../Shared.Index.ts").SliderParams | import("../Shared.Index.ts").RangeSliderParams | import("../Shared.Index.ts").MarkdownParams | import("../Shared.Index.ts").RichTextParams | import("../Shared.Index.ts").TreeParams | import("../Shared.Index.ts").TabularParams | import("../Shared.Index.ts").GroupParams;
|
|
16
|
+
/**
|
|
17
|
+
* Whether what is typed in an input is worth asking about before it is dropped: not a password or a one-time code (a
|
|
18
|
+
* `Code` box), which are typed again in a moment and never kept in a draft. Such a value alone makes no form ask
|
|
19
|
+
* "Discard your changes?" on Cancel or close, nor the page ask before it is left.
|
|
20
|
+
*/
|
|
21
|
+
export declare const keepsTyping: (params: InputParams) => boolean;
|
|
6
22
|
/** Inputs whose value is an array of rows. */
|
|
7
23
|
export declare const isRowsInput: (params: InputParams) => params is import("../Shared.Index.ts").TabularParams | import("../Shared.Index.ts").GroupParams;
|
|
8
24
|
/**
|
|
@@ -23,6 +39,33 @@ export declare function normalizeInputValue(params: InputParams, value: unknown)
|
|
|
23
39
|
export declare function isEmptyInputValue(value: unknown): boolean;
|
|
24
40
|
/** Equal values: the same primitive, or arrays with the same items in the same order. */
|
|
25
41
|
export declare function inputValuesEqual(a: unknown, b: unknown): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The inputs with some of them fixed for one opening of a form: each input `locked` names (input name -> why) turns
|
|
44
|
+
* read-only and says why on any try to change it (`readOnlyMessage`), the lock-and-note look every input has. Its value
|
|
45
|
+
* stays in the form and is sent as before. A required input with no value in `values` stays free: fixed empty, the form
|
|
46
|
+
* would never ask for it, since read-only inputs are not checked. Names a form does not have are skipped.
|
|
47
|
+
*
|
|
48
|
+
* Each fixed input is a copy made from its own property descriptors, never a spread: a spread would read a getter once
|
|
49
|
+
* (`get readOnly()` or `get options()` following other values through `dependsOn`) and freeze it.
|
|
50
|
+
*
|
|
51
|
+
* To show fixed inputs disabled instead (dimmed, out of the Tab order, no note), write `disabled` for `readOnly` in the
|
|
52
|
+
* copy below.
|
|
53
|
+
*
|
|
54
|
+
* `known` are records the caller holds for the fixed values - the row picked in another list that a new record is added
|
|
55
|
+
* for: a fixed dropdown whose value one of them holds (by the dropdown's `valueKey`) gets it as a selected option, so its
|
|
56
|
+
* box names the value at once, with no lookup.
|
|
57
|
+
*
|
|
58
|
+
* @example lockInputs(REGION_INPUTS, { countryId: "Added inside Bagmati." }, { countryId: "c7" })
|
|
59
|
+
*/
|
|
60
|
+
export declare function lockInputs(inputs: readonly InputParams[], locked: Readonly<Record<string, string>>, values?: Readonly<Record<string, unknown>>, known?: readonly Readonly<Record<string, unknown>>[]): InputParams[];
|
|
61
|
+
/**
|
|
62
|
+
* A form's `onFieldChange` that never writes the inputs fixed for an opening (`lockInputs`): a rule such as "a province
|
|
63
|
+
* sits right under its country" still changes a free field, and leaves a fixed one as the form opened it. With nothing
|
|
64
|
+
* fixed, or no rule, the rule as it is.
|
|
65
|
+
*
|
|
66
|
+
* @example lockedFieldChange(form.onFieldChange, ["countryId", "parentId"])
|
|
67
|
+
*/
|
|
68
|
+
export declare function lockedFieldChange<TValues extends Record<string, unknown>>(onFieldChange: ((name: string, value: unknown, values: TValues) => Partial<TValues> | null | undefined | void) | undefined, fixed: readonly string[]): ((name: string, value: unknown, values: TValues) => Partial<TValues> | null | undefined | void) | undefined;
|
|
26
69
|
/**
|
|
27
70
|
* Checks a value against its input's rules and returns the first message, or `null` when it is valid: the same
|
|
28
71
|
* message the input shows. Pure, so a form can check thousands of values on submit, including rows not on screen.
|