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
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { isComputedInput, withComputeSources } from "./InputConditions.js";
|
|
3
|
+
import { findRememberedOpening, forgetInputMemory, isEmptyInputValue, isPickerInput, lastAddedRowKey, readInputMemory, rememberDependencies, rememberedOpeningNow, writeInputMemory, } from "./InputMemory.js";
|
|
4
|
+
import { isEmptyNarrowed, narrowedFollowers } from "./InputNarrowing.js";
|
|
5
|
+
import { duplicateMessageFor, holdsValue, initialInputValue, inputValuesEqual, normalizeInputValue, rowsItemNames, rowsMessage, uniqueKeyOf, validateInput, } from "./InputParamsHelpers.js";
|
|
6
|
+
/** The options with each worked-out input following the inputs it is worked out from (`compute.from` in `dependsOn`). */
|
|
7
|
+
function withSources(options) {
|
|
8
|
+
const inputs = withComputeSources(options.inputs);
|
|
9
|
+
return inputs === options.inputs ? options : { ...options, inputs };
|
|
10
|
+
}
|
|
2
11
|
export class RowsStore {
|
|
3
12
|
options;
|
|
4
13
|
inputByName = new Map();
|
|
@@ -17,6 +26,12 @@ export class RowsStore {
|
|
|
17
26
|
showAll = false;
|
|
18
27
|
touched = new Map();
|
|
19
28
|
removed = null;
|
|
29
|
+
// Remembered row inputs (`remember`): where they are kept, the rows added since the rows were loaded (in the order they
|
|
30
|
+
// were added: a save keeps the last one's values), and the cells a picker is still asked about.
|
|
31
|
+
memory = null;
|
|
32
|
+
added = [];
|
|
33
|
+
asking = new Map();
|
|
34
|
+
memoryRound = null;
|
|
20
35
|
// Rendering: a revision per value set from code (its input mounts again with it), a version per row.
|
|
21
36
|
revisions = new Map();
|
|
22
37
|
versions = new Map();
|
|
@@ -30,7 +45,7 @@ export class RowsStore {
|
|
|
30
45
|
/** The array last given to value listeners. A controlled parent that passes it back changes nothing. */
|
|
31
46
|
lastValue = null;
|
|
32
47
|
constructor(options, rows = []) {
|
|
33
|
-
this.options = options;
|
|
48
|
+
this.options = withSources(options);
|
|
34
49
|
this.indexInputs();
|
|
35
50
|
this.load(rows);
|
|
36
51
|
this.initial = this.getValue();
|
|
@@ -153,7 +168,7 @@ export class RowsStore {
|
|
|
153
168
|
* callbacks (`validate`, `onRowChange`, `newRow`) are swapped, which a parent's render does every time.
|
|
154
169
|
*/
|
|
155
170
|
configure(options, recheck = true) {
|
|
156
|
-
this.options = options;
|
|
171
|
+
this.options = withSources(options);
|
|
157
172
|
if (!recheck)
|
|
158
173
|
return;
|
|
159
174
|
this.indexInputs();
|
|
@@ -171,6 +186,30 @@ export class RowsStore {
|
|
|
171
186
|
return;
|
|
172
187
|
this.settle(changed);
|
|
173
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Values a person set in a row through another of its inputs - one worked out from them and typed in (`compute.solve`)
|
|
191
|
+
* - as one change. Their cells show them at once, as values from code do, and they are taken as the person's own:
|
|
192
|
+
* `onRowChange` runs for each, the row's inputs narrowed by them lose what they picked, and the rows' value listeners
|
|
193
|
+
* hear of it (a parent form's unsaved-changes state, its Save and its leave guard). A name worked out itself is not
|
|
194
|
+
* taken. Answers whether anything changed.
|
|
195
|
+
*/
|
|
196
|
+
setFromPerson(key, values) {
|
|
197
|
+
const before = this.getValue();
|
|
198
|
+
const changed = new Set();
|
|
199
|
+
for (const [name, value] of Object.entries(values))
|
|
200
|
+
this.write(key, name, value, "person", changed, 0);
|
|
201
|
+
if (this.getValue() === before)
|
|
202
|
+
return false;
|
|
203
|
+
this.settle(changed);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Draws every row again, values untouched: the form around the rows changed a value their cells follow
|
|
208
|
+
* (`narrowedBy` `from: "form"`), so each cell is narrowed afresh as it draws.
|
|
209
|
+
*/
|
|
210
|
+
redrawRows() {
|
|
211
|
+
this.touchAllRows();
|
|
212
|
+
}
|
|
174
213
|
/** Marks a value as left by the user: a repeated value shows its message from now on. */
|
|
175
214
|
touch(key, name) {
|
|
176
215
|
let names = this.touched.get(key);
|
|
@@ -182,18 +221,25 @@ export class RowsStore {
|
|
|
182
221
|
if (!this.showAll && this.duplicateErrors.get(key)?.has(name))
|
|
183
222
|
this.bump(key);
|
|
184
223
|
}
|
|
185
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Adds a row with the inputs' defaults, `newRow`, and `values`, at `index` or at the end. Returns its key. A remembered
|
|
226
|
+
* input (`remember`) that neither `newRow` nor `values` names with a value opens on the value kept for it, while its
|
|
227
|
+
* input offers it; a picker's opens once the picker answers, unless the person picked one first.
|
|
228
|
+
*/
|
|
186
229
|
add(values, index = this.entries.length) {
|
|
187
230
|
const position = Math.max(0, Math.min(index, this.entries.length));
|
|
188
231
|
const wasEmpty = this.entries.length === 0;
|
|
189
232
|
const row = {};
|
|
190
233
|
for (const params of this.options.inputs)
|
|
191
|
-
|
|
234
|
+
if (holdsValue(params) && !isComputedInput(params))
|
|
235
|
+
row[params.name] = initialInputValue(params);
|
|
192
236
|
const { newRow } = this.options;
|
|
193
|
-
|
|
237
|
+
const named = { ...(typeof newRow === "function" ? newRow(position) : newRow), ...values };
|
|
238
|
+
Object.assign(row, named);
|
|
239
|
+
const asks = this.openRemembered(row, named);
|
|
194
240
|
// The first row of a list has each radio on: the first address added is the primary one.
|
|
195
241
|
for (const params of this.options.inputs) {
|
|
196
|
-
if (params.type ===
|
|
242
|
+
if (params.type === EInputTypes.Radio && wasEmpty && values?.[params.name] === undefined)
|
|
197
243
|
row[params.name] = true;
|
|
198
244
|
}
|
|
199
245
|
const entry = { key: this.newKey(), values: this.normalizeRow(row) };
|
|
@@ -203,15 +249,41 @@ export class RowsStore {
|
|
|
203
249
|
for (const params of this.options.inputs) {
|
|
204
250
|
this.checkCell(entry, params);
|
|
205
251
|
// Only one row keeps a radio on: a new row that has it takes it from the others.
|
|
206
|
-
if (params.type ===
|
|
252
|
+
if (params.type === EInputTypes.Radio && entry.values[params.name] === true)
|
|
207
253
|
this.radioOn(entry, params.name, changed);
|
|
208
254
|
}
|
|
209
255
|
this.messageShown = true;
|
|
210
256
|
this.removed = null;
|
|
257
|
+
this.added.push(entry.key);
|
|
211
258
|
this.markUniqueDirty();
|
|
212
259
|
this.settle(changed);
|
|
260
|
+
this.askPickers(entry.key, asks);
|
|
213
261
|
return entry.key;
|
|
214
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* The storage remembered row inputs read and keep (`InputLayoutParams.remember`), given once the rows are drawn in the
|
|
265
|
+
* browser: rows added from then on open on the kept values. The rows already there keep theirs.
|
|
266
|
+
*/
|
|
267
|
+
attachMemory(storage) {
|
|
268
|
+
this.memory = storage;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A save of the form holding the rows: each remembered input keeps its value in the row added last that is still there
|
|
272
|
+
* (an empty value forgets), under that row's `per` value and where its `when` holds on that row. No row added since the
|
|
273
|
+
* rows were loaded: nothing is kept, since no new choice was made.
|
|
274
|
+
*/
|
|
275
|
+
rememberAdded() {
|
|
276
|
+
if (!this.memory)
|
|
277
|
+
return;
|
|
278
|
+
const key = lastAddedRowKey(this.added, added => this.positions.has(added));
|
|
279
|
+
const row = key === null ? undefined : this.row(key);
|
|
280
|
+
if (!row)
|
|
281
|
+
return;
|
|
282
|
+
const read = (name) => row[name];
|
|
283
|
+
for (const params of this.options.inputs)
|
|
284
|
+
if (params.remember && !params.hidden && !isComputedInput(params))
|
|
285
|
+
writeInputMemory(this.memory, params, row[params.name], read);
|
|
286
|
+
}
|
|
215
287
|
/** Adds a copy of a row after it, without its id and with its radios off. Returns the copy's key. */
|
|
216
288
|
duplicate(key) {
|
|
217
289
|
const index = this.positions.get(key);
|
|
@@ -222,7 +294,7 @@ export class RowsStore {
|
|
|
222
294
|
if (this.options.rowKey)
|
|
223
295
|
delete values[this.options.rowKey];
|
|
224
296
|
for (const params of this.options.inputs)
|
|
225
|
-
if (params.type ===
|
|
297
|
+
if (params.type === EInputTypes.Radio)
|
|
226
298
|
values[params.name] = false;
|
|
227
299
|
return this.add(values, index + 1);
|
|
228
300
|
}
|
|
@@ -243,7 +315,7 @@ export class RowsStore {
|
|
|
243
315
|
const moved = [];
|
|
244
316
|
const first = this.entries[0];
|
|
245
317
|
for (const params of this.options.inputs) {
|
|
246
|
-
if (params.type !==
|
|
318
|
+
if (params.type !== EInputTypes.Radio || entry.values[params.name] !== true || !first)
|
|
247
319
|
continue;
|
|
248
320
|
this.write(first.key, params.name, true, "code", changed, 1);
|
|
249
321
|
moved.push({ key: first.key, name: params.name });
|
|
@@ -306,6 +378,9 @@ export class RowsStore {
|
|
|
306
378
|
*/
|
|
307
379
|
replace(rows, { asInitial = true, emit = false } = {}) {
|
|
308
380
|
this.load(rows);
|
|
381
|
+
// Loaded rows: none of them was added here, and no picker's answer is wanted any more.
|
|
382
|
+
this.added = [];
|
|
383
|
+
this.stopAskingPickers();
|
|
309
384
|
if (asInitial)
|
|
310
385
|
this.initial = this.getValue();
|
|
311
386
|
this.generation++;
|
|
@@ -343,6 +418,11 @@ export class RowsStore {
|
|
|
343
418
|
normalizeRow(row) {
|
|
344
419
|
const values = { ...row };
|
|
345
420
|
for (const params of this.options.inputs) {
|
|
421
|
+
// A worked-out input holds no value: a field the row came with under its name is not the row's to send.
|
|
422
|
+
if (isComputedInput(params)) {
|
|
423
|
+
delete values[params.name];
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
346
426
|
// A hidden input's value stays in the data as it was.
|
|
347
427
|
if (params.hidden)
|
|
348
428
|
continue;
|
|
@@ -363,7 +443,7 @@ export class RowsStore {
|
|
|
363
443
|
});
|
|
364
444
|
// Saved data with a radio on in more than one row keeps the first.
|
|
365
445
|
for (const params of this.options.inputs) {
|
|
366
|
-
if (params.type !==
|
|
446
|
+
if (params.type !== EInputTypes.Radio)
|
|
367
447
|
continue;
|
|
368
448
|
let found = false;
|
|
369
449
|
for (const entry of this.entries) {
|
|
@@ -383,6 +463,76 @@ export class RowsStore {
|
|
|
383
463
|
this.valueCache = null;
|
|
384
464
|
this.keysCache = null;
|
|
385
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Opens a new row's remembered inputs on their kept values, where the row is not added with one: the company's
|
|
468
|
+
* `preferred`, else the person's last choice, while the input offers it (`rememberedOpeningNow`, `when` and `per` read
|
|
469
|
+
* in the row). A kept choice no longer offered is forgotten. Answers the pickers that must be asked first.
|
|
470
|
+
*/
|
|
471
|
+
openRemembered(row, named) {
|
|
472
|
+
const asks = [];
|
|
473
|
+
const read = (name) => row[name];
|
|
474
|
+
for (const params of this.options.inputs) {
|
|
475
|
+
const { name } = params;
|
|
476
|
+
if (!params.remember || params.hidden || !holdsValue(params) || isComputedInput(params))
|
|
477
|
+
continue;
|
|
478
|
+
if (name in named && !isEmptyInputValue(params, named[name]))
|
|
479
|
+
continue;
|
|
480
|
+
if (!this.memory && isPickerInput(params))
|
|
481
|
+
continue;
|
|
482
|
+
const stored = this.memory ? readInputMemory(this.memory, params.remember, read) : undefined;
|
|
483
|
+
const opening = rememberedOpeningNow(params, stored, read);
|
|
484
|
+
if (opening.forget)
|
|
485
|
+
forgetInputMemory(this.memory, params.remember, read);
|
|
486
|
+
if (opening.ask)
|
|
487
|
+
asks.push({ params, stored });
|
|
488
|
+
if (opening.value !== undefined)
|
|
489
|
+
row[name] = opening.value;
|
|
490
|
+
}
|
|
491
|
+
return asks;
|
|
492
|
+
}
|
|
493
|
+
/** Stops asking pickers about remembered values: their answers no longer open anything. */
|
|
494
|
+
stopAskingPickers() {
|
|
495
|
+
this.memoryRound?.abort();
|
|
496
|
+
this.memoryRound = null;
|
|
497
|
+
this.asking.clear();
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Asks each picker whether it still offers the remembered value of a new row's input (`findRememberedOpening`), judged
|
|
501
|
+
* on the row as it was added: an answer forgets a choice no longer offered (under that row's `per` value), and opens an
|
|
502
|
+
* offered one while the row is there, the person has not picked in that cell since, and the row's values it was judged
|
|
503
|
+
* on (`rememberDependencies`) are unchanged.
|
|
504
|
+
*/
|
|
505
|
+
askPickers(key, asks) {
|
|
506
|
+
const added = this.row(key);
|
|
507
|
+
if (!asks.length || !added)
|
|
508
|
+
return;
|
|
509
|
+
this.memoryRound ??= new AbortController();
|
|
510
|
+
const round = this.memoryRound;
|
|
511
|
+
const memory = this.memory;
|
|
512
|
+
const names = this.asking.get(key) ?? new Set();
|
|
513
|
+
this.asking.set(key, names);
|
|
514
|
+
const opening = { ...added };
|
|
515
|
+
const read = (name) => opening[name];
|
|
516
|
+
for (const { params, stored } of asks) {
|
|
517
|
+
names.add(params.name);
|
|
518
|
+
const judgedOn = rememberDependencies(params);
|
|
519
|
+
void findRememberedOpening(params, stored, read, round.signal).then(answer => {
|
|
520
|
+
if (round.signal.aborted)
|
|
521
|
+
return;
|
|
522
|
+
if (answer.forget)
|
|
523
|
+
forgetInputMemory(memory, params.remember, read);
|
|
524
|
+
const waiting = this.asking.get(key);
|
|
525
|
+
if (!waiting?.delete(params.name))
|
|
526
|
+
return;
|
|
527
|
+
if (!waiting.size)
|
|
528
|
+
this.asking.delete(key);
|
|
529
|
+
const row = this.row(key);
|
|
530
|
+
if (answer.value === undefined || !row || !judgedOn.every(name => inputValuesEqual(row[name], opening[name])))
|
|
531
|
+
return;
|
|
532
|
+
this.set(key, params.name, answer.value);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
386
536
|
/** Drops what the store knew about a removed row. */
|
|
387
537
|
forget(key) {
|
|
388
538
|
this.invalidCells -= this.invalidByRow.get(key) ?? 0;
|
|
@@ -391,20 +541,33 @@ export class RowsStore {
|
|
|
391
541
|
this.duplicateErrors.delete(key);
|
|
392
542
|
this.touched.delete(key);
|
|
393
543
|
this.revisions.delete(key);
|
|
544
|
+
this.asking.delete(key);
|
|
394
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Writes one value. `input` was typed in its own cell; `code` comes from code (its cell mounts again with it); `person`
|
|
548
|
+
* is a person's change made through another cell (`setFromPerson`): written as code writes, followed as typing is.
|
|
549
|
+
*/
|
|
395
550
|
write(key, name, value, source, changed, depth) {
|
|
396
551
|
const index = this.positions.get(key);
|
|
397
552
|
const entry = index === undefined ? undefined : this.entries[index];
|
|
398
553
|
if (!entry || index === undefined)
|
|
399
554
|
return;
|
|
400
555
|
const params = this.inputByName.get(name);
|
|
401
|
-
|
|
556
|
+
// A worked-out input holds no value: one set from code (an `onRowChange` or `deriveRows` patch) is not taken.
|
|
557
|
+
if (isComputedInput(params))
|
|
558
|
+
return;
|
|
559
|
+
const asCode = source !== "input";
|
|
560
|
+
const byPerson = source !== "code";
|
|
561
|
+
const next = params && asCode ? normalizeInputValue(params, value) : value;
|
|
562
|
+
// The person's own pick: a picker's answer about a remembered value no longer replaces it.
|
|
563
|
+
if (byPerson)
|
|
564
|
+
this.asking.get(key)?.delete(name);
|
|
402
565
|
if (inputValuesEqual(entry.values[name], next))
|
|
403
566
|
return;
|
|
404
567
|
const shownBefore = this.shownError(key, name);
|
|
405
568
|
entry.values = { ...entry.values, [name]: next };
|
|
406
569
|
this.valueCache = null;
|
|
407
|
-
if (
|
|
570
|
+
if (asCode) {
|
|
408
571
|
// The input shows its value from when it mounted; a new revision mounts it again with this one.
|
|
409
572
|
let names = this.revisions.get(key);
|
|
410
573
|
if (!names)
|
|
@@ -414,7 +577,7 @@ export class RowsStore {
|
|
|
414
577
|
}
|
|
415
578
|
if (params) {
|
|
416
579
|
this.checkCell(entry, params);
|
|
417
|
-
if (params.type ===
|
|
580
|
+
if (params.type === EInputTypes.Radio) {
|
|
418
581
|
// The row shows which radios it has on (a section's "Primary" tag), so it renders again, typed or not.
|
|
419
582
|
changed.add(key);
|
|
420
583
|
if (next === true)
|
|
@@ -426,7 +589,7 @@ export class RowsStore {
|
|
|
426
589
|
if (this.shownError(key, name) !== shownBefore)
|
|
427
590
|
changed.add(key);
|
|
428
591
|
// Values the row derives from the one the user changed, such as an amount from a quantity and a rate.
|
|
429
|
-
if (
|
|
592
|
+
if (byPerson && depth === 0 && this.options.onRowChange) {
|
|
430
593
|
const patch = this.options.onRowChange(entry.values, name, index);
|
|
431
594
|
if (patch) {
|
|
432
595
|
for (const [field, fieldValue] of Object.entries(patch)) {
|
|
@@ -435,6 +598,30 @@ export class RowsStore {
|
|
|
435
598
|
}
|
|
436
599
|
}
|
|
437
600
|
}
|
|
601
|
+
if (byPerson && depth === 0)
|
|
602
|
+
this.follow(key, name, changed);
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* After a person changed a row's value: the row's inputs narrowed by it (`narrowedBy`, a line's Tags by its Item) lose
|
|
606
|
+
* what they picked among its old options, down the row's chain, and the row draws again so each is narrowed afresh;
|
|
607
|
+
* a row input that `dependsOn` it (a Tags box whose most picks are the row's quantity, through `cellParams`) draws
|
|
608
|
+
* again too, its rule asked again.
|
|
609
|
+
*/
|
|
610
|
+
follow(key, name, changed) {
|
|
611
|
+
const index = this.positions.get(key);
|
|
612
|
+
const entry = index === undefined ? undefined : this.entries[index];
|
|
613
|
+
if (!entry)
|
|
614
|
+
return;
|
|
615
|
+
const followers = narrowedFollowers(this.options.inputs, [name], "row");
|
|
616
|
+
for (const follower of followers) {
|
|
617
|
+
if (!isEmptyNarrowed(entry.values[follower]))
|
|
618
|
+
this.write(key, follower, null, "code", changed, 1);
|
|
619
|
+
}
|
|
620
|
+
const dependents = this.options.inputs.filter(params => params.dependsOn?.includes(name));
|
|
621
|
+
for (const params of dependents)
|
|
622
|
+
this.checkCell(entry, params);
|
|
623
|
+
if (followers.length || dependents.length)
|
|
624
|
+
changed.add(key);
|
|
438
625
|
}
|
|
439
626
|
/** Turns a radio off in every row but this one. */
|
|
440
627
|
radioOn(entry, name, changed) {
|
|
@@ -445,7 +632,7 @@ export class RowsStore {
|
|
|
445
632
|
}
|
|
446
633
|
/** Checks one value against its input's rules. */
|
|
447
634
|
checkCell(entry, params) {
|
|
448
|
-
const message = params.hidden ? null : validateInput(params, entry.values[params.name], this.options.context);
|
|
635
|
+
const message = params.hidden || isComputedInput(params) ? null : validateInput(params, entry.values[params.name], this.options.context);
|
|
449
636
|
this.setError(this.ruleErrors, entry.key, params.name, message);
|
|
450
637
|
}
|
|
451
638
|
setError(map, key, name, message) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** What a preview shows now. A new object after every change. */
|
|
2
|
+
export interface ServerPreviewState<TQuestion, TResult> {
|
|
3
|
+
/** The last answer, kept while a newer question waits or runs, and after a newer one failed. Null before the first. */
|
|
4
|
+
result: TResult | null;
|
|
5
|
+
/** The question `result` answers: compare it with the values a save sends, to save only what was shown. */
|
|
6
|
+
question: TQuestion | null;
|
|
7
|
+
/** A question waits for its moment: the pause after the last request (`delay`). */
|
|
8
|
+
waiting: boolean;
|
|
9
|
+
/** A question is with the server. A page that says "Working it out…" says it while `waiting || loading`. */
|
|
10
|
+
loading: boolean;
|
|
11
|
+
/** Why the last question failed, in words; null once a newer answer came, or while a newer question waits. */
|
|
12
|
+
error: string | null;
|
|
13
|
+
}
|
|
14
|
+
/** Asks the server: the answer, or a throw whose message says why (an abort is not a failure). */
|
|
15
|
+
export type ServerPreviewAsk<TQuestion, TResult> = (question: TQuestion, signal: AbortSignal) => Promise<TResult>;
|
|
16
|
+
export interface ServerPreviewOptions {
|
|
17
|
+
/** Milliseconds after the last `request` before the question goes. Default 300. */
|
|
18
|
+
delay?: number;
|
|
19
|
+
/** The words of a failure whose error has none. Default "The preview cannot be worked out just now." */
|
|
20
|
+
failure?: string;
|
|
21
|
+
}
|
|
22
|
+
/** The preview's questions and answers, outside React. */
|
|
23
|
+
export declare class ServerPreviewStore<TQuestion, TResult> {
|
|
24
|
+
private state;
|
|
25
|
+
private listeners;
|
|
26
|
+
private timer;
|
|
27
|
+
private running;
|
|
28
|
+
private asked;
|
|
29
|
+
/** Read on each question, so a page's latest function is the one asked. */
|
|
30
|
+
private readonly ask;
|
|
31
|
+
private readonly options;
|
|
32
|
+
constructor(ask: () => ServerPreviewAsk<TQuestion, TResult>, options?: ServerPreviewOptions);
|
|
33
|
+
getState: () => ServerPreviewState<TQuestion, TResult>;
|
|
34
|
+
subscribe: (listener: () => void) => (() => void);
|
|
35
|
+
private patch;
|
|
36
|
+
/** Stops the question waiting and the one running; their answers are never shown. */
|
|
37
|
+
private stop;
|
|
38
|
+
/** Asks about this question a moment after the last request (the delay), dropping any older one. */
|
|
39
|
+
request(question: TQuestion): void;
|
|
40
|
+
private send;
|
|
41
|
+
/** Drops the question waiting or running; the last answer stays. */
|
|
42
|
+
cancel(): void;
|
|
43
|
+
/** Drops every question and the last answer: nothing is shown. */
|
|
44
|
+
clear(): void;
|
|
45
|
+
}
|
|
46
|
+
/** A page's preview: its state, and `request`, `cancel` and `clear`. */
|
|
47
|
+
export interface ServerPreview<TQuestion, TResult> extends ServerPreviewState<TQuestion, TResult> {
|
|
48
|
+
/** Asks about these values a moment after the last request; an older question is dropped. */
|
|
49
|
+
request: (question: TQuestion) => void;
|
|
50
|
+
/** Drops the question waiting or running (the form lacks what the server needs); the last answer stays. */
|
|
51
|
+
cancel: () => void;
|
|
52
|
+
/** Drops every question and the last answer. */
|
|
53
|
+
clear: () => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A preview the server works out a moment after the last change: `request(values)` on every change, and the page shows
|
|
57
|
+
* `result` (kept while a newer question runs), `waiting` (the pause), `loading` (with the server) and `error`. An older question is aborted and its late answer
|
|
58
|
+
* never shown; leaving the page stops what runs. `ask` throws to fail, with the words to show.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const preview = useServerPreview(async (body: CounterBody, signal) => {
|
|
62
|
+
* const answer = await api.send<CounterPreview>(COUNTER_PREVIEW, { body, signal, toast: false });
|
|
63
|
+
* if (!answer.ok || !answer.result) throw new Error(answer.message ?? "This cannot be worked out just now.");
|
|
64
|
+
* return answer.result;
|
|
65
|
+
* });
|
|
66
|
+
* <Form inputs={inputs} onValueChange={values => preview.request(counterBody(values))} />
|
|
67
|
+
* {preview.waiting || preview.loading ? "Working it out…" : preview.error ?? previewLine(preview.result)}
|
|
68
|
+
*/
|
|
69
|
+
export declare function useServerPreview<TQuestion, TResult>(ask: ServerPreviewAsk<TQuestion, TResult>, options?: ServerPreviewOptions): ServerPreview<TQuestion, TResult>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { isAbortError } from "./ApiResponses.js";
|
|
4
|
+
const IDLE = { result: null, question: null, waiting: false, loading: false, error: null };
|
|
5
|
+
/** The preview's questions and answers, outside React. */
|
|
6
|
+
export class ServerPreviewStore {
|
|
7
|
+
state = IDLE;
|
|
8
|
+
listeners = new Set();
|
|
9
|
+
timer;
|
|
10
|
+
running = null;
|
|
11
|
+
asked = 0;
|
|
12
|
+
/** Read on each question, so a page's latest function is the one asked. */
|
|
13
|
+
ask;
|
|
14
|
+
options;
|
|
15
|
+
constructor(ask, options = {}) {
|
|
16
|
+
this.ask = ask;
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
getState = () => this.state;
|
|
20
|
+
subscribe = (listener) => {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => this.listeners.delete(listener);
|
|
23
|
+
};
|
|
24
|
+
patch(changes) {
|
|
25
|
+
this.state = { ...this.state, ...changes };
|
|
26
|
+
for (const listener of [...this.listeners])
|
|
27
|
+
listener();
|
|
28
|
+
}
|
|
29
|
+
/** Stops the question waiting and the one running; their answers are never shown. */
|
|
30
|
+
stop() {
|
|
31
|
+
clearTimeout(this.timer);
|
|
32
|
+
this.timer = undefined;
|
|
33
|
+
this.running?.abort();
|
|
34
|
+
this.running = null;
|
|
35
|
+
this.asked++;
|
|
36
|
+
}
|
|
37
|
+
/** Asks about this question a moment after the last request (the delay), dropping any older one. */
|
|
38
|
+
request(question) {
|
|
39
|
+
this.stop();
|
|
40
|
+
const turn = this.asked;
|
|
41
|
+
this.patch({ waiting: true, loading: false, error: null });
|
|
42
|
+
this.timer = setTimeout(() => void this.send(question, turn), this.options.delay ?? 300);
|
|
43
|
+
}
|
|
44
|
+
async send(question, turn) {
|
|
45
|
+
this.timer = undefined;
|
|
46
|
+
const controller = new AbortController();
|
|
47
|
+
this.running = controller;
|
|
48
|
+
this.patch({ waiting: false, loading: true });
|
|
49
|
+
try {
|
|
50
|
+
const result = await this.ask()(question, controller.signal);
|
|
51
|
+
if (turn !== this.asked)
|
|
52
|
+
return;
|
|
53
|
+
this.running = null;
|
|
54
|
+
this.patch({ result, question, loading: false, error: null });
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (turn !== this.asked || isAbortError(error) || controller.signal.aborted)
|
|
58
|
+
return;
|
|
59
|
+
this.running = null;
|
|
60
|
+
const message = error instanceof Error && error.message ? error.message : (this.options.failure ?? "The preview cannot be worked out just now.");
|
|
61
|
+
this.patch({ loading: false, error: message });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Drops the question waiting or running; the last answer stays. */
|
|
65
|
+
cancel() {
|
|
66
|
+
this.stop();
|
|
67
|
+
if (this.state.waiting || this.state.loading)
|
|
68
|
+
this.patch({ waiting: false, loading: false });
|
|
69
|
+
}
|
|
70
|
+
/** Drops every question and the last answer: nothing is shown. */
|
|
71
|
+
clear() {
|
|
72
|
+
this.stop();
|
|
73
|
+
this.patch(IDLE);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* A preview the server works out a moment after the last change: `request(values)` on every change, and the page shows
|
|
78
|
+
* `result` (kept while a newer question runs), `waiting` (the pause), `loading` (with the server) and `error`. An older question is aborted and its late answer
|
|
79
|
+
* never shown; leaving the page stops what runs. `ask` throws to fail, with the words to show.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const preview = useServerPreview(async (body: CounterBody, signal) => {
|
|
83
|
+
* const answer = await api.send<CounterPreview>(COUNTER_PREVIEW, { body, signal, toast: false });
|
|
84
|
+
* if (!answer.ok || !answer.result) throw new Error(answer.message ?? "This cannot be worked out just now.");
|
|
85
|
+
* return answer.result;
|
|
86
|
+
* });
|
|
87
|
+
* <Form inputs={inputs} onValueChange={values => preview.request(counterBody(values))} />
|
|
88
|
+
* {preview.waiting || preview.loading ? "Working it out…" : preview.error ?? previewLine(preview.result)}
|
|
89
|
+
*/
|
|
90
|
+
export function useServerPreview(ask, options) {
|
|
91
|
+
const latest = useRef(ask);
|
|
92
|
+
latest.current = ask;
|
|
93
|
+
const [store] = useState(() => new ServerPreviewStore(() => latest.current, options));
|
|
94
|
+
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
95
|
+
// The same functions on every draw, so a page can name them in an effect's dependencies.
|
|
96
|
+
const [actions] = useState(() => ({ request: (question) => store.request(question), cancel: () => store.cancel(), clear: () => store.clear() }));
|
|
97
|
+
useEffect(() => () => store.cancel(), [store]);
|
|
98
|
+
return { ...state, ...actions };
|
|
99
|
+
}
|
|
@@ -40,12 +40,13 @@ export function headLeafCells(head) {
|
|
|
40
40
|
return cells;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
* The headings a dragged one can be dropped between: the ones pinned to the same side, in the order they are drawn
|
|
43
|
+
* The headings a dragged one can be dropped between: the ones pinned to the same side, in the order they are drawn -
|
|
44
|
+
* by the side chosen (`data-pin-side`), so a pinned column scrolling on a narrow table still moves among its side's.
|
|
44
45
|
* They are taken from the whole header, so a column under a band and one beside it are the same list.
|
|
45
46
|
*/
|
|
46
47
|
function dropTargets(head, heading) {
|
|
47
|
-
const
|
|
48
|
-
return headLeafCells(head).filter(cell => cell.hasAttribute(KEY) && (cell.getAttribute("data-pin") ?? "") ===
|
|
48
|
+
const side = heading.getAttribute("data-pin-side") ?? "";
|
|
49
|
+
return headLeafCells(head).filter(cell => cell.hasAttribute(KEY) && (cell.getAttribute("data-pin-side") ?? "") === side);
|
|
49
50
|
}
|
|
50
51
|
function clearDropMarks(head) {
|
|
51
52
|
for (const cell of head?.querySelectorAll(`[${DROP_BEFORE}], [${DROP_AFTER}]`) ?? []) {
|
|
@@ -17,7 +17,13 @@ export interface TableOutput<TRow extends TableRow = TableRow> {
|
|
|
17
17
|
export declare function tableToCsv<TRow extends TableRow>({ columns, rows, cellOptions }: TableOutput<TRow>): string;
|
|
18
18
|
/** A file name without characters a file system refuses. */
|
|
19
19
|
export declare function safeFileName(name: string): string;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Hands the user a file to save, from text made in the browser: a table's CSV, a person's recovery codes as a `.txt`.
|
|
22
|
+
* The text goes into a Blob behind a link put in the page, clicked and taken out again, and the Blob's address is let go
|
|
23
|
+
* ten seconds later. The file starts with a byte order mark, so Excel and Windows editors read it as UTF-8.
|
|
24
|
+
*
|
|
25
|
+
* @example downloadText("nexus-recovery-codes.txt", codes.join("\n"), "text/plain;charset=utf-8");
|
|
26
|
+
*/
|
|
21
27
|
export declare function downloadText(name: string, text: string, type: string): void;
|
|
22
28
|
/** Writes the table as a CSV file the user saves. */
|
|
23
29
|
export declare function exportTableCsv<TRow extends TableRow>(output: TableOutput<TRow>): void;
|
|
@@ -10,14 +10,20 @@ function csvField(text) {
|
|
|
10
10
|
export function tableToCsv({ columns, rows, cellOptions = {} }) {
|
|
11
11
|
const lines = [columns.map(column => csvField(column.label)).join(",")];
|
|
12
12
|
for (const row of rows)
|
|
13
|
-
lines.push(columns.map(column => csvField(formatCellText(column, columnValue(column, row), cellOptions))).join(","));
|
|
13
|
+
lines.push(columns.map(column => csvField(formatCellText(column, columnValue(column, row), cellOptions, row))).join(","));
|
|
14
14
|
return lines.join(CRLF) + CRLF;
|
|
15
15
|
}
|
|
16
16
|
/** A file name without characters a file system refuses. */
|
|
17
17
|
export function safeFileName(name) {
|
|
18
18
|
return name.replace(/[\/:*?"<>|]+/g, " ").trim().replace(/\s+/g, "-").toLowerCase() || "table";
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Hands the user a file to save, from text made in the browser: a table's CSV, a person's recovery codes as a `.txt`.
|
|
22
|
+
* The text goes into a Blob behind a link put in the page, clicked and taken out again, and the Blob's address is let go
|
|
23
|
+
* ten seconds later. The file starts with a byte order mark, so Excel and Windows editors read it as UTF-8.
|
|
24
|
+
*
|
|
25
|
+
* @example downloadText("nexus-recovery-codes.txt", codes.join("\n"), "text/plain;charset=utf-8");
|
|
26
|
+
*/
|
|
21
27
|
export function downloadText(name, text, type) {
|
|
22
28
|
// A BOM, so Excel reads the file as UTF-8 and shows accents and Devanagari.
|
|
23
29
|
const blob = new Blob([String.fromCharCode(0xfeff) + text], { type });
|
|
@@ -57,7 +63,7 @@ export function tableToPrintHtml({ columns, rows, cellOptions = {}, name = "Tabl
|
|
|
57
63
|
const cellClass = (column) => (align(column) === "end" ? ' class="nx-end"' : align(column) === "center" ? ' class="nx-center"' : "");
|
|
58
64
|
const head = columns.map(column => "<th" + cellClass(column) + ">" + escapeHtml(column.label) + "</th>").join("");
|
|
59
65
|
const body = rows
|
|
60
|
-
.map(row => "<tr>" + columns.map(column => "<td" + cellClass(column) + ">" + escapeHtml(formatCellText(column, columnValue(column, row), cellOptions)) + "</td>").join("") + "</tr>")
|
|
66
|
+
.map(row => "<tr>" + columns.map(column => "<td" + cellClass(column) + ">" + escapeHtml(formatCellText(column, columnValue(column, row), cellOptions, row)) + "</td>").join("") + "</tr>")
|
|
61
67
|
.join("");
|
|
62
68
|
return [
|
|
63
69
|
"<!doctype html><html><head><meta charset=\"utf-8\"><title>" + escapeHtml(name) + "</title><style>" + PRINT_STYLE + "</style></head><body>",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
2
2
|
import type { TableColumn, TableFilters, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
3
|
/** The params as a filter: the same object without `required`, `defaultValue`, `defaultChecked`. */
|
|
4
4
|
export declare function asFilterInput(params: InputParams): InputParams;
|