@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.c351d160a8

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 (158) hide show
  1. package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1188 -1139
  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 +31 -38
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/chunk-GI72MQSG.mjs +1753 -0
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs +71 -0
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1188 -1139
  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 +31 -38
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  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 +3 -3
  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 +16 -12
  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 +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -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 +5 -5
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  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 +10 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +112 -30
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +26 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  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/util/dynamic-table.d.ts +14 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/schema.d.ts +1 -1
  86. package/dist/types/src/util/schema.d.ts.map +1 -1
  87. package/dist/types/src/util/sort.d.ts +12 -0
  88. package/dist/types/src/util/sort.d.ts.map +1 -0
  89. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  90. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +61 -56
  93. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  94. package/src/components/Table/ColumnSettings.tsx +9 -10
  95. package/src/components/Table/DynamicTable.stories.tsx +28 -24
  96. package/src/components/Table/DynamicTable.tsx +34 -28
  97. package/src/components/Table/Relations.stories.tsx +240 -0
  98. package/src/components/Table/RowActionsMenu.tsx +26 -17
  99. package/src/components/Table/Table.stories.tsx +148 -118
  100. package/src/components/Table/Table.tsx +351 -296
  101. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  102. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  103. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -66
  104. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  105. package/src/components/TableCellEditor/index.ts +1 -0
  106. package/src/components/TableToolbar/TableToolbar.tsx +54 -38
  107. package/src/components/testing.ts +39 -13
  108. package/src/hooks/index.ts +2 -1
  109. package/src/hooks/useAddRow.ts +14 -14
  110. package/src/hooks/useProjectionModel.ts +39 -0
  111. package/src/hooks/useTableModel.ts +36 -34
  112. package/src/model/modal-controller.ts +24 -41
  113. package/src/model/selection-model.ts +75 -43
  114. package/src/model/table-model.test.ts +92 -55
  115. package/src/model/table-model.ts +453 -175
  116. package/src/model/table-presentation.test.ts +70 -58
  117. package/src/model/table-presentation.ts +111 -64
  118. package/src/playwright/TableManager.ts +13 -6
  119. package/src/playwright/playwright.config.ts +1 -1
  120. package/src/playwright/smoke.spec.ts +171 -33
  121. package/src/testing/useTestTableModel.ts +46 -35
  122. package/src/translations.ts +3 -3
  123. package/src/types/Table.ts +79 -0
  124. package/src/types/index.ts +1 -1
  125. package/src/util/dynamic-table.test.ts +70 -0
  126. package/src/util/dynamic-table.ts +46 -33
  127. package/src/util/index.ts +2 -1
  128. package/src/util/query-ast.ts +22 -0
  129. package/src/util/schema.ts +4 -4
  130. package/src/util/sort.ts +90 -0
  131. package/src/util/table-buttons.ts +4 -3
  132. package/src/util/table-controls.ts +6 -3
  133. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  134. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  138. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  140. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  141. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  143. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  144. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  146. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  147. package/dist/types/src/model/table-sorting.d.ts +0 -53
  148. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  149. package/dist/types/src/types/schema.d.ts +0 -8
  150. package/dist/types/src/types/schema.d.ts.map +0 -1
  151. package/dist/types/src/util/table.d.ts +0 -11
  152. package/dist/types/src/util/table.d.ts.map +0 -1
  153. package/src/components/Table/CreateRefPanel.tsx +0 -86
  154. package/src/components/Table/relations.stories.tsx +0 -116
  155. package/src/components/TableCellEditor/extension.ts +0 -61
  156. package/src/model/table-sorting.ts +0 -216
  157. package/src/types/schema.ts +0 -16
  158. package/src/util/table.ts +0 -49
@@ -2,102 +2,114 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
5
+ import { Registry } from '@effect-atom/atom-react';
6
+ import * as Schema from 'effect/Schema';
7
+ import { beforeEach, describe, expect, it } from 'vitest';
7
8
 
