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.
Files changed (116) hide show
  1. package/CHANGELOG.md +262 -0
  2. package/README.md +122 -25
  3. package/dist/Client.Index.d.ts +11 -0
  4. package/dist/Client.Index.js +11 -0
  5. package/dist/Components/Chats/Chat.d.ts +28 -0
  6. package/dist/Components/Chats/Chat.js +19 -0
  7. package/dist/Components/Chats/ChatButton.d.ts +26 -0
  8. package/dist/Components/Chats/ChatButton.js +41 -0
  9. package/dist/Components/Chats/ChatComposer.d.ts +41 -0
  10. package/dist/Components/Chats/ChatComposer.js +177 -0
  11. package/dist/Components/Chats/ChatConversations.d.ts +35 -0
  12. package/dist/Components/Chats/ChatConversations.js +38 -0
  13. package/dist/Components/Chats/ChatPanel.d.ts +66 -0
  14. package/dist/Components/Chats/ChatPanel.js +94 -0
  15. package/dist/Components/Chats/ChatParts.d.ts +87 -0
  16. package/dist/Components/Chats/ChatParts.js +100 -0
  17. package/dist/Components/Chats/ChatThread.d.ts +22 -0
  18. package/dist/Components/Chats/ChatThread.js +96 -0
  19. package/dist/Components/Documents/Menu.js +24 -20
  20. package/dist/Components/Documents/SplitButton.js +5 -3
  21. package/dist/Components/Documents/TabButtons.d.ts +24 -6
  22. package/dist/Components/Documents/TabButtons.js +23 -4
  23. package/dist/Components/Forms/ApiForm.d.ts +6 -4
  24. package/dist/Components/Forms/ApiForm.js +15 -14
  25. package/dist/Components/Forms/Crud.js +202 -50
  26. package/dist/Components/Forms/ExcelImport.d.ts +42 -0
  27. package/dist/Components/Forms/ExcelImport.js +190 -0
  28. package/dist/Components/Forms/Form.js +5 -1
  29. package/dist/Components/Inputs/DateTimePicker.js +2 -1
  30. package/dist/Components/Inputs/GroupForm.js +4 -2
  31. package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
  32. package/dist/Components/Inputs/InputRenderer.js +1 -1
  33. package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
  34. package/dist/Components/Inputs/RowsInput.d.ts +12 -1
  35. package/dist/Components/Inputs/RowsInput.js +53 -4
  36. package/dist/Components/Inputs/TabularForm.d.ts +7 -3
  37. package/dist/Components/Inputs/TabularForm.js +85 -20
  38. package/dist/Components/Inputs/TimePicker.js +2 -1
  39. package/dist/Components/Layouts/ThemeSwitcher.d.ts +2 -2
  40. package/dist/Components/Layouts/ThemeSwitcher.js +35 -15
  41. package/dist/Components/Viewers/DataTable.js +88 -34
  42. package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
  43. package/dist/Components/Viewers/DataTableColumns.js +243 -0
  44. package/dist/Components/Viewers/DataTableParts.d.ts +6 -2
  45. package/dist/Components/Viewers/DataTableParts.js +21 -9
  46. package/dist/Helpers/ApiClient.d.ts +22 -0
  47. package/dist/Helpers/ApiClient.js +4 -1
  48. package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
  49. package/dist/Helpers/ApiFormHelpers.js +73 -9
  50. package/dist/Helpers/ApiModules.d.ts +0 -4
  51. package/dist/Helpers/ApiModules.js +1 -0
  52. package/dist/Helpers/ApiResponses.d.ts +14 -4
  53. package/dist/Helpers/ApiResponses.js +116 -25
  54. package/dist/Helpers/ApiRoutes.d.ts +0 -10
  55. package/dist/Helpers/ApiRoutes.js +2 -10
  56. package/dist/Helpers/ChatBackend.d.ts +134 -0
  57. package/dist/Helpers/ChatBackend.js +332 -0
  58. package/dist/Helpers/ChatHelpers.d.ts +189 -0
  59. package/dist/Helpers/ChatHelpers.js +486 -0
  60. package/dist/Helpers/ChatHooks.d.ts +81 -0
  61. package/dist/Helpers/ChatHooks.js +175 -0
  62. package/dist/Helpers/ChatStore.d.ts +132 -0
  63. package/dist/Helpers/ChatStore.js +394 -0
  64. package/dist/Helpers/ChatSummaries.d.ts +39 -0
  65. package/dist/Helpers/ChatSummaries.js +118 -0
  66. package/dist/Helpers/CrudBackend.d.ts +47 -5
  67. package/dist/Helpers/CrudBackend.js +99 -4
  68. package/dist/Helpers/CrudHelpers.d.ts +39 -4
  69. package/dist/Helpers/CrudHelpers.js +76 -11
  70. package/dist/Helpers/ExcelBackend.d.ts +106 -0
  71. package/dist/Helpers/ExcelBackend.js +290 -0
  72. package/dist/Helpers/ExcelHelpers.d.ts +98 -0
  73. package/dist/Helpers/ExcelHelpers.js +302 -0
  74. package/dist/Helpers/FormStore.d.ts +2 -0
  75. package/dist/Helpers/FormStore.js +19 -1
  76. package/dist/Helpers/MessageBuilder.js +0 -2
  77. package/dist/Helpers/PopoverHelpers.d.ts +31 -6
  78. package/dist/Helpers/PopoverHelpers.js +116 -12
  79. package/dist/Helpers/RowsStore.d.ts +7 -1
  80. package/dist/Helpers/RowsStore.js +22 -0
  81. package/dist/Helpers/TableColumns.d.ts +7 -0
  82. package/dist/Helpers/TableColumns.js +41 -13
  83. package/dist/Helpers/TableHelpers.d.ts +9 -1
  84. package/dist/Helpers/TableHelpers.js +20 -0
  85. package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
  86. package/dist/Interfaces/ApiInterfaces.js +16 -10
  87. package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
  88. package/dist/Interfaces/ChatInterfaces.js +1 -0
  89. package/dist/Interfaces/CrudInterfaces.d.ts +73 -12
  90. package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
  91. package/dist/Interfaces/ExcelInterfaces.js +45 -0
  92. package/dist/Interfaces/FormInterfaces.d.ts +52 -2
  93. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  94. package/dist/Interfaces/TableInterfaces.d.ts +62 -2
  95. package/dist/Services/BrowserApi.d.ts +18 -1
  96. package/dist/Services/BrowserApi.js +18 -0
  97. package/dist/Services/ChatApi.d.ts +37 -0
  98. package/dist/Services/ChatApi.js +98 -0
  99. package/dist/Services/ExcelApi.d.ts +46 -0
  100. package/dist/Services/ExcelApi.js +196 -0
  101. package/dist/Services/ServerApi.d.ts +6 -1
  102. package/dist/Services/ServerApi.js +3 -0
  103. package/dist/Shared.Index.d.ts +8 -0
  104. package/dist/Shared.Index.js +8 -0
  105. package/package.json +6 -3
  106. package/src/Styles/Nexus.Button.css +2 -0
  107. package/src/Styles/Nexus.Chat.css +1489 -0
  108. package/src/Styles/Nexus.Crud.css +77 -0
  109. package/src/Styles/Nexus.Excel.css +370 -0
  110. package/src/Styles/Nexus.Form.css +23 -0
  111. package/src/Styles/Nexus.Index.css +2 -0
  112. package/src/Styles/Nexus.Menu.css +12 -2
  113. package/src/Styles/Nexus.Rows.css +78 -48
  114. package/src/Styles/Nexus.Tab.Buttons.css +55 -0
  115. package/src/Styles/Nexus.Table.css +286 -4
  116. 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
- /** The Nexus record status of a flagged record (`ERecordStatus.Flagged`). */
29
- export const NEXUS_FLAGGED_STATUS = 15;
30
- /** The Nexus record status of an active record (`ERecordStatus.Active`). */
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` 15, a pinned one `isPinned`.
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. Archive, trash, restore, recover, delete, and moving back to
34
- * the trash take it to another list, and unflagging leaves the flagged list. Pinning and unpinning never do: a pinned
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 archived.", "Unpin 3 guests?" */
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", "archive", "trash", "deleted"];
8
- const ACTIVE_ROW_ACTIONS = ["view", "edit", "flag", "pin", "archive", "trash"];
9
- const ACTIVE_MASS_ACTIONS = ["flag", "pin", "archive", "description", "trash"];
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. Archive, trash, restore, recover, delete, and moving back to
100
- * the trash take it to another list, and unflagging leaves the flagged list. Pinning and unpinning never do: a pinned
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 archived.", "Unpin 3 guests?" */
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;