@dxos/react-ui-table 0.8.4-main.ae835ea → 0.8.4-main.bcb3aa67d6

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 (146) hide show
  1. package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-G7MGPBSR.mjs → chunk-EK6D2CVM.mjs} +656 -681
  4. package/dist/lib/browser/chunk-EK6D2CVM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1193 -1034
  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-LD5JEBA6.mjs → chunk-LYYDF6JP.mjs} +656 -680
  12. package/dist/lib/node-esm/chunk-LYYDF6JP.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs +71 -0
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1193 -1034
  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 +21 -37
  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 +82 -81
  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 +28 -21
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +90 -26
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/TableContent.d.ts +21 -0
  37. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  38. package/dist/types/src/components/Table/TableToolbar.d.ts +25 -0
  39. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +24 -26
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/index.d.ts +0 -1
  48. package/dist/types/src/components/index.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +22 -15
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -1
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +105 -25
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +3 -1
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +21 -23
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +26 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/util/dynamic-table.d.ts +14 -11
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -0
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/sort.d.ts +12 -0
  86. package/dist/types/src/util/sort.d.ts.map +1 -0
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +55 -55
  89. package/src/components/Table/ColumnActionsMenu.tsx +15 -11
  90. package/src/components/Table/ColumnSettings.tsx +8 -9
  91. package/src/components/Table/DynamicTable.stories.tsx +16 -16
  92. package/src/components/Table/DynamicTable.tsx +32 -31
  93. package/src/components/Table/Relations.stories.tsx +64 -58
  94. package/src/components/Table/RowActionsMenu.tsx +24 -15
  95. package/src/components/Table/Table.stories.tsx +115 -98
  96. package/src/components/Table/Table.tsx +43 -406
  97. package/src/components/Table/TableContent.tsx +422 -0
  98. package/src/components/Table/TableToolbar.tsx +111 -0
  99. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
  100. package/src/components/TableCellEditor/FormCellEditor.tsx +117 -134
  101. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +20 -15
  102. package/src/components/TableCellEditor/TableCellEditor.tsx +37 -32
  103. package/src/components/index.ts +0 -1
  104. package/src/components/testing.ts +38 -12
  105. package/src/hooks/index.ts +2 -1
  106. package/src/hooks/useAddRow.ts +8 -10
  107. package/src/hooks/useProjectionModel.ts +39 -0
  108. package/src/hooks/useTableModel.ts +36 -34
  109. package/src/model/modal-controller.ts +23 -12
  110. package/src/model/selection-model.ts +75 -44
  111. package/src/model/table-model.test.ts +82 -54
  112. package/src/model/table-model.ts +436 -170
  113. package/src/model/table-presentation.test.ts +59 -49
  114. package/src/model/table-presentation.ts +43 -37
  115. package/src/playwright/TableManager.ts +13 -6
  116. package/src/playwright/playwright.config.ts +1 -1
  117. package/src/playwright/smoke.spec.ts +53 -7
  118. package/src/testing/useTestTableModel.ts +45 -38
  119. package/src/translations.ts +20 -22
  120. package/src/types/Table.ts +79 -0
  121. package/src/types/index.ts +1 -1
  122. package/src/util/dynamic-table.test.ts +70 -0
  123. package/src/util/dynamic-table.ts +41 -28
  124. package/src/util/index.ts +2 -0
  125. package/src/util/query-ast.ts +22 -0
  126. package/src/util/sort.ts +90 -0
  127. package/src/util/table-buttons.ts +1 -1
  128. package/src/util/table-controls.ts +2 -2
  129. package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
  130. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
  131. package/dist/lib/browser/chunk-G7MGPBSR.mjs.map +0 -7
  132. package/dist/lib/node-esm/chunk-LD5JEBA6.mjs.map +0 -7
  133. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
  134. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
  135. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  136. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  137. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  138. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  139. package/dist/types/src/model/table-sorting.d.ts +0 -52
  140. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  141. package/dist/types/src/types/schema.d.ts +0 -28
  142. package/dist/types/src/types/schema.d.ts.map +0 -1
  143. package/src/components/TableToolbar/TableToolbar.tsx +0 -105
  144. package/src/components/TableToolbar/index.ts +0 -5
  145. package/src/model/table-sorting.ts +0 -225
  146. package/src/types/schema.ts +0 -73
