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

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