8
- import { Obj } from '@dxos/echo';
9
- import { TypedObject } from '@dxos/echo-schema';
10
- import { live } from '@dxos/live-object';
11
- import { createEchoSchema } from '@dxos/live-object/testing';
12
- import { createView } from '@dxos/schema';
9
+ import { Filter, JsonSchema, Query, Type } from '@dxos/echo';
10
+ import { createEchoSchema } from '@dxos/echo/testing';
11
+ import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
12
+
13
+ import { Table } from '../types';
13
14
 
14
15
  import { TableModel, type TableModelProps } from './table-model';
15
16
  import { TablePresentation } from './table-presentation';
16
- import { TableView } from '../types';
17
17
 
18
18
  describe('TablePresentation', () => {
19
- describe('row reactivity', () => {
19
+ describe('getCells', () => {
20
+ let registry: Registry.Registry;
20
21
  let data: any[];
21
- let updateCount: number;
22
22
  let model: any;
23
23
  let presentation: TablePresentation;
24
24
 
25
25
  beforeEach(async () => {
26
- updateCount = 0;
27
- ({ data } = live({
28
- data: [
29
- { col1: 'A', col2: 1, col3: true },
30
- { col1: 'B', col2: 2, col3: false },
31
- ],
32
- }));
33
-
34
- model = createTableModel({
35
- onCellUpdate: () => updateCount++,
36
- });
37
- model.setRows(data);
26
+ registry = Registry.make();
27
+ data = [
28
+ { id: '1', title: 'A', count: 1 },
29
+ { id: '2', title: 'B', count: 2 },
30
+ ];
31
+
32
+ model = createTableModel(registry, {});
38
33
  await model.open();
39
- presentation = new TablePresentation(model);
34
+ model.setRows(data);
35
+ presentation = new TablePresentation(registry, model);
40
36
  });
41
37
 
42
- it('should update with row-level reactivity', () => {
43
- // Set up visible range to include our test data.
44
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
38
+ it('should return cells for the visible range', () => {
39
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
45
40
 
46
- // Trigger a row update.
47
- data[0].col1 = 'New Value';
48
- expect(updateCount).toBe(1);
41
+ // Verify that cells are returned for the range. The exact column indices depend on the projection.
42
+ const cellKeys = Object.keys(cells);
43
+ expect(cellKeys.length).toBeGreaterThan(0);
49
44
 
50
- // Verify the new value through getCells.
51
- const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
52
- expect(cells['0,0']?.value).toBe('New Value');
45
+ // Check that values from the data are present in the cells.
46
+ const values = Object.values(cells).map((c) => c.value);
47
+ expect(values).toContain('A');
48
+ expect(values).toContain('B');
53
49
  });
54
50
 
55
- it('should update reactively when adding a new row', () => {
56
- // Set up visible range to include our test data.
57
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
51
+ it('should reflect row data changes', () => {
52
+ // Update the row data and refresh.
53
+ data[0].title = 'New Value';
54
+ model.setRows(data);
58
55
 
59
- data.push({ col1: 'C', col2: 3, col3: true });
60
- expect(updateCount).toBe(1);
56
+ // Verify the new value through getCells.
57
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 2 } }, 'grid');
58
+ const values = Object.values(cells).map((c) => c.value);
59
+ expect(values).toContain('New Value');
60
+ });
61
+
62
+ it('should reflect new rows', () => {
63
+ // Add a new row.
64
+ data.push({ id: '3', title: 'C', count: 3 });
65
+ model.setRows(data);
61
66
 
62
67
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
63
68
 
64
69
  // Verify the new row's data.
65
70
  expect(cells['0,2']?.value).toBe('C');
66
71
  expect(cells['1,2']?.value).toBe('3');
67
- expect(cells['2,2']?.value).toBe('true');
68
72
  });
69
73
 
70
- it('should handle combined operations reactively', () => {
71
- // Set up visible range.
72
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
73
-
74
- // Multiple operations.
75
- data[0].col1 = 'Updated A';
76
- data.push({ col1: 'C', col2: 3, col3: true });
74
+ it('should handle row modifications', () => {
75
+ // Modify the data.
76
+ data[0].title = 'Updated A';
77
+ data.push({ id: '3', title: 'C', count: 3 });
77
78
  data.splice(1, 1);
78
-
79
- // We expect one update per operation affecting visible rows.
80
- expect(updateCount).toBe(3);
79
+ model.setRows(data);
81
80
 
82
81
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
83
82
 
84
83
  // Verify final state.
85
84
  expect(cells['0,0']?.value).toBe('Updated A');
86
85
  expect(cells['0,1']?.value).toBe('C');
87
- expect(cells['1,1']?.value).toBe('3');
88
- expect(cells['2,1']?.value).toBe('true');
89
86
  });
90
87
  });
91
88
  });
92
89
 
93
- class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
90
+ const Test = Schema.Struct({
94
91
  title: Schema.String,
95
- completed: Schema.Boolean,
96
- }) {}
97
-
98
- const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
92
+ count: Schema.Number,
93
+ }).pipe(
94
+ Type.object({
95
+ typename: 'com.example.type.test',
96
+ version: '0.1.0',
97
+ }),
98
+ );
99
+
100
+ const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
99
101
  const schema = createEchoSchema(Test);
100
- const table = Obj.make(TableView, { sizes: {} });
101
- const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
102
- return new TableModel({ view, schema: schema.jsonSchema, ...props });
102
+ const view = ViewModel.make({
103
+ query: Query.select(Filter.type(schema)),
104
+ jsonSchema: schema.jsonSchema,
105
+ });
106
+ const object = Table.make({ view });
107
+ const projection = new ProjectionModel({
108
+ registry,
109
+ view,
110
+ baseSchema: schema.jsonSchema,
111
+ change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(schema)),
112
+ });
113
+ projection.normalizeView();
114
+ return new TableModel({ registry, object, projection, ...props });
103
115
  };
@@ -2,11 +2,12 @@
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
8
  import { Obj } from '@dxos/echo';
9
- import { FormatEnum, getValue, TypeEnum } from '@dxos/echo-schema';
9
+ import { type View } from '@dxos/echo';
10
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
10
11
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
12
  import {
12
13
  type DxGridCellValue,
@@ -15,28 +16,34 @@ import {
15
16
  type DxGridPlaneRange,
16
17
  toPlaneCellIndex,
17
18
  } from '@dxos/react-ui-grid';
18
- import { mx } from '@dxos/react-ui-theme';
19
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { VIEW_FIELD_LIMIT } from '@dxos/schema';
20
+ import { mx } from '@dxos/ui-theme';
20
21
 
21
- import { type SelectionMode } from './selection-model';
22
- import { type TableRow, type TableModel } from './table-model';
23
22
  import { tableButtons, tableControls } from '../util';
24
23
 
24
+ import { type SelectionMode } from './selection-model';
25
+ import { type TableModel, type TableRow } from './table-model';
26
+
25
27
  /**
26
28
  * Presentation layer for a table component, handling cell rendering and grid display logic.
27
29
  * Manages rendering of table cells, headers, selection columns, and action columns across
28
30
  * different grid planes.
29
31
  */
30
32
  export class TablePresentation<T extends TableRow = TableRow> {
33
+ private readonly _registry: Registry.Registry;
34
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
31
35
  private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
32
36
 
33
37
  constructor(
38
+ registry: Registry.Registry,
34
39
  private readonly model: TableModel<T>,
35
- private readonly _visibleRange = signal<DxGridPlaneRange>({
40
+ ) {
41
+ this._registry = registry;
42
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
36
43
  start: { row: 0, col: 0 },
37
44
  end: { row: 0, col: 0 },
38
- }),
39
- ) {}
45
+ });
46
+ }
40
47
 
