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

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 (171) hide show
  1. package/dist/lib/browser/chunk-GTGYBRJA.mjs +1748 -0
  2. package/dist/lib/browser/chunk-GTGYBRJA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1156 -2508
  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 +114 -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-DPNSZHU5.mjs +1750 -0
  12. package/dist/lib/node-esm/chunk-DPNSZHU5.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1156 -2508
  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 +115 -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 +2 -3
  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/DynamicTable.d.ts +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +37 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +13 -9
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +14 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -18
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +26 -56
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -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/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +11 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/index.d.ts +1 -2
  60. package/dist/types/src/index.d.ts.map +1 -1
  61. package/dist/types/src/model/modal-controller.d.ts +5 -8
  62. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  63. package/dist/types/src/model/selection-model.d.ts +17 -7
  64. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-model.d.ts +149 -32
  66. package/dist/types/src/model/table-model.d.ts.map +1 -1
  67. package/dist/types/src/model/table-presentation.d.ts +13 -2
  68. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  69. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  70. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  71. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  72. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  73. package/dist/types/src/testing/index.d.ts +2 -0
  74. package/dist/types/src/testing/index.d.ts.map +1 -0
  75. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  76. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +5 -6
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/types/Table.d.ts +31 -0
  80. package/dist/types/src/types/Table.d.ts.map +1 -0
  81. package/dist/types/src/types/index.d.ts +1 -1
  82. package/dist/types/src/types/index.d.ts.map +1 -1
  83. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  84. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  85. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  86. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  87. package/dist/types/src/util/index.d.ts +2 -1
  88. package/dist/types/src/util/index.d.ts.map +1 -1
  89. package/dist/types/src/util/query-ast.d.ts +7 -0
  90. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  91. package/dist/types/src/util/schema.d.ts +1 -1
  92. package/dist/types/src/util/schema.d.ts.map +1 -1
  93. package/dist/types/src/util/sort.d.ts +12 -0
  94. package/dist/types/src/util/sort.d.ts.map +1 -0
  95. package/dist/types/src/util/table-buttons.d.ts +15 -1
  96. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  97. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  98. package/dist/types/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +69 -56
  100. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  101. package/src/components/Table/ColumnSettings.tsx +16 -12
  102. package/src/components/Table/DynamicTable.stories.tsx +29 -25
  103. package/src/components/Table/DynamicTable.tsx +34 -22
  104. package/src/components/Table/Relations.stories.tsx +237 -0
  105. package/src/components/Table/RowActionsMenu.tsx +26 -17
  106. package/src/components/Table/Table.stories.tsx +171 -194
  107. package/src/components/Table/Table.tsx +364 -286
  108. package/src/components/TableCellEditor/CellValidationMessage.tsx +8 -3
  109. package/src/components/TableCellEditor/FormCellEditor.tsx +146 -49
  110. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +36 -76
  111. package/src/components/TableCellEditor/TableCellEditor.tsx +106 -198
  112. package/src/components/TableCellEditor/index.ts +1 -0
  113. package/src/components/TableToolbar/TableToolbar.tsx +49 -33
  114. package/src/components/testing.ts +41 -32
  115. package/src/hooks/index.ts +2 -0
  116. package/src/hooks/useAddRow.ts +36 -0
  117. package/src/hooks/useProjectionModel.ts +39 -0
  118. package/src/hooks/useTableModel.ts +41 -35
  119. package/src/index.ts +1 -2
  120. package/src/model/modal-controller.ts +24 -41
  121. package/src/model/selection-model.ts +75 -41
  122. package/src/model/table-model.test.ts +92 -56
  123. package/src/model/table-model.ts +628 -162
  124. package/src/model/table-presentation.test.ts +70 -59
  125. package/src/model/table-presentation.ts +278 -149
  126. package/src/playwright/TableManager.ts +13 -6
  127. package/src/playwright/playwright.config.ts +17 -0
  128. package/src/playwright/smoke.spec.ts +180 -41
  129. package/src/testing/index.ts +5 -0
  130. package/src/testing/useTestTableModel.ts +133 -0
  131. package/src/translations.ts +7 -5
  132. package/src/types/Table.ts +90 -0
  133. package/src/types/index.ts +2 -2
  134. package/src/util/dynamic-table.test.ts +70 -0
  135. package/src/util/dynamic-table.ts +50 -36
  136. package/src/util/index.ts +2 -1
  137. package/src/util/query-ast.ts +19 -0
  138. package/src/util/schema.ts +4 -4
  139. package/src/util/sort.ts +90 -0
  140. package/src/util/table-buttons.ts +28 -4
  141. package/src/util/table-controls.ts +4 -1
  142. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  143. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  144. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  145. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  146. package/dist/lib/node/index.cjs +0 -2739
  147. package/dist/lib/node/index.cjs.map +0 -7
  148. package/dist/lib/node/meta.json +0 -1
  149. package/dist/lib/node/types/index.cjs +0 -31
  150. package/dist/lib/node/types/index.cjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  152. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  153. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  154. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  155. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  156. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  157. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  158. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  159. package/dist/types/src/model/table-sorting.d.ts +0 -53
  160. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  161. package/dist/types/src/types/table.d.ts +0 -56
  162. package/dist/types/src/types/table.d.ts.map +0 -1
  163. package/dist/types/src/util/table.d.ts +0 -14
  164. package/dist/types/src/util/table.d.ts.map +0 -1
  165. package/src/components/Table/CreateRefPanel.tsx +0 -84
  166. package/src/components/Table/relations.stories.tsx +0 -124
  167. package/src/components/TableCellEditor/extension.ts +0 -61
  168. package/src/model/table-sorting.ts +0 -216
  169. package/src/playwright/playwright.config.cts +0 -18
  170. package/src/types/table.ts +0 -27
  171. package/src/util/table.ts +0 -127
