@dxos/react-ui-table 0.8.3 → 0.8.4-main.28f8d3d

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 (143) hide show
  1. package/dist/lib/browser/chunk-L2LSB6KV.mjs +18 -0
  2. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
  4. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +728 -2104
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +123 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
  14. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +728 -2104
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +124 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +1 -1
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.d.ts +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
  32. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  34. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.d.ts +1 -0
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  38. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -40
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +1 -0
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +1 -2
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts +2 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +58 -21
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +10 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +4 -4
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  69. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  70. package/dist/types/src/testing/index.d.ts +2 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  73. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +5 -6
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/index.d.ts +1 -1
  77. package/dist/types/src/types/index.d.ts.map +1 -1
  78. package/dist/types/src/types/schema.d.ts +8 -0
  79. package/dist/types/src/types/schema.d.ts.map +1 -0
  80. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  81. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts +15 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/src/util/table.d.ts +9 -11
  86. package/dist/types/src/util/table.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +52 -40
  89. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  90. package/src/components/Table/ColumnSettings.tsx +15 -11
  91. package/src/components/Table/CreateRefPanel.tsx +6 -4
  92. package/src/components/Table/DynamicTable.stories.tsx +4 -3
  93. package/src/components/Table/DynamicTable.tsx +17 -10
  94. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
  95. package/src/components/Table/RowActionsMenu.tsx +3 -3
  96. package/src/components/Table/Table.stories.tsx +109 -148
  97. package/src/components/Table/Table.tsx +95 -66
  98. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  99. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  100. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
  101. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
  102. package/src/components/TableCellEditor/extension.ts +2 -2
  103. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  104. package/src/components/testing.ts +7 -24
  105. package/src/hooks/index.ts +1 -0
  106. package/src/hooks/useAddRow.ts +36 -0
  107. package/src/hooks/useTableModel.ts +21 -17
  108. package/src/index.ts +1 -2
  109. package/src/model/modal-controller.ts +1 -1
  110. package/src/model/selection-model.ts +6 -3
  111. package/src/model/table-model.test.ts +8 -8
  112. package/src/model/table-model.ts +278 -86
  113. package/src/model/table-presentation.test.ts +8 -8
  114. package/src/model/table-presentation.ts +240 -135
  115. package/src/model/table-sorting.ts +11 -11
  116. package/src/playwright/playwright.config.ts +17 -0
  117. package/src/playwright/smoke.spec.ts +113 -20
  118. package/src/testing/index.ts +5 -0
  119. package/src/testing/useTestTableModel.ts +123 -0
  120. package/src/translations.ts +7 -5
  121. package/src/types/index.ts +2 -2
  122. package/src/types/schema.ts +16 -0
  123. package/src/util/dynamic-table.ts +20 -18
  124. package/src/util/schema.ts +1 -1
  125. package/src/util/table-buttons.ts +26 -2
  126. package/src/util/table-controls.ts +4 -1
  127. package/src/util/table.ts +37 -111
  128. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  129. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  130. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  131. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  132. package/dist/lib/node/index.cjs +0 -2739
  133. package/dist/lib/node/index.cjs.map +0 -7
  134. package/dist/lib/node/meta.json +0 -1
  135. package/dist/lib/node/types/index.cjs +0 -31
  136. package/dist/lib/node/types/index.cjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  138. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  139. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  140. package/dist/types/src/types/table.d.ts +0 -56
  141. package/dist/types/src/types/table.d.ts.map +0 -1
  142. package/src/playwright/playwright.config.cts +0 -18
  143. package/src/types/table.ts +0 -27
@@ -2,44 +2,60 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
- import { type Space } from '@dxos/client/echo';
8
7
  import { Resource } from '@dxos/context';
8
+ import { Obj, Ref } from '@dxos/echo';
9
9
  import {
10
10
  type FieldSortType,
11
11
  FormatEnum,
12
- getValue,
13
- setValue,
14
12
  type JsonProp,
15
- getSnapshot,
13
+ type JsonSchemaType,
16
14
  getSchema,
15
+ getSnapshot,
16
+ getValue,
17
+ setValue,
18
+ toEffectSchema,
17
19
  } from '@dxos/echo-schema';
