@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
@@ -5,13 +5,14 @@
5
5
  import { signal } from '@preact/signals-core';
6
6
  import { isNotNullable } from 'effect/Predicate';
7
7
 
8
- import { FormatEnum, getObjectDXN, getValue, TypeEnum } from '@dxos/echo-schema';
8
+ import { Obj } from '@dxos/echo';
9
+ import { FormatEnum, getValue, TypeEnum } from '@dxos/echo-schema';
9
10
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
10
11
  import {
12
+ type DxGridCellValue,
11
13
  type DxGridPlane,
12
14
  type DxGridPlaneCells,
13
15
  type DxGridPlaneRange,
14
- type DxGridCellValue,
15
16
  toPlaneCellIndex,
16
17
  } from '@dxos/react-ui-grid';
17
18
  import { mx } from '@dxos/react-ui-theme';
@@ -27,6 +28,8 @@ import { tableButtons, tableControls } from '../util';
27
28
  * different grid planes.
28
29
  */
29
30
  export class TablePresentation<T extends TableRow = TableRow> {
31
+ private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
32
+
30
33
  constructor(
31
34
  private readonly model: TableModel<T>,
32
35
  private readonly _visibleRange = signal<DxGridPlaneRange>({
@@ -36,6 +39,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
36
39
  ) {}
37
40
 
38
41
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
42
+ // Clear cache at the start of each render pass
43
+ this.fieldProjectionCache.clear();
44
+
39
45
  let cells: DxGridPlaneCells;
40
46
 
41
47
  switch (plane) {
@@ -58,12 +64,18 @@ export class TablePresentation<T extends TableRow = TableRow> {
58
64
  case 'fixedStartEnd':
59
65
  cells = this.getNewColumnCell();
60
66
  break;
67
+ case 'frozenRowsEnd':
68
+ cells = this.getDraftRowCells(range);
69
+ break;
70
+ case 'fixedEndEnd':
71
+ cells = this.getDraftActionCells(range);
72
+ break;
61
73
  default:
62
74
  cells = {};
63
75
  }
64
76
 
65
77
  if (plane === 'grid' && this.model.features.dataEditable === false) {
66
- Object.values(cells).forEach((cell) => {
78
+ Object.values<DxGridCellValue>(cells).forEach((cell) => {
67
79
  cell.readonly = 'text-select';
68
80
  });
69
81
  }
@@ -71,164 +83,217 @@ export class TablePresentation<T extends TableRow = TableRow> {
71
83
  return cells;
72
84
  }
73
85
 
74
- private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
75
- const cells: DxGridPlaneCells = {};
76
- const fields = this.model.view?.fields ?? [];
86
+ /**
87
+ * Gets a field projection, using cache if available within the current render pass.
88
+ */
89
+ private getFieldProjection(fieldId: string) {
90
+ let projection = this.fieldProjectionCache.get(fieldId);
91
+ if (!projection) {
92
+ projection = this.model.projection.getFieldProjection(fieldId);
93
+ this.fieldProjectionCache.set(fieldId, projection);
94
+ }
95
+ return projection;
96
+ }
77
97
 
78
- const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
79
- const { props } = this.model.projection.getFieldProjection(field.id);
98
+ private createDataCell(
99
+ cells: DxGridPlaneCells,
100
+ obj: T,
101
+ field: FieldType,
102
+ colIndex: number,
103
+ displayIndex: number,
104
+ ): void {
105
+ const { props } = this.getFieldProjection(field.id);
106
+
107
+ const cell: DxGridCellValue = {
108
+ get value() {
109
+ const value = getValue(obj, field.path);
110
+ if (value == null) {
111
+ return '';
112
+ }
80
113
 
81
- const cell: DxGridCellValue = {
82
- get value() {
83
- const value = getValue(obj, field.path);
84
- if (value == null) {
85
- return '';
86
- }
114
+ if (props.type === 'array') {
115
+ return '';
116
+ }
87
117
 
88
- if (props.type === 'array') {
118
+ switch (props.format) {
119
+ case FormatEnum.Boolean:
120
+ case FormatEnum.SingleSelect:
121
+ case FormatEnum.MultiSelect: {
89
122
  return '';
90
123
  }
91
-
92
- switch (props.format) {
93
- case FormatEnum.Boolean:
94
- case FormatEnum.SingleSelect:
95
- case FormatEnum.MultiSelect: {
96
- return '';
124
+ case FormatEnum.Ref: {
125
+ if (!field.referencePath) {
126
+ return ''; // TODO(burdon): Show error.
97
127
  }
98
- case FormatEnum.Ref: {
99
- if (!field.referencePath) {
100
- return ''; // TODO(burdon): Show error.
101
- }
102
128
 
103
- return getValue(value.target, field.referencePath);
104
- }
129
+ return getValue(value.target, field.referencePath);
130
+ }
105
131
 
106
- default: {
107
- return formatForDisplay({ type: props.type, format: props.format, value });
108
- }
132
+ default: {
133
+ return formatForDisplay({ type: props.type, format: props.format, value });
109
134
  }
110
- },
111
- };
135
+ }
136
+ },
137
+ };
112
138
 
113
- const classes = [];
114
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
115
- if (formatClasses) {
116
- classes.push(formatClasses);
117
- }
118
- const rowSelectionClasses = cellClassesForRowSelection(
119
- this.model.selection.isObjectSelected(obj),
120
- this.model.selection.selectionMode,
121
- );
122
- if (rowSelectionClasses) {
123
- classes.push(rowSelectionClasses);
124
- }
125
- if (classes.length > 0) {
126
- cell.className = mx(classes.flat());
127
- }
139
+ const classes = [];
140
+ const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
141
+ if (formatClasses) {
142
+ classes.push(formatClasses);
143
+ }
144
+ const rowSelectionClasses = cellClassesForRowSelection(
145
+ this.model.selection.isObjectSelected(obj),
146
+ this.model.selection.selectionMode,
147
+ );
148
+ if (rowSelectionClasses) {
149
+ classes.push(rowSelectionClasses);
150
+ }
151
+ if (classes.length > 0) {
152
+ cell.className = mx(classes.flat());
153
+ }
128
154
 
129
- if (props.type === TypeEnum.Array) {
130
- const targetArray = getValue(obj, field.path);
131
- if (targetArray && Array.isArray(targetArray)) {
132
- // TODO(ZaymonFC): For arrays of objects, objects should have a 'label' annotation on the field
133
- // that can be used to render tags.
134
- // TODO(ZaymonFC): Move to util.
135
- const getLabel = (value: any) => {
136
- // If the value is falsy, return undefined
137
- // If the object is an array, check for 'name' property
138
- // If the object is a primitive type (string, number, boolean) stringify
139
- // If the object is an array return 'Array'
140
- if (!value) {
141
- return undefined;
142
- }
143
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
144
- return String(value);
145
- }
146
- if (Array.isArray(value)) {
147
- return 'Array';
148
- }
149
- if (typeof value === 'object' && value.name) {
150
- return value.name;
151
- }
152
- if (typeof value === 'object' && value.value) {
153
- return `${value.value}`;
154
- }
155
- return 'Object';
156
- };
157
-
158
- const tags = targetArray
159
- .map(getLabel)
160
- .filter(isNotNullable)
161
- .map((title) => {
162
- return `<span class="dx-tag rounded-xl" data-hue="neutral">${title}</span>`;
163
- })
164
- .join('');
155
+ // Arrays.
156
+ if (props.type === TypeEnum.Array) {
157
+ const targetArray = getValue(obj, field.path);
158
+ if (targetArray && Array.isArray(targetArray)) {
159
+ // TODO(ZaymonFC): For arrays of objects, objects should have a 'label' annotation on the field
160
+ // that can be used to render tags.
161
+ // TODO(ZaymonFC): Move to util.
162
+ const getLabel = (value: any) => {
163
+ // If the value is falsy, return undefined
164
+ // If the object is an array, check for 'name' property
165
+ // If the object is a primitive type (string, number, boolean) stringify
166
+ // If the object is an array return 'Array'
167
+ if (!value) {
168
+ return undefined;
169
+ }
170
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
171
+ return String(value);
172
+ }
173
+ if (Array.isArray(value)) {
174
+ return 'Array';
175
+ }
176
+ if (typeof value === 'object' && value.name) {
177
+ return value.name;
178
+ }
179
+ if (typeof value === 'object' && value.value) {
180
+ return `${value.value}`;
181
+ }
182
+ return 'Object';
183
+ };
184
+
185
+ const tags = targetArray
186
+ .map(getLabel)
187
+ .filter(isNotNullable)
188
+ .map((title) => {
189
+ return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
190
+ })
191
+ .join('');
192
+
193
+ cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
194
+ }
195
+ }
165
196
 
166
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
167
- }
197
+ // References.
198
+ if (props.format === FormatEnum.Ref && props.referenceSchema) {
199
+ const targetObj = getValue(obj, field.path)?.target;
200
+ if (targetObj) {
201
+ const dxn = Obj.getDXN(targetObj)?.toString();
202
+ cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
168
203
  }
204
+ }
169
205
 
170
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
171
- const targetObj = getValue(obj, field.path)?.target;
172
- if (targetObj) {
173
- const dxn = getObjectDXN(targetObj)?.toString();
174
- cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
206
+ // Booleans.
207
+ if (props.format === FormatEnum.Boolean) {
208
+ const value = getValue(obj, field.path);
209
+ cell.accessoryHtml = tableControls.switch.render({
210
+ colIndex,
211
+ rowIndex: displayIndex,
212
+ checked: value ?? false,
213
+ });
214
+ cell.readonly = 'no-text-select';
215
+ }
216
+
217
+ // Single-Selects.
218
+ if (props.format === FormatEnum.SingleSelect) {
219
+ const value = getValue(obj, field.path);
220
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
221
+ if (options) {
222
+ const option = options.find((o) => o.id === value);
223
+ if (option) {
224
+ cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
175
225
  }
176
226
  }
227
+ }
177
228
 
178
- if (props.format === FormatEnum.Boolean) {
179
- const value = getValue(obj, field.path);
180
- cell.accessoryHtml = tableControls.switch.render({
181
- colIndex,
182
- rowIndex: displayIndex,
183
- checked: value ?? false,
184
- });
185
- cell.readonly = 'no-text-select';
186
- }
229
+ // Multi-Selects.
230
+ if (props.format === FormatEnum.MultiSelect) {
231
+ const values = getValue(obj, field.path) as string[] | undefined;
232
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
233
+ if (options && values && values.length > 0) {
234
+ const tags = values
235
+ .map((value) => {
236
+ const option = options.find((o) => o.id === value);
237
+ if (option) {
238
+ return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
239
+ }
240
+ return null;
241
+ })
242
+ .filter(Boolean)
243
+ .join('');
187
244
 
188
- if (props.format === FormatEnum.SingleSelect) {
189
- const value = getValue(obj, field.path);
190
- const options = this.model.projection.getFieldProjection(field.id).props.options;
191
- if (options) {
192
- const option = options.find((o) => o.id === value);
193
- if (option) {
194
- cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
195
- }
245
+ if (tags) {
246
+ cell.accessoryHtml = tags;
196
247
  }
197
248
  }
249
+ }
198
250
 
199
- if (props.format === FormatEnum.MultiSelect) {
200
- const values = getValue(obj, field.path) as string[] | undefined;
201
- const options = this.model.projection.getFieldProjection(field.id).props.options;
202
- if (options && values && values.length > 0) {
203
- const tags = values
204
- .map((value) => {
205
- const option = options.find((o) => o.id === value);
206
- if (option) {
207
- return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
208
- }
209
- return null;
210
- })
211
- .filter(Boolean)
212
- .join(' ');
251
+ const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
252
+ cells[idx] = cell;
253
+ }
213
254
 
214
- if (tags) {
215
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1">${tags}</div>`;
216
- }
255
+ private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
256
+ const cells: DxGridPlaneCells = {};
257
+ const fields = this.model.projection?.fields ?? [];
258
+
259
+ for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
260
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
261
+ const field = fields[col];
262
+ if (!field) {
263
+ continue;
217
264
  }
265
+
266
+ this.createDataCell(cells, this.model.rows.value[row], field, col, row);
218
267
  }
268
+ }
219
269
 
220
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
221
- cells[idx] = cell;
222
- };
270
+ return cells;
271
+ }
223
272
 
224
- for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
273
+ private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
274
+ const cells: DxGridPlaneCells = {};
275
+ const fields = this.model.projection?.fields ?? [];
276
+ const draftRows = this.model.draftRows.value;
277
+
278
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
279
+ const draftRow = draftRows[row];
225
280
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
226
281
  const field = fields[col];
227
282
  if (!field) {
228
283
  continue;
229
284
  }
230
285
 
231
- addCell(this.model.rows.value[row], field, col, row);
286
+ this.createDataCell(cells, draftRow.data, field, col, row);
287
+
288
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
289
+ const cellIndex = toPlaneCellIndex({ col, row });
290
+ const cellValue = cells[cellIndex];
291
+ if (cellValue) {
292
+ const existingClasses = cellValue.className || '';
293
+ const draftClasses = 'dx-grid__cell--flagged';
294
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
295
+ }
296
+ }
232
297
  }
233
298
  }
234
299
 
@@ -237,7 +302,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
237
302
 
238
303
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
239
304
  const cells: DxGridPlaneCells = {};
240
- const fields = this.model.view?.fields ?? [];
305
+ const fields = this.model.projection?.fields ?? [];
241
306
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
242
307
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
243
308
  const sorting = this.model.sorting?.sorting;
@@ -321,7 +386,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
321
386
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
322
387
  accessoryHtml: this.model.features.schemaEditable
323
388
  ? tableButtons.addColumn.render({
324
- disabled: (this.model.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
389
+ disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
325
390
  })
326
391
  : undefined,
327
392
  className: '!bg-toolbarSurface',
@@ -330,6 +395,24 @@ export class TablePresentation<T extends TableRow = TableRow> {
330
395
  },
331
396
  };
332
397
  }
398
+
399
+ private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
400
+ const cells: DxGridPlaneCells = {};
401
+ const draftRows = this.model.draftRows.value;
402
+
403
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
404
+ const draftRow = draftRows[row];
405
+ const disabled = !draftRow.valid;
406
+
407
+ cells[toPlaneCellIndex({ col: 0, row })] = {
408
+ value: '',
409
+ readonly: true,
410
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
411
+ };
412
+ }
413
+
414
+ return cells;
415
+ }
333
416
  }
334
417
 
335
418
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
@@ -344,7 +427,7 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
344
427
  switch (selectionMode) {
345
428
  case 'single':
346
429
  // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
347
- return ['!bg-currentRelated hover:bg-hoverSurface', '!cursor-pointer'];
430
+ return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
348
431
  case 'multiple':
349
432
  return ['!bg-gridCellSelected'];
350
433
  }
@@ -7,7 +7,7 @@ import orderBy from 'lodash.orderby';
7
7
 
8
8
  import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
9
9
  import { formatForDisplay } from '@dxos/react-ui-form';
10
- import type { PropertyType, FieldType, ViewProjection, ViewType } from '@dxos/schema';
10
+ import type { PropertyType, FieldType, DataType, ProjectionModel } from '@dxos/schema';
11
11
 
12
12
  import { type TableRow } from './table-model';
13
13
 
@@ -49,13 +49,13 @@ export class TableSorting<T extends TableRow> {
49
49
 
50
50
  constructor(
51
51
  rows: Signal<T[]>,
52
- private readonly _view: ViewType | undefined,
53
- private readonly _projection: ViewProjection,
52
+ private readonly _view: DataType.View,
53
+ private readonly _projection: ProjectionModel,
54
54
  ) {
55
55
  this._rows = rows;
56
56
  this._isDirty = computed(() => {
57
57
  const local = this._localSort.value;
58
- const viewSort = this._view?.query.sort?.[0];
58
+ const viewSort = this._view.query.sort?.[0];
59
59
  if (local?.type === 'cleared') {
60
60
  return viewSort !== undefined;
61
61
  }
@@ -83,7 +83,7 @@ export class TableSorting<T extends TableRow> {
83
83
  return local.sort;
84
84
  }
85
85
 
86
- return this._view?.query.sort?.[0];
86
+ return this._view.query.sort?.[0];
87
87
  }
88
88
 
89
89
  /**
@@ -106,7 +106,7 @@ export class TableSorting<T extends TableRow> {
106
106
  }
107
107
 
108
108
  public toggleSort(fieldId: string): void {
109
- if (!this._view || !this.sorting || this.sorting.fieldId !== fieldId) {
109
+ if (!this.sorting || this.sorting.fieldId !== fieldId) {
110
110
  return;
111
111
  }
112
112
 
@@ -127,7 +127,7 @@ export class TableSorting<T extends TableRow> {
127
127
  }
128
128
 
129
129
  public save(): void {
130
- if (this._view && this._localSort.value !== undefined) {
130
+ if (this._localSort.value !== undefined) {
131
131
  if (this._localSort.value.type === 'active') {
132
132
  this._view.query.sort = [this._localSort.value.sort];
133
133
  } else {
@@ -145,11 +145,11 @@ export class TableSorting<T extends TableRow> {
145
145
  return computed(() => {
146
146
  this._displayToDataIndex.clear();
147
147
  const sort = this.sorting;
148
- if (!sort || !this._view) {
148
+ if (!sort) {
149
149
  return this._rows.value;
150
150
  }
151
151
 
152
- const field = this._view.fields.find((f) => f.id === sort.fieldId);
152
+ const field = this._projection.fields.find((f) => f.id === sort.fieldId);
153
153
  if (!field) {
154
154
  return this._rows.value;
155
155
  }
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { defineConfig } from '@playwright/test';
6
+
7
+ import { e2ePreset } from '@dxos/test-utils/playwright';
8
+
9
+ export default defineConfig({
10
+ ...e2ePreset(import.meta.dirname),
11
+ // TODO(wittjosiah): Avoid hard-coding ports.
12
+ webServer: {
13
+ command: 'moon run storybook:serve-e2e -- --port=9004',
14
+ port: 9004,
15
+ reuseExistingServer: false,
16
+ },
17
+ });
@@ -8,7 +8,7 @@ import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
8
8
 
9
9
  import { TableManager } from './TableManager';
10
10
 
11
- const storyUrl = storybookUrl('ui-react-ui-table-table--default');
11
+ const storyUrl = storybookUrl('ui-react-ui-table-table--default', 9004);
12
12
 
13
13
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
14
14
  test.describe('Table', () => {
@@ -28,12 +28,12 @@ test.describe('Table', () => {
28
28
 
29
29
  await table.grid.ready();
30
30
  await table.sortColumn(0, 'descending');
31
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Uwe Øvergård');
32
- await expect(table.grid.cell(0, 9, 'grid')).toContainText('Anita');
31
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sit.');
32
+ await expect(table.grid.cell(0, 9, 'grid')).toContainText('Aut.');
33
33
 
34
34
  await table.sortColumn(0, 'ascending');
35
- await expect(table.grid.cell(0, 0, 'grid')).toContainText('Anita');
36
- await expect(table.grid.cell(0, 9, 'grid')).toHaveText('Uwe Øvergård');
35
+ await expect(table.grid.cell(0, 0, 'grid')).toContainText('Aut.');
36
+ await expect(table.grid.cell(0, 9, 'grid')).toHaveText('Sit.');
37
37
  await page.close();
38
38
  });
39
39
 
@@ -65,7 +65,7 @@ test.describe('Table', () => {
65
65
 
66
66
  await table.grid.ready();
67
67
  await table.deleteRow(0);
68
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
68
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
69
69
  await page.close();
70
70
  });
71
71
 
@@ -80,8 +80,8 @@ test.describe('Table', () => {
80
80
  // Delete action affects all selected rows.
81
81
  await table.deleteRow(0);
82
82
 
83
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toHaveCount(0);
84
- await expect(page.getByRole('gridcell', { name: 'Phonthip Sigurjónsson' })).toHaveCount(0);
83
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
84
+ await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
85
85
  await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
86
86
  await page.close();
87
87
  });
@@ -112,7 +112,6 @@ test.describe('Table', () => {
112
112
  });
113
113
 
114
114
  // Rest of add column test remains the same as it's a more complex flow.
115
- // TODO(ZaymonFC): Restore this after fixing format selection.
116
115
  test('add column', async ({ browser, browserName }) => {
117
116
  test.skip(browserName === 'webkit');
118
117
  const { page } = await setupPage(browser, { url: storyUrl });
@@ -139,19 +138,21 @@ test.describe('Table', () => {
139
138
  await table.grid.panByWheel(10000, 0);
140
139
 
141
140
  await table.grid.cell(4, 0, 'grid').click();
142
- await page.keyboard.press('A');
143
- await page.keyboard.press('N');
144
- await page.getByRole('option', { name: 'Anita Mayer' }).click();
141
+ // TODO(wittjosiah): Surprisingly long delay needed here.
142
+ await page.keyboard.press('A', { delay: 1_000 });
143
+ await page.keyboard.press('u', { delay: 1_000 });
144
+ await page.getByRole('option', { name: 'Aut.' }).click();
145
145
 
146
146
  // Assert that the value is shown in the cell.
147
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' }).first()).toBeVisible();
147
+ await expect(page.getByRole('gridcell', { name: 'Aut.' }).first()).toBeVisible();
148
148
 
149
149
  // Create new object.
150
150
  await table.grid.cell(4, 1, 'grid').click();
151
- await page.keyboard.type('t');
152
- await page.keyboard.type('e');
153
- await page.keyboard.type('s');
154
- await page.keyboard.type('t');
151
+ // TODO(wittjosiah): Surprisingly long delay needed here.
152
+ await page.keyboard.type('t', { delay: 1_000 });
153
+ await page.keyboard.type('e', { delay: 1_000 });
154
+ await page.keyboard.type('s', { delay: 1_000 });
155
+ await page.keyboard.type('t', { delay: 1_000 });
155
156
  await page.getByText('Create new object', { exact: false }).click();
156
157
  await page.getByTestId('save-button').click();
157
158
 
@@ -172,13 +173,13 @@ test.describe('Table', () => {
172
173
  await page.getByTestId('table-switch').nth(7).click();
173
174
 
174
175
  // Test that checks are durable in the data model by sorting.
175
- await table.sortColumn(3, 'descending');
176
+ await table.sortColumn(1, 'descending');
176
177
 
177
178
  // Assert the first two switch checkboxes are checked.
178
179
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
179
180
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
180
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Anita Mayer');
181
- await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Uwe Øvergård');
181
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
182
+ await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
182
183
 
183
184
  await page.close();
184
185
  });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './useTestTableModel';