@@ -2,45 +2,57 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { signal } from '@preact/signals-core';
6
- import { isNotNullable } from 'effect/Predicate';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import * as Predicate from 'effect/Predicate';
7
7
 
8
- import { FormatEnum, getObjectDXN, getValue, TypeEnum } from '@dxos/echo-schema';
8
+ import { Obj } from '@dxos/echo';
9
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
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
- import { mx } from '@dxos/react-ui-theme';
18
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
18
+ import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
19
+ import { mx } from '@dxos/ui-theme';
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 readonly _registry: Registry.Registry;
33
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
34
+ private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
35
+
30
36
  constructor(
37
+ registry: Registry.Registry,
31
38
  private readonly model: TableModel<T>,
32
- private readonly _visibleRange = signal<DxGridPlaneRange>({
39
+ ) {
40
+ this._registry = registry;
41
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
33
42
  start: { row: 0, col: 0 },
34
43
  end: { row: 0, col: 0 },
35
- }),
36
- ) {}
44
+ });
45
+ }
37
46
 
38
47
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
48
+ // Clear cache at the start of each render pass
49
+ this.fieldProjectionCache.clear();
50
+
39
51
  let cells: DxGridPlaneCells;
40
52
 
41
53
  switch (plane) {
42
54
  case 'grid':
43
- this._visibleRange.value = range;
55
+ this._registry.set(this._visibleRange, range);
44
56
  cells = this.getMainGridCells(range);
45
57
  break;
46
58
  case 'frozenRowsStart':
@@ -58,12 +70,21 @@ export class TablePresentation<T extends TableRow = TableRow> {
58
70
  case 'fixedStartEnd':
59
71
  cells = this.getNewColumnCell();
60
72
  break;
73
+ case 'frozenRowsEnd':
74
+ cells = this.getDraftRowCells(range);
75
+ break;
76
+ case 'fixedEndStart':
77
+ cells = this.getDraftIconCells(range);
78
+ break;
79
+ case 'fixedEndEnd':
80
+ cells = this.getDraftActionCells(range);
81
+ break;
61
82
  default:
62
83
  cells = {};
63
84
  }
64
85
 
65
86
  if (plane === 'grid' && this.model.features.dataEditable === false) {
66
- Object.values(cells).forEach((cell) => {
87
+ Object.values<DxGridCellValue>(cells).forEach((cell) => {
67
88
  cell.readonly = 'text-select';
68
89
  });
69
90
  }
@@ -71,155 +92,180 @@ export class TablePresentation<T extends TableRow = TableRow> {
71
92
  return cells;
72
93
  }
73
94
 
74
- private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
75
- const cells: DxGridPlaneCells = {};
76
- const fields = this.model.view?.fields ?? [];
95
+ /**
96
+ * Gets a field projection, using cache if available within the current render pass.
97
+ */
98
+ private getFieldProjection(fieldId: string) {
99
+ let projection = this.fieldProjectionCache.get(fieldId);
100
+ if (!projection) {
101
+ projection = this.model.projection.getFieldProjection(fieldId);
102
+ this.fieldProjectionCache.set(fieldId, projection);
103
+ }
104
+ return projection;
105
+ }
77
106
 
78
- const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
79
- const { props } = this.model.projection.getFieldProjection(field.id);
107
+ private createDataCell(
108
+ cells: DxGridPlaneCells,
109
+ obj: T,
110
+ field: FieldType,
111
+ colIndex: number,
112
+ displayIndex: number,
113
+ ): void {
114
+ const { props } = this.getFieldProjection(field.id);
115
+
116
+ const cell: DxGridCellValue = {
117
+ get value() {
118
+ const value = getValue(obj, field.path);
119
+ if (value == null) {
120
+ return '';
121
+ }
80
122
 
81
- const cell: DxGridCellValue = {
82
- get value() {
83
- const value = getValue(obj, field.path);
84
- if (value == null) {
85
- return '';
86
- }
123
+ if (props.type === 'array') {
124
+ return '';
125
+ }
87
126
 
88
- if (props.type === 'array') {
127
+ switch (props.format) {
128
+ case Format.TypeFormat.Boolean:
129
+ case Format.TypeFormat.SingleSelect:
130
+ case Format.TypeFormat.MultiSelect: {
89
131
  return '';
90
132
  }
91
-
92
- switch (props.format) {
93
- case FormatEnum.Boolean:
94
- case FormatEnum.SingleSelect:
95
- case FormatEnum.MultiSelect: {
96
- return '';
133
+ case Format.TypeFormat.Ref: {
134
+ if (!field.referencePath) {
135
+ return ''; // TODO(burdon): Show error.
97
136
  }
98
- case FormatEnum.Ref: {
99
- if (!field.referencePath) {
100
- return ''; // TODO(burdon): Show error.
101
- }
102
137
 
103
- return getValue(value.target, field.referencePath);
104
- }
138
+ return getValue(value.target, field.referencePath);
139
+ }
105
140
 
106
- default: {
107
- return formatForDisplay({ type: props.type, format: props.format, value });
108
- }
141
+ default: {
142
+ return formatForDisplay({ type: props.type, format: props.format, value });
109
143
  }
110
- },
111
- };
144
+ }
145
+ },
146
+ };
112
147
 
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
- }
148
+ const classes = [];
149
+ const formatClasses = cellClassesForFieldType(props);
128
150
 
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('');
151
+ if (formatClasses) {
152
+ classes.push(formatClasses);
153
+ }
165
154
 
166
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
167
- }
168
- }
155
+ const rowSelectionClasses = cellClassesForRowSelection(
156
+ this.model.selection.isObjectSelected(obj),
157
+ this.model.selection.selectionMode,
158
+ );
169
159
 
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
- }
160
+ if (rowSelectionClasses) {
161
+ classes.push(rowSelectionClasses);
162
+ }
163
+
164
+ cell.className = mx(classes);
165
+
166
+ // Arrays.
167
+ if (props.type === TypeEnum.Array) {
168
+ const targetArray = getValue(obj, field.path);
169
+ if (targetArray && Array.isArray(targetArray)) {
170
+ // TODO(ZaymonFC): For arrays of objects, objects should have a 'label' annotation on the field
171
+ // that can be used to render tags.
172
+ // TODO(ZaymonFC): Move to util.
173
+ const getLabel = (value: any) => {
174
+ // If the value is falsy, return undefined
175
+ // If the object is an array, check for 'name' property
176
+ // If the object is a primitive type (string, number, boolean) stringify
177
+ // If the object is an array return 'Array'
178
+ if (!value) {
179
+ return undefined;
180
+ }
181
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
182
+ return String(value);
183
+ }
184
+ if (Array.isArray(value)) {
185
+ return 'Array';
186
+ }
187
+ if (typeof value === 'object' && value.name) {
188
+ return value.name;
189
+ }
190
+ if (typeof value === 'object' && value.value) {
191
+ return `${value.value}`;
192
+ }
193
+ return 'Object';
194
+ };
195
+
196
+ const tags = targetArray
197
+ .map(getLabel)
198
+ .filter(Predicate.isNotNullable)
199
+ .map((title) => {
200
+ return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
201
+ })
202
+ .join('');
203
+
204
+ cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1 overflow-x-auto">${tags}</div>`;
176
205
  }
206
+ }
177
207
 
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';
208
+ // References.
209
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
210
+ const targetObj = getValue(obj, field.path)?.target;
211
+ if (targetObj) {
212
+ const dxn = Obj.getDXN(targetObj)?.toString();
213
+ cell.accessoryHtml = `<div role="none" class="absolute inline-end-0 inset-block-0 p-[--dx-grid-cell-content-padding-block]"><dx-anchor dxn=${dxn} class="dx-button is-6 aspect-square min-bs-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
186
214
  }
215
+ }
187
216
 
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
- }
217
+ // Booleans.
218
+ if (props.format === Format.TypeFormat.Boolean) {
219
+ const value = getValue(obj, field.path);
220
+ cell.accessoryHtml = tableControls.switch.render({
221
+ colIndex,
222
+ rowIndex: displayIndex,
223
+ checked: value ?? false,
224
+ });
225
+ cell.readonly = 'no-text-select';
226
+ }
227
+
228
+ // Single-Selects.
229
+ if (props.format === Format.TypeFormat.SingleSelect) {
230
+ const value = getValue(obj, field.path);
231
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
232
+ if (options) {
233
+ const option = options.find((o) => o.id === value);
234
+ if (option) {
235
+ cell.accessoryHtml = `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
196
236
  }
197
237
  }
238
+ }
198
239
 
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(' ');
240
+ // Multi-Selects.
241
+ if (props.format === Format.TypeFormat.MultiSelect) {
242
+ const values = getValue(obj, field.path) as string[] | undefined;
243
+ const options = this.model.projection.getFieldProjection(field.id).props.options;
244
+ if (options && values && values.length > 0) {
245
+ const tags = values
246
+ .map((value) => {
247
+ const option = options.find((o) => o.id === value);
248
+ if (option) {
249
+ return `<span class="dx-tag" data-hue="${option.color}">${option.title}</span>`;
250
+ }
251
+ return null;
252
+ })
253
+ .filter(Boolean)
254
+ .join('');
213
255
 
214
- if (tags) {
215
- cell.accessoryHtml = `<div role='none' class="flex flex-row gap-1">${tags}</div>`;
216
- }
256
+ if (tags) {
257
+ cell.accessoryHtml = tags;
217
258
  }
218
259
  }
260
+ }
219
261
 
