@dxos/react-ui-table 0.8.4-main.3c1ae3b → 0.8.4-main.3eb6e50203
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-V7A7ZA5E.mjs → chunk-GTGYBRJA.mjs} +606 -641
- package/dist/lib/browser/chunk-GTGYBRJA.mjs.map +7 -0
- package/dist/lib/browser/{chunk-CJQ6T4LL.mjs → chunk-OZBLY7XM.mjs} +4 -5
- package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1082 -969
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -16
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-P73ALX3N.mjs → chunk-DPNSZHU5.mjs} +606 -640
- package/dist/lib/node-esm/chunk-DPNSZHU5.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-T6PXZQ6W.mjs → chunk-WNZSKIRF.mjs} +4 -5
- package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1082 -969
- 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 +20 -16
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +3 -2
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +2 -2
- 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.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +2 -2
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +1 -2
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -13
- package/dist/types/src/components/testing.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 +2 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +6 -5
- 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 +99 -18
- 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 +4 -5
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +17 -14
- package/dist/types/src/types/Table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +10 -7
- 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 +53 -54
- package/src/components/Table/ColumnActionsMenu.tsx +6 -5
- package/src/components/Table/ColumnSettings.tsx +4 -3
- package/src/components/Table/DynamicTable.stories.tsx +10 -8
- package/src/components/Table/DynamicTable.tsx +18 -18
- package/src/components/Table/Relations.stories.tsx +23 -19
- package/src/components/Table/RowActionsMenu.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +47 -31
- package/src/components/Table/Table.tsx +34 -17
- package/src/components/TableCellEditor/FormCellEditor.tsx +49 -65
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +3 -3
- package/src/components/TableCellEditor/TableCellEditor.tsx +2 -2
- package/src/components/TableToolbar/TableToolbar.tsx +18 -12
- package/src/components/testing.ts +37 -11
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +28 -8
- package/src/hooks/useTableModel.ts +27 -23
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +80 -52
- package/src/model/table-model.ts +409 -138
- package/src/model/table-presentation.test.ts +57 -47
- package/src/model/table-presentation.ts +23 -18
- package/src/playwright/TableManager.ts +10 -3
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +48 -4
- package/src/testing/useTestTableModel.ts +23 -20
- package/src/translations.ts +1 -1
- package/src/types/Table.ts +8 -8
- package/src/util/dynamic-table.test.ts +70 -0
- package/src/util/dynamic-table.ts +31 -13
- package/src/util/index.ts +2 -0
- package/src/util/query-ast.ts +19 -0
- package/src/util/sort.ts +90 -0
- package/dist/lib/browser/chunk-CJQ6T4LL.mjs.map +0 -7
- package/dist/lib/browser/chunk-V7A7ZA5E.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-P73ALX3N.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -56
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -234
|
@@ -6,18 +6,19 @@ 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 { Annotation, Format, Obj, type QueryAST, Type } from '@dxos/echo';
|
|
10
|
-
import { PropertyMetaAnnotationId } from '@dxos/echo/internal';
|
|
9
|
+
import { Annotation, type Database, Format, Obj, type QueryAST, Type } from '@dxos/echo';
|
|
10
|
+
import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { faker } from '@dxos/random';
|
|
13
13
|
import { PublicKey } from '@dxos/react-client';
|
|
14
|
-
import { type Space } from '@dxos/react-client/echo';
|
|
15
14
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
15
|
+
import { ScrollArea } from '@dxos/react-ui';
|
|
16
16
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
17
17
|
import { ViewEditor, translations as formTranslations } from '@dxos/react-ui-form';
|
|
18
18
|
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
19
19
|
import { View, getSchemaFromPropertyDefinitions, getTypenameFromQuery } from '@dxos/schema';
|
|
20
20
|
import { TestSchema, createObjectFactory } from '@dxos/schema/testing';
|
|
21
|
+
import { withRegistry } from '@dxos/storybook-utils';
|
|
21
22
|
|
|
22
23
|
import { useTestTableModel } from '../../testing';
|
|
23
24
|
import { translations } from '../../translations';
|
|
@@ -51,7 +52,7 @@ const Example = Schema.Struct({
|
|
|
51
52
|
title: 'Parent',
|
|
52
53
|
}),
|
|
53
54
|
}).pipe(
|
|
54
|
-
Type.
|
|
55
|
+
Type.object({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
|
|
55
56
|
Annotation.LabelAnnotation.set(['name']),
|
|
56
57
|
);
|
|
57
58
|
interface Example extends Schema.Schema.Type<typeof Example> {}
|
|
@@ -59,12 +60,12 @@ interface Example extends Schema.Schema.Type<typeof Example> {}
|
|
|
59
60
|
const StoryViewEditor = ({
|
|
60
61
|
view,
|
|
61
62
|
schema,
|
|
62
|
-
|
|
63
|
+
db,
|
|
63
64
|
handleDeleteColumn,
|
|
64
65
|
}: {
|
|
65
66
|
view?: View.View;
|
|
66
67
|
schema?: Schema.Schema.AnyNoContext;
|
|
67
|
-
|
|
68
|
+
db?: Database.Database;
|
|
68
69
|
handleDeleteColumn: (fieldId: string) => void;
|
|
69
70
|
}) => {
|
|
70
71
|
const handleQueryChanged = useCallback(
|
|
@@ -73,7 +74,9 @@ const StoryViewEditor = ({
|
|
|
73
74
|
invariant(Type.isMutable(schema));
|
|
74
75
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
75
76
|
invariant(view);
|
|
76
|
-
view
|
|
77
|
+
Obj.change(view, (v) => {
|
|
78
|
+
v.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
79
|
+
});
|
|
77
80
|
},
|
|
78
81
|
[schema, view],
|
|
79
82
|
);
|
|
@@ -84,7 +87,7 @@ const StoryViewEditor = ({
|
|
|
84
87
|
|
|
85
88
|
return (
|
|
86
89
|
<ViewEditor
|
|
87
|
-
registry={
|
|
90
|
+
registry={db?.schemaRegistry}
|
|
88
91
|
schema={schema}
|
|
89
92
|
view={view}
|
|
90
93
|
onQueryChanged={handleQueryChanged}
|
|
@@ -98,9 +101,18 @@ const StoryViewEditor = ({
|
|
|
98
101
|
//
|
|
99
102
|
|
|
100
103
|
const DefaultStory = () => {
|
|
101
|
-
const {
|
|
104
|
+
const { db, schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView, handleDeleteColumn } =
|
|
102
105
|
useTestTableModel();
|
|
103
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
|
+
|
|
104
116
|
if (!schema || !table?.view.target) {
|
|
105
117
|
return <div />;
|
|
106
118
|
}
|
|
@@ -115,21 +127,19 @@ const DefaultStory = () => {
|
|
|
115
127
|
schema={schema}
|
|
116
128
|
model={model}
|
|
117
129
|
presentation={presentation}
|
|
130
|
+
onRowClick={handleRowClick}
|
|
118
131
|
ignoreAttention
|
|
119
132
|
/>
|
|
120
133
|
</TableComponent.Root>
|
|
121
134
|
</div>
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
view={table.view.target}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{JSON.stringify({ view: table.view.target, schema }, null, 2)}
|
|
131
|
-
</SyntaxHighlighter>
|
|
132
|
-
</div>
|
|
135
|
+
<ScrollArea.Root orientation='vertical' classNames='bs-full border-l border-separator'>
|
|
136
|
+
<ScrollArea.Viewport>
|
|
137
|
+
<StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
|
|
138
|
+
<SyntaxHighlighter language='json' className='text-xs'>
|
|
139
|
+
{JSON.stringify({ view: table.view.target, schema }, null, 2)}
|
|
140
|
+
</SyntaxHighlighter>
|
|
141
|
+
</ScrollArea.Viewport>
|
|
142
|
+
</ScrollArea.Root>
|
|
133
143
|
</div>
|
|
134
144
|
);
|
|
135
145
|
};
|
|
@@ -146,19 +156,22 @@ const meta = {
|
|
|
146
156
|
title: 'ui/react-ui-table/Table',
|
|
147
157
|
render: DefaultStory,
|
|
148
158
|
decorators: [
|
|
149
|
-
withTheme,
|
|
159
|
+
withTheme(),
|
|
160
|
+
withRegistry,
|
|
150
161
|
withClientProvider({
|
|
151
162
|
types: [View.View, Table.Table],
|
|
152
163
|
createIdentity: true,
|
|
153
164
|
createSpace: true,
|
|
154
165
|
onCreateSpace: async ({ space }) => {
|
|
155
166
|
const [schema] = await space.db.schemaRegistry.register([Example]);
|
|
156
|
-
const { view, jsonSchema } = await View.
|
|
167
|
+
const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: schema.typename });
|
|
157
168
|
const table = Table.make({ view, jsonSchema });
|
|
158
|
-
view
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
169
|
+
Obj.change(view, (v) => {
|
|
170
|
+
v.projection.fields = [
|
|
171
|
+
v.projection.fields.find((field) => field.path === 'name')!,
|
|
172
|
+
...v.projection.fields.filter((field) => field.path !== 'name'),
|
|
173
|
+
];
|
|
174
|
+
});
|
|
162
175
|
|
|
163
176
|
space.db.add(table);
|
|
164
177
|
|
|
@@ -197,7 +210,10 @@ export const StaticSchema: StoryObj = {
|
|
|
197
210
|
createIdentity: true,
|
|
198
211
|
createSpace: true,
|
|
199
212
|
onCreateSpace: async ({ space }) => {
|
|
200
|
-
const { view, jsonSchema } = await View.
|
|
213
|
+
const { view, jsonSchema } = await View.makeFromDatabase({
|
|
214
|
+
db: space.db,
|
|
215
|
+
typename: TestSchema.Person.typename,
|
|
216
|
+
});
|
|
201
217
|
const table = Table.make({ view, jsonSchema });
|
|
202
218
|
space.db.add(table);
|
|
203
219
|
|
|
@@ -226,7 +242,7 @@ const ContactWithArrayOfEmails = Schema.Struct({
|
|
|
226
242
|
),
|
|
227
243
|
),
|
|
228
244
|
}).pipe(
|
|
229
|
-
Type.
|
|
245
|
+
Type.object({
|
|
230
246
|
typename: 'dxos.org/type/ContactWithArrayOfEmails',
|
|
231
247
|
version: '0.1.0',
|
|
232
248
|
}),
|
|
@@ -240,8 +256,8 @@ export const ArrayOfObjects: StoryObj = {
|
|
|
240
256
|
createIdentity: true,
|
|
241
257
|
createSpace: true,
|
|
242
258
|
onCreateSpace: async ({ space }) => {
|
|
243
|
-
const { view, jsonSchema } = await View.
|
|
244
|
-
space,
|
|
259
|
+
const { view, jsonSchema } = await View.makeFromDatabase({
|
|
260
|
+
db: space.db,
|
|
245
261
|
typename: ContactWithArrayOfEmails.typename,
|
|
246
262
|
});
|
|
247
263
|
const table = Table.make({ view, jsonSchema });
|
|
@@ -298,7 +314,7 @@ export const Tags: Meta<StoryProps> = {
|
|
|
298
314
|
const [storedSchema] = await space.db.schemaRegistry.register([schema]);
|
|
299
315
|
|
|
300
316
|
// Initialize table.
|
|
301
|
-
const { view, jsonSchema } = await View.
|
|
317
|
+
const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename });
|
|
302
318
|
const table = Table.make({ view, jsonSchema });
|
|
303
319
|
space.db.add(table);
|
|
304
320
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
5
6
|
import * as String from 'effect/String';
|
|
6
7
|
import React, {
|
|
7
8
|
type JSX,
|
|
@@ -11,6 +12,7 @@ import React, {
|
|
|
11
12
|
type WheelEvent,
|
|
12
13
|
forwardRef,
|
|
13
14
|
useCallback,
|
|
15
|
+
useContext,
|
|
14
16
|
useEffect,
|
|
15
17
|
useImperativeHandle,
|
|
16
18
|
useMemo,
|
|
@@ -18,8 +20,10 @@ import React, {
|
|
|
18
20
|
} from 'react';
|
|
19
21
|
|
|
20
22
|
import { type Type } from '@dxos/echo';
|
|
23
|
+
import { log } from '@dxos/log';
|
|
21
24
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
22
25
|
import {
|
|
26
|
+
type DxGridAxisMeta,
|
|
23
27
|
type DxGridElement,
|
|
24
28
|
type DxGridPlane,
|
|
25
29
|
type DxGridPlaneRange,
|
|
@@ -31,7 +35,7 @@ import {
|
|
|
31
35
|
gridSeparatorInlineEnd,
|
|
32
36
|
} from '@dxos/react-ui-grid';
|
|
33
37
|
import { DxEditRequest } from '@dxos/react-ui-grid';
|
|
34
|
-
import { mx } from '@dxos/
|
|
38
|
+
import { mx } from '@dxos/ui-theme';
|
|
35
39
|
|
|
36
40
|
import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
37
41
|
import { tableButtons, tableControls } from '../../util';
|
|
@@ -43,6 +47,7 @@ import { RowActionsMenu } from './RowActionsMenu';
|
|
|
43
47
|
|
|
44
48
|
const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
|
|
45
49
|
const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
|
|
50
|
+
const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
|
|
46
51
|
|
|
47
52
|
//
|
|
48
53
|
// Table.Root
|
|
@@ -88,13 +93,24 @@ export type TableMainProps<T extends Type.Entity.Any = Type.Entity.Any> = {
|
|
|
88
93
|
testId?: string;
|
|
89
94
|
};
|
|
90
95
|
|
|
96
|
+
const emptyRowsAtom = Atom.make<unknown[]>([]);
|
|
97
|
+
const emptyCellUpdateAtom = Atom.make<number>(0);
|
|
98
|
+
|
|
91
99
|
const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
92
100
|
{ schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId }: TableMainProps<T>,
|
|
93
101
|
forwardedRef: Ref<TableController>,
|
|
94
102
|
) => {
|
|
103
|
+
const registry = useContext(RegistryContext);
|
|
95
104
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
96
105
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
97
|
-
const modals = useMemo(() => new ModalController(), []);
|
|
106
|
+
const modals = useMemo(() => new ModalController(registry), [registry]);
|
|
107
|
+
const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
|
|
108
|
+
// Subscribe to rows atom to trigger re-render when rows change.
|
|
109
|
+
const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
|
|
110
|
+
// Subscribe to cell update counter to trigger re-render when individual cells change.
|
|
111
|
+
const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
|
|
112
|
+
// Derive column count from columnMeta (reactive) with fallback.
|
|
113
|
+
const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
|
|
98
114
|
|
|
99
115
|
const draftRowCount = model?.getDraftRowCount() ?? 0;
|
|
100
116
|
|
|
@@ -117,15 +133,6 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
117
133
|
[presentation],
|
|
118
134
|
);
|
|
119
135
|
|
|
120
|
-
// Set initial sort order.
|
|
121
|
-
// TODO(burdon): Save sort order to local storage.
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
const fieldId = model?.projection?.fields?.[0]?.id;
|
|
124
|
-
if (fieldId) {
|
|
125
|
-
model.sorting.setSort(fieldId, 'asc');
|
|
126
|
-
}
|
|
127
|
-
}, [model]);
|
|
128
|
-
|
|
129
136
|
useEffect(() => {
|
|
130
137
|
if (!dxGrid || !presentation) {
|
|
131
138
|
return;
|
|
@@ -134,6 +141,16 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
134
141
|
dxGrid.getCells = getCells;
|
|
135
142
|
}, [dxGrid, presentation, getCells]);
|
|
136
143
|
|
|
144
|
+
// Trigger grid update when rows or cell values change.
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
if (!dxGrid) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
dxGrid.updateCells(true);
|
|
151
|
+
dxGrid.requestUpdate();
|
|
152
|
+
}, [dxGrid, rows, cellUpdateCounter]);
|
|
153
|
+
|
|
137
154
|
const handleInsertRowResult = useCallback(
|
|
138
155
|
(insertResult?: InsertRowResult) => {
|
|
139
156
|
if (insertResult === 'draft') {
|
|
@@ -220,7 +237,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
220
237
|
break;
|
|
221
238
|
}
|
|
222
239
|
case 'sort': {
|
|
223
|
-
model?.
|
|
240
|
+
model?.toggleSort(data.fieldId);
|
|
224
241
|
break;
|
|
225
242
|
}
|
|
226
243
|
case 'saveDraftRow': {
|
|
@@ -246,7 +263,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
246
263
|
switch (data.type) {
|
|
247
264
|
case 'checkbox': {
|
|
248
265
|
if (data.header) {
|
|
249
|
-
model?.selection.setSelection(model.selection.
|
|
266
|
+
model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
|
|
250
267
|
} else {
|
|
251
268
|
model?.selection.toggleSelectionForRowIndex(data.rowIndex);
|
|
252
269
|
}
|
|
@@ -302,7 +319,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
302
319
|
void navigator.clipboard.writeText(textContent);
|
|
303
320
|
event.preventDefault();
|
|
304
321
|
} catch (error) {
|
|
305
|
-
|
|
322
|
+
log.catch(error);
|
|
306
323
|
}
|
|
307
324
|
break;
|
|
308
325
|
}
|
|
@@ -423,11 +440,11 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
|
423
440
|
<Grid.Content
|
|
424
441
|
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
425
442
|
frozen={frozen}
|
|
426
|
-
columns={
|
|
443
|
+
columns={columnMeta}
|
|
427
444
|
columnDefault={columnDefault}
|
|
428
445
|
rowDefault={rowDefault}
|
|
429
|
-
limitRows={
|
|
430
|
-
limitColumns={
|
|
446
|
+
limitRows={rows.length}
|
|
447
|
+
limitColumns={columnCount}
|
|
431
448
|
overscroll='trap'
|
|
432
449
|
onAxisResize={handleAxisResize}
|
|
433
450
|
onClick={handleGridClick}
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
import type * as Schema from 'effect/Schema';
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { Entity, type Type } from '@dxos/echo';
|
|
9
|
+
import { Ref, getValue } from '@dxos/echo/internal';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { getSnapshot } from '@dxos/live-object';
|
|
12
|
-
import { getSpace } from '@dxos/react-client/echo';
|
|
13
11
|
import { type Label, Popover } from '@dxos/react-ui';
|
|
14
|
-
import { Form, type FormRootProps,
|
|
12
|
+
import { Form, type FormRootProps, type RefFieldProps } from '@dxos/react-ui-form';
|
|
15
13
|
import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
16
14
|
import { type FieldProjection } from '@dxos/schema';
|
|
17
15
|
import { getDeep, isTruthy, setDeep } from '@dxos/util';
|
|
@@ -22,7 +20,7 @@ import { narrowSchema } from '../../util';
|
|
|
22
20
|
|
|
23
21
|
const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
|
|
24
22
|
|
|
25
|
-
export type OnCreateHandler = (schema:
|
|
23
|
+
export type OnCreateHandler = (schema: Type.Entity.Any, values: any) => Parameters<typeof Ref.make>[0];
|
|
26
24
|
|
|
27
25
|
export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
|
|
28
26
|
__gridScope: any;
|
|
@@ -67,33 +65,12 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
67
65
|
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
68
66
|
}, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
|
|
69
67
|
|
|
70
|
-
const getSchema = useCallback(
|
|
71
|
-
({ typename }: { typename: string }) => {
|
|
72
|
-
const space = getSpace(model!.view);
|
|
73
|
-
invariant(space);
|
|
74
|
-
const schema = space.db.schemaRegistry.query({ typename, location: ['database', 'runtime'] }).runSync()[0];
|
|
75
|
-
return { space, schema };
|
|
76
|
-
},
|
|
77
|
-
[model],
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
const refSchema = useMemo(() => {
|
|
81
|
-
if (fieldProjection.props.format === Format.TypeFormat.Ref && fieldProjection.props.referenceSchema) {
|
|
82
|
-
const { schema } = getSchema({ typename: fieldProjection.props.referenceSchema });
|
|
83
|
-
return schema;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
}, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
|
|
88
|
-
|
|
89
|
-
const createSchema = useMemo(() => (refSchema ? omitId(refSchema) : null), [refSchema]);
|
|
90
|
-
|
|
91
68
|
const originalRow = useMemo<TableRow | undefined>(() => {
|
|
92
69
|
if (model && contextEditing) {
|
|
93
70
|
// Check if this is a draft cell and get the appropriate row data
|
|
94
71
|
const cell = parseCellIndex(contextEditing.index);
|
|
95
72
|
if (model.isDraftCell(cell)) {
|
|
96
|
-
const draftRow = model.
|
|
73
|
+
const draftRow = model.getDraftRows()[cell.row];
|
|
97
74
|
invariant(draftRow);
|
|
98
75
|
return draftRow.data;
|
|
99
76
|
} else {
|
|
@@ -106,8 +83,9 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
106
83
|
return undefined;
|
|
107
84
|
}, [model, contextEditing]);
|
|
108
85
|
|
|
109
|
-
// NOTE: Important to get a
|
|
110
|
-
|
|
86
|
+
// NOTE: Important to get a mutable deep clone to eject from the echo object.
|
|
87
|
+
// TODO(wittjosiah): Consider using something like Obj.clone for this use case.
|
|
88
|
+
const formValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
|
|
111
89
|
|
|
112
90
|
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
113
91
|
if (nextOpen === false) {
|
|
@@ -119,24 +97,39 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
119
97
|
|
|
120
98
|
const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
|
|
121
99
|
(values) => {
|
|
100
|
+
if (!originalRow) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
122
103
|
const path = fieldProjection.field.path;
|
|
123
104
|
const value = getDeep(values, [path]);
|
|
124
|
-
|
|
105
|
+
// Use model's changeRow for consistent mutation handling.
|
|
106
|
+
if (model) {
|
|
107
|
+
model.changeRow(originalRow, (mutableRow) => {
|
|
108
|
+
setDeep(mutableRow, [path], value);
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
setDeep(originalRow, [path], value);
|
|
112
|
+
}
|
|
125
113
|
contextEditing?.cellElement?.focus();
|
|
126
114
|
setEditing(null);
|
|
127
115
|
setLocalEditing(false);
|
|
128
116
|
onSave?.();
|
|
129
117
|
},
|
|
130
|
-
[fieldProjection.field.path, onSave, contextEditing, originalRow],
|
|
118
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow, model],
|
|
131
119
|
);
|
|
132
120
|
|
|
133
121
|
const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
|
|
134
|
-
(values) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
122
|
+
(schema, values) => {
|
|
123
|
+
const objectWithId = onCreate?.(schema, values);
|
|
124
|
+
if (objectWithId && originalRow) {
|
|
125
|
+
const ref = Ref.make(objectWithId);
|
|
126
|
+
const path = fieldProjection.field.path;
|
|
127
|
+
// Use model's changeRow for consistent mutation handling.
|
|
128
|
+
if (model) {
|
|
129
|
+
model.changeRow(originalRow, (mutableRow) => {
|
|
130
|
+
setDeep(mutableRow, [path], ref);
|
|
131
|
+
});
|
|
132
|
+
} else {
|
|
140
133
|
setDeep(originalRow, [path], ref);
|
|
141
134
|
}
|
|
142
135
|
}
|
|
@@ -145,27 +138,20 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
145
138
|
setLocalEditing(false);
|
|
146
139
|
onSave?.();
|
|
147
140
|
},
|
|
148
|
-
[fieldProjection.field.path, onSave, contextEditing, originalRow,
|
|
141
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow, onCreate, model],
|
|
149
142
|
);
|
|
150
143
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
})
|
|
163
|
-
.filter(isTruthy);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return [];
|
|
167
|
-
},
|
|
168
|
-
[model],
|
|
144
|
+
const getOptions = useCallback<NonNullable<RefFieldProps['getOptions']>>(
|
|
145
|
+
(results) =>
|
|
146
|
+
results
|
|
147
|
+
.map((obj) => {
|
|
148
|
+
return {
|
|
149
|
+
id: Entity.getDXN(obj).toString(),
|
|
150
|
+
label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
|
|
151
|
+
};
|
|
152
|
+
})
|
|
153
|
+
.filter(isTruthy),
|
|
154
|
+
[fieldProjection],
|
|
169
155
|
);
|
|
170
156
|
|
|
171
157
|
if (!editing) {
|
|
@@ -184,15 +170,13 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
184
170
|
autoFocus
|
|
185
171
|
schema={narrowedSchema}
|
|
186
172
|
values={formValues}
|
|
173
|
+
createInitialValuePath={fieldProjection.field.referencePath}
|
|
174
|
+
createOptionIcon='ph--plus--regular'
|
|
175
|
+
createOptionLabel={createOptionLabel}
|
|
176
|
+
db={model?.db}
|
|
177
|
+
getOptions={getOptions}
|
|
178
|
+
onCreate={handleCreate}
|
|
187
179
|
onSave={handleSave}
|
|
188
|
-
onQueryRefOptions={handleQueryRefOptions}
|
|
189
|
-
{...(createSchema && {
|
|
190
|
-
createSchema,
|
|
191
|
-
createInitialValuePath: fieldProjection.field.referencePath,
|
|
192
|
-
createOptionIcon: 'ph--plus--regular',
|
|
193
|
-
createOptionLabel,
|
|
194
|
-
onCreate: handleCreate,
|
|
195
|
-
})}
|
|
196
180
|
>
|
|
197
181
|
<Form.Viewport>
|
|
198
182
|
<Form.Content>
|
|
@@ -45,14 +45,14 @@ const meta = {
|
|
|
45
45
|
component: DefaultStory,
|
|
46
46
|
render: DefaultStory,
|
|
47
47
|
decorators: [
|
|
48
|
-
withTheme,
|
|
49
|
-
withLayout({
|
|
48
|
+
withTheme(),
|
|
49
|
+
withLayout({ layout: 'column' }),
|
|
50
50
|
withClientProvider({
|
|
51
51
|
types: [View.View, Task.Task, Table.Table],
|
|
52
52
|
createIdentity: true,
|
|
53
53
|
createSpace: true,
|
|
54
54
|
onCreateSpace: async ({ space }) => {
|
|
55
|
-
const { view, jsonSchema } = await View.
|
|
55
|
+
const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
|
|
56
56
|
const table = Table.make({ view, jsonSchema });
|
|
57
57
|
space.db.add(table);
|
|
58
58
|
Array.from({ length: 10 }).forEach(() => {
|
|
@@ -68,7 +68,7 @@ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const { col } = parseCellIndex(editing.index);
|
|
71
|
-
const field = model.projection.
|
|
71
|
+
const field = model.projection.getFields()[col];
|
|
72
72
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
73
73
|
invariant(fieldProjection);
|
|
74
74
|
return fieldProjection;
|
|
@@ -115,7 +115,7 @@ export const TableCellEditor = ({
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
const { col } = parseCellIndex(editing.index);
|
|
118
|
-
const field = model.projection.
|
|
118
|
+
const field = model.projection.getFields()[col];
|
|
119
119
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
120
120
|
invariant(fieldProjection);
|
|
121
121
|
return fieldProjection;
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
-
import React, { useMemo } from 'react';
|
|
5
|
+
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { useContext, useEffect, useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
import { live } from '@dxos/live-object';
|
|
9
8
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
9
|
import {
|
|
11
10
|
type ActionGraphEdges,
|
|
@@ -18,7 +17,6 @@ import {
|
|
|
18
17
|
createMenuAction,
|
|
19
18
|
useMenuActions,
|
|
20
19
|
} from '@dxos/react-ui-menu';
|
|
21
|
-
import { atomFromSignal } from '@dxos/react-ui-menu';
|
|
22
20
|
|
|
23
21
|
import { translationKey } from '../../translations';
|
|
24
22
|
|
|
@@ -34,20 +32,20 @@ type TableToolbarState = Partial<{ viewDirty: boolean }>;
|
|
|
34
32
|
|
|
35
33
|
export type TableToolbarProps = ThemedClassName<
|
|
36
34
|
TableToolbarState & {
|
|
37
|
-
onAdd
|
|
38
|
-
onSave
|
|
35
|
+
onAdd?: () => void;
|
|
36
|
+
onSave?: () => void;
|
|
39
37
|
attendableId?: string;
|
|
40
38
|
customActions?: Atom.Atom<ActionGraphProps>;
|
|
41
39
|
}
|
|
42
40
|
>;
|
|
43
41
|
|
|
44
42
|
const createTableToolbarActions = ({
|
|
45
|
-
|
|
43
|
+
stateAtom,
|
|
46
44
|
onAdd,
|
|
47
45
|
onSave,
|
|
48
46
|
customActions,
|
|
49
47
|
}: {
|
|
50
|
-
|
|
48
|
+
stateAtom: Atom.Atom<TableToolbarState>;
|
|
51
49
|
onAdd?: () => void;
|
|
52
50
|
onSave?: () => void;
|
|
53
51
|
customActions?: Atom.Atom<ActionGraphProps>;
|
|
@@ -64,13 +62,14 @@ const createTableToolbarActions = ({
|
|
|
64
62
|
nodes.push(add);
|
|
65
63
|
}
|
|
66
64
|
if (onSave) {
|
|
65
|
+
const state = get(stateAtom);
|
|
67
66
|
const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
|
|
68
67
|
type: 'save-view' as const,
|
|
69
68
|
icon: 'ph--floppy-disk--regular',
|
|
70
69
|
label: ['save view label', { ns: translationKey }],
|
|
71
70
|
testId: 'table.toolbar.save-view',
|
|
72
71
|
iconOnly: false,
|
|
73
|
-
hidden:
|
|
72
|
+
hidden: !state.viewDirty,
|
|
74
73
|
});
|
|
75
74
|
nodes.push(save);
|
|
76
75
|
}
|
|
@@ -97,10 +96,17 @@ export const TableToolbar = ({
|
|
|
97
96
|
onSave,
|
|
98
97
|
customActions,
|
|
99
98
|
}: TableToolbarProps) => {
|
|
100
|
-
const
|
|
99
|
+
const registry = useContext(RegistryContext);
|
|
100
|
+
const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
|
|
101
|
+
|
|
102
|
+
// Update state.viewDirty when the prop changes.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
registry.set(stateAtom, { viewDirty });
|
|
105
|
+
}, [registry, stateAtom, viewDirty]);
|
|
106
|
+
|
|
101
107
|
const actionsCreator = useMemo(
|
|
102
|
-
() => createTableToolbarActions({
|
|
103
|
-
[
|
|
108
|
+
() => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
|
|
109
|
+
[stateAtom, onAdd, onSave, customActions],
|
|
104
110
|
);
|
|
105
111
|
const menu = useMenuActions(actionsCreator);
|
|
106
112
|
|