@dxos/react-ui-table 0.10.0 → 0.11.1

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 (110) hide show
  1. package/dist/lib/chunk-types.mjs +65 -0
  2. package/dist/lib/chunk-types.mjs.map +1 -0
  3. package/dist/lib/chunk-useTableModel.mjs +1595 -0
  4. package/dist/lib/chunk-useTableModel.mjs.map +1 -0
  5. package/dist/lib/index.mjs +1133 -0
  6. package/dist/lib/index.mjs.map +1 -0
  7. package/dist/lib/testing.mjs +88 -0
  8. package/dist/lib/testing.mjs.map +1 -0
  9. package/dist/lib/translations.mjs +30 -0
  10. package/dist/lib/translations.mjs.map +1 -0
  11. package/dist/lib/types.mjs +2 -0
  12. package/dist/types/src/components/Table/DynamicTable.d.ts +4 -1
  13. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +3 -1
  15. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Table/Relations.stories.d.ts +1 -0
  17. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Table/Table.d.ts +3 -9
  19. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  20. package/dist/types/src/components/Table/Table.stories.d.ts +2 -0
  21. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/TableContent.d.ts +1 -1
  23. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/TableContext.d.ts +10 -0
  25. package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
  26. package/dist/types/src/components/Table/TableToolbar.d.ts +1 -1
  27. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/index.d.ts +1 -0
  29. package/dist/types/src/components/Table/index.d.ts.map +1 -1
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/testing.d.ts +1 -1
  33. package/dist/types/src/components/testing.d.ts.map +1 -1
  34. package/dist/types/src/hooks/useProjectionModel.d.ts +1 -1
  35. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  36. package/dist/types/src/hooks/useTableModel.d.ts +8 -1
  37. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  38. package/dist/types/src/model/modal-controller.d.ts +1 -1
  39. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  40. package/dist/types/src/model/selection-model.d.ts +1 -1
  41. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  42. package/dist/types/src/model/table-model.d.ts +42 -7
  43. package/dist/types/src/model/table-model.d.ts.map +1 -1
  44. package/dist/types/src/model/table-presentation.d.ts +20 -5
  45. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  46. package/dist/types/src/model/table-view-state.d.ts +5 -1
  47. package/dist/types/src/model/table-view-state.d.ts.map +1 -1
  48. package/dist/types/src/types/index.d.ts.map +1 -1
  49. package/dist/types/src/util/dynamic-table.d.ts +1 -1
  50. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  51. package/dist/types/src/util/table-controls.d.ts +5 -3
  52. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +48 -50
  55. package/src/components/Table/DynamicTable.stories.tsx +59 -1
  56. package/src/components/Table/DynamicTable.tsx +16 -8
  57. package/src/components/Table/Relations.stories.tsx +2 -2
  58. package/src/components/Table/Table.stories.tsx +76 -2
  59. package/src/components/Table/Table.tsx +3 -15
  60. package/src/components/Table/TableContent.tsx +6 -5
  61. package/src/components/Table/TableContext.ts +21 -0
  62. package/src/components/Table/index.ts +2 -0
  63. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -3
  64. package/src/components/TableCellEditor/TableCellEditor.tsx +27 -22
  65. package/src/components/testing.ts +1 -1
  66. package/src/hooks/useProjectionModel.ts +1 -1
  67. package/src/hooks/useTableModel.ts +18 -6
  68. package/src/model/modal-controller.ts +1 -1
  69. package/src/model/selection-model.ts +1 -1
  70. package/src/model/table-model.test.ts +3 -3
  71. package/src/model/table-model.ts +148 -31
  72. package/src/model/table-presentation.test.ts +59 -1
  73. package/src/model/table-presentation.ts +171 -81
  74. package/src/model/table-view-state.ts +2 -2
  75. package/src/testing/useTestTableModel.ts +1 -1
  76. package/src/types/index.ts +4 -0
  77. package/src/util/dynamic-table.test.ts +1 -1
  78. package/src/util/dynamic-table.ts +1 -1
  79. package/src/util/table-buttons.ts +1 -1
  80. package/src/util/table-controls.ts +18 -10
  81. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  82. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-K4J2CKTZ.mjs +0 -1751
  84. package/dist/lib/browser/chunk-K4J2CKTZ.mjs.map +0 -7
  85. package/dist/lib/browser/chunk-VV3P3ADH.mjs +0 -67
  86. package/dist/lib/browser/chunk-VV3P3ADH.mjs.map +0 -7
  87. package/dist/lib/browser/index.mjs +0 -1416
  88. package/dist/lib/browser/index.mjs.map +0 -7
  89. package/dist/lib/browser/meta.json +0 -1
  90. package/dist/lib/browser/testing/index.mjs +0 -115
  91. package/dist/lib/browser/testing/index.mjs.map +0 -7
  92. package/dist/lib/browser/translations.mjs +0 -39
  93. package/dist/lib/browser/translations.mjs.map +0 -7
  94. package/dist/lib/browser/types/index.mjs +0 -8
  95. package/dist/lib/browser/types/index.mjs.map +0 -7
  96. package/dist/lib/node-esm/chunk-E6V274ZW.mjs +0 -1753
  97. package/dist/lib/node-esm/chunk-E6V274ZW.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  99. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs +0 -68
  101. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs.map +0 -7
  102. package/dist/lib/node-esm/index.mjs +0 -1417
  103. package/dist/lib/node-esm/index.mjs.map +0 -7
  104. package/dist/lib/node-esm/meta.json +0 -1
  105. package/dist/lib/node-esm/testing/index.mjs +0 -116
  106. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  107. package/dist/lib/node-esm/translations.mjs +0 -40
  108. package/dist/lib/node-esm/translations.mjs.map +0 -7
  109. package/dist/lib/node-esm/types/index.mjs +0 -9
  110. package/dist/lib/node-esm/types/index.mjs.map +0 -7