220
- const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
221
- cells[idx] = cell;
222
- };
262
+ const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
263
+ cells[idx] = cell;
264
+ }
265
+
266
+ private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
267
+ const cells: DxGridPlaneCells = {};
268
+ const fields = this.model.projection?.getFields() ?? [];
223
269
 
224
270
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
225
271
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
@@ -228,7 +274,48 @@ export class TablePresentation<T extends TableRow = TableRow> {
228
274
  continue;
229
275
  }
230
276
 
231
- addCell(this.model.rows.value[row], field, col, row);
277
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row);
278
+ }
279
+ }
280
+
281
+ return cells;
282
+ }
283
+
284
+ private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
285
+ const cells: DxGridPlaneCells = {};
286
+ const fields = this.model.projection?.getFields() ?? [];
287
+ const draftRows = this.model.getDraftRows();
288
+
289
+ // Return cells of the CTA row if no draft row is active
290
+ if (draftRows.length === 0) {
291
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
292
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
293
+ value: '',
294
+ readonly: true,
295
+ className: 'dx-grid__row--cta__cell',
296
+ };
297
+ }
298
+ } else {
299
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
300
+ const draftRow = draftRows[row];
301
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
302
+ const cellIndex = toPlaneCellIndex({ col, row });
303
+ const field = fields[col];
304
+ if (!field) {
305
+ continue;
306
+ }
307
+
308
+ this.createDataCell(cells, draftRow.data, field, col, row);
309
+
310
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
311
+ const cellValue = cells[cellIndex];
312
+ if (cellValue) {
313
+ const existingClasses = cellValue.className || '';
314
+ const draftClasses = 'dx-grid__cell--flagged';
315
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
316
+ }
317
+ }
318
+ }
232
319
  }