41
48
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
42
49
  // Clear cache at the start of each render pass
@@ -46,7 +53,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
46
53
 
47
54
  switch (plane) {
48
55
  case 'grid':
49
- this._visibleRange.value = range;
56
+ this._registry.set(this._visibleRange, range);
50
57
  cells = this.getMainGridCells(range);
51
58
  break;
52
59
  case 'frozenRowsStart':
@@ -67,6 +74,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
67
74
  case 'frozenRowsEnd':
68
75
  cells = this.getDraftRowCells(range);
69
76
  break;
77
+ case 'fixedEndStart':
78
+ cells = this.getDraftIconCells(range);
79
+ break;
70
80
  case 'fixedEndEnd':
71
81
  cells = this.getDraftActionCells(range);
72
82
  break;
@@ -98,7 +108,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
98
108
  private createDataCell(
99
109
  cells: DxGridPlaneCells,
100
110
  obj: T,
101
- field: FieldType,
111
+ field: View.FieldType,
102
112
  colIndex: number,
103
113
  displayIndex: number,
104
114
  ): void {
@@ -116,12 +126,12 @@ export class TablePresentation<T extends TableRow = TableRow> {
116
126
  }
117
127
 
118
128
  switch (props.format) {
119
- case FormatEnum.Boolean:
120
- case FormatEnum.SingleSelect:
121
- case FormatEnum.MultiSelect: {
129
+ case Format.TypeFormat.Boolean:
130
+ case Format.TypeFormat.SingleSelect:
131
+ case Format.TypeFormat.MultiSelect: {
122
132
  return '';
123
133
  }
124
- case FormatEnum.Ref: {
134
+ case Format.TypeFormat.Ref: {
125
135
  if (!field.referencePath) {
126
136
  return ''; // TODO(burdon): Show error.
127
137
  }
@@ -137,20 +147,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
137
147
  };
138
148
 
139
149
  const classes = [];
140
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
150
+ const formatClasses = cellClassesForFieldType(props);
151
+
141
152
  if (formatClasses) {
142
153
  classes.push(formatClasses);
143
154
  }
155
+
144
156
  const rowSelectionClasses = cellClassesForRowSelection(
145
157
  this.model.selection.isObjectSelected(obj),
146
158
  this.model.selection.selectionMode,
147
159
  );
160
+
148
161
  if (rowSelectionClasses) {
149
162
  classes.push(rowSelectionClasses);
150
163
  }
151
- if (classes.length > 0) {
152
- cell.className = mx(classes.flat());
153
- }
164
+
165
+ cell.className = mx(classes);
154
166
 
155
167
  // Arrays.
156
168
  if (props.type === TypeEnum.Array) {
@@ -184,7 +196,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
184
196
 
185
197
  const tags = targetArray
186
198
  .map(getLabel)
187
- .filter(isNotNullable)
199
+ .filter(Predicate.isNotNullable)
188
200
  .map((title) => {
189
201
  return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
190
202
  })
@@ -195,16 +207,16 @@ export class TablePresentation<T extends TableRow = TableRow> {
195
207
  }
196
208
 
197
209
  // References.
198
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
210
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
199
211
  const targetObj = getValue(obj, field.path)?.target;
200
212
  if (targetObj) {
201
213
  const dxn = Obj.getDXN(targetObj)?.toString();
202
- cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
214
+ 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>`;
203
215
  }
204
216
  }
205
217
 
206
218
  // Booleans.
207
- if (props.format === FormatEnum.Boolean) {
219
+ if (props.format === Format.TypeFormat.Boolean) {
208
220
  const value = getValue(obj, field.path);
209
221
  cell.accessoryHtml = tableControls.switch.render({
210
222
  colIndex,
@@ -215,7 +227,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
215
227
  }
216
228
 
217
229
  // Single-Selects.
218
- if (props.format === FormatEnum.SingleSelect) {
230
+ if (props.format === Format.TypeFormat.SingleSelect) {
219
231
  const value = getValue(obj, field.path);
220
232
  const options = this.model.projection.getFieldProjection(field.id).props.options;
221
233
  if (options) {
@@ -227,7 +239,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
227
239
  }
228
240
 
229
241
  // Multi-Selects.
230
- if (props.format === FormatEnum.MultiSelect) {
242
+ if (props.format === Format.TypeFormat.MultiSelect) {
231
243
  const values = getValue(obj, field.path) as string[] | undefined;
232
244
  const options = this.model.projection.getFieldProjection(field.id).props.options;
233
245
  if (options && values && values.length > 0) {
@@ -254,7 +266,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
254
266
 
255
267
  private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
256
268
  const cells: DxGridPlaneCells = {};
257
- const fields = this.model.projection?.fields ?? [];
269
+ const fields = this.model.projection?.getFields() ?? [];
258
270
 
259
271
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
260
272
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
@@ -263,7 +275,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
263
275
  continue;
264
276
  }
265
277
 
266
- this.createDataCell(cells, this.model.rows.value[row], field, col, row);
278
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row);
267
279
  }
268
280
  }
269
281
 
@@ -272,26 +284,37 @@ export class TablePresentation<T extends TableRow = TableRow> {
272
284
 
273
285
  private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
274
286
  const cells: DxGridPlaneCells = {};
275
- const fields = this.model.projection?.fields ?? [];
276
- const draftRows = this.model.draftRows.value;
287
+ const fields = this.model.projection?.getFields() ?? [];
288
+ const draftRows = this.model.getDraftRows();
277
289
 
278
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
279
- const draftRow = draftRows[row];
290
+ // Return cells of the CTA row if no draft row is active
291
+ if (draftRows.length === 0) {
280
292
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
281
- const field = fields[col];
282
- if (!field) {
283
- continue;
284
- }
293
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
294
+ value: '',
295
+ readonly: true,
296
+ className: 'dx-grid__row--cta__cell',
297
+ };
298
+ }
299
+ } else {
300
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
301
+ const draftRow = draftRows[row];
302
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
303
+ const cellIndex = toPlaneCellIndex({ col, row });
304
+ const field = fields[col];
305
+ if (!field) {
306
+ continue;
307
+ }
285
308
 
286
- this.createDataCell(cells, draftRow.data, field, col, row);
309
+ this.createDataCell(cells, draftRow.data, field, col, row);
287
310
 
288
- if (this.model.hasDraftRowValidationError(row, field.path)) {
289
- const cellIndex = toPlaneCellIndex({ col, row });
290
- const cellValue = cells[cellIndex];
291
- if (cellValue) {
292
- const existingClasses = cellValue.className || '';
293
- const draftClasses = 'dx-grid__cell--flagged';
294
- cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
311
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
312
+ const cellValue = cells[cellIndex];
313
+ if (cellValue) {
314
+ const existingClasses = cellValue.className || '';
315
+ const draftClasses = 'dx-grid__cell--flagged';
316
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
317
+ }
295
318
  }
296
319
  }
297
320
  }
@@ -302,22 +325,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
302
325
 
303
326
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
304
327
  const cells: DxGridPlaneCells = {};
305
- const fields = this.model.projection?.fields ?? [];
328
+ const fields = this.model.projection?.getFields() ?? [];
329
+ const currentSort = this.model.getSorting();
306
330
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
307
331
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
308
- const sorting = this.model.sorting?.sorting;
309
- const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
332
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
310
333
 
311
334
  cells[toPlaneCellIndex({ col, row: 0 })] = {
312
335
  // TODO(burdon): Use same logic as form for fallback title.
313
- value: props.title ?? field.path,
314
- readonly: true,
336
+ value: '',
315
337
  resizeHandle: 'col',
316
338
  accessoryHtml: `
339
+ <span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
317
340
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
318
341
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
319
342
  `,
320
- className: '!bg-toolbarSurface',
343
+ className: '!bg-toolbar-surface text-description! [&>div]:flex [&>div]:items-stretch',
321
344
  };
322
345
  }
323
346
 
@@ -360,7 +383,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
360
383
  if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
361
384
  return {
362
385
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
363
- className: '!bg-toolbarSurface',
386
+ className: '!bg-toolbar-surface',
364
387
  readonly: true,
365
388
  value: '',
366
389
  },
@@ -372,9 +395,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
372
395
  accessoryHtml: tableControls.checkbox.render({
373
396
  rowIndex: 0,
374
397
  header: true,
375
- checked: this.model.selection.allRowsSeleted.value,
398
+ checked: this.model.selection.allRowsSelected,
376
399
  }),
377
- className: '!bg-toolbarSurface',
400
+ className: '!bg-toolbar-surface',
378
401
  readonly: true,
379
402
  value: '',
380
403
  },
@@ -386,10 +409,10 @@ export class TablePresentation<T extends TableRow = TableRow> {
386
409
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
387
410
  accessoryHtml: this.model.features.schemaEditable
388
411
  ? tableButtons.addColumn.render({
389
- disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
412
+ disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT,
390
413
  })
391
414
  : undefined,
392
- className: '!bg-toolbarSurface',
415
+ className: '!bg-toolbar-surface',
393
416
  readonly: true,
394
417
  value: '',
395
418
  },
@@ -398,16 +421,41 @@ export class TablePresentation<T extends TableRow = TableRow> {
398
421
 
399
422
  private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
400
423
  const cells: DxGridPlaneCells = {};
401
- const draftRows = this.model.draftRows.value;
424
+ const draftRows = this.model.getDraftRows();
425
+
426
+ // Return cells of the CTA row if no draft row is active
427
+ if (draftRows.length === 0) {
428
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
429
+ value: '',
430
+ className: 'dx-grid__row--cta__cell',
431
+ readonly: true,
432
+ };
433
+ } else {
434
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
435
+ const draftRow = draftRows[row];
436
+ const disabled = !draftRow.valid;
402
437
 
403
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
404
- const draftRow = draftRows[row];
405
- const disabled = !draftRow.valid;
438
+ cells[toPlaneCellIndex({ col: 0, row })] = {
439
+ value: '',
440
+ readonly: true,
441
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
442
+ };
443
+ }
444
+ }
445
+
446
+ return cells;
447
+ }
448
+
449
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
450
+ const cells: DxGridPlaneCells = {};
451
+ const draftRows = this.model.getDraftRows();
406
452
 
453
+ for (let row = range.start.row; row <= range.end.row; row++) {
407
454
  cells[toPlaneCellIndex({ col: 0, row })] = {
408
455
  value: '',
409
456
  readonly: true,
410
- accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
457
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
458
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
411
459
  };
412
460
  }
413
461
 
@@ -418,7 +466,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
418
466
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
419
467
  if (!selected) {
420
468
  if (selectionMode === 'single') {
421
- return ['!cursor-pointer'];
469
+ return ['cursor-pointer!'];
422
470
  } else {
423
471
  return undefined;
424
472
  }
@@ -426,9 +474,8 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
426
474
 
427
475
  switch (selectionMode) {
428
476
  case 'single':
429
- // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
430
- return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
477
+ return ['dx-grid__cell--no-focus-unfurl !bg-current-surface hover:bg-hover-surface cursor-pointer!'];
431
478
  case 'multiple':
432
- return ['!bg-gridCellSelected'];
479
+ return ['!bg-grid-cell-selected'];
433
480
  }
434
481
  };
@@ -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
  }
@@ -10,7 +10,7 @@ export default defineConfig({
10
10
  ...e2ePreset(import.meta.dirname),
11
11
  // TODO(wittjosiah): Avoid hard-coding ports.
12
12
  webServer: {
13
- command: 'moon run storybook:serve-e2e -- --port=9004',
13
+ command: 'pnpm storybook dev --ci --quiet --port=9004 --config-dir=.storybook',
14
14
  port: 9004,
15
15
  reuseExistingServer: false,
16
16
  },