@@ -2,25 +2,28 @@
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';
8
- import { expect, userEvent, within } from 'storybook/test';
7
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
8
+ import { expect, userEvent, waitFor, 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
- import '@dxos/lit-ui/dx-tag-picker.pcss';
14
14
  import { faker } from '@dxos/random';
15
- import { useClient } from '@dxos/react-client';
16
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
15
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
17
16
  import { useAsyncEffect } from '@dxos/react-ui';
18
17
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
19
18
  import { translations as formTranslations } from '@dxos/react-ui-form';
20
- import { DataType } from '@dxos/schema';
19
+ import { ViewModel } from '@dxos/schema';
21
20
  import { type ValueGenerator, createAsyncGenerator } from '@dxos/schema/testing';
21
+ import { withRegistry } from '@dxos/storybook-utils';
22
+ import { Organization, Person } from '@dxos/types';
23
+
24
+ import '@dxos/lit-ui/dx-tag-picker.pcss';
22
25
 
23
- import { useTableModel } from '../../hooks';
26
+ import { useProjectionModel, useTableModel } from '../../hooks';
24
27
  import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
25
28
  import { translations } from '../../translations';
26
29
  import { Table } from '../../types';
@@ -34,11 +37,10 @@ const generator: ValueGenerator = faker as any;
34
37
  // TODO(burdon): Mutable and immutable views.
35
38
  // TODO(burdon): Reconcile schemas types and utils (see API PR).
36
39
  // 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>();
40
+ const useTestModel = <S extends Type.AnyObj>(schema: S, count: number) => {
41
+ const registry = useContext(RegistryContext);
42
+ const { space } = useClientStory();
43
+ const [object, setObject] = useState<Table.Table>();
42
44
 
43
45
  const features = useMemo<TableFeatures>(
44
46
  () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
@@ -50,18 +52,14 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
50
52
  return;
51
53
  }
52
54
 
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]);
55
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename: Type.getTypename(schema) });
56
+ const object = Table.make({ view, jsonSchema });
57
+ setObject(object);
58
+ space.db.add(object);
59
+ }, [space, schema]);
63
60
 
64
- const model = useTableModel<TableRow>({ view, schema: jsonSchema, rows: [], features });
61
+ const projection = useProjectionModel(schema, object, registry);
62
+ const model = useTableModel<TableRow>({ object, projection, db: space?.db, features });
65
63
 
66
64
  useEffect(() => {
67
65
  if (!model || !space) {
@@ -79,45 +77,44 @@ const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
79
77
  return;
80
78
  }
81
79
 
82
- return new TablePresentation(model);
83
- }, [model]);
80
+ return new TablePresentation(registry, model);
81
+ }, [registry, model]);
84
82
 
85
83
  return { model, presentation };
86
84
  };
87
85
 
88
86
  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();
87
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(Organization.Organization, 50);
88
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(Person.Person, 50);
89
+ const { space } = useClientStory();
93
90
 
