@dxos/react-ui-table 0.8.2-main.f081794 → 0.8.2-main.fbd8ed0
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-YWNRUF4J.mjs → chunk-PFFO5LYM.mjs} +7 -6
- package/dist/lib/browser/chunk-PFFO5LYM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +323 -174
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node/{chunk-TGG6OZ54.cjs → chunk-VREQOYRW.cjs} +9 -8
- package/dist/lib/node/chunk-VREQOYRW.cjs.map +7 -0
- package/dist/lib/node/index.cjs +302 -156
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +3 -3
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/{chunk-KKLP54L3.mjs → chunk-34JG3SDI.mjs} +7 -6
- package/dist/lib/node-esm/chunk-34JG3SDI.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +323 -174
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- 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/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +3 -4
- package/dist/types/src/components/Table/DynamicTable.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 +2 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +1 -0
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -0
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -0
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -0
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +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 +39 -39
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/types/table.d.ts +44 -43
- package/dist/types/src/types/table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +4 -3
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +12 -0
- package/dist/types/src/util/schema.d.ts.map +1 -0
- package/dist/types/src/util/signals.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts +0 -14
- 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/src/util/table.d.ts +2 -2
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/src/util/tag.d.ts +11 -0
- package/dist/types/src/util/tag.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -33
- package/src/components/Table/CreateRefPanel.tsx +3 -3
- package/src/components/Table/DynamicTable.stories.tsx +2 -2
- package/src/components/Table/DynamicTable.tsx +4 -5
- package/src/components/Table/Table.stories.tsx +66 -20
- package/src/components/Table/Table.tsx +25 -28
- package/src/components/Table/relations.stories.tsx +37 -30
- package/src/components/TableCellEditor/FormCellEditor.tsx +94 -0
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +8 -7
- package/src/components/TableCellEditor/TableCellEditor.tsx +45 -4
- package/src/components/TableToolbar/TableToolbar.tsx +10 -11
- package/src/components/testing.ts +7 -6
- package/src/model/table-model.test.ts +4 -3
- package/src/model/table-model.ts +17 -0
- package/src/model/table-presentation.test.ts +4 -3
- package/src/model/table-presentation.ts +54 -10
- package/src/types/table.ts +15 -7
- package/src/util/dynamic-table.ts +7 -4
- package/src/util/index.ts +1 -0
- package/src/util/schema.ts +36 -0
- package/src/util/table-buttons.ts +0 -23
- package/src/util/table.ts +12 -15
- package/src/util/tag.ts +33 -0
- package/dist/lib/browser/chunk-YWNRUF4J.mjs.map +0 -7
- package/dist/lib/node/chunk-TGG6OZ54.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-KKLP54L3.mjs.map +0 -7
- package/dist/types/src/components/Table/RefPanel.d.ts +0 -9
- package/dist/types/src/components/Table/RefPanel.d.ts.map +0 -1
- package/src/components/Table/RefPanel.tsx +0 -52
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Schema } from 'effect/Schema';
|
|
5
6
|
import React, {
|
|
6
7
|
type MouseEvent,
|
|
7
8
|
type PropsWithChildren,
|
|
@@ -19,26 +20,28 @@ import { invariant } from '@dxos/invariant';
|
|
|
19
20
|
import { Filter } from '@dxos/react-client/echo';
|
|
20
21
|
import { useTranslation } from '@dxos/react-ui';
|
|
21
22
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
closestCell,
|
|
25
|
+
type DxGridElement,
|
|
26
|
+
type DxGridPosition,
|
|
27
|
+
type GridContentProps,
|
|
28
|
+
Grid,
|
|
29
|
+
type DxGridPlane,
|
|
30
|
+
type DxGridPlaneRange,
|
|
31
|
+
gridSeparatorInlineEnd,
|
|
32
|
+
gridSeparatorBlockEnd,
|
|
33
|
+
} from '@dxos/react-ui-grid';
|
|
23
34
|
import { mx } from '@dxos/react-ui-theme';
|
|
24
35
|
import { isNotFalsy, safeParseInt } from '@dxos/util';
|
|
25
36
|
|
|
26
37
|
import { ColumnActionsMenu } from './ColumnActionsMenu';
|
|
27
38
|
import { ColumnSettings } from './ColumnSettings';
|
|
28
39
|
import { CreateRefPanel } from './CreateRefPanel';
|
|
29
|
-
import { RefPanel } from './RefPanel';
|
|
30
40
|
import { RowActionsMenu } from './RowActionsMenu';
|
|
31
41
|
import { ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
32
42
|
import { translationKey } from '../../translations';
|
|
33
43
|
import { tableButtons, tableControls } from '../../util';
|
|
34
|
-
import { createOption,
|
|
35
|
-
|
|
36
|
-
// NOTE(Zan): These fragments add border to inline-end and block-end of the grid using pseudo-elements.
|
|
37
|
-
// These are offset by 1px to avoid double borders in planks.
|
|
38
|
-
const inlineEndLine =
|
|
39
|
-
'[&>.dx-grid]:relative [&>.dx-grid]:after:absolute [&>.dx-grid]:after:inset-block-0 [&>.dx-grid]:after:-inline-end-px [&>.dx-grid]:after:is-px [&>.dx-grid]:after:bg-separator';
|
|
40
|
-
const blockEndLine =
|
|
41
|
-
'[&>.dx-grid]:before:absolute [&>.dx-grid]:before:inset-inline-0 [&>.dx-grid]:before:-block-end-px [&>.dx-grid]:before:bs-px [&>.dx-grid]:before:bg-separator';
|
|
44
|
+
import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
|
|
42
45
|
|
|
43
46
|
//
|
|
44
47
|
// Table.Root
|
|
@@ -73,13 +76,14 @@ export type TableController = {
|
|
|
73
76
|
export type TableMainProps = {
|
|
74
77
|
model?: TableModel;
|
|
75
78
|
presentation?: TablePresentation;
|
|
79
|
+
schema?: Schema.AnyNoContext;
|
|
76
80
|
// TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
|
|
77
81
|
ignoreAttention?: boolean;
|
|
78
82
|
onRowClick?: (row: any) => void;
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
const TableMain = forwardRef<TableController, TableMainProps>(
|
|
82
|
-
({ model, presentation, ignoreAttention, onRowClick }, forwardedRef) => {
|
|
86
|
+
({ model, presentation, ignoreAttention, schema, onRowClick }, forwardedRef) => {
|
|
83
87
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
84
88
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
85
89
|
const { t } = useTranslation(translationKey);
|
|
@@ -98,20 +102,17 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
98
102
|
};
|
|
99
103
|
}, [model]);
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// );
|
|
105
|
+
const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
|
|
106
|
+
(range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
|
|
107
|
+
[presentation],
|
|
108
|
+
);
|
|
106
109
|
|
|
107
110
|
useEffect(() => {
|
|
108
111
|
if (!presentation || !dxGrid) {
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
dxGrid.getCells = (range, plane) => presentation.getCells(range, plane);
|
|
114
|
-
}, [presentation, dxGrid]);
|
|
114
|
+
dxGrid.getCells = getCells;
|
|
115
|
+
}, [presentation, dxGrid, getCells]);
|
|
115
116
|
|
|
116
117
|
/**
|
|
117
118
|
* Provides an external controller that can be called to repaint the table.
|
|
@@ -181,10 +182,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
181
182
|
modals.showColumnCreator();
|
|
182
183
|
break;
|
|
183
184
|
}
|
|
184
|
-
case 'referencedCell': {
|
|
185
|
-
modals.showReferencePanel(data.targetId, data.schemaId);
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
185
|
case 'sort': {
|
|
189
186
|
model?.sorting?.toggleSort(data.fieldId);
|
|
190
187
|
break;
|
|
@@ -312,7 +309,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
312
309
|
invariant(space);
|
|
313
310
|
const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
|
|
314
311
|
if (schema) {
|
|
315
|
-
const { objects } = await space.db.query(Filter.
|
|
312
|
+
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
316
313
|
const options = objects
|
|
317
314
|
.map((obj) => {
|
|
318
315
|
const value = getValue(obj, field.referencePath!);
|
|
@@ -348,15 +345,16 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
348
345
|
|
|
349
346
|
return (
|
|
350
347
|
<Grid.Root id={model.id ?? 'table-grid'}>
|
|
351
|
-
<
|
|
348
|
+
<TableValueEditor
|
|
352
349
|
model={model}
|
|
353
350
|
modals={modals}
|
|
351
|
+
schema={schema}
|
|
354
352
|
onEnter={handleEnter}
|
|
355
353
|
onFocus={handleFocus}
|
|
356
354
|
onQuery={handleQuery}
|
|
357
355
|
/>
|
|
358
356
|
<Grid.Content
|
|
359
|
-
className={mx('[--dx-grid-base:var(--
|
|
357
|
+
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
360
358
|
frozen={frozen}
|
|
361
359
|
// getCells={getCells}
|
|
362
360
|
columns={model.columnMeta.value}
|
|
@@ -373,7 +371,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
373
371
|
<ColumnActionsMenu model={model} modals={modals} />
|
|
374
372
|
<ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
|
|
375
373
|
<CreateRefPanel model={model} modals={modals} />
|
|
376
|
-
<RefPanel model={model} modals={modals} />
|
|
377
374
|
</Grid.Root>
|
|
378
375
|
);
|
|
379
376
|
},
|
|
@@ -5,20 +5,23 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type StoryObj, type Meta } from '@storybook/react';
|
|
8
|
+
import { type Schema } from 'effect';
|
|
9
|
+
import { SchemaAST } from 'effect';
|
|
8
10
|
import React, { useEffect, useMemo } from 'react';
|
|
9
11
|
|
|
10
|
-
import {
|
|
12
|
+
import { type BaseObject, getSchemaTypename, type HasId, toJsonSchema } from '@dxos/echo-schema';
|
|
11
13
|
import { getAnnotation } from '@dxos/effect';
|
|
14
|
+
import { invariant } from '@dxos/invariant';
|
|
12
15
|
import { faker } from '@dxos/random';
|
|
13
|
-
import { live, makeRef
|
|
14
|
-
import { withClientProvider } from '@dxos/react-client/testing';
|
|
15
|
-
import { createView, ViewProjection, ViewType } from '@dxos/schema';
|
|
16
|
-
import {
|
|
16
|
+
import { live, makeRef } from '@dxos/react-client/echo';
|
|
17
|
+
import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
|
|
18
|
+
import { DataType, createView, ViewProjection, ViewType } from '@dxos/schema';
|
|
19
|
+
import { createAsyncGenerator, type ValueGenerator } from '@dxos/schema/testing';
|
|
17
20
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
18
21
|
|
|
19
22
|
import { Table } from './Table';
|
|
20
23
|
import { useTableModel } from '../../hooks';
|
|
21
|
-
import { TablePresentation, type TableRow } from '../../model';
|
|
24
|
+
import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
|
|
22
25
|
import translations from '../../translations';
|
|
23
26
|
import { TableType } from '../../types';
|
|
24
27
|
|
|
@@ -29,12 +32,15 @@ const generator: ValueGenerator = faker as any;
|
|
|
29
32
|
// TODO(burdon): Mutable and immutable views.
|
|
30
33
|
// TODO(burdon): Reconcile schemas types and utils (see API PR).
|
|
31
34
|
// TODO(burdon): Base type for T (with id); see ECHO API PR?
|
|
32
|
-
const useTestModel = <T extends BaseObject & HasId>(schema:
|
|
35
|
+
const useTestModel = <T extends BaseObject & HasId>(schema: Schema.Schema<T>, count: number) => {
|
|
36
|
+
const { space } = useClientProvider();
|
|
37
|
+
|
|
38
|
+
const jsonSchema = useMemo(() => toJsonSchema(schema), [schema]);
|
|
33
39
|
const table = useMemo(() => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const name = getAnnotation<string>(
|
|
37
|
-
const view = createView({ name, typename, jsonSchema
|
|
40
|
+
const typename = getSchemaTypename(schema);
|
|
41
|
+
invariant(typename);
|
|
42
|
+
const name = getAnnotation<string>(SchemaAST.TitleAnnotationId)(schema.ast) ?? typename;
|
|
43
|
+
const view = createView({ name, typename, jsonSchema });
|
|
38
44
|
return live(TableType, { view: makeRef(view) });
|
|
39
45
|
}, [schema]);
|
|
40
46
|
|
|
@@ -44,19 +50,25 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
|
|
47
|
-
return new ViewProjection(
|
|
53
|
+
return new ViewProjection(jsonSchema, table.view.target);
|
|
48
54
|
}, [schema, table]);
|
|
49
55
|
|
|
50
|
-
const
|
|
56
|
+
const features = useMemo<TableFeatures>(
|
|
57
|
+
() => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const model = useTableModel<TableRow>({ table, projection, rows: [], features });
|
|
51
62
|
useEffect(() => {
|
|
52
|
-
if (!model) {
|
|
63
|
+
if (!model || !space) {
|
|
53
64
|
return;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
|
-
const objectGenerator =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
const objectGenerator = createAsyncGenerator(generator, schema, { optional: true, db: space?.db });
|
|
68
|
+
void objectGenerator.createObjects(count).then((objects) => {
|
|
69
|
+
model.setRows(objects);
|
|
70
|
+
});
|
|
71
|
+
}, [model, space]);
|
|
60
72
|
|
|
61
73
|
const presentation = useMemo(() => {
|
|
62
74
|
if (!model) {
|
|
@@ -70,22 +82,17 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
|
|
|
70
82
|
};
|
|
71
83
|
|
|
72
84
|
const DefaultStory = () => {
|
|
73
|
-
|
|
74
|
-
const orgSchema = useMemo(() => new ImmutableSchema(Testing.Org), []);
|
|
75
|
-
const { model: orgModel, presentation: orgPresentation } = useTestModel<Testing.Org>(orgSchema, 50);
|
|
76
|
-
|
|
85
|
+
const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
|
|
77
86
|
// TODO(burdon): Generate links with references.
|
|
78
|
-
const
|
|
79
|
-
const { model: contactModel, presentation: contactPresentation } = useTestModel<Testing.Contact>(contactSchema, 50);
|
|
87
|
+
const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
|
|
80
88
|
|
|
81
|
-
// TODO(burdon): Scrolling isn't working.
|
|
82
89
|
return (
|
|
83
|
-
<div className='
|
|
90
|
+
<div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
|
|
84
91
|
<Table.Root>
|
|
85
|
-
<Table.Main model={orgModel} presentation={orgPresentation} />
|
|
92
|
+
<Table.Main model={orgModel} presentation={orgPresentation} ignoreAttention />
|
|
86
93
|
</Table.Root>
|
|
87
94
|
<Table.Root>
|
|
88
|
-
<Table.Main model={contactModel} presentation={contactPresentation} />
|
|
95
|
+
<Table.Main model={contactModel} presentation={contactPresentation} ignoreAttention />
|
|
89
96
|
</Table.Root>
|
|
90
97
|
</div>
|
|
91
98
|
);
|
|
@@ -97,12 +104,12 @@ const meta: Meta<typeof DefaultStory> = {
|
|
|
97
104
|
parameters: { translations },
|
|
98
105
|
decorators: [
|
|
99
106
|
withClientProvider({
|
|
100
|
-
types: [TableType, ViewType,
|
|
107
|
+
types: [TableType, ViewType, DataType.Organization, DataType.Person],
|
|
101
108
|
createIdentity: true,
|
|
102
109
|
createSpace: true,
|
|
103
110
|
}),
|
|
104
111
|
withTheme,
|
|
105
|
-
withLayout({ fullscreen: true
|
|
112
|
+
withLayout({ fullscreen: true }),
|
|
106
113
|
],
|
|
107
114
|
};
|
|
108
115
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Schema } from 'effect';
|
|
6
|
+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { getSnapshot } from '@dxos/echo-schema';
|
|
9
|
+
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { Popover } from '@dxos/react-ui';
|
|
11
|
+
import { Form } from '@dxos/react-ui-form';
|
|
12
|
+
import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
13
|
+
import { type FieldProjection } from '@dxos/schema';
|
|
14
|
+
import { getDeep, setDeep } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
import { type TableModel } from '../../model';
|
|
17
|
+
import { narrowSchema } from '../../util';
|
|
18
|
+
|
|
19
|
+
type FormCellEditorProps = {
|
|
20
|
+
fieldProjection: FieldProjection;
|
|
21
|
+
model?: TableModel;
|
|
22
|
+
schema?: Schema.Schema.AnyNoContext;
|
|
23
|
+
__gridScope: any;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }: FormCellEditorProps) => {
|
|
27
|
+
const { editing, setEditing } = useGridContext('ArrayEditor', __gridScope);
|
|
28
|
+
const cellRef = useRef<HTMLButtonElement | null>(null);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (editing && editing.cellElement) {
|
|
32
|
+
cellRef.current = editing.cellElement as HTMLButtonElement;
|
|
33
|
+
}
|
|
34
|
+
}, [editing?.cellElement]);
|
|
35
|
+
|
|
36
|
+
const narrowedSchema = useMemo(() => {
|
|
37
|
+
if (!schema) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
41
|
+
}, [JSON.stringify(schema), fieldProjection.field.path]);
|
|
42
|
+
|
|
43
|
+
const originalRow = useMemo(() => {
|
|
44
|
+
if (model && editing) {
|
|
45
|
+
const cell = parseCellIndex(editing.index);
|
|
46
|
+
const row = model.getRowAt(cell.row);
|
|
47
|
+
invariant(row);
|
|
48
|
+
|
|
49
|
+
return row;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return undefined;
|
|
53
|
+
}, [model, editing]);
|
|
54
|
+
|
|
55
|
+
const formValues = useMemo(() => {
|
|
56
|
+
if (originalRow) {
|
|
57
|
+
// NOTE(ZaymonFC): Important to get a snapshot to eject from the live object.
|
|
58
|
+
return getSnapshot(originalRow);
|
|
59
|
+
}
|
|
60
|
+
}, [originalRow]);
|
|
61
|
+
|
|
62
|
+
const handleSave = useCallback(
|
|
63
|
+
(values: any) => {
|
|
64
|
+
const path = fieldProjection.field.path;
|
|
65
|
+
const value = getDeep(values, [path]);
|
|
66
|
+
setDeep(originalRow, [path], value);
|
|
67
|
+
setEditing(null);
|
|
68
|
+
},
|
|
69
|
+
[fieldProjection.field.path, originalRow],
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const handleOpenChange = useCallback(
|
|
73
|
+
(nextOpen: boolean) => {
|
|
74
|
+
if (nextOpen === false) {
|
|
75
|
+
setEditing(null);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[setEditing],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
if (!editing) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Popover.Root open={editing !== null} onOpenChange={handleOpenChange}>
|
|
87
|
+
<Popover.VirtualTrigger virtualRef={cellRef} />
|
|
88
|
+
<Popover.Content tabIndex={-1} classNames='popover-consistent-width'>
|
|
89
|
+
<Popover.Arrow />
|
|
90
|
+
<Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
|
|
91
|
+
</Popover.Content>
|
|
92
|
+
</Popover.Root>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -7,12 +7,12 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { type EchoSchema, isMutable } from '@dxos/echo-schema';
|
|
11
11
|
import { invariant } from '@dxos/invariant';
|
|
12
12
|
import { faker } from '@dxos/random';
|
|
13
13
|
import { Filter, useQuery, live } from '@dxos/react-client/echo';
|
|
14
14
|
import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
|
|
15
|
-
import {
|
|
15
|
+
import { defaultRowSize, Grid, type GridEditing } from '@dxos/react-ui-grid';
|
|
16
16
|
import { ViewProjection, ViewType } from '@dxos/schema';
|
|
17
17
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
@@ -31,7 +31,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
|
|
|
31
31
|
const { space } = useClientProvider();
|
|
32
32
|
invariant(space);
|
|
33
33
|
|
|
34
|
-
const tables = useQuery(space, Filter.
|
|
34
|
+
const tables = useQuery(space, Filter.type(TableType));
|
|
35
35
|
const [table, setTable] = useState<TableType>();
|
|
36
36
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
37
37
|
useEffect(() => {
|
|
@@ -53,9 +53,9 @@ const DefaultStory = ({ editing }: StoryProps) => {
|
|
|
53
53
|
() => ({
|
|
54
54
|
selection: { enabled: true, mode: 'multiple' },
|
|
55
55
|
dataEditable: true,
|
|
56
|
-
schemaEditable:
|
|
56
|
+
schemaEditable: schema && isMutable(schema),
|
|
57
57
|
}),
|
|
58
|
-
[],
|
|
58
|
+
[schema],
|
|
59
59
|
);
|
|
60
60
|
|
|
61
61
|
const model = useTableModel({ table, projection, features });
|
|
@@ -76,7 +76,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
return (
|
|
79
|
-
<div className='flex w-[300px] border border-separator' style={{ height:
|
|
79
|
+
<div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
|
|
80
80
|
<Grid.Root id='test' editing={editing}>
|
|
81
81
|
<TableCellEditor model={model} onQuery={handleQuery} />
|
|
82
82
|
</Grid.Root>
|
|
@@ -107,7 +107,7 @@ const meta: Meta<StoryProps> = {
|
|
|
107
107
|
},
|
|
108
108
|
}),
|
|
109
109
|
withTheme,
|
|
110
|
-
withLayout(
|
|
110
|
+
withLayout(),
|
|
111
111
|
],
|
|
112
112
|
};
|
|
113
113
|
|
|
@@ -120,6 +120,7 @@ export const Default: Story = {
|
|
|
120
120
|
editing: {
|
|
121
121
|
index: 'grid,0,3',
|
|
122
122
|
initialContent: 'Test',
|
|
123
|
+
cellElement: null,
|
|
123
124
|
},
|
|
124
125
|
},
|
|
125
126
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
//
|
|
2
|
-
// Copyright
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Completion } from '@codemirror/autocomplete';
|
|
6
|
+
import { type Schema } from 'effect/Schema';
|
|
6
7
|
import React, { useCallback, useMemo, useRef } from 'react';
|
|
7
8
|
|
|
8
|
-
import { FormatEnum } from '@dxos/echo-schema';
|
|
9
|
+
import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
10
11
|
import { type DxGrid } from '@dxos/lit-grid';
|
|
11
12
|
import { useThemeContext } from '@dxos/react-ui';
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
|
|
27
28
|
import { type FieldProjection } from '@dxos/schema';
|
|
28
29
|
|
|
30
|
+
import { FormCellEditor } from './FormCellEditor';
|
|
29
31
|
import { completion } from './extension';
|
|
30
32
|
import { type TableModel, type ModalController } from '../../model';
|
|
31
33
|
|
|
@@ -43,11 +45,52 @@ export type QueryResult = Pick<Completion, 'label'> & { data: any };
|
|
|
43
45
|
export type TableCellEditorProps = {
|
|
44
46
|
model?: TableModel;
|
|
45
47
|
modals?: ModalController;
|
|
48
|
+
schema?: Schema.AnyNoContext;
|
|
46
49
|
onEnter?: (cell: DxGridPlanePosition) => void;
|
|
47
50
|
onFocus?: DxGrid['refocus'];
|
|
48
51
|
onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
|
|
49
52
|
};
|
|
50
53
|
|
|
54
|
+
export const TableValueEditor = ({
|
|
55
|
+
model,
|
|
56
|
+
modals,
|
|
57
|
+
schema,
|
|
58
|
+
onEnter,
|
|
59
|
+
onFocus,
|
|
60
|
+
onQuery,
|
|
61
|
+
__gridScope,
|
|
62
|
+
}: GridScopedProps<TableCellEditorProps>) => {
|
|
63
|
+
const { editing } = useGridContext('TableValueEditor', __gridScope);
|
|
64
|
+
|
|
65
|
+
const fieldProjection = useMemo<FieldProjection | undefined>(() => {
|
|
66
|
+
if (!model || !editing) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { col } = parseCellIndex(editing.index);
|
|
71
|
+
const field = model.projection.view.fields[col];
|
|
72
|
+
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
73
|
+
invariant(fieldProjection);
|
|
74
|
+
return fieldProjection;
|
|
75
|
+
}, [model, editing]);
|
|
76
|
+
|
|
77
|
+
if (fieldProjection?.props.type === TypeEnum.Array) {
|
|
78
|
+
return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// For all other types, use the existing cell editor
|
|
82
|
+
return (
|
|
83
|
+
<TableCellEditor
|
|
84
|
+
model={model}
|
|
85
|
+
modals={modals}
|
|
86
|
+
onEnter={onEnter}
|
|
87
|
+
onFocus={onFocus}
|
|
88
|
+
onQuery={onQuery}
|
|
89
|
+
__gridScope={__gridScope}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
51
94
|
export const TableCellEditor = ({
|
|
52
95
|
model,
|
|
53
96
|
modals,
|
|
@@ -261,11 +304,9 @@ export const TableCellEditor = ({
|
|
|
261
304
|
}
|
|
262
305
|
}
|
|
263
306
|
|
|
264
|
-
// Return empty string if no value
|
|
265
307
|
return '';
|
|
266
308
|
}
|
|
267
309
|
|
|
268
|
-
// Default behavior for other formats
|
|
269
310
|
const value = model.getCellData(cell);
|
|
270
311
|
return value !== undefined ? String(value) : '';
|
|
271
312
|
}
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
createGapSeparator,
|
|
11
11
|
createMenuAction,
|
|
12
12
|
type MenuAction,
|
|
13
|
-
type MenuActionHandler,
|
|
14
13
|
MenuProvider,
|
|
15
14
|
ToolbarMenu,
|
|
16
15
|
useMenuActions,
|
|
@@ -27,18 +26,18 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
|
|
|
27
26
|
type TableToolbarState = Partial<{ viewDirty: boolean }>;
|
|
28
27
|
|
|
29
28
|
export type TableToolbarProps = ThemedClassName<
|
|
30
|
-
TableToolbarState & { onAction
|
|
29
|
+
TableToolbarState & { onAction: (action: TableToolbarAction) => void; attendableId?: string }
|
|
31
30
|
>;
|
|
32
31
|
|
|
33
|
-
const createTableToolbarActions = (state: TableToolbarState) => {
|
|
34
|
-
const add = createMenuAction<TableToolbarActionProperties>('add-row', {
|
|
35
|
-
type: 'add-row',
|
|
32
|
+
const createTableToolbarActions = (state: TableToolbarState, onAction: (action: TableToolbarAction) => void) => {
|
|
33
|
+
const add = createMenuAction<TableToolbarActionProperties>('add-row', () => onAction(add), {
|
|
34
|
+
type: 'add-row' as const,
|
|
36
35
|
icon: 'ph--plus--regular',
|
|
37
36
|
label: ['add row', { ns: translationKey }],
|
|
38
37
|
testId: 'table.toolbar.add-row',
|
|
39
38
|
});
|
|
40
|
-
const save = createMenuAction<TableToolbarActionProperties>('save-view', {
|
|
41
|
-
type: 'save-view',
|
|
39
|
+
const save = createMenuAction<TableToolbarActionProperties>('save-view', () => onAction(save), {
|
|
40
|
+
type: 'save-view' as const,
|
|
42
41
|
icon: 'ph--floppy-disk--regular',
|
|
43
42
|
label: ['save view label', { ns: translationKey }],
|
|
44
43
|
testId: 'table.toolbar.save-view',
|
|
@@ -46,8 +45,8 @@ const createTableToolbarActions = (state: TableToolbarState) => {
|
|
|
46
45
|
hidden: !state.viewDirty,
|
|
47
46
|
});
|
|
48
47
|
const gap = createGapSeparator();
|
|
49
|
-
const comment = createMenuAction('comment', {
|
|
50
|
-
type: 'comment',
|
|
48
|
+
const comment = createMenuAction('comment', () => onAction(comment), {
|
|
49
|
+
type: 'comment' as const,
|
|
51
50
|
icon: 'ph--chat-text--regular',
|
|
52
51
|
label: ['create comment', { ns: translationKey }],
|
|
53
52
|
testId: 'table.toolbar.comment',
|
|
@@ -61,11 +60,11 @@ const createTableToolbarActions = (state: TableToolbarState) => {
|
|
|
61
60
|
|
|
62
61
|
export const TableToolbar = ({ classNames, viewDirty, attendableId, onAction }: TableToolbarProps) => {
|
|
63
62
|
const state = useMemo(() => live<TableToolbarState>({ viewDirty }), []);
|
|
64
|
-
const actionsCreator = useCallback(() => createTableToolbarActions(state), [state]);
|
|
63
|
+
const actionsCreator = useCallback(() => createTableToolbarActions(state, onAction), [state, onAction]);
|
|
65
64
|
const menu = useMenuActions(actionsCreator);
|
|
66
65
|
|
|
67
66
|
return (
|
|
68
|
-
<MenuProvider {...menu} attendableId={attendableId}
|
|
67
|
+
<MenuProvider {...menu} attendableId={attendableId}>
|
|
69
68
|
<ToolbarMenu classNames={classNames} />
|
|
70
69
|
</MenuProvider>
|
|
71
70
|
);
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Schema } from 'effect';
|
|
5
6
|
import { useEffect } from 'react';
|
|
6
7
|
|
|
7
|
-
import { setValue, toJsonSchema,
|
|
8
|
+
import { setValue, toJsonSchema, TypeEnum, TypedObject, FormatEnum, ObjectId } from '@dxos/echo-schema';
|
|
8
9
|
import { faker } from '@dxos/random';
|
|
9
10
|
import { live, makeRef } from '@dxos/react-client/echo';
|
|
10
11
|
import { createView, type ViewProjection } from '@dxos/schema';
|
|
@@ -12,11 +13,11 @@ import { createView, type ViewProjection } from '@dxos/schema';
|
|
|
12
13
|
import { TableType } from '../types';
|
|
13
14
|
|
|
14
15
|
export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
15
|
-
id:
|
|
16
|
-
name:
|
|
17
|
-
age:
|
|
18
|
-
active:
|
|
19
|
-
netWorth:
|
|
16
|
+
id: ObjectId,
|
|
17
|
+
name: Schema.optional(Schema.String),
|
|
18
|
+
age: Schema.optional(Schema.Number),
|
|
19
|
+
active: Schema.optional(Schema.Boolean),
|
|
20
|
+
netWorth: Schema.optional(Schema.Number),
|
|
20
21
|
});
|
|
21
22
|
|
|
22
23
|
export const createTable = (schema = TestSchema) => {
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { computed } from '@preact/signals-core';
|
|
6
|
+
import { Schema } from 'effect';
|
|
6
7
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
+
import { TypedObject } from '@dxos/echo-schema';
|
|
9
10
|
import { updateCounter } from '@dxos/echo-schema/testing';
|
|
10
11
|
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
11
12
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
@@ -99,8 +100,8 @@ describe('TableModel', () => {
|
|
|
99
100
|
});
|
|
100
101
|
|
|
101
102
|
class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
102
|
-
title:
|
|
103
|
-
completed:
|
|
103
|
+
title: Schema.String,
|
|
104
|
+
completed: Schema.Boolean,
|
|
104
105
|
}) {}
|
|
105
106
|
|
|
106
107
|
const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
package/src/model/table-model.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { type ViewType, type ViewProjection } from '@dxos/schema';
|
|
|
21
21
|
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
22
22
|
import { TableSorting } from './table-sorting';
|
|
23
23
|
import { touch } from '../util';
|
|
24
|
+
import { extractTagIds } from '../util/tag';
|
|
24
25
|
|
|
25
26
|
// TODO(burdon): Use schema types.
|
|
26
27
|
export type TableRow = Record<JsonProp, any> & { id: string };
|
|
@@ -348,6 +349,22 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
348
349
|
break;
|
|
349
350
|
}
|
|
350
351
|
|
|
352
|
+
case FormatEnum.SingleSelect: {
|
|
353
|
+
const ids = extractTagIds(value);
|
|
354
|
+
if (ids && ids.length > 0) {
|
|
355
|
+
setValue(this._rows.value[rowIdx], field.path, ids[0]);
|
|
356
|
+
}
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
case FormatEnum.MultiSelect: {
|
|
361
|
+
const ids = extractTagIds(value);
|
|
362
|
+
if (ids) {
|
|
363
|
+
setValue(this._rows.value[rowIdx], field.path, ids);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
351
368
|
default: {
|
|
352
369
|
setValue(
|
|
353
370
|
this._rows.value[rowIdx],
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Schema } from 'effect';
|
|
5
6
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { TypedObject } from '@dxos/echo-schema';
|
|
8
9
|
import { live, makeRef } from '@dxos/live-object';
|
|
9
10
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
10
11
|
import { createView, ViewProjection } from '@dxos/schema';
|
|
@@ -89,8 +90,8 @@ describe('TablePresentation', () => {
|
|
|
89
90
|
});
|
|
90
91
|
|
|
91
92
|
class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
92
|
-
title:
|
|
93
|
-
completed:
|
|
93
|
+
title: Schema.String,
|
|
94
|
+
completed: Schema.Boolean,
|
|
94
95
|
}) {}
|
|
95
96
|
|
|
96
97
|
const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|