@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.
- package/dist/lib/browser/chunk-BF54SSNL.mjs +70 -0
- package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
- package/dist/lib/browser/{chunk-G7MGPBSR.mjs → chunk-EK6D2CVM.mjs} +656 -681
- package/dist/lib/browser/chunk-EK6D2CVM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1193 -1034
- 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-LD5JEBA6.mjs → chunk-LYYDF6JP.mjs} +656 -680
- package/dist/lib/node-esm/chunk-LYYDF6JP.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 +1193 -1034
- 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 +21 -37
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +82 -81
- 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 +28 -21
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +90 -26
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableContent.d.ts +21 -0
- package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableToolbar.d.ts +25 -0
- package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
- 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 +24 -26
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.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 +21 -23
- 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 +55 -55
- package/src/components/Table/ColumnActionsMenu.tsx +15 -11
- package/src/components/Table/ColumnSettings.tsx +8 -9
- package/src/components/Table/DynamicTable.stories.tsx +16 -16
- package/src/components/Table/DynamicTable.tsx +32 -31
- package/src/components/Table/Relations.stories.tsx +64 -58
- package/src/components/Table/RowActionsMenu.tsx +24 -15
- package/src/components/Table/Table.stories.tsx +115 -98
- package/src/components/Table/Table.tsx +43 -406
- package/src/components/Table/TableContent.tsx +422 -0
- package/src/components/Table/TableToolbar.tsx +111 -0
- package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
- package/src/components/TableCellEditor/FormCellEditor.tsx +117 -134
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +20 -15
- package/src/components/TableCellEditor/TableCellEditor.tsx +37 -32
- package/src/components/index.ts +0 -1
- 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 +53 -7
- package/src/testing/useTestTableModel.ts +45 -38
- package/src/translations.ts +20 -22
- 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-G7MGPBSR.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LD5JEBA6.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/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
- package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
- package/dist/types/src/components/TableToolbar/index.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/TableToolbar/TableToolbar.tsx +0 -105
- package/src/components/TableToolbar/index.ts +0 -5
- package/src/model/table-sorting.ts +0 -225
- 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
|
|
7
|
-
import
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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.
|
|
38
|
-
const
|
|
39
|
-
const { space } =
|
|
40
|
-
const [
|
|
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
|
-
|
|
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]);
|
|
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
|
|
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
|
|
90
|
-
const { model:
|
|
91
|
-
const {
|
|
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:
|
|
96
|
-
|
|
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='
|
|
101
|
+
<div className='dx-expander grid grid-cols-2 divide-x divide-separator'>
|
|
103
102
|
<TableComponent.Root>
|
|
104
|
-
<TableComponent.
|
|
103
|
+
<TableComponent.Content
|
|
105
104
|
model={orgModel}
|
|
106
|
-
schema={
|
|
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.
|
|
113
|
+
<TableComponent.Content
|
|
116
114
|
model={contactModel}
|
|
117
|
-
schema={
|
|
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
|
-
|
|
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: [
|
|
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 }
|
|
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
|
-
|
|
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
|
-
//
|
|
227
|
-
const
|
|
228
|
-
await
|
|
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
|
|
239
|
-
await
|
|
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
|
-
}
|
|
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
|
|
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 { Obj, type QueryAST, Type } from '@dxos/echo';
|
|
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,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(():
|
|
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(
|
|
@@ -81,7 +74,9 @@ const StoryViewEditor = ({
|
|
|
81
74
|
invariant(Type.isMutable(schema));
|
|
82
75
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
83
76
|
invariant(view);
|
|
84
|
-
view
|
|
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={
|
|
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
|
-
|
|
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 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
|
|
154
|
+
type DefaultStoryProps = { rows?: number };
|
|
152
155
|
|
|
153
156
|
//
|
|
154
157
|
// Story definitions.
|
|
155
158
|
//
|
|
156
159
|
|
|
157
|
-
// TODO(burdon): Need
|
|
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: [
|
|
170
|
+
types: [View.View, Table.Table],
|
|
166
171
|
createIdentity: true,
|
|
167
172
|
createSpace: true,
|
|
168
|
-
onCreateSpace: async ({
|
|
169
|
-
const [schema] = await space.db.schemaRegistry.register([
|
|
170
|
-
const { view } = await
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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(
|
|
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: [
|
|
217
|
+
types: [View.View, Table.Table],
|
|
210
218
|
createIdentity: true,
|
|
211
219
|
createSpace: true,
|
|
212
|
-
onCreateSpace: async ({
|
|
213
|
-
const { view } = await
|
|
214
|
-
|
|
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:
|
|
219
|
-
// { type:
|
|
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
|
-
|
|
242
|
-
typename: 'dxos.
|
|
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: [
|
|
263
|
+
types: [View.View, Table.Table, TestSchema.Person, TestSchema.Organization, ContactWithArrayOfEmails],
|
|
252
264
|
createIdentity: true,
|
|
253
265
|
createSpace: true,
|
|
254
|
-
onCreateSpace: async ({
|
|
255
|
-
const { view } = await
|
|
256
|
-
|
|
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:
|
|
261
|
-
// { type:
|
|
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<
|
|
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: [
|
|
294
|
+
types: [View.View, Table.Table],
|
|
279
295
|
createIdentity: true,
|
|
280
296
|
createSpace: true,
|
|
281
|
-
onCreateSpace: async ({
|
|
297
|
+
onCreateSpace: async ({ space }) => {
|
|
282
298
|
// Configure schema.
|
|
283
|
-
const typename = 'example.
|
|
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:
|
|
313
|
+
format: Format.TypeFormat.SingleSelect,
|
|
298
314
|
config: { options: selectOptions },
|
|
299
315
|
},
|
|
300
316
|
{
|
|
301
317
|
name: 'multiple',
|
|
302
|
-
format:
|
|
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
|
|
310
|
-
|
|
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
|
-
|
|
332
|
+
Obj.make(storedSchema, {
|
|
316
333
|
single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
|
|
317
334
|
multiple: faker.helpers.randomSubset(selectOptionIds),
|
|
318
335
|
}),
|