@@ -0,0 +1,1595 @@
1
+ import { Atom } from "@effect-atom/atom";
2
+ import { useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import { RegistryContext } from "@effect-atom/atom-react";
4
+ import { useAsyncEffect } from "@dxos/react-ui";
5
+ import { ViewState, useManagerOptional, useSelection, useSelectionActions } from "@dxos/react-ui-attention";
6
+ import { toPlaneCellIndex } from "@dxos/react-ui-grid";
7
+ import { mx } from "@dxos/ui-theme";
8
+ import { Resource } from "@dxos/context";
9
+ import { Format, Obj, Order, Query, QueryAST, Ref, Type } from "@dxos/echo";
10
+ import { toEffectSchema } from "@dxos/echo/JsonSchema";
11
+ import { getSnapshot } from "@dxos/echo/Obj";
12
+ import { SchemaEx } from "@dxos/effect";
13
+ import { invariant } from "@dxos/invariant";
14
+ import { EntityId } from "@dxos/keys";
15
+ import { cellClassesForFieldType, parseValue } from "@dxos/react-ui-form";
16
+ import { ProjectionModel, VIEW_FIELD_LIMIT, createEchoChangeCallback, formatForDisplay, formatForEditing, validateSchema } from "@dxos/schema";
17
+ import * as Schema from "effect/Schema";
18
+ import { accessoryHandlesPointerdownAttrs } from "@dxos/lit-grid";
19
+ import * as Predicate from "effect/Predicate";
20
+ import { Format as Format$1, TypeEnum } from "@dxos/echo/Format";
21
+ //#region src/util/query-ast.ts
22
+ /**
23
+ * Extracts order clauses from a query AST.
24
+ * Handles nested structures (options, order, etc.).
25
+ */
26
+ var extractOrder = (queryAst) => {
27
+ if (queryAst.type === "order") return queryAst.order;
28
+ if (queryAst.type === "options") return extractOrder(queryAst.query);
29
+ if (queryAst.type === "from") return extractOrder(queryAst.query);
30
+ };
31
+ //#endregion
32
+ //#region src/util/sort.ts
33
+ /**
34
+ * Gets a normalized sort value for a given value and format.
35
+ * This ensures consistent sorting behavior across different data types.
36
+ */
37
+ var getSortValue = (value, format) => {
38
+ if (value === void 0 || value === null) return null;
39
+ if (format === Format.TypeFormat.DateTime || format === Format.TypeFormat.Date) {
40
+ const date = new Date(value);
41
+ if (!isNaN(date.getTime())) return date.getTime();
42
+ return String(value);
43
+ }
44
+ if (format === Format.TypeFormat.Number || format === Format.TypeFormat.Integer || format === Format.TypeFormat.Currency || format === Format.TypeFormat.Percent || format === Format.TypeFormat.Duration) {
45
+ const num = typeof value === "number" ? value : Number(value);
46
+ if (!isNaN(num)) return num;
47
+ return String(value);
48
+ }
49
+ if (typeof value === "string") return value;
50
+ if (typeof value === "number") return value;
51
+ return String(value);
52
+ };
53
+ /**
54
+ * Compares two values for sorting based on the field format.
55
+ * Handles dates, numbers, and strings appropriately.
56
+ */
57
+ var compareValues = (aValue, bValue, format, direction) => {
58
+ if (aValue === void 0 || aValue === null) return bValue === void 0 || bValue === null ? 0 : 1;
59
+ if (bValue === void 0 || bValue === null) return -1;
60
+ const aSortValue = getSortValue(aValue, format);
61
+ const bSortValue = getSortValue(bValue, format);
62
+ let comparison = 0;
63
+ if (typeof aSortValue === "number" && typeof bSortValue === "number") comparison = aSortValue - bSortValue;
64
+ else if (typeof aSortValue === "string" && typeof bSortValue === "string") comparison = aSortValue.localeCompare(bSortValue);
65
+ else comparison = String(aSortValue).localeCompare(String(bSortValue));
66
+ return direction === "desc" ? -comparison : comparison;
67
+ };
68
+ //#endregion
69
+ //#region src/util/table-buttons.ts
70
+ var BUTTON_IDENTIFIERS = {
71
+ columnSettings: "data-table-column-settings-button",
72
+ newColumn: "data-table-new-column-button",
73
+ referencedCell: "data-table-ref-cell-button",
74
+ rowMenu: "data-table-row-menu-button",
75
+ saveDraftRow: "data-table-save-draft-row-button",
76
+ sort: "data-table-sort-button"
77
+ };
78
+ var createButton = ({ attr, data, disabled = false, icon, testId, type: _type = "primary" }) => {
79
+ return `<div role="none" class="dx-grid__cell__block"><button ${attr} data-testid="${testId}" class="dx-button w-6 aspect-square px-0.5 min-h-0" ${Object.entries(data).map(([k, v]) => `${k}="${v}"`).join(" ")} ${disabled ? "disabled" : ""} data-dx-grid-action="accessory"><svg data-size="4"><use href="/icons.svg#${icon}"/></svg></button></div>`;
80
+ };
81
+ var addColumnButton = {
82
+ attr: BUTTON_IDENTIFIERS.newColumn,
83
+ icon: "ph--plus--regular",
84
+ render: ({ disabled }) => {
85
+ return createButton({
86
+ attr: BUTTON_IDENTIFIERS.newColumn,
87
+ icon: addColumnButton.icon,
88
+ disabled,
89
+ data: {},
90
+ testId: "table-new-column-button"
91
+ });
92
+ },
93
+ getData: (_el) => ({ type: "newColumn" })
94
+ };
95
+ var columnSettingsButton = {
96
+ attr: BUTTON_IDENTIFIERS.columnSettings,
97
+ icon: "ph--caret-down--regular",
98
+ render: ({ fieldId }) => {
99
+ return createButton({
100
+ attr: BUTTON_IDENTIFIERS.columnSettings,
101
+ icon: columnSettingsButton.icon,
102
+ data: {
103
+ "data-field-id": fieldId,
104
+ "data-variant": "ghost"
105
+ },
106
+ testId: "table-column-settings-button"
107
+ });
108
+ },
109
+ getData: (el) => ({
110
+ type: "columnSettings",
111
+ fieldId: el.getAttribute("data-field-id")
112
+ })
113
+ };
114
+ var rowMenuButton = {
115
+ attr: BUTTON_IDENTIFIERS.rowMenu,
116
+ icon: "ph--dots-three--regular",
117
+ render: ({ rowIndex }) => {
118
+ return createButton({
119
+ attr: BUTTON_IDENTIFIERS.rowMenu,
120
+ icon: rowMenuButton.icon,
121
+ data: { "data-row-index": rowIndex.toString() },
122
+ testId: "table-row-menu-button"
123
+ });
124
+ },
125
+ getData: (el) => ({
126
+ type: "rowMenu",
127
+ rowIndex: Number(el.getAttribute("data-row-index"))
128
+ })
129
+ };
130
+ var sortButton = {
131
+ attr: BUTTON_IDENTIFIERS.sort,
132
+ icon: "ph--sort-ascending--regular",
133
+ render: ({ fieldId, direction }) => {
134
+ return createButton({
135
+ attr: BUTTON_IDENTIFIERS.sort,
136
+ icon: direction === "desc" ? "ph--sort-descending--regular" : "ph--sort-ascending--regular",
137
+ data: {
138
+ "data-field-id": fieldId,
139
+ "data-direction": direction ?? ""
140
+ },
141
+ testId: "table-sort-button",
142
+ type: "secondary"
143
+ });
144
+ },
145
+ getData: (el) => ({
146
+ type: "sort",
147
+ fieldId: el.getAttribute("data-field-id"),
148
+ direction: el.getAttribute("data-direction")
149
+ })
150
+ };
151
+ var saveDraftRowButton = {
152
+ attr: BUTTON_IDENTIFIERS.saveDraftRow,
153
+ icon: "ph--floppy-disk--regular",
154
+ render: ({ rowIndex, disabled }) => {
155
+ return createButton({
156
+ attr: BUTTON_IDENTIFIERS.saveDraftRow,
157
+ icon: saveDraftRowButton.icon,
158
+ disabled,
159
+ data: {
160
+ "data-row-index": rowIndex.toString(),
161
+ "data-variant": "primary"
162
+ },
163
+ testId: "table-save-draft-row-button"
164
+ });
165
+ },
166
+ getData: (el) => ({
167
+ type: "saveDraftRow",
168
+ rowIndex: Number(el.getAttribute("data-row-index"))
169
+ })
170
+ };
171
+ var tableButtons = {
172
+ addColumn: addColumnButton,
173
+ columnSettings: columnSettingsButton,
174
+ rowMenu: rowMenuButton,
175
+ saveDraftRow: saveDraftRowButton,
176
+ sort: sortButton
177
+ };
178
+ //#endregion
179
+ //#region src/util/table-controls.ts
180
+ var CONTROL_IDENTIFIERS = {
181
+ checkbox: "data-table-checkbox",
182
+ switch: "data-table-switch"
183
+ };
184
+ var BASE_CLASSES = {
185
+ checkbox: "dx-checkbox",
186
+ switch: "dx-checkbox--switch"
187
+ };
188
+ var renderAttributes = (data) => {
189
+ return Object.entries(data).map(([k, v]) => `${k}="${v}"`).join(" ");
190
+ };
191
+ var renderInput = (baseClass, attrs, checked = false, disabled = false, preventToggle = false, testId) => {
192
+ return `<div role="none" class="dx-grid__cell__block"><input type="checkbox" class="${baseClass}" ${renderAttributes(attrs)} ${checked ? "checked" : ""} ${preventToggle ? "onclick=\"return false\"" : ""} ${disabled ? "disabled" : ""} data-testid="${testId}" data-dx-grid-action="accessory"/></div>`;
193
+ };
194
+ var CheckboxComponent = ({ rowIndex, header = false, checked = false, disabled = false }) => {
195
+ const attrs = {
196
+ [CONTROL_IDENTIFIERS.checkbox]: "",
197
+ "data-row-index": rowIndex.toString(),
198
+ ...header && { "data-header": "" }
199
+ };
200
+ return renderInput(BASE_CLASSES.checkbox, attrs, checked, disabled, true, "table-selection");
201
+ };
202
+ var SwitchComponent = ({ colIndex, rowIndex, plane, checked = false, disabled = false }) => {
203
+ const attrs = {
204
+ [CONTROL_IDENTIFIERS.switch]: "",
205
+ "data-row-index": rowIndex.toString(),
206
+ "data-col-index": colIndex.toString(),
207
+ "data-plane": plane,
208
+ ...accessoryHandlesPointerdownAttrs
209
+ };
210
+ return renderInput(BASE_CLASSES.switch, attrs, checked, disabled, true, "table-switch");
211
+ };
212
+ var tableControls = {
213
+ checkbox: {
214
+ attr: CONTROL_IDENTIFIERS.checkbox,
215
+ render: CheckboxComponent,
216
+ getData: (el) => ({
217
+ type: "checkbox",
218
+ rowIndex: Number(el.getAttribute("data-row-index")),
219
+ header: el.hasAttribute("data-header")
220
+ })
221
+ },
222
+ switch: {
223
+ attr: CONTROL_IDENTIFIERS.switch,
224
+ render: SwitchComponent,
225
+ getData: (el) => ({
226
+ type: "switch",
227
+ rowIndex: Number(el.getAttribute("data-row-index")),
228
+ colIndex: Number(el.getAttribute("data-col-index")),
229
+ plane: el.getAttribute("data-plane") ?? "grid"
230
+ })
231
+ }
232
+ };
233
+ //#endregion
234
+ //#region src/util/tag.ts
235
+ /**
236
+ * Regex pattern for matching tags in the format [Label](id)
237
+ */
238
+ var TAG_PATTERN_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
239
+ /**
240
+ * Extracts tag IDs from a string in the format [Label](id)
241
+ * @param value The string to extract IDs from
242
+ * @returns Array of extracted IDs or undefined if no matches or value is not a string
243
+ */
244
+ var extractTagIds = (value) => {
245
+ if (typeof value !== "string") return;
246
+ const ids = [];
247
+ let match;
248
+ while ((match = TAG_PATTERN_REGEX.exec(value)) !== null) if (match[2]) ids.push(match[2]);
249
+ return ids.length > 0 ? ids : void 0;
250
+ };
251
+ //#endregion
252
+ //#region src/model/selection-model.ts
253
+ var SelectionModel = class extends Resource {
254
+ _registry;
255
+ _rows;
256
+ _selection;
257
+ _validSelectedIds;
258
+ _hasSelection;
259
+ _allSelected;
260
+ _selectionMode;
261
+ _onSelectionChanged;
262
+ constructor(registry, rows, selectionMode, initialSelection, onSelectionChanged) {
263
+ super();
264
+ this._registry = registry;
265
+ this._rows = rows;
266
+ this._selectionMode = selectionMode;
267
+ this._onSelectionChanged = onSelectionChanged;
268
+ this._selection = Atom.make(new Set(initialSelection));
269
+ this._validSelectedIds = Atom.make((get) => {
270
+ const rows = get(this._rows);
271
+ if (!rows) return /* @__PURE__ */ new Set();
272
+ const validIds = new Set(rows.map((row) => row.id));
273
+ const selection = get(this._selection);
274
+ return new Set([...selection].filter((id) => validIds.has(id)));
275
+ });
276
+ this._hasSelection = Atom.make((get) => get(this._validSelectedIds).size > 0);
277
+ this._allSelected = Atom.make((get) => {
278
+ const rows = get(this._rows);
279
+ if (rows.length === 0) return false;
280
+ const selection = get(this._selection);
281
+ return rows.every((row) => selection.has(row.id));
282
+ });
283
+ }
284
+ async _open() {
285
+ const selectionUnsubscribe = this._registry.subscribe(this._selection, () => {
286
+ this._onSelectionChanged?.();
287
+ });
288
+ this._ctx.onDispose(selectionUnsubscribe);
289
+ }
290
+ /** Get the selection atom for reactive access. */
291
+ get selectionAtom() {
292
+ return this._selection;
293
+ }
294
+ /** Get the current selection value. */
295
+ get selection() {
296
+ return this._registry.get(this._selection);
297
+ }
298
+ /** Get the hasSelection atom for reactive access. */
299
+ get hasSelectionAtom() {
300
+ return this._hasSelection;
301
+ }
302
+ /** Check if there is any selection. */
303
+ get hasSelection() {
304
+ return this._registry.get(this._hasSelection);
305
+ }
306
+ /** Get the allRowsSelected atom for reactive access. */
307
+ get allRowsSelectedAtom() {
308
+ return this._allSelected;
309
+ }
310
+ /** Check if all rows are selected. */
311
+ get allRowsSelected() {
312
+ return this._registry.get(this._allSelected);
313
+ }
314
+ get selectionMode() {
315
+ return this._selectionMode;
316
+ }
317
+ getSelectedRows = () => {
318
+ const selectedIds = this._registry.get(this._validSelectedIds);
319
+ return this._registry.get(this._rows).filter((row) => selectedIds.has(row.id));
320
+ };
321
+ isRowIndexSelected = (rowIndex) => {
322
+ const row = this._registry.get(this._rows)[rowIndex];
323
+ return this._registry.get(this._selection).has(row.id);
324
+ };
325
+ isObjectSelected = (object) => {
326
+ return this._registry.get(this._selection).has(object.id);
327
+ };
328
+ toggleSelectionForRowIndex = (rowIndex) => {
329
+ const row = this._registry.get(this._rows)[rowIndex];
330
+ const currentSelection = this._registry.get(this._selection);
331
+ const newSelection = new Set(currentSelection);
332
+ if (newSelection.has(row.id)) newSelection.delete(row.id);
333
+ else {
334
+ if (this._selectionMode === "single") newSelection.clear();
335
+ newSelection.add(row.id);
336
+ }
337
+ this._registry.set(this._selection, newSelection);
338
+ };
339
+ setSelection = (mode) => {
340
+ switch (mode) {
341
+ case "all":
342
+ this._registry.set(this._selection, new Set(this._registry.get(this._rows).map((row) => row.id)));
343
+ break;
344
+ case "none":
345
+ this._registry.set(this._selection, /* @__PURE__ */ new Set());
346
+ break;
347
+ }
348
+ };
349
+ };
350
+ //#endregion
351
+ //#region src/model/table-view-state.ts
352
+ /**
353
+ * Field sort configuration.
354
+ */
355
+ var FieldSortSchema = Schema.Struct({
356
+ fieldId: Schema.String,
357
+ direction: QueryAST.OrderDirection
358
+ });
359
+ /**
360
+ * Per-user column sort, scoped to a table by its object URI. Stored in the `local` backend so a
361
+ * viewer's chosen sort survives remounts and reloads without writing to the shared view query
362
+ * (which `TableModel.saveView` does explicitly).
363
+ */
364
+ var tableSortAspect = ViewState.define({
365
+ key: "table-sort",
366
+ backend: "local",
367
+ schema: Schema.UndefinedOr(FieldSortSchema),
368
+ defaultValue: () => void 0
369
+ });
370
+ //#endregion
371
+ //#region src/model/table-model.ts
372
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-table/src/model/table-model.ts";
373
+ /**
374
+ * Creates a change callback for ECHO-backed table and rows.
375
+ * Use this when the table and rows are stored in the ECHO database.
376
+ */
377
+ var createEchoChangeCallback$1 = (table) => ({
378
+ table: (mutate) => Obj.update(table, (table) => mutate(table)),
379
+ row: (row, mutate) => {
380
+ if (Obj.isObject(row)) Obj.update(row, (row) => mutate(row));
381
+ else mutate(row);
382
+ }
383
+ });
384
+ /**
385
+ * Creates a change callback that directly mutates objects without wrapping.
386
+ * Use this for plain JavaScript objects (tests, non-ECHO scenarios).
387
+ */
388
+ var createDirectChangeCallback = (table) => ({
389
+ table: (mutate) => mutate(table),
390
+ row: (row, mutate) => mutate(row)
391
+ });
392
+ var defaultFeatures = {
393
+ selection: {
394
+ enabled: true,
395
+ mode: "multiple"
396
+ },
397
+ dataEditable: false,
398
+ schemaEditable: false,
399
+ pinColumns: 0
400
+ };
401
+ var TableModel = class extends Resource {
402
+ _registry;
403
+ _object;
404
+ _projection;
405
+ _db;
406
+ _change;
407
+ _visibleRange;
408
+ _onInsertRow;
409
+ _onDeleteRows;
410
+ _onColumnDelete;
411
+ _onCellUpdate;
412
+ _onRowOrderChange;
413
+ _onRowAction;
414
+ _rowActions;
415
+ _features;
416
+ _rows;
417
+ _draftRows;
418
+ _viewState;
419
+ _inMemorySort;
420
+ _persistedSort;
421
+ _sorting;
422
+ _sortedRows;
423
+ _viewDirty;
424
+ _pinnedRows;
425
+ _selection;
426
+ _columnMeta;
427
+ _cellUpdateCounter;
428
+ constructor({ registry, object, projection, db, viewState, change, features = {}, initialSelection = [], pinnedRows = {
429
+ top: [],
430
+ bottom: []
431
+ }, rowActions = [], onCellUpdate, onColumnDelete, onDeleteRows, onInsertRow, onRowOrderChange, onRowAction }) {
432
+ super();
433
+ this._registry = registry;
434
+ this._object = object;
435
+ this._viewState = viewState;
436
+ this._projection = projection;
437
+ this._projection.normalizeView();
438
+ this._db = db;
439
+ this._change = change ?? createEchoChangeCallback$1(object);
440
+ this._features = {
441
+ ...defaultFeatures,
442
+ ...features
443
+ };
444
+ invariant(!(this._features.dataEditable && this._features.selection.enabled && this._features.selection.mode === "single"), "Single selection is not compatible with editable tables.", {
445
+ "~LogMeta": "~LogMeta",
446
+ F: __dxlog_file,
447
+ L: 208,
448
+ S: this,
449
+ A: ["!(this._features.dataEditable && this._features.selection.enabled && this._features.selection.mode === 'single')", "'Single selection is not compatible with editable tables.'"]
450
+ });
451
+ this._visibleRange = Atom.make({
452
+ start: {
453
+ row: 0,
454
+ col: 0
455
+ },
456
+ end: {
457
+ row: 0,
458
+ col: 0
459
+ }
460
+ });
461
+ this._rows = Atom.make([]);
462
+ this._draftRows = Atom.make([]);
463
+ this._inMemorySort = this._viewState ? this._viewState.atom(tableSortAspect, this.id) : Atom.make(void 0);
464
+ this._cellUpdateCounter = Atom.make(0);
465
+ this._persistedSort = Atom.make((get) => {
466
+ const view = get(this._object.view.atom);
467
+ if (!view) return;
468
+ const ast = Obj.getSnapshot(view).query.ast;
469
+ const orders = extractOrder(ast);
470
+ if (orders && orders.length > 0) {
471
+ const firstOrder = orders[0];
472
+ if (firstOrder.kind === "property") {
473
+ const field = this._projection.getFields().find((f) => f.path === firstOrder.property);
474
+ if (field) return {
475
+ fieldId: field.id,
476
+ direction: firstOrder.direction
477
+ };
478
+ }
479
+ }
480
+ });
481
+ this._sorting = Atom.make((get) => {
482
+ return get(this._inMemorySort) ?? get(this._persistedSort);
483
+ });
484
+ this._sortedRows = Atom.make((get) => {
485
+ const rows = get(this._rows);
486
+ const sort = get(this._sorting);
487
+ if (!sort || rows.length === 0) return rows;
488
+ const field = this._projection.getFields().find((f) => f.id === sort.fieldId);
489
+ if (!field) return rows;
490
+ const fieldProjection = this._projection.getFieldProjection(field.id);
491
+ return [...rows].sort((a, b) => {
492
+ return compareValues(SchemaEx.getValue(a, field.path), SchemaEx.getValue(b, field.path), fieldProjection.props.format, sort.direction);
493
+ });
494
+ });
495
+ this._selection = new SelectionModel(this._registry, this._sortedRows, this._features.selection.mode ?? "multiple", initialSelection, () => this._onRowOrderChange?.());
496
+ this._viewDirty = Atom.make((get) => {
497
+ const inMemorySort = get(this._inMemorySort);
498
+ if (!inMemorySort) return false;
499
+ const persisted = get(this._persistedSort);
500
+ if (!persisted) return true;
501
+ return inMemorySort.fieldId !== persisted.fieldId || inMemorySort.direction !== persisted.direction;
502
+ });
503
+ this._pinnedRows = pinnedRows;
504
+ this._rowActions = rowActions;
505
+ this._onInsertRow = onInsertRow;
506
+ this._onDeleteRows = onDeleteRows;
507
+ this._onColumnDelete = onColumnDelete;
508
+ this._onCellUpdate = onCellUpdate;
509
+ this._onRowOrderChange = onRowOrderChange;
510
+ this._onRowAction = onRowAction;
511
+ }
512
+ get id() {
513
+ return Obj.getURI(this._object);
514
+ }
515
+ get view() {
516
+ const view = this._object.view.target;
517
+ invariant(view, "Table model not initialized", {
518
+ "~LogMeta": "~LogMeta",
519
+ F: __dxlog_file,
520
+ L: 328,
521
+ S: this,
522
+ A: ["view", "'Table model not initialized'"]
523
+ });
524
+ return view;
525
+ }
526
+ get table() {
527
+ return this._object;
528
+ }
529
+ get projection() {
530
+ return this._projection;
531
+ }
532
+ get db() {
533
+ return this._db;
534
+ }
535
+ /**
536
+ * Atom for reactive access to rows with in-memory sorting applied.
537
+ */
538
+ get rows() {
539
+ return this._sortedRows;
540
+ }
541
+ /**
542
+ * Gets rows with in-memory sorting applied.
543
+ * In-memory sort is local to this instance until saved.
544
+ */
545
+ getRows() {
546
+ return this._registry.get(this._sortedRows);
547
+ }
548
+ /** Atom for reactive access to cell changes. */
549
+ get cellUpdate() {
550
+ return this._cellUpdateCounter;
551
+ }
552
+ /**
553
+ * Reports the grid's currently-visible cell range (called by the presentation from `getCells`). Drives the
554
+ * per-row subscriptions that keep cells reactive to external mutations, so it must be wired for the grid to
555
+ * reflect edits made outside it. Guarded so identical ranges don't churn the subscriptions.
556
+ */
557
+ setVisibleRange(range) {
558
+ const current = this._registry.get(this._visibleRange);
559
+ if (current.start.row === range.start.row && current.end.row === range.end.row && current.start.col === range.start.col && current.end.col === range.end.col) return;
560
+ this._registry.set(this._visibleRange, range);
561
+ }
562
+ /**
563
+ * Change a row using the configured change callback.
564
+ * Use this instead of directly mutating to ensure consistent mutation handling.
565
+ */
566
+ changeRow(row, mutate) {
567
+ this._change.row(row, mutate);
568
+ }
569
+ get features() {
570
+ return this._features;
571
+ }
572
+ /**
573
+ * Number of leading data columns pinned (frozen), clamped to the available field count.
574
+ */
575
+ get pinColumns() {
576
+ const fieldCount = this.getColumnCount();
577
+ return Math.max(0, Math.min(this._features.pinColumns, fieldCount));
578
+ }
579
+ /**
580
+ * Number of frozen-start columns preceding the pinned data columns (the selection column, if enabled).
581
+ */
582
+ get selectionColumns() {
583
+ return this._features.selection.enabled ? 1 : 0;
584
+ }
585
+ /**
586
+ * Total number of columns rendered in the `frozenColsStart` plane: the selection column (if enabled)
587
+ * plus the pinned data columns. Single source of truth for the frozen-start boundary.
588
+ */
589
+ get frozenColsStart() {
590
+ return this.selectionColumns + this.pinColumns;
591
+ }
592
+ /**
593
+ * Maps a plane-local column index to a field index for the data-bearing planes.
594
+ * Returns undefined for non-data columns (e.g. the selection or action columns).
595
+ *
596
+ * Column layout across planes:
597
+ * - `frozenColsStart`: [selection?] then the first `pinColumns` data fields.
598
+ * - `grid` / `frozenRowsStart` / `frozenRowsEnd`: data fields starting at index `pinColumns`.
599
+ */
600
+ getFieldIndex(plane, planeCol) {
601
+ switch (plane) {
602
+ case "frozenColsStart":
603
+ case "fixedStartStart":
604
+ case "fixedEndStart": {
605
+ const dataCol = planeCol - this.selectionColumns;
606
+ if (dataCol < 0 || dataCol >= this.pinColumns) return;
607
+ return dataCol;
608
+ }
609
+ case "grid":
610
+ case "frozenRowsStart":
611
+ case "frozenRowsEnd": return this.pinColumns + planeCol;
612
+ default: return;
613
+ }
614
+ }
615
+ /**
616
+ * Gets the current sort state.
617
+ * Priority: in-memory sort (local changes) > persisted sort (from query AST).
618
+ */
619
+ getSorting() {
620
+ return this._registry.get(this._sorting);
621
+ }
622
+ /**
623
+ * Gets the row data at the specified display index.
624
+ */
625
+ getRowAt(displayIndex) {
626
+ const sortedRows = this._registry.get(this._sortedRows);
627
+ if (displayIndex < 0 || displayIndex >= sortedRows.length) return;
628
+ return sortedRows[displayIndex];
629
+ }
630
+ get pinnedRows() {
631
+ return this._pinnedRows;
632
+ }
633
+ get columnMeta() {
634
+ invariant(this._columnMeta, void 0, {
635
+ "~LogMeta": "~LogMeta",
636
+ F: __dxlog_file,
637
+ L: 470,
638
+ S: this,
639
+ A: ["this._columnMeta", ""]
640
+ });
641
+ return this._columnMeta;
642
+ }
643
+ get selection() {
644
+ return this._selection;
645
+ }
646
+ get rowActions() {
647
+ return this._rowActions;
648
+ }
649
+ async _open() {
650
+ await this._object.view.load();
651
+ this.initializeColumnMeta();
652
+ this.initializeEffects();
653
+ await this._selection.open(this._ctx);
654
+ }
655
+ initializeColumnMeta() {
656
+ invariant(this._object.view.target, "View must be loaded before initializing column meta", {
657
+ "~LogMeta": "~LogMeta",
658
+ F: __dxlog_file,
659
+ L: 494,
660
+ S: this,
661
+ A: ["this._object.view.target", "'View must be loaded before initializing column meta'"]
662
+ });
663
+ this._columnMeta = Atom.make((get) => {
664
+ const fields = get(this._projection.fields);
665
+ const pin = this.pinColumns;
666
+ const selectionColumns = this.selectionColumns;
667
+ const fieldSize = (field) => ({
668
+ size: this.table.sizes[field.path] ?? 256,
669
+ resizeable: true
670
+ });
671
+ const grid = Object.fromEntries(fields.slice(pin).map((field, index) => [index, fieldSize(field)]));
672
+ const frozenColsStart = {};
673
+ if (selectionColumns > 0) frozenColsStart[0] = {
674
+ size: 32,
675
+ resizeable: false
676
+ };
677
+ fields.slice(0, pin).forEach((field, index) => {
678
+ frozenColsStart[selectionColumns + index] = fieldSize(field);
679
+ });
680
+ return {
681
+ grid,
682
+ frozenColsStart,
683
+ frozenColsEnd: { 0: {
684
+ size: 32,
685
+ resizeable: false
686
+ } }
687
+ };
688
+ });
689
+ }
690
+ initializeEffects() {
691
+ const rowsUnsubscribe = this._registry.subscribe(this._rows, () => {
692
+ this._onRowOrderChange?.();
693
+ });
694
+ this._ctx.onDispose(rowsUnsubscribe);
695
+ const rowSubscriptions = /* @__PURE__ */ new Map();
696
+ const subscribeVisibleRows = () => {
697
+ for (const [id, unsub] of rowSubscriptions) {
698
+ unsub();
699
+ rowSubscriptions.delete(id);
700
+ }
701
+ const { start, end } = this._registry.get(this._visibleRange);
702
+ const sortedRows = this._registry.get(this._sortedRows);
703
+ for (let row = start.row; row <= end.row && row < sortedRows.length; row++) {
704
+ const obj = sortedRows[row];
705
+ if (Obj.isObject(obj) && !rowSubscriptions.has(obj.id)) {
706
+ const cell = {
707
+ row,
708
+ col: start.col,
709
+ plane: "grid"
710
+ };
711
+ const unsub = Obj.subscribe(obj, () => {
712
+ this._registry.set(this._cellUpdateCounter, this._registry.get(this._cellUpdateCounter) + 1);
713
+ this._onCellUpdate?.(cell);
714
+ });
715
+ rowSubscriptions.set(obj.id, unsub);
716
+ }
717
+ }
718
+ };
719
+ const visibleRangeUnsubscribe = this._registry.subscribe(this._visibleRange, subscribeVisibleRows);
720
+ const sortedRowsUnsubscribe = this._registry.subscribe(this._sortedRows, subscribeVisibleRows);
721
+ this._ctx.onDispose(() => {
722
+ visibleRangeUnsubscribe();
723
+ sortedRowsUnsubscribe();
724
+ for (const unsub of rowSubscriptions.values()) unsub();
725
+ });
726
+ const draftRowsUnsubscribe = this._registry.subscribe(this._draftRows, () => {
727
+ this._onRowOrderChange?.();
728
+ });
729
+ this._ctx.onDispose(draftRowsUnsubscribe);
730
+ }
731
+ setRows = (obj) => {
732
+ this._registry.set(this._rows, obj);
733
+ };
734
+ getRowCount = () => this._registry.get(this._sortedRows).length;
735
+ getDraftRowCount() {
736
+ return this._registry.get(this._draftRows).length;
737
+ }
738
+ /** Atom for reactive access to draft rows. */
739
+ get draftRows() {
740
+ return this._draftRows;
741
+ }
742
+ /** Gets the current draft rows. */
743
+ getDraftRows() {
744
+ return this._registry.get(this._draftRows);
745
+ }
746
+ /**
747
+ * Checks if a specific field path has validation errors for a given draft row.
748
+ * @param draftRowIndex - The index of the draft row to check
749
+ * @param fieldPath - The path of the field to check for validation errors
750
+ * @returns true if the field has validation errors, false otherwise
751
+ */
752
+ hasDraftRowValidationError(draftRowIndex, fieldPath) {
753
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) return false;
754
+ return (this._registry.get(this._draftRows)[draftRowIndex].validationErrors || []).some((error) => error.path === fieldPath);
755
+ }
756
+ getColumnCount = () => this._projection?.getFields().length ?? 0;
757
+ insertRow = () => {
758
+ const result = this._onInsertRow?.();
759
+ if (result === "draft" && this._registry.get(this._draftRows).length === 0) this.createDraftRow();
760
+ return result ?? "final";
761
+ };
762
+ createDraftRow() {
763
+ if (!this._projection) return;
764
+ const draftData = { id: EntityId.random() };
765
+ const validationErrors = this.validateDraftRowData(draftData);
766
+ const draftRow = {
767
+ data: draftData,
768
+ valid: validationErrors.length === 0,
769
+ validationErrors
770
+ };
771
+ this._registry.set(this._draftRows, [...this._registry.get(this._draftRows), draftRow]);
772
+ }
773
+ validateDraftRow(draftRowIndex) {
774
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) return;
775
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
776
+ const validationErrors = this.validateDraftRowData(draftRow.data);
777
+ const updatedDraftRow = {
778
+ ...draftRow,
779
+ valid: validationErrors.length === 0,
780
+ validationErrors
781
+ };
782
+ const newDraftRows = [...this._registry.get(this._draftRows)];
783
+ newDraftRows[draftRowIndex] = updatedDraftRow;
784
+ this._registry.set(this._draftRows, newDraftRows);
785
+ }
786
+ validateDraftRowData(data) {
787
+ return validateSchema(toEffectSchema(this._projection.baseSchema), data) || [];
788
+ }
789
+ commitDraftRow = (draftRowIndex) => {
790
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) return false;
791
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
792
+ if (!draftRow.valid) return false;
793
+ const insertRowResult = this._onInsertRow?.(draftRow.data);
794
+ if (insertRowResult === "final") {
795
+ const newDraftRows = this._registry.get(this._draftRows).filter((_, index) => index !== draftRowIndex);
796
+ this._registry.set(this._draftRows, newDraftRows);
797
+ }
798
+ return insertRowResult === "final";
799
+ };
800
+ deleteRow = (rowIndex) => {
801
+ const obj = this._registry.get(this._sortedRows)[rowIndex];
802
+ const objectsToDelete = [];
803
+ if (this.features.selection.enabled && this._selection.hasSelection) {
804
+ const selectedRows = this._selection.getSelectedRows();
805
+ objectsToDelete.push(...selectedRows);
806
+ } else if (obj) objectsToDelete.push(obj);
807
+ if (objectsToDelete.length > 0) this._onDeleteRows?.(rowIndex, objectsToDelete);
808
+ };
809
+ handleRowAction = (actionId, rowIndex) => {
810
+ if (!this._onRowAction) return;
811
+ const data = this._registry.get(this._sortedRows)[rowIndex];
812
+ if (data) this._onRowAction(actionId, data);
813
+ };
814
+ getCellData = (cell) => {
815
+ const { col, row, plane } = cell;
816
+ const fields = this._projection?.getFields() ?? [];
817
+ const fieldIndex = this.getFieldIndex(plane, col);
818
+ if (fieldIndex === void 0 || fieldIndex < 0 || fieldIndex >= fields.length) return;
819
+ const field = fields[fieldIndex];
820
+ let value;
821
+ if (plane === "frozenRowsEnd") if (row >= 0 && row < this._registry.get(this._draftRows).length) value = SchemaEx.getValue(this._registry.get(this._draftRows)[row].data, field.path);
822
+ else return;
823
+ else {
824
+ const sortedRows = this._registry.get(this._sortedRows);
825
+ value = SchemaEx.getValue(sortedRows[row], field.path);
826
+ }
827
+ if (value == null) return "";
828
+ const { props } = this._projection.getFieldProjection(field.id);
829
+ switch (props.format) {
830
+ case Format.TypeFormat.Ref:
831
+ if (!field.referencePath) return "";
832
+ return SchemaEx.getValue(value.target, field.referencePath);
833
+ default: return formatForEditing({
834
+ type: props.type,
835
+ format: props.format,
836
+ value
837
+ });
838
+ }
839
+ };
840
+ validateCellData = async ({ col, row, plane }, value) => {
841
+ const fields = this._projection?.getFields() ?? [];
842
+ const fieldIndex = this.getFieldIndex(plane, col);
843
+ if (fieldIndex === void 0 || fieldIndex < 0 || fieldIndex >= fields.length) return {
844
+ valid: false,
845
+ error: "Invalid column index"
846
+ };
847
+ const field = fields[fieldIndex];
848
+ const { props } = this._projection.getFieldProjection(field.id);
849
+ const transformedValue = editorTextToCellValue(props, value);
850
+ if (plane === "frozenRowsEnd") {
851
+ if (row < 0 || row >= this._registry.get(this._draftRows).length) return {
852
+ valid: false,
853
+ error: "Invalid draft row index"
854
+ };
855
+ const snapshot = { ...this._registry.get(this._draftRows)[row].data };
856
+ SchemaEx.setValue(snapshot, field.path, transformedValue);
857
+ const validationErrors = this.validateDraftRowData(snapshot);
858
+ if (validationErrors.length > 0) {
859
+ const error = validationErrors.find((err) => err.path === field.path);
860
+ if (error) return {
861
+ valid: false,
862
+ error: error.message
863
+ };
864
+ }
865
+ return { valid: true };
866
+ } else {
867
+ const currentRow = this._registry.get(this._rows)[row];
868
+ invariant(currentRow, "Invalid row index", {
869
+ "~LogMeta": "~LogMeta",
870
+ F: __dxlog_file,
871
+ L: 813,
872
+ S: this,
873
+ A: ["currentRow", "'Invalid row index'"]
874
+ });
875
+ const snapshot = { ...getSnapshot(currentRow) };
876
+ SchemaEx.setValue(snapshot, field.path, transformedValue);
877
+ const type = Obj.getType(currentRow);
878
+ invariant(type, void 0, {
879
+ "~LogMeta": "~LogMeta",
880
+ F: __dxlog_file,
881
+ L: 820,
882
+ S: this,
883
+ A: ["type", ""]
884
+ });
885
+ const validationResult = validateSchema(Type.getSchema(type), snapshot);
886
+ if (validationResult && validationResult.length > 0) {
887
+ const error = validationResult.find((err) => err.path === field.path);
888
+ if (error) return {
889
+ valid: false,
890
+ error: error.message
891
+ };
892
+ }
893
+ return { valid: true };
894
+ }
895
+ };
896
+ setCellData = (cell, value) => {
897
+ const { col, row, plane } = cell;
898
+ const fields = this._projection?.getFields() ?? [];
899
+ const fieldIndex = this.getFieldIndex(plane, col);
900
+ if (fieldIndex === void 0 || fieldIndex < 0 || fieldIndex >= fields.length) return;
901
+ const field = fields[fieldIndex];
902
+ const { props } = this._projection.getFieldProjection(field.id);
903
+ const transformedValue = editorTextToCellValue(props, value);
904
+ if (props.format === Format.TypeFormat.Ref && !Obj.isObject(value)) return;
905
+ if (plane === "frozenRowsEnd") {
906
+ if (row >= 0 && row < this._registry.get(this._draftRows).length) {
907
+ SchemaEx.setValue(this._registry.get(this._draftRows)[row].data, field.path, transformedValue);
908
+ this.validateDraftRow(row);
909
+ }
910
+ } else {
911
+ const rowData = this._registry.get(this._sortedRows)[row];
912
+ this._change.row(rowData, (mutableRow) => {
913
+ SchemaEx.setValue(mutableRow, field.path, transformedValue);
914
+ });
915
+ }
916
+ };
917
+ /**
918
+ * Updates a cell's value using a transform function.
919
+ * @param {DxGridPlanePosition} position - The position of the cell to update.
920
+ * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
921
+ */
922
+ updateCellData({ col, row, plane = "grid" }, update) {
923
+ const fields = this._projection?.getFields() ?? [];
924
+ const fieldIndex = this.getFieldIndex(plane, col);
925
+ if (fieldIndex === void 0 || fieldIndex < 0 || fieldIndex >= fields.length) return;
926
+ const field = fields[fieldIndex];
927
+ const rowData = this._registry.get(this._sortedRows)[row];
928
+ const updatedValue = update(SchemaEx.getValue(rowData, field.path));
929
+ this._change.row(rowData, (mutableRow) => {
930
+ SchemaEx.setValue(mutableRow, field.path, updatedValue);
931
+ });
932
+ }
933
+ /**
934
+ * Gets the domain-appropriate cell type for a given cell position.
935
+ * This abstracts away the low-level grid plane concepts and provides domain-specific types.
936
+ * @param cell - The cell position to classify
937
+ * @returns The domain-appropriate cell type
938
+ */
939
+ getCellType(cell) {
940
+ switch (cell.plane) {
941
+ case "frozenRowsEnd": return "draft";
942
+ case "frozenRowsStart": return "header";
943
+ default: return "standard";
944
+ }
945
+ }
946
+ /**
947
+ * Convenience method to check if a cell is a draft cell.
948
+ * @param cell - The cell position to check
949
+ * @returns true if the cell is a draft cell, false otherwise
950
+ */
951
+ isDraftCell(cell) {
952
+ return this.getCellType(cell) === "draft";
953
+ }
954
+ deleteColumn(fieldId) {
955
+ if (!this._projection) return;
956
+ const field = this._projection?.getFields().find((field) => field.id === fieldId);
957
+ if (field && this._onColumnDelete) this._onColumnDelete(field.id);
958
+ }
959
+ setColumnWidth(columnIndex, width, plane = "grid") {
960
+ const fields = this._projection?.getFields() ?? [];
961
+ const fieldIndex = this.getFieldIndex(plane, columnIndex);
962
+ if (fieldIndex !== void 0 && fieldIndex < fields.length) {
963
+ const newWidth = Math.max(0, width);
964
+ const field = fields[fieldIndex];
965
+ if (field) this._change.table((mutableTable) => {
966
+ mutableTable.sizes[field.path] = newWidth;
967
+ });
968
+ }
969
+ }
970
+ pinRow(rowIndex, side) {
971
+ this._pinnedRows[side].push(rowIndex);
972
+ }
973
+ unpinRow(rowIndex) {
974
+ this._pinnedRows.top = this._pinnedRows.top.filter((index) => index !== rowIndex);
975
+ this._pinnedRows.bottom = this._pinnedRows.bottom.filter((index) => index !== rowIndex);
976
+ }
977
+ /**
978
+ * Sets the sort order for a field in-memory (local to this instance).
979
+ * Use saveView() to persist the sort to the view query.
980
+ */
981
+ setSort(fieldId, direction) {
982
+ if (!this._projection.getFields().find((f) => f.id === fieldId)) return;
983
+ this._writeSort({
984
+ fieldId,
985
+ direction
986
+ });
987
+ }
988
+ /**
989
+ * Writes the per-user sort, routing through the ViewState.Manager (so the `local` backend persists)
990
+ * when one is configured, otherwise updating the ephemeral atom directly.
991
+ */
992
+ _writeSort(value) {
993
+ if (this._viewState) this._viewState.set(tableSortAspect, this.id, value);
994
+ else this._registry.set(this._inMemorySort, value);
995
+ }
996
+ /**
997
+ * Toggles the sort direction for a field in-memory.
998
+ */
999
+ toggleSort(fieldId) {
1000
+ const currentSort = this.getSorting();
1001
+ if (!currentSort || currentSort.fieldId !== fieldId) {
1002
+ this.setSort(fieldId, "asc");
1003
+ return;
1004
+ }
1005
+ const newDirection = currentSort.direction === "asc" ? "desc" : "asc";
1006
+ this.setSort(fieldId, newDirection);
1007
+ }
1008
+ /**
1009
+ * Clears the in-memory sort order.
1010
+ */
1011
+ clearSort() {
1012
+ this._writeSort(void 0);
1013
+ }
1014
+ /**
1015
+ * Saves the current in-memory sort to the view query AST.
1016
+ * This persists the sort so it becomes the default for all viewers.
1017
+ */
1018
+ saveView() {
1019
+ const view = this.view;
1020
+ const inMemorySort = this._registry.get(this._inMemorySort);
1021
+ const viewSnapshot = Obj.getSnapshot(view);
1022
+ const currentQuery = Query.fromAst(viewSnapshot.query.ast);
1023
+ const baseQuery = this._getBaseQuery(currentQuery);
1024
+ if (inMemorySort) {
1025
+ const field = this._projection.getFields().find((f) => f.id === inMemorySort.fieldId);
1026
+ if (field) {
1027
+ const newQuery = baseQuery.orderBy(Order.property(field.path, inMemorySort.direction));
1028
+ Obj.update(view, (view) => {
1029
+ view.query.ast = newQuery.ast;
1030
+ });
1031
+ }
1032
+ } else Obj.update(view, (view) => {
1033
+ view.query.ast = baseQuery.ast;
1034
+ });
1035
+ this._writeSort(void 0);
1036
+ }
1037
+ /**
1038
+ * Extracts the base query without order clauses.
1039
+ */
1040
+ _getBaseQuery(query) {
1041
+ const ast = query.ast;
1042
+ if (ast.type === "order") return Query.fromAst(ast.query);
1043
+ return query;
1044
+ }
1045
+ /**
1046
+ * Checks if the view has unsaved changes (in-memory sort differs from persisted sort).
1047
+ */
1048
+ getViewDirty() {
1049
+ return this._registry.get(this._viewDirty);
1050
+ }
1051
+ };
1052
+ var editorTextToCellValue = (props, value) => {
1053
+ switch (props.format) {
1054
+ case Format.TypeFormat.Ref: if (Obj.isObject(value)) return Ref.make(value);
1055
+ else return value;
1056
+ case Format.TypeFormat.SingleSelect: {
1057
+ const ids = extractTagIds(value);
1058
+ if (ids && ids.length > 0) return ids[0];
1059
+ else return value;
1060
+ }
1061
+ case Format.TypeFormat.MultiSelect: return extractTagIds(value) || value;
1062
+ default: return parseValue({
1063
+ type: props.type,
1064
+ format: props.format,
1065
+ value
1066
+ });
1067
+ }
1068
+ };
1069
+ //#endregion
1070
+ //#region src/model/table-presentation.ts
1071
+ /**
1072
+ * Presentation layer for a table component, handling cell rendering and grid display logic.
1073
+ * Manages rendering of table cells, headers, selection columns, and action columns across
1074
+ * different grid planes.
1075
+ */
1076
+ var TablePresentation = class {
1077
+ model;
1078
+ _registry;
1079
+ fieldProjectionCache = /* @__PURE__ */ new Map();
1080
+ constructor(registry, model) {
1081
+ this.model = model;
1082
+ this._registry = registry;
1083
+ }
1084
+ getCells(range, plane) {
1085
+ this.fieldProjectionCache.clear();
1086
+ let cells;
1087
+ switch (plane) {
1088
+ case "grid":
1089
+ this.model.setVisibleRange(range);
1090
+ cells = this.getMainGridCells(range);
1091
+ break;
1092
+ case "frozenRowsStart":
1093
+ cells = this.getHeaderCells(range);
1094
+ break;
1095
+ case "frozenColsStart":
1096
+ cells = this.getFrozenColsStartCells(range);
1097
+ break;
1098
+ case "frozenColsEnd":
1099
+ cells = this.getActionColumnCells(range);
1100
+ break;
1101
+ case "fixedStartStart":
1102
+ cells = this.getFixedStartStartCells();
1103
+ break;
1104
+ case "fixedStartEnd":
1105
+ cells = this.getNewColumnCell();
1106
+ break;
1107
+ case "frozenRowsEnd":
1108
+ cells = this.getDraftRowCells(range);
1109
+ break;
1110
+ case "fixedEndStart":
1111
+ cells = this.getFixedEndStartCells(range);
1112
+ break;
1113
+ case "fixedEndEnd":
1114
+ cells = this.getDraftActionCells(range);
1115
+ break;
1116
+ default: cells = {};
1117
+ }
1118
+ if (plane === "grid" && this.model.features.dataEditable === false) Object.values(cells).forEach((cell) => {
1119
+ cell.readonly = "text-select";
1120
+ });
1121
+ return cells;
1122
+ }
1123
+ /**
1124
+ * Gets a field projection, using cache if available within the current render pass.
1125
+ */
1126
+ getFieldProjection(fieldId) {
1127
+ let projection = this.fieldProjectionCache.get(fieldId);
1128
+ if (!projection) {
1129
+ projection = this.model.projection.getFieldProjection(fieldId);
1130
+ this.fieldProjectionCache.set(fieldId, projection);
1131
+ }
1132
+ return projection;
1133
+ }
1134
+ createDataCell(cells, obj, field, colIndex, displayIndex, plane = "grid") {
1135
+ const { props } = this.getFieldProjection(field.id);
1136
+ const cell = { get value() {
1137
+ const value = SchemaEx.getValue(obj, field.path);
1138
+ if (value == null) return "";
1139
+ if (props.type === "array") return "";
1140
+ switch (props.format) {
1141
+ case Format$1.TypeFormat.Boolean:
1142
+ case Format$1.TypeFormat.SingleSelect:
1143
+ case Format$1.TypeFormat.MultiSelect: return "";
1144
+ case Format$1.TypeFormat.Ref:
1145
+ if (!field.referencePath) return "";
1146
+ return SchemaEx.getValue(value.target, field.referencePath);
1147
+ default: return formatForDisplay({
1148
+ type: props.type,
1149
+ format: props.format,
1150
+ value
1151
+ });
1152
+ }
1153
+ } };
1154
+ const classes = [];
1155
+ const formatClasses = cellClassesForFieldType(props);
1156
+ if (formatClasses) classes.push(formatClasses);
1157
+ const rowSelectionClasses = cellClassesForRowSelection(this.model.selection.isObjectSelected(obj), this.model.selection.selectionMode);
1158
+ if (rowSelectionClasses) classes.push(rowSelectionClasses);
1159
+ cell.className = mx(classes);
1160
+ if (props.type === TypeEnum.Array) {
1161
+ const targetArray = SchemaEx.getValue(obj, field.path);
1162
+ if (targetArray && Array.isArray(targetArray)) {
1163
+ const getLabel = (value) => {
1164
+ if (!value) return;
1165
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
1166
+ if (Array.isArray(value)) return "Array";
1167
+ if (typeof value === "object" && value.name) return value.name;
1168
+ if (typeof value === "object" && value.value) return `${value.value}`;
1169
+ return "Object";
1170
+ };
1171
+ cell.accessoryHtml = `<div class="flex flex-row gap-1 overflow-x-auto">${targetArray.map(getLabel).filter(Predicate.isNotNullable).map((title) => {
1172
+ return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
1173
+ }).join("")}</div>`;
1174
+ }
1175
+ }
1176
+ if (props.format === Format$1.TypeFormat.Ref && props.referenceSchema) {
1177
+ const targetObj = SchemaEx.getValue(obj, field.path)?.target;
1178
+ if (targetObj) cell.accessoryHtml = `<div role="none" class="dx-grid__cell__block"><dx-anchor uri=${Obj.getURI(targetObj)} class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
1179
+ }
1180
+ if (props.format === Format$1.TypeFormat.URL) {
1181
+ const value = SchemaEx.getValue(obj, field.path);
1182
+ const href = typeof value === "string" ? safeHttpUrl(value) : void 0;
1183
+ if (href) cell.accessoryHtml = `<div role="none" class="dx-grid__cell__block"><a href="${escapeHtmlAttribute(href)}" target="_blank" rel="noopener noreferrer" class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--arrow-square-out--regular"/></a></div>`;
1184
+ }
1185
+ if (props.format === Format$1.TypeFormat.Boolean) {
1186
+ const value = SchemaEx.getValue(obj, field.path);
1187
+ cell.accessoryHtml = tableControls.switch.render({
1188
+ colIndex,
1189
+ rowIndex: displayIndex,
1190
+ plane,
1191
+ checked: value ?? false
1192
+ });
1193
+ cell.readonly = "no-text-select";
1194
+ }
1195
+ if (props.format === Format$1.TypeFormat.SingleSelect) {
1196
+ const value = SchemaEx.getValue(obj, field.path);
1197
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
1198
+ if (options) {
1199
+ const option = options.find((o) => o.id === value);
1200
+ if (option) cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
1201
+ }
1202
+ }
1203
+ if (props.format === Format$1.TypeFormat.MultiSelect) {
1204
+ const values = SchemaEx.getValue(obj, field.path);
1205
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
1206
+ if (options && values && values.length > 0) {
1207
+ const tags = values.map((value) => {
1208
+ const option = options.find((o) => o.id === value);
1209
+ if (option) return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
1210
+ return null;
1211
+ }).filter(Boolean).join("");
1212
+ if (tags) cell.accessoryHtml = tags;
1213
+ }
1214
+ }
1215
+ const idx = toPlaneCellIndex({
1216
+ col: colIndex,
1217
+ row: displayIndex
1218
+ });
1219
+ cells[idx] = cell;
1220
+ }
1221
+ getMainGridCells(range) {
1222
+ const cells = {};
1223
+ const fields = this.model.projection?.getFields() ?? [];
1224
+ const pin = this.model.pinColumns;
1225
+ const scrollingFields = fields.length - pin;
1226
+ for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
1227
+ const field = fields[pin + col];
1228
+ if (!field) continue;
1229
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row, "grid");
1230
+ }
1231
+ return cells;
1232
+ }
1233
+ getDraftRowCells(range) {
1234
+ const cells = {};
1235
+ const fields = this.model.projection?.getFields() ?? [];
1236
+ const draftRows = this.model.getDraftRows();
1237
+ const pin = this.model.pinColumns;
1238
+ const scrollingFields = fields.length - pin;
1239
+ if (draftRows.length === 0) for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) cells[toPlaneCellIndex({
1240
+ col,
1241
+ row: 0
1242
+ })] = {
1243
+ value: "",
1244
+ readonly: true,
1245
+ className: "dx-grid__row--cta__cell"
1246
+ };
1247
+ else for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
1248
+ const draftRow = draftRows[row];
1249
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) {
1250
+ const field = fields[pin + col];
1251
+ if (!field) continue;
1252
+ this.createDraftDataCell(cells, draftRow.data, field, col, row, "frozenRowsEnd");
1253
+ }
1254
+ }
1255
+ return cells;
1256
+ }
1257
+ /**
1258
+ * Renders a draft-row data cell and applies the validation-error flag when the field is invalid.
1259
+ */
1260
+ createDraftDataCell(cells, obj, field, planeCol, rowIndex, plane) {
1261
+ const cellIndex = toPlaneCellIndex({
1262
+ col: planeCol,
1263
+ row: rowIndex
1264
+ });
1265
+ this.createDataCell(cells, obj, field, planeCol, rowIndex, plane);
1266
+ if (this.model.hasDraftRowValidationError(rowIndex, field.path)) {
1267
+ const cellValue = cells[cellIndex];
1268
+ if (cellValue) {
1269
+ const existingClasses = cellValue.className || "";
1270
+ const draftClasses = "dx-grid__cell--flagged";
1271
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
1272
+ }
1273
+ }
1274
+ }
1275
+ getHeaderCells(range) {
1276
+ const cells = {};
1277
+ const fields = this.model.projection?.getFields() ?? [];
1278
+ const pin = this.model.pinColumns;
1279
+ const scrollingFields = fields.length - pin;
1280
+ for (let col = range.start.col; col <= range.end.col && col < scrollingFields; col++) this.createHeaderCell(cells, fields[pin + col].id, col);
1281
+ return cells;
1282
+ }
1283
+ createHeaderCell(cells, fieldId, planeCol) {
1284
+ const currentSort = this.model.getSorting();
1285
+ const { field, props } = this.model.projection.getFieldProjection(fieldId);
1286
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : void 0;
1287
+ cells[toPlaneCellIndex({
1288
+ col: planeCol,
1289
+ row: 0
1290
+ })] = {
1291
+ value: "",
1292
+ resizeHandle: "col",
1293
+ accessoryHtml: `
1294
+ <span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
1295
+ ${direction !== void 0 ? tableButtons.sort.render({
1296
+ fieldId: field.id,
1297
+ direction
1298
+ }) : ""}
1299
+ ${tableButtons.columnSettings.render({ fieldId: field.id })}
1300
+ `,
1301
+ className: "bg-axis-surface! text-axis-text! [&>div]:flex [&>div]:items-stretch"
1302
+ };
1303
+ }
1304
+ /**
1305
+ * Renders the frozenColsStart plane: the selection column (if enabled) followed by the pinned data columns.
1306
+ */
1307
+ getFrozenColsStartCells(range) {
1308
+ const cells = {};
1309
+ const selectionColumns = this.model.selectionColumns;
1310
+ const fields = this.model.projection?.getFields() ?? [];
1311
+ const pin = this.model.pinColumns;
1312
+ for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
1313
+ if (selectionColumns > 0) {
1314
+ const isSelected = this.model.selection.isRowIndexSelected(row);
1315
+ const classes = cellClassesForRowSelection(isSelected, this.model.selection.selectionMode);
1316
+ cells[toPlaneCellIndex({
1317
+ col: 0,
1318
+ row
1319
+ })] = {
1320
+ value: "",
1321
+ readonly: "no-text-select",
1322
+ className: classes ? mx(classes) : void 0,
1323
+ accessoryHtml: tableControls.checkbox.render({
1324
+ rowIndex: row,
1325
+ checked: isSelected
1326
+ })
1327
+ };
1328
+ }
1329
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
1330
+ const field = fields[dataCol];
1331
+ if (!field) continue;
1332
+ this.createDataCell(cells, this.model.getRows()[row], field, selectionColumns + dataCol, row, "frozenColsStart");
1333
+ }
1334
+ }
1335
+ return cells;
1336
+ }
1337
+ getActionColumnCells(range) {
1338
+ const cells = {};
1339
+ for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
1340
+ const classes = cellClassesForRowSelection(this.model.selection.isRowIndexSelected(row), this.model.selection.selectionMode);
1341
+ cells[toPlaneCellIndex({
1342
+ col: 0,
1343
+ row
1344
+ })] = {
1345
+ value: "",
1346
+ readonly: true,
1347
+ className: classes ? mx(classes) : void 0,
1348
+ accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row })
1349
+ };
1350
+ }
1351
+ return cells;
1352
+ }
1353
+ /**
1354
+ * Renders the fixedStartStart corner (frozen header row x frozen columns): the select-all cell (if the
1355
+ * selection column is enabled) followed by the pinned columns' header cells.
1356
+ */
1357
+ getFixedStartStartCells() {
1358
+ const cells = {};
1359
+ const selectionColumns = this.model.selectionColumns;
1360
+ if (selectionColumns > 0) {
1361
+ const showSelectAll = this.model.features.selection.enabled && this.model.selection.selectionMode !== "single";
1362
+ cells[toPlaneCellIndex({
1363
+ col: 0,
1364
+ row: 0
1365
+ })] = {
1366
+ accessoryHtml: showSelectAll ? tableControls.checkbox.render({
1367
+ rowIndex: 0,
1368
+ header: true,
1369
+ checked: this.model.selection.allRowsSelected
1370
+ }) : void 0,
1371
+ className: "bg-axis-surface!",
1372
+ readonly: true,
1373
+ value: ""
1374
+ };
1375
+ }
1376
+ const fields = this.model.projection?.getFields() ?? [];
1377
+ const pin = this.model.pinColumns;
1378
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
1379
+ const field = fields[dataCol];
1380
+ if (!field) continue;
1381
+ this.createHeaderCell(cells, field.id, selectionColumns + dataCol);
1382
+ }
1383
+ return cells;
1384
+ }
1385
+ getNewColumnCell() {
1386
+ return { [toPlaneCellIndex({
1387
+ col: 0,
1388
+ row: 0
1389
+ })]: {
1390
+ accessoryHtml: this.model.features.schemaEditable ? tableButtons.addColumn.render({ disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT }) : void 0,
1391
+ className: "bg-axis-surface!",
1392
+ readonly: true,
1393
+ value: ""
1394
+ } };
1395
+ }
1396
+ getDraftActionCells(range) {
1397
+ const cells = {};
1398
+ const draftRows = this.model.getDraftRows();
1399
+ if (draftRows.length === 0) cells[toPlaneCellIndex({
1400
+ col: 0,
1401
+ row: 0
1402
+ })] = {
1403
+ value: "",
1404
+ className: "dx-grid__row--cta__cell",
1405
+ readonly: true
1406
+ };
1407
+ else for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
1408
+ const disabled = !draftRows[row].valid;
1409
+ cells[toPlaneCellIndex({
1410
+ col: 0,
1411
+ row
1412
+ })] = {
1413
+ value: "",
1414
+ readonly: true,
1415
+ accessoryHtml: tableButtons.saveDraftRow.render({
1416
+ rowIndex: row,
1417
+ disabled
1418
+ })
1419
+ };
1420
+ }
1421
+ return cells;
1422
+ }
1423
+ /**
1424
+ * Renders the fixedEndStart corner (draft row x frozen columns): the draft "+" icon (if the selection
1425
+ * column is enabled) followed by the pinned columns' draft cells.
1426
+ */
1427
+ getFixedEndStartCells(range) {
1428
+ const cells = {};
1429
+ const draftRows = this.model.getDraftRows();
1430
+ const selectionColumns = this.model.selectionColumns;
1431
+ const fields = this.model.projection?.getFields() ?? [];
1432
+ const pin = this.model.pinColumns;
1433
+ for (let row = range.start.row; row <= range.end.row; row++) {
1434
+ if (selectionColumns > 0) cells[toPlaneCellIndex({
1435
+ col: 0,
1436
+ row
1437
+ })] = {
1438
+ value: "",
1439
+ readonly: true,
1440
+ accessoryHtml: "<div role=\"none\" class=\"dx-grid__cell__block\"><dx-icon icon=\"ph--plus--regular\" class=\"contents\"></dx-icon></div>",
1441
+ className: mx(draftRows.length < 1 && "dx-grid__row--cta__cell")
1442
+ };
1443
+ for (let dataCol = 0; dataCol < pin; dataCol++) {
1444
+ const field = fields[dataCol];
1445
+ if (!field) continue;
1446
+ const planeCol = selectionColumns + dataCol;
1447
+ if (draftRows.length === 0) cells[toPlaneCellIndex({
1448
+ col: planeCol,
1449
+ row
1450
+ })] = {
1451
+ value: "",
1452
+ readonly: true,
1453
+ className: "dx-grid__row--cta__cell"
1454
+ };
1455
+ else if (row < draftRows.length) this.createDraftDataCell(cells, draftRows[row].data, field, planeCol, row, "fixedEndStart");
1456
+ }
1457
+ }
1458
+ return cells;
1459
+ }
1460
+ };
1461
+ /**
1462
+ * Parses a cell value as an HTTP(S) URL, returning the normalized href or `undefined`.
1463
+ * Restricting to http/https keeps `javascript:`/`data:` URLs out of the rendered anchor.
1464
+ */
1465
+ var safeHttpUrl = (value) => {
1466
+ try {
1467
+ const normalized = /^https?:\/\//i.test(value) ? value : `https://${value}`;
1468
+ const url = new URL(normalized);
1469
+ return url.protocol === "http:" || url.protocol === "https:" ? url.href : void 0;
1470
+ } catch {
1471
+ return;
1472
+ }
1473
+ };
1474
+ /**
1475
+ * Escapes a string for safe interpolation into a double-quoted HTML attribute, since
1476
+ * `accessoryHtml` is injected into the grid via `unsafeStatic`.
1477
+ */
1478
+ var escapeHtmlAttribute = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1479
+ var cellClassesForRowSelection = (selected, selectionMode) => {
1480
+ if (!selected) if (selectionMode === "single") return ["cursor-pointer!"];
1481
+ else return;
1482
+ switch (selectionMode) {
1483
+ case "single": return ["dx-grid__cell--no-focus-unfurl bg-current-surface! hover:bg-hover-surface cursor-pointer!"];
1484
+ case "multiple": return ["bg-grid-cell-selected!"];
1485
+ }
1486
+ };
1487
+ //#endregion
1488
+ //#region src/hooks/useAddRow.ts
1489
+ /**
1490
+ * Hook that provides a safe row creation function that handles schema validation failures.
1491
+ * Returns a callback that attempts to create an object and returns boolean success/failure.
1492
+ * Can accept optional data to create objects with specific content (used for committing draft rows).
1493
+ */
1494
+ var useAddRow = ({ db, schema }) => {
1495
+ return useCallback((data) => {
1496
+ if (db && schema) try {
1497
+ db.add(Obj.make(schema, data ?? {}));
1498
+ return "final";
1499
+ } catch (error) {
1500
+ return "draft";
1501
+ }
1502
+ return "final";
1503
+ }, [db, schema]);
1504
+ };
1505
+ //#endregion
1506
+ //#region src/hooks/useProjectionModel.ts
1507
+ var useProjectionModel = (schema, table, registry) => {
1508
+ const [projection, setProjection] = useState();
1509
+ useAsyncEffect(async () => {
1510
+ if (schema && table) {
1511
+ const view = await table.view.load();
1512
+ const jsonSchema = schema.jsonSchema;
1513
+ const projection = new ProjectionModel({
1514
+ registry,
1515
+ view,
1516
+ baseSchema: jsonSchema,
1517
+ change: createEchoChangeCallback(view, Type.getDatabase(schema) != null ? schema : void 0)
1518
+ });
1519
+ projection.normalizeView();
1520
+ setProjection(projection);
1521
+ }
1522
+ }, [
1523
+ schema,
1524
+ table,
1525
+ registry
1526
+ ]);
1527
+ return projection;
1528
+ };
1529
+ //#endregion
1530
+ //#region src/hooks/useTableModel.ts
1531
+ var useTableModel = ({ object, projection, db, rows, rowActions, features, selection, onSelectionChanged, onRowAction, ...props }) => {
1532
+ const registry = useContext(RegistryContext);
1533
+ const viewState = useManagerOptional();
1534
+ const selected = useSelection(object && Obj.getURI(object), "multi");
1535
+ const initialSelection = useRef(selection ?? selected);
1536
+ initialSelection.current = selection ?? selected;
1537
+ const [model, setModel] = useState();
1538
+ useEffect(() => {
1539
+ if (!object || !projection) return;
1540
+ let model;
1541
+ const t = setTimeout(async () => {
1542
+ model = new TableModel({
1543
+ registry,
1544
+ object,
1545
+ projection,
1546
+ db,
1547
+ viewState,
1548
+ change: createEchoChangeCallback$1(object),
1549
+ features,
1550
+ rowActions,
1551
+ initialSelection: [...initialSelection.current],
1552
+ onRowAction,
1553
+ ...props
1554
+ });
1555
+ await model.open();
1556
+ setModel(model);
1557
+ });
1558
+ return () => {
1559
+ clearTimeout(t);
1560
+ model?.close();
1561
+ };
1562
+ }, [
1563
+ registry,
1564
+ viewState,
1565
+ object,
1566
+ projection,
1567
+ features,
1568
+ rowActions
1569
+ ]);
1570
+ useEffect(() => {
1571
+ if (rows && model) model.setRows(rows);
1572
+ }, [model, rows]);
1573
+ const { multi, clear } = useSelectionActions(model?.id);
1574
+ useEffect(() => {
1575
+ if (!model) return;
1576
+ const unsubscribe = registry.subscribe(model.selection.selectionAtom, () => {
1577
+ const selectedItems = [...model.selection.selection];
1578
+ multi(selectedItems);
1579
+ onSelectionChanged?.(selectedItems);
1580
+ });
1581
+ return () => {
1582
+ clear();
1583
+ unsubscribe();
1584
+ };
1585
+ }, [
1586
+ registry,
1587
+ model,
1588
+ onSelectionChanged
1589
+ ]);
1590
+ return model;
1591
+ };
1592
+ //#endregion
1593
+ export { compareValues as _, cellClassesForRowSelection as a, createEchoChangeCallback$1 as c, CONTROL_IDENTIFIERS as d, CheckboxComponent as f, tableButtons as g, BUTTON_IDENTIFIERS as h, TablePresentation as i, FieldSortSchema as l, tableControls as m, useProjectionModel as n, TableModel as o, SwitchComponent as p, useAddRow as r, createDirectChangeCallback as s, useTableModel as t, tableSortAspect as u, getSortValue as v, extractOrder as y };
1594
+
1595
+ //# sourceMappingURL=chunk-useTableModel.mjs.map