nexus-shared 3.1.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 +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -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 +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -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 +56 -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 +125 -16
- 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/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -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 +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- 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 +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- 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/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- 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/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- 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 +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -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 +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- 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 +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- 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 +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- 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.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- package/src/Styles/Nexus.Tree.css +530 -431
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { DEFAULT_FIELD_NAME, requiredMessage } from "./ValidationHelpers.js";
|
|
2
|
+
/*
|
|
3
|
+
* Codes typed one character to a box: a one-time code, a PIN, an SSN, a licence key, a hex colour. Pure functions, so a
|
|
4
|
+
* form checks a code it loaded the way the box checks one typed, without a browser.
|
|
5
|
+
*/
|
|
6
|
+
/** The boxes a code has when it names neither `length` nor `groups`. */
|
|
7
|
+
export const DEFAULT_CODE_LENGTH = 6;
|
|
8
|
+
/** One character of each kind, as a regular expression. */
|
|
9
|
+
export const CODE_CHARACTER_PATTERNS = {
|
|
10
|
+
digits: "[0-9]",
|
|
11
|
+
letters: "[A-Za-z]",
|
|
12
|
+
alphanumeric: "[0-9A-Za-z]",
|
|
13
|
+
hex: "[0-9A-Fa-f]",
|
|
14
|
+
};
|
|
15
|
+
/** What messages call the characters: "The code needs all 6 digits." */
|
|
16
|
+
const CHARACTER_NAMES = { digits: "digits", letters: "letters", alphanumeric: "characters", hex: "hex digits" };
|
|
17
|
+
/** What a code takes, for "SSN takes digits only." */
|
|
18
|
+
const ONLY_NAMES = { digits: "digits", letters: "letters", alphanumeric: "letters and digits", hex: "hex digits, 0–9 and A–F" };
|
|
19
|
+
/** The boxes in their groups: `groups` when given (`[3, 2, 4]`), else one group of `length`, 6 by default. */
|
|
20
|
+
export function codeGroups(rules) {
|
|
21
|
+
const groups = (rules.groups ?? []).filter(size => Number.isInteger(size) && size > 0);
|
|
22
|
+
if (groups.length)
|
|
23
|
+
return groups;
|
|
24
|
+
const { length = DEFAULT_CODE_LENGTH } = rules;
|
|
25
|
+
return [Number.isInteger(length) && length > 0 ? length : DEFAULT_CODE_LENGTH];
|
|
26
|
+
}
|
|
27
|
+
/** How many boxes a code has: 9 for `[3, 2, 4]`. */
|
|
28
|
+
export const codeLength = (rules) => codeGroups(rules).reduce((sum, size) => sum + size, 0);
|
|
29
|
+
const characterTests = new Map();
|
|
30
|
+
/** Whether one character is one the code takes. Built once per pattern, since a form may check thousands of codes. */
|
|
31
|
+
function characterTest(rules) {
|
|
32
|
+
const source = rules.allowed || CODE_CHARACTER_PATTERNS[rules.characters ?? "digits"];
|
|
33
|
+
let test = characterTests.get(source);
|
|
34
|
+
if (!test) {
|
|
35
|
+
test = new RegExp(`^(?:${source})$`, "u");
|
|
36
|
+
characterTests.set(source, test);
|
|
37
|
+
}
|
|
38
|
+
return test;
|
|
39
|
+
}
|
|
40
|
+
function withCase(text, letterCase = "upper") {
|
|
41
|
+
return letterCase === "upper" ? text.toUpperCase() : letterCase === "lower" ? text.toLowerCase() : text;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Text as the code holds it: letters in the code's case, every character it does not take dropped, and cut to its
|
|
45
|
+
* length: "123-45-6789" is "123456789" for an SSN. The box never does this to a value on its own: text that arrives in
|
|
46
|
+
* it at once is read by `readCodeText`, which finds the code in a message, and a saved value by `loadCode`, which keeps
|
|
47
|
+
* what does not fit so its check can say so.
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeCode(text, rules = {}) {
|
|
50
|
+
if (!text)
|
|
51
|
+
return "";
|
|
52
|
+
const test = characterTest(rules);
|
|
53
|
+
const length = codeLength(rules);
|
|
54
|
+
const kept = [];
|
|
55
|
+
for (const character of withCase(text, rules.letterCase)) {
|
|
56
|
+
if (kept.length === length)
|
|
57
|
+
break;
|
|
58
|
+
if (test.test(character))
|
|
59
|
+
kept.push(character);
|
|
60
|
+
}
|
|
61
|
+
return kept.join("");
|
|
62
|
+
}
|
|
63
|
+
/** One space, dash, slash or dot between the groups of a code written out: "482 913", "123-45-6789", "03/04/1990". */
|
|
64
|
+
const GROUP_JOINER = /^[\s./–-]$/u;
|
|
65
|
+
/**
|
|
66
|
+
* A code as it was saved or given (a form's loaded record, `value`, `defaultValue`), as the box holds it. It loses only
|
|
67
|
+
* what writing a code out adds: letters take the code's case, and separators the code does not take - spaces, dashes,
|
|
68
|
+
* slashes, dots, the field's own `separator`, and a hex code's leading "#" - come out: "123-45-6789" is "123456789".
|
|
69
|
+
* Nothing else changes. A character the code does not take, or one past its boxes, stays, so its check says what is
|
|
70
|
+
* wrong ("SSN takes digits only.", "The code is 6 digits long, not 7.") and a form never saves it back changed.
|
|
71
|
+
*/
|
|
72
|
+
export function loadCode(value, shape = {}) {
|
|
73
|
+
if (value === null || value === undefined)
|
|
74
|
+
return "";
|
|
75
|
+
const test = characterTest(shape);
|
|
76
|
+
const separator = shape.separator ?? "";
|
|
77
|
+
const hex = shape.characters === "hex" && !shape.allowed;
|
|
78
|
+
let kept = "";
|
|
79
|
+
for (const character of withCase(String(value), shape.letterCase)) {
|
|
80
|
+
const writtenOut = GROUP_JOINER.test(character) || (separator !== "" && separator.includes(character)) || (hex && character === "#" && kept === "");
|
|
81
|
+
if (writtenOut && !test.test(character))
|
|
82
|
+
continue;
|
|
83
|
+
kept += character;
|
|
84
|
+
}
|
|
85
|
+
return kept;
|
|
86
|
+
}
|
|
87
|
+
/** Whether the boxes can hold a code as it is: every character one the code takes, and no more than the boxes. */
|
|
88
|
+
export function codeFits(value, rules = {}) {
|
|
89
|
+
const characters = [...(value ?? "")];
|
|
90
|
+
if (characters.length > codeLength(rules))
|
|
91
|
+
return false;
|
|
92
|
+
const test = characterTest(rules);
|
|
93
|
+
return characters.every(character => test.test(withCase(character, rules.letterCase)));
|
|
94
|
+
}
|
|
95
|
+
/** What a description calls one character of a code, and several: "6 digits", "1 letter". */
|
|
96
|
+
const DESCRIBED_NAMES = {
|
|
97
|
+
digits: ["digit", "digits"],
|
|
98
|
+
letters: ["letter", "letters"],
|
|
99
|
+
alphanumeric: ["letter or digit", "letters and digits"],
|
|
100
|
+
hex: ["hex digit", "hex digits"],
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* What a screen reader is told about the boxes it cannot see, after the label: how many characters of which kind, in
|
|
104
|
+
* which groups, and the hint in each box - "6 digits", "9 digits, in groups of 3, 2 and 4", "8 digits: DD/MM/YYYY".
|
|
105
|
+
*/
|
|
106
|
+
export function describeCode(shape = {}) {
|
|
107
|
+
const groups = codeGroups(shape);
|
|
108
|
+
const total = codeLength(shape);
|
|
109
|
+
const [one, many] = shape.allowed ? ["character", "characters"] : DESCRIBED_NAMES[shape.characters ?? "digits"];
|
|
110
|
+
const count = `${total} ${total === 1 ? one : many}`;
|
|
111
|
+
const hints = [...(shape.slotPlaceholder ?? "")];
|
|
112
|
+
if (hints.length === total && total > 1)
|
|
113
|
+
return `${count}: ${formatCode(hints.join(""), { groups, separator: shape.separator })}`;
|
|
114
|
+
if (groups.length < 2)
|
|
115
|
+
return count;
|
|
116
|
+
const sizes = groups.map(String);
|
|
117
|
+
return `${count}, in groups of ${sizes.slice(0, -1).join(", ")} and ${sizes[sizes.length - 1]}`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The code in text that arrives at once - a paste, a drop, a keyboard's suggestion, autofill, an input method's word.
|
|
121
|
+
* It is whole when the text holds one code of exactly as many characters as the boxes: one run of them ("482913" in
|
|
122
|
+
* "Your 2-step code is 482913"), or one run split by single spaces, dashes, slashes or dots ("482 913", "123-45-6789"),
|
|
123
|
+
* or every character the text holds when they number exactly that ("(482) 913"). Otherwise its characters are a part of
|
|
124
|
+
* a code, or `null` when they are more than the boxes ("Codes 4829135 and 1234567"): a message is never guessed at.
|
|
125
|
+
*/
|
|
126
|
+
export function readCodeText(text, rules = {}) {
|
|
127
|
+
const test = characterTest(rules);
|
|
128
|
+
const total = codeLength(rules);
|
|
129
|
+
// The runs of characters the code takes, each marked when a single joiner links it to the run before.
|
|
130
|
+
const runs = [];
|
|
131
|
+
let run = null;
|
|
132
|
+
let gap = [];
|
|
133
|
+
for (const character of withCase(text ?? "", rules.letterCase)) {
|
|
134
|
+
if (!test.test(character)) {
|
|
135
|
+
run = null;
|
|
136
|
+
gap.push(character);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (!run) {
|
|
140
|
+
const joiner = gap.length === 1 ? gap[0] : undefined;
|
|
141
|
+
run = { characters: [], joined: runs.length > 0 && joiner !== undefined && GROUP_JOINER.test(joiner) };
|
|
142
|
+
runs.push(run);
|
|
143
|
+
}
|
|
144
|
+
run.characters.push(character);
|
|
145
|
+
gap = [];
|
|
146
|
+
}
|
|
147
|
+
// Each way of writing a whole code; two different ones are no answer.
|
|
148
|
+
const candidates = new Set();
|
|
149
|
+
for (const run of runs) {
|
|
150
|
+
if (run.characters.length === total)
|
|
151
|
+
candidates.add(run.characters.join(""));
|
|
152
|
+
}
|
|
153
|
+
let group = [];
|
|
154
|
+
let parts = 0;
|
|
155
|
+
for (const [index, run] of runs.entries()) {
|
|
156
|
+
if (index > 0 && !run.joined) {
|
|
157
|
+
if (parts > 1 && group.length === total)
|
|
158
|
+
candidates.add(group.join(""));
|
|
159
|
+
group = [];
|
|
160
|
+
parts = 0;
|
|
161
|
+
}
|
|
162
|
+
group.push(...run.characters);
|
|
163
|
+
parts++;
|
|
164
|
+
}
|
|
165
|
+
if (parts > 1 && group.length === total)
|
|
166
|
+
candidates.add(group.join(""));
|
|
167
|
+
if (candidates.size === 1) {
|
|
168
|
+
const [code = ""] = candidates;
|
|
169
|
+
return { code, whole: true };
|
|
170
|
+
}
|
|
171
|
+
const all = runs.flatMap(run => run.characters);
|
|
172
|
+
if (all.length > total)
|
|
173
|
+
return null;
|
|
174
|
+
return { code: all.join(""), whole: all.length === total };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A full code with its caret at the end selects its last box, so the next key replaces it and Backspace or Delete
|
|
178
|
+
* remove it: the box with the ring is the one they act on. Any other edit is returned as it is.
|
|
179
|
+
*/
|
|
180
|
+
export function settleCode(edit, rules = {}) {
|
|
181
|
+
const total = codeLength(rules);
|
|
182
|
+
if (edit.start === edit.end && edit.start >= total && [...edit.value].length === total)
|
|
183
|
+
return { value: edit.value, start: total - 1, end: total };
|
|
184
|
+
return edit;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A code after text arrives at its caret - the same for a key, a phone's keyboard, an input method and a paste. A whole
|
|
188
|
+
* code (see `readCodeText`) replaces everything. Otherwise each character goes in as if typed: over the selection, or at
|
|
189
|
+
* a caret into the box after it - inserted while the code has an empty box, so a gap Backspace or Delete left takes back
|
|
190
|
+
* what was there and the boxes after it move along, and over that box once the code is full. A key at the end of a full
|
|
191
|
+
* code takes the last box; the rest of a paste that finds no box left is not typed. `null` when the text changes nothing.
|
|
192
|
+
*/
|
|
193
|
+
export function typeCode(edit, text, rules = {}) {
|
|
194
|
+
const read = readCodeText(text, rules);
|
|
195
|
+
if (!read || !read.code)
|
|
196
|
+
return null;
|
|
197
|
+
const total = codeLength(rules);
|
|
198
|
+
if (read.whole)
|
|
199
|
+
return settleCode({ value: read.code, start: total, end: total }, rules);
|
|
200
|
+
const characters = [...edit.value];
|
|
201
|
+
let start = Math.max(0, Math.min(edit.start, characters.length));
|
|
202
|
+
let end = Math.max(start, Math.min(edit.end, characters.length));
|
|
203
|
+
let typed = 0;
|
|
204
|
+
for (const character of read.code) {
|
|
205
|
+
if (end > start) {
|
|
206
|
+
characters.splice(start, end - start, character);
|
|
207
|
+
}
|
|
208
|
+
else if (characters.length < total) {
|
|
209
|
+
characters.splice(start, 0, character);
|
|
210
|
+
}
|
|
211
|
+
else if (start < total) {
|
|
212
|
+
characters[start] = character;
|
|
213
|
+
}
|
|
214
|
+
else if (typed === 0) {
|
|
215
|
+
start = total - 1;
|
|
216
|
+
characters[start] = character;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
start += 1;
|
|
222
|
+
end = start;
|
|
223
|
+
typed += 1;
|
|
224
|
+
}
|
|
225
|
+
return settleCode({ value: characters.join(""), start, end }, rules);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Where ← → Home End take a code's caret: a box at a time, and a filled box it lands on is selected, so a key replaces
|
|
229
|
+
* it and Backspace or Delete remove it. It stops at the first empty box, where the next character goes, or on a full
|
|
230
|
+
* code at the last box. With several boxes selected, ← goes to the first of them and → to the last. End on a saved code
|
|
231
|
+
* longer than its boxes goes to its very end, so Backspace takes what the boxes cannot show.
|
|
232
|
+
*/
|
|
233
|
+
export function moveCode(edit, key, rules = {}) {
|
|
234
|
+
const total = codeLength(rules);
|
|
235
|
+
const length = [...edit.value].length;
|
|
236
|
+
if (key === "End" && length > total)
|
|
237
|
+
return { value: edit.value, start: edit.value.length, end: edit.value.length };
|
|
238
|
+
const furthest = Math.min(length, total - 1);
|
|
239
|
+
let box;
|
|
240
|
+
if (key === "Home")
|
|
241
|
+
box = 0;
|
|
242
|
+
else if (key === "End")
|
|
243
|
+
box = furthest;
|
|
244
|
+
else if (edit.end - edit.start > 1)
|
|
245
|
+
box = key === "ArrowLeft" ? edit.start : edit.end - 1;
|
|
246
|
+
else
|
|
247
|
+
box = Math.min(edit.start, total - 1) + (key === "ArrowLeft" ? -1 : 1);
|
|
248
|
+
box = Math.max(0, Math.min(box, furthest));
|
|
249
|
+
return { value: edit.value, start: box, end: box < length ? box + 1 : box };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* A code after the browser changed its input itself - a phone's keyboard, an input method's word, autofill, a drop,
|
|
253
|
+
* Backspace or Delete - worked out from the edit before the change and the input as the browser left it. Text that
|
|
254
|
+
* arrived goes in by `typeCode`, so every way of typing follows one model, and text it does not take leaves the code as
|
|
255
|
+
* it was; a removal stands as the browser made it, the boxes after it closed up.
|
|
256
|
+
*/
|
|
257
|
+
export function codeAfterInput(before, after, rules = {}) {
|
|
258
|
+
if (after.value === before.value)
|
|
259
|
+
return after;
|
|
260
|
+
const head = before.value.slice(0, before.start);
|
|
261
|
+
const tail = before.value.slice(before.end);
|
|
262
|
+
let start = before.start;
|
|
263
|
+
let end = before.end;
|
|
264
|
+
let arrived;
|
|
265
|
+
if (after.value.length >= head.length + tail.length && after.value.startsWith(head) && after.value.endsWith(tail)) {
|
|
266
|
+
// Text in the selection's place or at the caret: a key, a keyboard's word, an input method's commit.
|
|
267
|
+
arrived = after.value.slice(head.length, after.value.length - tail.length);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
// Anything else, such as Backspace at a caret or autofill: the part that differs.
|
|
271
|
+
start = 0;
|
|
272
|
+
while (start < before.value.length && start < after.value.length && before.value[start] === after.value[start])
|
|
273
|
+
start++;
|
|
274
|
+
let same = 0;
|
|
275
|
+
while (same < before.value.length - start &&
|
|
276
|
+
same < after.value.length - start &&
|
|
277
|
+
before.value[before.value.length - 1 - same] === after.value[after.value.length - 1 - same]) {
|
|
278
|
+
same++;
|
|
279
|
+
}
|
|
280
|
+
end = before.value.length - same;
|
|
281
|
+
arrived = after.value.slice(start, after.value.length - same);
|
|
282
|
+
}
|
|
283
|
+
if (!arrived)
|
|
284
|
+
return after;
|
|
285
|
+
return typeCode({ value: before.value, start, end }, arrived, rules) ?? before;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Checks a code against its rules and returns the first message, or `null` when it is valid: required, every box
|
|
289
|
+
* filled, and only the characters it takes. Pure and DOM-free, like `validateText`.
|
|
290
|
+
*/
|
|
291
|
+
export function validateCode(value, rules, fieldName = DEFAULT_FIELD_NAME) {
|
|
292
|
+
const code = (value ?? "").trim();
|
|
293
|
+
if (code) {
|
|
294
|
+
const kind = rules.characters ?? "digits";
|
|
295
|
+
const test = characterTest(rules);
|
|
296
|
+
const characters = [...code];
|
|
297
|
+
if (characters.some(character => !test.test(withCase(character, rules.letterCase)))) {
|
|
298
|
+
return rules.allowed ? `${fieldName} has a character it does not take.` : `${fieldName} takes ${ONLY_NAMES[kind]} only.`;
|
|
299
|
+
}
|
|
300
|
+
const length = codeLength(rules);
|
|
301
|
+
const names = rules.allowed ? "characters" : CHARACTER_NAMES[kind];
|
|
302
|
+
if (characters.length < length)
|
|
303
|
+
return `${fieldName} needs all ${length} ${names}.`;
|
|
304
|
+
if (characters.length > length)
|
|
305
|
+
return `${fieldName} is ${length} ${names} long, not ${characters.length}.`;
|
|
306
|
+
}
|
|
307
|
+
else if (rules.required) {
|
|
308
|
+
return requiredMessage(fieldName);
|
|
309
|
+
}
|
|
310
|
+
return rules.validate?.(code) || null;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* A code as text to read, in its groups: "123-45-6789" with `separator: "-"`, "482 913" for `[3, 3]` with none, and a dot
|
|
314
|
+
* for each character when masked. "" for no code.
|
|
315
|
+
*/
|
|
316
|
+
export function formatCode(value, rules = {}) {
|
|
317
|
+
const characters = [...(value ?? "")].map(character => (rules.masked ? "•" : character));
|
|
318
|
+
if (!characters.length)
|
|
319
|
+
return "";
|
|
320
|
+
const parts = [];
|
|
321
|
+
let start = 0;
|
|
322
|
+
for (const size of codeGroups(rules)) {
|
|
323
|
+
if (start >= characters.length)
|
|
324
|
+
break;
|
|
325
|
+
parts.push(characters.slice(start, start + size).join(""));
|
|
326
|
+
start += size;
|
|
327
|
+
}
|
|
328
|
+
// A code longer than its boxes (a value from elsewhere) keeps the rest, so nothing is hidden.
|
|
329
|
+
if (start < characters.length)
|
|
330
|
+
parts.push(characters.slice(start).join(""));
|
|
331
|
+
return parts.join(rules.separator || " ");
|
|
332
|
+
}
|
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { ApiQuery } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { CrudMark, CrudMassActionName, CrudMode } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
+
import type { TableKey, TablePage, TableQuery, TableRow, TableTag } from "../Interfaces/TableInterfaces.ts";
|
|
3
4
|
type Row = Record<string, unknown>;
|
|
5
|
+
/** What a write of marks sends beside the endpoint's own path: query values, a body, or both. */
|
|
6
|
+
export interface CrudMarksRequest {
|
|
7
|
+
query?: ApiQuery;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** The words of a record's history, by the numbers the backend keeps (`CrudBackend.auditWords`). */
|
|
11
|
+
export interface CrudAuditWords {
|
|
12
|
+
/** What a request did, by its command number: "Added", "Moved to the trash". */
|
|
13
|
+
commands: Readonly<Record<number, string>>;
|
|
14
|
+
/** Audit keys that are the whole story, by number: "Flagged", "Made a favourite". */
|
|
15
|
+
directions: Readonly<Record<number, string>>;
|
|
16
|
+
/** The part of the record an update changed, by audit key: "basic information". */
|
|
17
|
+
parts: Readonly<Record<number, string>>;
|
|
18
|
+
/** The command number of a plain update: the one whose audit key names a part. */
|
|
19
|
+
update?: number;
|
|
20
|
+
}
|
|
4
21
|
export declare abstract class CrudBackend {
|
|
5
22
|
/**
|
|
6
23
|
* What a server table sends for one page, from the query. `sortKey` is the sorted column's server name. `filters` are
|
|
@@ -16,9 +33,57 @@ export declare abstract class CrudBackend {
|
|
|
16
33
|
abstract isFlagged(row: TableRow): boolean;
|
|
17
34
|
/** The row with its flag set or cleared. */
|
|
18
35
|
abstract withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
36
|
+
/** Whether a record can be flagged or unflagged at all. Default yes. */
|
|
37
|
+
canFlag(row: TableRow): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* What flagging one record or many sends beside the key(s): the state wanted, never a toggle. Default the Nexus
|
|
40
|
+
* backend's `?isFlagged=true`. A backend that toggles ignores it, and a CRUD sends only the records a change is for.
|
|
41
|
+
*/
|
|
42
|
+
flagRequest(on: boolean): CrudMarksRequest;
|
|
19
43
|
abstract isPinned(row: TableRow): boolean;
|
|
20
44
|
/** The row with its pin set or cleared. */
|
|
21
45
|
abstract withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
46
|
+
/**
|
|
47
|
+
* This user's marks on a record as a set of bits (`CRUD_MARK_BITS`: pinned 1, favourite 2), as a bookmark listing
|
|
48
|
+
* answers them on each record. Bits the package does not know are kept as they are. Default: the pin alone, through
|
|
49
|
+
* `isPinned`.
|
|
50
|
+
*/
|
|
51
|
+
marksOf(row: TableRow): number;
|
|
52
|
+
/** The row carrying this set of marks. Default: the pin alone, through `withPin`. */
|
|
53
|
+
withMarks<TRow extends TableRow>(row: TRow, marks: number): TRow;
|
|
54
|
+
/**
|
|
55
|
+
* What setting one record's marks sends beside its key: the **whole set** it is to have, never a toggle. Default the
|
|
56
|
+
* Nexus backend's: `?bookmarkTypes=3`.
|
|
57
|
+
*/
|
|
58
|
+
marksRequest(marks: number): CrudMarksRequest;
|
|
59
|
+
/**
|
|
60
|
+
* What putting one mark on many records, or taking it off, sends: the keys, the mark and which way. Each record keeps
|
|
61
|
+
* its other marks. Default the Nexus backend's: the keys as the body, `?bookmarkTypes=2&isAdding=true`.
|
|
62
|
+
*/
|
|
63
|
+
massMarksRequest(keys: TableKey[], mark: CrudMark, on: boolean): CrudMarksRequest;
|
|
64
|
+
/** The marks a write left on the record, read from its answer. `null` when it does not say, and the set sent stands. */
|
|
65
|
+
answeredMarks(result: unknown): number | null;
|
|
66
|
+
/** What an export sends to name the list on screen, beside the table's own filters. Default nothing. */
|
|
67
|
+
exportFilters(mode: CrudMode): Record<string, unknown>;
|
|
68
|
+
/** A record status in words, for a status column and the history: "Active", "Flagged", "Trashed". Default the number. */
|
|
69
|
+
statusText(status: number): string;
|
|
70
|
+
/**
|
|
71
|
+
* The short tag a reference to a record carries when that record is no longer available - "Trashed" beside a parent in
|
|
72
|
+
* the trash, "Deleted" beside one deleted - read from its status. `null` for a live record, and for no status at all.
|
|
73
|
+
* A joined column (`crudColumns.dbJoin`, `crudColumns.clientJoin`) draws it after the name. Default: none.
|
|
74
|
+
*/
|
|
75
|
+
unavailableTag(status: unknown): TableTag | null;
|
|
76
|
+
/**
|
|
77
|
+
* Whether a record may be picked in a form: what a picker offers, and what it shows tagged and will not offer again when
|
|
78
|
+
* a form already holds it. Read from the record. Default: every record.
|
|
79
|
+
*/
|
|
80
|
+
isPickable(record: TableRow): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The words of a record's history: what each command did (`commands`), the audit keys that are the whole story of a
|
|
83
|
+
* switch (`directions`), and the parts of the record an update names (`parts`, with `update` the command that names
|
|
84
|
+
* one). Numbers an audit row keeps for good, so each backend's own. Default none: "Action 3".
|
|
85
|
+
*/
|
|
86
|
+
get auditWords(): CrudAuditWords;
|
|
22
87
|
/**
|
|
23
88
|
* Changes an action's answer makes to rows, by key: a mass flag that answers each record's new status. Rows without
|
|
24
89
|
* an entry take the change the action stands for (a toggled flag). Default: none.
|
|
@@ -34,16 +99,25 @@ export declare abstract class CrudBackend {
|
|
|
34
99
|
* `null` when it says nothing about it - a bare key, an empty body - and then the caller keeps what it has.
|
|
35
100
|
*
|
|
36
101
|
* **This is what spares a page its reload.** A save or a mark whose answer carries the record is written into the
|
|
37
|
-
* table, the
|
|
102
|
+
* table, the sections above it and every list kept in the cache, and nothing is loaded again. Only answers of actions on
|
|
38
103
|
* **one** record are read here; a mass answer goes through `answeredChanges`, record by record.
|
|
39
104
|
*/
|
|
40
105
|
answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
|
|
41
106
|
/**
|
|
42
107
|
* The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
|
|
43
|
-
* is the record itself, or says nothing. With a key the new record goes straight into the list,
|
|
44
|
-
* a
|
|
108
|
+
* is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
|
|
109
|
+
* only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
|
|
45
110
|
*/
|
|
46
111
|
answeredKey(result: unknown): TableKey | null;
|
|
112
|
+
/**
|
|
113
|
+
* A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
|
|
114
|
+
* filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
|
|
115
|
+
* the record as the page holds it now (an edit; `undefined` for a new one): an edit's form carries its own copy of those
|
|
116
|
+
* fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
|
|
117
|
+
* record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
|
|
118
|
+
* one.
|
|
119
|
+
*/
|
|
120
|
+
savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
|
|
47
121
|
/**
|
|
48
122
|
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
49
123
|
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
@@ -65,16 +139,41 @@ export declare const NEXUS_DELETED_STATUS = 60;
|
|
|
65
139
|
/**
|
|
66
140
|
* The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
67
141
|
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
68
|
-
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25,
|
|
142
|
+
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, and this user's marks are
|
|
143
|
+
* its `bookmarkTypes` (`[Flags] EBookmarkTypes`: 1 pinned, 2 favourite), or `isPinned` from a backend before them.
|
|
69
144
|
*/
|
|
70
145
|
export declare class NexusCrudBackend extends CrudBackend {
|
|
71
146
|
pageRequest(query: TableQuery, sortKey: string | undefined, filters?: Readonly<Record<string, unknown>>): unknown;
|
|
72
147
|
readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
|
|
73
148
|
massRequest(action: CrudMassActionName, keys: TableKey[], values?: Row): unknown;
|
|
149
|
+
/** The whole flag band, 25-29: every status comparison is a range test. */
|
|
74
150
|
isFlagged(row: TableRow): boolean;
|
|
151
|
+
/**
|
|
152
|
+
* A live record that is not pending (0-29): the backend flags 0-24 and unflags 25-29, and never touches a pending
|
|
153
|
+
* record (30-39), which would lose that status for good. A record with no status yet is not refused here.
|
|
154
|
+
*/
|
|
155
|
+
canFlag(row: TableRow): boolean;
|
|
75
156
|
withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
76
157
|
isPinned(row: TableRow): boolean;
|
|
77
158
|
withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
159
|
+
marksOf(row: TableRow): number;
|
|
160
|
+
/** `bookmarkTypes`, and `isPinned` too on a record that has one, so neither says something the other does not. */
|
|
161
|
+
withMarks<TRow extends TableRow>(row: TRow, marks: number): TRow;
|
|
162
|
+
/** `SwitchBookmarks` answers the marks it stored, as a number. */
|
|
163
|
+
answeredMarks(result: unknown): number | null;
|
|
164
|
+
/** An export names the list on screen as `flag`, by the number `ECommandTypes` gives its listing. */
|
|
165
|
+
exportFilters(mode: CrudMode): Record<string, unknown>;
|
|
166
|
+
/** The members `ERecordStatus` names, then the band of any other number in it. */
|
|
167
|
+
statusText(status: number): string;
|
|
168
|
+
/**
|
|
169
|
+
* Out of the live band, 40 and up (the owner, 2026-10-08: "if status is not active range, like trash or deleted, show a
|
|
170
|
+
* very short tag"): "Trashed" in the warning color, "Deleted" in the danger color. A flagged or pending record is still
|
|
171
|
+
* live, and carries none.
|
|
172
|
+
*/
|
|
173
|
+
unavailableTag(status: unknown): TableTag | null;
|
|
174
|
+
/** Usable, under 30, as a Nexus `ActiveListing` answers: a pending record is live but not one to pick. No status reads as usable. */
|
|
175
|
+
isPickable(record: TableRow): boolean;
|
|
176
|
+
get auditWords(): CrudAuditWords;
|
|
78
177
|
/** Mass answers list the records they changed, with their new `recordStatus` and `isPinned`. */
|
|
79
178
|
answeredChanges(result: unknown, rowKey: string): Map<string, Row>;
|
|
80
179
|
/**
|
|
@@ -86,6 +185,15 @@ export declare class NexusCrudBackend extends CrudBackend {
|
|
|
86
185
|
answeredRow<TRow extends TableRow>(result: unknown): Partial<TRow> | null;
|
|
87
186
|
/** `Create` and `Update` answer the record's id: a token when ids are encrypted, the number as text when not. */
|
|
88
187
|
answeredKey(result: unknown): TableKey | null;
|
|
188
|
+
/**
|
|
189
|
+
* What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
|
|
190
|
+
* 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
|
|
191
|
+
* Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
|
|
192
|
+
* said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
|
|
193
|
+
* page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
|
|
194
|
+
* column reads them.
|
|
195
|
+
*/
|
|
196
|
+
savedRow<TRow extends TableRow>(action: "create" | "edit", record: TRow, before?: TRow): TRow;
|
|
89
197
|
/**
|
|
90
198
|
* The bands the owner settled on: under 40 is live, 25–29 of that is flagged, 40–59 is the trash, and 60 and up is
|
|
91
199
|
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|