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

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 +814 -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 +814 -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 +167 -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
@@ -3,17 +3,18 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
 
8
- import { Obj, Ref } from '@dxos/echo';
8
+ import { Filter, Query } from '@dxos/echo';
9
9
  import { TypedObject } from '@dxos/echo-schema';
10
10
  import { live } from '@dxos/live-object';
11
11
  import { createEchoSchema } from '@dxos/live-object/testing';
12
- import { createView, ViewProjection } from '@dxos/schema';
12
+ import { createView } from '@dxos/schema';
13
+
14
+ import { Table } from '../types';
13
15
 
14
16
  import { TableModel, type TableModelProps } from './table-model';
15
17
  import { TablePresentation } from './table-presentation';
16
- import { TableType } from '../types';
17
18
 
18
19
  describe('TablePresentation', () => {
19
20
  describe('row reactivity', () => {
@@ -97,8 +98,11 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
97
98
 
98
99
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
99
100
  const schema = createEchoSchema(Test);
100
- const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
101
- const projection = new ViewProjection(schema.jsonSchema, view);
102
- const table = Obj.make(TableType, { view: Ref.make(view) });
103
- return new TableModel({ id: table.id, view, projection, ...props });
101
+ const table = Table.make();
102
+ const view = createView({
103
+ query: Query.select(Filter.type(schema)),
104
+ jsonSchema: schema.jsonSchema,
105
+ presentation: table,
106
+ });
107
+ return new TableModel({ view, schema: schema.jsonSchema, ...props });
104
108
  };
@@ -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.getDraftIconCells(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,155 +87,180 @@ 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(props);
128
145
 
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('');
146
+ if (formatClasses) {
147
+ classes.push(formatClasses);
148
+ }
165
149
 
166
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
167
- }
168
- }
150
+ const rowSelectionClasses = cellClassesForRowSelection(
151
+ this.model.selection.isObjectSelected(obj),
152
+ this.model.selection.selectionMode,
153
+ );
169
154
 
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>`;
175
- }
155
+ if (rowSelectionClasses) {
156
+ classes.push(rowSelectionClasses);
157
+ }
158
+
159
+ cell.className = mx(classes);
160
+
161
+ // Arrays.
162
+ if (props.type === TypeEnum.Array) {
163
+ const targetArray = getValue(obj, field.path);
164
+ if (targetArray && Array.isArray(targetArray)) {
165
+ // TODO(ZaymonFC): For arrays of objects, objects should have a 'label' annotation on the field
166
+ // that can be used to render tags.
167
+ // TODO(ZaymonFC): Move to util.
168
+ const getLabel = (value: any) => {
169
+ // If the value is falsy, return undefined
170
+ // If the object is an array, check for 'name' property
171
+ // If the object is a primitive type (string, number, boolean) stringify
172
+ // If the object is an array return 'Array'
173
+ if (!value) {
174
+ return undefined;
175
+ }
176
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
177
+ return String(value);
178
+ }
179
+ if (Array.isArray(value)) {
180
+ return 'Array';
181
+ }
182
+ if (typeof value === 'object' && value.name) {
183
+ return value.name;
184
+ }
185
+ if (typeof value === 'object' && value.value) {
186
+ return `${value.value}`;
187
+ }
188
+ return 'Object';
189
+ };
190
+
191
+ const tags = targetArray
192
+ .map(getLabel)
193
+ .filter(isNotNullable)
194
+ .map((title) => {
195
+ return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
196
+ })
197
+ .join('');
198
+
199
+ cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
176
200
  }
201
+ }
177
202
 
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';
203
+ // References.
204
+ if (props.format === FormatEnum.Ref && props.referenceSchema) {
205
+ const targetObj = getValue(obj, field.path)?.target;
206
+ if (targetObj) {
207
+ const dxn = Obj.getDXN(targetObj)?.toString();
208
+ cell.accessoryHtml = `<dx-anchor 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-anchor>`;
186
209
  }
210
+ }
187
211
 
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
- }
212
+ // Booleans.
213
+ if (props.format === FormatEnum.Boolean) {
214
+ const value = getValue(obj, field.path);
215
+ cell.accessoryHtml = tableControls.switch.render({
216
+ colIndex,
217
+ rowIndex: displayIndex,
218
+ checked: value ?? false,
219
+ });
220
+ cell.readonly = 'no-text-select';
221
+ }
222
+
223
+ // Single-Selects.
224
+ if (props.format === FormatEnum.SingleSelect) {
225
+ const value = getValue(obj, field.path);
226
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
227
+ if (options) {
228
+ const option = options.find((o) => o.id === value);
229
+ if (option) {
230
+ cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
196
231
  }
197
232
  }
233
+ }
198
234
 
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(' ');
235
+ // Multi-Selects.
236
+ if (props.format === FormatEnum.MultiSelect) {
237
+ const values = getValue(obj, field.path) as string[] | undefined;
238
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
239
+ if (options && values && values.length > 0) {
240
+ const tags = values
241
+ .map((value) => {
242
+ const option = options.find((o) => o.id === value);
243
+ if (option) {
244
+ return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
245
+ }
246
+ return null;
247
+ })
248
+ .filter(Boolean)
249
+ .join('');
213
250
 
214
- if (tags) {
215
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1">${tags}</div>`;
216
- }
251
+ if (tags) {
252
+ cell.accessoryHtml = tags;
217
253
  }
218
254
  }
