nexus-shared 3.2.0 → 4.0.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 +1564 -0
- package/README.md +1705 -1123
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- 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/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +576 -243
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +55 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -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 +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +123 -17
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -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 +195 -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/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +5 -5
- 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 +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -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 +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- 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 +53 -2
- package/dist/Components/Inputs/TreeView.js +147 -13
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- 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/Viewers/DataTable.js +150 -48
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +21 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -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/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +53 -6
- package/dist/Helpers/CrudColumns.js +98 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +81 -2
- package/dist/Helpers/CrudHelpers.js +195 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -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 +78 -0
- package/dist/Helpers/FileAdapter.js +102 -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 +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -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 +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- 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 +23 -2
- package/dist/Helpers/TableHelpers.js +51 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +21 -1
- package/dist/Helpers/TableStore.js +64 -7
- package/dist/Helpers/TreeStore.d.ts +81 -0
- package/dist/Helpers/TreeStore.js +365 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +465 -5
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +391 -30
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +15 -3
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +74 -1
- 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 +37 -5
- package/dist/Services/JoinSources.js +90 -14
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +16 -0
- package/dist/Shared.Index.js +16 -0
- package/package.json +7 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +10 -4
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +383 -9
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +4 -0
- package/src/Styles/Nexus.Input.css +42 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +77 -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 +67 -5
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +31 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { findRememberedOpening, forgetInputMemory, isEmptyInputValue, isPickerInput, lastAddedRowKey, readInputMemory, rememberDependencies, rememberedOpeningNow, writeInputMemory, } from "./InputMemory.js";
|
|
3
|
+
import { duplicateMessageFor, holdsValue, initialInputValue, inputValuesEqual, normalizeInputValue, rowsItemNames, rowsMessage, uniqueKeyOf, validateInput, } from "./InputParamsHelpers.js";
|
|
2
4
|
export class RowsStore {
|
|
3
5
|
options;
|
|
4
6
|
inputByName = new Map();
|
|
@@ -17,6 +19,12 @@ export class RowsStore {
|
|
|
17
19
|
showAll = false;
|
|
18
20
|
touched = new Map();
|
|
19
21
|
removed = null;
|
|
22
|
+
// Remembered row inputs (`remember`): where they are kept, the rows added since the rows were loaded (in the order they
|
|
23
|
+
// were added: a save keeps the last one's values), and the cells a picker is still asked about.
|
|
24
|
+
memory = null;
|
|
25
|
+
added = [];
|
|
26
|
+
asking = new Map();
|
|
27
|
+
memoryRound = null;
|
|
20
28
|
// Rendering: a revision per value set from code (its input mounts again with it), a version per row.
|
|
21
29
|
revisions = new Map();
|
|
22
30
|
versions = new Map();
|
|
@@ -182,18 +190,25 @@ export class RowsStore {
|
|
|
182
190
|
if (!this.showAll && this.duplicateErrors.get(key)?.has(name))
|
|
183
191
|
this.bump(key);
|
|
184
192
|
}
|
|
185
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Adds a row with the inputs' defaults, `newRow`, and `values`, at `index` or at the end. Returns its key. A remembered
|
|
195
|
+
* input (`remember`) that neither `newRow` nor `values` names with a value opens on the value kept for it, while its
|
|
196
|
+
* input offers it; a picker's opens once the picker answers, unless the person picked one first.
|
|
197
|
+
*/
|
|
186
198
|
add(values, index = this.entries.length) {
|
|
187
199
|
const position = Math.max(0, Math.min(index, this.entries.length));
|
|
188
200
|
const wasEmpty = this.entries.length === 0;
|
|
189
201
|
const row = {};
|
|
190
202
|
for (const params of this.options.inputs)
|
|
191
|
-
|
|
203
|
+
if (holdsValue(params))
|
|
204
|
+
row[params.name] = initialInputValue(params);
|
|
192
205
|
const { newRow } = this.options;
|
|
193
|
-
|
|
206
|
+
const named = { ...(typeof newRow === "function" ? newRow(position) : newRow), ...values };
|
|
207
|
+
Object.assign(row, named);
|
|
208
|
+
const asks = this.openRemembered(row, named);
|
|
194
209
|
// The first row of a list has each radio on: the first address added is the primary one.
|
|
195
210
|
for (const params of this.options.inputs) {
|
|
196
|
-
if (params.type ===
|
|
211
|
+
if (params.type === EInputTypes.Radio && wasEmpty && values?.[params.name] === undefined)
|
|
197
212
|
row[params.name] = true;
|
|
198
213
|
}
|
|
199
214
|
const entry = { key: this.newKey(), values: this.normalizeRow(row) };
|
|
@@ -203,15 +218,41 @@ export class RowsStore {
|
|
|
203
218
|
for (const params of this.options.inputs) {
|
|
204
219
|
this.checkCell(entry, params);
|
|
205
220
|
// Only one row keeps a radio on: a new row that has it takes it from the others.
|
|
206
|
-
if (params.type ===
|
|
221
|
+
if (params.type === EInputTypes.Radio && entry.values[params.name] === true)
|
|
207
222
|
this.radioOn(entry, params.name, changed);
|
|
208
223
|
}
|
|
209
224
|
this.messageShown = true;
|
|
210
225
|
this.removed = null;
|
|
226
|
+
this.added.push(entry.key);
|
|
211
227
|
this.markUniqueDirty();
|
|
212
228
|
this.settle(changed);
|
|
229
|
+
this.askPickers(entry.key, asks);
|
|
213
230
|
return entry.key;
|
|
214
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The storage remembered row inputs read and keep (`InputLayoutParams.remember`), given once the rows are drawn in the
|
|
234
|
+
* browser: rows added from then on open on the kept values. The rows already there keep theirs.
|
|
235
|
+
*/
|
|
236
|
+
attachMemory(storage) {
|
|
237
|
+
this.memory = storage;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* A save of the form holding the rows: each remembered input keeps its value in the row added last that is still there
|
|
241
|
+
* (an empty value forgets), under that row's `per` value and where its `when` holds on that row. No row added since the
|
|
242
|
+
* rows were loaded: nothing is kept, since no new choice was made.
|
|
243
|
+
*/
|
|
244
|
+
rememberAdded() {
|
|
245
|
+
if (!this.memory)
|
|
246
|
+
return;
|
|
247
|
+
const key = lastAddedRowKey(this.added, added => this.positions.has(added));
|
|
248
|
+
const row = key === null ? undefined : this.row(key);
|
|
249
|
+
if (!row)
|
|
250
|
+
return;
|
|
251
|
+
const read = (name) => row[name];
|
|
252
|
+
for (const params of this.options.inputs)
|
|
253
|
+
if (params.remember && !params.hidden)
|
|
254
|
+
writeInputMemory(this.memory, params, row[params.name], read);
|
|
255
|
+
}
|
|
215
256
|
/** Adds a copy of a row after it, without its id and with its radios off. Returns the copy's key. */
|
|
216
257
|
duplicate(key) {
|
|
217
258
|
const index = this.positions.get(key);
|
|
@@ -222,7 +263,7 @@ export class RowsStore {
|
|
|
222
263
|
if (this.options.rowKey)
|
|
223
264
|
delete values[this.options.rowKey];
|
|
224
265
|
for (const params of this.options.inputs)
|
|
225
|
-
if (params.type ===
|
|
266
|
+
if (params.type === EInputTypes.Radio)
|
|
226
267
|
values[params.name] = false;
|
|
227
268
|
return this.add(values, index + 1);
|
|
228
269
|
}
|
|
@@ -243,7 +284,7 @@ export class RowsStore {
|
|
|
243
284
|
const moved = [];
|
|
244
285
|
const first = this.entries[0];
|
|
245
286
|
for (const params of this.options.inputs) {
|
|
246
|
-
if (params.type !==
|
|
287
|
+
if (params.type !== EInputTypes.Radio || entry.values[params.name] !== true || !first)
|
|
247
288
|
continue;
|
|
248
289
|
this.write(first.key, params.name, true, "code", changed, 1);
|
|
249
290
|
moved.push({ key: first.key, name: params.name });
|
|
@@ -306,6 +347,9 @@ export class RowsStore {
|
|
|
306
347
|
*/
|
|
307
348
|
replace(rows, { asInitial = true, emit = false } = {}) {
|
|
308
349
|
this.load(rows);
|
|
350
|
+
// Loaded rows: none of them was added here, and no picker's answer is wanted any more.
|
|
351
|
+
this.added = [];
|
|
352
|
+
this.stopAskingPickers();
|
|
309
353
|
if (asInitial)
|
|
310
354
|
this.initial = this.getValue();
|
|
311
355
|
this.generation++;
|
|
@@ -363,7 +407,7 @@ export class RowsStore {
|
|
|
363
407
|
});
|
|
364
408
|
// Saved data with a radio on in more than one row keeps the first.
|
|
365
409
|
for (const params of this.options.inputs) {
|
|
366
|
-
if (params.type !==
|
|
410
|
+
if (params.type !== EInputTypes.Radio)
|
|
367
411
|
continue;
|
|
368
412
|
let found = false;
|
|
369
413
|
for (const entry of this.entries) {
|
|
@@ -383,6 +427,76 @@ export class RowsStore {
|
|
|
383
427
|
this.valueCache = null;
|
|
384
428
|
this.keysCache = null;
|
|
385
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Opens a new row's remembered inputs on their kept values, where the row is not added with one: the company's
|
|
432
|
+
* `preferred`, else the person's last choice, while the input offers it (`rememberedOpeningNow`, `when` and `per` read
|
|
433
|
+
* in the row). A kept choice no longer offered is forgotten. Answers the pickers that must be asked first.
|
|
434
|
+
*/
|
|
435
|
+
openRemembered(row, named) {
|
|
436
|
+
const asks = [];
|
|
437
|
+
const read = (name) => row[name];
|
|
438
|
+
for (const params of this.options.inputs) {
|
|
439
|
+
const { name } = params;
|
|
440
|
+
if (!params.remember || params.hidden || !holdsValue(params))
|
|
441
|
+
continue;
|
|
442
|
+
if (name in named && !isEmptyInputValue(params, named[name]))
|
|
443
|
+
continue;
|
|
444
|
+
if (!this.memory && isPickerInput(params))
|
|
445
|
+
continue;
|
|
446
|
+
const stored = this.memory ? readInputMemory(this.memory, params.remember, read) : undefined;
|
|
447
|
+
const opening = rememberedOpeningNow(params, stored, read);
|
|
448
|
+
if (opening.forget)
|
|
449
|
+
forgetInputMemory(this.memory, params.remember, read);
|
|
450
|
+
if (opening.ask)
|
|
451
|
+
asks.push({ params, stored });
|
|
452
|
+
if (opening.value !== undefined)
|
|
453
|
+
row[name] = opening.value;
|
|
454
|
+
}
|
|
455
|
+
return asks;
|
|
456
|
+
}
|
|
457
|
+
/** Stops asking pickers about remembered values: their answers no longer open anything. */
|
|
458
|
+
stopAskingPickers() {
|
|
459
|
+
this.memoryRound?.abort();
|
|
460
|
+
this.memoryRound = null;
|
|
461
|
+
this.asking.clear();
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Asks each picker whether it still offers the remembered value of a new row's input (`findRememberedOpening`), judged
|
|
465
|
+
* on the row as it was added: an answer forgets a choice no longer offered (under that row's `per` value), and opens an
|
|
466
|
+
* offered one while the row is there, the person has not picked in that cell since, and the row's values it was judged
|
|
467
|
+
* on (`rememberDependencies`) are unchanged.
|
|
468
|
+
*/
|
|
469
|
+
askPickers(key, asks) {
|
|
470
|
+
const added = this.row(key);
|
|
471
|
+
if (!asks.length || !added)
|
|
472
|
+
return;
|
|
473
|
+
this.memoryRound ??= new AbortController();
|
|
474
|
+
const round = this.memoryRound;
|
|
475
|
+
const memory = this.memory;
|
|
476
|
+
const names = this.asking.get(key) ?? new Set();
|
|
477
|
+
this.asking.set(key, names);
|
|
478
|
+
const opening = { ...added };
|
|
479
|
+
const read = (name) => opening[name];
|
|
480
|
+
for (const { params, stored } of asks) {
|
|
481
|
+
names.add(params.name);
|
|
482
|
+
const judgedOn = rememberDependencies(params);
|
|
483
|
+
void findRememberedOpening(params, stored, read, round.signal).then(answer => {
|
|
484
|
+
if (round.signal.aborted)
|
|
485
|
+
return;
|
|
486
|
+
if (answer.forget)
|
|
487
|
+
forgetInputMemory(memory, params.remember, read);
|
|
488
|
+
const waiting = this.asking.get(key);
|
|
489
|
+
if (!waiting?.delete(params.name))
|
|
490
|
+
return;
|
|
491
|
+
if (!waiting.size)
|
|
492
|
+
this.asking.delete(key);
|
|
493
|
+
const row = this.row(key);
|
|
494
|
+
if (answer.value === undefined || !row || !judgedOn.every(name => inputValuesEqual(row[name], opening[name])))
|
|
495
|
+
return;
|
|
496
|
+
this.set(key, params.name, answer.value);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
386
500
|
/** Drops what the store knew about a removed row. */
|
|
387
501
|
forget(key) {
|
|
388
502
|
this.invalidCells -= this.invalidByRow.get(key) ?? 0;
|
|
@@ -391,6 +505,7 @@ export class RowsStore {
|
|
|
391
505
|
this.duplicateErrors.delete(key);
|
|
392
506
|
this.touched.delete(key);
|
|
393
507
|
this.revisions.delete(key);
|
|
508
|
+
this.asking.delete(key);
|
|
394
509
|
}
|
|
395
510
|
write(key, name, value, source, changed, depth) {
|
|
396
511
|
const index = this.positions.get(key);
|
|
@@ -399,6 +514,9 @@ export class RowsStore {
|
|
|
399
514
|
return;
|
|
400
515
|
const params = this.inputByName.get(name);
|
|
401
516
|
const next = params && source === "code" ? normalizeInputValue(params, value) : value;
|
|
517
|
+
// The person's own pick: a picker's answer about a remembered value no longer replaces it.
|
|
518
|
+
if (source === "input")
|
|
519
|
+
this.asking.get(key)?.delete(name);
|
|
402
520
|
if (inputValuesEqual(entry.values[name], next))
|
|
403
521
|
return;
|
|
404
522
|
const shownBefore = this.shownError(key, name);
|
|
@@ -414,7 +532,7 @@ export class RowsStore {
|
|
|
414
532
|
}
|
|
415
533
|
if (params) {
|
|
416
534
|
this.checkCell(entry, params);
|
|
417
|
-
if (params.type ===
|
|
535
|
+
if (params.type === EInputTypes.Radio) {
|
|
418
536
|
// The row shows which radios it has on (a section's "Primary" tag), so it renders again, typed or not.
|
|
419
537
|
changed.add(key);
|
|
420
538
|
if (next === true)
|
|
@@ -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;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
1
2
|
import { addDays } from "./DateHelpers.js";
|
|
2
3
|
import { fromLocalDateTime, toLocalDateTime, toUtcDateTime } from "./DateTimeHelpers.js";
|
|
3
4
|
import { foldSearchText, searchWords } from "./DropdownHelpers.js";
|
|
4
|
-
import { emptyInputValue, inputFieldName, inputValuesEqual, isEmptyInputValue } from "./InputParamsHelpers.js";
|
|
5
|
+
import { emptyInputValue, inputFieldName, inputTypeName, inputValuesEqual, isEmptyInputValue } from "./InputParamsHelpers.js";
|
|
5
6
|
import { columnValue, readPath, searchTextOf } from "./TableHelpers.js";
|
|
6
7
|
/*
|
|
7
8
|
* A data table's advanced filter, with no React: input params as filters, the values that filter, a client table's
|
|
8
9
|
* test of a row, and the names a server takes.
|
|
9
10
|
*/
|
|
10
11
|
// Inputs whose value cannot filter a row, and names a Nexus page request already uses for its paging.
|
|
11
|
-
const UNSUPPORTED = new Set([
|
|
12
|
+
const UNSUPPORTED = new Set([EInputTypes.Markdown, EInputTypes.RichText, EInputTypes.Tree, EInputTypes.Tabular, EInputTypes.Group, EInputTypes.Password, EInputTypes.Picture]);
|
|
12
13
|
const RESERVED = new Set(["searchKeyword", "skipCount", "maxCount", "sorting", "sortType"]);
|
|
13
14
|
const warned = new Set();
|
|
14
15
|
function filterable(params) {
|
|
@@ -17,7 +18,7 @@ function filterable(params) {
|
|
|
17
18
|
return true;
|
|
18
19
|
if (!warned.has(params.name)) {
|
|
19
20
|
warned.add(params.name);
|
|
20
|
-
console.warn(`DataTable: the filter "${params.name}" is ignored: ${reserved ? "a page request already sends that name" : `a ${params.type} input cannot filter rows`}.`);
|
|
21
|
+
console.warn(`DataTable: the filter "${params.name}" is ignored: ${reserved ? "a page request already sends that name" : `a ${inputTypeName(params.type)} input cannot filter rows`}.`);
|
|
21
22
|
}
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
@@ -62,12 +63,12 @@ function cellTest(params, value, column, bound) {
|
|
|
62
63
|
// A moment's day is the one the table shows it on; any other value is read as an ISO date.
|
|
63
64
|
const dayOf = (cell) => (column?.format === "date-time" ? toLocalDateTime(cell)?.date : String(cell).slice(0, 10));
|
|
64
65
|
switch (params.type) {
|
|
65
|
-
case
|
|
66
|
-
case
|
|
67
|
-
case
|
|
68
|
-
case
|
|
69
|
-
case
|
|
70
|
-
case
|
|
66
|
+
case EInputTypes.Text:
|
|
67
|
+
case EInputTypes.Email:
|
|
68
|
+
case EInputTypes.Search:
|
|
69
|
+
case EInputTypes.Tel:
|
|
70
|
+
case EInputTypes.Url:
|
|
71
|
+
case EInputTypes.TextArea: {
|
|
71
72
|
// Every word, folded as the search folds it, in the text the cell shows (an option's label, Yes or No).
|
|
72
73
|
const words = searchWords(String(value));
|
|
73
74
|
return (cell, row) => {
|
|
@@ -75,31 +76,31 @@ function cellTest(params, value, column, bound) {
|
|
|
75
76
|
return words.every(word => text.includes(word));
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
|
-
case
|
|
79
|
-
case
|
|
80
|
-
case
|
|
79
|
+
case EInputTypes.CheckBox:
|
|
80
|
+
case EInputTypes.Switch:
|
|
81
|
+
case EInputTypes.Radio:
|
|
81
82
|
return cell => cell === true || cell === "true" || cell === 1;
|
|
82
|
-
case
|
|
83
|
-
case
|
|
83
|
+
case EInputTypes.Number:
|
|
84
|
+
case EInputTypes.Slider: {
|
|
84
85
|
const number = Number(value);
|
|
85
86
|
return cell => (bound === "From" ? Number(cell) >= number : bound === "To" ? Number(cell) <= number : Number(cell) === number);
|
|
86
87
|
}
|
|
87
|
-
case
|
|
88
|
+
case EInputTypes.RangeSlider: {
|
|
88
89
|
const [low, high] = value;
|
|
89
90
|
return cell => Number(cell) >= low && Number(cell) <= high;
|
|
90
91
|
}
|
|
91
|
-
case
|
|
92
|
+
case EInputTypes.Date:
|
|
92
93
|
return cell => dayOf(cell) === value;
|
|
93
|
-
case
|
|
94
|
+
case EInputTypes.DateRange: {
|
|
94
95
|
const [from, to] = value;
|
|
95
96
|
return cell => {
|
|
96
97
|
const day = dayOf(cell);
|
|
97
98
|
return day !== undefined && (!from || day >= from) && (!to || day <= to);
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
case
|
|
101
|
+
case EInputTypes.Time:
|
|
101
102
|
return cell => String(cell).slice(0, 5) === String(value).slice(0, 5);
|
|
102
|
-
case
|
|
103
|
+
case EInputTypes.DateTime: {
|
|
103
104
|
// The same minute, however each side writes the moment ("…Z", "+05:45", or no zone for UTC).
|
|
104
105
|
const minute = (moment) => toUtcDateTime(moment)?.slice(0, 16);
|
|
105
106
|
const wanted = minute(value);
|
|
@@ -121,7 +122,7 @@ export function filterMatcher(columns, inputs, filters) {
|
|
|
121
122
|
const tests = activeInputs(inputs, filters).map(([params, value]) => {
|
|
122
123
|
const { name } = params;
|
|
123
124
|
let column = columns.find(candidate => candidate.key === name);
|
|
124
|
-
const [, key, bound] = (!column && params.type ===
|
|
125
|
+
const [, key, bound] = (!column && params.type === EInputTypes.Number && /^(.+)(From|To)$/.exec(name)) || [];
|
|
125
126
|
column ??= columns.find(candidate => candidate.key === key);
|
|
126
127
|
const test = cellTest(params, value, column, column ? bound : undefined);
|
|
127
128
|
return (row) => {
|
|
@@ -149,11 +150,11 @@ export function filterRequest(filters, inputs, columns, timeZone) {
|
|
|
149
150
|
if (to)
|
|
150
151
|
request[`${name}Before`] = moments ? fromLocalDateTime(addDays(to, 1), "00:00", timeZone) : addDays(to, 1);
|
|
151
152
|
};
|
|
152
|
-
if (params.type ===
|
|
153
|
+
if (params.type === EInputTypes.DateRange)
|
|
153
154
|
days(...value);
|
|
154
|
-
else if (params.type ===
|
|
155
|
+
else if (params.type === EInputTypes.Date && moments)
|
|
155
156
|
days(value, value);
|
|
156
|
-
else if (params.type ===
|
|
157
|
+
else if (params.type === EInputTypes.RangeSlider)
|
|
157
158
|
[request[`${name}From`], request[`${name}To`]] = value;
|
|
158
159
|
else
|
|
159
160
|
request[name] = value;
|
|
@@ -26,12 +26,17 @@ export interface CellTextOptions {
|
|
|
26
26
|
timeZone?: string;
|
|
27
27
|
/** How numbers are written: a locale, or the person's settings (`numberLocale`, `useNumberLocale`). Default `en-US`. */
|
|
28
28
|
locale?: string;
|
|
29
|
+
/** The currency of a `money` column without its own (`DataTableProps.currency`): "Rs.". */
|
|
30
|
+
currency?: string | null;
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
31
33
|
* A value as the cell writes it, in the column's format: "1,250.00", "Rs. 4,500.00", "12.5%", the date and time in the
|
|
32
|
-
* user's settings, "Yes" or "No", an option's label. Empty values give "".
|
|
34
|
+
* user's settings, "Yes" or "No", an option's label. Empty values give "". `row` is the record, for a currency the
|
|
35
|
+
* column reads from it (`currency: row => row.currencyText`).
|
|
33
36
|
*/
|
|
34
|
-
export declare function formatCellText<TRow extends TableRow>(column: TableColumn<TRow>, value: unknown, options?: CellTextOptions): string;
|
|
37
|
+
export declare function formatCellText<TRow extends TableRow>(column: TableColumn<TRow>, value: unknown, options?: CellTextOptions, row?: TRow): string;
|
|
38
|
+
/** A money column's currency: its own (a text, or read from the row), else the table's; "" for none. */
|
|
39
|
+
export declare function columnCurrency<TRow extends TableRow>(column: TableColumn<TRow>, options?: CellTextOptions, row?: TRow): string;
|
|
35
40
|
/** Text the search looks in: the column's `text`, an option's label, or the value as text (dates as ISO text). */
|
|
36
41
|
export declare function searchTextOf<TRow extends TableRow>(column: TableColumn<TRow>, row: TRow): string;
|
|
37
42
|
/** Compares two values of a column for sorting: numbers as numbers, text as a person reads it ("Room 9" before "Room 10"). */
|
|
@@ -95,7 +100,23 @@ export interface ViewColumnsOptions extends ColumnRights {
|
|
|
95
100
|
widths?: Readonly<Record<string, number>>;
|
|
96
101
|
/** Pixels from the pinned side, measured from the columns before it. */
|
|
97
102
|
offsets?: Readonly<Record<string, number>>;
|
|
103
|
+
/** Pinned columns that scroll for now, the table being too narrow for them (`releasedPins`): their `pin` is null. */
|
|
104
|
+
released?: ReadonlySet<string>;
|
|
98
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The most of a table's width on screen that the columns staying in place may take: the numbers, the checkboxes and the
|
|
108
|
+
* actions, which always stay, and the pinned columns. Past it the pinned ones give way (`releasedPins`), so a phone
|
|
109
|
+
* keeps at least 40% of the table for the columns that scroll.
|
|
110
|
+
*/
|
|
111
|
+
export declare const PINNED_SHARE = 0.6;
|
|
112
|
+
/**
|
|
113
|
+
* Which pinned columns scroll for now, the table being narrow: the pinned ones stay from the outer edge in - the left
|
|
114
|
+
* side first - while they and the columns that always stay (`frame`: the numbers and checkboxes, the actions; their
|
|
115
|
+
* width in pixels) take at most `share` of `view`, the width on screen; every one past that scrolls with the rest,
|
|
116
|
+
* and so do the ones inside it on the same side. `left` and `right`: each side's pinned columns with their widths,
|
|
117
|
+
* from the outer edge in. A table not laid out (`view` 0, in a closed tab) releases none.
|
|
118
|
+
*/
|
|
119
|
+
export declare function releasedPins(view: number, frame: number, left: readonly (readonly [string, number])[], right: readonly (readonly [string, number])[], share?: number): Set<string>;
|
|
99
120
|
/**
|
|
100
121
|
* The columns as the table draws them: in the user's order, with the pinned ones moved to their side (keeping their
|
|
101
122
|
* order among themselves), each with its width, its distance from that edge, and what the user may do with it.
|