nexus-shared 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +442 -0
  2. package/README.md +1123 -1000
  3. package/dist/Client.Index.d.ts +6 -0
  4. package/dist/Client.Index.js +6 -0
  5. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  6. package/dist/Components/Chats/ChatParts.js +3 -7
  7. package/dist/Components/Chats/ChatThread.js +3 -3
  8. package/dist/Components/Documents/Button.d.ts +6 -1
  9. package/dist/Components/Documents/Button.js +2 -2
  10. package/dist/Components/Documents/Menu.js +21 -9
  11. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  12. package/dist/Components/Documents/SplitButton.js +7 -5
  13. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  14. package/dist/Components/Documents/TabButtons.js +2 -2
  15. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  16. package/dist/Components/Forms/ApiForm.js +1 -1
  17. package/dist/Components/Forms/Crud.js +486 -206
  18. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  19. package/dist/Components/Forms/CrudHistory.js +140 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  21. package/dist/Components/Forms/CrudParts.js +38 -5
  22. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  23. package/dist/Components/Forms/ExcelImport.js +189 -60
  24. package/dist/Components/Forms/Form.js +9 -14
  25. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  26. package/dist/Components/Forms/SubmitForm.js +23 -12
  27. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  28. package/dist/Components/Inputs/CodeBox.js +316 -0
  29. package/dist/Components/Inputs/DatePicker.js +4 -1
  30. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  31. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  32. package/dist/Components/Inputs/Dropdown.js +4 -1
  33. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  34. package/dist/Components/Inputs/DropdownParts.js +4 -6
  35. package/dist/Components/Inputs/InputRenderer.js +3 -0
  36. package/dist/Components/Inputs/NumberBox.js +26 -2
  37. package/dist/Components/Inputs/RowsInput.js +2 -2
  38. package/dist/Components/Inputs/Slider.js +4 -1
  39. package/dist/Components/Inputs/TabularForm.js +42 -10
  40. package/dist/Components/Inputs/TextArea.js +4 -1
  41. package/dist/Components/Inputs/TextBox.js +4 -1
  42. package/dist/Components/Inputs/TimePanel.js +4 -3
  43. package/dist/Components/Inputs/TimePicker.js +4 -1
  44. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  45. package/dist/Components/Inputs/TreeView.js +372 -23
  46. package/dist/Components/Layouts/Popup.js +1 -4
  47. package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
  48. package/dist/Components/Layouts/ThemePicker.js +89 -6
  49. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  50. package/dist/Components/Layouts/ThemeScript.js +4 -2
  51. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  52. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  53. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  54. package/dist/Components/Viewers/Avatar.js +21 -0
  55. package/dist/Components/Viewers/DataTable.js +252 -62
  56. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  57. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  58. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  59. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  60. package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
  61. package/dist/Components/Viewers/DataTableParts.js +42 -13
  62. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  63. package/dist/Helpers/AnimationHelpers.js +20 -2
  64. package/dist/Helpers/ApiModules.d.ts +3 -34
  65. package/dist/Helpers/ApiModules.js +3 -3
  66. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  67. package/dist/Helpers/ApiRoutes.js +31 -27
  68. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  69. package/dist/Helpers/ArrayHelpers.js +2 -0
  70. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  71. package/dist/Helpers/CodeHelpers.js +332 -0
  72. package/dist/Helpers/CrudBackend.d.ts +101 -8
  73. package/dist/Helpers/CrudBackend.js +172 -11
  74. package/dist/Helpers/CrudColumns.d.ts +51 -0
  75. package/dist/Helpers/CrudColumns.js +69 -0
  76. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  77. package/dist/Helpers/CrudHelpers.js +109 -12
  78. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  79. package/dist/Helpers/ExcelBackend.js +8 -2
  80. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  81. package/dist/Helpers/ExcelHelpers.js +55 -13
  82. package/dist/Helpers/FormStore.d.ts +9 -5
  83. package/dist/Helpers/FormStore.js +25 -5
  84. package/dist/Helpers/InputParamsHelpers.js +8 -0
  85. package/dist/Helpers/MessageBuilder.js +3 -0
  86. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  87. package/dist/Helpers/NexusHelpers.js +1 -0
  88. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  89. package/dist/Helpers/NumberHelpers.js +41 -3
  90. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  91. package/dist/Helpers/PopoverHelpers.js +52 -17
  92. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  93. package/dist/Helpers/PopoverHooks.js +62 -0
  94. package/dist/Helpers/RequestCache.d.ts +35 -0
  95. package/dist/Helpers/RequestCache.js +113 -0
  96. package/dist/Helpers/RowsStore.d.ts +7 -2
  97. package/dist/Helpers/RowsStore.js +15 -3
  98. package/dist/Helpers/TableFilters.d.ts +25 -0
  99. package/dist/Helpers/TableFilters.js +162 -0
  100. package/dist/Helpers/TableHelpers.d.ts +7 -4
  101. package/dist/Helpers/TableHelpers.js +15 -6
  102. package/dist/Helpers/TableJoins.d.ts +70 -0
  103. package/dist/Helpers/TableJoins.js +250 -0
  104. package/dist/Helpers/TableStore.d.ts +16 -3
  105. package/dist/Helpers/TableStore.js +25 -5
  106. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  107. package/dist/Helpers/ThemeHelpers.js +46 -9
  108. package/dist/Helpers/TreeStore.d.ts +225 -11
  109. package/dist/Helpers/TreeStore.js +683 -90
  110. package/dist/Helpers/UserPreferences.d.ts +22 -0
  111. package/dist/Helpers/UserPreferences.js +116 -0
  112. package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
  113. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  114. package/dist/Interfaces/FormInterfaces.d.ts +37 -6
  115. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  116. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  117. package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
  118. package/dist/Interfaces/PreferenceInterfaces.js +25 -0
  119. package/dist/Interfaces/TableInterfaces.d.ts +145 -16
  120. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  121. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  122. package/dist/Services/ApiProxy.d.ts +36 -1
  123. package/dist/Services/ApiProxy.js +182 -52
  124. package/dist/Services/ApiProxyRules.d.ts +203 -0
  125. package/dist/Services/ApiProxyRules.js +426 -0
  126. package/dist/Services/ExcelApi.d.ts +3 -1
  127. package/dist/Services/ExcelApi.js +1 -1
  128. package/dist/Services/JoinSources.d.ts +48 -0
  129. package/dist/Services/JoinSources.js +74 -0
  130. package/dist/Services/ServerApi.d.ts +13 -2
  131. package/dist/Services/ServerApi.js +8 -3
  132. package/dist/Services/ThemeService.d.ts +18 -2
  133. package/dist/Services/ThemeService.js +80 -16
  134. package/dist/Shared.Index.d.ts +6 -0
  135. package/dist/Shared.Index.js +6 -0
  136. package/package.json +4 -2
  137. package/src/Styles/Nexus.Avatar.css +79 -0
  138. package/src/Styles/Nexus.Base.css +51 -4
  139. package/src/Styles/Nexus.Button.css +27 -0
  140. package/src/Styles/Nexus.Calendar.css +4 -6
  141. package/src/Styles/Nexus.Chat.css +5 -51
  142. package/src/Styles/Nexus.Choice.css +4 -6
  143. package/src/Styles/Nexus.Crud.css +16 -9
  144. package/src/Styles/Nexus.Dropdown.css +12 -61
  145. package/src/Styles/Nexus.Editor.css +4 -6
  146. package/src/Styles/Nexus.Excel.css +34 -15
  147. package/src/Styles/Nexus.Form.css +25 -67
  148. package/src/Styles/Nexus.Index.css +1 -0
  149. package/src/Styles/Nexus.Input.css +794 -637
  150. package/src/Styles/Nexus.Markdown.css +0 -28
  151. package/src/Styles/Nexus.Menu.css +8 -30
  152. package/src/Styles/Nexus.Popup.css +22 -29
  153. package/src/Styles/Nexus.RichText.css +2 -32
  154. package/src/Styles/Nexus.Rows.css +51 -10
  155. package/src/Styles/Nexus.Slider.css +3 -5
  156. package/src/Styles/Nexus.Tab.Buttons.css +20 -7
  157. package/src/Styles/Nexus.Table.css +1387 -1296
  158. package/src/Styles/Nexus.Theme.Picker.css +342 -232
  159. package/src/Styles/Nexus.Time.css +9 -12
  160. package/src/Styles/Nexus.Toast.css +6 -8
  161. package/src/Styles/Nexus.Tokens.css +17 -0
  162. package/src/Styles/Nexus.Tree.css +76 -11
