@dxos/react-ui-table 0.7.5-main.499c70c → 0.7.5-main.6a330ac

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 (117) hide show
  1. package/dist/lib/browser/{chunk-AT7O4RAD.mjs → chunk-3FR6NHPD.mjs} +6 -4
  2. package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +482 -284
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-YSBXNBDJ.cjs → chunk-NB3SFHGA.cjs} +8 -6
  8. package/dist/lib/node/chunk-NB3SFHGA.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +474 -270
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +3 -3
  13. package/dist/lib/node/types/index.cjs.map +1 -1
  14. package/dist/lib/node-esm/{chunk-6OH2NHUX.mjs → chunk-RLVVOZW5.mjs} +6 -4
  15. package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +482 -284
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +1 -1
  20. package/dist/types/src/_deprecated/components/Cell/Cell.d.ts +2 -2
  21. package/dist/types/src/_deprecated/components/Cell/Cell.d.ts.map +1 -1
  22. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts +1 -2
  23. package/dist/types/src/_deprecated/components/Cell/CellCombobox.d.ts.map +1 -1
  24. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts +2 -3
  25. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.d.ts.map +1 -1
  26. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts +6 -7
  27. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -1
  28. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts +1 -2
  29. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.d.ts.map +1 -1
  30. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts +6 -7
  31. package/dist/types/src/_deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts.map +1 -1
  32. package/dist/types/src/_deprecated/components/Table/Table.d.ts +5 -5
  33. package/dist/types/src/_deprecated/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts +8 -9
  35. package/dist/types/src/_deprecated/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts +1 -2
  37. package/dist/types/src/_deprecated/components/Table/TableBody.d.ts.map +1 -1
  38. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts +1 -1
  39. package/dist/types/src/_deprecated/components/Table/TableContext.d.ts.map +1 -1
  40. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts +1 -2
  41. package/dist/types/src/_deprecated/components/Table/TableHead.d.ts.map +1 -1
  42. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts +2 -3
  43. package/dist/types/src/_deprecated/schema/Schema.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  45. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  46. package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -2
  47. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  48. package/dist/types/src/components/Table/CreateRefPanel.d.ts +1 -2
  49. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  50. package/dist/types/src/components/Table/RefPanel.d.ts +1 -2
  51. package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
  52. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -2
  53. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  54. package/dist/types/src/components/Table/Table.d.ts +1 -1
  55. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  56. package/dist/types/src/components/Table/Table.stories.d.ts +1 -4
  57. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  58. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +1 -2
  59. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  60. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +17 -0
  61. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
  62. package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
  63. package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
  64. package/dist/types/src/components/index.d.ts +1 -1
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/components/testing.d.ts +12 -35
  67. package/dist/types/src/components/testing.d.ts.map +1 -1
  68. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  69. package/dist/types/src/model/table-model.d.ts +11 -19
  70. package/dist/types/src/model/table-model.d.ts.map +1 -1
  71. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  72. package/dist/types/src/model/table-sorting.d.ts +53 -0
  73. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +2 -1
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/table.d.ts +21 -90
  77. package/dist/types/src/types/table.d.ts.map +1 -1
  78. package/dist/types/src/util/table-buttons.d.ts +15 -1
  79. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  80. package/dist/types/src/util/table.d.ts +2 -1
  81. package/dist/types/src/util/table.d.ts.map +1 -1
  82. package/dist/types/tsconfig.tsbuildinfo +1 -1
  83. package/package.json +36 -34
  84. package/src/_deprecated/schema/Schema.stories.tsx +1 -1
  85. package/src/components/Table/ColumnActionsMenu.tsx +4 -4
  86. package/src/components/Table/CreateRefPanel.tsx +3 -4
  87. package/src/components/Table/Table.stories.tsx +55 -44
  88. package/src/components/Table/Table.tsx +4 -0
  89. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +1 -1
  90. package/src/components/TableToolbar/TableToolbar.tsx +72 -0
  91. package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
  92. package/src/components/index.ts +1 -1
  93. package/src/hooks/useTableModel.ts +21 -0
  94. package/src/model/table-model.test.ts +0 -5
  95. package/src/model/table-model.ts +41 -99
  96. package/src/model/table-presentation.ts +25 -4
  97. package/src/model/table-sorting.ts +215 -0
  98. package/src/playwright/smoke.spec.ts +35 -13
  99. package/src/translations.ts +2 -1
  100. package/src/types/table.ts +3 -3
  101. package/src/util/table-buttons.ts +33 -3
  102. package/src/util/table-controls.ts +2 -2
  103. package/src/util/table.ts +85 -52
  104. package/dist/lib/browser/chunk-AT7O4RAD.mjs.map +0 -7
  105. package/dist/lib/node/chunk-YSBXNBDJ.cjs.map +0 -7
  106. package/dist/lib/node-esm/chunk-6OH2NHUX.mjs.map +0 -7
  107. package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
  108. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
  109. package/dist/types/src/components/Toolbar/common.d.ts +0 -14
  110. package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
  111. package/dist/types/src/components/Toolbar/index.d.ts +0 -2
  112. package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
  113. package/dist/types/src/playwright/playwright.config.d.ts +0 -3
  114. package/dist/types/src/playwright/playwright.config.d.ts.map +0 -1
  115. package/src/components/Toolbar/Toolbar.tsx +0 -87
  116. package/src/components/Toolbar/common.tsx +0 -71
  117. /package/src/playwright/{playwright.config.ts → playwright.config.cts} +0 -0
