@dxos/react-ui-table 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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