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