@@ -0,0 +1,215 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { computed, signal, type Signal, type ReadonlySignal } from '@preact/signals-core';
6
+ import orderBy from 'lodash.orderby';
7
+
8
+ import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
9
+ import { formatForDisplay } from '@dxos/react-ui-form';
10
+ import type { PropertyType, FieldType, ViewProjection, ViewType } from '@dxos/schema';
11
+
12
+ import { type BaseTableRow } from './table-model';
13
+
14
+ /**
15
+ * Represents the local sort state.
16
+ * - undefined: No local sort set, fallback to view sort.
17
+ * - cleared: Sort explicitly cleared by user.
18
+ * - active: Active local sort with sort configuration.
19
+ */
20
+ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } | undefined;
21
+
22
+ /**
23
+ * Manages table sorting functionality through display and data index mapping.
24
+ *
25
+ * When a table is sorted, rows visually reorder but we need to maintain references
26
+ * to the original data positions. This is handled through two types of indices:
27
+ *
28
+ * - Display indices: The visual positions after sorting
29
+ * - Data indices: The original positions in the source data
30
+ *
31
+ * The _displayToDataIndex map maintains these relationships, enabling lookups
32
+ * between visual and source positions.
33
+ *
34
+ * Example mapping between display and data indices:
35
+ *
36
+ * Original Data: Sorted Display: Mapping: (Display → Data)
37
+ * ┌──────────────┐ ┌──────────────┐ ┌────────┐
38
+ * │ 0: "Banana" │ => │ 0: "Apple" │ => │ 0 => 1 │
39
+ * │ 1: "Apple" │ => │ 1: "Banana" │ => │ 1 => 0 │
40
+ * │ 2: "Cherry" │ => │ 2: "Cherry" │ => │ 2 => 2 │
41
+ * └──────────────┘ └──────────────┘ └────────┘
42
+ */
43
+ export class TableSorting<T extends BaseTableRow> {
44
+ private readonly _displayToDataIndex = new Map<number, number>();
45
+ private readonly _rows: Signal<T[]>;
46
+ private readonly _localSort = signal<LocalSort>(undefined);
47
+ private readonly _isDirty: ReadonlySignal<boolean>;
48
+ public readonly sortedRows: ReadonlySignal<T[]>;
49
+
50
+ constructor(
51
+ rows: Signal<T[]>,
52
+ private readonly _view: ViewType | undefined,
53
+ private readonly _projection: ViewProjection,
54
+ ) {
55
+ this._rows = rows;
56
+ this._isDirty = computed(() => {
57
+ const local = this._localSort.value;
58
+ const viewSort = this._view?.query.sort?.[0];
59
+ if (local?.type === 'cleared') {
60
+ return viewSort !== undefined;
61
+ }
62
+ if (local?.type === 'active') {
63
+ if (!viewSort) {
64
+ return true;
65
+ }
66
+ return local.sort.fieldId !== viewSort.fieldId || local.sort.direction !== viewSort.direction;
67
+ }
68
+ return false;
69
+ });
70
+ this.sortedRows = this.initialiseSortedRows();
71
+ }
72
+
73
+ /**
74
+ * @reactive
75
+ * @returns Local sort if present, falls back to view sort.
76
+ */
77
+ public get sorting(): FieldSortType | undefined {
78
+ const local = this._localSort.value;
79
+ if (local?.type === 'cleared') {
80
+ return undefined;
81
+ }
82
+ if (local?.type === 'active') {
83
+ return local.sort;
84
+ }
85
+ return this._view?.query.sort?.[0];
86
+ }
87
+
88
+ /**
89
+ * @reactive
90
+ * @returns Whether local sort differs from view sort.
91
+ */
92
+ public get isDirty(): boolean {
93
+ return this._isDirty.value;
94
+ }
95
+
96
+ public getDataIndex(displayIndex: number): number {
97
+ return this._displayToDataIndex.get(displayIndex) ?? displayIndex;
98
+ }
99
+
100
+ public setSort(fieldId: string, direction: SortDirectionType): void {
101
+ this._localSort.value = {
102
+ type: 'active',
103
+ sort: { fieldId, direction },
104
+ };
105
+ }
106
+
107
+ public toggleSort(fieldId: string): void {
108
+ if (!this._view || !this.sorting || this.sorting.fieldId !== fieldId) {
109
+ return;
110
+ }
111
+
112
+ switch (this.sorting.direction) {
113
+ case 'asc': {
114
+ this.setSort(fieldId, 'desc');
115
+ break;
116
+ }
117
+ case 'desc': {
118
+ this.setSort(fieldId, 'asc');
119
+ break;
120
+ }
121
+ }
122
+ }
123
+
124
+ public clearSort(): void {
125
+ this._localSort.value = { type: 'cleared' };
126
+ }
127
+
128
+ public save(): void {
129
+ if (this._view && this._localSort.value !== undefined) {
130
+ if (this._localSort.value.type === 'active') {
131
+ this._view.query.sort = [this._localSort.value.sort];
132
+ } else {
133
+ this._view.query.sort = [];
134
+ }
135
+ this._localSort.value = undefined;
136
+ }
137
+ }
138
+
139
+ //
140
+ // Initialisation.
141
+ //
142
+
143
+ private initialiseSortedRows(): ReadonlySignal<T[]> {
144
+ return computed(() => {
145
+ this._displayToDataIndex.clear();
146
+ const sort = this.sorting;
147
+ if (!sort || !this._view) {
148
+ return this._rows.value;
149
+ }
150
+
151
+ const field = this._view.fields.find((f) => f.id === sort.fieldId);
152
+ if (!field) {
153
+ return this._rows.value;
154
+ }
155
+
156
+ const { props } = this._projection.getFieldProjection(field.id);
157
+
158
+ const dataWithIndices = this._rows.value.map((item, index) => {
159
+ const sortValue = this.getSortValue(props, field, item);
160
+ const isEmpty = !(props.type === TypeEnum.Boolean) && (sortValue == null || sortValue === '');
161
+
162
+ return { item, index, value: sortValue, isEmpty };
163
+ });
164
+
165
+ const ordered = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
166
+
167
+ ordered.forEach(({ index: dataIndex }, displayIndex) => {
168
+ if (displayIndex !== dataIndex) {
169
+ this._displayToDataIndex.set(displayIndex, dataIndex);
170
+ }
171
+ });
172
+
173
+ return ordered.map(({ item }) => item);
174
+ });
175
+ }
176
+
177
+ private getSortValue(props: PropertyType, field: FieldType, item: T) {
178
+ const { type, format } = props;
179
+ const value = getValue(item, field.path);
180
+
181
+ if (type === TypeEnum.Boolean) {
182
+ return !!value; // Coerce null/undefined to false for booleans.
183
+ }
184
+ if (type === TypeEnum.Number) {
185
+ return value;
186
+ }
187
+ if (value == null) {
188
+ return value;
189
+ }
190
+
191
+ /**
192
+ * For single select fields, the sort value is determined by the index of the option in the
193
+ * options array. This ensures sort order matches the configured option order.
194
+ */
195
+ if (format === FormatEnum.SingleSelect) {
196
+ const options = props.options;
197
+ if (!options) {
198
+ return undefined;
199
+ }
200
+
201
+ const option = options.find((option) => option.id === value);
202
+ return option ? options.indexOf(option) : undefined;
203
+ }
204
+
205
+ // TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
206
+ // Right now we're going to be sorting based on the raw values.
207
+ // Maybe we need to recurse this function with the referenced prop format and field?
208
+ if (format === FormatEnum.Ref && field.referencePath) {
209
+ const refValue = getValue(value.target, field.referencePath);
210
+ return refValue;
211
+ }
212
+
213
+ return formatForDisplay({ type, format, value });
214
+ }
215
+ }
@@ -13,17 +13,17 @@ const storyUrl = `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story
13
13
 
14
14
  // NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
15
15
  test.describe('Table', () => {
16
- test('Loads', async ({ browser }) => {
16
+ test('Loads', async ({ browser, browserName }) => {
17
+ test.skip(browserName === 'webkit');
17
18
  const { page } = await setupPage(browser, { url: storyUrl });
18
19
  const table = new TableManager(page);
19
20
 
20
- // (name, email, salary, manager) * 10 rows.
21
21
  await table.grid.ready();
22
- await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
23
22
  await page.close();
24
23
  });
25
24
 
26
- test('sort', async ({ browser }) => {
25
+ test('sort', async ({ browser, browserName }) => {
26
+ test.skip(browserName === 'webkit');
27
27
  const { page } = await setupPage(browser, { url: storyUrl });
28
28
  const table = new TableManager(page);
29
29
 
@@ -38,7 +38,8 @@ test.describe('Table', () => {
38
38
  await page.close();
39
39
  });
40
40
 
41
- test('selection', async ({ browser }) => {
41
+ test('selection', async ({ browser, browserName }) => {
42
+ test.skip(browserName === 'webkit');
42
43
  const { page } = await setupPage(browser, { url: storyUrl });
43
44
  const table = new TableManager(page);
44
45
 
@@ -58,7 +59,8 @@ test.describe('Table', () => {
58
59
  await page.close();
59
60
  });
60
61
 
61
- test('delete row', async ({ browser }) => {
62
+ test('delete row', async ({ browser, browserName }) => {
63
+ test.skip(browserName === 'webkit');
62
64
  const { page } = await setupPage(browser, { url: storyUrl });
63
65
  const table = new TableManager(page);
64
66
 
@@ -68,7 +70,8 @@ test.describe('Table', () => {
68
70
  await page.close();
69
71
  });
70
72
 
71
- test('delete row--select all', async ({ browser }) => {
73
+ test('delete row--select all', async ({ browser, browserName }) => {
74
+ test.skip(browserName === 'webkit');
72
75
  const { page } = await setupPage(browser, { url: storyUrl });
73
76
  const table = new TableManager(page);
74
77
 
@@ -84,7 +87,8 @@ test.describe('Table', () => {
84
87
  await page.close();
85
88
  });
86
89
 
87
- test('delete column', async ({ browser }) => {
90
+ test('delete column', async ({ browser, browserName }) => {
91
+ test.skip(browserName === 'webkit');
88
92
  const { page } = await setupPage(browser, { url: storyUrl });
89
93
  const table = new TableManager(page);
90
94
 
@@ -110,7 +114,8 @@ test.describe('Table', () => {
110
114
 
111
115
  // Rest of add column test remains the same as it's a more complex flow.
112
116
  // TODO(ZaymonFC): Restore this after fixing format selection.
113
- test('add column', async ({ browser }) => {
117
+ test('add column', async ({ browser, browserName }) => {
118
+ test.skip(browserName === 'webkit');
114
119
  const { page } = await setupPage(browser, { url: storyUrl });
115
120
  const table = new TableManager(page);
116
121
 
@@ -123,25 +128,42 @@ test.describe('Table', () => {
123
128
  await page.close();
124
129
  });
125
130
 
126
- test('pick reference', async ({ browser }) => {
131
+ test('reference > reference / create new object', async ({ browser, browserName }) => {
132
+ test.skip(browserName === 'webkit');
127
133
  const { page } = await setupPage(browser, { url: storyUrl });
128
134
  const table = new TableManager(page);
129
135
 
130
136
  await table.grid.ready();
131
137
 
138
+ // Reference existing object.
139
+ // Scroll over to the manager column.
132
140
  await table.grid.panByWheel(10000, 0);
133
141
 
134
142
  await table.grid.cell(4, 0, 'grid').click();
135
143
  await page.keyboard.press('A');
144
+ await page.keyboard.press('N');
136
145
  await page.getByRole('option', { name: 'Anita Mayer' }).click();
137
146
 
138
147
  // Assert that the value is shown in the cell.
139
- await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toBeVisible();
148
+ await expect(page.getByRole('gridcell', { name: 'Anita Mayer' }).first()).toBeVisible();
149
+
150
+ // Create new object.
151
+ await table.grid.cell(4, 1, 'grid').click();
152
+ await page.keyboard.type('t');
153
+ await page.keyboard.type('e');
154
+ await page.keyboard.type('s');
155
+ await page.keyboard.type('t');
156
+ await page.getByText('Create new object', { exact: false }).click();
157
+ await page.getByTestId('save-button').click();
158
+
159
+ // Scroll to the left.
160
+ await page.getByRole('gridcell', { name: 'test' }).first().click();
140
161
 
141
162
  await page.close();
142
163
  });
143
164
 
144
- test('test toggles', async ({ browser }) => {
165
+ test('test toggles', async ({ browser, browserName }) => {
166
+ test.skip(browserName === 'webkit');
145
167
  const { page } = await setupPage(browser, { url: storyUrl });
146
168
  const table = new TableManager(page);
147
169
 
@@ -151,7 +173,7 @@ test.describe('Table', () => {
151
173
  await page.getByTestId('table-switch').nth(7).click();
152
174
 
153
175
  // Test that checks are durable in the data model by sorting.
154
- await table.sortColumn(1, 'descending');
176
+ await table.sortColumn(3, 'descending');
155
177
 
156
178
  // Assert the first two switch checkboxes are checked.
157
179
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
@@ -16,7 +16,7 @@ export default [
16
16
  'new schema': 'New schema',
17
17
  'continue label': 'Continue',
18
18
  'create stack section label': 'Create table',
19
- 'add row': 'Add row',
19
+ 'add row label': 'Add row',
20
20
  'create comment': 'Create thread',
21
21
  'column action sort descending': 'Sort Descending',
22
22
  'column action sort ascending': 'Sort Ascending',
@@ -28,6 +28,7 @@ export default [
28
28
  'column deleted label': 'Column deleted',
29
29
  'new column button label': 'Create column',
30
30
  'create new object label': 'Create new object “{{text}}”',
31
+ 'save view label': 'Save changes to view',
31
32
  },
32
33
  },
33
34
  },
@@ -2,8 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';
6
- import { ThreadType } from '@dxos/plugin-space';
5
+ import { Ref, ObjectId, S, TypedObject, Expando } from '@dxos/echo-schema';
6
+ // import { ThreadType } from '@dxos/plugin-space/types';
7
7
  import { ViewType } from '@dxos/schema';
8
8
 
9
9
  export const TableSchema = S.Struct({
@@ -11,7 +11,7 @@ export const TableSchema = S.Struct({
11
11
  name: S.optional(S.String),
12
12
  view: S.optional(Ref(ViewType)),
13
13
  // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.
14
- threads: S.optional(S.Array(Ref(ThreadType))),
14
+ threads: S.optional(S.Array(Ref(Expando /* ThreadType */))), // TODO(dmaretskyi): Breaks edge because plugin-space depends on react-client.
15
15
  });
16
16
 
17
17
  // type TableType = S.Schema.Type<typeof TableSchema>;
@@ -2,20 +2,22 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell';
5
+ export type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell' | 'sort';
6
6
 
7
7
  export const BUTTON_IDENTIFIERS: { [K in TableButton]: string } = {
8
8
  columnSettings: 'data-table-column-settings-button',
9
9
  newColumn: 'data-table-new-column-button',
10
10
  referencedCell: 'data-table-ref-cell-button',
11
11
  rowMenu: 'data-table-row-menu-button',
12
+ sort: 'data-table-sort-button',
12
13
  } as const;
13
14
 
14
15
  type ButtonData =
15
16
  | { type: 'columnSettings'; fieldId: string }
16
17
  | { type: 'newColumn'; disabled?: boolean }
17
18
  | { type: 'referencedCell'; schemaId: string; targetId: string }
18
- | { type: 'rowMenu'; rowIndex: number };
19
+ | { type: 'rowMenu'; rowIndex: number }
20
+ | { type: 'sort'; fieldId: string; direction?: 'asc' | 'desc' };
19
21
 
20
22
  const createButton = ({
21
23
  attr,
@@ -23,18 +25,21 @@ const createButton = ({
23
25
  disabled = false,
24
26
  icon,
25
27
  testId,
28
+ type = 'primary',
26
29
  }: {
27
30
  attr: string;
28
31
  data: Record<string, string>;
29
32
  disabled?: boolean;
30
33
  icon: string;
31
34
  testId: string;
35
+ type?: 'primary' | 'secondary';
32
36
  }) => {
33
37
  const dataAttrs = Object.entries(data)
34
38
  .map(([k, v]) => `${k}="${v}"`)
35
39
  .join(' ');
36
40
 
37
- return `<button ${attr} data-testid="${testId}" class="ch-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 inline-end-2" ${dataAttrs} ${disabled ? 'disabled' : ''}><svg><use href="/icons.svg#${icon}"/></svg></button>`;
41
+ const positionClass = type === 'primary' ? 'inline-end-2' : 'inline-end-9';
42
+ return `<button ${attr} data-testid="${testId}" class="dx-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 ${positionClass}" ${dataAttrs} ${disabled ? 'disabled' : ''}><svg><use href="/icons.svg#${icon}"/></svg></button>`;
38
43
  };
39
44
 
40
45
  const addColumnButton = {
@@ -111,9 +116,34 @@ const rowMenuButton = {
111
116
  }),
112
117
  } as const;
113
118
 
119
+ const sortButton = {
120
+ attr: BUTTON_IDENTIFIERS.sort,
121
+ icon: 'ph--sort-ascending--regular',
122
+ render: ({ fieldId, direction }: Omit<Extract<ButtonData, { type: 'sort' }>, 'type'>) => {
123
+ return createButton({
124
+ attr: BUTTON_IDENTIFIERS.sort,
125
+ icon: direction === 'desc' ? 'ph--sort-descending--regular' : 'ph--sort-ascending--regular',
126
+ data: {
127
+ 'data-field-id': fieldId,
128
+ 'data-direction': direction ?? '',
129
+ },
130
+ testId: 'table-sort-button',
131
+ // TODO(ZaymonFC): All buttons should be rendered into an absolutely positioned flex container.
132
+ // This is a bit hacky.
133
+ type: 'secondary',
134
+ });
135
+ },
136
+ getData: (el: HTMLElement): Extract<ButtonData, { type: 'sort' }> => ({
137
+ type: 'sort',
138
+ fieldId: el.getAttribute('data-field-id')!,
139
+ direction: el.getAttribute('data-direction') as 'asc' | 'desc' | undefined,
140
+ }),
141
+ } as const;
142
+
114
143
  export const tableButtons = {
115
144
  addColumn: addColumnButton,
116
145
  columnSettings: columnSettingsButton,
117
146
  referencedCell: referencedCellButton,
118
147
  rowMenu: rowMenuButton,
148
+ sort: sortButton,
119
149
  } as const;
@@ -23,8 +23,8 @@ export const CONTROL_IDENTIFIERS = {
23
23
  } as const;
24
24
 
25
25
  const BASE_CLASSES = {
26
- checkbox: 'absolute inset-block-[.375rem] inline-end-[.375rem] ch-checkbox',
27
- switch: 'absolute inset-block-[.25rem] inline-end-[.25rem] ch-checkbox--switch',
26
+ checkbox: 'absolute inset-block-[.375rem] inline-end-[.375rem] dx-checkbox',
27
+ switch: 'absolute inset-block-[.25rem] inline-end-[.25rem] dx-checkbox--switch',
28
28
  } as const;
29
29
 
30
30
  const renderAttributes = (data: Record<string, string>) => {
package/src/util/table.ts CHANGED
@@ -13,10 +13,9 @@ import {
13
13
  TypedObject,
14
14
  TypeEnum,
15
15
  } from '@dxos/echo-schema';
16
- import { log } from '@dxos/log';
17
16
  import { PublicKey } from '@dxos/react-client';
18
17
  import { create, makeRef, type Space } from '@dxos/react-client/echo';
19
- import { createFieldId, createView, ViewProjection } from '@dxos/schema';
18
+ import { createFieldId, createView, getSchemaProperties, ViewProjection } from '@dxos/schema';
20
19
 
21
20
  import { type TableType } from '../types';
22
21
 
@@ -24,70 +23,104 @@ type InitialiseTableProps = {
24
23
  space: Space;
25
24
  table: TableType;
26
25
  initialRow?: boolean;
26
+ initialSchema?: string;
27
27
  };
28
28
 
29
+ // TODO(ZaymonFC): Clean up the branching in this file.
30
+
29
31
  // TODO(burdon): Pass in type.
30
32
  // TODO(burdon): User should determine typename.
31
33
  export const initializeTable = async ({
32
34
  space,
33
35
  table,
34
36
  initialRow = true,
37
+ initialSchema,
35
38
  }: InitialiseTableProps): Promise<EchoSchema> => {
36
- log('initializeTable', { table });
39
+ if (initialSchema) {
40
+ const schema = await space.db.schemaRegistry.query({ typename: initialSchema }).firstOrUndefined();
37
41
 
38
- const ContactSchema = TypedObject({
39
- typename: `example.com/type/${PublicKey.random().truncate()}`,
40
- version: '0.1.0',
41
- })({
42
- name: S.optional(S.String).annotations({
43
- [AST.TitleAnnotationId]: 'Name',
44
- }),
45
- active: S.optional(S.Boolean),
46
- email: S.optional(Format.Email),
47
- salary: S.optional(Format.Currency()).annotations({
48
- [AST.TitleAnnotationId]: 'Salary',
49
- }), // TODO(burdon): Should default to prop name?
50
- });
42
+ if (!schema) {
43
+ throw new Error(`Schema not found: ${initialSchema}`);
44
+ }
51
45
 
52
- const [contactSchema] = await space.db.schemaRegistry.register([ContactSchema]);
46
+ // We need to get the schema properties here. For now, only simple types and refs, not compound types
47
+ // are going to be supported.
48
+ const fields = getSchemaProperties(schema.ast)
49
+ .filter((prop) => prop.format !== FormatEnum.Ref)
50
+ .map((prop) => prop.name);
53
51
 
54
- table.view = makeRef(
55
- createView({
56
- name: 'Test',
57
- typename: contactSchema.typename,
58
- jsonSchema: contactSchema.jsonSchema,
59
- fields: ['name', 'active', 'email', 'salary'],
60
- }),
61
- );
52
+ table.view = makeRef(
53
+ createView({
54
+ // TODO(ZaymonFC): Don't hardcode name?
55
+ name: 'View',
56
+ typename: schema.typename,
57
+ jsonSchema: schema.jsonSchema,
58
+ fields,
59
+ }),
60
+ );
62
61
 
63
- const projection = new ViewProjection(contactSchema, table.view.target!);
64
- projection.setFieldProjection({
65
- field: {
66
- id: table.view.target!.fields.find((f) => f.path === 'salary')!.id,
67
- path: 'salary' as JsonPath,
68
- size: 150,
69
- },
70
- });
62
+ return schema;
63
+ } else {
64
+ const ContactSchema = TypedObject({
65
+ typename: `example.com/type/${PublicKey.random().truncate()}`,
66
+ version: '0.1.0',
67
+ })({
68
+ name: S.optional(S.String).annotations({
69
+ [AST.TitleAnnotationId]: 'Name',
70
+ }),
71
+ active: S.optional(S.Boolean),
72
+ email: S.optional(Format.Email),
73
+ salary: S.optional(Format.Currency()).annotations({
74
+ [AST.TitleAnnotationId]: 'Salary',
75
+ }), // TODO(burdon): Should default to prop name?
76
+ });
71
77
 
72
- projection.setFieldProjection({
73
- field: {
74
- id: createFieldId(),
75
- path: 'manager' as JsonPath,
76
- referencePath: 'name' as JsonPath,
77
- },
78
- props: {
79
- property: 'manager' as JsonProp,
80
- type: TypeEnum.Ref,
81
- format: FormatEnum.Ref,
82
- referenceSchema: contactSchema.typename,
83
- title: 'Manager',
84
- },
85
- });
78
+ const [contactSchema] = await space.db.schemaRegistry.register([ContactSchema]);
86
79
 
87
- if (initialRow) {
88
- // TODO(burdon): Last (first) row should not be in db and should be managed by the model.
89
- space.db.add(create(contactSchema, {}));
90
- }
80
+ table.view = makeRef(
81
+ createView({
82
+ name: 'View',
83
+ typename: contactSchema.typename,
84
+ jsonSchema: contactSchema.jsonSchema,
85
+ fields: ['name', 'email', 'salary', 'active'],
86
+ }),
87
+ );
88
+
89
+ const projection = new ViewProjection(contactSchema, table.view.target!);
90
+ projection.setFieldProjection({
91
+ field: {
92
+ id: table.view.target!.fields.find((f) => f.path === 'salary')!.id,
93
+ path: 'salary' as JsonPath,
94
+ size: 150,
95
+ },
96
+ });
97
+ projection.setFieldProjection({
98
+ field: {
99
+ id: table.view.target!.fields.find((f) => f.path === 'active')!.id,
100
+ path: 'active' as JsonPath,
101
+ size: 100,
102
+ },
103
+ });
91
104
 
92
- return contactSchema;
105
+ projection.setFieldProjection({
106
+ field: {
107
+ id: createFieldId(),
108
+ path: 'manager' as JsonPath,
109
+ referencePath: 'name' as JsonPath,
110
+ },
111
+ props: {
112
+ property: 'manager' as JsonProp,
113
+ type: TypeEnum.Ref,
114
+ format: FormatEnum.Ref,
115
+ referenceSchema: contactSchema.typename,
116
+ title: 'Manager',
117
+ },
118
+ });
119
+ if (initialRow) {
120
+ // TODO(burdon): Last (first) row should not be in db and should be managed by the model.
121
+ space.db.add(create(contactSchema, {}));
122
+ }
123
+
124
+ return contactSchema;
125
+ }
93
126
  };
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/types/table.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';\nimport { ThreadType } from '@dxos/plugin-space';\nimport { ViewType } from '@dxos/schema';\n\nexport const TableSchema = S.Struct({\n id: ObjectId, // TODO(burdon): Where should this be?\n name: S.optional(S.String),\n view: S.optional(Ref(ViewType)),\n // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.\n threads: S.optional(S.Array(Ref(ThreadType))),\n});\n\n// type TableType = S.Schema.Type<typeof TableSchema>;\n\nexport class TableType extends TypedObject({\n typename: 'dxos.org/type/Table',\n version: '0.1.0',\n})(TableSchema.fields) {}\n"],
5
- "mappings": ";AAIA,SAASA,KAAKC,UAAUC,GAAGC,mBAAmB;AAC9C,SAASC,kBAAkB;AAC3B,SAASC,gBAAgB;AAElB,IAAMC,cAAcC,EAAEC,OAAO;EAClCC,IAAIC;EACJC,MAAMJ,EAAEK,SAASL,EAAEM,MAAM;EACzBC,MAAMP,EAAEK,SAASG,IAAIC,QAAAA,CAAAA;;EAErBC,SAASV,EAAEK,SAASL,EAAEW,MAAMH,IAAII,UAAAA,CAAAA,CAAAA;AAClC,CAAA;AAIO,IAAMC,YAAN,cAAwBC,YAAY;EACzCC,UAAU;EACVC,SAAS;AACX,CAAA,EAAGjB,YAAYkB,MAAM,EAAA;AAAG;",
6
- "names": ["Ref", "ObjectId", "S", "TypedObject", "ThreadType", "ViewType", "TableSchema", "S", "Struct", "id", "ObjectId", "name", "optional", "String", "view", "Ref", "ViewType", "threads", "Array", "ThreadType", "TableType", "TypedObject", "typename", "version", "fields"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/types/table.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';\nimport { ThreadType } from '@dxos/plugin-space';\nimport { ViewType } from '@dxos/schema';\n\nexport const TableSchema = S.Struct({\n id: ObjectId, // TODO(burdon): Where should this be?\n name: S.optional(S.String),\n view: S.optional(Ref(ViewType)),\n // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.\n threads: S.optional(S.Array(Ref(ThreadType))),\n});\n\n// type TableType = S.Schema.Type<typeof TableSchema>;\n\nexport class TableType extends TypedObject({\n typename: 'dxos.org/type/Table',\n version: '0.1.0',\n})(TableSchema.fields) {}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;AAIA,yBAA8C;AAC9C,0BAA2B;AAC3B,oBAAyB;AAElB,IAAMA,cAAcC,qBAAEC,OAAO;EAClCC,IAAIC;EACJC,MAAMJ,qBAAEK,SAASL,qBAAEM,MAAM;EACzBC,MAAMP,qBAAEK,aAASG,wBAAIC,sBAAAA,CAAAA;;EAErBC,SAASV,qBAAEK,SAASL,qBAAEW,UAAMH,wBAAII,8BAAAA,CAAAA,CAAAA;AAClC,CAAA;AAIO,IAAMC,YAAN,kBAAwBC,gCAAY;EACzCC,UAAU;EACVC,SAAS;AACX,CAAA,EAAGjB,YAAYkB,MAAM,EAAA;AAAG;",
6
- "names": ["TableSchema", "S", "Struct", "id", "ObjectId", "name", "optional", "String", "view", "Ref", "ViewType", "threads", "Array", "ThreadType", "TableType", "TypedObject", "typename", "version", "fields"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/types/table.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';\nimport { ThreadType } from '@dxos/plugin-space';\nimport { ViewType } from '@dxos/schema';\n\nexport const TableSchema = S.Struct({\n id: ObjectId, // TODO(burdon): Where should this be?\n name: S.optional(S.String),\n view: S.optional(Ref(ViewType)),\n // TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.\n threads: S.optional(S.Array(Ref(ThreadType))),\n});\n\n// type TableType = S.Schema.Type<typeof TableSchema>;\n\nexport class TableType extends TypedObject({\n typename: 'dxos.org/type/Table',\n version: '0.1.0',\n})(TableSchema.fields) {}\n"],
5
- "mappings": ";;;AAIA,SAASA,KAAKC,UAAUC,GAAGC,mBAAmB;AAC9C,SAASC,kBAAkB;AAC3B,SAASC,gBAAgB;AAElB,IAAMC,cAAcC,EAAEC,OAAO;EAClCC,IAAIC;EACJC,MAAMJ,EAAEK,SAASL,EAAEM,MAAM;EACzBC,MAAMP,EAAEK,SAASG,IAAIC,QAAAA,CAAAA;;EAErBC,SAASV,EAAEK,SAASL,EAAEW,MAAMH,IAAII,UAAAA,CAAAA,CAAAA;AAClC,CAAA;AAIO,IAAMC,YAAN,cAAwBC,YAAY;EACzCC,UAAU;EACVC,SAAS;AACX,CAAA,EAAGjB,YAAYkB,MAAM,EAAA;AAAG;",
6
- "names": ["Ref", "ObjectId", "S", "TypedObject", "ThreadType", "ViewType", "TableSchema", "S", "Struct", "id", "ObjectId", "name", "optional", "String", "view", "Ref", "ViewType", "threads", "Array", "ThreadType", "TableType", "TypedObject", "typename", "version", "fields"]
7
- }