nexus-shared 3.1.0 → 3.2.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 +704 -446
- package/LICENSE +21 -21
- package/README.md +44 -5
- package/dist/Client.Index.d.ts +3 -0
- package/dist/Client.Index.js +3 -0
- package/dist/Components/Forms/Crud.js +322 -161
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudParts.js +2 -1
- package/dist/Components/Forms/Form.js +4 -1
- 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.js +4 -1
- package/dist/Components/Inputs/InputRenderer.js +3 -0
- package/dist/Components/Inputs/NumberBox.js +4 -2
- package/dist/Components/Inputs/TextArea.js +4 -1
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +55 -5
- package/dist/Components/Inputs/TreeView.js +372 -23
- package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +63 -21
- package/dist/Components/Viewers/DataTableParts.d.ts +26 -8
- package/dist/Components/Viewers/DataTableParts.js +39 -10
- 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/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +94 -4
- package/dist/Helpers/CrudBackend.js +168 -9
- package/dist/Helpers/CrudColumns.d.ts +51 -0
- package/dist/Helpers/CrudColumns.js +69 -0
- package/dist/Helpers/CrudHelpers.d.ts +50 -5
- package/dist/Helpers/CrudHelpers.js +109 -12
- package/dist/Helpers/InputParamsHelpers.js +8 -0
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/RequestCache.d.ts +35 -0
- package/dist/Helpers/RequestCache.js +113 -0
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableHelpers.d.ts +1 -1
- package/dist/Helpers/TableHelpers.js +7 -2
- package/dist/Helpers/TableJoins.d.ts +70 -0
- package/dist/Helpers/TableJoins.js +250 -0
- package/dist/Helpers/TableStore.d.ts +5 -0
- package/dist/Helpers/TableStore.js +9 -0
- package/dist/Helpers/TreeStore.d.ts +225 -11
- package/dist/Helpers/TreeStore.js +683 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +47 -9
- package/dist/Interfaces/FormInterfaces.d.ts +4 -1
- package/dist/Interfaces/InputInterfaces.d.ts +24 -0
- 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/TableInterfaces.d.ts +86 -4
- 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/JoinSources.d.ts +48 -0
- package/dist/Services/JoinSources.js +74 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +4 -0
- package/dist/Shared.Index.js +4 -0
- package/package.json +87 -85
- package/src/Styles/Nexus.Avatar.css +79 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +180 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +343 -343
- package/src/Styles/Nexus.Crud.css +168 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Form.css +279 -279
- package/src/Styles/Nexus.Index.css +37 -37
- package/src/Styles/Nexus.Input.css +196 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +237 -237
- package/src/Styles/Nexus.Popup.css +724 -724
- package/src/Styles/Nexus.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +432 -432
- package/src/Styles/Nexus.Table.css +31 -30
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +89 -89
- package/src/Styles/Nexus.Tree.css +500 -431
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CRUD_MARK_BITS, hasMark, withMark } from "./CrudHelpers.js";
|
|
2
|
+
const NO_AUDIT_WORDS = { commands: {}, directions: {}, parts: {} };
|
|
1
3
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
4
|
export class CrudBackend {
|
|
3
5
|
/** Reads a whole list: an array, or the rows of a page. */
|
|
@@ -6,6 +8,83 @@ export class CrudBackend {
|
|
|
6
8
|
return result;
|
|
7
9
|
return [...this.readPage(result).rows];
|
|
8
10
|
}
|
|
11
|
+
/** Whether a record can be flagged or unflagged at all. Default yes. */
|
|
12
|
+
canFlag(row) {
|
|
13
|
+
void row;
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* What flagging one record or many sends beside the key(s): the state wanted, never a toggle. Default the Nexus
|
|
18
|
+
* backend's `?isFlagged=true`. A backend that toggles ignores it, and a CRUD sends only the records a change is for.
|
|
19
|
+
*/
|
|
20
|
+
flagRequest(on) {
|
|
21
|
+
return { query: { isFlagged: on } };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* This user's marks on a record as a set of bits (`CRUD_MARK_BITS`: pinned 1, favourite 2), as a bookmark listing
|
|
25
|
+
* answers them on each record. Bits the package does not know are kept as they are. Default: the pin alone, through
|
|
26
|
+
* `isPinned`.
|
|
27
|
+
*/
|
|
28
|
+
marksOf(row) {
|
|
29
|
+
return this.isPinned(row) ? CRUD_MARK_BITS.pin : 0;
|
|
30
|
+
}
|
|
31
|
+
/** The row carrying this set of marks. Default: the pin alone, through `withPin`. */
|
|
32
|
+
withMarks(row, marks) {
|
|
33
|
+
return this.withPin(row, hasMark(marks, "pin"));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What setting one record's marks sends beside its key: the **whole set** it is to have, never a toggle. Default the
|
|
37
|
+
* Nexus backend's: `?bookmarkTypes=3`.
|
|
38
|
+
*/
|
|
39
|
+
marksRequest(marks) {
|
|
40
|
+
return { query: { bookmarkTypes: marks } };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What putting one mark on many records, or taking it off, sends: the keys, the mark and which way. Each record keeps
|
|
44
|
+
* its other marks. Default the Nexus backend's: the keys as the body, `?bookmarkTypes=2&isAdding=true`.
|
|
45
|
+
*/
|
|
46
|
+
massMarksRequest(keys, mark, on) {
|
|
47
|
+
return { body: keys, query: { bookmarkTypes: CRUD_MARK_BITS[mark], isAdding: on } };
|
|
48
|
+
}
|
|
49
|
+
/** The marks a write left on the record, read from its answer. `null` when it does not say, and the set sent stands. */
|
|
50
|
+
answeredMarks(result) {
|
|
51
|
+
void result;
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** What an export sends to name the list on screen, beside the table's own filters. Default nothing. */
|
|
55
|
+
exportFilters(mode) {
|
|
56
|
+
void mode;
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
/** A record status in words, for a status column and the history: "Active", "Flagged", "Trashed". Default the number. */
|
|
60
|
+
statusText(status) {
|
|
61
|
+
return String(status);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The short tag a reference to a record carries when that record is no longer available - "Trashed" beside a parent in
|
|
65
|
+
* the trash, "Deleted" beside one deleted - read from its status. `null` for a live record, and for no status at all.
|
|
66
|
+
* A joined column (`crudColumns.dbJoin`, `crudColumns.clientJoin`) draws it after the name. Default: none.
|
|
67
|
+
*/
|
|
68
|
+
unavailableTag(status) {
|
|
69
|
+
void status;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a record may be picked in a form: what a picker offers, and what it shows tagged and will not offer again when
|
|
74
|
+
* a form already holds it. Read from the record. Default: every record.
|
|
75
|
+
*/
|
|
76
|
+
isPickable(record) {
|
|
77
|
+
void record;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The words of a record's history: what each command did (`commands`), the audit keys that are the whole story of a
|
|
82
|
+
* switch (`directions`), and the parts of the record an update names (`parts`, with `update` the command that names
|
|
83
|
+
* one). Numbers an audit row keeps for good, so each backend's own. Default none: "Action 3".
|
|
84
|
+
*/
|
|
85
|
+
get auditWords() {
|
|
86
|
+
return NO_AUDIT_WORDS;
|
|
87
|
+
}
|
|
9
88
|
/**
|
|
10
89
|
* Changes an action's answer makes to rows, by key: a mass flag that answers each record's new status. Rows without
|
|
11
90
|
* an entry take the change the action stands for (a toggled flag). Default: none.
|
|
@@ -29,7 +108,7 @@ export class CrudBackend {
|
|
|
29
108
|
* `null` when it says nothing about it - a bare key, an empty body - and then the caller keeps what it has.
|
|
30
109
|
*
|
|
31
110
|
* **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
|
|
111
|
+
* table, the sections above it and every list kept in the cache, and nothing is loaded again. Only answers of actions on
|
|
33
112
|
* **one** record are read here; a mass answer goes through `answeredChanges`, record by record.
|
|
34
113
|
*/
|
|
35
114
|
answeredRow(result) {
|
|
@@ -70,10 +149,10 @@ export class CrudBackend {
|
|
|
70
149
|
* 40–59 trash
|
|
71
150
|
* 60+ delete
|
|
72
151
|
*
|
|
73
|
-
* The flag is a **range**, 25–29, and
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* the
|
|
152
|
+
* The flag is a **range**, 25–29, and so is every test here (`isFlagged`, `listMembership`), since 2026-10-08: the
|
|
153
|
+
* Regions page already read it as a range, and a list that reads 26 as flagged where the table reads it as not would
|
|
154
|
+
* disagree with itself. A flag written is still 25, the seeded `ERecordStatus.Flagged`. Per-project bands from
|
|
155
|
+
* `/v2/core-config` would change the numbers here, never the shape of the test.
|
|
77
156
|
*/
|
|
78
157
|
/** The Nexus record status of a flagged record (`ERecordStatus.Flagged`), the bottom of the 25–29 flag band. */
|
|
79
158
|
export const NEXUS_FLAGGED_STATUS = 25;
|
|
@@ -85,10 +164,37 @@ export const NEXUS_TRASH_STATUS = 40;
|
|
|
85
164
|
export const NEXUS_DELETED_STATUS = 60;
|
|
86
165
|
/** Just past the flag band: a flagged record is 25–29, so 30 and up is live but not flagged. */
|
|
87
166
|
const NEXUS_FLAG_BAND_END = 30;
|
|
167
|
+
/** Each list as the Nexus backend's `ECommandTypes` numbers its listing: what an export names the list on screen by. */
|
|
168
|
+
const NEXUS_LIST_COMMANDS = { normal: 50, flag: 52, trash: 54, deleted: 55 };
|
|
169
|
+
/** The statuses `ERecordStatus` names. Imported (1) is what a spreadsheet import writes unless the person picks another. */
|
|
170
|
+
const NEXUS_STATUS_WORDS = { 0: "Active", 1: "Imported", 25: "Flagged", 30: "Pending", 31: "Unverified", 32: "In review", 33: "Approval required", 40: "Trashed", 60: "Deleted" };
|
|
171
|
+
/** A record's history in words, by the Nexus backend's `ECommandTypes` and `EAuditKeys`. An audit row keeps the numbers for good. */
|
|
172
|
+
const NEXUS_AUDIT_WORDS = {
|
|
173
|
+
commands: {
|
|
174
|
+
0: "Added",
|
|
175
|
+
1: "Updated",
|
|
176
|
+
3: "Marks changed",
|
|
177
|
+
4: "Flag changed",
|
|
178
|
+
10: "Moved to the trash",
|
|
179
|
+
11: "Recovered from the trash",
|
|
180
|
+
12: "Deleted permanently",
|
|
181
|
+
13: "Moved back to the trash",
|
|
182
|
+
20: "Basic information updated",
|
|
183
|
+
21: "Billing information updated",
|
|
184
|
+
22: "Address updated",
|
|
185
|
+
23: "References updated",
|
|
186
|
+
24: "Contact information updated",
|
|
187
|
+
25: "Email updated",
|
|
188
|
+
},
|
|
189
|
+
directions: { 7: "Flagged", 8: "Unflagged", 9: "Made a favourite", 10: "Taken out of the favourites" },
|
|
190
|
+
parts: { 1: "basic information", 2: "contact information", 3: "email", 4: "address", 5: "profile picture", 6: "password", 11: "permissions", 12: "billing information", 13: "references" },
|
|
191
|
+
update: 1,
|
|
192
|
+
};
|
|
88
193
|
/**
|
|
89
194
|
* The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
90
195
|
* `{ 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,
|
|
196
|
+
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, and this user's marks are
|
|
197
|
+
* its `bookmarkTypes` (`[Flags] EBookmarkTypes`: 1 pinned, 2 favourite), or `isPinned` from a backend before them.
|
|
92
198
|
*/
|
|
93
199
|
export class NexusCrudBackend extends CrudBackend {
|
|
94
200
|
pageRequest(query, sortKey, filters) {
|
|
@@ -119,17 +225,70 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
119
225
|
massRequest(action, keys, values) {
|
|
120
226
|
return action === "description" ? { description: values?.description ?? null, accountIds: keys } : keys;
|
|
121
227
|
}
|
|
228
|
+
/** The whole flag band, 25-29: every status comparison is a range test. */
|
|
122
229
|
isFlagged(row) {
|
|
123
|
-
|
|
230
|
+
const status = Number(row.recordStatus);
|
|
231
|
+
return status >= NEXUS_FLAGGED_STATUS && status < NEXUS_FLAG_BAND_END;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* A live record that is not pending (0-29): the backend flags 0-24 and unflags 25-29, and never touches a pending
|
|
235
|
+
* record (30-39), which would lose that status for good. A record with no status yet is not refused here.
|
|
236
|
+
*/
|
|
237
|
+
canFlag(row) {
|
|
238
|
+
const status = row.recordStatus;
|
|
239
|
+
return typeof status !== "number" || status < NEXUS_FLAG_BAND_END;
|
|
124
240
|
}
|
|
125
241
|
withFlag(row, on) {
|
|
126
242
|
return { ...row, recordStatus: on ? NEXUS_FLAGGED_STATUS : NEXUS_ACTIVE_STATUS };
|
|
127
243
|
}
|
|
128
244
|
isPinned(row) {
|
|
129
|
-
return row
|
|
245
|
+
return hasMark(this.marksOf(row), "pin");
|
|
130
246
|
}
|
|
131
247
|
withPin(row, on) {
|
|
132
|
-
return
|
|
248
|
+
return this.withMarks(row, withMark(this.marksOf(row), "pin", on));
|
|
249
|
+
}
|
|
250
|
+
marksOf(row) {
|
|
251
|
+
const marks = row.bookmarkTypes;
|
|
252
|
+
if (typeof marks === "number" && Number.isInteger(marks) && marks >= 0)
|
|
253
|
+
return marks;
|
|
254
|
+
return row.isPinned === true ? CRUD_MARK_BITS.pin : 0;
|
|
255
|
+
}
|
|
256
|
+
/** `bookmarkTypes`, and `isPinned` too on a record that has one, so neither says something the other does not. */
|
|
257
|
+
withMarks(row, marks) {
|
|
258
|
+
const next = { ...row, bookmarkTypes: marks };
|
|
259
|
+
if ("isPinned" in row)
|
|
260
|
+
next.isPinned = hasMark(marks, "pin");
|
|
261
|
+
return next;
|
|
262
|
+
}
|
|
263
|
+
/** `SwitchBookmarks` answers the marks it stored, as a number. */
|
|
264
|
+
answeredMarks(result) {
|
|
265
|
+
return typeof result === "number" && Number.isInteger(result) && result >= 0 ? result : null;
|
|
266
|
+
}
|
|
267
|
+
/** An export names the list on screen as `flag`, by the number `ECommandTypes` gives its listing. */
|
|
268
|
+
exportFilters(mode) {
|
|
269
|
+
return { flag: NEXUS_LIST_COMMANDS[mode] };
|
|
270
|
+
}
|
|
271
|
+
/** The members `ERecordStatus` names, then the band of any other number in it. */
|
|
272
|
+
statusText(status) {
|
|
273
|
+
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");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 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
|
|
277
|
+
* very short tag"): "Trashed" in the warning color, "Deleted" in the danger color. A flagged or pending record is still
|
|
278
|
+
* live, and carries none.
|
|
279
|
+
*/
|
|
280
|
+
unavailableTag(status) {
|
|
281
|
+
if (typeof status !== "number" || !Number.isFinite(status) || status < NEXUS_TRASH_STATUS)
|
|
282
|
+
return null;
|
|
283
|
+
return { label: this.statusText(status), tone: status < NEXUS_DELETED_STATUS ? "warning" : "danger" };
|
|
284
|
+
}
|
|
285
|
+
/** Usable, under 30, as a Nexus `ActiveListing` answers: a pending record is live but not one to pick. No status reads as usable. */
|
|
286
|
+
isPickable(record) {
|
|
287
|
+
const status = record.recordStatus;
|
|
288
|
+
return typeof status !== "number" || status < NEXUS_FLAG_BAND_END;
|
|
289
|
+
}
|
|
290
|
+
get auditWords() {
|
|
291
|
+
return NEXUS_AUDIT_WORDS;
|
|
133
292
|
}
|
|
134
293
|
/** Mass answers list the records they changed, with their new `recordStatus` and `isPinned`. */
|
|
135
294
|
answeredChanges(result, rowKey) {
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
6
|
+
export declare const CRUD_DESCRIPTION_MAX = 1000;
|
|
7
|
+
export declare const crudColumns: {
|
|
8
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank until a new record is read back. */
|
|
9
|
+
status<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>, backend?: CrudBackend): TableColumn<TRow>;
|
|
10
|
+
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
11
|
+
description<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
12
|
+
/** When the record was added. Hidden until the user shows it. */
|
|
13
|
+
added<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
14
|
+
/** When it was changed last. Hidden until the user shows it. */
|
|
15
|
+
changed<TRow extends TableRow>(changes?: Partial<TableColumn<TRow>>): TableColumn<TRow>;
|
|
16
|
+
/**
|
|
17
|
+
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
18
|
+
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
19
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up.
|
|
20
|
+
*
|
|
21
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
|
|
22
|
+
*/
|
|
23
|
+
dbJoin<TRow extends TableRow>({ status, ...column }: DbJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
24
|
+
/**
|
|
25
|
+
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
26
|
+
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
27
|
+
* page, searched and sorted like any column - with the same short tag when the record is no longer available, and
|
|
28
|
+
* "Not found" for a key with no record. `field` is the looked-up record's field, when it is not the column's `key`.
|
|
29
|
+
*
|
|
30
|
+
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
31
|
+
*/
|
|
32
|
+
clientJoin<TRow extends TableRow>(api: JoinController, { from, field, ...column }: ClientJoinColumn<TRow>, backend?: CrudBackend): TableColumn<TRow>;
|
|
33
|
+
};
|
|
34
|
+
/** What `crudColumns.dbJoin` takes: a column, and the row's field that holds the joined record's status. */
|
|
35
|
+
export type DbJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & {
|
|
36
|
+
status: string;
|
|
37
|
+
};
|
|
38
|
+
/** What `crudColumns.clientJoin` takes: a column, the row's field that holds the other record's key, and its own field. */
|
|
39
|
+
export type ClientJoinColumn<TRow extends TableRow> = Partial<TableColumn<TRow>> & Pick<TableColumn<TRow>, "key" | "label"> & Pick<TableJoin, "from" | "field">;
|
|
40
|
+
/** The controller a client join names its records from: any with a `findListing` (`MODULES.controller(...)`). */
|
|
41
|
+
export interface JoinController {
|
|
42
|
+
findListing: ApiEndpoint;
|
|
43
|
+
}
|
|
44
|
+
export declare const crudInputs: {
|
|
45
|
+
/**
|
|
46
|
+
* The record's description: Markdown, at most `CRUD_DESCRIPTION_MAX` characters, across the form. One definition for
|
|
47
|
+
* every model, its hint included (the owner, 2026-10-08): a page gives none of its own. `changes` is for its place
|
|
48
|
+
* in a form, such as `span`.
|
|
49
|
+
*/
|
|
50
|
+
description(changes?: Partial<InputParams>): InputParams;
|
|
51
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getCrudBackend } from "./CrudBackend.js";
|
|
2
|
+
import { readPath } from "./TableHelpers.js";
|
|
3
|
+
/*
|
|
4
|
+
* The columns and inputs every Nexus record has, so a page names them instead of writing them again: its status in
|
|
5
|
+
* words, its Markdown description, and when it was added and changed - and the two ways a page shows another record's
|
|
6
|
+
* name, joined by the database (`dbJoin`) or by the table (`clientJoin`). Each takes changes over its defaults, as a
|
|
7
|
+
* column or an input of the page's own would: `crudColumns.added({ hidden: false })`.
|
|
8
|
+
*/
|
|
9
|
+
/** The longest description a record keeps: the owner's rule for every model (the column holds 4000). */
|
|
10
|
+
export const CRUD_DESCRIPTION_MAX = 1000;
|
|
11
|
+
export const crudColumns = {
|
|
12
|
+
/** The record's status in words ("Active", "Flagged", "Trashed"), from the backend's own numbers; blank until a new record is read back. */
|
|
13
|
+
status(changes = {}, backend) {
|
|
14
|
+
return {
|
|
15
|
+
key: "recordStatus",
|
|
16
|
+
label: "Status",
|
|
17
|
+
width: "9rem",
|
|
18
|
+
render: (_row, value) => (typeof value === "number" ? (backend ?? getCrudBackend()).statusText(value) : ""),
|
|
19
|
+
...changes,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
/** The description, as Markdown: text in the table, formatted in the details. */
|
|
23
|
+
description(changes = {}) {
|
|
24
|
+
return { key: "description", label: "Description", wrap: true, sortable: false, markdown: true, ...changes };
|
|
25
|
+
},
|
|
26
|
+
/** When the record was added. Hidden until the user shows it. */
|
|
27
|
+
added(changes = {}) {
|
|
28
|
+
return { key: "creationTime", label: "Added", format: "date-time", hidden: true, ...changes };
|
|
29
|
+
},
|
|
30
|
+
/** When it was changed last. Hidden until the user shows it. */
|
|
31
|
+
changed(changes = {}) {
|
|
32
|
+
return { key: "modificationTime", label: "Changed", format: "date-time", hidden: true, ...changes };
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Another record's name the **database** joined into the row: the row carries the name (`key`) and that record's own
|
|
36
|
+
* status (`status`), and the cell shows the name with a short tag when the record is no longer available ("Trashed",
|
|
37
|
+
* "Deleted": `CrudBackend.unavailableTag`). One call, nothing looked up.
|
|
38
|
+
*
|
|
39
|
+
* @example crudColumns.dbJoin({ key: "regionName", label: "Region", status: "regionRecordStatus" })
|
|
40
|
+
*/
|
|
41
|
+
dbJoin({ status, ...column }, backend) {
|
|
42
|
+
return { tag: row => (backend ?? getCrudBackend()).unavailableTag(readPath(row, status)), ...column };
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Another record's name the **table** joins: the row carries only that record's key (`from`), and the table names it
|
|
46
|
+
* from the other controller's `findListing` - every key the rows hold in one call, kept for every table and form on the
|
|
47
|
+
* page, searched and sorted like any column - with the same short tag when the record is no longer available, and
|
|
48
|
+
* "Not found" for a key with no record. `field` is the looked-up record's field, when it is not the column's `key`.
|
|
49
|
+
*
|
|
50
|
+
* @example crudColumns.clientJoin(SUB_REGIONS, { key: "subRegionName", label: "Sub-region", from: "subRegionId" })
|
|
51
|
+
*/
|
|
52
|
+
clientJoin(api, { from, field, ...column }, backend) {
|
|
53
|
+
return {
|
|
54
|
+
join: { from, field, source: api.findListing },
|
|
55
|
+
tag: (_row, joined) => (joined ? (backend ?? getCrudBackend()).unavailableTag(joined.recordStatus) : null),
|
|
56
|
+
...column,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const crudInputs = {
|
|
61
|
+
/**
|
|
62
|
+
* The record's description: Markdown, at most `CRUD_DESCRIPTION_MAX` characters, across the form. One definition for
|
|
63
|
+
* every model, its hint included (the owner, 2026-10-08): a page gives none of its own. `changes` is for its place
|
|
64
|
+
* in a form, such as `span`.
|
|
65
|
+
*/
|
|
66
|
+
description(changes = {}) {
|
|
67
|
+
return { type: "markdown", name: "description", label: "Description", maxLength: CRUD_DESCRIPTION_MAX, height: 160, hint: "Anything worth knowing about it. Markdown.", span: 12, ...changes };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
-
import type { CrudActionName, CrudEndpoints, CrudMassActionName, CrudMode, CrudRowActionName } from "../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { CrudActionName, CrudEndpoints, CrudMark, CrudMassActionName, CrudMode, CrudRowActionName } from "../Interfaces/CrudInterfaces.ts";
|
|
3
3
|
import type { MessageActionName } from "../Interfaces/MessageInterfaces.ts";
|
|
4
4
|
import type { TablePage, TablePaging, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
5
5
|
/** Every list, in the order the CRUD shows them. */
|
|
6
6
|
export declare const CRUD_MODES: readonly CrudMode[];
|
|
7
|
-
/** The built-in actions on one record, per list, in their menu order. */
|
|
7
|
+
/** The built-in actions on one record, per list, in their menu order. A record's history is on every list. */
|
|
8
8
|
export declare const CRUD_ROW_ACTIONS: Readonly<Record<CrudMode, readonly CrudRowActionName[]>>;
|
|
9
9
|
/** The built-in actions on selected records, per list. */
|
|
10
10
|
export declare const CRUD_MASS_ACTIONS: Readonly<Record<CrudMode, readonly CrudMassActionName[]>>;
|
|
@@ -18,8 +18,13 @@ export declare function crudRowActions(mode: CrudMode, allowed: (action: CrudRow
|
|
|
18
18
|
export declare function crudMassActions(mode: CrudMode, allowed: (action: CrudMassActionName) => boolean, trashless?: boolean): CrudMassActionName[];
|
|
19
19
|
/** The endpoint of a list: its full list for tables that page in the browser, its pages for server tables. */
|
|
20
20
|
export declare function crudListEndpoint(api: CrudEndpoints, mode: CrudMode, paging: TablePaging): ApiEndpoint | undefined;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* The endpoint of a row action. `view` needs none of its own: it shows the row (and calls `details` when there is one).
|
|
23
|
+
* Pin and Favourite both set the record's marks through `bookmark`; a backend without it may still pin, through `pin`.
|
|
24
|
+
*/
|
|
22
25
|
export declare function crudRowEndpoint(api: CrudEndpoints, action: CrudRowActionName): ApiEndpoint | undefined;
|
|
26
|
+
/** The endpoint of an action on many records: Pin and Favourite through `mass.bookmark`, or Pin through `mass.pin`. */
|
|
27
|
+
export declare function crudMassEndpoint(api: CrudEndpoints, action: CrudMassActionName): ApiEndpoint | undefined;
|
|
23
28
|
/** The lists a CRUD can offer: asked for, with an endpoint, and allowed. */
|
|
24
29
|
export declare function availableModes(api: CrudEndpoints, paging: TablePaging, wanted: readonly CrudMode[], can: (endpoint: ApiEndpoint) => boolean): CrudMode[];
|
|
25
30
|
/**
|
|
@@ -31,8 +36,8 @@ export declare function actionAllowed(action: CrudActionName, api: CrudEndpoints
|
|
|
31
36
|
export declare function massActionAllowed(action: CrudMassActionName, api: CrudEndpoints, wanted: readonly CrudActionName[], can: (endpoint: ApiEndpoint) => boolean): boolean;
|
|
32
37
|
/**
|
|
33
38
|
* Whether a record leaves the list shown after an action. Trash, recover, delete, and moving back to the trash take it
|
|
34
|
-
* to another list, and unflagging leaves the flagged list.
|
|
35
|
-
*
|
|
39
|
+
* to another list, and unflagging leaves the flagged list. A pin or a favourite never does: a marked record stays in
|
|
40
|
+
* the list it is in. `markOn` is the record's new flag.
|
|
36
41
|
*/
|
|
37
42
|
export declare function leavesList(action: CrudRowActionName | CrudMassActionName, mode: CrudMode, markOn?: boolean): boolean;
|
|
38
43
|
/** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
|
|
@@ -44,6 +49,8 @@ export interface CrudSettings {
|
|
|
44
49
|
* pins and can unpin, and may pin again once they are under it.
|
|
45
50
|
*/
|
|
46
51
|
maxPins: number;
|
|
52
|
+
/** Most records a user may make favourites, kept the same way. Default: whatever `maxPins` is. */
|
|
53
|
+
maxFavorites?: number;
|
|
47
54
|
}
|
|
48
55
|
/**
|
|
49
56
|
* Changes what every CRUD follows, once, where the app registers its API modules: `configureCrud({ maxPins: 50 })`.
|
|
@@ -57,6 +64,44 @@ export declare function getCrudSettings(): CrudSettings;
|
|
|
57
64
|
* when nothing counts the pins (no `pins` endpoint), and then there is no limit to keep.
|
|
58
65
|
*/
|
|
59
66
|
export declare function pinRoom(pinned: number | undefined, max?: number): number;
|
|
67
|
+
/** Every mark, in the order their sections show above the list: Pinned, then Favourites. */
|
|
68
|
+
export declare const CRUD_MARKS: readonly CrudMark[];
|
|
69
|
+
/**
|
|
70
|
+
* Each mark's bit in a record's set of marks, as the Nexus backend's `[Flags] EBookmarkTypes` numbers them. A set may
|
|
71
|
+
* hold bits this package does not know - a backend's next mark is its next bit - and a change here keeps them.
|
|
72
|
+
*/
|
|
73
|
+
export declare const CRUD_MARK_BITS: Readonly<Record<CrudMark, number>>;
|
|
74
|
+
/** Whether a set of marks holds this mark. */
|
|
75
|
+
export declare function hasMark(marks: number, mark: CrudMark): boolean;
|
|
76
|
+
/** The set of marks with this one put on or taken off, and every other bit as it was. */
|
|
77
|
+
export declare function withMark(marks: number, mark: CrudMark, on: boolean): number;
|
|
78
|
+
/** Most records a user may give a mark: `maxPins`, and for favourites `maxFavorites`, else the same as the pins. */
|
|
79
|
+
export declare function markLimit(mark: CrudMark, settings?: CrudSettings): number;
|
|
80
|
+
/**
|
|
81
|
+
* Which ways a selection can go for one mark, from each selected record's marks: on, when one of them lacks it, and
|
|
82
|
+
* off, when one of them has it. A mixed selection can go both ways, and each is offered on its own (the owner,
|
|
83
|
+
* 2026-10-07): no action that would change nothing.
|
|
84
|
+
*/
|
|
85
|
+
export declare function markDirections(marks: readonly number[], mark: CrudMark): {
|
|
86
|
+
on: boolean;
|
|
87
|
+
off: boolean;
|
|
88
|
+
};
|
|
89
|
+
/** This user's marked records, as a CRUD keeps them: every record's marks by key, and the records of each mark's section. */
|
|
90
|
+
export interface CrudMarkedRecords<TRow extends TableRow = TableRow> {
|
|
91
|
+
marks: ReadonlyMap<string, number>;
|
|
92
|
+
rows: Readonly<Record<CrudMark, readonly TRow[]>>;
|
|
93
|
+
}
|
|
94
|
+
/** The marked records as one listing answered them, in its order: `marksOf` reads each record's marks. */
|
|
95
|
+
export declare function markedRecords<TRow extends TableRow>(rows: readonly TRow[], marksOf: (row: TRow) => number, keyOf: (row: TRow) => string): CrudMarkedRecords<TRow>;
|
|
96
|
+
/**
|
|
97
|
+
* The marked records after a change, with no request. `stored` is each changed record's new set of marks (`0` for
|
|
98
|
+
* none). A record in `joining` - by default every record in `stored` - that now has a mark goes on top of that mark's
|
|
99
|
+
* section; one that lost a mark leaves it; every copy of a record in `rows` is replaced. With `joining` empty only the
|
|
100
|
+
* copies change: a flag or a save never moves a record into a section.
|
|
101
|
+
*/
|
|
102
|
+
export declare function patchMarkedRecords<TRow extends TableRow>(current: CrudMarkedRecords<TRow>, rows: readonly TRow[], stored: ReadonlyMap<string, number>, keyOf: (row: TRow) => string, joining?: ReadonlySet<string>): CrudMarkedRecords<TRow>;
|
|
103
|
+
/** The marked records with these records out of every section - they left the active lists - keeping their marks. */
|
|
104
|
+
export declare function dropMarkedRows<TRow extends TableRow>(current: CrudMarkedRecords<TRow>, keys: readonly string[], keyOf: (row: TRow) => string): CrudMarkedRecords<TRow>;
|
|
60
105
|
/** Default time a CRUD keeps a list: 5 minutes. */
|
|
61
106
|
export declare const CRUD_CACHE_LIFETIME: number;
|
|
62
107
|
/** Lists kept while a page is open, by key, each for a lifetime. Plain values; nothing is written to storage. */
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The rules of the CRUD component, with no React: which lists and actions a controller offers, which actions each list
|
|
3
|
-
* shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned
|
|
4
|
-
* and the cache of lists kept while the page is open - including how a change
|
|
5
|
-
* a record saved or marked costs no request on any list.
|
|
3
|
+
* shows, which records leave a list after an action, the settings every CRUD follows (how many records may be pinned
|
|
4
|
+
* or made favourites), a record's marks, and the cache of lists kept while the page is open - including how a change
|
|
5
|
+
* is written into the lists kept, so that a record saved or marked costs no request on any list.
|
|
6
6
|
*/
|
|
7
7
|
/** Every list, in the order the CRUD shows them. */
|
|
8
8
|
export const CRUD_MODES = ["normal", "flag", "trash", "deleted"];
|
|
9
|
-
const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "trash"];
|
|
10
|
-
const ACTIVE_MASS_ACTIONS = ["flag", "pin", "description", "trash"];
|
|
11
|
-
/** The built-in actions on one record, per list, in their menu order. */
|
|
9
|
+
const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "favorite", "history", "trash"];
|
|
10
|
+
const ACTIVE_MASS_ACTIONS = ["flag", "pin", "favorite", "description", "trash"];
|
|
11
|
+
/** The built-in actions on one record, per list, in their menu order. A record's history is on every list. */
|
|
12
12
|
export const CRUD_ROW_ACTIONS = {
|
|
13
13
|
normal: ACTIVE_ROW_ACTIONS,
|
|
14
14
|
flag: ACTIVE_ROW_ACTIONS,
|
|
15
|
-
trash: ["view", "recover", "delete"],
|
|
16
|
-
deleted: ["view", "trashFromDelete"],
|
|
15
|
+
trash: ["view", "history", "recover", "delete"],
|
|
16
|
+
deleted: ["view", "history", "trashFromDelete"],
|
|
17
17
|
};
|
|
18
18
|
/** The built-in actions on selected records, per list. */
|
|
19
19
|
export const CRUD_MASS_ACTIONS = {
|
|
@@ -56,14 +56,31 @@ export function crudMassActions(mode, allowed, trashless = false) {
|
|
|
56
56
|
export function crudListEndpoint(api, mode, paging) {
|
|
57
57
|
return paging === "server" ? api.pages?.[mode] : api.lists?.[mode];
|
|
58
58
|
}
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* The endpoint of a row action. `view` needs none of its own: it shows the row (and calls `details` when there is one).
|
|
61
|
+
* Pin and Favourite both set the record's marks through `bookmark`; a backend without it may still pin, through `pin`.
|
|
62
|
+
*/
|
|
60
63
|
export function crudRowEndpoint(api, action) {
|
|
61
64
|
if (action === "view")
|
|
62
65
|
return api.details;
|
|
63
66
|
if (action === "edit")
|
|
64
67
|
return api.update;
|
|
68
|
+
if (action === "favorite")
|
|
69
|
+
return api.bookmark;
|
|
70
|
+
if (action === "pin")
|
|
71
|
+
return api.bookmark ?? api.pin;
|
|
72
|
+
if (action === "history")
|
|
73
|
+
return api.audit;
|
|
65
74
|
return api[action];
|
|
66
75
|
}
|
|
76
|
+
/** The endpoint of an action on many records: Pin and Favourite through `mass.bookmark`, or Pin through `mass.pin`. */
|
|
77
|
+
export function crudMassEndpoint(api, action) {
|
|
78
|
+
if (action === "favorite")
|
|
79
|
+
return api.mass?.bookmark;
|
|
80
|
+
if (action === "pin")
|
|
81
|
+
return api.mass?.bookmark ?? api.mass?.pin;
|
|
82
|
+
return api.mass?.[action];
|
|
83
|
+
}
|
|
67
84
|
/** The lists a CRUD can offer: asked for, with an endpoint, and allowed. */
|
|
68
85
|
export function availableModes(api, paging, wanted, can) {
|
|
69
86
|
return CRUD_MODES.filter(mode => wanted.includes(mode)).filter(mode => {
|
|
@@ -91,13 +108,13 @@ export function actionAllowed(action, api, wanted, can) {
|
|
|
91
108
|
export function massActionAllowed(action, api, wanted, can) {
|
|
92
109
|
if (!wanted.includes(action))
|
|
93
110
|
return false;
|
|
94
|
-
const endpoint = api
|
|
111
|
+
const endpoint = crudMassEndpoint(api, action);
|
|
95
112
|
return Boolean(endpoint) && can(endpoint);
|
|
96
113
|
}
|
|
97
114
|
/**
|
|
98
115
|
* Whether a record leaves the list shown after an action. Trash, recover, delete, and moving back to the trash take it
|
|
99
|
-
* to another list, and unflagging leaves the flagged list.
|
|
100
|
-
*
|
|
116
|
+
* to another list, and unflagging leaves the flagged list. A pin or a favourite never does: a marked record stays in
|
|
117
|
+
* the list it is in. `markOn` is the record's new flag.
|
|
101
118
|
*/
|
|
102
119
|
export function leavesList(action, mode, markOn) {
|
|
103
120
|
switch (action) {
|
|
@@ -119,6 +136,8 @@ export function crudMessageAction(action, markOn) {
|
|
|
119
136
|
return markOn === false ? "unflag" : "flag";
|
|
120
137
|
case "pin":
|
|
121
138
|
return markOn === false ? "unpin" : "pin";
|
|
139
|
+
case "favorite":
|
|
140
|
+
return markOn === false ? "unfavorite" : "favorite";
|
|
122
141
|
case "trashFromDelete":
|
|
123
142
|
return "trash";
|
|
124
143
|
case "description":
|
|
@@ -126,6 +145,8 @@ export function crudMessageAction(action, markOn) {
|
|
|
126
145
|
case "edit":
|
|
127
146
|
case "view":
|
|
128
147
|
return "save";
|
|
148
|
+
case "history":
|
|
149
|
+
return "load";
|
|
129
150
|
default:
|
|
130
151
|
return action;
|
|
131
152
|
}
|
|
@@ -153,6 +174,82 @@ export function pinRoom(pinned, max = getCrudSettings().maxPins) {
|
|
|
153
174
|
return Number.POSITIVE_INFINITY;
|
|
154
175
|
return Math.max(0, max - pinned);
|
|
155
176
|
}
|
|
177
|
+
/* This user's marks on a record */
|
|
178
|
+
/** Every mark, in the order their sections show above the list: Pinned, then Favourites. */
|
|
179
|
+
export const CRUD_MARKS = ["pin", "favorite"];
|
|
180
|
+
/**
|
|
181
|
+
* Each mark's bit in a record's set of marks, as the Nexus backend's `[Flags] EBookmarkTypes` numbers them. A set may
|
|
182
|
+
* hold bits this package does not know - a backend's next mark is its next bit - and a change here keeps them.
|
|
183
|
+
*/
|
|
184
|
+
export const CRUD_MARK_BITS = { pin: 1, favorite: 2 };
|
|
185
|
+
/** Whether a set of marks holds this mark. */
|
|
186
|
+
export function hasMark(marks, mark) {
|
|
187
|
+
return (marks & CRUD_MARK_BITS[mark]) !== 0;
|
|
188
|
+
}
|
|
189
|
+
/** The set of marks with this one put on or taken off, and every other bit as it was. */
|
|
190
|
+
export function withMark(marks, mark, on) {
|
|
191
|
+
const bit = CRUD_MARK_BITS[mark];
|
|
192
|
+
return on ? marks | bit : marks & ~bit;
|
|
193
|
+
}
|
|
194
|
+
/** Most records a user may give a mark: `maxPins`, and for favourites `maxFavorites`, else the same as the pins. */
|
|
195
|
+
export function markLimit(mark, settings = getCrudSettings()) {
|
|
196
|
+
return mark === "pin" ? settings.maxPins : (settings.maxFavorites ?? settings.maxPins);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Which ways a selection can go for one mark, from each selected record's marks: on, when one of them lacks it, and
|
|
200
|
+
* off, when one of them has it. A mixed selection can go both ways, and each is offered on its own (the owner,
|
|
201
|
+
* 2026-10-07): no action that would change nothing.
|
|
202
|
+
*/
|
|
203
|
+
export function markDirections(marks, mark) {
|
|
204
|
+
return { on: marks.some(set => !hasMark(set, mark)), off: marks.some(set => hasMark(set, mark)) };
|
|
205
|
+
}
|
|
206
|
+
/** The marked records as one listing answered them, in its order: `marksOf` reads each record's marks. */
|
|
207
|
+
export function markedRecords(rows, marksOf, keyOf) {
|
|
208
|
+
const marks = new Map();
|
|
209
|
+
for (const row of rows) {
|
|
210
|
+
const set = marksOf(row);
|
|
211
|
+
if (set)
|
|
212
|
+
marks.set(keyOf(row), set);
|
|
213
|
+
}
|
|
214
|
+
const sections = {};
|
|
215
|
+
for (const mark of CRUD_MARKS)
|
|
216
|
+
sections[mark] = rows.filter(row => hasMark(marks.get(keyOf(row)) ?? 0, mark));
|
|
217
|
+
return { marks, rows: sections };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The marked records after a change, with no request. `stored` is each changed record's new set of marks (`0` for
|
|
221
|
+
* none). A record in `joining` - by default every record in `stored` - that now has a mark goes on top of that mark's
|
|
222
|
+
* section; one that lost a mark leaves it; every copy of a record in `rows` is replaced. With `joining` empty only the
|
|
223
|
+
* copies change: a flag or a save never moves a record into a section.
|
|
224
|
+
*/
|
|
225
|
+
export function patchMarkedRecords(current, rows, stored, keyOf, joining = new Set(stored.keys())) {
|
|
226
|
+
const marks = new Map(current.marks);
|
|
227
|
+
for (const [key, set] of stored) {
|
|
228
|
+
if (set)
|
|
229
|
+
marks.set(key, set);
|
|
230
|
+
else
|
|
231
|
+
marks.delete(key);
|
|
232
|
+
}
|
|
233
|
+
const changed = new Map(rows.map(row => [keyOf(row), row]));
|
|
234
|
+
const has = (row, mark) => hasMark(marks.get(keyOf(row)) ?? 0, mark);
|
|
235
|
+
const sections = {};
|
|
236
|
+
for (const mark of CRUD_MARKS) {
|
|
237
|
+
const kept = current.rows[mark].map(row => changed.get(keyOf(row)) ?? row).filter(row => has(row, mark));
|
|
238
|
+
const held = new Set(kept.map(keyOf));
|
|
239
|
+
sections[mark] = [...rows.filter(row => joining.has(keyOf(row)) && has(row, mark) && !held.has(keyOf(row))), ...kept];
|
|
240
|
+
}
|
|
241
|
+
return { marks, rows: sections };
|
|
242
|
+
}
|
|
243
|
+
/** The marked records with these records out of every section - they left the active lists - keeping their marks. */
|
|
244
|
+
export function dropMarkedRows(current, keys, keyOf) {
|
|
245
|
+
const gone = new Set(keys);
|
|
246
|
+
if (!CRUD_MARKS.some(mark => current.rows[mark].some(row => gone.has(keyOf(row)))))
|
|
247
|
+
return current;
|
|
248
|
+
const sections = {};
|
|
249
|
+
for (const mark of CRUD_MARKS)
|
|
250
|
+
sections[mark] = current.rows[mark].filter(row => !gone.has(keyOf(row)));
|
|
251
|
+
return { marks: current.marks, rows: sections };
|
|
252
|
+
}
|
|
156
253
|
/** Default time a CRUD keeps a list: 5 minutes. */
|
|
157
254
|
export const CRUD_CACHE_LIFETIME = 5 * 60 * 1000;
|
|
158
255
|
/** Lists kept while a page is open, by key, each for a lifetime. Plain values; nothing is written to storage. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_DATE_PREFERENCES } from "../Interfaces/DateInterfaces.js";
|
|
2
|
+
import { formatCode, loadCode, validateCode } from "./CodeHelpers.js";
|
|
2
3
|
import { formatDate, formatDateRange, toDateRange, toIsoDate, usesDevanagari } from "./DateHelpers.js";
|
|
3
4
|
import { formatDateTime, toUtcDateTime } from "./DateTimeHelpers.js";
|
|
4
5
|
import { formatNumber, parseNumber } from "./NumberHelpers.js";
|
|
@@ -108,6 +109,9 @@ export function normalizeInputValue(params, value) {
|
|
|
108
109
|
return Array.isArray(value) && value.length === 2 ? value : emptyInputValue(params);
|
|
109
110
|
case "radio-group":
|
|
110
111
|
return value;
|
|
112
|
+
case "code":
|
|
113
|
+
// Only separators and case: a saved code that does not fit stays as it is, so its check refuses the save.
|
|
114
|
+
return loadCode(value, params);
|
|
111
115
|
default:
|
|
112
116
|
return typeof value === "string" ? value : String(value);
|
|
113
117
|
}
|
|
@@ -197,6 +201,8 @@ export function validateInput(params, value, context = {}) {
|
|
|
197
201
|
seconds,
|
|
198
202
|
});
|
|
199
203
|
}
|
|
204
|
+
case "code":
|
|
205
|
+
return validateCode(value, params, name);
|
|
200
206
|
case "slider":
|
|
201
207
|
return params.validate?.(value) || null;
|
|
202
208
|
case "range-slider":
|
|
@@ -369,6 +375,8 @@ export function formatInputValue(params, value, context = {}) {
|
|
|
369
375
|
return formatTime(value, timeSettings(params, preferences));
|
|
370
376
|
case "date-time":
|
|
371
377
|
return formatDateTime(value, { ...dateSettings(params, preferences), timeFormat: params.timeFormat ?? preferences.timeFormat, timeZone: params.timeZone });
|
|
378
|
+
case "code":
|
|
379
|
+
return formatCode(value, params);
|
|
372
380
|
case "slider":
|
|
373
381
|
return formatNumber(value, params);
|
|
374
382
|
case "range-slider":
|