233
320
  }
234
321
 
@@ -237,22 +324,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
237
324
 
238
325
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
239
326
  const cells: DxGridPlaneCells = {};
240
- const fields = this.model.view?.fields ?? [];
327
+ const fields = this.model.projection?.getFields() ?? [];
328
+ const currentSort = this.model.getSorting();
241
329
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
242
330
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
243
- const sorting = this.model.sorting?.sorting;
244
- const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
331
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
245
332
 
246
333
  cells[toPlaneCellIndex({ col, row: 0 })] = {
247
334
  // TODO(burdon): Use same logic as form for fallback title.
248
- value: props.title ?? field.path,
249
- readonly: true,
335
+ value: '',
250
336
  resizeHandle: 'col',
251
337
  accessoryHtml: `
338
+ <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
252
339
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
253
340
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
254
341
  `,
255
- className: '!bg-toolbarSurface',
342
+ className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
256
343
  };
257
344
  }
258
345
 
@@ -307,7 +394,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
307
394
  accessoryHtml: tableControls.checkbox.render({
308
395
  rowIndex: 0,
309
396
  header: true,
310
- checked: this.model.selection.allRowsSeleted.value,
397
+ checked: this.model.selection.allRowsSelected,
311
398
  }),
312
399
  className: '!bg-toolbarSurface',
