@dxos/react-ui-table 0.8.3 → 0.8.4-main.03d5cd7b56

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 (194) hide show
  1. package/dist/lib/browser/chunk-FBURZBPH.mjs +70 -0
  2. package/dist/lib/browser/chunk-FBURZBPH.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-YGC7LZJN.mjs +1695 -0
  6. package/dist/lib/browser/chunk-YGC7LZJN.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +1162 -2554
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/testing/index.mjs +115 -0
  11. package/dist/lib/browser/testing/index.mjs.map +7 -0
  12. package/dist/lib/browser/translations.mjs +36 -0
  13. package/dist/lib/browser/translations.mjs.map +7 -0
  14. package/dist/lib/browser/types/index.mjs +4 -5
  15. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs +71 -0
  16. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs.map +7 -0
  17. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs +1697 -0
  18. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs.map +7 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  20. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  21. package/dist/lib/node-esm/index.mjs +1162 -2554
  22. package/dist/lib/node-esm/index.mjs.map +4 -4
  23. package/dist/lib/node-esm/meta.json +1 -1
  24. package/dist/lib/node-esm/testing/index.mjs +116 -0
  25. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  26. package/dist/lib/node-esm/translations.mjs +37 -0
  27. package/dist/lib/node-esm/translations.mjs.map +7 -0
  28. package/dist/lib/node-esm/types/index.mjs +4 -5
  29. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +2 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  34. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +35 -3
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  38. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  39. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  40. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.d.ts +21 -18
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +110 -6
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +17 -0
  46. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  47. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  48. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  49. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +14 -9
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -18
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  58. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  59. package/dist/types/src/components/index.d.ts +0 -1
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +26 -56
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/hooks/index.d.ts +2 -0
  64. package/dist/types/src/hooks/index.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  66. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  68. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  69. package/dist/types/src/hooks/useTableModel.d.ts +11 -10
  70. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +0 -3
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/modal-controller.d.ts +5 -8
  74. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  75. package/dist/types/src/model/selection-model.d.ts +18 -8
  76. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-model.d.ts +150 -32
  78. package/dist/types/src/model/table-model.d.ts.map +1 -1
  79. package/dist/types/src/model/table-presentation.d.ts +13 -2
  80. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  81. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  82. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  83. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  84. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  85. package/dist/types/src/testing/index.d.ts +2 -0
  86. package/dist/types/src/testing/index.d.ts.map +1 -0
  87. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  88. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  89. package/dist/types/src/translations.d.ts +22 -25
  90. package/dist/types/src/translations.d.ts.map +1 -1
  91. package/dist/types/src/types/Table.d.ts +26 -0
  92. package/dist/types/src/types/Table.d.ts.map +1 -0
  93. package/dist/types/src/types/index.d.ts +1 -1
  94. package/dist/types/src/types/index.d.ts.map +1 -1
  95. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  96. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  97. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  98. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  99. package/dist/types/src/util/index.d.ts +2 -1
  100. package/dist/types/src/util/index.d.ts.map +1 -1
  101. package/dist/types/src/util/query-ast.d.ts +7 -0
  102. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  103. package/dist/types/src/util/schema.d.ts +1 -1
  104. package/dist/types/src/util/schema.d.ts.map +1 -1
  105. package/dist/types/src/util/signals.d.ts.map +1 -1
  106. package/dist/types/src/util/sort.d.ts +12 -0
  107. package/dist/types/src/util/sort.d.ts.map +1 -0
  108. package/dist/types/src/util/table-buttons.d.ts +31 -17
  109. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  110. package/dist/types/src/util/table-controls.d.ts +4 -4
  111. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  112. package/dist/types/src/util/tag.d.ts.map +1 -1
  113. package/dist/types/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +76 -72
  115. package/src/components/Table/ColumnActionsMenu.tsx +18 -13
  116. package/src/components/Table/ColumnSettings.tsx +18 -15
  117. package/src/components/Table/DynamicTable.stories.tsx +40 -36
  118. package/src/components/Table/DynamicTable.tsx +38 -34
  119. package/src/components/Table/Relations.stories.tsx +281 -0
  120. package/src/components/Table/RowActionsMenu.tsx +29 -19
  121. package/src/components/Table/Table.stories.tsx +198 -214
  122. package/src/components/Table/Table.tsx +47 -363
  123. package/src/components/Table/TableContent.tsx +421 -0
  124. package/src/components/Table/TableToolbar.tsx +111 -0
  125. package/src/components/TableCellEditor/CellValidationMessage.tsx +9 -5
  126. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -49
  127. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +40 -79
  128. package/src/components/TableCellEditor/TableCellEditor.tsx +109 -199
  129. package/src/components/TableCellEditor/index.ts +1 -0
  130. package/src/components/index.ts +0 -1
  131. package/src/components/testing.ts +46 -37
  132. package/src/hooks/index.ts +2 -0
  133. package/src/hooks/useAddRow.ts +36 -0
  134. package/src/hooks/useProjectionModel.ts +39 -0
  135. package/src/hooks/useTableModel.ts +41 -35
  136. package/src/index.ts +0 -5
  137. package/src/model/modal-controller.ts +24 -41
  138. package/src/model/selection-model.ts +75 -41
  139. package/src/model/table-model.test.ts +91 -56
  140. package/src/model/table-model.ts +629 -162
  141. package/src/model/table-presentation.test.ts +69 -59
  142. package/src/model/table-presentation.ts +283 -154
  143. package/src/playwright/TableManager.ts +13 -6
  144. package/src/playwright/playwright.config.ts +17 -0
  145. package/src/playwright/smoke.spec.ts +181 -42
  146. package/src/testing/index.ts +5 -0
  147. package/src/testing/useTestTableModel.ts +133 -0
  148. package/src/translations.ts +24 -24
  149. package/src/types/Table.ts +79 -0
  150. package/src/types/index.ts +2 -2
  151. package/src/util/dynamic-table.test.ts +70 -0
  152. package/src/util/dynamic-table.ts +51 -36
  153. package/src/util/index.ts +2 -1
  154. package/src/util/query-ast.ts +22 -0
  155. package/src/util/schema.ts +4 -4
  156. package/src/util/sort.ts +90 -0
  157. package/src/util/table-buttons.ts +28 -4
  158. package/src/util/table-controls.ts +6 -3
  159. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  160. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  161. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  162. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  163. package/dist/lib/node/index.cjs +0 -2739
  164. package/dist/lib/node/index.cjs.map +0 -7
  165. package/dist/lib/node/meta.json +0 -1
  166. package/dist/lib/node/types/index.cjs +0 -31
  167. package/dist/lib/node/types/index.cjs.map +0 -7
  168. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  169. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  170. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  171. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  172. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  173. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  174. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  175. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  176. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  177. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  178. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  179. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  180. package/dist/types/src/model/table-sorting.d.ts +0 -53
  181. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  182. package/dist/types/src/types/table.d.ts +0 -56
  183. package/dist/types/src/types/table.d.ts.map +0 -1
  184. package/dist/types/src/util/table.d.ts +0 -14
  185. package/dist/types/src/util/table.d.ts.map +0 -1
  186. package/src/components/Table/CreateRefPanel.tsx +0 -84
  187. package/src/components/Table/relations.stories.tsx +0 -124
  188. package/src/components/TableCellEditor/extension.ts +0 -61
  189. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  190. package/src/components/TableToolbar/index.ts +0 -5
  191. package/src/model/table-sorting.ts +0 -216
  192. package/src/playwright/playwright.config.cts +0 -18
  193. package/src/types/table.ts +0 -27
  194. package/src/util/table.ts +0 -127