255
+ }
219
256
 
220
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
221
- cells[idx] = cell;
222
- };
257
+ const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
258
+ cells[idx] = cell;
259
+ }
260
+
261
+ private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
262
+ const cells: DxGridPlaneCells = {};
263
+ const fields = this.model.projection?.fields ?? [];
223
264
 
224
265
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
225
266
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
@@ -228,7 +269,48 @@ export class TablePresentation<T extends TableRow = TableRow> {
228
269
  continue;
229
270
  }
230
271
 
231
- addCell(this.model.rows.value[row], field, col, row);
272
+ this.createDataCell(cells, this.model.rows.value[row], field, col, row);
273
+ }
274
+ }
275
+
276
+ return cells;
277
+ }
278
+
279
+ private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
280
+ const cells: DxGridPlaneCells = {};
281
+ const fields = this.model.projection?.fields ?? [];
282
+ const draftRows = this.model.draftRows.value;
283
+
284
+ // Return cells of the CTA row if no draft row is active
285
+ if (draftRows.length === 0) {
286
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
287
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
288
+ value: '',
289
+ readonly: true,
290
+ className: 'dx-grid__row--cta__cell',
291
+ };
292
+ }
293
+ } else {
294
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
295
+ const draftRow = draftRows[row];
296
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
297
+ const cellIndex = toPlaneCellIndex({ col, row });
298
+ const field = fields[col];
299
+ if (!field) {
300
+ continue;
301
+ }
302
+
303
+ this.createDataCell(cells, draftRow.data, field, col, row);
304
+
305
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
306
+ const cellValue = cells[cellIndex];
307
+ if (cellValue) {
308
+ const existingClasses = cellValue.className || '';
309
+ const draftClasses = 'dx-grid__cell--flagged';
310
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
311
+ }
312
+ }
313
+ }
232
314
  }
233
315
  }
234
316
 
@@ -237,7 +319,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
237
319
 
238
320
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
239
321
  const cells: DxGridPlaneCells = {};
240
- const fields = this.model.view?.fields ?? [];
322
+ const fields = this.model.projection?.fields ?? [];
241
323
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
242
324
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
243
325
  const sorting = this.model.sorting?.sorting;
@@ -245,14 +327,14 @@ export class TablePresentation<T extends TableRow = TableRow> {
245
327
 
246
328
  cells[toPlaneCellIndex({ col, row: 0 })] = {
247
329
  // TODO(burdon): Use same logic as form for fallback title.
248
- value: props.title ?? field.path,
249
- readonly: true,
330
+ value: '',
250
331
  resizeHandle: 'col',
251
332
  accessoryHtml: `
333
+ <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
252
334
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
253
335
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
254
336
  `,
255
- className: '!bg-toolbarSurface',
337
+ className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
256
338
  };
257
339
  }
258
340
 
@@ -321,7 +403,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
321
403
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
322
404
  accessoryHtml: this.model.features.schemaEditable
323
405
  ? tableButtons.addColumn.render({
324
- disabled: (this.model.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
406
+ disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
325
407
  })
326
408
  : undefined,
327
409
  className: '!bg-toolbarSurface',
@@ -330,6 +412,49 @@ export class TablePresentation<T extends TableRow = TableRow> {
330
412
  },
331
413
  };
332
414
  }
