@dxos/react-ui-table 0.8.4-main.c4373fc → 0.8.4-main.d05673bc65
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.
- package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
- package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
- package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
- package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1158 -1024
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +30 -42
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +3 -3
- package/dist/lib/node-esm/chunk-GI72MQSG.mjs +1753 -0
- package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs +71 -0
- package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1158 -1024
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +30 -42
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +3 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +4 -18
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +12 -9
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +11 -11
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +71 -5
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +4 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +5 -5
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -15
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +5 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useTableModel.d.ts +9 -9
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +5 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +17 -8
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +105 -25
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +3 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +2 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +4 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +26 -0
- package/dist/types/src/types/Table.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +14 -11
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
- package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/sort.d.ts +12 -0
- package/dist/types/src/util/sort.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +54 -54
- package/src/components/Table/ColumnActionsMenu.tsx +10 -6
- package/src/components/Table/ColumnSettings.tsx +8 -9
- package/src/components/Table/DynamicTable.stories.tsx +16 -16
- package/src/components/Table/DynamicTable.tsx +30 -22
- package/src/components/Table/Relations.stories.tsx +52 -60
- package/src/components/Table/RowActionsMenu.tsx +24 -15
- package/src/components/Table/Table.stories.tsx +116 -98
- package/src/components/Table/Table.tsx +324 -299
- package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
- package/src/components/TableCellEditor/FormCellEditor.tsx +117 -134
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +18 -13
- package/src/components/TableCellEditor/TableCellEditor.tsx +31 -31
- package/src/components/TableToolbar/TableToolbar.tsx +53 -37
- package/src/components/testing.ts +38 -12
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +39 -0
- package/src/hooks/useTableModel.ts +36 -34
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +82 -54
- package/src/model/table-model.ts +436 -170
- package/src/model/table-presentation.test.ts +59 -49
- package/src/model/table-presentation.ts +43 -37
- package/src/playwright/TableManager.ts +13 -6
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +56 -10
- package/src/testing/useTestTableModel.ts +45 -38
- package/src/translations.ts +3 -3
- package/src/types/Table.ts +79 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.test.ts +70 -0
- package/src/util/dynamic-table.ts +41 -28
- package/src/util/index.ts +2 -0
- package/src/util/query-ast.ts +22 -0
- package/src/util/sort.ts +90 -0
- package/src/util/table-buttons.ts +1 -1
- package/src/util/table-controls.ts +2 -2
- package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
- package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
- package/dist/lib/browser/chunk-OZTXLZGM.mjs +0 -1804
- package/dist/lib/browser/chunk-OZTXLZGM.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-S56Z4V4X.mjs +0 -1805
- package/dist/lib/node-esm/chunk-S56Z4V4X.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -52
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -28
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -225
- 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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
38
|
-
const
|
|
39
|
-
const { space } =
|
|
40
|
-
const [
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
90
|
-
const { model:
|
|
91
|
-
const {
|
|
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:
|
|
96
|
-
|
|
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='
|
|
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={
|
|
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={
|
|
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: [
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
234
|
-
const
|
|
235
|
-
await expect(
|
|
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
|
-
|
|
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
|
|
17
|
-
const state = modals.state
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
|
|
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 {
|
|
25
|
-
import {
|
|
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 {
|
|
28
|
-
import {
|
|
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
|
|
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(
|
|
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(():
|
|
51
|
+
parent: Schema.optional(Schema.suspend((): Ref.RefSchema<Example> => Ref.Ref(Example))).annotations({
|
|
59
52
|
title: 'Parent',
|
|
60
53
|
}),
|
|
61
54
|
}).pipe(
|
|
62
|
-
Type.
|
|
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
|
|
58
|
+
interface Example extends Schema.Schema.Type<typeof Example> {}
|
|
66
59
|
|
|
67
60
|
const StoryViewEditor = ({
|
|
68
61
|
view,
|
|
69
62
|
schema,
|
|
70
|
-
|
|
63
|
+
db,
|
|
71
64
|
handleDeleteColumn,
|
|
72
65
|
}: {
|
|
73
|
-
view?:
|
|
66
|
+
view?: View.View;
|
|
74
67
|
schema?: Schema.Schema.AnyNoContext;
|
|
75
|
-
|
|
68
|
+
db?: Database.Database;
|
|
76
69
|
handleDeleteColumn: (fieldId: string) => void;
|
|
77
70
|
}) => {
|
|
78
71
|
const handleQueryChanged = useCallback(
|
|
79
|
-
(
|
|
72
|
+
(newQuery: QueryAST.Query) => {
|
|
80
73
|
invariant(schema);
|
|
81
74
|
invariant(Type.isMutable(schema));
|
|
82
|
-
schema.updateTypename(
|
|
75
|
+
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
83
76
|
invariant(view);
|
|
84
|
-
view
|
|
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={
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
|
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: [
|
|
171
|
+
types: [View.View, Table.Table],
|
|
166
172
|
createIdentity: true,
|
|
167
173
|
createSpace: true,
|
|
168
|
-
onCreateSpace: async ({
|
|
169
|
-
const [schema] = await space.db.schemaRegistry.register([
|
|
170
|
-
const { view } = await
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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(
|
|
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: [
|
|
218
|
+
types: [View.View, Table.Table],
|
|
210
219
|
createIdentity: true,
|
|
211
220
|
createSpace: true,
|
|
212
|
-
onCreateSpace: async ({
|
|
213
|
-
const { view } = await
|
|
214
|
-
|
|
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:
|
|
219
|
-
// { type:
|
|
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,8 +251,8 @@ const ContactWithArrayOfEmails = Schema.Struct({
|
|
|
238
251
|
),
|
|
239
252
|
),
|
|
240
253
|
}).pipe(
|
|
241
|
-
|
|
242
|
-
typename: 'dxos.
|
|
254
|
+
Type.object({
|
|
255
|
+
typename: 'org.dxos.type.contact-with-array-of-emails',
|
|
243
256
|
version: '0.1.0',
|
|
244
257
|
}),
|
|
245
258
|
);
|
|
@@ -248,17 +261,21 @@ export const ArrayOfObjects: StoryObj = {
|
|
|
248
261
|
render: DefaultStory,
|
|
249
262
|
decorators: [
|
|
250
263
|
withClientProvider({
|
|
251
|
-
types: [
|
|
264
|
+
types: [View.View, Table.Table, TestSchema.Person, TestSchema.Organization, ContactWithArrayOfEmails],
|
|
252
265
|
createIdentity: true,
|
|
253
266
|
createSpace: true,
|
|
254
|
-
onCreateSpace: async ({
|
|
255
|
-
const { view } = await
|
|
256
|
-
|
|
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:
|
|
261
|
-
// { type:
|
|
277
|
+
// { type: TestSchema.Person, count: 10 },
|
|
278
|
+
// { type: TestSchema.Organization, count: 1 },
|
|
262
279
|
{ type: ContactWithArrayOfEmails, count: 10 },
|
|
263
280
|
]);
|
|
264
281
|
},
|
|
@@ -275,12 +292,12 @@ export const Tags: Meta<StoryProps> = {
|
|
|
275
292
|
render: DefaultStory,
|
|
276
293
|
decorators: [
|
|
277
294
|
withClientProvider({
|
|
278
|
-
types: [
|
|
295
|
+
types: [View.View, Table.Table],
|
|
279
296
|
createIdentity: true,
|
|
280
297
|
createSpace: true,
|
|
281
|
-
onCreateSpace: async ({
|
|
298
|
+
onCreateSpace: async ({ space }) => {
|
|
282
299
|
// Configure schema.
|
|
283
|
-
const typename = 'example.
|
|
300
|
+
const typename = 'com.example.type.single-select';
|
|
284
301
|
const selectOptions = [
|
|
285
302
|
{ id: 'one', title: 'One', color: 'emerald' },
|
|
286
303
|
{ id: 'two', title: 'Two', color: 'blue' },
|
|
@@ -294,25 +311,26 @@ export const Tags: Meta<StoryProps> = {
|
|
|
294
311
|
const schema = getSchemaFromPropertyDefinitions(typename, [
|
|
295
312
|
{
|
|
296
313
|
name: 'single',
|
|
297
|
-
format:
|
|
314
|
+
format: Format.TypeFormat.SingleSelect,
|
|
298
315
|
config: { options: selectOptions },
|
|
299
316
|
},
|
|
300
317
|
{
|
|
301
318
|
name: 'multiple',
|
|
302
|
-
format:
|
|
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
|
|
310
|
-
|
|
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
|
-
|
|
333
|
+
Obj.make(storedSchema, {
|
|
316
334
|
single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
|
|
317
335
|
multiple: faker.helpers.randomSubset(selectOptionIds),
|
|
318
336
|
}),
|