@@ -53,8 +53,8 @@ export class NexusExcelBackend extends ExcelBackend {
53
53
  get uploadField() {
54
54
  return "file";
55
55
  }
56
- importRequest(rows) {
57
- return { rows };
56
+ importRequest(rows, recordStatus) {
57
+ return recordStatus === undefined ? { rows } : { rows, recordStatus };
58
58
  }
59
59
  /**
60
60
  * The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
@@ -79,6 +79,10 @@ export class NexusExcelBackend extends ExcelBackend {
79
79
  maxImportRows: count(answer.maxImportRows, DEFAULT_MAX_IMPORT_ROWS),
80
80
  columns: readColumns(answer.columns),
81
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,
82
86
  };
83
87
  }
84
88
  readVerify(result) {
@@ -233,6 +237,8 @@ export function readExcelResponse(body, status) {
233
237
  messageCode: text(first.messageCode) ?? undefined,
234
238
  errorCode: first.errorCode === undefined ? undefined : count(first.errorCode),
235
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 } : {}),
236
242
  };
237
243
  }
238
244
  }
@@ -8,11 +8,14 @@ export declare const EXCEL_ROW_NUMBER = "__excelRow";
8
8
  export declare const EXCEL_ROW_KEYS = "__excelKeys";
9
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
10
  export declare const EXCEL_ROW_SENT = "__excelSent";
11
- /** One row of a review editor: a value per column, plus the three fields the editor carries for itself. */
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
+ */
12
15
  export interface ExcelEditorRow {
13
- [EXCEL_ROW_NUMBER]: number;
14
- [EXCEL_ROW_KEYS]: Record<string, string | null>;
15
- [EXCEL_ROW_SENT]: Record<string, string | null>;
16
+ [EXCEL_ROW_NUMBER]?: number;
17
+ [EXCEL_ROW_KEYS]?: Record<string, string | null>;
18
+ [EXCEL_ROW_SENT]?: Record<string, string | null>;
16
19
  [field: string]: unknown;
17
20
  }
18
21
  declare const DEFAULT_TEXT: {
@@ -25,7 +28,15 @@ declare const DEFAULT_TEXT: {
25
28
  readonly verifyFailed: "The file could not be read.";
26
29
  readonly newRows: "New rows";
27
30
  readonly duplicates: "Already there";
28
- readonly duplicateHint: "Nothing here is updated. Edit a row to add it as a new record instead.";
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";
29
40
  readonly toFix: "What needs fixing";
30
41
  readonly whyDuplicate: "Why these are already there";
31
42
  readonly referenceLocked: "Picked in the spreadsheet: this list belongs to the server.";
@@ -66,18 +77,35 @@ export declare function isExcelColumnEditable(column: ExcelColumnInfo): boolean;
66
77
  * A reference cell shows the **words** the person picked, because its key is not something anybody reads or types.
67
78
  */
68
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;
69
82
  /**
70
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
71
84
  * row number kept so a problem can still name where it is.
72
85
  *
73
86
  * A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
74
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.
75
93
  */
76
- export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[]): ExcelSheetRow[];
94
+ export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[], after?: number): ExcelSheetRow[];
77
95
  /** Whether the person changed anything in a row since the verify answered it. */
78
96
  export declare function isExcelRowEdited(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
79
- /** Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). */
80
- export declare function excelProblemPlace(problem: ExcelCellProblem): string;
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;
81
109
  /** Every problem of these rows, in the order the rows read. */
82
110
  export declare function excelProblems(rows: readonly ExcelReviewRow[]): ExcelCellProblem[];
83
111
  /** The rows of one kind. */
@@ -1,4 +1,6 @@
1
1
  import { ExcelDataType, ExcelRowKind } from "../Interfaces/ExcelInterfaces.js";
2
+ import { toIsoDate } from "./DateHelpers.js";
3
+ import { toIsoTime } from "./TimeHelpers.js";
2
4
  /*
3
5
  * Turning a sheet's columns into inputs, a verify's rows into editable rows and back, and handing the person a file.
4
6
  * Nothing here calls anything: the calls are in Services/ExcelApi.ts.
@@ -28,7 +30,15 @@ const DEFAULT_TEXT = {
28
30
  verifyFailed: "The file could not be read.",
29
31
  newRows: "New rows",
30
32
  duplicates: "Already there",
31
- duplicateHint: "Nothing here is updated. Edit a row to add it as a new record instead.",
33
+ duplicateHint: "Already records: these are skipped, and nothing here is updated.",
34
+ failedHead: "Not written",
35
+ skippedHead: "Skipped: already there",
36
+ importAgain: "To import another file, start over.",
37
+ addAgain: "To add more, start over.",
38
+ bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.",
39
+ discardTitle: "Discard what you typed?",
40
+ discardLabel: "Discard",
41
+ keepEditing: "Keep editing",
32
42
  toFix: "What needs fixing",
33
43
  whyDuplicate: "Why these are already there",
34
44
  referenceLocked: "Picked in the spreadsheet: this list belongs to the server.",
@@ -159,12 +169,15 @@ export function excelEditorRows(rows, columns) {
159
169
  /** Which types are drawn by a number box, whose empty value is `null`. */
160
170
  const NUMBER_TYPES = new Set([ExcelDataType.WholeNumber, ExcelDataType.Decimal, ExcelDataType.Amount]);
161
171
  /**
162
- * A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty, and
163
- * everything else as it reads.
172
+ * A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty, a date
173
+ * and a time as the inputs hold them, and everything else as it reads.
164
174
  *
165
175
  * The three empties are not interchangeable. A number box is empty at `null`; a **dropdown** given `""` treats it as a
166
176
  * value and reports it as one its list no longer offers ("Not found"), so an empty choice has to be left unset; a text
167
177
  * or date box is empty at `""`.
178
+ *
179
+ * A date and a time are read the way their inputs read them: the sheet's "09:30:00" is the time box's "09:30". Read
180
+ * any other way, a cell nobody touched would compare as an edit - and the panel would ask before closing over it.
168
181
  */
169
182
  function toInputValue(column, value) {
170
183
  if (value === null || value === "")
@@ -184,6 +197,10 @@ function toInputValue(column, value) {
184
197
  return "No";
185
198
  return value;
186
199
  }
200
+ case ExcelDataType.Date:
201
+ return toIsoDate(value) ?? value;
202
+ case ExcelDataType.Time:
203
+ return toIsoTime(value) ?? value;
187
204
  default:
188
205
  return value;
189
206
  }
@@ -210,17 +227,28 @@ function isSameCell(column, current, sent) {
210
227
  return true;
211
228
  return toInputValue(column, asSheet) === toInputValue(column, sent ?? null);
212
229
  }
230
+ /** The sheet row a row came from, or 0 for a row added in the browser. */
231
+ export function excelRowNumber(row) {
232
+ const number = Number(row[EXCEL_ROW_NUMBER]);
233
+ return Number.isFinite(number) && number > 0 ? number : 0;
234
+ }
213
235
  /**
214
236
  * The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
215
237
  * row number kept so a problem can still name where it is.
216
238
  *
217
239
  * A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
218
240
  * backend reads its own text again, not a number an input rewrote on the way through.
241
+ *
242
+ * 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
243
+ * number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
244
+ * rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
245
+ * numbers its rows 1, 2, 3.
219
246
  */
220
- export function excelSheetRows(rows, columns) {
221
- return rows.map((row, index) => {
222
- const keys = (row[EXCEL_ROW_KEYS] ?? {});
223
- const sent = (row[EXCEL_ROW_SENT] ?? {});
247
+ export function excelSheetRows(rows, columns, after = 0) {
248
+ let next = rows.reduce((last, row) => Math.max(last, excelRowNumber(row)), Math.max(0, after));
249
+ return rows.map(row => {
250
+ const keys = row[EXCEL_ROW_KEYS] ?? {};
251
+ const sent = row[EXCEL_ROW_SENT] ?? {};
224
252
  const values = {};
225
253
  const labels = {};
226
254
  for (const column of columns) {
@@ -231,13 +259,12 @@ export function excelSheetRows(rows, columns) {
231
259
  }
232
260
  values[column.field] = isSameCell(column, row[column.field], sent[column.field]) ? (sent[column.field] ?? null) : toSheetValue(row[column.field]);
233
261
  }
234
- const number = Number(row[EXCEL_ROW_NUMBER]);
235
- return { number: Number.isFinite(number) && number > 0 ? number : index + 1, values, labels };
262
+ return { number: excelRowNumber(row) || ++next, values, labels };
236
263
  });
237
264
  }
238
265
  /** Whether the person changed anything in a row since the verify answered it. */
239
266
  export function isExcelRowEdited(row, columns) {
240
- const sent = (row[EXCEL_ROW_SENT] ?? {});
267
+ const sent = row[EXCEL_ROW_SENT] ?? {};
241
268
  for (const column of columns) {
242
269
  if (!isExcelColumnEditable(column))
243
270
  continue;
@@ -246,10 +273,25 @@ export function isExcelRowEdited(row, columns) {
246
273
  }
247
274
  return false;
248
275
  }
276
+ /** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
277
+ export function hasExcelValues(row, columns) {
278
+ return columns.some(column => isExcelColumnEditable(column) && toSheetValue(row[column.field]) !== null);
279
+ }
280
+ /**
281
+ * Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
282
+ * a row added here with anything in it.
283
+ */
284
+ export function isExcelRowTyped(row, columns) {
285
+ return excelRowNumber(row) > 0 ? isExcelRowEdited(row, columns) : hasExcelValues(row, columns);
286
+ }
249
287
  /* What is wrong, in the person's words */
250
- /** Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). */
251
- export function excelProblemPlace(problem) {
252
- return problem.cell ? `${problem.cell} ${String.fromCharCode(183)} ${problem.header}` : problem.header;
288
+ /**
289
+ * Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
290
+ * typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
291
+ */
292
+ export function excelProblemPlace(problem, byRow = false) {
293
+ const place = byRow ? `Row ${problem.row}` : problem.cell;
294
+ return place ? `${place} ${String.fromCharCode(183)} ${problem.header}` : problem.header;
253
295
  }
254
296
  /** Every problem of these rows, in the order the rows read. */
255
297
  export function excelProblems(rows) {
@@ -25,7 +25,7 @@ export interface FormStatus {
25
25
  changed: boolean;
26
26
  /** A submit is running. */
27
27
  submitting: boolean;
28
- /** The values are loading: the inputs show placeholders. */
28
+ /** The values are loading: the inputs show, disabled, and take nothing typed until the values are in. */
29
29
  loading: boolean;
30
30
  /** A submit ran and found problems: every message shows. */
31
31
  showAll: boolean;
@@ -85,6 +85,8 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
85
85
  private draftSeq;
86
86
  /** The user cancelled: no draft is written until they change a value again. */
87
87
  private draftDropped;
88
+ /** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
89
+ private refusedWhileLoading;
88
90
  /** A load during a render tells nobody; `announce` does, once the form has mounted. */
89
91
  private quiet;
90
92
  private unannounced;
@@ -137,13 +139,15 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
137
139
  * Loads values, such as a record from a server. They become the values the form compares with, and every input shows
138
140
  * them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
139
141
  * names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
140
- * a saved draft for a later load (the form's placeholders show while the real values load). `quiet` tells no listener
141
- * until `announce()`, for a load during a render.
142
+ * a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
143
+ * until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
144
+ * the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
142
145
  */
143
- load(values?: Partial<TValues>, { keepChanged, draft, quiet }?: {
146
+ load(values?: Partial<TValues>, { keepChanged, draft, quiet, loading }?: {
144
147
  keepChanged?: boolean;
145
148
  draft?: boolean;
146
149
  quiet?: boolean;
150
+ loading?: boolean;
147
151
  }): string[];
148
152
  /** Tells listeners about a quiet load. */
149
153
  announce(): void;
@@ -197,7 +201,7 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
197
201
  commit(saved: TValues, extra?: Partial<TValues>): void;
198
202
  /** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
199
203
  markSaved(): void;
200
- /** @internal */
204
+ /** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
201
205
  setLoading(loading: boolean): void;
202
206
  /** @internal */
203
207
  setFormId(formId: string | null): void;
@@ -70,6 +70,8 @@ export class FormStore {
70
70
  draftSeq = 0;
71
71
  /** The user cancelled: no draft is written until they change a value again. */
72
72
  draftDropped = false;
73
+ /** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
74
+ refusedWhileLoading = false;
73
75
  /** A load during a render tells nobody; `announce` does, once the form has mounted. */
74
76
  quiet = false;
75
77
  unannounced = false;
@@ -216,14 +218,18 @@ export class FormStore {
216
218
  * Loads values, such as a record from a server. They become the values the form compares with, and every input shows
217
219
  * them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
218
220
  * names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
219
- * a saved draft for a later load (the form's placeholders show while the real values load). `quiet` tells no listener
220
- * until `announce()`, for a load during a render.
221
+ * a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
222
+ * until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
223
+ * the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
221
224
  */
222
- load(values = {}, { keepChanged = false, draft = true, quiet = false } = {}) {
225
+ load(values = {}, { keepChanged = false, draft = true, quiet = false, loading } = {}) {
223
226
  this.quiet = quiet;
224
227
  this.unannounced ||= quiet;
225
228
  try {
226
- return this.loadValues(values, keepChanged, draft);
229
+ const changed = this.loadValues(values, keepChanged, draft);
230
+ if (loading !== undefined)
231
+ this.patchStatus({ loading });
232
+ return changed;
227
233
  }
228
234
  finally {
229
235
  this.quiet = false;
@@ -264,8 +270,10 @@ export class FormStore {
264
270
  this.nextRevision(params.name);
265
271
  }
266
272
  else {
273
+ // Every input mounts again with its loaded value, one that was typed in while loading included.
267
274
  this.generation++;
268
275
  this.touched.clear();
276
+ this.refusedWhileLoading = false;
269
277
  }
270
278
  this.serverErrors.clear();
271
279
  this.serverMessage = null;
@@ -321,6 +329,12 @@ export class FormStore {
321
329
  input(name, value, stamp) {
322
330
  if (stamp !== this.stamp(name))
323
331
  return;
332
+ // Nothing is taken while the values load, not even from an input whose `disabled` was taken off by hand: the
333
+ // loaded values would land on top of it, and a change to a record that is not there yet means nothing.
334
+ if (this.status.loading) {
335
+ this.refusedWhileLoading = true;
336
+ return;
337
+ }
324
338
  this.change([[name, value]], "input");
325
339
  }
326
340
  /** Marks an input as left by the user: the form's rule messages on it show from now on. */
@@ -481,8 +495,14 @@ export class FormStore {
481
495
  markSaved() {
482
496
  this.patchStatus({ submitting: false, savedAt: Date.now() });
483
497
  }
484
- /** @internal */
498
+ /** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
485
499
  setLoading(loading) {
500
+ // Something was typed and refused, and no load has drawn the inputs again since: they show the store's values.
501
+ if (!loading && this.refusedWhileLoading) {
502
+ this.refusedWhileLoading = false;
503
+ this.generation++;
504
+ this.bumpAll();
505
+ }
486
506
  this.patchStatus({ loading });
487
507
  }
488
508
  /** @internal */
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_DATE_PREFERENCES } from "../Interfaces/DateInterfaces.js";
2
+ import { formatCode, loadCode, validateCode } from "./CodeHelpers.js";
2
3
  import { formatDate, formatDateRange, toDateRange, toIsoDate, usesDevanagari } from "./DateHelpers.js";
3
4
  import { formatDateTime, toUtcDateTime } from "./DateTimeHelpers.js";
4
5
  import { formatNumber, parseNumber } from "./NumberHelpers.js";
@@ -108,6 +109,9 @@ export function normalizeInputValue(params, value) {
108
109
  return Array.isArray(value) && value.length === 2 ? value : emptyInputValue(params);
109
110
  case "radio-group":
110
111
  return value;
112
+ case "code":
113
+ // Only separators and case: a saved code that does not fit stays as it is, so its check refuses the save.
114
+ return loadCode(value, params);
111
115
  default:
112
116
  return typeof value === "string" ? value : String(value);
113
117
  }
@@ -197,6 +201,8 @@ export function validateInput(params, value, context = {}) {
197
201
  seconds,
198
202
  });
199
203
  }
204
+ case "code":
205
+ return validateCode(value, params, name);
200
206
  case "slider":
201
207
  return params.validate?.(value) || null;
202
208
  case "range-slider":
@@ -369,6 +375,8 @@ export function formatInputValue(params, value, context = {}) {
369
375
  return formatTime(value, timeSettings(params, preferences));
370
376
  case "date-time":
371
377
  return formatDateTime(value, { ...dateSettings(params, preferences), timeFormat: params.timeFormat ?? preferences.timeFormat, timeZone: params.timeZone });
378
+ case "code":
379
+ return formatCode(value, params);
372
380
  case "slider":
373
381
  return formatNumber(value, params);
374
382
  case "range-slider":
@@ -17,6 +17,8 @@ const DEFAULT_WORDS = {
17
17
  unflag: { verb: "unflag", done: "unflagged", doing: "Unflagging", object: "the record" },
18
18
  pin: { verb: "pin", done: "pinned", doing: "Pinning", object: "the record" },
19
19
  unpin: { verb: "unpin", done: "unpinned", doing: "Unpinning", object: "the record" },
20
+ favorite: { verb: "add {object} to the favourites", done: "added to the favourites", doing: "Adding {object} to the favourites", object: "the record" },
21
+ unfavorite: { verb: "remove {object} from the favourites", done: "removed from the favourites", doing: "Removing {object} from the favourites", object: "the record" },
20
22
  send: { verb: "send", done: "sent", doing: "Sending", object: "the message", sendsValues: true },
21
23
  submit: { verb: "submit", done: "submitted", doing: "Submitting", object: "the form", sendsValues: true },
22
24
  upload: { verb: "upload", done: "uploaded", doing: "Uploading", object: "the file", sendsValues: true },
@@ -43,6 +45,7 @@ const DEFAULT_TEXTS = {
43
45
  confirmDetails: {
44
46
  delete: "This cannot be undone.",
45
47
  trash: "It can be restored from the trash.",
48
+ recover: "It goes back to the active list.",
46
49
  },
47
50
  reasons: {
48
51
  network: "The server cannot be reached. Check your internet connection and try again.",
File without changes
@@ -0,0 +1 @@
1
+ "use strict";
@@ -1,4 +1,5 @@
1
1
  import type { NumberFormatOptions } from "../Interfaces/InputInterfaces.ts";
2
+ import { ENumberScales, type ThousandsSeparator } from "../Interfaces/PreferenceInterfaces.ts";
2
3
  /** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
3
4
  export declare const DEFAULT_NUMBER_LOCALE = "en-US";
4
5
  /** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
@@ -15,7 +16,12 @@ export interface NumberTextRules {
15
16
  /** Allows a leading minus sign. */
16
17
  negative: boolean;
17
18
  }
18
- /** The decimal and group separators of a locale. */
19
+ /**
20
+ * The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
21
+ * else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
22
+ */
23
+ export declare function numberLocale(scale: ENumberScales, thousands: ThousandsSeparator): string;
24
+ /** The decimal and group separators of a locale (or of a key from `numberLocale`). */
19
25
  export declare function getNumberSymbols(locale?: string): NumberSymbols;
20
26
  /**
21
27
  * Formats a number for display: `formatNumber(1250, { decimals: 2, fixedDecimals: true, grouping: true })` gives "1,250.00".
@@ -1,3 +1,4 @@
1
+ import { ENumberScales } from "../Interfaces/PreferenceInterfaces.js";
1
2
  /** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
2
3
  export const DEFAULT_NUMBER_LOCALE = "en-US";
3
4
  /** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
@@ -9,9 +10,43 @@ const BIDI_MARKS = /[\u061c\u200e\u200f]/g;
9
10
  // U+2212, the typographic minus sign some locales use.
10
11
  const MINUS_SIGN = "\u2212";
11
12
  const SPACES = /\s/g;
12
- /** The decimal and group separators of a locale. */
13
+ /*
14
+ * A person's number settings as a locale: the scale picks where the groups fall (en-US, or en-IN for lakh and crore)
15
+ * and the separator what marks them. A pair Intl already has is that locale; any other is a key of three parts,
16
+ * "en-IN|.|,", that the helpers here read as the base locale's grouping with these two marks. Everything that takes
17
+ * a `locale` (a number box, a table cell, a message) therefore takes the person's settings without knowing of them.
18
+ */
19
+ const SEPARATOR_MARKS = {
20
+ period: { group: ".", decimal: "," },
21
+ space: { group: "\u00a0", decimal: "." },
22
+ none: { group: "", decimal: "." },
23
+ };
24
+ /**
25
+ * The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
26
+ * else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
27
+ */
28
+ export function numberLocale(scale, thousands) {
29
+ const base = scale === ENumberScales.SouthAsian ? "en-IN" : DEFAULT_NUMBER_LOCALE;
30
+ if (thousands === "comma" || !(thousands in SEPARATOR_MARKS))
31
+ return base;
32
+ const { group, decimal } = SEPARATOR_MARKS[thousands];
33
+ return `${base}|${group}|${decimal}`;
34
+ }
35
+ /** A locale key from `numberLocale`: its base locale and its own marks, or `null` for a plain locale. */
36
+ function customMarks(locale) {
37
+ if (!locale.includes("|"))
38
+ return null;
39
+ const [base = DEFAULT_NUMBER_LOCALE, group = ",", decimal = "."] = locale.split("|");
40
+ return { base, group, decimal };
41
+ }
42
+ /** The decimal and group separators of a locale (or of a key from `numberLocale`). */
13
43
  export function getNumberSymbols(locale = DEFAULT_NUMBER_LOCALE) {
14
44
  let symbols = symbolsByLocale.get(locale);
45
+ const custom = symbols ? null : customMarks(locale);
46
+ if (custom) {
47
+ symbols = { decimal: custom.decimal, group: custom.group };
48
+ symbolsByLocale.set(locale, symbols);
49
+ }
15
50
  if (!symbols) {
16
51
  const parts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
17
52
  symbols = {
@@ -33,9 +68,10 @@ export function formatNumber(value, options = {}) {
33
68
  const maxDecimals = decimals ?? MAX_NUMBER_DIGITS;
34
69
  const minDecimals = fixedDecimals ? maxDecimals : 0;
35
70
  const key = `${locale}|${minDecimals}|${maxDecimals}|${grouping}`;
71
+ const custom = customMarks(locale);
36
72
  let formatter = formatters.get(key);
37
73
  if (!formatter) {
38
- formatter = new Intl.NumberFormat(locale, {
74
+ formatter = new Intl.NumberFormat(custom?.base ?? locale, {
39
75
  numberingSystem: "latn",
40
76
  useGrouping: grouping,
41
77
  minimumFractionDigits: minDecimals,
@@ -44,7 +80,9 @@ export function formatNumber(value, options = {}) {
44
80
  formatters.set(key, formatter);
45
81
  }
46
82
  // Rounded first, the same way as roundDecimal, which also turns -0.001 into 0 rather than "-0".
47
- return formatter.format(roundDecimal(value, maxDecimals)).replace(BIDI_MARKS, "").replace(MINUS_SIGN, "-");
83
+ const text = formatter.format(roundDecimal(value, maxDecimals)).replace(BIDI_MARKS, "").replace(MINUS_SIGN, "-");
84
+ // The base locales write "," between groups and "." before decimals; a key's own marks take their places.
85
+ return custom ? text.replace(/[,.]/g, mark => (mark === "," ? custom.group : custom.decimal)) : text;
48
86
  }
49
87
  /**
50
88
  * Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
@@ -20,8 +20,9 @@ export interface PlaceUnderAnchorOptions {
20
20
  arrowAt?: DOMRect;
21
21
  }
22
22
  /**
23
- * Places a panel under an anchor, or above it when there is no room below, aligned to the anchor's start or end
24
- * edge and kept inside the window, with an arrow aimed at the anchor. Returns whether the panel went above.
23
+ * Places a panel under an anchor, or above it when it fits only there, or on the side with more room when it fits on
24
+ * neither; aligned to the anchor's start or end edge and kept inside the window, with an arrow aimed at the anchor. A
25
+ * panel that scrolls (`.nx-popover__scroll`) is never taller than the room on its side. Returns whether it went above.
25
26
  */
26
27
  export declare function placeUnderAnchor(panel: HTMLElement, anchor: DOMRect, options?: PlaceUnderAnchorOptions): boolean;
27
28
  export interface PlaceListPanelOptions {
@@ -57,11 +58,22 @@ export interface PlaceAtPointOptions {
57
58
  /**
58
59
  * Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
59
60
  * is room, otherwise to the left of or above it, and never past the window's edges. With an arrow, the panel is set
60
- * back far enough along both axes for the arrow's tip to land on the point.
61
+ * back far enough along both axes for the arrow's tip to land on the point. A panel that scrolls is capped to the
62
+ * room on its side, as under an anchor.
61
63
  */
62
64
  export declare function placeAtPoint(panel: HTMLElement, x: number, y: number, options?: PlaceAtPointOptions): void;
63
- /** Runs `place` again, once per frame at most, while the page scrolls or the window changes size. Returns the stop function. */
64
- export declare function followAnchor(place: () => void): () => void;
65
+ /**
66
+ * Runs `place` again, once per frame at most, while the page scrolls, the window changes size, or `panel` does (its
67
+ * content grew). Returns the stop function. A frame later, never inside the observer, so a panel that `place` resizes
68
+ * is measured once more and settles instead of looping.
69
+ */
70
+ export declare function followAnchor(place: () => void, panel?: Element): () => void;
71
+ /**
72
+ * Shows a panel with its scroller (`.nx-popover__scroll`) at the top. The browser keeps where a panel was scrolled
73
+ * while it was shut, and a list that opens part-way down has lost its heading and its first rows. Does nothing on
74
+ * browsers without popover support.
75
+ */
76
+ export declare function showPanel(panel: HTMLElement): void;
65
77
  /** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
66
78
  export declare function isPopoverOpen(panel: HTMLElement | null): boolean;
67
79
  /** How far the arrow reaches past the panel's edge: its depth, less the panel's border it is drawn over. */