nexus-shared 2.0.0 → 3.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 +262 -0
- package/README.md +122 -25
- package/dist/Client.Index.d.ts +11 -0
- package/dist/Client.Index.js +11 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Menu.js +24 -20
- package/dist/Components/Documents/SplitButton.js +5 -3
- package/dist/Components/Documents/TabButtons.d.ts +24 -6
- package/dist/Components/Documents/TabButtons.js +23 -4
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +202 -50
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.js +5 -1
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +53 -4
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +85 -20
- package/dist/Components/Inputs/TimePicker.js +2 -1
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +2 -2
- package/dist/Components/Layouts/ThemeSwitcher.js +35 -15
- package/dist/Components/Viewers/DataTable.js +88 -34
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +6 -2
- package/dist/Components/Viewers/DataTableParts.js +21 -9
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +47 -5
- package/dist/Helpers/CrudBackend.js +99 -4
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormStore.d.ts +2 -0
- package/dist/Helpers/FormStore.js +19 -1
- package/dist/Helpers/MessageBuilder.js +0 -2
- package/dist/Helpers/PopoverHelpers.d.ts +31 -6
- package/dist/Helpers/PopoverHelpers.js +116 -12
- package/dist/Helpers/RowsStore.d.ts +7 -1
- package/dist/Helpers/RowsStore.js +22 -0
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- package/dist/Helpers/TableHelpers.d.ts +9 -1
- package/dist/Helpers/TableHelpers.js +20 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +73 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +52 -2
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/TableInterfaces.d.ts +62 -2
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Shared.Index.d.ts +8 -0
- package/dist/Shared.Index.js +8 -0
- package/package.json +6 -3
- package/src/Styles/Nexus.Button.css +2 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Crud.css +77 -0
- package/src/Styles/Nexus.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +23 -0
- package/src/Styles/Nexus.Index.css +2 -0
- package/src/Styles/Nexus.Menu.css +12 -2
- package/src/Styles/Nexus.Rows.css +78 -48
- package/src/Styles/Nexus.Tab.Buttons.css +55 -0
- package/src/Styles/Nexus.Table.css +286 -4
- package/src/Styles/Nexus.Theme.Picker.css +75 -2
|
@@ -24,15 +24,71 @@ export class CrudBackend {
|
|
|
24
24
|
void rowKey;
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* What an answer says about the one record an action wrote: the record itself, or only the fields that changed.
|
|
29
|
+
* `null` when it says nothing about it - a bare key, an empty body - and then the caller keeps what it has.
|
|
30
|
+
*
|
|
31
|
+
* **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 pinned band and every list kept in the cache, and nothing is loaded again. Only answers of actions on
|
|
33
|
+
* **one** record are read here; a mass answer goes through `answeredChanges`, record by record.
|
|
34
|
+
*/
|
|
35
|
+
answeredRow(result) {
|
|
36
|
+
void result;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 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, and where there is
|
|
42
|
+
* a `details` endpoint it is read back on its own - one small request, never the whole list again.
|
|
43
|
+
*/
|
|
44
|
+
answeredKey(result) {
|
|
45
|
+
void result;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a record belongs in a list, read from the record itself. It is what lets one answer be written into the
|
|
50
|
+
* lists that are **not** on screen: a flagged record joins the flagged list, a trashed one leaves the active list
|
|
51
|
+
* and joins the trash, in the cache as well as in the table.
|
|
52
|
+
*
|
|
53
|
+
* `undefined` when the record does not say, and a list it cannot answer for is dropped from the cache and loads
|
|
54
|
+
* again when it is shown. Default: nothing is known, which is how every list behaved before.
|
|
55
|
+
*/
|
|
56
|
+
listMembership(row, mode) {
|
|
57
|
+
void row;
|
|
58
|
+
void mode;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
27
61
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
/*
|
|
63
|
+
* The Nexus record statuses, as the backend's `ERecordStatus` numbers them. A record's status is a number in a band,
|
|
64
|
+
* and the bands the owner settled on 2026-09-25 are:
|
|
65
|
+
*
|
|
66
|
+
* 0–39 active (anything under 40 is a live record)
|
|
67
|
+
* 0–29 active account
|
|
68
|
+
* 25–29 flag
|
|
69
|
+
* 30–39 pending, unverified, in review, approval required
|
|
70
|
+
* 40–59 trash
|
|
71
|
+
* 60+ delete
|
|
72
|
+
*
|
|
73
|
+
* The flag is a **range**, 25–29, and this file checks a single value: the seeded `ERecordStatus.Flagged` is 25, and
|
|
74
|
+
* nothing yet defines what 26–29 would mean. A range-aware check belongs with the per-project bands being built on
|
|
75
|
+
* `/v2/core-config`, so it is deliberately not here — and this note is here so the single value is not mistaken for
|
|
76
|
+
* the final answer.
|
|
77
|
+
*/
|
|
78
|
+
/** The Nexus record status of a flagged record (`ERecordStatus.Flagged`), the bottom of the 25–29 flag band. */
|
|
79
|
+
export const NEXUS_FLAGGED_STATUS = 25;
|
|
80
|
+
/** The Nexus record status of an active record (`ERecordStatus.Active`), the bottom of the 0–39 active band. */
|
|
31
81
|
export const NEXUS_ACTIVE_STATUS = 0;
|
|
82
|
+
/** The Nexus record status of a record in the trash (`ERecordStatus.Trash`), the bottom of the 40–59 trash band. */
|
|
83
|
+
export const NEXUS_TRASH_STATUS = 40;
|
|
84
|
+
/** The Nexus record status of a deleted record (`ERecordStatus.PermanentDeleted`), the bottom of the 60+ delete band. */
|
|
85
|
+
export const NEXUS_DELETED_STATUS = 60;
|
|
86
|
+
/** Just past the flag band: a flagged record is 25–29, so 30 and up is live but not flagged. */
|
|
87
|
+
const NEXUS_FLAG_BAND_END = 30;
|
|
32
88
|
/**
|
|
33
89
|
* The Nexus backend: pages as `{ searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
34
90
|
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
35
|
-
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus`
|
|
91
|
+
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, a pinned one `isPinned`.
|
|
36
92
|
*/
|
|
37
93
|
export class NexusCrudBackend extends CrudBackend {
|
|
38
94
|
pageRequest(query, sortKey) {
|
|
@@ -93,6 +149,45 @@ export class NexusCrudBackend extends CrudBackend {
|
|
|
93
149
|
}
|
|
94
150
|
return changes;
|
|
95
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* A write answers the record it wrote, or a value that is not one. `Create` and `Update` answer the new record's
|
|
154
|
+
* **id token**, which says nothing the caller does not already hold, so it is read as nothing; a mark answers the
|
|
155
|
+
* record's new **`ERecordStatus`**, which is the one field that changed and is what puts the record in the right
|
|
156
|
+
* list without loading one. A backend that answers the whole record is read as that record.
|
|
157
|
+
*/
|
|
158
|
+
answeredRow(result) {
|
|
159
|
+
if (isRecord(result))
|
|
160
|
+
return result;
|
|
161
|
+
// A token is text and a status is a number, so the two can never be read for each other.
|
|
162
|
+
if (typeof result === "number" && Number.isInteger(result) && result >= 0)
|
|
163
|
+
return { recordStatus: result };
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
/** `Create` and `Update` answer the record's id: a token when ids are encrypted, the number as text when not. */
|
|
167
|
+
answeredKey(result) {
|
|
168
|
+
return typeof result === "string" && result !== "" ? result : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* 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
|
|
172
|
+
* deleted. A row without a `recordStatus` says nothing, and the lists it cannot answer for load again.
|
|
173
|
+
*/
|
|
174
|
+
listMembership(row, mode) {
|
|
175
|
+
const status = row.recordStatus;
|
|
176
|
+
if (typeof status !== "number" || !Number.isFinite(status))
|
|
177
|
+
return undefined;
|
|
178
|
+
switch (mode) {
|
|
179
|
+
case "normal":
|
|
180
|
+
return status < NEXUS_TRASH_STATUS;
|
|
181
|
+
case "flag":
|
|
182
|
+
return status >= NEXUS_FLAGGED_STATUS && status < NEXUS_FLAG_BAND_END;
|
|
183
|
+
case "trash":
|
|
184
|
+
return status >= NEXUS_TRASH_STATUS && status < NEXUS_DELETED_STATUS;
|
|
185
|
+
case "deleted":
|
|
186
|
+
return status >= NEXUS_DELETED_STATUS;
|
|
187
|
+
default:
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
96
191
|
/** Mass answers list the records they changed (as records, or as ids); other answers say nothing. */
|
|
97
192
|
answeredKeys(result, rowKey) {
|
|
98
193
|
const items = Array.isArray(result) ? result : isRecord(result) && Array.isArray(result.results) ? result.results : null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
2
|
import type { CrudActionName, CrudEndpoints, CrudMassActionName, CrudMode, CrudRowActionName } from "../Interfaces/CrudInterfaces.ts";
|
|
3
3
|
import type { MessageActionName } from "../Interfaces/MessageInterfaces.ts";
|
|
4
|
-
import type { TablePaging } from "../Interfaces/TableInterfaces.ts";
|
|
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
7
|
/** The built-in actions on one record, per list, in their menu order. */
|
|
@@ -30,12 +30,12 @@ export declare function actionAllowed(action: CrudActionName, api: CrudEndpoints
|
|
|
30
30
|
/** Whether a mass action can run: its row action is wanted, and its mass endpoint exists and is allowed. */
|
|
31
31
|
export declare function massActionAllowed(action: CrudMassActionName, api: CrudEndpoints, wanted: readonly CrudActionName[], can: (endpoint: ApiEndpoint) => boolean): boolean;
|
|
32
32
|
/**
|
|
33
|
-
* Whether a record leaves the list shown after an action.
|
|
34
|
-
*
|
|
33
|
+
* 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. Pinning and unpinning never do: a pinned
|
|
35
35
|
* record stays in the list it is in. `markOn` is the record's new flag.
|
|
36
36
|
*/
|
|
37
37
|
export declare function leavesList(action: CrudRowActionName | CrudMassActionName, mode: CrudMode, markOn?: boolean): boolean;
|
|
38
|
-
/** The message builder's action for a built-in action: "Guest
|
|
38
|
+
/** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
|
|
39
39
|
export declare function crudMessageAction(action: CrudRowActionName | CrudMassActionName, markOn?: boolean): MessageActionName;
|
|
40
40
|
/** What every CRUD follows, whatever module it shows. `configureCrud` changes it once, for the whole app. */
|
|
41
41
|
export interface CrudSettings {
|
|
@@ -69,8 +69,43 @@ export declare class CrudCache<T = unknown> {
|
|
|
69
69
|
set(key: string, value: T): void;
|
|
70
70
|
/** Replaces a value, keeping its age, so a changed list does not live longer than the list it came from. */
|
|
71
71
|
update(key: string, value: T): void;
|
|
72
|
+
/**
|
|
73
|
+
* Writes a change through every entry, each keeping its age: this is how a record that was saved or marked reaches
|
|
74
|
+
* the lists that are not on screen without a request. Returning `null` drops that entry, and its list loads again
|
|
75
|
+
* the next time it is shown.
|
|
76
|
+
*/
|
|
77
|
+
map(change: (key: string, value: T) => T | null): void;
|
|
72
78
|
/** Drops every entry whose key passes the test. */
|
|
73
79
|
drop(test: (key: string) => boolean): void;
|
|
74
80
|
clear(): void;
|
|
75
81
|
get size(): number;
|
|
76
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A record a change touched, as the lists kept in the cache are patched from it: how it is now, how it was, and
|
|
85
|
+
* whether it is in any list at all any more.
|
|
86
|
+
*/
|
|
87
|
+
export interface CrudRowChange<TRow extends TableRow = TableRow> {
|
|
88
|
+
/** The record's key, as the table reads it. */
|
|
89
|
+
key: string;
|
|
90
|
+
/** The record as it is now. */
|
|
91
|
+
row: TRow;
|
|
92
|
+
/** The record as it was, when the page had it. Without it, a list that holds the record counts as having held it. */
|
|
93
|
+
before: TRow | undefined;
|
|
94
|
+
/** The record is in no list any more, whatever it says: it is taken out of every list kept. */
|
|
95
|
+
gone?: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** The list a cache entry belongs to: the key itself for a whole list, its first part for one page of one. */
|
|
98
|
+
export declare function crudCacheMode(key: string): CrudMode;
|
|
99
|
+
/**
|
|
100
|
+
* One list kept in the cache, after a change: the records that belong in it are written in, the ones that no longer
|
|
101
|
+
* belong are taken out, and the entry is returned as it should now be. `null` means the list cannot be worked out
|
|
102
|
+
* and has to be loaded again.
|
|
103
|
+
*
|
|
104
|
+
* `member` is the backend's rule for which list a record belongs in (`CrudBackend.listMembership`). A record it
|
|
105
|
+
* cannot answer for drops the entry, which is what every list did before this.
|
|
106
|
+
*
|
|
107
|
+
* **A whole list** is patched: a record that joined it goes on top, where the table also shows a record just added.
|
|
108
|
+
* **One page of a list** is not: its order, its last row and its counts are the server's, so as soon as a record
|
|
109
|
+
* joined it or left it the page is dropped; a record that stayed is written in place, which costs nothing.
|
|
110
|
+
*/
|
|
111
|
+
export declare function patchCachedList<TRow extends TableRow>(value: TablePage<TRow> | TRow[], mode: CrudMode, changes: readonly CrudRowChange<TRow>[], keyOf: (row: TRow) => string, member: (row: TRow, mode: CrudMode) => boolean | undefined): TablePage<TRow> | TRow[] | null;
|
|
@@ -1,17 +1,17 @@
|
|
|
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
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
|
|
4
|
+
* and the cache of lists kept while the page is open - including how a change is written into the lists kept, so that
|
|
5
|
+
* a record saved or marked costs no request on any list.
|
|
5
6
|
*/
|
|
6
7
|
/** Every list, in the order the CRUD shows them. */
|
|
7
|
-
export const CRUD_MODES = ["normal", "flag", "
|
|
8
|
-
const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "
|
|
9
|
-
const ACTIVE_MASS_ACTIONS = ["flag", "pin", "
|
|
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"];
|
|
10
11
|
/** The built-in actions on one record, per list, in their menu order. */
|
|
11
12
|
export const CRUD_ROW_ACTIONS = {
|
|
12
13
|
normal: ACTIVE_ROW_ACTIONS,
|
|
13
14
|
flag: ACTIVE_ROW_ACTIONS,
|
|
14
|
-
archive: ["view", "restore", "trash"],
|
|
15
15
|
trash: ["view", "recover", "delete"],
|
|
16
16
|
deleted: ["view", "trashFromDelete"],
|
|
17
17
|
};
|
|
@@ -19,7 +19,6 @@ export const CRUD_ROW_ACTIONS = {
|
|
|
19
19
|
export const CRUD_MASS_ACTIONS = {
|
|
20
20
|
normal: ACTIVE_MASS_ACTIONS,
|
|
21
21
|
flag: ACTIVE_MASS_ACTIONS,
|
|
22
|
-
archive: ["restore", "description", "trash"],
|
|
23
22
|
trash: ["recover", "description", "delete"],
|
|
24
23
|
deleted: ["trashFromDelete"],
|
|
25
24
|
};
|
|
@@ -96,14 +95,12 @@ export function massActionAllowed(action, api, wanted, can) {
|
|
|
96
95
|
return Boolean(endpoint) && can(endpoint);
|
|
97
96
|
}
|
|
98
97
|
/**
|
|
99
|
-
* Whether a record leaves the list shown after an action.
|
|
100
|
-
*
|
|
98
|
+
* 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. Pinning and unpinning never do: a pinned
|
|
101
100
|
* record stays in the list it is in. `markOn` is the record's new flag.
|
|
102
101
|
*/
|
|
103
102
|
export function leavesList(action, mode, markOn) {
|
|
104
103
|
switch (action) {
|
|
105
|
-
case "archive":
|
|
106
|
-
case "restore":
|
|
107
104
|
case "trash":
|
|
108
105
|
case "recover":
|
|
109
106
|
case "delete":
|
|
@@ -115,7 +112,7 @@ export function leavesList(action, mode, markOn) {
|
|
|
115
112
|
return false;
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
|
-
/** The message builder's action for a built-in action: "Guest
|
|
115
|
+
/** The message builder's action for a built-in action: "Guest recovered.", "Unpin 3 guests?" */
|
|
119
116
|
export function crudMessageAction(action, markOn) {
|
|
120
117
|
switch (action) {
|
|
121
118
|
case "flag":
|
|
@@ -187,6 +184,20 @@ export class CrudCache {
|
|
|
187
184
|
if (entry)
|
|
188
185
|
entry.value = value;
|
|
189
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Writes a change through every entry, each keeping its age: this is how a record that was saved or marked reaches
|
|
189
|
+
* the lists that are not on screen without a request. Returning `null` drops that entry, and its list loads again
|
|
190
|
+
* the next time it is shown.
|
|
191
|
+
*/
|
|
192
|
+
map(change) {
|
|
193
|
+
for (const [key, entry] of [...this.entries]) {
|
|
194
|
+
const next = change(key, entry.value);
|
|
195
|
+
if (next === null)
|
|
196
|
+
this.entries.delete(key);
|
|
197
|
+
else
|
|
198
|
+
entry.value = next;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
190
201
|
/** Drops every entry whose key passes the test. */
|
|
191
202
|
drop(test) {
|
|
192
203
|
for (const key of [...this.entries.keys()])
|
|
@@ -200,3 +211,57 @@ export class CrudCache {
|
|
|
200
211
|
return this.entries.size;
|
|
201
212
|
}
|
|
202
213
|
}
|
|
214
|
+
/** The list a cache entry belongs to: the key itself for a whole list, its first part for one page of one. */
|
|
215
|
+
export function crudCacheMode(key) {
|
|
216
|
+
const bar = key.indexOf("|");
|
|
217
|
+
return (bar === -1 ? key : key.slice(0, bar));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* One list kept in the cache, after a change: the records that belong in it are written in, the ones that no longer
|
|
221
|
+
* belong are taken out, and the entry is returned as it should now be. `null` means the list cannot be worked out
|
|
222
|
+
* and has to be loaded again.
|
|
223
|
+
*
|
|
224
|
+
* `member` is the backend's rule for which list a record belongs in (`CrudBackend.listMembership`). A record it
|
|
225
|
+
* cannot answer for drops the entry, which is what every list did before this.
|
|
226
|
+
*
|
|
227
|
+
* **A whole list** is patched: a record that joined it goes on top, where the table also shows a record just added.
|
|
228
|
+
* **One page of a list** is not: its order, its last row and its counts are the server's, so as soon as a record
|
|
229
|
+
* joined it or left it the page is dropped; a record that stayed is written in place, which costs nothing.
|
|
230
|
+
*/
|
|
231
|
+
export function patchCachedList(value, mode, changes, keyOf, member) {
|
|
232
|
+
const paged = !Array.isArray(value);
|
|
233
|
+
const rows = paged ? [...value.rows] : [...value];
|
|
234
|
+
let touched = false;
|
|
235
|
+
for (const change of changes) {
|
|
236
|
+
const now = change.gone ? false : member(change.row, mode);
|
|
237
|
+
if (now === undefined)
|
|
238
|
+
return null;
|
|
239
|
+
const at = rows.findIndex(row => keyOf(row) === change.key);
|
|
240
|
+
const was = change.before ? member(change.before, mode) : at !== -1;
|
|
241
|
+
if (was === undefined)
|
|
242
|
+
return null;
|
|
243
|
+
if (paged) {
|
|
244
|
+
if (was !== now)
|
|
245
|
+
return null;
|
|
246
|
+
if (at !== -1) {
|
|
247
|
+
rows[at] = change.row;
|
|
248
|
+
touched = true;
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (now) {
|
|
253
|
+
if (at === -1)
|
|
254
|
+
rows.unshift(change.row);
|
|
255
|
+
else
|
|
256
|
+
rows[at] = change.row;
|
|
257
|
+
touched = true;
|
|
258
|
+
}
|
|
259
|
+
else if (at !== -1) {
|
|
260
|
+
rows.splice(at, 1);
|
|
261
|
+
touched = true;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (!touched)
|
|
265
|
+
return value;
|
|
266
|
+
return paged ? { ...value, rows } : rows;
|
|
267
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ApiModuleValue, ApiResult, HttpMethod } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { ExcelDownload, ExcelEndpoints, ExcelExportQuery, ExcelImportAnswer, ExcelSettings, ExcelSheetRow, ExcelVerifyAnswer } from "../Interfaces/ExcelInterfaces.ts";
|
|
3
|
+
/** The actions of a Nexus Excel controller, as the backend names them (`NexusExcelController`). */
|
|
4
|
+
export declare const EXCEL_ACTIONS: {
|
|
5
|
+
readonly settings: "Excel/Settings";
|
|
6
|
+
readonly template: "Excel/Template";
|
|
7
|
+
readonly verify: "Excel/Verify";
|
|
8
|
+
readonly import: "Excel/Import";
|
|
9
|
+
/** The model's own action, which the two controller bases leave to it. */
|
|
10
|
+
readonly export: "Excel/Export";
|
|
11
|
+
};
|
|
12
|
+
/** Which controller a model's spreadsheets sit on. An `apiController(...)` fits as it is. */
|
|
13
|
+
export interface ExcelController<TModule extends ApiModuleValue = ApiModuleValue> {
|
|
14
|
+
module: TModule;
|
|
15
|
+
/** The controller's name: "City". */
|
|
16
|
+
controller: string;
|
|
17
|
+
}
|
|
18
|
+
/** Changes to the endpoints a Nexus controller gets. */
|
|
19
|
+
export interface ExcelEndpointOptions {
|
|
20
|
+
/** What one row is, for the messages: "city". */
|
|
21
|
+
subject?: string;
|
|
22
|
+
/** No export is offered. Default: `Excel/Export` as a POST. */
|
|
23
|
+
exportable?: false;
|
|
24
|
+
/** The export action, when the controller named it something else: `"Export"`, or `["GET", "Download"]`. */
|
|
25
|
+
exportAction?: string | [HttpMethod, string];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The spreadsheet endpoints of a Nexus controller, named as the backend names them: `Excel/Settings`,
|
|
29
|
+
* `Excel/Template`, `Excel/Verify`, `Excel/Import`, and the model's own `Excel/Export`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* export const CITIES = MODULES.controller(NexusModule.Configuration, "City", "city");
|
|
33
|
+
* export const CITY_SHEET = excelEndpoints(CITIES, { subject: "city" });
|
|
34
|
+
*/
|
|
35
|
+
export declare function excelEndpoints<TModule extends ApiModuleValue>(controller: ExcelController<TModule>, options?: ExcelEndpointOptions): ExcelEndpoints<TModule>;
|
|
36
|
+
/** What a download's own headers are called, so a backend that names them differently still reads. */
|
|
37
|
+
export interface ExcelDownloadHeaders {
|
|
38
|
+
rows: string;
|
|
39
|
+
skipped: string;
|
|
40
|
+
limit: string;
|
|
41
|
+
note: string;
|
|
42
|
+
}
|
|
43
|
+
export declare abstract class ExcelBackend {
|
|
44
|
+
/** The form field an upload is sent under. */
|
|
45
|
+
abstract get uploadField(): string;
|
|
46
|
+
/** What an import sends: the rows the verify answered with, as the person left them. */
|
|
47
|
+
abstract importRequest(rows: readonly ExcelSheetRow[]): unknown;
|
|
48
|
+
/** What an export sends: the search, the sort and the filters the person has on screen. */
|
|
49
|
+
abstract exportRequest(query: ExcelExportQuery): unknown;
|
|
50
|
+
/** Reads what the settings endpoint answered. */
|
|
51
|
+
abstract readSettings(result: unknown): ExcelSettings;
|
|
52
|
+
/** Reads what a verify answered. */
|
|
53
|
+
abstract readVerify(result: unknown): ExcelVerifyAnswer;
|
|
54
|
+
/** Reads what an import answered. */
|
|
55
|
+
abstract readImport(result: unknown): ExcelImportAnswer;
|
|
56
|
+
/**
|
|
57
|
+
* What a download says about itself in its headers. Every one of them has to be named in the backend's CORS policy:
|
|
58
|
+
* a browser can read only the headers it is told it may, `Content-Disposition` included, so a frontend on another
|
|
59
|
+
* origin otherwise receives a file it cannot even name.
|
|
60
|
+
*/
|
|
61
|
+
abstract get downloadHeaders(): ExcelDownloadHeaders;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The Nexus backend. The four calls sit under one controller; an import sends `{ rows }`; an export sends the list
|
|
65
|
+
* request a Nexus pagination takes, without its paging, because an export is not a page; a download says what it came
|
|
66
|
+
* to in `X-Nexus-Export-*` headers.
|
|
67
|
+
*/
|
|
68
|
+
export declare class NexusExcelBackend extends ExcelBackend {
|
|
69
|
+
get uploadField(): string;
|
|
70
|
+
importRequest(rows: readonly ExcelSheetRow[]): unknown;
|
|
71
|
+
/**
|
|
72
|
+
* The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
|
|
73
|
+
* leaves, up to the backend's own limit, not the page the person happens to be on. With nothing sorted it follows
|
|
74
|
+
* the list's own order, newest first.
|
|
75
|
+
*/
|
|
76
|
+
exportRequest(query: ExcelExportQuery): unknown;
|
|
77
|
+
readSettings(result: unknown): ExcelSettings;
|
|
78
|
+
readVerify(result: unknown): ExcelVerifyAnswer;
|
|
79
|
+
readImport(result: unknown): ExcelImportAnswer;
|
|
80
|
+
get downloadHeaders(): ExcelDownloadHeaders;
|
|
81
|
+
}
|
|
82
|
+
/** The most rows one export writes when the backend does not say (`Nexus:Excel:MaxExportRows`). */
|
|
83
|
+
export declare const DEFAULT_MAX_EXPORT_ROWS = 5000;
|
|
84
|
+
/** The most rows one import takes when the backend does not say (`Nexus:Excel:MaxImportRows`). */
|
|
85
|
+
export declare const DEFAULT_MAX_IMPORT_ROWS = 2000;
|
|
86
|
+
/** What a Nexus download says about itself (`Excels/ExcelHeaders.cs`), all of them exposed by the CORS policy. */
|
|
87
|
+
export declare const NEXUS_DOWNLOAD_HEADERS: ExcelDownloadHeaders;
|
|
88
|
+
/**
|
|
89
|
+
* Reads an answer whose problems are in `errorMessages`, which is how the backend answers a refusal now: nothing at the
|
|
90
|
+
* top level, and one entry per wrong field (`{ message, messageCode, errorCode, inputName }`) so every one of them is
|
|
91
|
+
* told at once. The first entry words the answer, and each entry that names an input lands on that input.
|
|
92
|
+
*
|
|
93
|
+
* Anything else - a success, a backend of another shape, problem details, a proxy answering while the service is down -
|
|
94
|
+
* goes to `readApiResponse`, which is also the default for every call that does not name a reader. It is used here
|
|
95
|
+
* because a spreadsheet's refusals are the whole point of the review: "no row could be written" has to reach the person
|
|
96
|
+
* in the backend's own words, not as a generic sentence.
|
|
97
|
+
*/
|
|
98
|
+
export declare function readExcelResponse(body: unknown, status?: number): ApiResult;
|
|
99
|
+
/** Registers the app's spreadsheet conventions, once, beside its API modules. */
|
|
100
|
+
export declare function defineExcelBackend<T extends ExcelBackend>(backend: T): T;
|
|
101
|
+
/** The registered spreadsheet conventions, or the Nexus ones. */
|
|
102
|
+
export declare function getExcelBackend(): ExcelBackend;
|
|
103
|
+
/** The file name out of a `Content-Disposition`, `filename*` first (it carries the encoding) and `filename` after. */
|
|
104
|
+
export declare function readFileName(disposition: string | null | undefined): string | undefined;
|
|
105
|
+
/** What a download answered, as its own headers tell it. */
|
|
106
|
+
export declare function readDownloadHeaders(headers: Headers, blob: Blob, fallbackName: string): ExcelDownload;
|