415
+
416
+ private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
417
+ const cells: DxGridPlaneCells = {};
418
+ const draftRows = this.model.draftRows.value;
419
+
420
+ // Return cells of the CTA row if no draft row is active
421
+ if (draftRows.length === 0) {
422
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
423
+ value: '',
424
+ className: 'dx-grid__row--cta__cell',
425
+ readonly: true,
426
+ };
427
+ } else {
428
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
429
+ const draftRow = draftRows[row];
430
+ const disabled = !draftRow.valid;
431
+
432
+ cells[toPlaneCellIndex({ col: 0, row })] = {
433
+ value: '',
434
+ readonly: true,
435
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
436
+ };
437
+ }
438
+ }
439
+
440
+ return cells;
441
+ }
442
+
443
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
+ const cells: DxGridPlaneCells = {};
445
+ const draftRows = this.model.draftRows.value;
446
+
447
+ for (let row = range.start.row; row <= range.end.row; row++) {
448
+ cells[toPlaneCellIndex({ col: 0, row })] = {
449
+ value: '',
450
+ readonly: true,
451
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
452
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
453
+ };
454
+ }
455
+
456
+ return cells;
457
+ }
333
458
  }
334
459
 
335
460
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
@@ -344,7 +469,7 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
344
469
  switch (selectionMode) {
345
470
  case 'single':
346
471
  // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
347
- return ['!bg-currentRelated hover:bg-hoverSurface', '!cursor-pointer'];
472
+ return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
348
473
  case 'multiple':
349
474
  return ['!bg-gridCellSelected'];
350
475
  }
@@ -2,15 +2,24 @@
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 { FormatEnum, 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 {
11
+ DataType,
12
+ FieldSortType,
13
+ FieldType,
14
+ ProjectionModel,
15
+ PropertyType,
16
+ SortDirectionType,
17
+ } from '@dxos/schema';
11
18
 
12
19
  import { type TableRow } from './table-model';
13
20
 
21
+ // TODO(wittjosiah): Remove. Use ordering in queries instead.
22
+
14
23
  /**
15
24
  * Represents the local sort state.
16
25
  * - undefined: No local sort set, fallback to view sort.
@@ -49,13 +58,13 @@ export class TableSorting<T extends TableRow> {
49
58
 
50
59
  constructor(
51
60
  rows: Signal<T[]>,
52
- private readonly _view: ViewType | undefined,
53
- private readonly _projection: ViewProjection,
61
+ private readonly _view: DataType.View,
62
+ private readonly _projection: ProjectionModel,
54
63
  ) {
55
64
  this._rows = rows;
56
65
  this._isDirty = computed(() => {
57
66
  const local = this._localSort.value;
58
- const viewSort = this._view?.query.sort?.[0];
67
+ const viewSort = this._view.sort?.[0];
59
68
  if (local?.type === 'cleared') {
60
69
  return viewSort !== undefined;
61
70
  }
@@ -83,7 +92,7 @@ export class TableSorting<T extends TableRow> {
83
92
  return local.sort;
84
93
  }
85
94
 
86
- return this._view?.query.sort?.[0];
95
+ return this._view.sort?.[0];
87
96
  }
88
97
 
89
98
  /**
@@ -106,7 +115,7 @@ export class TableSorting<T extends TableRow> {
106
115
  }
107
116
 
108
117
  public toggleSort(fieldId: string): void {
109
- if (!this._view || !this.sorting || this.sorting.fieldId !== fieldId) {
118
+ if (!this.sorting || this.sorting.fieldId !== fieldId) {
110
119
  return;
111
120
  }
112
121
 
@@ -127,11 +136,11 @@ export class TableSorting<T extends TableRow> {
127
136
  }
128
137
 
129
138
  public save(): void {
130
- if (this._view && this._localSort.value !== undefined) {
139
+ if (this._localSort.value !== undefined) {
131
140
  if (this._localSort.value.type === 'active') {
132
- this._view.query.sort = [this._localSort.value.sort];
141
+ this._view.sort = [this._localSort.value.sort];
133
142
  } else {
134
- this._view.query.sort = [];
143
+ this._view.sort = [];
135
144
  }
136
145
  this._localSort.value = undefined;
137
146
  }
@@ -145,11 +154,11 @@ export class TableSorting<T extends TableRow> {
145
154
  return computed(() => {
146
155
  this._displayToDataIndex.clear();
147
156
  const sort = this.sorting;
148
- if (!sort || !this._view) {
157
+ if (!sort) {
149
158
  return this._rows.value;
150
159
  }
151
160
 
152
- const field = this._view.fields.find((f) => f.id === sort.fieldId);
161
+ const field = this._projection.fields.find((f) => f.id === sort.fieldId);
153
162
  if (!field) {
154
163
  return this._rows.value;
155
164
  }
@@ -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
+ });