@dxos/react-ui-table 0.8.4-main.1da679c → 0.8.4-main.21d9917

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 (140) hide show
  1. package/dist/lib/browser/chunk-MNZKOFMA.mjs +1746 -0
  2. package/dist/lib/browser/chunk-MNZKOFMA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1134 -1150
  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 +29 -39
  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-2ATAIIAO.mjs +1748 -0
  12. package/dist/lib/node-esm/chunk-2ATAIIAO.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1134 -1150
  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 +29 -39
  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 +1 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +5 -6
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +6 -20
  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 +77 -10
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +11 -8
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +76 -11
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -7
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +5 -6
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  44. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  45. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  47. package/dist/types/src/components/testing.d.ts +23 -16
  48. package/dist/types/src/components/testing.d.ts.map +1 -1
  49. package/dist/types/src/hooks/index.d.ts +2 -1
  50. package/dist/types/src/hooks/index.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  52. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  54. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/model/modal-controller.d.ts +5 -8
  58. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  59. package/dist/types/src/model/selection-model.d.ts +17 -8
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +103 -24
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +3 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  66. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  67. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  68. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  69. package/dist/types/src/translations.d.ts +4 -4
  70. package/dist/types/src/translations.d.ts.map +1 -1
  71. package/dist/types/src/types/Table.d.ts +31 -0
  72. package/dist/types/src/types/Table.d.ts.map +1 -0
  73. package/dist/types/src/types/index.d.ts +1 -1
  74. package/dist/types/src/types/index.d.ts.map +1 -1
  75. package/dist/types/src/util/dynamic-table.d.ts +13 -12
  76. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  77. package/dist/types/src/util/index.d.ts +2 -0
  78. package/dist/types/src/util/index.d.ts.map +1 -1
  79. package/dist/types/src/util/query-ast.d.ts +7 -0
  80. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  81. package/dist/types/src/util/schema.d.ts +1 -1
  82. package/dist/types/src/util/schema.d.ts.map +1 -1
  83. package/dist/types/src/util/sort.d.ts +12 -0
  84. package/dist/types/src/util/sort.d.ts.map +1 -0
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +56 -56
  87. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  88. package/src/components/Table/ColumnSettings.tsx +6 -6
  89. package/src/components/Table/DynamicTable.stories.tsx +18 -18
  90. package/src/components/Table/DynamicTable.tsx +23 -19
  91. package/src/components/Table/Relations.stories.tsx +143 -39
  92. package/src/components/Table/RowActionsMenu.tsx +24 -15
  93. package/src/components/Table/Table.stories.tsx +92 -95
  94. package/src/components/Table/Table.tsx +339 -307
  95. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  96. package/src/components/TableCellEditor/FormCellEditor.tsx +116 -129
  97. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +23 -47
  98. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -122
  99. package/src/components/TableCellEditor/index.ts +1 -0
  100. package/src/components/TableToolbar/TableToolbar.tsx +47 -31
  101. package/src/components/testing.ts +39 -13
  102. package/src/hooks/index.ts +2 -1
  103. package/src/hooks/useAddRow.ts +8 -10
  104. package/src/hooks/useProjectionModel.ts +40 -0
  105. package/src/hooks/useTableModel.ts +35 -33
  106. package/src/model/modal-controller.ts +23 -40
  107. package/src/model/selection-model.ts +75 -44
  108. package/src/model/table-model.test.ts +91 -58
  109. package/src/model/table-model.ts +429 -163
  110. package/src/model/table-presentation.test.ts +64 -57
  111. package/src/model/table-presentation.ts +34 -30
  112. package/src/playwright/TableManager.ts +5 -5
  113. package/src/playwright/playwright.config.ts +1 -1
  114. package/src/playwright/smoke.spec.ts +11 -8
  115. package/src/testing/useTestTableModel.ts +44 -35
  116. package/src/translations.ts +3 -3
  117. package/src/types/Table.ts +90 -0
  118. package/src/types/index.ts +1 -1
  119. package/src/util/dynamic-table.ts +41 -30
  120. package/src/util/index.ts +2 -0
  121. package/src/util/query-ast.ts +19 -0
  122. package/src/util/schema.ts +4 -4
  123. package/src/util/sort.ts +90 -0
  124. package/dist/lib/browser/chunk-63UUF4IG.mjs +0 -66
  125. package/dist/lib/browser/chunk-63UUF4IG.mjs.map +0 -7
  126. package/dist/lib/browser/chunk-BBXDTWCX.mjs +0 -1822
  127. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-K734DJB7.mjs +0 -67
  129. package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +0 -7
  130. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs +0 -1823
  131. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +0 -7
  132. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  133. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  134. package/dist/types/src/model/table-sorting.d.ts +0 -52
  135. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  136. package/dist/types/src/types/schema.d.ts +0 -28
  137. package/dist/types/src/types/schema.d.ts.map +0 -1
  138. package/src/components/Table/CreateRefPanel.tsx +0 -103
  139. package/src/model/table-sorting.ts +0 -225
  140. package/src/types/schema.ts +0 -73
