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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/lib/browser/chunk-L2LSB6KV.mjs +18 -0
  2. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
  4. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +728 -2104
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +123 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
  14. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +728 -2104
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +124 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +1 -1
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.d.ts +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
  32. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  34. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.d.ts +1 -0
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  38. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -40
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +1 -0
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +1 -2
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts +2 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +58 -21
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +10 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +4 -4
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  69. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  70. package/dist/types/src/testing/index.d.ts +2 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  73. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +5 -6
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/index.d.ts +1 -1
  77. package/dist/types/src/types/index.d.ts.map +1 -1
  78. package/dist/types/src/types/schema.d.ts +8 -0
  79. package/dist/types/src/types/schema.d.ts.map +1 -0
  80. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  81. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts +15 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/src/util/table.d.ts +9 -11
  86. package/dist/types/src/util/table.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +52 -40
  89. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  90. package/src/components/Table/ColumnSettings.tsx +15 -11
  91. package/src/components/Table/CreateRefPanel.tsx +6 -4
  92. package/src/components/Table/DynamicTable.stories.tsx +4 -3
  93. package/src/components/Table/DynamicTable.tsx +17 -10
  94. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
  95. package/src/components/Table/RowActionsMenu.tsx +3 -3
  96. package/src/components/Table/Table.stories.tsx +109 -148
  97. package/src/components/Table/Table.tsx +95 -66
  98. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  99. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  100. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
  101. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
  102. package/src/components/TableCellEditor/extension.ts +2 -2
  103. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  104. package/src/components/testing.ts +7 -24
  105. package/src/hooks/index.ts +1 -0
  106. package/src/hooks/useAddRow.ts +36 -0
  107. package/src/hooks/useTableModel.ts +21 -17
  108. package/src/index.ts +1 -2
  109. package/src/model/modal-controller.ts +1 -1
  110. package/src/model/selection-model.ts +6 -3
  111. package/src/model/table-model.test.ts +8 -8
  112. package/src/model/table-model.ts +278 -86
  113. package/src/model/table-presentation.test.ts +8 -8
  114. package/src/model/table-presentation.ts +240 -135
  115. package/src/model/table-sorting.ts +11 -11
  116. package/src/playwright/playwright.config.ts +17 -0
  117. package/src/playwright/smoke.spec.ts +113 -20
  118. package/src/testing/index.ts +5 -0
  119. package/src/testing/useTestTableModel.ts +123 -0
  120. package/src/translations.ts +7 -5
  121. package/src/types/index.ts +2 -2
  122. package/src/types/schema.ts +16 -0
  123. package/src/util/dynamic-table.ts +20 -18
  124. package/src/util/schema.ts +1 -1
  125. package/src/util/table-buttons.ts +26 -2
  126. package/src/util/table-controls.ts +4 -1
  127. package/src/util/table.ts +37 -111
  128. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  129. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  130. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  131. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  132. package/dist/lib/node/index.cjs +0 -2739
  133. package/dist/lib/node/index.cjs.map +0 -7
  134. package/dist/lib/node/meta.json +0 -1
  135. package/dist/lib/node/types/index.cjs +0 -31
  136. package/dist/lib/node/types/index.cjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  138. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  139. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  140. package/dist/types/src/types/table.d.ts +0 -56
  141. package/dist/types/src/types/table.d.ts.map +0 -1
  142. package/src/playwright/playwright.config.cts +0 -18
  143. package/src/types/table.ts +0 -27
@@ -5,28 +5,32 @@
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, TypeEnum, getValue } 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';
18
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
19
20
 
20
- import { type SelectionMode } from './selection-model';
21
- import { type TableRow, type TableModel } from './table-model';
22
21
  import { tableButtons, tableControls } from '../util';
23
22
 
23
+ import { type SelectionMode } from './selection-model';
24
+ import { type TableModel, type TableRow } from './table-model';
25
+
24
26
  /**
25
27
  * Presentation layer for a table component, handling cell rendering and grid display logic.
26
28
  * Manages rendering of table cells, headers, selection columns, and action columns across
27
29
  * different grid planes.
28
30
  */
