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
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { CRUD_MARK_BITS, hasMark, withMark } from "./CrudHelpers.js";
|
|
2
|
+
import { nowUtcDateTime } from "./DateTimeHelpers.js";
|
|
3
|
+
const NO_AUDIT_WORDS = { commands: {}, directions: {}, parts: {} };
|
|
1
4
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
5
|
export class CrudBackend {
|
|
3
6
|
/** Reads a whole list: an array, or the rows of a page. */
|
|
@@ -6,6 +9,83 @@ export class CrudBackend {
|
|
|
6
9
|
return result;
|
|
7
10
|
return [...this.readPage(result).rows];
|
|
8
11
|
}
|
|
12
|
+
/** Whether a record can be flagged or unflagged at all. Default yes. */
|
|
13
|
+
canFlag(row) {
|
|
14
|
+
void row;
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What flagging one record or many sends beside the key(s): the state wanted, never a toggle. Default the Nexus
|
|
19
|
+
* backend's `?isFlagged=true`. A backend that toggles ignores it, and a CRUD sends only the records a change is for.
|
|
20
|
+
*/
|
|
21
|
+
flagRequest(on) {
|
|
22
|
+
return { query: { isFlagged: on } };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* This user's marks on a record as a set of bits (`CRUD_MARK_BITS`: pinned 1, favourite 2), as a bookmark listing
|
|
26
|
+
* answers them on each record. Bits the package does not know are kept as they are. Default: the pin alone, through
|
|
27
|
+
* `isPinned`.
|
|
28
|
+
*/
|
|
29
|
+
marksOf(row) {
|
|
30
|
+
return this.isPinned(row) ? CRUD_MARK_BITS.pin : 0;
|
|
31
|
+
}
|
|
32
|
+
/** The row carrying this set of marks. Default: the pin alone, through `withPin`. */
|
|
33
|
+
withMarks(row, marks) {
|
|
34
|
+
return this.withPin(row, hasMark(marks, "pin"));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* What setting one record's marks sends beside its key: the **whole set** it is to have, never a toggle. Default the
|
|
38
|
+
* Nexus backend's: `?bookmarkTypes=3`.
|
|
39
|
+
*/
|
|
40
|
+
marksRequest(marks) {
|
|
41
|
+
return { query: { bookmarkTypes: marks } };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What putting one mark on many records, or taking it off, sends: the keys, the mark and which way. Each record keeps
|
|
45
|
+
* its other marks. Default the Nexus backend's: the keys as the body, `?bookmarkTypes=2&isAdding=true`.
|
|
46
|
+
*/
|
|
47
|
+
massMarksRequest(keys, mark, on) {
|
|
48
|
+
return { body: keys, query: { bookmarkTypes: CRUD_MARK_BITS[mark], isAdding: on } };
|
|
49
|
+
}
|
|
50
|
+
/** The marks a write left on the record, read from its answer. `null` when it does not say, and the set sent stands. */
|
|
51
|
+
answeredMarks(result) {
|
|
52
|
+
void result;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** What an export sends to name the list on screen, beside the table's own filters. Default nothing. */
|
|
56
|
+
exportFilters(mode) {
|
|
57
|
+
void mode;
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
/** A record status in words, for a status column and the history: "Active", "Flagged", "Trashed". Default the number. */
|
|
61
|
+
statusText(status) {
|
|
62
|
+
return String(status);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The short tag a reference to a record carries when that record is no longer available - "Trashed" beside a parent in
|
|
66
|
+
* the trash, "Deleted" beside one deleted - read from its status. `null` for a live record, and for no status at all.
|
|
67
|
+
* A joined column (`crudColumns.dbJoin`, `crudColumns.clientJoin`) draws it after the name. Default: none.
|
|
68
|
+
*/
|
|
69
|
+
unavailableTag(status) {
|
|
70
|
+
void status;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether a record may be picked in a form: what a picker offers, and what it shows tagged and will not offer again when
|
|
75
|
+
* a form already holds it. Read from the record. Default: every record.
|
|
76
|
+
*/
|
|
77
|
+
isPickable(record) {
|
|
78
|
+
void record;
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
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() {
|
|
87
|
+
return NO_AUDIT_WORDS;
|
|
88
|
+
}
|
|
9
89
|
/**
|
|
10
90
|
* Changes an action's answer makes to rows, by key: a mass flag that answers each record's new status. Rows without
|
|
11
91
|
* an entry take the change the action stands for (a toggled flag). Default: none.
|
|
@@ -29,7 +109,7 @@ export class CrudBackend {
|
|
|
29
109
|
* `null` when it says nothing about it - a bare key, an empty body - and then the caller keeps what it has.
|
|
30
110
|
*
|
|
31
111
|
* **This is what spares a page its reload.** A save or a mark whose answer carries the record is written into the
|
|
32
|
-
* table, the
|
|
112
|
+
* table, the sections above it and every list kept in the cache, and nothing is loaded again. Only answers of actions on
|
|
33
113
|
* **one** record are read here; a mass answer goes through `answeredChanges`, record by record.
|
|
34
114
|
*/
|
|
35
115
|
answeredRow(result) {
|
|
@@ -38,13 +118,26 @@ export class CrudBackend {
|
|
|
38
118
|
}
|
|
39
119
|
/**
|
|
40
120
|
* The key an answer gives in place of the record: `Create` answering the new record's id. `null` when the answer
|
|
41
|
-
* is the record itself, or says nothing. With a key the new record goes straight into the list,
|
|
42
|
-
* a
|
|
121
|
+
* is the record itself, or says nothing. With a key the new record goes straight into the list, finished by `savedRow`;
|
|
122
|
+
* only a record whose list still cannot be told is read back on its own through `details` - never the whole list again.
|
|
43
123
|
*/
|
|
44
124
|
answeredKey(result) {
|
|
45
125
|
void result;
|
|
46
126
|
return null;
|
|
47
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* A record a save wrote - what the form sent, under what the answer said - with the fields only the server keeps
|
|
130
|
+
* filled in as this backend writes them, so nothing has to be read back: a status, the times, the marks. `before` is
|
|
131
|
+
* 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
|
|
132
|
+
* fields from when it opened, which a flag or a mark made since has left behind. Default: the record as it is - and a
|
|
133
|
+
* record whose list still cannot be told from it (`listMembership`) is read back once through `details`, where there is
|
|
134
|
+
* one.
|
|
135
|
+
*/
|
|
136
|
+
savedRow(action, record, before) {
|
|
137
|
+
void action;
|
|
138
|
+
void before;
|
|
139
|
+
return record;
|
|
140
|
+
}
|
|
48
141
|
/**
|
|
49
142
|
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
50
143
|
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
@@ -70,10 +163,10 @@ export class CrudBackend {
|
|
|
70
163
|
* 40–59 trash
|
|
71
164
|
* 60+ delete
|
|
72
165
|
*
|
|
73
|
-
* The flag is a **range**, 25–29, and
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the
|
|
166
|
+
* The flag is a **range**, 25–29, and so is every test here (`isFlagged`, `listMembership`), since 2026-10-08: the
|
|
167
|
+
* Regions page already read it as a range, and a list that reads 26 as flagged where the table reads it as not would
|
|
168
|
+
* disagree with itself. A flag written is still 25, the seeded `ERecordStatus.Flagged`. Per-project bands from
|
|
169
|
+
* `/v2/core-config` would change the numbers here, never the shape of the test.
|
|
77
170
|
*/
|
|
78
171
|
/** The Nexus record status of a flagged record (`ERecordStatus.Flagged`), the bottom of the 25–29 flag band. */
|
|
79
172
|
export const NEXUS_FLAGGED_STATUS = 25;
|
|
@@ -85,10 +178,37 @@ export const NEXUS_TRASH_STATUS = 40;
|
|
|
85
178
|
export const NEXUS_DELETED_STATUS = 60;
|
|
86
179
|
/** Just past the flag band: a flagged record is 25–29, so 30 and up is live but not flagged. */
|
|
87
180
|
const NEXUS_FLAG_BAND_END = 30;
|
|
181
|
+
/** Each list as the Nexus backend's `ECommandTypes` numbers its listing: what an export names the list on screen by. */
|
|
182
|
+
const NEXUS_LIST_COMMANDS = { normal: 50, flag: 52, trash: 54, deleted: 55 };
|
|
183
|
+
/** The statuses `ERecordStatus` names. Imported (1) is what a spreadsheet import writes unless the person picks another. */
|
|
184
|
+
const NEXUS_STATUS_WORDS = { 0: "Active", 1: "Imported", 25: "Flagged", 30: "Pending", 31: "Unverified", 32: "In review", 33: "Approval required", 40: "Trashed", 60: "Deleted" };
|
|
185
|
+
/** A record's history in words, by the Nexus backend's `ECommandTypes` and `EAuditKeys`. An audit row keeps the numbers for good. */
|
|
186
|
+
const NEXUS_AUDIT_WORDS = {
|
|
187
|
+
commands: {
|
|
188
|
+
0: "Added",
|
|
189
|
+
1: "Updated",
|
|
190
|
+
3: "Marks changed",
|
|
191
|
+
4: "Flag changed",
|
|
192
|
+
10: "Moved to the trash",
|
|
193
|
+
11: "Recovered from the trash",
|
|
194
|
+
12: "Deleted permanently",
|
|
195
|
+
13: "Moved back to the trash",
|
|
196
|
+
20: "Basic information updated",
|
|
197
|
+
21: "Billing information updated",
|
|
198
|
+
22: "Address updated",
|
|
199
|
+
23: "References updated",
|
|
200
|
+
24: "Contact information updated",
|
|
201
|
+
25: "Email updated",
|
|
202
|
+
},
|
|
203
|
+
directions: { 7: "Flagged", 8: "Unflagged", 9: "Made a favourite", 10: "Taken out of the favourites" },
|
|
204
|
+
parts: { 1: "basic information", 2: "contact information", 3: "email", 4: "address", 5: "profile picture", 6: "password", 11: "permissions", 12: "billing information", 13: "references" },
|
|
205
|
+
update: 1,
|
|
206
|
+
};
|
|
88
207
|
/**
|
|
89
208
|
* The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
90
209
|
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
91
|
-
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25,
|
|
210
|
+
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, and this user's marks are
|
|
211
|
+
* its `bookmarkTypes` (`[Flags] EBookmarkTypes`: 1 pinned, 2 favourite), or `isPinned` from a backend before them.
|
|
92
212
|
*/
|
|
93
213
|
export class NexusCrudBackend extends CrudBackend {
|
|
94
214
|
pageRequest(query, sortKey, filters) {
|
|
@@ -119,17 +239,70 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
119
239
|
massRequest(action, keys, values) {
|
|
120
240
|
return action === "description" ? { description: values?.description ?? null, accountIds: keys } : keys;
|
|
121
241
|
}
|
|
242
|
+
/** The whole flag band, 25-29: every status comparison is a range test. */
|
|
122
243
|
isFlagged(row) {
|
|
123
|
-
|
|
244
|
+
const status = Number(row.recordStatus);
|
|
245
|
+
return status >= NEXUS_FLAGGED_STATUS && status < NEXUS_FLAG_BAND_END;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* A live record that is not pending (0-29): the backend flags 0-24 and unflags 25-29, and never touches a pending
|
|
249
|
+
* record (30-39), which would lose that status for good. A record with no status yet is not refused here.
|
|
250
|
+
*/
|
|
251
|
+
canFlag(row) {
|
|
252
|
+
const status = row.recordStatus;
|
|
253
|
+
return typeof status !== "number" || status < NEXUS_FLAG_BAND_END;
|
|
124
254
|
}
|
|
125
255
|
withFlag(row, on) {
|
|
126
256
|
return { ...row, recordStatus: on ? NEXUS_FLAGGED_STATUS : NEXUS_ACTIVE_STATUS };
|
|
127
257
|
}
|
|
128
258
|
isPinned(row) {
|
|
129
|
-
return row
|
|
259
|
+
return hasMark(this.marksOf(row), "pin");
|
|
130
260
|
}
|
|
131
261
|
withPin(row, on) {
|
|
132
|
-
return
|
|
262
|
+
return this.withMarks(row, withMark(this.marksOf(row), "pin", on));
|
|
263
|
+
}
|
|
264
|
+
marksOf(row) {
|
|
265
|
+
const marks = row.bookmarkTypes;
|
|
266
|
+
if (typeof marks === "number" && Number.isInteger(marks) && marks >= 0)
|
|
267
|
+
return marks;
|
|
268
|
+
return row.isPinned === true ? CRUD_MARK_BITS.pin : 0;
|
|
269
|
+
}
|
|
270
|
+
/** `bookmarkTypes`, and `isPinned` too on a record that has one, so neither says something the other does not. */
|
|
271
|
+
withMarks(row, marks) {
|
|
272
|
+
const next = { ...row, bookmarkTypes: marks };
|
|
273
|
+
if ("isPinned" in row)
|
|
274
|
+
next.isPinned = hasMark(marks, "pin");
|
|
275
|
+
return next;
|
|
276
|
+
}
|
|
277
|
+
/** `SwitchBookmarks` answers the marks it stored, as a number. */
|
|
278
|
+
answeredMarks(result) {
|
|
279
|
+
return typeof result === "number" && Number.isInteger(result) && result >= 0 ? result : null;
|
|
280
|
+
}
|
|
281
|
+
/** An export names the list on screen as `flag`, by the number `ECommandTypes` gives its listing. */
|
|
282
|
+
exportFilters(mode) {
|
|
283
|
+
return { flag: NEXUS_LIST_COMMANDS[mode] };
|
|
284
|
+
}
|
|
285
|
+
/** The members `ERecordStatus` names, then the band of any other number in it. */
|
|
286
|
+
statusText(status) {
|
|
287
|
+
return NEXUS_STATUS_WORDS[status] ?? (status < NEXUS_FLAGGED_STATUS ? "Active" : status < NEXUS_FLAG_BAND_END ? "Flagged" : status < NEXUS_TRASH_STATUS ? "Pending" : status < NEXUS_DELETED_STATUS ? "Trashed" : "Deleted");
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 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
|
|
291
|
+
* very short tag"): "Trashed" in the warning color, "Deleted" in the danger color. A flagged or pending record is still
|
|
292
|
+
* live, and carries none.
|
|
293
|
+
*/
|
|
294
|
+
unavailableTag(status) {
|
|
295
|
+
if (typeof status !== "number" || !Number.isFinite(status) || status < NEXUS_TRASH_STATUS)
|
|
296
|
+
return null;
|
|
297
|
+
return { label: this.statusText(status), tone: status < NEXUS_DELETED_STATUS ? "warning" : "danger" };
|
|
298
|
+
}
|
|
299
|
+
/** Usable, under 30, as a Nexus `ActiveListing` answers: a pending record is live but not one to pick. No status reads as usable. */
|
|
300
|
+
isPickable(record) {
|
|
301
|
+
const status = record.recordStatus;
|
|
302
|
+
return typeof status !== "number" || status < NEXUS_FLAG_BAND_END;
|
|
303
|
+
}
|
|
304
|
+
get auditWords() {
|
|
305
|
+
return NEXUS_AUDIT_WORDS;
|
|
133
306
|
}
|
|
134
307
|
/** Mass answers list the records they changed, with their new `recordStatus` and `isPinned`. */
|
|
135
308
|
answeredChanges(result, rowKey) {
|
|
@@ -169,6 +342,36 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
169
342
|
answeredKey(result) {
|
|
170
343
|
return typeof result === "string" && result !== "" ? result : null;
|
|
171
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* What a Nexus save leaves to the server, written as the server writes it, so nothing is read back (the owner,
|
|
347
|
+
* 2026-10-10: "the creationTime, recordStatus we will provide fake or just right now"). A new record lacking them is
|
|
348
|
+
* Active (every Create writes `NEXUS_ACTIVE_STATUS`), has no marks, was added now and never changed; what its answer
|
|
349
|
+
* said stays. An Update writes none of the status, the marks and the time added, so an edited record has the ones the
|
|
350
|
+
* page holds (`before`) - over its form's older copies - and was changed now. Times are UTC ISO strings, as a date-time
|
|
351
|
+
* column reads them.
|
|
352
|
+
*/
|
|
353
|
+
savedRow(action, record, before) {
|
|
354
|
+
const row = { ...record };
|
|
355
|
+
const now = nowUtcDateTime();
|
|
356
|
+
const fill = (field, value) => {
|
|
357
|
+
if (value !== undefined && (row[field] === undefined || row[field] === null))
|
|
358
|
+
row[field] = value;
|
|
359
|
+
};
|
|
360
|
+
if (action === "create") {
|
|
361
|
+
fill("recordStatus", NEXUS_ACTIVE_STATUS);
|
|
362
|
+
fill("bookmarkTypes", 0);
|
|
363
|
+
fill("creationTime", now);
|
|
364
|
+
fill("modificationTime", null);
|
|
365
|
+
return row;
|
|
366
|
+
}
|
|
367
|
+
const held = (before ?? {});
|
|
368
|
+
for (const field of ["recordStatus", "bookmarkTypes", "creationTime"]) {
|
|
369
|
+
if (held[field] !== undefined)
|
|
370
|
+
row[field] = held[field];
|
|
371
|
+
}
|
|
372
|
+
row.modificationTime = now;
|
|
373
|
+
return row;
|
|
374
|
+
}
|
|
172
375
|
/**
|
|
173
376
|
* 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
|
|
174
377
|
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import { type InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
3
|
+
import type { TableColumn, TableJoin, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
4
|
+
import { type CrudBackend } from "./CrudBackend.ts";
|
|
5
|
+
import { type PriceLadderOptions } from "./NumberHelpers.ts";
|
|
6
|
+
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
7
|
+
export declare const CRUD_DESCRIPTION_MAX = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* The fields a record keeps its price's figures in (`crudInputs.netAmount`, `crudColumns.amounts`). Defaults: `price`,
|
|
10
|
+
* `discountPercentage` and `vatPercentage`, and no quantity (one of it).
|
|
11
|
+
*/
|
|
12
|
+
export type PriceKeys = {
|
|
13
|
+
price?: string;
|
|
14
|
+
quantity?: string;
|
|
15
|
+
discount?: string;
|
|
16
|
+
vat?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare const crudColumns: {
|
|
19
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
20
|
+
status<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>, backend?: CrudBackend): TableColumn<TRow>;
|
|
21
|
+
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
22
|
+
description<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
23
|
+
/** When the record was added. Hidden until the user shows it. */
|
|
24
|
+
added<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
25
|
+
/** When it was changed last. Hidden until the user shows it. */
|
|
26
|
+
changed<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
27
|
+
/**
|
|
28
|
+
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
29
|
+
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
30
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
|
|
31
|
+
* record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
|
|
32
|
+
* it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
|
|
33
|
+
* record back once (`details`) to name it.
|
|
34
|
+
*
|
|
35
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
|
|
36
|
+
*/
|
|
37
|
+
dbJoin<TRow extends TableRow>({ status, from, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
38
|
+
/**
|
|
39
|
+
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
40
|
+
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
41
|
+
* page, searched and sorted like any column - with the same short tag when the record is no longer available, and
|
|
42
|
+
* "Not found" for a key with no record. `field` is the looked-up record's field, when it is not the column's `key`.
|
|
43
|
+
*
|
|
44
|
+
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
45
|
+
*/
|
|
46
|
+
clientJoin<TRow extends TableRow>(api: JoinController, { from, field, ...column }: ClientJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
47
|
+
/**
|
|
48
|
+
* A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
|
|
49
|
+
* (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
|
|
50
|
+
* row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
|
|
51
|
+
* spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
|
|
52
|
+
*
|
|
53
|
+
* @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
|
|
54
|
+
*/
|
|
55
|
+
amounts<TRow extends TableRow>(keys?: PriceKeys, changes?: {
|
|
56
|
+
group?: string;
|
|
57
|
+
}): TableColumn<TRow>[];
|
|
58
|
+
/**
|
|
59
|
+
* A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
|
|
60
|
+
* "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
|
|
61
|
+
* with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
|
|
62
|
+
* `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
|
|
63
|
+
*
|
|
64
|
+
* @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
|
|
65
|
+
*/
|
|
66
|
+
priceLadder<TRow extends TableRow>(row: Readonly<Record<string, unknown>>, options?: PriceLadderOptions & {
|
|
67
|
+
group?: string;
|
|
68
|
+
}): TableColumn<TRow>[];
|
|
69
|
+
};
|
|
70
|
+
/** What `crudColumns.dbJoin` takes: a column, the row's field that holds the joined record's status, and the one with its key. */
|
|
71
|
+
export type DbJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & {
|
|
72
|
+
status: string;
|
|
73
|
+
from?: string;
|
|
74
|
+
};
|
|
75
|
+
/** What `crudColumns.clientJoin` takes: a column, the row's field that holds the other record's key, and its own field. */
|
|
76
|
+
export type ClientJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & Pick<TableJoin, "from" | "field">;
|
|
77
|
+
/** The controller a client join names its records from: any with a `findListing` (`MODULES.controller(...)`). */
|
|
78
|
+
export interface JoinController {
|
|
79
|
+
findListing: ApiEndpoint;
|
|
80
|
+
}
|
|
81
|
+
export declare const crudInputs: {
|
|
82
|
+
/**
|
|
83
|
+
* The record's description: Markdown, at most `CRUD_DESCRIPTION_MAX` characters, across the form. One definition for
|
|
84
|
+
* every model, its hint included (the owner, 2026-10-08): a page gives none of its own. `changes` is for its place
|
|
85
|
+
* in a form, such as `span`.
|
|
86
|
+
*/
|
|
87
|
+
description(changes?: Partial<InputParams>): InputParams;
|
|
88
|
+
/**
|
|
89
|
+
* The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
|
|
90
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never typed, never sent, never a change. Blank while the price
|
|
91
|
+
* is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
|
|
92
|
+
* pairs on a narrow form). `changes` may replace any key, `compute` included: a counter's box shows the server's
|
|
93
|
+
* preview instead (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`).
|
|
94
|
+
*
|
|
95
|
+
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
96
|
+
*/
|
|
97
|
+
netAmount(keys?: PriceKeys, changes?: Partial<InputParams>): InputParams;
|
|
98
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { EInputTypes } from "../Interfaces/FormInterfaces.js";
|
|
2
|
+
import { getCrudBackend } from "./CrudBackend.js";
|
|
3
|
+
import { priceAmounts, priceLadder } from "./NumberHelpers.js";
|
|
4
|
+
import { readPath } from "./TableHelpers.js";
|
|
5
|
+
/*
|
|
6
|
+
* The columns and inputs every Nexus record has, so a page names them instead of writing them again: its status in
|
|
7
|
+
* words, its Markdown description, and when it was added and changed - and the two ways a page shows another record's
|
|
8
|
+
* name, joined by the database (`dbJoin`) or by the table (`clientJoin`). Each takes changes over its defaults, as a
|
|
9
|
+
* column or an input of the page's own would: `crudColumns.added({ hidden: false })`. A record with a price has its
|
|
10
|
+
* money the same way: its figures and the Calculated amount (`amounts`, `netAmount`), and a receipt's ladder
|
|
11
|
+
* (`priceLadder`), each in one wording for every page.
|
|
12
|
+
*/
|
|
13
|
+
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
14
|
+
export const CRUD_DESCRIPTION_MAX = 1000;
|
|
15
|
+
/** The words of the worked-out figure, wherever it is a label: the owner's "calculate value", 2026-06-20's "Calculated Amount". */
|
|
16
|
+
const NET_AMOUNT_LABEL = "Calculated amount";
|
|
17
|
+
/** The price's fields, with the defaults. */
|
|
18
|
+
function priceKeysOf(keys = {}) {
|
|
19
|
+
return { price: keys.price ?? "price", quantity: keys.quantity, discount: keys.discount ?? "discountPercentage", vat: keys.vat ?? "vatPercentage" };
|
|
20
|
+
}
|
|
21
|
+
/** A figure among a record's (or a form's) values: a number, or the text of one; anything else is missing. */
|
|
22
|
+
function figureAt(values, key) {
|
|
23
|
+
if (key === undefined)
|
|
24
|
+
return null;
|
|
25
|
+
const value = readPath(values, key);
|
|
26
|
+
if (typeof value === "number")
|
|
27
|
+
return Number.isFinite(value) ? value : null;
|
|
28
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
return Number.isFinite(number) ? number : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/** The Calculated amount of a record's (or a form's) values by the server's rule (`priceAmounts`); null while the price is empty. */
|
|
35
|
+
function netAmountAt(values, keys) {
|
|
36
|
+
const price = readPath(values, keys.price);
|
|
37
|
+
if (price === null || price === undefined || price === "")
|
|
38
|
+
return null;
|
|
39
|
+
return priceAmounts({
|
|
40
|
+
price: figureAt(values, keys.price),
|
|
41
|
+
quantity: keys.quantity === undefined ? 1 : figureAt(values, keys.quantity),
|
|
42
|
+
discountPercentage: figureAt(values, keys.discount),
|
|
43
|
+
vatPercentage: figureAt(values, keys.vat),
|
|
44
|
+
}).netAmount;
|
|
45
|
+
}
|
|
46
|
+
export const crudColumns = {
|
|
47
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank for a record that says none. */
|
|
48
|
+
status(changes = {}, backend) {
|
|
49
|
+
return {
|
|
50
|
+
key: "recordStatus",
|
|
51
|
+
label: "Status",
|
|
52
|
+
width: "9rem",
|
|
53
|
+
render: (_row, value) => (typeof value === "number" ? (backend ?? getCrudBackend()).statusText(value) : ""),
|
|
54
|
+
...changes,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
58
|
+
description(changes = {}) {
|
|
59
|
+
return { key: "description", label: "Description", wrap: true, sortable: false, markdown: true, ...changes };
|
|
60
|
+
},
|
|
61
|
+
/** When the record was added. Hidden until the user shows it. */
|
|
62
|
+
added(changes = {}) {
|
|
63
|
+
return { key: "creationTime", label: "Added", format: "date-time", hidden: true, ...changes };
|
|
64
|
+
},
|
|
65
|
+
/** When it was changed last. Hidden until the user shows it. */
|
|
66
|
+
changed(changes = {}) {
|
|
67
|
+
return { key: "modificationTime", label: "Changed", format: "date-time", hidden: true, ...changes };
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
71
|
+
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
72
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up. Give it `from` - the field holding that
|
|
73
|
+
* record's key, an input of the form: a save answers what was sent, not the names the database joins, so a CRUD names
|
|
74
|
+
* it again from the record picked for the key, with no call (`crudJoinedNames`). Without `from` a CRUD reads a new
|
|
75
|
+
* record back once (`details`) to name it.
|
|
76
|
+
*
|
|
77
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus", from: "regionId" })
|
|
78
|
+
*/
|
|
79
|
+
dbJoin({ status, from, ...column }, backend) {
|
|
80
|
+
return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column, dbJoin: from ? { from, status } : { status } };
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
84
|
+
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
85
|
+
* page, searched and sorted like any column - with the same short tag when the record is no longer available, and
|
|
86
|
+
* "Not found" for a key with no record. `field` is the looked-up record's field, when it is not the column's `key`.
|
|
87
|
+
*
|
|
88
|
+
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
89
|
+
*/
|
|
90
|
+
clientJoin(api, { from, field, ...column }, backend) {
|
|
91
|
+
return {
|
|
92
|
+
join: { from, field, source: api.findListing },
|
|
93
|
+
tag: (_row, joined) => (joined ? (backend ?? getCrudBackend()).unavailableTag(joined.recordStatus) : null),
|
|
94
|
+
...column,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* A price's figures under one band, "Amount details" (2026-06-20's): **Price** (money), **Discount** and **VAT**
|
|
99
|
+
* (percents), and the **Calculated amount** - the price less the discount, plus VAT on the rest, worked out from the
|
|
100
|
+
* row's own figures by the server's rule (`priceAmounts`), so a row a save just answered shows it at once. A page
|
|
101
|
+
* spreads them where its price columns go: `...crudColumns.amounts()`. The Calculated amount's key is `netAmount`.
|
|
102
|
+
*
|
|
103
|
+
* @example const COLUMNS = [crudColumns.dbJoin({ ... }), ...crudColumns.amounts(), crudColumns.status()];
|
|
104
|
+
*/
|
|
105
|
+
amounts(keys, changes = {}) {
|
|
106
|
+
const k = priceKeysOf(keys);
|
|
107
|
+
const group = changes.group ?? "Amount details";
|
|
108
|
+
return [
|
|
109
|
+
{ key: k.price, label: "Price", format: "money", decimals: 2, width: "8rem", group },
|
|
110
|
+
{ key: k.discount, label: "Discount", format: "percent", decimals: 2, width: "7rem", group },
|
|
111
|
+
{ key: k.vat, label: "VAT", format: "percent", decimals: 2, width: "6rem", group },
|
|
112
|
+
{ key: "netAmount", label: NET_AMOUNT_LABEL, format: "money", decimals: 2, width: "9rem", group, value: row => netAmountAt(row, k) },
|
|
113
|
+
];
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* A receipt's money ladder as columns for a record's details (`CrudDetails`), in one section (`group`, default
|
|
117
|
+
* "Money"): Rate, Amount, Discount (5%), VAT (13%) - each left out when it is none - and the Calculated amount, money
|
|
118
|
+
* with 2 places, each read from the row's own field (`priceLadder`, on the server's `CounterAmounts` names unless
|
|
119
|
+
* `keys` says otherwise). Pass the person's number locale, so the percentages are written as they read numbers.
|
|
120
|
+
*
|
|
121
|
+
* @example ...crudColumns.priceLadder(line, { locale: useNumberLocale() })
|
|
122
|
+
*/
|
|
123
|
+
priceLadder(row, options = {}) {
|
|
124
|
+
const { group = "Money", ...ladder } = options;
|
|
125
|
+
return priceLadder(row, ladder).map(line => ({ key: line.key, label: line.label, format: "money", decimals: 2, group }));
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
export const crudInputs = {
|
|
129
|
+
/**
|
|
130
|
+
* The record's description: Markdown, at most `CRUD_DESCRIPTION_MAX` characters, across the form. One definition for
|
|
131
|
+
* every model, its hint included (the owner, 2026-10-08): a page gives none of its own. `changes` is for its place
|
|
132
|
+
* in a form, such as `span`.
|
|
133
|
+
*/
|
|
134
|
+
description(changes = {}) {
|
|
135
|
+
return { type: EInputTypes.Markdown, name: "description", label: "Description", maxLength: CRUD_DESCRIPTION_MAX, height: 160, hint: "Anything worth knowing about it. Markdown.", span: 12, ...changes };
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* The **Calculated amount** in a price's form: a box after Price, Discount and VAT that follows them as they are typed
|
|
139
|
+
* (`compute`), by the server's rule (`priceAmounts`) - never typed, never sent, never a change. Blank while the price
|
|
140
|
+
* is empty, so a cleared price never reads 0.00. Span 3, so Price | Discount | VAT | Calculated amount fill a row (two
|
|
141
|
+
* pairs on a narrow form). `changes` may replace any key, `compute` included: a counter's box shows the server's
|
|
142
|
+
* preview instead (`compute: { from: [], value: (_values, line) => line?.netAmount }` with the form's `computeWith`).
|
|
143
|
+
*
|
|
144
|
+
* @example INPUTS = [..., price, discountPercentage, vatPercentage, crudInputs.netAmount()]
|
|
145
|
+
*/
|
|
146
|
+
netAmount(keys, changes = {}) {
|
|
147
|
+
const k = priceKeysOf(keys);
|
|
148
|
+
const from = [k.price, k.quantity, k.discount, k.vat].filter((key) => key !== undefined);
|
|
149
|
+
return {
|
|
150
|
+
type: EInputTypes.Number,
|
|
151
|
+
name: "netAmount",
|
|
152
|
+
label: NET_AMOUNT_LABEL,
|
|
153
|
+
decimals: 2,
|
|
154
|
+
span: 3,
|
|
155
|
+
hint: "Price less the discount, plus VAT on the rest.",
|
|
156
|
+
compute: { from, value: values => netAmountAt(values, k) },
|
|
157
|
+
...changes,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { CrudDetailsPicture, CrudDetailsProfile, CrudDetailsTab } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
+
import type { FormValues } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { TableColumn } from "../Interfaces/TableInterfaces.ts";
|
|
5
|
+
/** The built-in tab: the record's fields, with the details' `aside` beside them. */
|
|
6
|
+
export declare const CRUD_DETAILS_TAB = "details";
|
|
7
|
+
/** A tab as offered: its label settled and its badge worked out for the record. */
|
|
8
|
+
export interface OfferedCrudTab<TRow extends FormValues = FormValues> extends CrudDetailsTab<TRow> {
|
|
9
|
+
label: string;
|
|
10
|
+
/** Whether it is the built-in fields (a `details` entry without a `render`). */
|
|
11
|
+
builtIn: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The tabs offered for a record, in the page's order: the built-in `details` first unless the page lists it, each tab
|
|
15
|
+
* whose `endpoint` the person may not call left out (`can`, the rule every action follows), and each the page hides
|
|
16
|
+
* for this record. A tab id listed twice is offered once, where it is first listed.
|
|
17
|
+
*/
|
|
18
|
+
export declare function offeredCrudTabs<TRow extends FormValues>(tabs: readonly CrudDetailsTab<TRow>[] | undefined, row: TRow, can: (endpoint: ApiEndpoint) => boolean): OfferedCrudTab<TRow>[];
|
|
19
|
+
/** The tab a popup opens on: the one asked for, else the one kept, while offered; otherwise the first. */
|
|
20
|
+
export declare function openingCrudTab(offered: readonly {
|
|
21
|
+
id: string;
|
|
22
|
+
}[], ...wanted: (string | null | undefined)[]): string | undefined;
|
|
23
|
+
/** Where this browser keeps the last tab: `nx-crud-tab:{key}:{viewer}`; nothing without a viewer. */
|
|
24
|
+
export declare function crudTabStorageKey(remember: {
|
|
25
|
+
key: string;
|
|
26
|
+
viewer?: string | null;
|
|
27
|
+
} | undefined): string | null;
|
|
28
|
+
/** The kept tab's id, or null. */
|
|
29
|
+
export declare function readCrudTab(storage: Pick<Storage, "getItem"> | null, key: string | null): string | null;
|
|
30
|
+
/** Keeps the tab's id; a browser that will not keep it (storage off, full) is left as it is. */
|
|
31
|
+
export declare function writeCrudTab(storage: Pick<Storage, "setItem"> | null, key: string | null, tab: string): void;
|
|
32
|
+
/** Whether a profile is given as its parts (a `title`), rather than drawn by the page. */
|
|
33
|
+
export declare function isCrudProfile<TRow extends FormValues = FormValues>(value: unknown): value is CrudDetailsProfile<TRow>;
|
|
34
|
+
/** How a details popup's full record (its details endpoint) is coming along: being read, read, or not read (it failed). */
|
|
35
|
+
export type CrudRecordRead = "loading" | "done" | "failed";
|
|
36
|
+
/**
|
|
37
|
+
* Whether a field waits on the full record: it is still being read (or its read failed) and the row has no value under
|
|
38
|
+
* the field's key at all - the list left it out. A column that draws or works out its own value (`render`, `value`)
|
|
39
|
+
* never waits: only it knows what it reads. The Details tab and the profile draw such a field blank while it is read,
|
|
40
|
+
* and "Not loaded" when the read failed.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isAwaitedCrudField<TRow extends FormValues>(column: TableColumn<TRow>, row: TRow, read: CrudRecordRead): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The fields a record's profile shows (`CrudDetailsProfile.fields`), in the page's order: a column given whole, or the
|
|
45
|
+
* details' column of the key given (a key no column has is left out); a key named twice is shown once, where first
|
|
46
|
+
* named; and a field with no value for the record - its own `value`, a join's or the field's: null, undefined, blank
|
|
47
|
+
* text, an empty list, NaN - is left out. `false` and `0` are values. A field the row lacks while the full record is
|
|
48
|
+
* read, or after its read failed (`isAwaitedCrudField`), is kept: the profile holds its line, then says "Not loaded".
|
|
49
|
+
*/
|
|
50
|
+
export declare function crudProfileFields<TRow extends FormValues>(fields: readonly (TableColumn<TRow> | string)[] | undefined, columns: readonly TableColumn<TRow>[], row: TRow, read?: CrudRecordRead): TableColumn<TRow>[];
|
|
51
|
+
/**
|
|
52
|
+
* The details' columns less every key a profile's `fields` names, so the Details tab never shows a field the profile
|
|
53
|
+
* does. A key the profile leaves out for having no value stays out of the Details too: it has nothing to show there. A
|
|
54
|
+
* field still being read, or whose read failed, is in the profile (`crudProfileFields`), held or "Not loaded".
|
|
55
|
+
*/
|
|
56
|
+
export declare function crudDetailsWithoutProfile<TRow extends FormValues>(columns: readonly TableColumn<TRow>[], fields: readonly (TableColumn<TRow> | string)[] | undefined): readonly TableColumn<TRow>[];
|
|
57
|
+
/** Whether a profile's picture is given by the owner's Drive params, rather than drawn by the page. */
|
|
58
|
+
export declare function isCrudPicture(value: unknown): value is CrudDetailsPicture;
|