@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.c4373fc
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-4K7JPALT.mjs +66 -0
- package/dist/lib/browser/chunk-4K7JPALT.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-OZTXLZGM.mjs} +544 -491
- package/dist/lib/browser/chunk-OZTXLZGM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +590 -675
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +13 -8
- 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-MK6OVUF6.mjs +67 -0
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-S56Z4V4X.mjs} +544 -491
- package/dist/lib/node-esm/chunk-S56Z4V4X.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +590 -675
- 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 +13 -8
- 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 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +108 -0
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +8 -4
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +42 -4
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
- 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.map +1 -1
- package/dist/types/src/components/testing.d.ts +3 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +3 -2
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +2 -2
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +0 -7
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +9 -7
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +2 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-sorting.d.ts +2 -3
- package/dist/types/src/model/table-sorting.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +3 -22
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- 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/types/schema.d.ts +25 -5
- package/dist/types/src/types/schema.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +4 -5
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- 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/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +55 -50
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +1 -1
- package/src/components/Table/DynamicTable.stories.tsx +15 -11
- package/src/components/Table/DynamicTable.tsx +8 -10
- package/src/components/Table/Relations.stories.tsx +248 -0
- package/src/components/Table/RowActionsMenu.tsx +3 -3
- package/src/components/Table/Table.stories.tsx +66 -54
- package/src/components/Table/Table.tsx +147 -117
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +24 -64
- package/src/components/TableCellEditor/TableCellEditor.tsx +74 -224
- package/src/components/TableCellEditor/index.ts +1 -0
- package/src/components/TableToolbar/TableToolbar.tsx +2 -2
- package/src/components/testing.ts +2 -2
- package/src/hooks/useAddRow.ts +7 -5
- package/src/hooks/useTableModel.ts +2 -2
- package/src/model/modal-controller.ts +1 -29
- package/src/model/selection-model.ts +3 -2
- package/src/model/table-model.test.ts +19 -10
- package/src/model/table-model.ts +37 -25
- package/src/model/table-presentation.test.ts +23 -21
- package/src/model/table-presentation.ts +74 -33
- package/src/model/table-sorting.ts +16 -7
- package/src/playwright/smoke.spec.ts +129 -37
- package/src/testing/useTestTableModel.ts +11 -7
- package/src/translations.ts +2 -2
- package/src/types/index.ts +1 -1
- package/src/types/schema.ts +63 -6
- package/src/util/dynamic-table.ts +13 -13
- package/src/util/index.ts +0 -1
- package/src/util/schema.ts +4 -4
- package/src/util/table-buttons.ts +4 -3
- package/src/util/table-controls.ts +4 -1
- package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
- package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
- package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-Y66YLSTC.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/components/Table/relations.stories.d.ts +0 -9
- package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
- package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
- package/dist/types/src/util/table.d.ts +0 -11
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/Table/CreateRefPanel.tsx +0 -86
- package/src/components/Table/relations.stories.tsx +0 -116
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/util/table.ts +0 -49
|
@@ -2,38 +2,38 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import { type StoryObj, type Meta } from '@storybook/react-vite';
|
|
8
|
-
import { Schema } from 'effect';
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
9
7
|
import React, { useCallback } from 'react';
|
|
10
8
|
|
|
11
|
-
import { Obj, Type } from '@dxos/echo';
|
|
9
|
+
import { Filter, Obj, Query, Type } from '@dxos/echo';
|
|
12
10
|
import {
|
|
13
|
-
FormatEnum,
|
|
14
11
|
EchoObject,
|
|
15
|
-
GeneratorAnnotation,
|
|
16
12
|
FormatAnnotation,
|
|
13
|
+
FormatEnum,
|
|
14
|
+
GeneratorAnnotation,
|
|
15
|
+
LabelAnnotation,
|
|
17
16
|
PropertyMetaAnnotationId,
|
|
18
|
-
} from '@dxos/echo
|
|
17
|
+
} from '@dxos/echo/internal';
|
|
18
|
+
import { live } from '@dxos/echo/internal';
|
|
19
19
|
import { invariant } from '@dxos/invariant';
|
|
20
20
|
import { faker } from '@dxos/random';
|
|
21
21
|
import { PublicKey } from '@dxos/react-client';
|
|
22
|
-
import {
|
|
22
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
23
23
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
24
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
24
25
|
import { ViewEditor } from '@dxos/react-ui-form';
|
|
25
26
|
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
26
|
-
import {
|
|
27
|
+
import { DataType, getSchemaFromPropertyDefinitions } from '@dxos/schema';
|
|
27
28
|
import { Testing, createObjectFactory } from '@dxos/schema/testing';
|
|
28
|
-
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
29
29
|
|
|
30
|
-
import { Table } from './Table';
|
|
31
30
|
import { useTestTableModel } from '../../testing';
|
|
32
31
|
import { translations } from '../../translations';
|
|
33
|
-
import {
|
|
34
|
-
import { createTable } from '../../util';
|
|
32
|
+
import { Table } from '../../types';
|
|
35
33
|
import { TableToolbar } from '../TableToolbar';
|
|
36
34
|
|
|
35
|
+
import { Table as TableComponent } from './Table';
|
|
36
|
+
|
|
37
37
|
const TestSchema = Schema.Struct({
|
|
38
38
|
// TODO(wittjosiah): Should be title. Currently name to work with default label.
|
|
39
39
|
name: Schema.optional(Schema.String).annotations({ title: 'Title' }),
|
|
@@ -58,7 +58,10 @@ const TestSchema = Schema.Struct({
|
|
|
58
58
|
parent: Schema.optional(Schema.suspend((): Type.Ref<TestSchema> => Type.Ref(TestSchema))).annotations({
|
|
59
59
|
title: 'Parent',
|
|
60
60
|
}),
|
|
61
|
-
}).pipe(
|
|
61
|
+
}).pipe(
|
|
62
|
+
Type.Obj({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
|
|
63
|
+
LabelAnnotation.set(['name']),
|
|
64
|
+
);
|
|
62
65
|
interface TestSchema extends Schema.Schema.Type<typeof TestSchema> {}
|
|
63
66
|
|
|
64
67
|
const StoryViewEditor = ({
|
|
@@ -72,13 +75,13 @@ const StoryViewEditor = ({
|
|
|
72
75
|
space?: Space;
|
|
73
76
|
handleDeleteColumn: (fieldId: string) => void;
|
|
74
77
|
}) => {
|
|
75
|
-
const
|
|
78
|
+
const handleQueryChanged = useCallback(
|
|
76
79
|
(typename: string) => {
|
|
77
80
|
invariant(schema);
|
|
78
81
|
invariant(Type.isMutable(schema));
|
|
79
82
|
schema.updateTypename(typename);
|
|
80
83
|
invariant(view);
|
|
81
|
-
view.query.
|
|
84
|
+
view.query.ast = Query.select(Filter.typename(typename)).ast;
|
|
82
85
|
},
|
|
83
86
|
[schema, view],
|
|
84
87
|
);
|
|
@@ -92,7 +95,7 @@ const StoryViewEditor = ({
|
|
|
92
95
|
registry={space?.db.schemaRegistry}
|
|
93
96
|
schema={schema}
|
|
94
97
|
view={view}
|
|
95
|
-
|
|
98
|
+
onQueryChanged={handleQueryChanged}
|
|
96
99
|
onDelete={handleDeleteColumn}
|
|
97
100
|
/>
|
|
98
101
|
);
|
|
@@ -124,8 +127,8 @@ const DefaultStory = () => {
|
|
|
124
127
|
<div className='grow grid grid-cols-[1fr_350px]'>
|
|
125
128
|
<div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
|
|
126
129
|
<TableToolbar classNames='border-be border-subduedSeparator' onAdd={handleInsertRow} onSave={handleSaveView} />
|
|
127
|
-
<
|
|
128
|
-
<
|
|
130
|
+
<TableComponent.Root>
|
|
131
|
+
<TableComponent.Main
|
|
129
132
|
ref={tableRef}
|
|
130
133
|
model={model}
|
|
131
134
|
presentation={presentation}
|
|
@@ -133,11 +136,11 @@ const DefaultStory = () => {
|
|
|
133
136
|
client={client}
|
|
134
137
|
ignoreAttention
|
|
135
138
|
/>
|
|
136
|
-
</
|
|
139
|
+
</TableComponent.Root>
|
|
137
140
|
</div>
|
|
138
141
|
<div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
|
|
139
142
|
<StoryViewEditor view={view} schema={schema} space={space} handleDeleteColumn={handleDeleteColumn} />
|
|
140
|
-
<SyntaxHighlighter language='json' className='
|
|
143
|
+
<SyntaxHighlighter language='json' className='text-xs'>
|
|
141
144
|
{JSON.stringify({ view, schema }, null, 2)}
|
|
142
145
|
</SyntaxHighlighter>
|
|
143
146
|
</div>
|
|
@@ -153,26 +156,23 @@ type StoryProps = { rows?: number };
|
|
|
153
156
|
|
|
154
157
|
// TODO(burdon): Need super simple story.
|
|
155
158
|
|
|
156
|
-
const meta
|
|
159
|
+
const meta = {
|
|
157
160
|
title: 'ui/react-ui-table/Table',
|
|
158
161
|
render: DefaultStory,
|
|
159
|
-
parameters: {
|
|
160
|
-
translations,
|
|
161
|
-
layout: 'fullscreen',
|
|
162
|
-
controls: {
|
|
163
|
-
disable: true,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
162
|
decorators: [
|
|
167
163
|
withTheme,
|
|
168
|
-
withLayout({ fullscreen: true }),
|
|
169
164
|
withClientProvider({
|
|
170
|
-
types: [DataType.View,
|
|
165
|
+
types: [DataType.View, Table.Table],
|
|
171
166
|
createIdentity: true,
|
|
172
167
|
createSpace: true,
|
|
173
|
-
|
|
168
|
+
onCreateSpace: async ({ client, space }) => {
|
|
174
169
|
const [schema] = await space.db.schemaRegistry.register([TestSchema]);
|
|
175
|
-
const { view } = await
|
|
170
|
+
const { view } = await Table.makeView({ client, space, typename: schema.typename });
|
|
171
|
+
view.projection.fields = [
|
|
172
|
+
view.projection.fields.find((field) => field.path === 'name')!,
|
|
173
|
+
...view.projection.fields.filter((field) => field.path !== 'name'),
|
|
174
|
+
];
|
|
175
|
+
|
|
176
176
|
space.db.add(view);
|
|
177
177
|
|
|
178
178
|
Array.from({ length: 10 }).map(() => {
|
|
@@ -187,22 +187,30 @@ const meta: Meta<StoryProps> = {
|
|
|
187
187
|
},
|
|
188
188
|
}),
|
|
189
189
|
],
|
|
190
|
-
|
|
190
|
+
parameters: {
|
|
191
|
+
layout: 'fullscreen',
|
|
192
|
+
controls: {
|
|
193
|
+
disable: true,
|
|
194
|
+
},
|
|
195
|
+
translations,
|
|
196
|
+
},
|
|
197
|
+
} satisfies Meta<typeof TableComponent>;
|
|
191
198
|
|
|
192
199
|
export default meta;
|
|
193
200
|
|
|
194
|
-
|
|
201
|
+
type Story = StoryObj<typeof meta>;
|
|
202
|
+
|
|
203
|
+
export const Default: Story = {};
|
|
195
204
|
|
|
196
205
|
export const StaticSchema: StoryObj = {
|
|
197
206
|
render: DefaultStory,
|
|
198
|
-
parameters: { translations },
|
|
199
207
|
decorators: [
|
|
200
208
|
withClientProvider({
|
|
201
|
-
types: [DataType.View,
|
|
209
|
+
types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization],
|
|
202
210
|
createIdentity: true,
|
|
203
211
|
createSpace: true,
|
|
204
|
-
|
|
205
|
-
const { view } = await
|
|
212
|
+
onCreateSpace: async ({ client, space }) => {
|
|
213
|
+
const { view } = await Table.makeView({ client, space, typename: Testing.Contact.typename });
|
|
206
214
|
space.db.add(view);
|
|
207
215
|
|
|
208
216
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -212,9 +220,11 @@ export const StaticSchema: StoryObj = {
|
|
|
212
220
|
]);
|
|
213
221
|
},
|
|
214
222
|
}),
|
|
215
|
-
withLayout({ fullscreen: true }),
|
|
216
|
-
withTheme,
|
|
217
223
|
],
|
|
224
|
+
parameters: {
|
|
225
|
+
layout: 'fullscreen',
|
|
226
|
+
translations,
|
|
227
|
+
},
|
|
218
228
|
};
|
|
219
229
|
|
|
220
230
|
const ContactWithArrayOfEmails = Schema.Struct({
|
|
@@ -236,14 +246,13 @@ const ContactWithArrayOfEmails = Schema.Struct({
|
|
|
236
246
|
|
|
237
247
|
export const ArrayOfObjects: StoryObj = {
|
|
238
248
|
render: DefaultStory,
|
|
239
|
-
parameters: { translations },
|
|
240
249
|
decorators: [
|
|
241
250
|
withClientProvider({
|
|
242
|
-
types: [DataType.View,
|
|
251
|
+
types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
|
|
243
252
|
createIdentity: true,
|
|
244
253
|
createSpace: true,
|
|
245
|
-
|
|
246
|
-
const { view } = await
|
|
254
|
+
onCreateSpace: async ({ client, space }) => {
|
|
255
|
+
const { view } = await Table.makeView({ client, space, typename: ContactWithArrayOfEmails.typename });
|
|
247
256
|
space.db.add(view);
|
|
248
257
|
|
|
249
258
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -254,21 +263,22 @@ export const ArrayOfObjects: StoryObj = {
|
|
|
254
263
|
]);
|
|
255
264
|
},
|
|
256
265
|
}),
|
|
257
|
-
withLayout({ fullscreen: true }),
|
|
258
|
-
withTheme,
|
|
259
266
|
],
|
|
267
|
+
parameters: {
|
|
268
|
+
layout: 'fullscreen',
|
|
269
|
+
translations,
|
|
270
|
+
},
|
|
260
271
|
};
|
|
261
272
|
|
|
262
273
|
export const Tags: Meta<StoryProps> = {
|
|
263
274
|
title: 'ui/react-ui-table/Table',
|
|
264
275
|
render: DefaultStory,
|
|
265
|
-
parameters: { translations },
|
|
266
276
|
decorators: [
|
|
267
277
|
withClientProvider({
|
|
268
|
-
types: [DataType.View,
|
|
278
|
+
types: [DataType.View, Table.Table],
|
|
269
279
|
createIdentity: true,
|
|
270
280
|
createSpace: true,
|
|
271
|
-
|
|
281
|
+
onCreateSpace: async ({ client, space }) => {
|
|
272
282
|
// Configure schema.
|
|
273
283
|
const typename = 'example.com/SingleSelect';
|
|
274
284
|
const selectOptions = [
|
|
@@ -296,7 +306,7 @@ export const Tags: Meta<StoryProps> = {
|
|
|
296
306
|
const [storedSchema] = await space.db.schemaRegistry.register([schema]);
|
|
297
307
|
|
|
298
308
|
// Initialize table.
|
|
299
|
-
const { view } = await
|
|
309
|
+
const { view } = await Table.makeView({ client, space, typename });
|
|
300
310
|
space.db.add(view);
|
|
301
311
|
|
|
302
312
|
// Populate.
|
|
@@ -310,7 +320,9 @@ export const Tags: Meta<StoryProps> = {
|
|
|
310
320
|
});
|
|
311
321
|
},
|
|
312
322
|
}),
|
|
313
|
-
withLayout({ fullscreen: true }),
|
|
314
|
-
withTheme,
|
|
315
323
|
],
|
|
324
|
+
parameters: {
|
|
325
|
+
layout: 'fullscreen',
|
|
326
|
+
translations,
|
|
327
|
+
},
|
|
316
328
|
};
|
|
@@ -2,49 +2,47 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import * as String from 'effect/String';
|
|
6
7
|
import React, {
|
|
7
8
|
type MouseEvent,
|
|
8
9
|
type PropsWithChildren,
|
|
9
10
|
type WheelEvent,
|
|
10
11
|
forwardRef,
|
|
11
12
|
useCallback,
|
|
13
|
+
useEffect,
|
|
12
14
|
useImperativeHandle,
|
|
13
|
-
useState,
|
|
14
15
|
useMemo,
|
|
15
|
-
|
|
16
|
+
useState,
|
|
16
17
|
} from 'react';
|
|
17
18
|
|
|
18
|
-
import { type Client } from '@dxos/client';
|
|
19
|
-
import { Filter } from '@dxos/echo';
|
|
20
|
-
import { getValue } from '@dxos/echo-schema';
|
|
21
|
-
import { invariant } from '@dxos/invariant';
|
|
22
19
|
// TODO(wittjosiah): Remove dependency on react-client.
|
|
23
|
-
import {
|
|
24
|
-
import { useTranslation } from '@dxos/react-ui';
|
|
20
|
+
import { type Client } from '@dxos/react-client';
|
|
25
21
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
26
22
|
import {
|
|
27
|
-
closestCell,
|
|
28
23
|
type DxGridElement,
|
|
29
|
-
type DxGridPosition,
|
|
30
|
-
type GridContentProps,
|
|
31
|
-
Grid,
|
|
32
24
|
type DxGridPlane,
|
|
33
25
|
type DxGridPlaneRange,
|
|
34
|
-
|
|
26
|
+
type DxGridPosition,
|
|
27
|
+
Grid,
|
|
28
|
+
type GridContentProps,
|
|
29
|
+
closestCell,
|
|
35
30
|
gridSeparatorBlockEnd,
|
|
31
|
+
gridSeparatorInlineEnd,
|
|
36
32
|
} from '@dxos/react-ui-grid';
|
|
33
|
+
import { DxEditRequest } from '@dxos/react-ui-grid';
|
|
37
34
|
import { mx } from '@dxos/react-ui-theme';
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
37
|
+
import { tableButtons, tableControls } from '../../util';
|
|
38
|
+
import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
|
|
39
39
|
|
|
40
40
|
import { ColumnActionsMenu } from './ColumnActionsMenu';
|
|
41
41
|
import { ColumnSettings } from './ColumnSettings';
|
|
42
|
-
import { CreateRefPanel } from './CreateRefPanel';
|
|
43
42
|
import { RowActionsMenu } from './RowActionsMenu';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
|
|
43
|
+
|
|
44
|
+
const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
|
|
45
|
+
const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
|
|
48
46
|
|
|
49
47
|
//
|
|
50
48
|
// Table.Root
|
|
@@ -76,27 +74,34 @@ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
|
|
|
76
74
|
|
|
77
75
|
export type TableController = {
|
|
78
76
|
update?: (cell?: DxGridPosition) => void;
|
|
77
|
+
handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
export type TableMainProps = {
|
|
82
81
|
model?: TableModel;
|
|
83
82
|
presentation?: TablePresentation;
|
|
84
|
-
schema?: Schema.AnyNoContext;
|
|
83
|
+
schema?: Schema.Schema.AnyNoContext;
|
|
85
84
|
client?: Client;
|
|
86
85
|
// TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
|
|
87
86
|
ignoreAttention?: boolean;
|
|
87
|
+
onCreate?: OnCreateHandler;
|
|
88
88
|
onRowClick?: (row: any) => void;
|
|
89
|
+
testId?: string;
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
const TableMain = forwardRef<TableController, TableMainProps>(
|
|
92
|
-
({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
|
|
93
|
+
({ model, presentation, ignoreAttention, schema, client, onCreate, onRowClick, testId }, forwardedRef) => {
|
|
93
94
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
94
95
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
95
|
-
const { t } = useTranslation(translationKey);
|
|
96
96
|
const modals = useMemo(() => new ModalController(), []);
|
|
97
97
|
|
|
98
98
|
const draftRowCount = model?.getDraftRowCount() ?? 0;
|
|
99
99
|
|
|
100
|
+
const handleSave = useCallback(() => {
|
|
101
|
+
dxGrid?.updateCells(true);
|
|
102
|
+
dxGrid?.requestUpdate();
|
|
103
|
+
}, [dxGrid]);
|
|
104
|
+
|
|
100
105
|
const frozen = useMemo(() => {
|
|
101
106
|
const noActionColumn =
|
|
102
107
|
model?.features.dataEditable === false &&
|
|
@@ -105,7 +110,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
105
110
|
|
|
106
111
|
return {
|
|
107
112
|
frozenRowsStart: 1,
|
|
108
|
-
frozenRowsEnd: draftRowCount,
|
|
113
|
+
frozenRowsEnd: Math.max(1, draftRowCount),
|
|
109
114
|
frozenColsStart: model?.features.selection.enabled ? 1 : 0,
|
|
110
115
|
frozenColsEnd: noActionColumn ? 0 : 1,
|
|
111
116
|
};
|
|
@@ -123,6 +128,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
123
128
|
dxGrid.getCells = getCells;
|
|
124
129
|
}, [presentation, dxGrid, getCells]);
|
|
125
130
|
|
|
131
|
+
const handleInsertRowResult = useCallback(
|
|
132
|
+
(insertResult?: InsertRowResult) => {
|
|
133
|
+
if (insertResult === 'draft') {
|
|
134
|
+
requestAnimationFrame(() => {
|
|
135
|
+
dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
|
|
136
|
+
dxGrid?.refocus();
|
|
137
|
+
});
|
|
138
|
+
} else {
|
|
139
|
+
requestAnimationFrame(() => {
|
|
140
|
+
dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
|
|
141
|
+
dxGrid?.refocus();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[model, dxGrid],
|
|
146
|
+
);
|
|
147
|
+
|
|
126
148
|
/**
|
|
127
149
|
* Provides an external controller that can be called to repaint the table.
|
|
128
150
|
*/
|
|
@@ -140,16 +162,36 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
140
162
|
dxGrid.requestUpdate();
|
|
141
163
|
}
|
|
142
164
|
},
|
|
165
|
+
handleInsertRowResult,
|
|
143
166
|
};
|
|
144
|
-
}, [presentation, dxGrid]);
|
|
167
|
+
}, [presentation, dxGrid, model]);
|
|
168
|
+
|
|
169
|
+
const handleSaveDraftRow = useCallback(
|
|
170
|
+
(rowIndex = 0) => {
|
|
171
|
+
if (model && dxGrid) {
|
|
172
|
+
const didCommitSuccessfully = model.commitDraftRow(rowIndex);
|
|
173
|
+
if (didCommitSuccessfully) {
|
|
174
|
+
requestAnimationFrame(() => {
|
|
175
|
+
dxGrid.scrollToEndRow();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
[model, dxGrid],
|
|
181
|
+
);
|
|
145
182
|
|
|
146
183
|
const handleGridClick = useCallback(
|
|
147
184
|
(event: MouseEvent) => {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
185
|
+
const cell = closestCell(event.target as HTMLElement);
|
|
186
|
+
if (cell) {
|
|
187
|
+
const { row: rowIndex, plane } = cell;
|
|
150
188
|
if (onRowClick) {
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
if (plane === 'grid') {
|
|
190
|
+
const row = model?.getRowAt(rowIndex);
|
|
191
|
+
row && onRowClick(row);
|
|
192
|
+
} else {
|
|
193
|
+
onRowClick(cell);
|
|
194
|
+
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
|
|
@@ -192,14 +234,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
192
234
|
break;
|
|
193
235
|
}
|
|
194
236
|
case 'saveDraftRow': {
|
|
195
|
-
|
|
196
|
-
const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
|
|
197
|
-
if (dxGrid && didCommitSuccessfully) {
|
|
198
|
-
requestAnimationFrame(() => {
|
|
199
|
-
dxGrid.scrollToEndRow();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
237
|
+
handleSaveDraftRow();
|
|
203
238
|
break;
|
|
204
239
|
}
|
|
205
240
|
}
|
|
@@ -240,32 +275,19 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
240
275
|
);
|
|
241
276
|
|
|
242
277
|
const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
|
|
243
|
-
(increment, delta) => {
|
|
244
|
-
if (dxGrid) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
(cell) => {
|
|
253
|
-
if (!model?.features.dataEditable) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// TODO(burdon): Insert row only if bottom row isn't completely blank already.
|
|
258
|
-
if (model && cell.row === model.getRowCount() - 1) {
|
|
259
|
-
model.insertRow();
|
|
260
|
-
if (dxGrid) {
|
|
261
|
-
requestAnimationFrame(() => {
|
|
262
|
-
dxGrid?.scrollToRow(cell.row + 1);
|
|
263
|
-
dxGrid?.refocus('row', 1);
|
|
264
|
-
});
|
|
278
|
+
(increment, delta, cell) => {
|
|
279
|
+
if (dxGrid && model) {
|
|
280
|
+
if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
|
|
281
|
+
handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
|
|
282
|
+
} else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
|
|
283
|
+
handleSaveDraftRow(cell.row);
|
|
284
|
+
handleInsertRowResult(model.insertRow());
|
|
285
|
+
} else {
|
|
286
|
+
dxGrid.refocus(increment, delta);
|
|
265
287
|
}
|
|
266
288
|
}
|
|
267
289
|
},
|
|
268
|
-
[
|
|
290
|
+
[dxGrid, model],
|
|
269
291
|
);
|
|
270
292
|
|
|
271
293
|
const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
|
|
@@ -279,15 +301,72 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
279
301
|
return;
|
|
280
302
|
}
|
|
281
303
|
|
|
304
|
+
// Handle Meta+C (Copy) and Meta+V (Paste) commands
|
|
305
|
+
if (event.metaKey || event.ctrlKey) {
|
|
306
|
+
switch (event.key) {
|
|
307
|
+
case 'c': {
|
|
308
|
+
// Copy focused cell's text content to clipboard
|
|
309
|
+
try {
|
|
310
|
+
const cellData = model.getCellData(cell);
|
|
311
|
+
const textContent = cellData?.toString() ?? '';
|
|
312
|
+
void navigator.clipboard.writeText(textContent);
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.warn('Failed to copy cell content:', error);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case 'v': {
|
|
320
|
+
// Paste clipboard content to focused cell
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
void navigator.clipboard.readText().then((clipboardText) => {
|
|
323
|
+
try {
|
|
324
|
+
// Attempt to set the cell's content to clipboard content
|
|
325
|
+
model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
|
|
326
|
+
handleSave();
|
|
327
|
+
} catch {
|
|
328
|
+
// If validation fails, emit a DxEditRequest event with initialContent from clipboard
|
|
329
|
+
// TODO(thure): Should `dx-grid` expose a method like this?
|
|
330
|
+
const cellElement = (event.target as HTMLElement).closest(
|
|
331
|
+
'[data-dx-grid-action="cell"]',
|
|
332
|
+
) as HTMLElement;
|
|
333
|
+
if (cellElement) {
|
|
334
|
+
const rect = cellElement.getBoundingClientRect();
|
|
335
|
+
const editRequest = new DxEditRequest({
|
|
336
|
+
cellIndex: `${cell.plane},${cell.col},${cell.row}`,
|
|
337
|
+
cellBox: {
|
|
338
|
+
insetInlineStart: rect.left,
|
|
339
|
+
insetBlockStart: rect.top,
|
|
340
|
+
inlineSize: rect.width,
|
|
341
|
+
blockSize: rect.height,
|
|
342
|
+
},
|
|
343
|
+
cellElement,
|
|
344
|
+
initialContent: clipboardText,
|
|
345
|
+
});
|
|
346
|
+
cellElement.dispatchEvent(editRequest);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
282
355
|
switch (event.key) {
|
|
283
356
|
case 'Backspace':
|
|
284
357
|
case 'Delete': {
|
|
285
|
-
|
|
358
|
+
try {
|
|
359
|
+
model.setCellData(cell, undefined);
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
handleSave();
|
|
362
|
+
} catch {
|
|
363
|
+
// Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
|
|
364
|
+
}
|
|
286
365
|
break;
|
|
287
366
|
}
|
|
288
367
|
}
|
|
289
368
|
},
|
|
290
|
-
[model],
|
|
369
|
+
[model, dxGrid, handleSave],
|
|
291
370
|
);
|
|
292
371
|
|
|
293
372
|
const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
|
|
@@ -316,57 +395,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
316
395
|
}
|
|
317
396
|
}, [model, dxGrid]);
|
|
318
397
|
|
|
319
|
-
// TODO(burdon): Factor out?
|
|
320
|
-
// TODO(burdon): Generalize to handle other value types (e.g., enums).
|
|
321
|
-
const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
|
|
322
|
-
async ({ field, props }, text) => {
|
|
323
|
-
if (model && props.referenceSchema && field.referencePath) {
|
|
324
|
-
const space = getSpace(model.view);
|
|
325
|
-
invariant(space);
|
|
326
|
-
|
|
327
|
-
let schema;
|
|
328
|
-
if (client) {
|
|
329
|
-
schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
|
|
330
|
-
}
|
|
331
|
-
if (!schema) {
|
|
332
|
-
schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (schema) {
|
|
336
|
-
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
337
|
-
const options = objects
|
|
338
|
-
.map((obj) => {
|
|
339
|
-
const value = getValue(obj, field.referencePath!);
|
|
340
|
-
if (!value || typeof value !== 'string') {
|
|
341
|
-
return undefined;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
label: value,
|
|
346
|
-
data: obj,
|
|
347
|
-
};
|
|
348
|
-
})
|
|
349
|
-
.filter(isNotFalsy);
|
|
350
|
-
|
|
351
|
-
return [
|
|
352
|
-
...options,
|
|
353
|
-
{
|
|
354
|
-
label: t('create new object label', { text }),
|
|
355
|
-
data: createOption(text),
|
|
356
|
-
},
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return [];
|
|
362
|
-
},
|
|
363
|
-
[model, client, t],
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const handleSave = useCallback(() => {
|
|
367
|
-
dxGrid?.updateCells(true);
|
|
368
|
-
}, [dxGrid]);
|
|
369
|
-
|
|
370
398
|
if (!model || !modals) {
|
|
371
399
|
return <span role='none' className='attention-surface' />;
|
|
372
400
|
}
|
|
@@ -377,28 +405,30 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
377
405
|
model={model}
|
|
378
406
|
modals={modals}
|
|
379
407
|
schema={schema}
|
|
380
|
-
onEnter={handleEnter}
|
|
381
408
|
onFocus={handleFocus}
|
|
382
|
-
onQuery={handleQuery}
|
|
383
409
|
onSave={handleSave}
|
|
410
|
+
onCreate={onCreate}
|
|
411
|
+
client={client}
|
|
384
412
|
/>
|
|
385
413
|
<Grid.Content
|
|
386
414
|
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
387
415
|
frozen={frozen}
|
|
388
416
|
columns={model.columnMeta.value}
|
|
417
|
+
columnDefault={columnDefault}
|
|
418
|
+
rowDefault={rowDefault}
|
|
389
419
|
limitRows={model.getRowCount() ?? 0}
|
|
390
420
|
limitColumns={model.projection.fields.length}
|
|
391
421
|
overscroll='trap'
|
|
392
422
|
onAxisResize={handleAxisResize}
|
|
393
423
|
onClick={handleGridClick}
|
|
394
|
-
|
|
424
|
+
onKeyDownCapture={handleKeyDown}
|
|
395
425
|
onWheelCapture={handleWheel}
|
|
426
|
+
{...(testId && { 'data-testid': testId })}
|
|
396
427
|
ref={setDxGrid}
|
|
397
428
|
/>
|
|
398
429
|
<RowActionsMenu model={model} modals={modals} />
|
|
399
430
|
<ColumnActionsMenu model={model} modals={modals} />
|
|
400
431
|
<ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
|
|
401
|
-
<CreateRefPanel model={model} modals={modals} />
|
|
402
432
|
</Grid.Root>
|
|
403
433
|
);
|
|
404
434
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type GridScopedProps, useGridContext } from '@dxos/react-ui-grid';
|
|
8
8
|
|
|
9
9
|
export type CellValidationMessageProps = {
|
|
10
10
|
validationError: string | null;
|