@@ -2,22 +2,26 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
5
+ import { RegistryContext } from '@effect-atom/atom-react';
7
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { useEffect, useMemo, useState } from 'react';
7
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+ import { expect, userEvent, within } from 'storybook/test';
9
9
 
10
- import { Type } from '@dxos/echo';
11
- import { type JsonSchemaType } from '@dxos/echo-schema';
10
+ import { Obj, Type } from '@dxos/echo';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { type DxGrid } from '@dxos/lit-grid';
13
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
12
14
  import { faker } from '@dxos/random';
13
- import { useClient } from '@dxos/react-client';
14
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
15
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
15
16
  import { useAsyncEffect } from '@dxos/react-ui';
16
- import { DataType } from '@dxos/schema';
17
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
18
+ import { translations as formTranslations } from '@dxos/react-ui-form';
19
+ import { View } from '@dxos/schema';
17
20
  import { type ValueGenerator, createAsyncGenerator } from '@dxos/schema/testing';
18
- import { withLayout, withTheme } from '@dxos/storybook-utils';
21
+ import { withRegistry } from '@dxos/storybook-utils';
22
+ import { Organization, Person } from '@dxos/types';
19
23
 
20
- import { useTableModel } from '../../hooks';
24
+ import { useProjectionModel, useTableModel } from '../../hooks';
21
25
  import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
22
26
  import { translations } from '../../translations';
23
27
  import { Table } from '../../types';
@@ -32,10 +36,9 @@ const generator: ValueGenerator = faker as any;
32
36
  // TODO(burdon): Reconcile schemas types and utils (see API PR).
33
37
  // TODO(burdon): Base type for T (with id); see ECHO API PR?
34
38
  const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
35
- const client = useClient();
36
- const { space } = useClientProvider();
37
- const [view, setView] = useState<DataType.View>();
38
- const [jsonSchema, setJsonSchema] = useState<JsonSchemaType>();
39
+ const registry = useContext(RegistryContext);
40
+ const { space } = useClientStory();
41
+ const [object, setObject] = useState<Table.Table>();
39
42
 
40
43
  const features = useMemo<TableFeatures>(
41
44
  () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
@@ -47,18 +50,14 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
47
50
  return;
48
51
  }
49
52
 
50
- const {
51
- jsonSchema,
52
- schema: effectSchema,
53
- view,
54
- } = await Table.makeView({ client, space, typename: Type.getTypename(schema) });
55
- setJsonSchema(jsonSchema);
56
- setView(view);
57
- space.db.add(view);
58
- await space.db.schemaRegistry.register([effectSchema]);
59
- }, [client, space, schema]);
53
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Type.getTypename(schema) });
54
+ const object = Table.make({ view, jsonSchema });
55
+ setObject(object);
56
+ space.db.add(object);
57
+ }, [space, schema]);
60
58
 
61
- const model = useTableModel<TableRow>({ view, schema: jsonSchema, rows: [], features });
59
+ const projection = useProjectionModel(schema, object, registry);
60
+ const model = useTableModel<TableRow>({ object, projection, db: space?.db, features });
62
61
 
63
62
  useEffect(() => {
64
63
  if (!model || !space) {
@@ -76,35 +75,46 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
76
75
  return;
77
76
  }
78
77
 
79
- return new TablePresentation(model);
80
- }, [model]);
78
+ return new TablePresentation(registry, model);
79
+ }, [registry, model]);
81
80
 
