nexus-shared 2.0.0 → 3.1.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 (176) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -21
  3. package/README.md +1084 -903
  4. package/dist/Client.Index.d.ts +14 -0
  5. package/dist/Client.Index.js +14 -0
  6. package/dist/Components/Chats/Chat.d.ts +28 -0
  7. package/dist/Components/Chats/Chat.js +19 -0
  8. package/dist/Components/Chats/ChatButton.d.ts +26 -0
  9. package/dist/Components/Chats/ChatButton.js +41 -0
  10. package/dist/Components/Chats/ChatComposer.d.ts +41 -0
  11. package/dist/Components/Chats/ChatComposer.js +177 -0
  12. package/dist/Components/Chats/ChatConversations.d.ts +35 -0
  13. package/dist/Components/Chats/ChatConversations.js +38 -0
  14. package/dist/Components/Chats/ChatPanel.d.ts +66 -0
  15. package/dist/Components/Chats/ChatPanel.js +94 -0
  16. package/dist/Components/Chats/ChatParts.d.ts +87 -0
  17. package/dist/Components/Chats/ChatParts.js +96 -0
  18. package/dist/Components/Chats/ChatThread.d.ts +22 -0
  19. package/dist/Components/Chats/ChatThread.js +96 -0
  20. package/dist/Components/Documents/Button.d.ts +6 -1
  21. package/dist/Components/Documents/Button.js +2 -2
  22. package/dist/Components/Documents/Menu.js +44 -28
  23. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  24. package/dist/Components/Documents/SplitButton.js +12 -8
  25. package/dist/Components/Documents/TabButtons.d.ts +29 -8
  26. package/dist/Components/Documents/TabButtons.js +24 -5
  27. package/dist/Components/Forms/ApiForm.d.ts +6 -4
  28. package/dist/Components/Forms/ApiForm.js +15 -14
  29. package/dist/Components/Forms/Crud.js +342 -71
  30. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  31. package/dist/Components/Forms/CrudParts.js +37 -5
  32. package/dist/Components/Forms/ExcelImport.d.ts +58 -0
  33. package/dist/Components/Forms/ExcelImport.js +319 -0
  34. package/dist/Components/Forms/Form.js +9 -13
  35. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  36. package/dist/Components/Forms/SubmitForm.js +23 -12
  37. package/dist/Components/Inputs/DateTimePicker.js +2 -1
  38. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  39. package/dist/Components/Inputs/DropdownParts.js +4 -6
  40. package/dist/Components/Inputs/GroupForm.js +4 -2
  41. package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
  42. package/dist/Components/Inputs/InputRenderer.js +1 -1
  43. package/dist/Components/Inputs/NumberBox.js +24 -2
  44. package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
  45. package/dist/Components/Inputs/RowsInput.d.ts +12 -1
  46. package/dist/Components/Inputs/RowsInput.js +55 -6
  47. package/dist/Components/Inputs/Slider.js +4 -1
  48. package/dist/Components/Inputs/TabularForm.d.ts +7 -3
  49. package/dist/Components/Inputs/TabularForm.js +117 -20
  50. package/dist/Components/Inputs/TimePanel.js +4 -3
  51. package/dist/Components/Inputs/TimePicker.js +2 -1
  52. package/dist/Components/Layouts/Popup.js +1 -4
  53. package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
  54. package/dist/Components/Layouts/ThemePicker.js +77 -6
  55. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  56. package/dist/Components/Layouts/ThemeScript.js +4 -2
  57. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  58. package/dist/Components/Layouts/ThemeSwitcher.js +39 -18
  59. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  60. package/dist/Components/Viewers/Avatar.js +21 -0
  61. package/dist/Components/Viewers/DataTable.js +262 -60
  62. package/dist/Components/Viewers/DataTableColumns.d.ts +32 -0
  63. package/dist/Components/Viewers/DataTableColumns.js +200 -0
  64. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  65. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  66. package/dist/Components/Viewers/DataTableParts.d.ts +8 -4
  67. package/dist/Components/Viewers/DataTableParts.js +24 -12
  68. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  69. package/dist/Helpers/AnimationHelpers.js +20 -2
  70. package/dist/Helpers/ApiClient.d.ts +22 -0
  71. package/dist/Helpers/ApiClient.js +4 -1
  72. package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
  73. package/dist/Helpers/ApiFormHelpers.js +73 -9
  74. package/dist/Helpers/ApiModules.d.ts +0 -4
  75. package/dist/Helpers/ApiModules.js +1 -0
  76. package/dist/Helpers/ApiResponses.d.ts +14 -4
  77. package/dist/Helpers/ApiResponses.js +116 -25
  78. package/dist/Helpers/ApiRoutes.d.ts +0 -10
  79. package/dist/Helpers/ApiRoutes.js +2 -10
  80. package/dist/Helpers/ChatBackend.d.ts +134 -0
  81. package/dist/Helpers/ChatBackend.js +332 -0
  82. package/dist/Helpers/ChatHelpers.d.ts +189 -0
  83. package/dist/Helpers/ChatHelpers.js +486 -0
  84. package/dist/Helpers/ChatHooks.d.ts +81 -0
  85. package/dist/Helpers/ChatHooks.js +175 -0
  86. package/dist/Helpers/ChatStore.d.ts +132 -0
  87. package/dist/Helpers/ChatStore.js +394 -0
  88. package/dist/Helpers/ChatSummaries.d.ts +39 -0
  89. package/dist/Helpers/ChatSummaries.js +118 -0
  90. package/dist/Helpers/CrudBackend.d.ts +54 -9
  91. package/dist/Helpers/CrudBackend.js +103 -6
  92. package/dist/Helpers/CrudHelpers.d.ts +39 -4
  93. package/dist/Helpers/CrudHelpers.js +76 -11
  94. package/dist/Helpers/ExcelBackend.d.ts +106 -0
  95. package/dist/Helpers/ExcelBackend.js +296 -0
  96. package/dist/Helpers/ExcelHelpers.d.ts +126 -0
  97. package/dist/Helpers/ExcelHelpers.js +344 -0
  98. package/dist/Helpers/FormStore.d.ts +11 -5
  99. package/dist/Helpers/FormStore.js +44 -6
  100. package/dist/Helpers/MessageBuilder.js +1 -2
  101. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  102. package/dist/Helpers/NumberHelpers.js +41 -3
  103. package/dist/Helpers/PopoverHelpers.d.ts +46 -9
  104. package/dist/Helpers/PopoverHelpers.js +163 -24
  105. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  106. package/dist/Helpers/PopoverHooks.js +62 -0
  107. package/dist/Helpers/RowsStore.d.ts +14 -3
  108. package/dist/Helpers/RowsStore.js +37 -3
  109. package/dist/Helpers/TableColumns.d.ts +7 -0
  110. package/dist/Helpers/TableColumns.js +41 -13
  111. package/dist/Helpers/TableExport.js +12 -12
  112. package/dist/Helpers/TableFilters.d.ts +25 -0
  113. package/dist/Helpers/TableFilters.js +162 -0
  114. package/dist/Helpers/TableHelpers.d.ts +15 -4
  115. package/dist/Helpers/TableHelpers.js +28 -4
  116. package/dist/Helpers/TableStore.d.ts +11 -3
  117. package/dist/Helpers/TableStore.js +16 -5
  118. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  119. package/dist/Helpers/ThemeHelpers.js +46 -9
  120. package/dist/Helpers/UserPreferences.d.ts +14 -0
  121. package/dist/Helpers/UserPreferences.js +99 -0
  122. package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
  123. package/dist/Interfaces/ApiInterfaces.js +16 -10
  124. package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
  125. package/dist/Interfaces/ChatInterfaces.js +1 -0
  126. package/dist/Interfaces/CrudInterfaces.d.ts +92 -12
  127. package/dist/Interfaces/ExcelInterfaces.d.ts +227 -0
  128. package/dist/Interfaces/ExcelInterfaces.js +45 -0
  129. package/dist/Interfaces/FormInterfaces.d.ts +85 -7
  130. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  131. package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
  132. package/dist/Interfaces/PreferenceInterfaces.js +28 -0
  133. package/dist/Interfaces/TableInterfaces.d.ts +116 -9
  134. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  135. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  136. package/dist/Services/BrowserApi.d.ts +18 -1
  137. package/dist/Services/BrowserApi.js +18 -0
  138. package/dist/Services/ChatApi.d.ts +37 -0
  139. package/dist/Services/ChatApi.js +98 -0
  140. package/dist/Services/ExcelApi.d.ts +48 -0
  141. package/dist/Services/ExcelApi.js +196 -0
  142. package/dist/Services/ServerApi.d.ts +6 -1
  143. package/dist/Services/ServerApi.js +3 -0
  144. package/dist/Services/ThemeService.d.ts +18 -2
  145. package/dist/Services/ThemeService.js +80 -16
  146. package/dist/Shared.Index.d.ts +10 -0
  147. package/dist/Shared.Index.js +10 -0
  148. package/package.json +85 -82
  149. package/src/Styles/Nexus.Avatar.css +79 -0
  150. package/src/Styles/Nexus.Base.css +117 -70
  151. package/src/Styles/Nexus.Button.css +180 -151
  152. package/src/Styles/Nexus.Calendar.css +273 -275
  153. package/src/Styles/Nexus.Chat.css +1443 -0
  154. package/src/Styles/Nexus.Choice.css +343 -345
  155. package/src/Styles/Nexus.Crud.css +168 -84
  156. package/src/Styles/Nexus.DateTime.css +81 -81
  157. package/src/Styles/Nexus.Dropdown.css +725 -774
  158. package/src/Styles/Nexus.Editor.css +295 -297
  159. package/src/Styles/Nexus.Excel.css +389 -0
  160. package/src/Styles/Nexus.Form.css +279 -298
  161. package/src/Styles/Nexus.Index.css +37 -34
  162. package/src/Styles/Nexus.Input.css +602 -637
  163. package/src/Styles/Nexus.Markdown.css +237 -265
  164. package/src/Styles/Nexus.Menu.css +237 -249
  165. package/src/Styles/Nexus.Popup.css +724 -731
  166. package/src/Styles/Nexus.RichText.css +2 -32
  167. package/src/Styles/Nexus.Rows.css +637 -566
  168. package/src/Styles/Nexus.Slider.css +276 -278
  169. package/src/Styles/Nexus.Tab.Buttons.css +432 -364
  170. package/src/Styles/Nexus.Table.css +1386 -1014
  171. package/src/Styles/Nexus.Theme.Picker.css +286 -159
  172. package/src/Styles/Nexus.Themes.css +194 -194
  173. package/src/Styles/Nexus.Time.css +144 -147
  174. package/src/Styles/Nexus.Toast.css +263 -265
  175. package/src/Styles/Nexus.Tokens.css +89 -72
  176. package/src/Styles/Nexus.Tree.css +431 -435
