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

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