29
31
  export class TablePresentation<T extends TableRow = TableRow> {
32
+ private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
33
+
30
34
  constructor(
31
35
  private readonly model: TableModel<T>,
32
36
  private readonly _visibleRange = signal<DxGridPlaneRange>({
@@ -36,6 +40,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
36
40
  ) {}
37
41
 
38
42
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
43
+ // Clear cache at the start of each render pass
44
+ this.fieldProjectionCache.clear();
45
+
39
46
  let cells: DxGridPlaneCells;
40
47
 
41
48
  switch (plane) {
@@ -58,12 +65,21 @@ export class TablePresentation<T extends TableRow = TableRow> {
58
65
  case 'fixedStartEnd':
59
66
  cells = this.getNewColumnCell();
60
67
  break;
68
+ case 'frozenRowsEnd':
69
+ cells = this.getDraftRowCells(range);
70
+ break;
71
+ case 'fixedEndStart':
72
+ cells = this.getDraftSelectCells(range);
73
+ break;
74
+ case 'fixedEndEnd':
75
+ cells = this.getDraftActionCells(range);
76
+ break;
61
77
  default:
62
78
  cells = {};
63
79
  }
64
80
 
65
81
  if (plane === 'grid' && this.model.features.dataEditable === false) {
66
- Object.values(cells).forEach((cell) => {
82
+ Object.values<DxGridCellValue>(cells).forEach((cell) => {
67
83
  cell.readonly = 'text-select';
68
84
  });
69
85
  }
@@ -71,164 +87,219 @@ export class TablePresentation<T extends TableRow = TableRow> {
71
87
  return cells;
72
88
  }
73
89
 
74
- private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
75
- const cells: DxGridPlaneCells = {};
76
- const fields = this.model.view?.fields ?? [];
90
+ /**
91
+ * Gets a field projection, using cache if available within the current render pass.
92
+ */
93
+ private getFieldProjection(fieldId: string) {
94
+ let projection = this.fieldProjectionCache.get(fieldId);
95
+ if (!projection) {
96
+ projection = this.model.projection.getFieldProjection(fieldId);
97
+ this.fieldProjectionCache.set(fieldId, projection);
98
+ }
99
+ return projection;
100
+ }
77
101
 
78
- const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
79
- const { props } = this.model.projection.getFieldProjection(field.id);
102
+ private createDataCell(
103
+ cells: DxGridPlaneCells,
104
+ obj: T,
105
+ field: FieldType,
106
+ colIndex: number,
107
+ displayIndex: number,
108
+ ): void {
109
+ const { props } = this.getFieldProjection(field.id);
110
+
111
+ const cell: DxGridCellValue = {
112
+ get value() {
113
+ const value = getValue(obj, field.path);
114
+ if (value == null) {
115
+ return '';
116
+ }
80
117
 
81
- const cell: DxGridCellValue = {
82
- get value() {
83
- const value = getValue(obj, field.path);
84
- if (value == null) {
85
- return '';
86
- }
118
+ if (props.type === 'array') {
119
+ return '';
120
+ }
87
121
 
88
- if (props.type === 'array') {
122
+ switch (props.format) {
123
+ case FormatEnum.Boolean:
124
+ case FormatEnum.SingleSelect:
125
+ case FormatEnum.MultiSelect: {
89
126
  return '';
90
127
  }
91
-
92
- switch (props.format) {
93
- case FormatEnum.Boolean:
94
- case FormatEnum.SingleSelect:
95
- case FormatEnum.MultiSelect: {
96
- return '';
128
+ case FormatEnum.Ref: {
129
+ if (!field.referencePath) {
130
+ return ''; // TODO(burdon): Show error.
97
131
  }
98
- case FormatEnum.Ref: {
99
- if (!field.referencePath) {
100
- return ''; // TODO(burdon): Show error.
101
- }
102
132
 
103
- return getValue(value.target, field.referencePath);
104
- }
133
+ return getValue(value.target, field.referencePath);
134
+ }
105
135
 
106
- default: {
107
- return formatForDisplay({ type: props.type, format: props.format, value });
108
- }
136
+ default: {
137
+ return formatForDisplay({ type: props.type, format: props.format, value });
109
138
  }
110
- },
111
- };
139
+ }
140
+ },
141
+ };
112
142
 
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
- }
143
+ const classes = [];
144
+ const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
145
+ if (formatClasses) {
146
+ classes.push(formatClasses);
147
+ }
148
+ const rowSelectionClasses = cellClassesForRowSelection(
149
+ this.model.selection.isObjectSelected(obj),
150
+ this.model.selection.selectionMode,
151
+ );
152
+ if (rowSelectionClasses) {
153
+ classes.push(rowSelectionClasses);
154
+ }
155
+ if (classes.length > 0) {
156
+ cell.className = mx(classes.flat());
157
+ }
128
158
 
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('');
159
+ // Arrays.
160
+ if (props.type === TypeEnum.Array) {
161
+ const targetArray = getValue(obj, field.path);
162
+ if (targetArray && Array.isArray(targetArray)) {
163
+ // TODO(ZaymonFC): For arrays of objects, objects should have a 'label' annotation on the field
164
+ // that can be used to render tags.
165
+ // TODO(ZaymonFC): Move to util.
166
+ const getLabel = (value: any) => {
167
+ // If the value is falsy, return undefined
168
+ // If the object is an array, check for 'name' property
169
+ // If the object is a primitive type (string, number, boolean) stringify
170
+ // If the object is an array return 'Array'
171
+ if (!value) {
172
+ return undefined;
173
+ }
174
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
175
+ return String(value);
176
+ }
177
+ if (Array.isArray(value)) {
178
+ return 'Array';
179
+ }
180
+ if (typeof value === 'object' && value.name) {
181
+ return value.name;
182
+ }
183
+ if (typeof value === 'object' && value.value) {
184
+ return `${value.value}`;
185
+ }
186
+ return 'Object';
187
+ };
188
+
189
+ const tags = targetArray
190
+ .map(getLabel)
191
+ .filter(isNotNullable)
192
+ .map((title) => {
193
+ return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
194
+ })
195
+ .join('');
196
+
197
+ cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
198
+ }
199
+ }
165
200
 
166
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
167
- }
201
+ // References.
202
+ if (props.format === FormatEnum.Ref && props.referenceSchema) {
203
+ const targetObj = getValue(obj, field.path)?.target;
204
+ if (targetObj) {
205
+ const dxn = Obj.getDXN(targetObj)?.toString();
206
+ cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-2 block-start-[.2rem]" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
168
207
  }
208
+ }
209
+
210
+ // Booleans.
211
+ if (props.format === FormatEnum.Boolean) {
212
+ const value = getValue(obj, field.path);
213
+ cell.accessoryHtml = tableControls.switch.render({
214
+ colIndex,
215
+ rowIndex: displayIndex,
216
+ checked: value ?? false,
217
+ });
218
+ cell.readonly = 'no-text-select';
219
+ }
169
220
 
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>`;
221
+ // Single-Selects.
222
+ if (props.format === FormatEnum.SingleSelect) {
223
+ const value = getValue(obj, field.path);
224
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
225
+ if (options) {
226
+ const option = options.find((o) => o.id === value);
227
+ if (option) {
228
+ cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
175
229
  }
176
230
  }
231
+ }
177
232
 
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
- }
233
+ // Multi-Selects.
234
+ if (props.format === FormatEnum.MultiSelect) {
235
+ const values = getValue(obj, field.path) as string[] | undefined;
236
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
237
+ if (options && values && values.length > 0) {
238
+ const tags = values
239
+ .map((value) => {
240
+ const option = options.find((o) => o.id === value);
241
+ if (option) {
242
+ return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
243
+ }
244
+ return null;
245
+ })
246
+ .filter(Boolean)
247
+ .join('');
187
248
 
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
- }
249
+ if (tags) {
250
+ cell.accessoryHtml = tags;
196
251
  }
197
252
  }
253
+ }
198
254
 
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(' ');
255
+ const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
256
+ cells[idx] = cell;
257
+ }
213
258
 
214
- if (tags) {
215
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1">${tags}</div>`;
216
- }
259
+ private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
260
+ const cells: DxGridPlaneCells = {};
261
+ const fields = this.model.projection?.fields ?? [];
262
+
263
+ for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
264
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
265
+ const field = fields[col];
266
+ if (!field) {
267
+ continue;
217
268
  }
269
+
270
+ this.createDataCell(cells, this.model.rows.value[row], field, col, row);
218
271
  }
272
+ }
219
273
 
220
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
221
- cells[idx] = cell;
222
- };
274
+ return cells;
275
+ }
223
276
 
224
- for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
277
+ private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
278
+ const cells: DxGridPlaneCells = {};
279
+ const fields = this.model.projection?.fields ?? [];
280
+ const draftRows = this.model.draftRows.value;
281
+
282
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
283
+ const draftRow = draftRows[row];
225
284
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
285
+ const cellIndex = toPlaneCellIndex({ col, row });
226
286
  const field = fields[col];
227
287
  if (!field) {
228
288
  continue;
229
289
  }
230
290
 
231
- addCell(this.model.rows.value[row], field, col, row);
291
+ this.createDataCell(cells, draftRow.data, field, col, row);
292
+
293
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
294
+ const cellValue = cells[cellIndex];
295
+ if (cellValue) {
296
+ const existingClasses = cellValue.className || '';
297
+ const draftClasses = 'dx-grid__cell--flagged';
298
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
299
+ }
300
+ }
301
+
302
+ cells[cellIndex].className += ' !bg-toolbarSurface';
232
303
  }