@@ -2,24 +2,26 @@
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 { type View } from '@dxos/echo';
10
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
9
11
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
10
12
  import {
13
+ type DxGridCellValue,
11
14
  type DxGridPlane,
12
15
  type DxGridPlaneCells,
13
16
  type DxGridPlaneRange,
14
- type DxGridCellValue,
15
17
  toPlaneCellIndex,
16
18
  } from '@dxos/react-ui-grid';
17
- import { mx } from '@dxos/react-ui-theme';
18
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { VIEW_FIELD_LIMIT } from '@dxos/schema';
20
+ import { mx } from '@dxos/ui-theme';
19
21
 
20
- import { type SelectionMode } from './selection-model';
21
- import { type TableRow, type TableModel } from './table-model';
22
22
  import { tableButtons, tableControls } from '../util';
23
+ import { type SelectionMode } from './selection-model';
24
+ import { type TableModel, type TableRow } from './table-model';
23
25
 
24
26
  /**
25
27
  * Presentation layer for a table component, handling cell rendering and grid display logic.
@@ -27,20 +29,30 @@ import { tableButtons, tableControls } from '../util';
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: View.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 end-0 inset-y-0 p-(--dx-grid-cell-content-padding-block)"><dx-anchor dxn=${dxn} class="dx-button w-6 aspect-square min-h-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-w-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-toolbar-surface text-description! [&>div]:flex [&>div]:items-stretch',
256
343
  };
257
344
  }
258
345
 
@@ -295,7 +382,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
295
382
  if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
296
383
  return {
297
384
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
298
- className: '!bg-toolbarSurface',
385
+ className: '!bg-toolbar-surface',
299
386
  readonly: true,
300
387
  value: '',
301
388
  },
@@ -307,9 +394,9 @@ 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
- className: '!bg-toolbarSurface',
399
+ className: '!bg-toolbar-surface',
313
400
  readonly: true,
314
401
  value: '',
315
402
  },
@@ -321,21 +408,64 @@ 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
- className: '!bg-toolbarSurface',
414
+ className: '!bg-toolbar-surface',
328
415
  readonly: true,
329
416
  value: '',
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) => {
336
466
  if (!selected) {
337
467
  if (selectionMode === 'single') {
338
- return ['!cursor-pointer'];
468
+ return ['cursor-pointer!'];
339
469
  } else {
340
470
  return undefined;
341
471
  }
@@ -343,9 +473,8 @@ 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 ['dx-grid__cell--no-focus-unfurl !bg-current-surface hover:bg-hover-surface cursor-pointer!'];
348
477
  case 'multiple':
349
- return ['!bg-gridCellSelected'];
478
+ return ['!bg-grid-cell-selected'];
350
479
  }
351
480
  };
@@ -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: 'pnpm storybook dev --ci --quiet --port=9004 --config-dir=.storybook',
14
+ port: 9004,
15
+ reuseExistingServer: false,
16
+ },
17
+ });