313
400
  readonly: true,
@@ -321,7 +408,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
321
408
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
322
409
  accessoryHtml: this.model.features.schemaEditable
323
410
  ? tableButtons.addColumn.render({
324
- disabled: (this.model.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
411
+ disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT,
325
412
  })
326
413
  : undefined,
327
414
  className: '!bg-toolbarSurface',
@@ -330,6 +417,49 @@ export class TablePresentation<T extends TableRow = TableRow> {
330
417
  },
331
418
  };
332
419
  }
420
+
421
+ private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
422
+ const cells: DxGridPlaneCells = {};
423
+ const draftRows = this.model.getDraftRows();
424
+
425
+ // Return cells of the CTA row if no draft row is active
426
+ if (draftRows.length === 0) {
427
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
428
+ value: '',
429
+ className: 'dx-grid__row--cta__cell',
430
+ readonly: true,
431
+ };
432
+ } else {
433
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
434
+ const draftRow = draftRows[row];
435
+ const disabled = !draftRow.valid;
436
+
437
+ cells[toPlaneCellIndex({ col: 0, row })] = {
438
+ value: '',
439
+ readonly: true,
440
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
441
+ };
442
+ }
443
+ }
444
+
445
+ return cells;
446
+ }
447
+
448
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
449
+ const cells: DxGridPlaneCells = {};
450
+ const draftRows = this.model.getDraftRows();
451
+
452
+ for (let row = range.start.row; row <= range.end.row; row++) {
453
+ cells[toPlaneCellIndex({ col: 0, row })] = {
454
+ value: '',
455
+ readonly: true,
456
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
457
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
458
+ };
459
+ }
460
+
461
+ return cells;
462
+ }
333
463
  }
334
464
 
335
465
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
@@ -343,8 +473,7 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
343
473
 
344
474
  switch (selectionMode) {
345
475
  case 'single':
346
- // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
347
- return ['!bg-currentRelated hover:bg-hoverSurface', '!cursor-pointer'];
476
+ return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
348
477
  case 'multiple':
349
478
  return ['!bg-gridCellSelected'];
350
479
  }
@@ -17,6 +17,7 @@ const TABLE_SELECTORS = {
17
17
  },
18
18
  columnDelete: 'column-delete',
19
19
  rowDelete: 'row-menu-delete',
20
+ addRow: 'table.toolbar.add-row',
20
21
  } as const;
21
22
 
22
23
  export class TableManager {
@@ -53,12 +54,18 @@ export class TableManager {
53
54
  await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
54
55
  }
55
56
 
56
- public async addColumn({ label, format }: { label: string; format: 'number' }): Promise<void> {
57
+ public async addRow(): Promise<void> {
58
+ await this.page.getByTestId(TABLE_SELECTORS.addRow).click();
59
+ }
60
+
61
+ public async addColumn({ label, format }: { label: string; format?: string }): Promise<void> {
57
62
  await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
58
- await this.page.getByRole('combobox').click();
59
- await this.page.getByLabel(`format ${format}`).click();
60
- await this.page.getByPlaceholder('Property label.').click();
61
- await this.page.getByPlaceholder('Property label.').fill(label);
62
- await this.page.getByRole('button', { name: 'button save' }).click();
63
+ if (format) {
64
+ await this.page.getByRole('combobox').click();
65
+ await this.page.getByLabel(format).click();
66
+ }
67
+ await this.page.getByPlaceholder('Property label').click();
68
+ await this.page.getByPlaceholder('Property label').fill(label);
69
+ await this.page.getByRole('button', { name: 'Save' }).click();
63
70
  }
64
71
  }
@@ -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-react:serve-e2e -- --port=9004',
14
+ port: 9004,
15
+ reuseExistingServer: false,
16
+ },
17
+ });