233
304
  }
234
305
 
@@ -237,7 +308,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
237
308
 
238
309
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
239
310
  const cells: DxGridPlaneCells = {};
240
- const fields = this.model.view?.fields ?? [];
311
+ const fields = this.model.projection?.fields ?? [];
241
312
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
242
313
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
243
314
  const sorting = this.model.sorting?.sorting;
@@ -321,7 +392,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
321
392
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
322
393
  accessoryHtml: this.model.features.schemaEditable
323
394
  ? tableButtons.addColumn.render({
324
- disabled: (this.model.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
395
+ disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
325
396
  })
326
397
  : undefined,
327
398
  className: '!bg-toolbarSurface',
@@ -330,6 +401,40 @@ export class TablePresentation<T extends TableRow = TableRow> {
330
401
  },
331
402
  };
332
403
  }
404
+
405
+ private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
406
+ const cells: DxGridPlaneCells = {};
407
+ const draftRows = this.model.draftRows.value;
408
+
409
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
410
+ const draftRow = draftRows[row];
411
+ const disabled = !draftRow.valid;
412
+
413
+ cells[toPlaneCellIndex({ col: 0, row })] = {
414
+ value: '',
415
+ readonly: true,
416
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
417
+ className: '!bg-toolbarSurface',
418
+ };
419
+ }
420
+
421
+ return cells;
422
+ }
423
+
424
+ private getDraftSelectCells(range: DxGridPlaneRange): DxGridPlaneCells {
425
+ const cells: DxGridPlaneCells = {};
426
+ const draftRows = this.model.draftRows.value;
427
+
428
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
429
+ cells[toPlaneCellIndex({ col: 0, row })] = {
430
+ value: '',
431
+ readonly: true,
432
+ className: '!bg-toolbarSurface',
433
+ };
434
+ }
435
+
436
+ return cells;
437
+ }
333
438
  }
334
439
 
335
440
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
@@ -344,7 +449,7 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
344
449
  switch (selectionMode) {
345
450
  case 'single':
346
451
  // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
347
- return ['!bg-currentRelated hover:bg-hoverSurface', '!cursor-pointer'];
452
+ return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
348
453
  case 'multiple':
349
454
  return ['!bg-gridCellSelected'];
350
455
  }
@@ -2,12 +2,12 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, signal, type Signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, type Signal, computed, signal } from '@preact/signals-core';
6
6
  import orderBy from 'lodash.orderby';
7
7
 
8
- import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
8
+ import { type FieldSortType, FormatEnum, type SortDirectionType, TypeEnum, getValue } 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 { DataType, FieldType, ProjectionModel, PropertyType } 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
+ });