94
91
  const handleCreate = useCallback(
95
- (schema: Schema.Schema.AnyNoContext, values: any) => {
96
- return client.spaces.default.db.add(Obj.make(schema, values));
92
+ (schema: Type.AnyEntity, values: any) => {
93
+ invariant(Type.isObjectSchema(schema));
94
+ invariant(space);
95
+ return space.db.add(Obj.make(schema, values));
97
96
  },
98
97
  [space],
99
98
  );
100
99
 
101
100
  return (
102
- <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
101
+ <div className='dx-expander grid grid-cols-2 divide-x divide-separator'>
103
102
  <TableComponent.Root>
104
- <TableComponent.Main
103
+ <TableComponent.Content
105
104
  model={orgModel}
106
- schema={DataType.Organization}
105
+ schema={Organization.Organization}
107
106
  presentation={orgPresentation}
108
107
  onCreate={handleCreate}
109
- client={client}
110
108
  ignoreAttention
111
109
  testId='relations-0'
112
110
  />
113
111
  </TableComponent.Root>
114
112
  <TableComponent.Root>
115
- <TableComponent.Main
113
+ <TableComponent.Content
116
114
  model={contactModel}
117
- schema={DataType.Person}
115
+ schema={Person.Person}
118
116
  presentation={contactPresentation}
119
117
  onCreate={handleCreate}
120
- client={client}
121
118
  ignoreAttention
122
119
  testId='relations-1'
123
120
  />
@@ -130,11 +127,12 @@ const meta = {
130
127
  title: 'ui/react-ui-table/Relations',
131
128
  render: DefaultStory,
132
129
  decorators: [
133
- withTheme,
134
- // TODO(thure): Shouldn’t `layout: 'fullscreen'` below make this unnecessary?
130
+ withTheme(),
131
+ withRegistry,
132
+ // TODO(thure): Shouldn't `layout: 'fullscreen'` below make this unnecessary?
135
133
  withLayout({ classNames: 'fixed inset-0' }),
136
134
  withClientProvider({
137
- types: [DataType.View, DataType.Organization, DataType.Person, Table.Table],
135
+ types: [View.View, Organization.Organization, Person.Person, Table.Table],
138
136
  createIdentity: true,
139
137
  createSpace: true,
140
138
  }),
@@ -151,7 +149,7 @@ export default meta;
151
149
  type Story = StoryObj<typeof meta>;
152
150
 
153
151
  export const Default: Story = {
154
- play: async ({ canvasElement }: any) => {
152
+ play: async ({ canvasElement }) => {
155
153
  const canvas = within(canvasElement);
156
154
  const body = within(document.body);
157
155
 
@@ -199,8 +197,15 @@ export const Default: Story = {
199
197
  const saveButton = await body.findByTestId('save-button');
200
198
  await userEvent.click(saveButton);
201
199
 
202
- // Verify the relation was set (cell should now contain the org name)
203
- await expect(targetCell).toHaveTextContent(orgName.substring(0, 4));
200
+ // Verify the relation was set (cell should now contain the org name).
201
+ // Wait for the grid (Lit web component) to repaint after the reactive data update.
202
+ await waitFor(
203
+ async () => {
204
+ const updatedCell = within(secondGrid).getByTestId('grid.4.0');
205
+ await expect(updatedCell).toHaveTextContent(orgName.substring(0, 4));
206
+ },
207
+ { timeout: 5000 },
208
+ );
204
209
 
205
210
  // Test object creation (new relations) - equivalent to "new relations work as expected" test
206
211
  // Find a different cell to test object creation (second row, relations column)
@@ -219,23 +224,24 @@ export const Default: Story = {
219
224
  const newSearchField = await body.findByPlaceholderText('Search…');
220
225
  await userEvent.click(newSearchField);
221
226
 
222
- // Type a new object name (this will create a new object)
227
+ // Type a new object name (this will create a new object).
223
228
  const newOrgName = 'Salieri LLC';
224
229
  await userEvent.type(newSearchField, newOrgName);
225
230
 
226
- // Look for an option to select (should be the create new option)
227
- const newOption = await body.findAllByRole('option');
228
- await expect(newOption[0]).toBeVisible();
229
-
230
- // Press Enter to select/create
231
- await userEvent.keyboard('{Enter}');
231
+ // Click the create option directly to open the create form.
232
+ const createOptionLabel = await body.findByText(/Create new object/, undefined, { timeout: 2000 });
233
+ await userEvent.click(createOptionLabel.closest('[role="option"]') as HTMLElement);
232
234
 
233
- // Look for and click save button
234
- const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form');
235
+ // Look for and click save button.
236
+ const createReferencedObjectForm = await body.findByTestId('create-referenced-object-form', undefined, {
237
+ timeout: 2000,
238
+ });
235
239
  const saveObjectButton = await within(createReferencedObjectForm).findByTestId('save-button');
240
+ await expect(saveObjectButton).not.toBeDisabled();
236
241
  await userEvent.click(saveObjectButton);
237
242
 
238
- // Verify the new object was created and relation was set
239
- await expect(newTargetCell).toHaveTextContent(newOrgName);
243
+ // Verify the relation was set by checking for the accessory link anchor in the cell.
244
+ const updatedNewCell = await within(secondGrid).findByTestId('grid.4.1', undefined, { timeout: 5000 });
245
+ await expect(updatedNewCell.querySelector('dx-anchor')).toBeTruthy();
240
246
  },
241
- } as any;
247
+ };
@@ -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 { Obj, type QueryAST, 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, getTypenameFromQuery } 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,24 +48,24 @@ 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: `com.example.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(
@@ -81,7 +74,9 @@ const StoryViewEditor = ({
81
74
  invariant(Type.isMutable(schema));
82
75
  schema.updateTypename(getTypenameFromQuery(newQuery));
83
76
  invariant(view);
84
- view.query.ast = newQuery;
77
+ Obj.change(view, (obj) => {
78
+ obj.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,74 +101,87 @@ 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 ref={tableRef}>
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.Content
133
+ schema={schema}
134
+ model={model}
135
+ presentation={presentation}
136
+ onRowClick={handleRowClick}
137
+ ignoreAttention
138
+ />
139
+ </Panel.Content>
140
+ </Panel.Root>
141
+ </TableComponent.Root>
142
+ <ScrollArea.Root orientation='vertical' classNames='border-l border-separator'>
143
+ <ScrollArea.Viewport>
144
+ <StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
145
+ <SyntaxHighlighter language='json' className='text-xs'>
146
+ {JSON.stringify({ view: table.view.target, schema }, null, 2)}
147
+ </SyntaxHighlighter>
148
+ </ScrollArea.Viewport>
149
+ </ScrollArea.Root>
147
150
  </div>
148
151
  );
149
152
  };
150
153
 
151
- type StoryProps = { rows?: number };
154
+ type DefaultStoryProps = { rows?: number };
152
155
 
153
156
  //
154
157
  // Story definitions.
155
158
  //
156
159
 
157
- // TODO(burdon): Need super simple story.
160
+ // TODO(burdon): Need simpler story.
158
161
 
159
162
  const meta = {
160
163
  title: 'ui/react-ui-table/Table',
161
164
  render: DefaultStory,
162
165
  decorators: [
163
- withTheme,
166
+ withTheme(),
167
+ withLayout({ layout: 'fullscreen' }),
168
+ withRegistry,
164
169
  withClientProvider({
165
- types: [DataType.View, Table.Table],
170
+ types: [View.View, Table.Table],
166
171
  createIdentity: true,
167
172
  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
- ];
173
+ onCreateSpace: async ({ space }) => {
174
+ const [schema] = await space.db.schemaRegistry.register([Example]);
175
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename: schema.typename });
176
+ const table = Table.make({ view, jsonSchema });
177
+ Obj.change(view, (obj) => {
178
+ obj.projection.fields = [
179
+ obj.projection.fields.find((field) => field.path === 'name')!,
180
+ ...obj.projection.fields.filter((field) => field.path !== 'name'),
181
+ ];
182
+ });
175
183
 
176
- space.db.add(view);
184
+ space.db.add(table);
177
185
 
178
186
  Array.from({ length: 10 }).map(() => {
179
187
  return space.db.add(
@@ -192,7 +200,7 @@ const meta = {
192
200
  controls: {
193
201
  disable: true,
194
202
  },
195
- translations,
203
+ translations: [...translations, ...formTranslations],
196
204
  },
197
205
  } satisfies Meta<typeof TableComponent>;
198
206
 
@@ -206,29 +214,33 @@ export const StaticSchema: StoryObj = {
206
214
  render: DefaultStory,
207
215
  decorators: [
208
216
  withClientProvider({
209
- types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization],
217
+ types: [View.View, Table.Table],
210
218
  createIdentity: true,
211
219
  createSpace: true,
212
- onCreateSpace: async ({ client, space }) => {
213
- const { view } = await Table.makeView({ client, space, typename: Testing.Contact.typename });
214
- space.db.add(view);
220
+ onCreateSpace: async ({ space }) => {
221
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({
222
+ db: space.db,
223
+ typename: TestSchema.Person.typename,
224
+ });
225
+ const table = Table.make({ view, jsonSchema });
226
+ space.db.add(table);
215
227
 
216
228
  const factory = createObjectFactory(space.db, faker as any);
217
229
  await factory([
218
- { type: Testing.Contact, count: 10 },
219
- // { type: Testing.Organization, count: 1 },
230
+ { type: TestSchema.Person, count: 10 },
231
+ // { type: TestSchema.Organization, count: 1 },
220
232
  ]);
221
233
  },
222
234
  }),
223
235
  ],
224
236
  parameters: {
225
237
  layout: 'fullscreen',
226
- translations,
238
+ translations: [...translations, ...formTranslations],
227
239
  },
228
240
  };
229
241
 
230
242
  const ContactWithArrayOfEmails = Schema.Struct({
231
- name: Schema.String.pipe(GeneratorAnnotation.set('person.fullName')),
243
+ name: Schema.String.pipe(Annotation.GeneratorAnnotation.set('person.fullName')),
232
244
  emails: Schema.optional(
233
245
  Schema.Array(
234
246
  Schema.Struct({
@@ -238,8 +250,8 @@ const ContactWithArrayOfEmails = Schema.Struct({
238
250
  ),
239
251
  ),
240
252
  }).pipe(
241
- EchoObject({
242
- typename: 'dxos.org/type/ContactWithArrayOfEmails',
253
+ Type.object({
254
+ typename: 'org.dxos.type.contactWithArrayOfEmails',
243
255
  version: '0.1.0',
244
256
  }),
245
257
  );
@@ -248,17 +260,21 @@ export const ArrayOfObjects: StoryObj = {
248
260
  render: DefaultStory,
249
261
  decorators: [
250
262
  withClientProvider({
251
- types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
263
+ types: [View.View, Table.Table, TestSchema.Person, TestSchema.Organization, ContactWithArrayOfEmails],
252
264
  createIdentity: true,
253
265
  createSpace: true,
254
- onCreateSpace: async ({ client, space }) => {
255
- const { view } = await Table.makeView({ client, space, typename: ContactWithArrayOfEmails.typename });
256
- space.db.add(view);
266
+ onCreateSpace: async ({ space }) => {
267
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({
268
+ db: space.db,
269
+ typename: ContactWithArrayOfEmails.typename,
270
+ });
271
+ const table = Table.make({ view, jsonSchema });
272
+ space.db.add(table);
257
273
 
258
274
  const factory = createObjectFactory(space.db, faker as any);
259
275
  await factory([
260
- // { type: Testing.Contact, count: 10 },
261
- // { type: Testing.Organization, count: 1 },
276
+ // { type: TestSchema.Person, count: 10 },
277
+ // { type: TestSchema.Organization, count: 1 },
262
278
  { type: ContactWithArrayOfEmails, count: 10 },
263
279
  ]);
264
280
  },
@@ -270,17 +286,17 @@ export const ArrayOfObjects: StoryObj = {
270
286
  },
271
287
  };
272
288
 
273
- export const Tags: Meta<StoryProps> = {
289
+ export const Tags: Meta<DefaultStoryProps> = {
274
290
  title: 'ui/react-ui-table/Table',
275
291
  render: DefaultStory,
276
292
  decorators: [
277
293
  withClientProvider({
278
- types: [DataType.View, Table.Table],
294
+ types: [View.View, Table.Table],
279
295
  createIdentity: true,
280
296
  createSpace: true,
281
- onCreateSpace: async ({ client, space }) => {
297
+ onCreateSpace: async ({ space }) => {
282
298
  // Configure schema.
283
- const typename = 'example.com/SingleSelect';
299
+ const typename = 'com.example.type.single-select';
284
300
  const selectOptions = [
285
301
  { id: 'one', title: 'One', color: 'emerald' },
286
302
  { id: 'two', title: 'Two', color: 'blue' },
@@ -294,25 +310,26 @@ export const Tags: Meta<StoryProps> = {
294
310
  const schema = getSchemaFromPropertyDefinitions(typename, [
295
311
  {
296
312
  name: 'single',
297
- format: FormatEnum.SingleSelect,
313
+ format: Format.TypeFormat.SingleSelect,
298
314
  config: { options: selectOptions },
299
315
  },
300
316
  {
301
317
  name: 'multiple',
302
- format: FormatEnum.MultiSelect,
318
+ format: Format.TypeFormat.MultiSelect,
303
319
  config: { options: selectOptions },
304
320
  },
305
321
  ]);
306
322
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
307
323
 
308
324
  // Initialize table.
309
- const { view } = await Table.makeView({ client, space, typename });
310
- space.db.add(view);
325
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({ db: space.db, typename });
326
+ const table = Table.make({ view, jsonSchema });
327
+ space.db.add(table);
311
328
 
312
329
  // Populate.
313
330
  Array.from({ length: 10 }).map(() => {
314
331
  return space.db.add(
315
- live(storedSchema, {
332
+ Obj.make(storedSchema, {
316
333
  single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
317
334
  multiple: faker.helpers.randomSubset(selectOptionIds),
318
335
  }),