82
81
  return { model, presentation };
83
82
  };
84
83
 
85
84
  const DefaultStory = () => {
86
- const client = useClient();
87
- const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
88
- const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
85
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(Organization.Organization, 50);
86
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(Person.Person, 50);
87
+ const { space } = useClientStory();
88
+
89
+ const handleCreate = useCallback(
90
+ (schema: Type.Entity.Any, values: any) => {
91
+ invariant(Type.isObjectSchema(schema));
92
+ invariant(space);
93
+ return space.db.add(Obj.make(schema, values));
94
+ },
95
+ [space],
96
+ );
89
97
 
90
98
  return (
91
99
  <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
92
100
  <TableComponent.Root>
93
101
  <TableComponent.Main
94
102
  model={orgModel}
95
- schema={DataType.Organization}
103
+ schema={Organization.Organization}
96
104
  presentation={orgPresentation}
97
- client={client}
105
+ onCreate={handleCreate}
98
106
  ignoreAttention
107
+ testId='relations-0'
99
108
  />
100
109
  </TableComponent.Root>
101
110
  <TableComponent.Root>
102
111
  <TableComponent.Main
103
112
  model={contactModel}
104
- schema={DataType.Person}
113
+ schema={Person.Person}
105
114
  presentation={contactPresentation}
106
- client={client}
115
+ onCreate={handleCreate}
107
116
  ignoreAttention
117
+ testId='relations-1'
108
118
  />
109
119
  </TableComponent.Root>
110
120
  </div>
@@ -114,20 +124,114 @@ const DefaultStory = () => {
114
124
  const meta = {
115
125
  title: 'ui/react-ui-table/Relations',
116
126
  render: DefaultStory,
117
- parameters: { translations, controls: { disable: true } },
118
127
  decorators: [
128
+ withTheme,
129
+ withRegistry,
130
+ // TODO(thure): Shouldn't `layout: 'fullscreen'` below make this unnecessary?
131
+ withLayout({ classNames: 'fixed inset-0' }),
119
132
  withClientProvider({
120
- types: [DataType.View, DataType.Organization, DataType.Person, Table.Table],
133
+ types: [View.View, Organization.Organization, Person.Person, Table.Table],
121
134
  createIdentity: true,
122
135
  createSpace: true,
123
136
  }),
124
- withTheme,
125
- withLayout({ fullscreen: true }),
126
137
  ],
138
+ parameters: {
139
+ layout: 'fullscreen',
140
+ controls: { disable: true },
141
+ translations: [...translations, ...formTranslations],
142
+ },
127
143
  } satisfies Meta<typeof DefaultStory>;
128
144
 
129
145
  export default meta;
130
146
 
131
147
  type Story = StoryObj<typeof meta>;
132
148
 
133
- export const Default: Story = {};
149
+ export const Default: Story = {
150
+ play: async ({ canvasElement }: any) => {
151
+ const canvas = within(canvasElement);
152
+ const body = within(document.body);
153
+
154
+ // Get all dx-grid elements (should have 2 - one for each table)
155
+ const firstGrid = await canvas.findByTestId('relations-0', undefined, { timeout: 30_000 });
156
+ const secondGrid = await canvas.findByTestId('relations-1', undefined, { timeout: 30_000 });
157
+
158
+ // Focus on the second table (Person/contacts table)
159
+ await expect(secondGrid).toBeVisible();
160
+
161
+ // Scroll to the relations column (column 4) - this mimics the scrollToColumn call
162
+ (secondGrid.closest('dx-grid') as DxGrid).scrollToColumn(4);
163
+
164
+ // Find and click the target cell (first row, relations column)
165
+ const targetCell = await within(secondGrid).findByTestId('grid.4.0', undefined, { timeout: 10_000 });
166
+ await expect(targetCell).toBeVisible();
167
+
168
+ // Click to focus the cell
169
+ await userEvent.click(targetCell as HTMLElement);
170
+ await expect(targetCell).toHaveFocus();
171
+ await userEvent.keyboard('{Enter}');
172
+
173
+ // Look for the combobox that should appear in edit mode
174
+ const combobox = await body.findByRole('combobox', undefined, { timeout: 5000 });
175
+ await userEvent.click(combobox);
176
+
177
+ const searchField = await body.findByPlaceholderText('Search…');
178
+ await userEvent.click(searchField);
179
+
180
+ // Get the first organization name from the first table to search for
181
+ const orgCell = await within(firstGrid).findByTestId('grid.0.0', undefined, { timeout: 10_000 });
182
+
183
+ const orgName = (orgCell.querySelector('.dx-grid__cell__content') as HTMLElement).textContent;
184
+ // Type the first 4 characters to search
185
+ await userEvent.type(searchField, orgName.substring(0, 4));
186
+
187
+ // Look for an option to select
188
+ const option = await body.findAllByRole('option');
189
+ await expect(option[0]).toBeVisible();
190
+
191
+ // Press Enter to select
192
+ await userEvent.keyboard('{Enter}');
193
+
194
+ // Look for and click save button
195
+ const saveButton = await body.findByTestId('save-button');
196
+ await userEvent.click(saveButton);
197
+
198
+ // Verify the relation was set (cell should now contain the org name)
199
+ await expect(targetCell).toHaveTextContent(orgName.substring(0, 4));
200
+
201
+ // Test object creation (new relations) - equivalent to "new relations work as expected" test
202
+ // Find a different cell to test object creation (second row, relations column)
203
+ const newTargetCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 10_000 });
204
+ await expect(newTargetCell).toBeTruthy();
205
+
206
+ // Click to focus the cell
207
+ await userEvent.click(newTargetCell as Element);
208
+
209
+ await userEvent.keyboard('{Enter}');
210
+
211
+ // Look for the combobox that should appear in edit mode
212
+ const newCombobox = await body.findByRole('combobox');
213
+ await userEvent.click(newCombobox);
214
+
215
+ const newSearchField = await body.findByPlaceholderText('Search…');
216
+ await userEvent.click(newSearchField);
217
+
218
+ // Type a new object name (this will create a new object)
219
+ const newOrgName = 'Salieri LLC';
220
+ await userEvent.type(newSearchField, newOrgName);
221
+
222
+ // Wait for the create option to appear (debounce is 200ms, allow time for render)
223
+ const createOption = await body.findByRole('option', undefined, { timeout: 500 });
224
+ await expect(createOption).toBeVisible();
225
+
226
+ // Press Enter to select/create
227
+ await userEvent.keyboard('{Enter}');
228
+
229
+ // Look for and click save button
230
+ const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form');
231
+ const saveObjectButton = await within(createReferencedObjectForm).findByTestId('save-button');
232
+ await userEvent.click(saveObjectButton);
233
+
234
+ // Verify the new object was created and relation was set
235
+ await expect(newTargetCell).toHaveTextContent(newOrgName);
236
+ },
237
+ } as any;
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { useAtomValue } from '@effect-atom/atom-react';
5
6
  import React from 'react';
6
7
 
7
8
  import { DropdownMenu, toLocalizedString, useTranslation } from '@dxos/react-ui';
@@ -13,8 +14,8 @@ type RowActionsMenuProps = { model: TableModel; modals: ModalController };
13
14
 
14
15
  export const RowActionsMenu = ({ model, modals }: RowActionsMenuProps) => {
15
16
  const { t } = useTranslation(translationKey);
16
- const hasSelection = model.selection.hasSelection.value;
17
- const state = modals.state.value;
17
+ const hasSelection = model.selection.hasSelection;
18
+ const state = useAtomValue(modals.state);
18
19
  if (state?.type !== 'row') {
19
20
  return null;
20
21
  }
@@ -23,24 +24,32 @@ export const RowActionsMenu = ({ model, modals }: RowActionsMenuProps) => {
23
24
  <DropdownMenu.VirtualTrigger virtualRef={modals.trigger} />
24
25
  <DropdownMenu.Content>
25
26
  <DropdownMenu.Viewport>
27
+ {/* Custom actions */}
28
+ {model.rowActions?.length > 0 && (
29
+ <>
30
+ <DropdownMenu.Group>
31
+ {model.rowActions?.map((action) => (
32
+ <DropdownMenu.Item
33
+ key={action.id}
34
+ data-testid={`row-action-${action.id}`}
35
+ onClick={() => {
36
+ modals.close();
37
+ model.handleRowAction(action.id, state.rowIndex);
38
+ }}
39
+ >
40
+ {toLocalizedString(action.label, t)}
41
+ </DropdownMenu.Item>
42
+ ))}
43
+ </DropdownMenu.Group>
44
+ <DropdownMenu.Separator />
45
+ </>
46
+ )}
47
+ {/* Default actions */}
26
48
  {model.features.dataEditable !== false && (
27
49
  <DropdownMenu.Item data-testid='row-menu-delete' onClick={() => model.deleteRow(state.rowIndex)}>
28
50
  {t(hasSelection ? 'bulk delete row label' : 'delete row label')}
29
51
  </DropdownMenu.Item>
30
52
  )}
31
- {/* Custom actions */}
32
- {model.rowActions?.map((action) => (
33
- <DropdownMenu.Item
34
- key={action.id}
35
- data-testid={`row-action-${action.id}`}
36
- onClick={() => {
37
- modals.close();
38
- model.handleRowAction(action.id, state.rowIndex);
39
- }}
40
- >
41
- {toLocalizedString(action.label, t)}
42
- </DropdownMenu.Item>
43
- ))}
44
53
  </DropdownMenu.Viewport>
45
54
  <DropdownMenu.Arrow />
46
55
  </DropdownMenu.Content>
@@ -2,31 +2,22 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
9
7
  import React, { useCallback } from 'react';
10
8
 
11
- import { Filter, Obj, Query, Type } from '@dxos/echo';
12
- import {
13
- EchoObject,
14
- FormatAnnotation,
15
- FormatEnum,
16
- GeneratorAnnotation,
17
- LabelAnnotation,
18
- PropertyMetaAnnotationId,
19
- } from '@dxos/echo-schema';
9
+ import { Annotation, type Database, Format, Obj, type QueryAST, Type } from '@dxos/echo';
10
+ import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
20
11
  import { invariant } from '@dxos/invariant';
21
12
  import { faker } from '@dxos/random';
22
13
  import { PublicKey } from '@dxos/react-client';
23
- import { type Space, live } from '@dxos/react-client/echo';
24
14
  import { withClientProvider } from '@dxos/react-client/testing';
25
- import { ViewEditor } from '@dxos/react-ui-form';
15
+ import { withTheme } from '@dxos/react-ui/testing';
16
+ import { ViewEditor, translations as formTranslations } from '@dxos/react-ui-form';
26
17
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
27
- import { DataType, getSchemaFromPropertyDefinitions } from '@dxos/schema';
28
- import { Testing, createObjectFactory } from '@dxos/schema/testing';
29
- import { withLayout, withTheme } from '@dxos/storybook-utils';
18
+ import { View, getSchemaFromPropertyDefinitions, getTypenameFromQuery } from '@dxos/schema';
19
+ import { TestSchema, createObjectFactory } from '@dxos/schema/testing';
20
+ import { withRegistry } from '@dxos/storybook-utils';
30
21
 
31
22
  import { useTestTableModel } from '../../testing';
32
23
  import { translations } from '../../translations';
@@ -35,13 +26,13 @@ import { TableToolbar } from '../TableToolbar';
35
26
 
36
27
  import { Table as TableComponent } from './Table';
37
28
 
38
- const TestSchema = Schema.Struct({
29
+ const Example = Schema.Struct({
39
30
  // TODO(wittjosiah): Should be title. Currently name to work with default label.
40
31
  name: Schema.optional(Schema.String).annotations({ title: 'Title' }),
41
32
  urgent: Schema.optional(Schema.Boolean).annotations({ title: 'Urgent' }),
42
33
  status: Schema.optional(
43
34
  Schema.Literal('todo', 'in-progress', 'done')
44
- .pipe(FormatAnnotation.set(FormatEnum.SingleSelect))
35
+ .pipe(Format.FormatAnnotation.set(Format.TypeFormat.SingleSelect))
45
36
  .annotations({
46
37
  title: 'Status',
47
38
  [PropertyMetaAnnotationId]: {
@@ -56,33 +47,35 @@ const TestSchema = Schema.Struct({
56
47
  }),
57
48
  ),
58
49
  description: Schema.optional(Schema.String).annotations({ title: 'Description' }),
59
- parent: Schema.optional(Schema.suspend((): Type.Ref<TestSchema> => Type.Ref(TestSchema))).annotations({
50
+ parent: Schema.optional(Schema.suspend((): Type.Ref<Example> => Type.Ref(Example))).annotations({
60
51
  title: 'Parent',
61
52
  }),
62
53
  }).pipe(
63
- Type.Obj({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
64
- LabelAnnotation.set(['name']),
54
+ Type.object({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
55
+ Annotation.LabelAnnotation.set(['name']),
65
56
  );
66
- interface TestSchema extends Schema.Schema.Type<typeof TestSchema> {}
57
+ interface Example extends Schema.Schema.Type<typeof Example> {}
67
58
 
68
59
  const StoryViewEditor = ({
69
60
  view,
70
61
  schema,
71
- space,
62
+ db,
72
63
  handleDeleteColumn,
73
64
  }: {
74
- view?: DataType.View;
65
+ view?: View.View;
75
66
  schema?: Schema.Schema.AnyNoContext;
76
- space?: Space;
67
+ db?: Database.Database;
77
68
  handleDeleteColumn: (fieldId: string) => void;
78
69
  }) => {
79
70
  const handleQueryChanged = useCallback(
80
- (typename: string) => {
71
+ (newQuery: QueryAST.Query) => {
81
72
  invariant(schema);
82
73
  invariant(Type.isMutable(schema));
83
- schema.updateTypename(typename);
74
+ schema.updateTypename(getTypenameFromQuery(newQuery));
84
75
  invariant(view);
85
- view.query = Query.select(Filter.typename(typename)).ast;
76
+ Obj.change(view, (v) => {
77
+ v.query.ast = newQuery as Mutable<typeof newQuery>;
78
+ });
86
79
  },
87
80
  [schema, view],
88
81
  );
@@ -93,7 +86,7 @@ const StoryViewEditor = ({
93
86
 
94
87
  return (
95
88
  <ViewEditor
96
- registry={space?.db.schemaRegistry}
89
+ registry={db?.schemaRegistry}
97
90
  schema={schema}
98
91
  view={view}
99
92
  onQueryChanged={handleQueryChanged}
@@ -107,20 +100,10 @@ const StoryViewEditor = ({
107
100
  //
108
101
 
109
102
  const DefaultStory = () => {
110
- const {
111
- space,
112
- schema,
113
- view,
114
- tableRef,
115
- model,
116
- presentation,
117
- client,
118
- handleInsertRow,
119
- handleSaveView,
120
- handleDeleteColumn,
121
- } = useTestTableModel();
103
+ const { db, schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView, handleDeleteColumn } =
104
+ useTestTableModel();
122
105
 
123
- if (!schema || !view) {
106
+ if (!schema || !table?.view.target) {
124
107
  return <div />;
125
108
  }
126
109
 
@@ -131,18 +114,17 @@ const DefaultStory = () => {
131
114
  <TableComponent.Root>
132
115
  <TableComponent.Main
133
116
  ref={tableRef}
117
+ schema={schema}
134
118
  model={model}
135
119
  presentation={presentation}
136
- schema={schema}
137
- client={client}
138
120
  ignoreAttention
139
121
  />
140
122
  </TableComponent.Root>
141
123
  </div>
142
- <div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
143
- <StoryViewEditor view={view} schema={schema} space={space} handleDeleteColumn={handleDeleteColumn} />
124
+ <div className='flex flex-col bs-full border-l border-separator overflow-y-auto'>
125
+ <StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
144
126
  <SyntaxHighlighter language='json' className='text-xs'>
145
- {JSON.stringify({ view, schema }, null, 2)}
127
+ {JSON.stringify({ view: table.view.target, schema }, null, 2)}
146
128
  </SyntaxHighlighter>
147
129
  </div>
148
130
  </div>
@@ -160,29 +142,25 @@ type StoryProps = { rows?: number };
160
142
  const meta = {
161
143
  title: 'ui/react-ui-table/Table',
162
144
  render: DefaultStory,
163
- parameters: {
164
- translations,
165
- layout: 'fullscreen',
166
- controls: {
167
- disable: true,
168
- },
169
- },
170
145
  decorators: [
171
146
  withTheme,
172
- withLayout({ fullscreen: true }),
147
+ withRegistry,
173
148
  withClientProvider({
174
- types: [DataType.View, Table.Table],
149
+ types: [View.View, Table.Table],
175
150
  createIdentity: true,
176
151
  createSpace: true,
177
- onSpaceCreated: async ({ client, space }) => {
178
- const [schema] = await space.db.schemaRegistry.register([TestSchema]);
179
- const { view } = await Table.makeView({ client, space, typename: schema.typename });
180
- view.projection.fields = [
181
- view.projection.fields.find((field) => field.path === 'name')!,
182
- ...view.projection.fields.filter((field) => field.path !== 'name'),
183
- ];
152
+ onCreateSpace: async ({ space }) => {
153
+ const [schema] = await space.db.schemaRegistry.register([Example]);
154
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: schema.typename });
155
+ const table = Table.make({ view, jsonSchema });
156
+ Obj.change(view, (v) => {
157
+ v.projection.fields = [
158
+ v.projection.fields.find((field) => field.path === 'name')!,
159
+ ...v.projection.fields.filter((field) => field.path !== 'name'),
160
+ ];
161
+ });
184
162
 
185
- space.db.add(view);
163
+ space.db.add(table);
186
164
 
187
165
  Array.from({ length: 10 }).map(() => {
188
166
  return space.db.add(
@@ -196,6 +174,13 @@ const meta = {
196
174
  },
197
175
  }),
198
176
  ],
177
+ parameters: {
178
+ layout: 'fullscreen',
179
+ controls: {
180
+ disable: true,
181
+ },
182
+ translations: [...translations, ...formTranslations],
183
+ },
199
184
  } satisfies Meta<typeof TableComponent>;
200
185
 
201
186
  export default meta;
@@ -206,30 +191,35 @@ export const Default: Story = {};
206
191
 
207
192
  export const StaticSchema: StoryObj = {
208
193
  render: DefaultStory,
209
- parameters: { translations },
210
194
  decorators: [
211
195
  withClientProvider({
212
- types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization],
196
+ types: [View.View, Table.Table],
213
197
  createIdentity: true,
214
198
  createSpace: true,
215
- onSpaceCreated: async ({ client, space }) => {
216
- const { view } = await Table.makeView({ client, space, typename: Testing.Contact.typename });
217
- space.db.add(view);
199
+ onCreateSpace: async ({ space }) => {
200
+ const { view, jsonSchema } = await View.makeFromDatabase({
201
+ db: space.db,
202
+ typename: TestSchema.Person.typename,
203
+ });
204
+ const table = Table.make({ view, jsonSchema });
205
+ space.db.add(table);
218
206
 
219
207
  const factory = createObjectFactory(space.db, faker as any);
220
208
  await factory([
221
- { type: Testing.Contact, count: 10 },
222
- // { type: Testing.Organization, count: 1 },
209
+ { type: TestSchema.Person, count: 10 },
210
+ // { type: TestSchema.Organization, count: 1 },
223
211
  ]);
224
212
  },
225
213
  }),
226
- withLayout({ fullscreen: true }),
227
- withTheme,
228
214
  ],
215
+ parameters: {
216
+ layout: 'fullscreen',
217
+ translations: [...translations, ...formTranslations],
218
+ },
229
219
  };
230
220
 
231
221
  const ContactWithArrayOfEmails = Schema.Struct({
232
- name: Schema.String.pipe(GeneratorAnnotation.set('person.fullName')),
222
+ name: Schema.String.pipe(Annotation.GeneratorAnnotation.set('person.fullName')),
233
223
  emails: Schema.optional(
234
224
  Schema.Array(
235
225
  Schema.Struct({
@@ -239,7 +229,7 @@ const ContactWithArrayOfEmails = Schema.Struct({
239
229
  ),
240
230
  ),
241
231
  }).pipe(
242
- EchoObject({
232
+ Type.object({
243
233
  typename: 'dxos.org/type/ContactWithArrayOfEmails',
244
234
  version: '0.1.0',
245
235
  }),
@@ -247,39 +237,43 @@ const ContactWithArrayOfEmails = Schema.Struct({
247
237
 
248
238
  export const ArrayOfObjects: StoryObj = {
249
239
  render: DefaultStory,
250
- parameters: { translations },
251
240
  decorators: [
252
241
  withClientProvider({
253
- types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
242
+ types: [View.View, Table.Table, TestSchema.Person, TestSchema.Organization, ContactWithArrayOfEmails],
254
243
  createIdentity: true,
255
244
  createSpace: true,
256
- onSpaceCreated: async ({ client, space }) => {
257
- const { view } = await Table.makeView({ client, space, typename: ContactWithArrayOfEmails.typename });
258
- space.db.add(view);
245
+ onCreateSpace: async ({ space }) => {
246
+ const { view, jsonSchema } = await View.makeFromDatabase({
247
+ db: space.db,
248
+ typename: ContactWithArrayOfEmails.typename,
249
+ });
250
+ const table = Table.make({ view, jsonSchema });
251
+ space.db.add(table);
259
252
 
260
253
  const factory = createObjectFactory(space.db, faker as any);
261
254
  await factory([
262
- // { type: Testing.Contact, count: 10 },
263
- // { type: Testing.Organization, count: 1 },
255
+ // { type: TestSchema.Person, count: 10 },
256
+ // { type: TestSchema.Organization, count: 1 },
264
257
  { type: ContactWithArrayOfEmails, count: 10 },
265
258
  ]);
266
259
  },
267
260
  }),
268
- withLayout({ fullscreen: true }),
269
- withTheme,
270
261
  ],
262
+ parameters: {
263
+ layout: 'fullscreen',
264
+ translations,
265
+ },
271
266
  };
272
267
 
273
268
  export const Tags: Meta<StoryProps> = {
274
269
  title: 'ui/react-ui-table/Table',
275
270
  render: DefaultStory,
276
- parameters: { translations },
277
271
  decorators: [
278
272
  withClientProvider({
279
- types: [DataType.View, Table.Table],
273
+ types: [View.View, Table.Table],
280
274
  createIdentity: true,
281
275
  createSpace: true,
282
- onSpaceCreated: async ({ client, space }) => {
276
+ onCreateSpace: async ({ space }) => {
283
277
  // Configure schema.
284
278
  const typename = 'example.com/SingleSelect';
285
279
  const selectOptions = [
@@ -295,25 +289,26 @@ export const Tags: Meta<StoryProps> = {
295
289
  const schema = getSchemaFromPropertyDefinitions(typename, [
296
290
  {
297
291
  name: 'single',
298
- format: FormatEnum.SingleSelect,
292
+ format: Format.TypeFormat.SingleSelect,
299
293
  config: { options: selectOptions },
300
294
  },
301
295
  {
302
296
  name: 'multiple',
303
- format: FormatEnum.MultiSelect,
297
+ format: Format.TypeFormat.MultiSelect,
304
298
  config: { options: selectOptions },
305
299
  },
306
300
  ]);
307
301
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
308
302
 
309
303
  // Initialize table.
310
- const { view } = await Table.makeView({ client, space, typename });
311
- space.db.add(view);
304
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename });
305
+ const table = Table.make({ view, jsonSchema });
306
+ space.db.add(table);
312
307
 
313
308
  // Populate.
314
309
  Array.from({ length: 10 }).map(() => {
315
310
  return space.db.add(
316
- live(storedSchema, {
311
+ Obj.make(storedSchema, {
317
312
  single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
318
313
  multiple: faker.helpers.randomSubset(selectOptionIds),
319
314
  }),
@@ -321,7 +316,9 @@ export const Tags: Meta<StoryProps> = {
321
316
  });
322
317
  },
323
318
  }),
324
- withLayout({ fullscreen: true }),
325
- withTheme,
326
319
  ],
320
+ parameters: {
321
+ layout: 'fullscreen',
322
+ translations,
323
+ },
327
324
  };