@@ -0,0 +1,296 @@
1
+ import { readApiResponse } from "./ApiResponses.js";
2
+ import { ExcelRowKind } from "../Interfaces/ExcelInterfaces.js";
3
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
+ const text = (value) => {
5
+ if (value === undefined || value === null)
6
+ return null;
7
+ const written = String(value).trim();
8
+ return written.length ? written : null;
9
+ };
10
+ const count = (value, fallback = 0) => {
11
+ const number = Number(value);
12
+ return Number.isFinite(number) ? number : fallback;
13
+ };
14
+ /** The actions of a Nexus Excel controller, as the backend names them (`NexusExcelController`). */
15
+ export const EXCEL_ACTIONS = {
16
+ settings: "Excel/Settings",
17
+ template: "Excel/Template",
18
+ verify: "Excel/Verify",
19
+ import: "Excel/Import",
20
+ /** The model's own action, which the two controller bases leave to it. */
21
+ export: "Excel/Export",
22
+ };
23
+ const at = (controller, action) => `${controller}/${action}`;
24
+ /**
25
+ * The spreadsheet endpoints of a Nexus controller, named as the backend names them: `Excel/Settings`,
26
+ * `Excel/Template`, `Excel/Verify`, `Excel/Import`, and the model's own `Excel/Export`.
27
+ *
28
+ * @example
29
+ * export const CITIES = MODULES.controller(NexusModule.Configuration, "City", "city");
30
+ * export const CITY_SHEET = excelEndpoints(CITIES, { subject: "city" });
31
+ */
32
+ export function excelEndpoints(controller, options = {}) {
33
+ const { module, controller: name } = controller;
34
+ const subject = options.subject;
35
+ const endpoint = (method, action, extras) => ({ module, path: at(name, action), method, ...(subject && { subject }), ...extras });
36
+ const [exportMethod, exportAction] = Array.isArray(options.exportAction) ? options.exportAction : ["POST", options.exportAction ?? EXCEL_ACTIONS.export];
37
+ return {
38
+ settings: endpoint("GET", EXCEL_ACTIONS.settings, { action: "load" }),
39
+ template: endpoint("GET", EXCEL_ACTIONS.template, { action: "download" }),
40
+ verify: endpoint("POST", EXCEL_ACTIONS.verify, { action: "upload", bodyType: "form-data" }),
41
+ import: endpoint("POST", EXCEL_ACTIONS.import, { action: "import" }),
42
+ ...(options.exportable === false ? {} : { export: endpoint(exportMethod, exportAction, { action: "export" }) }),
43
+ };
44
+ }
45
+ export class ExcelBackend {
46
+ }
47
+ /**
48
+ * The Nexus backend. The four calls sit under one controller; an import sends `{ rows }`; an export sends the list
49
+ * request a Nexus pagination takes, without its paging, because an export is not a page; a download says what it came
50
+ * to in `X-Nexus-Export-*` headers.
51
+ */
52
+ export class NexusExcelBackend extends ExcelBackend {
53
+ get uploadField() {
54
+ return "file";
55
+ }
56
+ importRequest(rows, recordStatus) {
57
+ return recordStatus === undefined ? { rows } : { rows, recordStatus };
58
+ }
59
+ /**
60
+ * The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
61
+ * leaves, up to the backend's own limit, not the page the person happens to be on. With nothing sorted it follows
62
+ * the list's own order, newest first.
63
+ */
64
+ exportRequest(query) {
65
+ return {
66
+ ...query.filters,
67
+ searchKeyword: text(query.search) ?? undefined,
68
+ sorting: text(query.sort) ?? "creationTime",
69
+ sortType: query.direction ?? (query.sort ? "asc" : "desc"),
70
+ };
71
+ }
72
+ readSettings(result) {
73
+ const answer = isRecord(result) ? result : {};
74
+ return {
75
+ subject: text(answer.subject) ?? "row",
76
+ templateFileName: text(answer.templateFileName) ?? undefined,
77
+ sheetRows: answer.sheetRows === undefined ? undefined : count(answer.sheetRows),
78
+ maxExportRows: count(answer.maxExportRows, DEFAULT_MAX_EXPORT_ROWS),
79
+ maxImportRows: count(answer.maxImportRows, DEFAULT_MAX_IMPORT_ROWS),
80
+ columns: readColumns(answer.columns),
81
+ inputs: Array.isArray(answer.inputs) ? answer.inputs : [],
82
+ recordStatuses: Array.isArray(answer.recordStatuses)
83
+ ? answer.recordStatuses.filter(isRecord).flatMap(status => (typeof status.value === "number" && typeof status.label === "string" ? [{ value: status.value, label: status.label }] : []))
84
+ : [],
85
+ defaultRecordStatus: typeof answer.defaultRecordStatus === "number" ? answer.defaultRecordStatus : undefined,
86
+ };
87
+ }
88
+ readVerify(result) {
89
+ const answer = isRecord(result) ? result : {};
90
+ const rows = readRows(answer.rows);
91
+ const kinds = (kind) => rows.filter(row => row.kind === kind).length;
92
+ const read = count(answer.read, rows.length);
93
+ const fresh = answer.new === undefined ? kinds(ExcelRowKind.New) : count(answer.new);
94
+ const invalid = answer.invalid === undefined ? kinds(ExcelRowKind.Invalid) : count(answer.invalid);
95
+ return {
96
+ subject: text(answer.subject) ?? "row",
97
+ read,
98
+ new: fresh,
99
+ duplicates: answer.duplicates === undefined ? kinds(ExcelRowKind.Duplicate) : count(answer.duplicates),
100
+ invalid,
101
+ isReady: typeof answer.isReady === "boolean" ? answer.isReady : invalid === 0 && fresh > 0,
102
+ note: text(answer.note) ?? "",
103
+ columns: readColumns(answer.columns),
104
+ rows,
105
+ inputs: Array.isArray(answer.inputs) ? answer.inputs : [],
106
+ };
107
+ }
108
+ readImport(result) {
109
+ const answer = isRecord(result) ? result : {};
110
+ return {
111
+ sent: count(answer.sent),
112
+ created: count(answer.created),
113
+ duplicates: count(answer.duplicates),
114
+ invalid: count(answer.invalid),
115
+ refused: readRows(answer.refused),
116
+ note: text(answer.note) ?? "",
117
+ };
118
+ }
119
+ get downloadHeaders() {
120
+ return NEXUS_DOWNLOAD_HEADERS;
121
+ }
122
+ }
123
+ /** The most rows one export writes when the backend does not say (`Nexus:Excel:MaxExportRows`). */
124
+ export const DEFAULT_MAX_EXPORT_ROWS = 5000;
125
+ /** The most rows one import takes when the backend does not say (`Nexus:Excel:MaxImportRows`). */
126
+ export const DEFAULT_MAX_IMPORT_ROWS = 2000;
127
+ /** What a Nexus download says about itself (`Excels/ExcelHeaders.cs`), all of them exposed by the CORS policy. */
128
+ export const NEXUS_DOWNLOAD_HEADERS = {
129
+ rows: "X-Nexus-Export-Rows",
130
+ skipped: "X-Nexus-Export-Skipped",
131
+ limit: "X-Nexus-Export-Limit",
132
+ note: "X-Nexus-Export-Note",
133
+ };
134
+ function readColumns(value) {
135
+ if (!Array.isArray(value))
136
+ return [];
137
+ const columns = [];
138
+ for (const item of value) {
139
+ if (!isRecord(item))
140
+ continue;
141
+ const field = text(item.field);
142
+ if (!field)
143
+ continue;
144
+ columns.push({
145
+ field,
146
+ header: text(item.header) ?? field,
147
+ dataType: count(item.dataType),
148
+ isRequired: item.isRequired === true,
149
+ isReadOnly: item.isReadOnly === true,
150
+ isReference: item.isReference === true,
151
+ list: text(item.list),
152
+ minLength: count(item.minLength),
153
+ maxLength: count(item.maxLength),
154
+ minimum: item.minimum === undefined || item.minimum === null ? null : count(item.minimum),
155
+ maximum: item.maximum === undefined || item.maximum === null ? null : count(item.maximum),
156
+ note: text(item.note),
157
+ });
158
+ }
159
+ return columns;
160
+ }
161
+ /** A dictionary of texts: every value as text, so nothing read out of an answer can be sent back as a number or an id. */
162
+ function readValues(value) {
163
+ const values = {};
164
+ if (!isRecord(value))
165
+ return values;
166
+ for (const [key, item] of Object.entries(value))
167
+ values[key] = text(item);
168
+ return values;
169
+ }
170
+ function readProblems(value) {
171
+ if (!Array.isArray(value))
172
+ return [];
173
+ const problems = [];
174
+ for (const item of value) {
175
+ if (!isRecord(item))
176
+ continue;
177
+ problems.push({
178
+ row: count(item.row),
179
+ column: count(item.column),
180
+ cell: text(item.cell) ?? "",
181
+ header: text(item.header) ?? "",
182
+ field: text(item.field) ?? "",
183
+ message: text(item.message) ?? "",
184
+ messageCode: text(item.messageCode) ?? undefined,
185
+ errorCode: item.errorCode === undefined ? undefined : count(item.errorCode),
186
+ });
187
+ }
188
+ return problems;
189
+ }
190
+ function readRows(value) {
191
+ if (!Array.isArray(value))
192
+ return [];
193
+ const rows = [];
194
+ for (const [index, item] of value.entries()) {
195
+ if (!isRecord(item))
196
+ continue;
197
+ const problems = readProblems(item.problems);
198
+ const kind = item.kind === undefined ? (problems.length ? ExcelRowKind.Invalid : ExcelRowKind.New) : count(item.kind);
199
+ rows.push({
200
+ number: count(item.number, index + 1),
201
+ kind,
202
+ values: readValues(item.values),
203
+ labels: readValues(item.labels),
204
+ problems,
205
+ duplicate: text(item.duplicate),
206
+ });
207
+ }
208
+ return rows;
209
+ }
210
+ /**
211
+ * Reads an answer whose problems are in `errorMessages`, which is how the backend answers a refusal now: nothing at the
212
+ * top level, and one entry per wrong field (`{ message, messageCode, errorCode, inputName }`) so every one of them is
213
+ * told at once. The first entry words the answer, and each entry that names an input lands on that input.
214
+ *
215
+ * Anything else - a success, a backend of another shape, problem details, a proxy answering while the service is down -
216
+ * goes to `readApiResponse`, which is also the default for every call that does not name a reader. It is used here
217
+ * because a spreadsheet's refusals are the whole point of the review: "no row could be written" has to reach the person
218
+ * in the backend's own words, not as a generic sentence.
219
+ */
220
+ export function readExcelResponse(body, status) {
221
+ if (isRecord(body) && body.isSuccess !== true) {
222
+ const list = Array.isArray(body.errorMessages) ? body.errorMessages : isRecord(body.errorMessages) ? [body.errorMessages] : [];
223
+ const problems = list.filter(isRecord);
224
+ if (problems.length) {
225
+ const first = problems[0];
226
+ const errors = {};
227
+ for (const problem of problems) {
228
+ const input = text(problem.inputName);
229
+ const message = text(problem.message);
230
+ if (input && message && !(input in errors))
231
+ errors[input] = message;
232
+ }
233
+ return {
234
+ ok: false,
235
+ status: status ?? (typeof body.code === "number" ? body.code : undefined),
236
+ message: text(first.message) ?? undefined,
237
+ messageCode: text(first.messageCode) ?? undefined,
238
+ errorCode: first.errorCode === undefined ? undefined : count(first.errorCode),
239
+ ...(Object.keys(errors).length ? { errors } : {}),
240
+ // An import that wrote nothing still says, row by row, what it refused and why (XLS_X044): keep that report.
241
+ ...(isRecord(body.result) ? { result: body.result } : {}),
242
+ };
243
+ }
244
+ }
245
+ return readApiResponse(body, status);
246
+ }
247
+ let current = null;
248
+ /** Registers the app's spreadsheet conventions, once, beside its API modules. */
249
+ export function defineExcelBackend(backend) {
250
+ current = backend;
251
+ return backend;
252
+ }
253
+ /** The registered spreadsheet conventions, or the Nexus ones. */
254
+ export function getExcelBackend() {
255
+ return (current ??= new NexusExcelBackend());
256
+ }
257
+ /** The file name out of a `Content-Disposition`, `filename*` first (it carries the encoding) and `filename` after. */
258
+ export function readFileName(disposition) {
259
+ if (!disposition)
260
+ return undefined;
261
+ const encoded = /filename\*\s*=\s*([^;]+)/i.exec(disposition)?.[1]?.trim();
262
+ if (encoded) {
263
+ const parts = encoded.split("'");
264
+ const value = (parts.length >= 3 ? parts.slice(2).join("'") : encoded).trim();
265
+ try {
266
+ const name = decodeURIComponent(value.replace(/^"|"$/g, ""));
267
+ if (name)
268
+ return name;
269
+ }
270
+ catch {
271
+ // A name that is not valid percent-encoding: fall through to the plain one.
272
+ }
273
+ }
274
+ const plain = /filename\s*=\s*("([^"]*)"|[^;]+)/i.exec(disposition);
275
+ const name = (plain?.[2] ?? plain?.[1] ?? "").trim();
276
+ return name.length ? name : undefined;
277
+ }
278
+ /** What a download answered, as its own headers tell it. */
279
+ export function readDownloadHeaders(headers, blob, fallbackName) {
280
+ const names = getExcelBackend().downloadHeaders;
281
+ const number = (name) => {
282
+ const value = headers.get(name);
283
+ if (value === null)
284
+ return undefined;
285
+ const read = Number(value);
286
+ return Number.isFinite(read) ? read : undefined;
287
+ };
288
+ return {
289
+ blob,
290
+ fileName: readFileName(headers.get("Content-Disposition")) ?? fallbackName,
291
+ rows: number(names.rows),
292
+ skipped: number(names.skipped),
293
+ limit: number(names.limit),
294
+ note: text(headers.get(names.note)) ?? undefined,
295
+ };
296
+ }
@@ -0,0 +1,126 @@
1
+ import type { InputParams } from "../Interfaces/FormInterfaces.ts";
2
+ import type { ExcelCellProblem, ExcelColumnInfo, ExcelDownload, ExcelReviewRow, ExcelSheetRow } from "../Interfaces/ExcelInterfaces.ts";
3
+ /** What a spreadsheet file is called in a file picker. */
4
+ export declare const EXCEL_FILE_ACCEPT = ".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
5
+ /** The field of an editor row that holds the sheet row it came from. Not a column, so no input is drawn for it. */
6
+ export declare const EXCEL_ROW_NUMBER = "__excelRow";
7
+ /** The field of an editor row that holds each reference column's key, which the person never sees or types. */
8
+ export declare const EXCEL_ROW_KEYS = "__excelKeys";
9
+ /** The field of an editor row that holds what the verify answered for it, so an edit can be told from an untouched row. */
10
+ export declare const EXCEL_ROW_SENT = "__excelSent";
11
+ /**
12
+ * One row of a review editor: a value per column, plus the three fields the editor carries for itself. A row added in
13
+ * the browser - by Add, Insert or Duplicate, or every row of a bulk insert - has none of the three, or only some.
14
+ */
15
+ export interface ExcelEditorRow {
16
+ [EXCEL_ROW_NUMBER]?: number;
17
+ [EXCEL_ROW_KEYS]?: Record<string, string | null>;
18
+ [EXCEL_ROW_SENT]?: Record<string, string | null>;
19
+ [field: string]: unknown;
20
+ }
21
+ declare const DEFAULT_TEXT: {
22
+ readonly template: "Download template";
23
+ readonly templateHint: "The file to fill in: every list is a dropdown, and the ids beside a choice fill themselves.";
24
+ readonly choose: "Choose file";
25
+ readonly change: "Choose another file";
26
+ readonly checking: "Checking";
27
+ readonly nothingPicked: "No file picked yet.";
28
+ readonly verifyFailed: "The file could not be read.";
29
+ readonly newRows: "New rows";
30
+ readonly duplicates: "Already there";
31
+ readonly duplicateHint: "Already records: these are skipped, and nothing here is updated.";
32
+ readonly failedHead: "Not written";
33
+ readonly skippedHead: "Skipped: already there";
34
+ readonly importAgain: "To import another file, start over.";
35
+ readonly addAgain: "To add more, start over.";
36
+ readonly bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.";
37
+ readonly discardTitle: "Discard what you typed?";
38
+ readonly discardLabel: "Discard";
39
+ readonly keepEditing: "Keep editing";
40
+ readonly toFix: "What needs fixing";
41
+ readonly whyDuplicate: "Why these are already there";
42
+ readonly referenceLocked: "Picked in the spreadsheet: this list belongs to the server.";
43
+ readonly generated: "The file fills this in.";
44
+ readonly importLabel: "Import";
45
+ readonly importNothing: "There is no row to import.";
46
+ readonly importedNone: "Nothing was written.";
47
+ readonly refused: "Not written";
48
+ readonly exportWarning: "Export is cut short";
49
+ readonly close: "Close";
50
+ readonly startOver: "Start over";
51
+ };
52
+ export type ExcelTextKey = keyof typeof DEFAULT_TEXT;
53
+ /** Rewords or translates the spreadsheet side's own texts. Names only what changes. */
54
+ export declare function configureExcelText(changes: Partial<Record<ExcelTextKey, string>>): void;
55
+ /** One of the spreadsheet side's texts. */
56
+ export declare function excelText(key: ExcelTextKey): string;
57
+ /**
58
+ * One column of a sheet as an input of the review editors. The rules are the ones the sheet told the person about and
59
+ * no others, so a fix made here is checked the way the file was.
60
+ *
61
+ * Three columns are read-only on purpose:
62
+ * - a **reference**, because its list is the backend's and is picked in the spreadsheet; the cell shows the words the
63
+ * person picked and the key travels back untouched (a key read out of an answer is never typed, and never an id);
64
+ * - a column the **file** fills;
65
+ * - and a **date and time** is a text box rather than a picker, because the sheet's moment is a zone-less wall clock
66
+ * while the picker's value is a UTC moment: a picker here would shift every value by the person's own offset.
67
+ */
68
+ export declare function excelColumnInput(column: ExcelColumnInfo): InputParams;
69
+ /** Every column of a sheet as the inputs of one editor row, in the order they read. */
70
+ export declare function excelColumnInputs(columns: readonly ExcelColumnInfo[]): InputParams[];
71
+ /** Whether the person edits this column's cell at all, or only reads it. */
72
+ export declare function isExcelColumnEditable(column: ExcelColumnInfo): boolean;
73
+ /**
74
+ * A verify's rows as the editors' rows: a value per column, the sheet's row number, and - held beside them, never
75
+ * drawn - each reference column's key and the values the verify answered with.
76
+ *
77
+ * A reference cell shows the **words** the person picked, because its key is not something anybody reads or types.
78
+ */
79
+ export declare function excelEditorRows(rows: readonly ExcelReviewRow[], columns: readonly ExcelColumnInfo[]): ExcelEditorRow[];
80
+ /** The sheet row a row came from, or 0 for a row added in the browser. */
81
+ export declare function excelRowNumber(row: ExcelEditorRow): number;
82
+ /**
83
+ * The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
84
+ * row number kept so a problem can still name where it is.
85
+ *
86
+ * A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
87
+ * backend reads its own text again, not a number an input rewrote on the way through.
88
+ *
89
+ * A row added in the browser has no sheet row, so it is numbered after the last one the sheet had: no two rows share a
90
+ * number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
91
+ * rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
92
+ * numbers its rows 1, 2, 3.
93
+ */
94
+ export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[], after?: number): ExcelSheetRow[];
95
+ /** Whether the person changed anything in a row since the verify answered it. */
96
+ export declare function isExcelRowEdited(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
97
+ /** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
98
+ export declare function hasExcelValues(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
99
+ /**
100
+ * Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
101
+ * a row added here with anything in it.
102
+ */
103
+ export declare function isExcelRowTyped(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
104
+ /**
105
+ * Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
106
+ * typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
107
+ */
108
+ export declare function excelProblemPlace(problem: ExcelCellProblem, byRow?: boolean): string;
109
+ /** Every problem of these rows, in the order the rows read. */
110
+ export declare function excelProblems(rows: readonly ExcelReviewRow[]): ExcelCellProblem[];
111
+ /** The rows of one kind. */
112
+ export declare function excelRowsOfKind(rows: readonly ExcelReviewRow[], ...kinds: readonly number[]): ExcelReviewRow[];
113
+ /** The rows an import would try to write: the new ones, and the ones still waiting on a fix. */
114
+ export declare function excelWritableRows(rows: readonly ExcelReviewRow[]): ExcelReviewRow[];
115
+ /** More than one of the subject: "cities", "boxes", "rows". An English ending, which a text change can replace. */
116
+ export declare function excelPlural(subject: string, plural?: string): string;
117
+ /** A count said in the subject's own words: "1 city", "12 cities". */
118
+ export declare function excelCount(rows: number, subject: string, plural?: string): string;
119
+ /**
120
+ * What to tell the person before an export that will be cut short, as the owner asked: what is on screen, what will be
121
+ * written, and that filtering is how they get the rest.
122
+ */
123
+ export declare function excelExportWarning(rows: number, limit: number, subject: string): string;
124
+ /** Hands the person a file to save, under the name the answer gave it. */
125
+ export declare function saveExcelFile(download: ExcelDownload): void;
126
+ export {};