@dxos/react-ui-table 0.8.4-main.ae835ea → 0.8.4-main.bc674ce
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-G7MGPBSR.mjs → chunk-I5IFW7UF.mjs} +633 -663
- package/dist/lib/browser/chunk-I5IFW7UF.mjs.map +7 -0
- package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
- package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1131 -999
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +29 -41
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +3 -3
- package/dist/lib/node-esm/{chunk-LD5JEBA6.mjs → chunk-DDJKYJFY.mjs} +633 -662
- package/dist/lib/node-esm/chunk-DDJKYJFY.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
- package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1131 -999
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +29 -41
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +3 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +4 -5
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +4 -18
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +12 -9
- 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 +8 -8
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +71 -5
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +4 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -15
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +5 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useTableModel.d.ts +8 -8
- 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 +102 -23
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +3 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +1 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +4 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +31 -0
- package/dist/types/src/types/Table.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +13 -11
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/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 -53
- package/src/components/Table/ColumnActionsMenu.tsx +10 -6
- package/src/components/Table/ColumnSettings.tsx +6 -6
- package/src/components/Table/DynamicTable.stories.tsx +12 -13
- package/src/components/Table/DynamicTable.tsx +21 -14
- package/src/components/Table/Relations.stories.tsx +35 -39
- package/src/components/Table/RowActionsMenu.tsx +24 -15
- package/src/components/Table/Table.stories.tsx +71 -75
- package/src/components/Table/Table.tsx +335 -305
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +110 -133
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +16 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +31 -31
- package/src/components/TableToolbar/TableToolbar.tsx +44 -31
- package/src/components/testing.ts +38 -12
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +39 -0
- package/src/hooks/useTableModel.ts +35 -33
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +82 -54
- package/src/model/table-model.ts +428 -163
- package/src/model/table-presentation.test.ts +59 -49
- package/src/model/table-presentation.ts +31 -26
- package/src/playwright/TableManager.ts +5 -5
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +8 -5
- package/src/testing/useTestTableModel.ts +44 -37
- package/src/translations.ts +3 -3
- package/src/types/Table.ts +90 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.ts +39 -27
- 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-4K7JPALT.mjs +0 -66
- package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
- package/dist/lib/browser/chunk-G7MGPBSR.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LD5JEBA6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -52
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -28
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -225
- package/src/types/schema.ts +0 -73
|
@@ -12,9 +12,9 @@ export type CellValidationMessageProps = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
export const CellValidationMessage = ({
|
|
15
|
+
__gridScope,
|
|
15
16
|
validationError,
|
|
16
17
|
variant = 'error',
|
|
17
|
-
__gridScope,
|
|
18
18
|
}: GridScopedProps<CellValidationMessageProps>) => {
|
|
19
19
|
const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
|
|
20
20
|
|
|
@@ -2,123 +2,48 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import * as Schema from 'effect/Schema';
|
|
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 {
|
|
12
|
-
import { type
|
|
13
|
-
import { getSpace } from '@dxos/react-client/echo';
|
|
14
|
-
import { Popover } from '@dxos/react-ui';
|
|
15
|
-
import { Form, type FormProps } from '@dxos/react-ui-form';
|
|
11
|
+
import { type Label, Popover } from '@dxos/react-ui';
|
|
12
|
+
import { Form, type FormRootProps, type RefFieldProps } from '@dxos/react-ui-form';
|
|
16
13
|
import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
17
14
|
import { type FieldProjection } from '@dxos/schema';
|
|
18
15
|
import { getDeep, isTruthy, setDeep } from '@dxos/util';
|
|
19
16
|
|
|
20
|
-
import { type ModalController, type TableModel } from '../../model';
|
|
17
|
+
import { type ModalController, type TableModel, type TableRow } from '../../model';
|
|
21
18
|
import { translationKey } from '../../translations';
|
|
22
19
|
import { narrowSchema } from '../../util';
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
|
|
25
22
|
|
|
26
|
-
export type
|
|
27
|
-
|
|
23
|
+
export type OnCreateHandler = (schema: Type.Entity.Any, values: any) => Parameters<typeof Ref.make>[0];
|
|
24
|
+
|
|
25
|
+
export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
|
|
26
|
+
__gridScope: any;
|
|
27
|
+
schema?: T;
|
|
28
28
|
model?: TableModel;
|
|
29
|
-
|
|
29
|
+
fieldProjection: FieldProjection;
|
|
30
|
+
modals?: ModalController; // TODO(burdon): Not used. Remove?
|
|
30
31
|
onSave?: () => void;
|
|
31
32
|
onCreate?: OnCreateHandler;
|
|
32
|
-
|
|
33
|
-
modals?: ModalController;
|
|
34
|
-
__gridScope: any;
|
|
35
|
-
} & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
|
|
36
|
-
|
|
37
|
-
const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
|
|
33
|
+
} & Omit<FormRootProps<any>, 'values' | 'schema' | 'onCreate'>;
|
|
38
34
|
|
|
39
|
-
export const FormCellEditor = ({
|
|
40
|
-
|
|
41
|
-
model,
|
|
35
|
+
export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
36
|
+
__gridScope,
|
|
42
37
|
schema,
|
|
38
|
+
model,
|
|
39
|
+
fieldProjection,
|
|
43
40
|
onSave,
|
|
44
|
-
client,
|
|
45
|
-
modals,
|
|
46
41
|
onCreate,
|
|
47
|
-
__gridScope,
|
|
48
42
|
...formProps
|
|
49
|
-
}: FormCellEditorProps) => {
|
|
50
|
-
const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
|
|
51
|
-
const [editing, setLocalEditing] = useState(false);
|
|
43
|
+
}: FormCellEditorProps<T>) => {
|
|
52
44
|
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
(typeAnnotation: Pick<TypeAnnotation, 'typename'>) => {
|
|
56
|
-
const space = getSpace(model!.view);
|
|
57
|
-
invariant(space);
|
|
58
|
-
|
|
59
|
-
let schema;
|
|
60
|
-
if (client) {
|
|
61
|
-
schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
62
|
-
}
|
|
63
|
-
if (!schema) {
|
|
64
|
-
schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
65
|
-
}
|
|
66
|
-
return { space, schema };
|
|
67
|
-
},
|
|
68
|
-
[client, model],
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const handleQueryRefOptions = useCallback(
|
|
72
|
-
async (typeAnnotation: TypeAnnotation) => {
|
|
73
|
-
const { schema, space } = getSchema(typeAnnotation);
|
|
74
|
-
if (model && schema && space) {
|
|
75
|
-
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
76
|
-
return objects
|
|
77
|
-
.map((obj) => {
|
|
78
|
-
return {
|
|
79
|
-
dxn: Obj.getDXN(obj),
|
|
80
|
-
label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
|
|
81
|
-
};
|
|
82
|
-
})
|
|
83
|
-
.filter(isTruthy);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return [];
|
|
87
|
-
},
|
|
88
|
-
[client, model],
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const originalRow = useMemo(() => {
|
|
92
|
-
if (model && contextEditing) {
|
|
93
|
-
const cell = parseCellIndex(contextEditing.index);
|
|
94
|
-
|
|
95
|
-
// Check if this is a draft cell and get the appropriate row data
|
|
96
|
-
if (model.isDraftCell(cell)) {
|
|
97
|
-
const draftRow = model.draftRows.value[cell.row];
|
|
98
|
-
invariant(draftRow);
|
|
99
|
-
return draftRow.data;
|
|
100
|
-
} else {
|
|
101
|
-
const row = model.getRowAt(cell.row);
|
|
102
|
-
invariant(row);
|
|
103
|
-
return row;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return undefined;
|
|
108
|
-
}, [model, contextEditing]);
|
|
109
|
-
|
|
110
|
-
const handleSave = useCallback(
|
|
111
|
-
(values: any) => {
|
|
112
|
-
const path = fieldProjection.field.path;
|
|
113
|
-
const value = getDeep(values, [path]);
|
|
114
|
-
setDeep(originalRow, [path], value);
|
|
115
|
-
contextEditing?.cellElement?.focus();
|
|
116
|
-
setEditing(null);
|
|
117
|
-
setLocalEditing(false);
|
|
118
|
-
onSave?.();
|
|
119
|
-
},
|
|
120
|
-
[fieldProjection.field.path, onSave, contextEditing, originalRow],
|
|
121
|
-
);
|
|
45
|
+
const { id: _gridId, editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
|
|
46
|
+
const [editing, setLocalEditing] = useState(false);
|
|
122
47
|
|
|
123
48
|
useEffect(() => {
|
|
124
49
|
if (contextEditing && contextEditing.cellElement) {
|
|
@@ -132,15 +57,35 @@ export const FormCellEditor = ({
|
|
|
132
57
|
}
|
|
133
58
|
}, [contextEditing]);
|
|
134
59
|
|
|
135
|
-
const narrowedSchema = useMemo(() => {
|
|
60
|
+
const narrowedSchema = useMemo<Schema.Schema.AnyNoContext | undefined>(() => {
|
|
136
61
|
if (!schema) {
|
|
137
62
|
return undefined;
|
|
138
63
|
}
|
|
64
|
+
|
|
139
65
|
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
140
66
|
}, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
|
|
141
67
|
|
|
142
|
-
|
|
143
|
-
|
|
68
|
+
const originalRow = useMemo<TableRow | undefined>(() => {
|
|
69
|
+
if (model && contextEditing) {
|
|
70
|
+
// Check if this is a draft cell and get the appropriate row data
|
|
71
|
+
const cell = parseCellIndex(contextEditing.index);
|
|
72
|
+
if (model.isDraftCell(cell)) {
|
|
73
|
+
const draftRow = model.getDraftRows()[cell.row];
|
|
74
|
+
invariant(draftRow);
|
|
75
|
+
return draftRow.data;
|
|
76
|
+
} else {
|
|
77
|
+
const row = model.getRowAt(cell.row);
|
|
78
|
+
invariant(row);
|
|
79
|
+
return row;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
}, [model, contextEditing]);
|
|
85
|
+
|
|
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]);
|
|
144
89
|
|
|
145
90
|
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
146
91
|
if (nextOpen === false) {
|
|
@@ -150,27 +95,41 @@ export const FormCellEditor = ({
|
|
|
150
95
|
setLocalEditing(nextOpen);
|
|
151
96
|
}, []);
|
|
152
97
|
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
if (!refSchema) {
|
|
159
|
-
return null;
|
|
98
|
+
const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
|
|
99
|
+
(values) => {
|
|
100
|
+
if (!originalRow) {
|
|
101
|
+
return;
|
|
160
102
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
103
|
+
const path = fieldProjection.field.path;
|
|
104
|
+
const value = getDeep(values, [path]);
|
|
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
|
+
}
|
|
113
|
+
contextEditing?.cellElement?.focus();
|
|
114
|
+
setEditing(null);
|
|
115
|
+
setLocalEditing(false);
|
|
116
|
+
onSave?.();
|
|
117
|
+
},
|
|
118
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow, model],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
|
|
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 {
|
|
174
133
|
setDeep(originalRow, [path], ref);
|
|
175
134
|
}
|
|
176
135
|
}
|
|
@@ -179,7 +138,20 @@ export const FormCellEditor = ({
|
|
|
179
138
|
setLocalEditing(false);
|
|
180
139
|
onSave?.();
|
|
181
140
|
},
|
|
182
|
-
[fieldProjection.field.path, onSave, contextEditing, originalRow,
|
|
141
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow, onCreate, model],
|
|
142
|
+
);
|
|
143
|
+
|
|
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],
|
|
183
155
|
);
|
|
184
156
|
|
|
185
157
|
if (!editing) {
|
|
@@ -193,21 +165,26 @@ export const FormCellEditor = ({
|
|
|
193
165
|
<Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
|
|
194
166
|
<Popover.Arrow />
|
|
195
167
|
<Popover.Viewport>
|
|
196
|
-
<Form
|
|
168
|
+
<Form.Root
|
|
169
|
+
{...formProps}
|
|
197
170
|
autoFocus
|
|
171
|
+
schema={narrowedSchema}
|
|
198
172
|
values={formValues}
|
|
199
|
-
|
|
173
|
+
createInitialValuePath={fieldProjection.field.referencePath}
|
|
174
|
+
createOptionIcon='ph--plus--regular'
|
|
175
|
+
createOptionLabel={createOptionLabel}
|
|
176
|
+
db={model?.db}
|
|
177
|
+
getOptions={getOptions}
|
|
178
|
+
onCreate={handleCreate}
|
|
200
179
|
onSave={handleSave}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
})}
|
|
210
|
-
/>
|
|
180
|
+
>
|
|
181
|
+
<Form.Viewport>
|
|
182
|
+
<Form.Content>
|
|
183
|
+
<Form.FieldSet />
|
|
184
|
+
<Form.Actions />
|
|
185
|
+
</Form.Content>
|
|
186
|
+
</Form.Viewport>
|
|
187
|
+
</Form.Root>
|
|
211
188
|
</Popover.Viewport>
|
|
212
189
|
</Popover.Content>
|
|
213
190
|
</Popover.Portal>
|
|
@@ -8,9 +8,10 @@ import React from 'react';
|
|
|
8
8
|
import { Obj } from '@dxos/echo';
|
|
9
9
|
import { faker } from '@dxos/random';
|
|
10
10
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
11
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
12
|
import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
|
|
13
|
-
import {
|
|
13
|
+
import { View } from '@dxos/schema';
|
|
14
|
+
import { Task } from '@dxos/types';
|
|
14
15
|
|
|
15
16
|
import { useTestTableModel } from '../../testing';
|
|
16
17
|
import { translations } from '../../translations';
|
|
@@ -22,17 +23,18 @@ type StoryProps = {
|
|
|
22
23
|
editing: GridEditing;
|
|
23
24
|
};
|
|
24
25
|
|
|
26
|
+
// TODO(burdon): Broken layout.
|
|
25
27
|
const DefaultStory = ({ editing }: StoryProps) => {
|
|
26
|
-
const { model,
|
|
28
|
+
const { model, table } = useTestTableModel();
|
|
27
29
|
|
|
28
|
-
if (!model || !
|
|
30
|
+
if (!model || !table) {
|
|
29
31
|
return <div />;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
return (
|
|
33
|
-
<div className='
|
|
35
|
+
<div className='border border-separator' style={{ height: defaultRowSize }}>
|
|
34
36
|
<Grid.Root id='test' editing={editing}>
|
|
35
|
-
<TableCellEditor model={model} schema={
|
|
37
|
+
<TableCellEditor model={model} schema={Task.Task} />
|
|
36
38
|
</Grid.Root>
|
|
37
39
|
</div>
|
|
38
40
|
);
|
|
@@ -44,16 +46,18 @@ const meta = {
|
|
|
44
46
|
render: DefaultStory,
|
|
45
47
|
decorators: [
|
|
46
48
|
withTheme,
|
|
49
|
+
withLayout({ layout: 'column' }),
|
|
47
50
|
withClientProvider({
|
|
48
|
-
types: [
|
|
51
|
+
types: [View.View, Task.Task, Table.Table],
|
|
49
52
|
createIdentity: true,
|
|
50
53
|
createSpace: true,
|
|
51
|
-
onCreateSpace: async ({
|
|
52
|
-
const { view } = await
|
|
53
|
-
|
|
54
|
+
onCreateSpace: async ({ space }) => {
|
|
55
|
+
const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
|
|
56
|
+
const table = Table.make({ view, jsonSchema });
|
|
57
|
+
space.db.add(table);
|
|
54
58
|
Array.from({ length: 10 }).forEach(() => {
|
|
55
59
|
space.db.add(
|
|
56
|
-
Obj.make(
|
|
60
|
+
Obj.make(Task.Task, {
|
|
57
61
|
title: faker.person.fullName(),
|
|
58
62
|
status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
|
|
59
63
|
description: faker.lorem.sentence(),
|
|
@@ -64,8 +68,8 @@ const meta = {
|
|
|
64
68
|
}),
|
|
65
69
|
],
|
|
66
70
|
parameters: {
|
|
71
|
+
layout: 'fullscreen',
|
|
67
72
|
translations,
|
|
68
|
-
layout: 'centered',
|
|
69
73
|
},
|
|
70
74
|
} satisfies Meta<typeof DefaultStory>;
|
|
71
75
|
|
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Completion } from '@codemirror/autocomplete';
|
|
6
6
|
import { EditorView } from '@codemirror/view';
|
|
7
|
-
import type * as Schema from 'effect/Schema';
|
|
8
7
|
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
9
8
|
|
|
10
9
|
import { debounce } from '@dxos/async';
|
|
11
|
-
import type
|
|
12
|
-
import {
|
|
10
|
+
import { type Type } from '@dxos/echo';
|
|
11
|
+
import { Format, TypeEnum } from '@dxos/echo/internal';
|
|
13
12
|
import { invariant } from '@dxos/invariant';
|
|
14
13
|
import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
|
|
15
14
|
import {
|
|
@@ -30,34 +29,37 @@ import { type ModalController, type TableModel } from '../../model';
|
|
|
30
29
|
import { CellValidationMessage } from './CellValidationMessage';
|
|
31
30
|
import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
|
|
32
31
|
|
|
32
|
+
const editorSlots = {
|
|
33
|
+
scroll: {
|
|
34
|
+
className: '!plb-[--dx-grid-cell-editor-padding-block]',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Option to create new object/value.
|
|
35
40
|
*/
|
|
36
|
-
export type QueryResult = Pick<Completion, 'label'> & {
|
|
41
|
+
export type QueryResult = Pick<Completion, 'label'> & {
|
|
42
|
+
data: any;
|
|
43
|
+
};
|
|
37
44
|
|
|
38
|
-
export type TableCellEditorProps = {
|
|
45
|
+
export type TableCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
|
|
46
|
+
schema?: T;
|
|
39
47
|
model?: TableModel;
|
|
40
48
|
modals?: ModalController;
|
|
41
|
-
schema?: Schema.Schema.AnyNoContext;
|
|
42
49
|
onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
|
|
43
50
|
onCreate?: OnCreateHandler;
|
|
44
51
|
onSave?: () => void;
|
|
45
|
-
client?: Client;
|
|
46
52
|
};
|
|
47
53
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export const TableValueEditor = ({
|
|
54
|
+
export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
|
|
55
|
+
__gridScope,
|
|
56
|
+
schema,
|
|
52
57
|
model,
|
|
53
58
|
modals,
|
|
54
|
-
schema,
|
|
55
59
|
onFocus,
|
|
56
60
|
onSave,
|
|
57
61
|
onCreate,
|
|
58
|
-
|
|
59
|
-
__gridScope,
|
|
60
|
-
}: GridScopedProps<TableCellEditorProps>) => {
|
|
62
|
+
}: GridScopedProps<TableCellEditorProps<T>>) => {
|
|
61
63
|
const { editing } = useGridContext('TableValueEditor', __gridScope);
|
|
62
64
|
|
|
63
65
|
const fieldProjection = useMemo<FieldProjection | undefined>(() => {
|
|
@@ -66,7 +68,7 @@ export const TableValueEditor = ({
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
const { col } = parseCellIndex(editing.index);
|
|
69
|
-
const field = model.projection.
|
|
71
|
+
const field = model.projection.getFields()[col];
|
|
70
72
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
71
73
|
invariant(fieldProjection);
|
|
72
74
|
return fieldProjection;
|
|
@@ -74,36 +76,33 @@ export const TableValueEditor = ({
|
|
|
74
76
|
|
|
75
77
|
if (
|
|
76
78
|
fieldProjection?.props.type === TypeEnum.Array ||
|
|
77
|
-
fieldProjection?.props.format ===
|
|
78
|
-
fieldProjection?.props.format ===
|
|
79
|
-
// TODO(thure): Support `
|
|
79
|
+
fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
|
|
80
|
+
fieldProjection?.props.format === Format.TypeFormat.Ref
|
|
81
|
+
// TODO(thure): Support `Format.TypeFormat.MultiSelect`
|
|
80
82
|
) {
|
|
81
83
|
return (
|
|
82
|
-
<FormCellEditor
|
|
83
|
-
fieldProjection={fieldProjection}
|
|
84
|
-
model={model}
|
|
85
|
-
schema={schema}
|
|
84
|
+
<FormCellEditor<T>
|
|
86
85
|
__gridScope={__gridScope}
|
|
86
|
+
schema={schema}
|
|
87
|
+
model={model}
|
|
88
|
+
fieldProjection={fieldProjection}
|
|
89
|
+
modals={modals}
|
|
87
90
|
onSave={onSave}
|
|
88
91
|
onCreate={onCreate}
|
|
89
|
-
client={client}
|
|
90
|
-
modals={modals}
|
|
91
92
|
/>
|
|
92
93
|
);
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
// For all other types, use the existing cell editor
|
|
96
|
+
// For all other types, use the existing cell editor.
|
|
96
97
|
return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
|
|
97
98
|
};
|
|
98
99
|
|
|
99
|
-
const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
|
|
100
|
-
|
|
101
100
|
export const TableCellEditor = ({
|
|
101
|
+
__gridScope,
|
|
102
102
|
model,
|
|
103
103
|
modals,
|
|
104
104
|
onFocus,
|
|
105
105
|
onSave,
|
|
106
|
-
__gridScope,
|
|
107
106
|
}: GridScopedProps<TableCellEditorProps>) => {
|
|
108
107
|
const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
|
|
109
108
|
const suppressNextBlur = useRef(false);
|
|
@@ -116,12 +115,13 @@ export const TableCellEditor = ({
|
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
const { col } = parseCellIndex(editing.index);
|
|
119
|
-
const field = model.projection.
|
|
118
|
+
const field = model.projection.getFields()[col];
|
|
120
119
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
121
120
|
invariant(fieldProjection);
|
|
122
121
|
return fieldProjection;
|
|
123
122
|
}, [model, editing]);
|
|
124
123
|
|
|
124
|
+
// TOOD(burdon): Attach to event handler?
|
|
125
125
|
const handleEnter = useCallback(
|
|
126
126
|
async (value: any) => {
|
|
127
127
|
if (!model || !editing) {
|
|
@@ -138,7 +138,7 @@ export const TableCellEditor = ({
|
|
|
138
138
|
setEditing(null);
|
|
139
139
|
onSave?.();
|
|
140
140
|
} else {
|
|
141
|
-
setValidationError(
|
|
141
|
+
setValidationError(validationResult.error);
|
|
142
142
|
setValidationVariant('error');
|
|
143
143
|
}
|
|
144
144
|
},
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
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 { rxFromSignal } from '@dxos/react-ui-menu';
|
|
22
20
|
|
|
23
21
|
import { translationKey } from '../../translations';
|
|
24
22
|
|
|
@@ -34,41 +32,49 @@ 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
|
-
customActions?:
|
|
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
|
-
|
|
51
|
-
onAdd
|
|
52
|
-
onSave
|
|
53
|
-
customActions?:
|
|
48
|
+
stateAtom: Atom.Atom<TableToolbarState>;
|
|
49
|
+
onAdd?: () => void;
|
|
50
|
+
onSave?: () => void;
|
|
51
|
+
customActions?: Atom.Atom<ActionGraphProps>;
|
|
54
52
|
}) =>
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
53
|
+
Atom.make((get) => {
|
|
54
|
+
const nodes: ActionGraphNodes = [];
|
|
55
|
+
if (onAdd) {
|
|
56
|
+
const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
|
|
57
|
+
type: 'add-row' as const,
|
|
58
|
+
icon: 'ph--plus--regular',
|
|
59
|
+
label: ['add row label', { ns: translationKey }],
|
|
60
|
+
testId: 'table.toolbar.add-row',
|
|
61
|
+
});
|
|
62
|
+
nodes.push(add);
|
|
63
|
+
}
|
|
64
|
+
if (onSave) {
|
|
65
|
+
const state = get(stateAtom);
|
|
66
|
+
const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
|
|
67
|
+
type: 'save-view' as const,
|
|
68
|
+
icon: 'ph--floppy-disk--regular',
|
|
69
|
+
label: ['save view label', { ns: translationKey }],
|
|
70
|
+
testId: 'table.toolbar.save-view',
|
|
71
|
+
iconOnly: false,
|
|
72
|
+
hidden: !state.viewDirty,
|
|
73
|
+
});
|
|
74
|
+
nodes.push(save);
|
|
75
|
+
}
|
|
70
76
|
const gap = createGapSeparator();
|
|
71
|
-
|
|
77
|
+
nodes.push(...gap.nodes);
|
|
72
78
|
const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
|
|
73
79
|
if (customActions) {
|
|
74
80
|
const custom = get(customActions);
|
|
@@ -90,10 +96,17 @@ export const TableToolbar = ({
|
|
|
90
96
|
onSave,
|
|
91
97
|
customActions,
|
|
92
98
|
}: TableToolbarProps) => {
|
|
93
|
-
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
|
+
|
|
94
107
|
const actionsCreator = useMemo(
|
|
95
|
-
() => createTableToolbarActions({
|
|
96
|
-
[
|
|
108
|
+
() => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
|
|
109
|
+
[stateAtom, onAdd, onSave, customActions],
|
|
97
110
|
);
|
|
98
111
|
const menu = useMenuActions(actionsCreator);
|
|
99
112
|
|