18
20
  import { invariant } from '@dxos/invariant';
19
- import { isLiveObject, makeRef } from '@dxos/live-object';
21
+ import { ObjectId } from '@dxos/keys';
22
+ import { isLiveObject } from '@dxos/live-object';
23
+ import { fullyQualifiedId } from '@dxos/react-client/echo';
24
+ import { type Label } from '@dxos/react-ui';
20
25
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
21
26
  import {
22
27
  type DxGridAxisMeta,
23
- type DxGridPlaneRange,
24
28
  type DxGridPlanePosition,
29
+ type DxGridPlaneRange,
25
30
  type DxGridPosition,
26
31
  } from '@dxos/react-ui-grid';
27
- import { type ViewType, type ViewProjection, type PropertyType, validateSchema } from '@dxos/schema';
32
+ import { type DataType, ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
28
33
 
29
- import { type SelectionMode, SelectionModel } from './selection-model';
30
- import { TableSorting } from './table-sorting';
34
+ import { TableView } from '../types';
31
35
  import { touch } from '../util';
32
36
  import { extractTagIds } from '../util/tag';
33
37
 
38
+ import { type SelectionMode, SelectionModel } from './selection-model';
39
+ import { TableSorting } from './table-sorting';
40
+
41
+ // Domain types for cell classification
42
+ export type TableCellType = 'standard' | 'draft' | 'header';
43
+
34
44
  // TODO(ZaymonFC): Use a common type?
35
45
  export type ValidationResult = { valid: true } | { valid: false; error: string };
36
46
 
47
+ export type DraftRow<T extends TableRow = TableRow> = {
48
+ data: T;
49
+ valid: boolean;
50
+ validationErrors: ValidationError[];
51
+ };
52
+
37
53
  // TODO(burdon): Use schema types.
38
54
  export type TableRow = Record<JsonProp, any> & { id: string };
39
55
 
40
56
  export type TableRowAction = {
41
57
  id: string;
42
- translationKey: string;
58
+ label: Label;
43
59
  };
44
60
 
45
61
  export type TableFeatures = {
@@ -55,15 +71,16 @@ const defaultFeatures: TableFeatures = {
55
71
  };
56
72
 
57
73
  export type TableModelProps<T extends TableRow = TableRow> = {
58
- id?: string;
59
- space?: Space;
60
- view?: ViewType;
61
- projection: ViewProjection;
74
+ view: DataType.View;
75
+ schema: JsonSchemaType;
76
+ projection?: ProjectionModel;
62
77
  features?: Partial<TableFeatures>;
63
78
  sorting?: FieldSortType[];
79
+ initialSelection?: string[];
64
80
  pinnedRows?: { top: number[]; bottom: number[] };
65
81
  rowActions?: TableRowAction[];
66
- onInsertRow?: (index?: number) => void;
82
+ onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
83
+ onInsertRow?: (data?: any) => boolean;
67
84
  onDeleteRows?: (index: number, obj: T[]) => void;
68
85
  onDeleteColumn?: (fieldId: string) => void;
69
86
  onCellUpdate?: (cell: DxGridPosition) => void;
@@ -72,17 +89,16 @@ export type TableModelProps<T extends TableRow = TableRow> = {
72
89
  };
73
90
 
74
91
  export class TableModel<T extends TableRow = TableRow> extends Resource {
75
- private readonly _id: string | undefined;
76
- private readonly _space: Space | undefined;
77
- private readonly _view: ViewType | undefined;
78
- private readonly _projection: ViewProjection;
92
+ private readonly _view: DataType.View;
93
+ private readonly _projection: ProjectionModel;
94
+ private _table?: TableView;
79
95
 
80
96
  private readonly _visibleRange = signal<DxGridPlaneRange>({
81
97
  start: { row: 0, col: 0 },
82
98
  end: { row: 0, col: 0 },
83
99
  });
84
100
 
85
- private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
101
+ private readonly _onInsertRow?: (data?: any) => boolean;
86
102
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
87
103
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
88
104
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
@@ -92,6 +108,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
92
108
  private readonly _features: TableFeatures;
93
109
 
94
110
  private readonly _rows = signal<T[]>([]);
111
+ private readonly _draftRows = signal<DraftRow<T>[]>([]);
95
112
  private readonly _sorting: TableSorting<T>;
96
113
 
97
114
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
@@ -99,12 +116,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
99
116
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
100
117
 
101
118
  constructor({
102
- id,
103
- space,
104
119
  view,
120
+ schema,
105
121
  projection,
106
122
  features = {},
107
123
  sorting = [],
124
+ initialSelection = [],
108
125
  pinnedRows = { top: [], bottom: [] },
109
126
  rowActions = [],
110
127
  onCellUpdate,
@@ -115,10 +132,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
115
132
  onRowAction,
116
133
  }: TableModelProps<T>) {
117
134
  super();
118
- this._id = id;
119
- this._space = space;
120
135
  this._view = view;
121
- this._projection = projection;
136
+ this._projection = projection ?? new ProjectionModel(schema, view.projection);
122
137
 
123
138
  // TODO(ZaymonFC): Use our more robust config merging module?
124
139
  this._features = { ...defaultFeatures, ...features };
@@ -128,13 +143,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
128
143
  'Single selection is not compatible with editable tables.',
129
144
  );
130
145
 
131
- this._sorting = new TableSorting(this._rows, this._view, projection);
146
+ this._sorting = new TableSorting(this._rows, this._view, this._projection);
132
147
 
133
148
  if (sorting.length > 0) {
134
149
  const [sort] = sorting;
135
150
  this._sorting.setSort(sort.fieldId, sort.direction);
136
151
  }
137
152
 
153
+ this._selection = new SelectionModel(
154
+ this._sorting.sortedRows,
155
+ this._features.selection.mode ?? 'multiple',
156
+ initialSelection,
157
+ () => this._onRowOrderChange?.(),
158
+ );
159
+
138
160
  this._pinnedRows = pinnedRows;
139
161
  this._rowActions = rowActions;
140
162
  this._onInsertRow = onInsertRow;
@@ -145,24 +167,25 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
145
167
  this._onRowAction = onRowAction;
146
168
  }
147
169
 
148
- public get id() {
149
- return this._id;
170
+ public get id(): string {
171
+ return fullyQualifiedId(this._view);
150
172
  }
151
173
 
152
- public get space() {
153
- return this._space;
174
+ public get view(): DataType.View {
175
+ return this._view;
154
176
  }
155
177
 
156
- public get view() {
157
- return this._view;
178
+ public get table(): TableView {
179
+ invariant(this._table, 'Model not initialized');
180
+ return this._table;
158
181
  }
159
182
 
160
- public get projection() {
183
+ public get projection(): ProjectionModel {
161
184
  return this._projection;
162
185
  }
163
186
 
164
187
  public get rows(): ReadonlySignal<T[]> {
165
- return this._sorting.sortedRows;
188
+ return this.sorting.sortedRows;
166
189
  }
167
190
 
168
191
  public get features(): TableFeatures {
@@ -172,7 +195,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
172
195
  /**
173
196
  * @reactive
174
197
  */
175
- public get sorting(): TableSorting<T> | undefined {
198
+ public get sorting(): TableSorting<T> {
176
199
  return this._sorting;
177
200
  }
178
201
 
@@ -180,11 +203,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
180
203
  * Gets the row data at the specified display index.
181
204
  */
182
205
  public getRowAt(displayIndex: number): T | undefined {
183
- if (displayIndex < 0 || displayIndex >= this._sorting.sortedRows.value.length) {
206
+ if (displayIndex < 0 || displayIndex >= this.sorting.sortedRows.value.length) {
184
207
  return undefined;
185
208
  }
186
209
 
187
- return this._sorting.sortedRows.value[displayIndex];
210
+ return this.sorting.sortedRows.value[displayIndex];
188
211
  }
189
212
 
190
213
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
@@ -202,7 +225,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
202
225
 
203
226
  /** @reactive */
204
227
  public get isViewDirty(): boolean {
205
- return this._sorting.isDirty;
228
+ return this.sorting.isDirty;
206
229
  }
207
230
 
208
231
  public get rowActions(): TableRowAction[] {
@@ -214,19 +237,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
214
237
  //
215
238
 
216
239
  protected override async _open(): Promise<void> {
240
+ const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
241
+ invariant(Obj.instanceOf(TableView, presentation));
242
+ this._table = presentation;
243
+
217
244
  this.initializeColumnMeta();
218
245
  this.initializeEffects();
219
- this._selection = new SelectionModel(this._sorting.sortedRows, this._features.selection.mode ?? 'multiple', () =>
220
- this._onRowOrderChange?.(),
221
- );
222
246
  await this._selection.open(this._ctx);
223
247
  }
224
248
 
225
249
  private initializeColumnMeta(): void {
226
250
  this._columnMeta = computed(() => {
227
- const fields = this._view?.fields ?? [];
251
+ const fields = this._projection?.fields ?? [];
228
252
  const meta = Object.fromEntries(
229
- fields.map((field, index: number) => [index, { size: field?.size ?? 256, resizeable: true }]),
253
+ fields.map((field, index: number) => [index, { size: this.table.sizes[field.path] ?? 256, resizeable: true }]),
230
254
  );
231
255
 
232
256
  return {
@@ -239,7 +263,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
239
263
 
240
264
  private initializeEffects(): void {
241
265
  const rowOrderWatcher = effect(() => {
242
- touch(this._sorting.sortedRows.value);
266
+ touch(this.sorting.sortedRows.value);
243
267
  this._onRowOrderChange?.();
244
268
  });
245
269
  this._ctx.onDispose(rowOrderWatcher);
@@ -255,8 +279,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
255
279
  for (let row = start.row; row <= end.row; row++) {
256
280
  rowEffects.push(
257
281
  effect(() => {
258
- const obj = this._sorting.sortedRows.value[row];
259
- this._view?.fields.forEach((field) => touch(getValue(obj, field.path)));
282
+ const obj = this.sorting.sortedRows.value[row];
283
+ this._projection?.fields.forEach((field) => touch(getValue(obj, field.path)));
260
284
  this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
261
285
  }),
262
286
  );
@@ -265,6 +289,13 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
265
289
  return () => rowEffects.forEach((cleanup) => cleanup());
266
290
  });
267
291
  this._ctx.onDispose(rowEffectManager);
292
+
293
+ const draftRowsWatcher = effect(() => {
294
+ touch(this._draftRows.value);
295
+ // Notify grid that draft rows have changed
296
+ this._onRowOrderChange?.();
297
+ });
298
+ this._ctx.onDispose(draftRowsWatcher);
268
299
  }
269
300
 
270
301
  //
@@ -277,11 +308,101 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
277
308
 
278
309
  public getRowCount = (): number => this._rows.value.length;
279
310
 
280
- public getColumnCount = (): number => this._view?.fields.length ?? 0;
311
+ /**
312
+ * @reactive
313
+ */
314
+ public getDraftRowCount = (): number => this._draftRows.value.length;
315
+
316
+ // TODO(ZaymonFC): Return .value instead of exposing the signal.
317
+ public get draftRows(): ReadonlySignal<DraftRow<T>[]> {
318
+ return this._draftRows;
319
+ }
320
+
321
+ /**
322
+ * Checks if a specific field path has validation errors for a given draft row.
323
+ * @param draftRowIndex - The index of the draft row to check
324
+ * @param fieldPath - The path of the field to check for validation errors
325
+ * @returns true if the field has validation errors, false otherwise
326
+ */
327
+ public hasDraftRowValidationError(draftRowIndex: number, fieldPath: string): boolean {
328
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
329
+ return false;
330
+ }
331
+
332
+ const draftRow = this._draftRows.value[draftRowIndex];
333
+ const validationErrors = draftRow.validationErrors || [];
334
+ return validationErrors.some((error) => error.path === fieldPath);
335
+ }
281
336
 
282
- public insertRow = (rowIndex?: number): void => {
283
- const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
284
- this._onInsertRow?.(row);
337
+ public getColumnCount = (): number => this._projection?.fields.length ?? 0;
338
+
339
+ public insertRow = (): void => {
340
+ const result = this._onInsertRow?.();
341
+ if (result === false && this._draftRows.value.length === 0) {
342
+ this.createDraftRow();
343
+ }
344
+ };
345
+
346
+ private createDraftRow(): void {
347
+ if (!this._projection) {
348
+ return;
349
+ }
350
+
351
+ // NOTE(ZaymonFC): This is initialized with id because it's required in all schemata?
352
+ const draftData = { id: ObjectId.random() } as any as T;
353
+ const validationErrors = this.validateDraftRowData(draftData);
354
+
355
+ const draftRow: DraftRow<T> = {
356
+ data: draftData,
357
+ valid: validationErrors.length === 0,
358
+ validationErrors,
359
+ };
360
+
361
+ this._draftRows.value = [...this._draftRows.value, draftRow];
362
+ }
363
+
364
+ private validateDraftRow(draftRowIndex: number): void {
365
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
366
+ return;
367
+ }
368
+
369
+ const draftRow = this._draftRows.value[draftRowIndex];
370
+ const validationErrors = this.validateDraftRowData(draftRow.data);
371
+
372
+ const updatedDraftRow = {
373
+ ...draftRow,
374
+ valid: validationErrors.length === 0,
375
+ validationErrors,
376
+ };
377
+
378
+ const newDraftRows = [...this._draftRows.value];
379
+ newDraftRows[draftRowIndex] = updatedDraftRow;
380
+ this._draftRows.value = newDraftRows;
381
+ }
382
+
383
+ private validateDraftRowData(data: T): ValidationError[] {
384
+ const schema = toEffectSchema(this._projection.baseSchema);
385
+ return validateSchema(schema, data) || [];
386
+ }
387
+
388
+ public commitDraftRow = (draftRowIndex: number): boolean => {
389
+ if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
390
+ return false;
391
+ }
392
+
393
+ const draftRow = this._draftRows.value[draftRowIndex];
394
+ if (!draftRow.valid) {
395
+ return false;
396
+ }
397
+
398
+ const success = this._onInsertRow?.(draftRow.data);
399
+
400
+ if (success) {
401
+ const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
402
+ this._draftRows.value = newDraftRows;
403
+ }
404
+
405
+ return success ?? false;
285
406
  };
286
407
 
287
408
  public deleteRow = (rowIndex: number): void => {
@@ -312,15 +433,29 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
312
433
  }
313
434
  };
314
435
 
315
- public getCellData = ({ col, row }: DxGridPlanePosition): any => {
316
- const fields = this._view?.fields ?? [];
436
+ public getCellData = (cell: DxGridPosition): any => {
437
+ const { col, row, plane } = cell;
438
+ const fields = this._projection?.fields ?? [];
317
439
  if (col < 0 || col >= fields.length) {
318
440
  return undefined;
319
441
  }
320
442
 
321
443
  const field = fields[col];
322
- const dataIndex = this._sorting.getDataIndex(row);
323
- const value = getValue(this._rows.value[dataIndex], field.path);
444
+ let value: any;
445
+
446
+ if (plane === 'frozenRowsEnd') {
447
+ // Get data from draft row
448
+ if (row >= 0 && row < this._draftRows.value.length) {
449
+ value = getValue(this._draftRows.value[row].data, field.path);
450
+ } else {
451
+ return undefined;
452
+ }
453
+ } else {
454
+ // Get data from regular row
455
+ const dataIndex = this._sorting.getDataIndex(row);
456
+ value = getValue(this._rows.value[dataIndex], field.path);
457
+ }
458
+
324
459
  if (value == null) {
325
460
  return '';
326
461
  }
@@ -341,42 +476,62 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
341
476
  }
342
477
  };
343
478
 
344
- public validateCellData = async ({ col, row }: DxGridPlanePosition, value: any): Promise<ValidationResult> => {
345
- const rowIdx = this._sorting.getDataIndex(row);
346
- const fields = this._view?.fields ?? [];
479
+ public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
480
+ const fields = this._projection?.fields ?? [];
347
481
  if (col < 0 || col >= fields.length) {
348
482
  return { valid: false, error: 'Invalid column index' };
349
483
  }
350
484
 
351
485
  const field = fields[col];
352
486
  const { props } = this._projection.getFieldProjection(field.id);
487
+ const transformedValue = editorTextToCellValue(props, value);
353
488
 
354
- const currentRow = this._rows.value[rowIdx];
355
- invariant(currentRow, 'Invalid row index');
489
+ const isDraftRow = plane === 'frozenRowsEnd';
356
490
 
357
- // Get a snapshot of the current object.
358
- const snapshot = getSnapshot(currentRow);
359
- const transformedValue = editorTextToCellValue(props, value);
491
+ if (isDraftRow) {
492
+ const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.value.length;
493
+ if (isInvalidDraftRowIndex) {
494
+ return { valid: false, error: 'Invalid draft row index' };
495
+ }
360
496
 
361
- // Set the proposed value.
362
- setValue(snapshot, field.path, transformedValue);
497
+ const draftRow = this._draftRows.value[row];
498
+ const snapshot = { ...draftRow.data };
499
+ setValue(snapshot, field.path, transformedValue);
363
500
 
364
- const schema = getSchema(currentRow);
365
- invariant(schema);
501
+ const validationErrors = this.validateDraftRowData(snapshot);
502
+ // TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
503
+ if (validationErrors.length > 0) {
504
+ const error = validationErrors.find((err) => err.path === field.path);
505
+ if (error) {
506
+ return { valid: false, error: error.message };
507
+ }
508
+ }
509
+ return { valid: true };
510
+ } else {
511
+ const rowIdx = this._sorting.getDataIndex(row);
512
+ const currentRow = this._rows.value[rowIdx];
513
+ invariant(currentRow, 'Invalid row index');
366
514
 
367
- const validationResult = validateSchema(schema, snapshot);
368
- if (validationResult && validationResult.length > 0) {
369
- const error = validationResult[0];
370
- invariant(error.path === field.path);
371
- return { valid: false, error: error.message };
372
- }
515
+ const snapshot = getSnapshot(currentRow);
516
+ setValue(snapshot, field.path, transformedValue);
373
517
 
374
- return { valid: true };
518
+ const schema = getSchema(currentRow);
519
+ invariant(schema);
520
+
521
+ const validationResult = validateSchema(schema, snapshot);
522
+ if (validationResult && validationResult.length > 0) {
523
+ const error = validationResult[0];
524
+ invariant(error.path === field.path);
525
+ return { valid: false, error: error.message };
526
+ }
527
+
528
+ return { valid: true };
529
+ }
375
530
  };
376
531
 
377
- public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
378
- const rowIdx = this._sorting.getDataIndex(row);
379
- const fields = this._view?.fields ?? [];
532
+ public setCellData = (cell: DxGridPosition, value: any): void => {
533
+ const { col, row, plane } = cell;
534
+ const fields = this._projection?.fields ?? [];
380
535
  if (col < 0 || col >= fields.length) {
381
536
  return;
382
537
  }
@@ -387,12 +542,21 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
387
542
 
388
543
  // Special handling for Ref format to preserve existing behavior
389
544
  if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
390
- // TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
391
- // plain string value. Maybe onBlur should be called with the actual value?
392
545
  return;
393
546
  }
394
547
 
395
- setValue(this._rows.value[rowIdx], field.path, transformedValue);
548
+ if (plane === 'frozenRowsEnd') {
549
+ // Update draft row data
550
+ if (row >= 0 && row < this._draftRows.value.length) {
551
+ setValue(this._draftRows.value[row].data, field.path, transformedValue);
552
+ // Re-validate the draft row after the update
553
+ this.validateDraftRow(row);
554
+ }
555
+ } else {
556
+ // Update regular row data
557
+ const rowIdx = this._sorting.getDataIndex(row);
558
+ setValue(this._rows.value[rowIdx], field.path, transformedValue);
559
+ }
396
560
  };
397
561
 
398
562
  /**
@@ -402,7 +566,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
402
566
  */
403
567
  public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
404
568
  const dataRow = this._sorting.getDataIndex(row);
405
- const fields = this._view?.fields ?? [];
569
+ const fields = this._projection?.fields ?? [];
406
570
  const field = fields[col];
407
571
 
408
572
  const value = getValue(this._rows.value[dataRow], field.path);
@@ -410,16 +574,43 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
410
574
  setValue(this._rows.value[dataRow], field.path, updatedValue);
411
575
  }
412
576
 
577
+ /**
578
+ * Gets the domain-appropriate cell type for a given cell position.
579
+ * This abstracts away the low-level grid plane concepts and provides domain-specific types.
580
+ * @param cell - The cell position to classify
581
+ * @returns The domain-appropriate cell type
582
+ */
583
+ public getCellType(cell: DxGridPosition): TableCellType {
584
+ switch (cell.plane) {
585
+ case 'frozenRowsEnd':
586
+ return 'draft';
587
+ case 'frozenRowsStart':
588
+ return 'header';
589
+ case 'grid':
590
+ default:
591
+ return 'standard';
592
+ }
593
+ }
594
+
595
+ /**
596
+ * Convenience method to check if a cell is a draft cell.
597
+ * @param cell - The cell position to check
598
+ * @returns true if the cell is a draft cell, false otherwise
599
+ */
600
+ public isDraftCell(cell: DxGridPosition): boolean {
601
+ return this.getCellType(cell) === 'draft';
602
+ }
603
+
413
604
  //
414
605
  // Column Operations
415
606
  //
416
607
 
417
608
  public deleteColumn(fieldId: string): void {
418
- if (!this._view) {
609
+ if (!this._projection) {
419
610
  return;
420
611
  }
421
612
 
422
- const field = this._view?.fields.find((field) => field.id === fieldId);
613
+ const field = this._projection?.fields.find((field) => field.id === fieldId);
423
614
  if (field && this._onDeleteColumn) {
424
615
  this._onDeleteColumn(field.id);
425
616
  }
@@ -430,12 +621,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
430
621
  //
431
622
 
432
623
  public setColumnWidth(columnIndex: number, width: number): void {
433
- const fields = this._view?.fields ?? [];
624
+ const fields = this._projection?.fields ?? [];
434
625
  if (columnIndex < fields.length) {
435
626
  const newWidth = Math.max(0, width);
436
627
  const field = fields[columnIndex];
437
628
  if (field) {
438
- field.size = newWidth;
629
+ this.table.sizes[field.path] = newWidth;
439
630
  }
440
631
  }
441
632
  }
@@ -457,6 +648,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
457
648
  //
458
649
  // View operations
459
650
  //
651
+
460
652
  public saveView(): void {
461
653
  this._sorting.save();
462
654
  }
@@ -466,7 +658,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
466
658
  switch (props.format) {
467
659
  case FormatEnum.Ref: {
468
660
  if (isLiveObject(value)) {
469
- return makeRef(value);
661
+ return Ref.make(value);
470
662
  } else {
471
663
  return value;
472
664
  }
@@ -3,17 +3,18 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
 
8
- import { Obj, Ref } from '@dxos/echo';
8
+ import { Obj } from '@dxos/echo';
9
9
  import { TypedObject } from '@dxos/echo-schema';
10
10
  import { live } from '@dxos/live-object';
11
11
  import { createEchoSchema } from '@dxos/live-object/testing';
12
- import { createView, ViewProjection } from '@dxos/schema';
12
+ import { createView } from '@dxos/schema';
13
+
14
+ import { TableView } from '../types';
13
15
 
14
16
  import { TableModel, type TableModelProps } from './table-model';
15
17
  import { TablePresentation } from './table-presentation';
16
- import { TableType } from '../types';
17
18
 
18
19
  describe('TablePresentation', () => {
19
20
  describe('row reactivity', () => {
@@ -97,8 +98,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
97
98
 
98
99
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
99
100
  const schema = createEchoSchema(Test);
100
- const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
101
- const projection = new ViewProjection(schema.jsonSchema, view);
102
- const table = Obj.make(TableType, { view: Ref.make(view) });
103
- return new TableModel({ id: table.id, view, projection, ...props });
101
+ const table = Obj.make(TableView, { sizes: {} });
102
+ const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
103
+ return new TableModel({ view, schema: schema.jsonSchema, ...props });
104
104
  };