@dxos/react-ui-form 0.8.4-main.66e292d → 0.8.4-main.69d29f4
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/index.mjs +2212 -2284
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2212 -2284
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +7 -4
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +6 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +9 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +81 -27
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +53 -92
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +37 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/{custom/SelectOptionsInput.stories.d.ts → fields/SelectOptionField.stories.d.ts} +6 -3
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +14 -9
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +5 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +0 -2
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +6 -3
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/echo.d.ts +6 -0
- package/dist/types/src/util/echo.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +1 -2
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +49 -44
- package/src/components/ControlSection/ControlSection.tsx +8 -8
- package/src/components/FieldEditor/FieldEditor.stories.tsx +42 -21
- package/src/components/FieldEditor/FieldEditor.tsx +72 -75
- package/src/components/Form/Form-old.stories.tsx +314 -0
- package/src/components/Form/Form.stories.tsx +146 -246
- package/src/components/Form/Form.tsx +320 -110
- package/src/components/Form/FormField.tsx +286 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +174 -0
- package/src/components/Form/FormFieldSet.tsx +125 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +151 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +181 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +17 -14
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +20 -14
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +15 -8
- package/src/components/ObjectPicker/ObjectPicker.tsx +76 -62
- package/src/components/ViewEditor/ViewEditor.stories.tsx +29 -25
- package/src/components/ViewEditor/ViewEditor.test.tsx +6 -2
- package/src/components/ViewEditor/ViewEditor.tsx +223 -154
- package/src/components/testing.tsx +9 -7
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +362 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +8 -6
- package/src/util/echo.ts +25 -0
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +7 -13
- package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -63
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -29
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -22
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -29
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/Form/ArrayField.tsx +0 -127
- package/src/components/Form/Defaults.tsx +0 -205
- package/src/components/Form/FormActions.tsx +0 -59
- package/src/components/Form/FormContent.tsx +0 -362
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -54
- package/src/components/Form/RefField.tsx +0 -283
- package/src/components/Form/custom/GeoPointInput.tsx +0 -80
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -34
- package/src/hooks/useForm.ts +0 -296
- package/src/hooks/useQueryRefOptions.ts +0 -62
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -12
|
@@ -7,16 +7,16 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
|
|
10
|
-
import { type EchoSchema, Format } from '@dxos/echo/internal';
|
|
10
|
+
import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
|
|
11
11
|
import { useQuery } from '@dxos/react-client/echo';
|
|
12
|
-
import {
|
|
12
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
13
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
14
14
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
15
15
|
import { type ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
|
|
16
16
|
import { Employer, Organization, Person, Project } from '@dxos/types';
|
|
17
17
|
|
|
18
18
|
import { translations } from '../../translations';
|
|
19
|
-
import { TestLayout,
|
|
19
|
+
import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
20
20
|
|
|
21
21
|
import { ViewEditor, type ViewEditorProps } from './ViewEditor';
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ const types = [
|
|
|
24
24
|
// TODO(burdon): Get label from annotation.
|
|
25
25
|
{ value: Organization.Organization.typename, label: 'Organization' },
|
|
26
26
|
{ value: Person.Person.typename, label: 'Person' },
|
|
27
|
-
{ value:
|
|
27
|
+
{ value: Project.Project.typename, label: 'Project' },
|
|
28
28
|
{ value: Employer.Employer.typename, label: 'Employer' },
|
|
29
29
|
];
|
|
30
30
|
|
|
@@ -38,12 +38,12 @@ export type ViewEditorDebugObjects = {
|
|
|
38
38
|
type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
|
|
39
39
|
|
|
40
40
|
const DefaultStory = (props: StoryProps) => {
|
|
41
|
-
const { space } =
|
|
41
|
+
const { space } = useClientStory();
|
|
42
42
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
43
43
|
const [view, setView] = useState<View.View>();
|
|
44
44
|
const projectionRef = useRef<ProjectionModel>(null);
|
|
45
45
|
|
|
46
|
-
const tags = useQuery(space, Filter.type(Tag.Tag));
|
|
46
|
+
const tags = useQuery(space?.db, Filter.type(Tag.Tag));
|
|
47
47
|
|
|
48
48
|
useAsyncEffect(async () => {
|
|
49
49
|
if (space) {
|
|
@@ -52,7 +52,7 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
52
52
|
email: Format.Email,
|
|
53
53
|
salary: Format.Currency(),
|
|
54
54
|
}).pipe(
|
|
55
|
-
Type.
|
|
55
|
+
Type.object({
|
|
56
56
|
typename: 'example.com/type/Test',
|
|
57
57
|
version: '0.1.0',
|
|
58
58
|
}),
|
|
@@ -63,7 +63,7 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
63
63
|
description: Schema.String,
|
|
64
64
|
completed: Schema.Boolean,
|
|
65
65
|
}).pipe(
|
|
66
|
-
Type.
|
|
66
|
+
Type.object({
|
|
67
67
|
typename: 'example.com/type/Alternate',
|
|
68
68
|
version: '0.1.0',
|
|
69
69
|
}),
|
|
@@ -90,7 +90,9 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
90
90
|
if (props.mode === 'tag') {
|
|
91
91
|
const queue = target && DXN.tryParse(target) ? target : undefined;
|
|
92
92
|
const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
|
|
93
|
-
view
|
|
93
|
+
Obj.change(view, (v) => {
|
|
94
|
+
v.query.ast = query.ast as Mutable<typeof query.ast>;
|
|
95
|
+
});
|
|
94
96
|
|
|
95
97
|
const typename = getTypenameFromQuery(query.ast);
|
|
96
98
|
const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
|
|
@@ -102,10 +104,14 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
102
104
|
query,
|
|
103
105
|
jsonSchema: newSchema.jsonSchema,
|
|
104
106
|
});
|
|
105
|
-
|
|
107
|
+
Obj.change(view, (v) => {
|
|
108
|
+
v.projection = Obj.getSnapshot(newView).projection as Mutable<typeof v.projection>;
|
|
109
|
+
});
|
|
106
110
|
setSchema(() => newSchema);
|
|
107
111
|
} else {
|
|
108
|
-
view
|
|
112
|
+
Obj.change(view, (v) => {
|
|
113
|
+
v.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
114
|
+
});
|
|
109
115
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
110
116
|
}
|
|
111
117
|
},
|
|
@@ -140,20 +146,18 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
140
146
|
|
|
141
147
|
return (
|
|
142
148
|
<TestLayout json={json}>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
/>
|
|
156
|
-
</TestPanel>
|
|
149
|
+
<ViewEditor
|
|
150
|
+
ref={projectionRef}
|
|
151
|
+
schema={schema}
|
|
152
|
+
view={view}
|
|
153
|
+
registry={space?.db.schemaRegistry}
|
|
154
|
+
mode={props.mode}
|
|
155
|
+
readonly={props.readonly}
|
|
156
|
+
types={types}
|
|
157
|
+
tags={tags}
|
|
158
|
+
onQueryChanged={updateViewQuery}
|
|
159
|
+
onDelete={handleDelete}
|
|
160
|
+
/>
|
|
157
161
|
</TestLayout>
|
|
158
162
|
);
|
|
159
163
|
};
|
|
@@ -54,7 +54,7 @@ describe('ViewEditor', () => {
|
|
|
54
54
|
const nameField = screen.getByText('name');
|
|
55
55
|
fireEvent.click(nameField);
|
|
56
56
|
|
|
57
|
-
const fieldInput = screen.getByPlaceholderText('
|
|
57
|
+
const fieldInput = screen.getByPlaceholderText('Property name');
|
|
58
58
|
fireEvent.change(fieldInput, { target: { value: 'new_property' } });
|
|
59
59
|
|
|
60
60
|
fireEvent.click(screen.getByTestId('save-button'));
|
|
@@ -93,8 +93,12 @@ describe('ViewEditor', () => {
|
|
|
93
93
|
const addButton = screen.getByText('Add property');
|
|
94
94
|
fireEvent.click(addButton);
|
|
95
95
|
|
|
96
|
+
// Open the last field.
|
|
97
|
+
const buttons = screen.getAllByTestId('field.toggle');
|
|
98
|
+
fireEvent.click(buttons.at(-1)!);
|
|
99
|
+
|
|
96
100
|
// Fill out the property field.
|
|
97
|
-
const fieldInput = screen.getByPlaceholderText('
|
|
101
|
+
const fieldInput = screen.getByPlaceholderText('Property name');
|
|
98
102
|
fireEvent.change(fieldInput, { target: { value: 'added_property' } });
|
|
99
103
|
|
|
100
104
|
// Click the format combo box and select the first option.
|
|
@@ -2,33 +2,40 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
5
6
|
import * as Array from 'effect/Array';
|
|
6
7
|
import * as Match from 'effect/Match';
|
|
7
8
|
import * as Option from 'effect/Option';
|
|
8
9
|
import * as Schema from 'effect/Schema';
|
|
9
|
-
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useState } from 'react';
|
|
10
|
+
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
|
|
10
11
|
|
|
11
|
-
import { QueryAST } from '@dxos/echo';
|
|
12
|
-
import { EchoSchema,
|
|
13
|
-
import { Filter, Query, type SchemaRegistry } from '@dxos/echo-db';
|
|
12
|
+
import { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
|
|
13
|
+
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
14
14
|
import { invariant } from '@dxos/invariant';
|
|
15
|
+
import { useObject } from '@dxos/react-client/echo';
|
|
15
16
|
import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
16
17
|
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
17
18
|
import { List } from '@dxos/react-ui-list';
|
|
18
|
-
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
19
|
-
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
20
19
|
import {
|
|
21
20
|
FieldSchema,
|
|
22
21
|
type FieldType,
|
|
23
22
|
ProjectionModel,
|
|
24
23
|
VIEW_FIELD_LIMIT,
|
|
25
24
|
type View,
|
|
25
|
+
createEchoChangeCallback,
|
|
26
26
|
getTypenameFromQuery,
|
|
27
27
|
} from '@dxos/schema';
|
|
28
|
+
import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
|
|
28
29
|
|
|
29
30
|
import { translationKey } from '../../translations';
|
|
30
31
|
import { FieldEditor } from '../FieldEditor';
|
|
31
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
Form,
|
|
34
|
+
type FormFieldComponent,
|
|
35
|
+
type FormFieldComponentProps,
|
|
36
|
+
FormFieldLabel,
|
|
37
|
+
type FormFieldMap,
|
|
38
|
+
} from '../Form';
|
|
32
39
|
|
|
33
40
|
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
34
41
|
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
@@ -38,13 +45,12 @@ export type ViewEditorProps = ThemedClassName<
|
|
|
38
45
|
schema: Schema.Schema.AnyNoContext;
|
|
39
46
|
view: View.View;
|
|
40
47
|
mode?: 'schema' | 'tag';
|
|
41
|
-
registry?: SchemaRegistry;
|
|
48
|
+
registry?: SchemaRegistry.SchemaRegistry;
|
|
42
49
|
readonly?: boolean;
|
|
43
50
|
showHeading?: boolean;
|
|
44
51
|
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
45
52
|
onDelete?: (fieldId: string) => void;
|
|
46
|
-
} & Pick<
|
|
47
|
-
Pick<QueryFormProps, 'types' | 'tags'>
|
|
53
|
+
} & Pick<QueryFormProps, 'types' | 'tags'>
|
|
48
54
|
>;
|
|
49
55
|
|
|
50
56
|
/**
|
|
@@ -64,17 +70,31 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
64
70
|
tags,
|
|
65
71
|
onQueryChanged,
|
|
66
72
|
onDelete,
|
|
67
|
-
outerSpacing = true,
|
|
68
73
|
},
|
|
69
74
|
forwardedRef,
|
|
70
75
|
) => {
|
|
76
|
+
const atomRegistry = useContext(RegistryContext);
|
|
71
77
|
const schemaReadonly = !isMutable(schema);
|
|
72
78
|
const { t } = useTranslation(translationKey);
|
|
79
|
+
|
|
73
80
|
const projectionModel = useMemo(() => {
|
|
74
81
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
75
82
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
|
|
84
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
85
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
86
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
87
|
+
|
|
88
|
+
const model = new ProjectionModel({
|
|
89
|
+
registry: atomRegistry,
|
|
90
|
+
view,
|
|
91
|
+
baseSchema: jsonSchema,
|
|
92
|
+
change,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return model;
|
|
96
|
+
}, [atomRegistry, schema, view]);
|
|
97
|
+
|
|
78
98
|
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
79
99
|
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
80
100
|
|
|
@@ -116,20 +136,17 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
116
136
|
[mode, view.query.ast, queueTarget],
|
|
117
137
|
);
|
|
118
138
|
|
|
119
|
-
const
|
|
120
|
-
(
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
[readonly],
|
|
139
|
+
const fieldMap = useMemo<FormFieldMap | undefined>(
|
|
140
|
+
() => (mode === 'tag' ? customFields({ types, tags }) : undefined),
|
|
141
|
+
[mode, types, tags],
|
|
124
142
|
);
|
|
125
143
|
|
|
126
|
-
// TODO(burdon): Check if mutable
|
|
127
|
-
|
|
144
|
+
// TODO(burdon): Check if mutable.
|
|
128
145
|
const handleAdd = useCallback(() => {
|
|
129
146
|
invariant(!readonly);
|
|
130
147
|
const field = projectionModel.createFieldProjection();
|
|
131
148
|
setExpandedField(field.id);
|
|
132
|
-
}, [
|
|
149
|
+
}, [projectionModel, readonly]);
|
|
133
150
|
|
|
134
151
|
const handleUpdate = useCallback(
|
|
135
152
|
(values: any) => {
|
|
@@ -153,143 +170,32 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
153
170
|
[expandedField, onDelete, readonly],
|
|
154
171
|
);
|
|
155
172
|
|
|
156
|
-
const handleMove = useCallback(
|
|
157
|
-
(fromIndex: number, toIndex: number) => {
|
|
158
|
-
invariant(!readonly);
|
|
159
|
-
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
160
|
-
const fields = [...view.projection.fields];
|
|
161
|
-
const [moved] = fields.splice(fromIndex, 1);
|
|
162
|
-
fields.splice(toIndex, 0, moved);
|
|
163
|
-
view.projection.fields = fields;
|
|
164
|
-
},
|
|
165
|
-
[view.projection.fields, readonly],
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
169
|
-
|
|
170
|
-
const handleHide = useCallback(
|
|
171
|
-
(fieldId: string) => {
|
|
172
|
-
setExpandedField(undefined);
|
|
173
|
-
projectionModel.hideFieldProjection(fieldId);
|
|
174
|
-
},
|
|
175
|
-
[projectionModel],
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
const handleShow = useCallback(
|
|
179
|
-
(property: string) => {
|
|
180
|
-
setExpandedField(undefined);
|
|
181
|
-
projectionModel.showFieldProjection(property as JsonProp);
|
|
182
|
-
},
|
|
183
|
-
[projectionModel],
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
const custom = useMemo(() => (mode === 'tag' ? customFields({ types, tags }) : undefined), [types, tags, mode]);
|
|
187
|
-
|
|
188
173
|
return (
|
|
189
174
|
<div role='none' className={mx(classNames)}>
|
|
190
|
-
{/* If
|
|
175
|
+
{/* If readonly is set, then the callout is not needed. */}
|
|
191
176
|
{schemaReadonly && !readonly && (
|
|
192
|
-
<Callout.Root valence='info' classNames=
|
|
177
|
+
<Callout.Root valence='info' classNames='mlb-cardSpacingBlock'>
|
|
193
178
|
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
194
179
|
</Callout.Root>
|
|
195
180
|
)}
|
|
196
181
|
|
|
197
182
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
/>
|
|
207
|
-
|
|
208
|
-
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
209
|
-
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
210
|
-
|
|
211
|
-
<List.Root<FieldType>
|
|
212
|
-
items={view.projection.fields}
|
|
213
|
-
isItem={Schema.is(FieldSchema)}
|
|
214
|
-
getId={(field) => field.id}
|
|
215
|
-
onMove={readonly ? undefined : handleMove}
|
|
183
|
+
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} autoSave onSave={handleUpdate}>
|
|
184
|
+
<Form.FieldSet />
|
|
185
|
+
|
|
186
|
+
<FormFieldLabel label={t('fields label')} asChild />
|
|
187
|
+
<FieldList
|
|
188
|
+
schema={schema}
|
|
189
|
+
view={view}
|
|
190
|
+
registry={registry}
|
|
216
191
|
readonly={readonly}
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
<div role='list' className={listGrid}>
|
|
222
|
-
{fields?.map((field) => {
|
|
223
|
-
const hidden = field.visible === false;
|
|
224
|
-
return (
|
|
225
|
-
<List.Item<FieldType>
|
|
226
|
-
key={field.id}
|
|
227
|
-
item={field}
|
|
228
|
-
classNames={listItemGrid}
|
|
229
|
-
aria-expanded={expandedField === field.id}
|
|
230
|
-
>
|
|
231
|
-
<div
|
|
232
|
-
role='none'
|
|
233
|
-
className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}
|
|
234
|
-
>
|
|
235
|
-
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
236
|
-
<List.ItemTitle
|
|
237
|
-
classNames={hidden && 'text-subdued'}
|
|
238
|
-
onClick={() => handleToggleField(field)}
|
|
239
|
-
>
|
|
240
|
-
{field.path}
|
|
241
|
-
</List.ItemTitle>
|
|
242
|
-
<List.ItemButton
|
|
243
|
-
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
244
|
-
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
245
|
-
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
246
|
-
autoHide={false}
|
|
247
|
-
disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
|
|
248
|
-
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
249
|
-
/>
|
|
250
|
-
{!readonly && (
|
|
251
|
-
<>
|
|
252
|
-
<List.ItemDeleteButton
|
|
253
|
-
label={t('delete field label')}
|
|
254
|
-
autoHide={false}
|
|
255
|
-
disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
|
|
256
|
-
onClick={() => handleDelete(field.id)}
|
|
257
|
-
data-testid='field.delete'
|
|
258
|
-
/>
|
|
259
|
-
<IconButton
|
|
260
|
-
iconOnly
|
|
261
|
-
variant='ghost'
|
|
262
|
-
label={t('toggle expand label', { ns: 'os' })}
|
|
263
|
-
icon={
|
|
264
|
-
expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'
|
|
265
|
-
}
|
|
266
|
-
onClick={() => handleToggleField(field)}
|
|
267
|
-
/>
|
|
268
|
-
</>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
{expandedField === field.id && !readonly && (
|
|
272
|
-
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
273
|
-
<FieldEditor
|
|
274
|
-
readonly={readonly || schemaReadonly ? 'disabled-input' : false}
|
|
275
|
-
projection={projectionModel}
|
|
276
|
-
field={field}
|
|
277
|
-
registry={registry}
|
|
278
|
-
onSave={handleClose}
|
|
279
|
-
/>
|
|
280
|
-
</div>
|
|
281
|
-
)}
|
|
282
|
-
</List.Item>
|
|
283
|
-
);
|
|
284
|
-
})}
|
|
285
|
-
</div>
|
|
286
|
-
</>
|
|
287
|
-
)}
|
|
288
|
-
</List.Root>
|
|
289
|
-
</div>
|
|
192
|
+
showHeading={showHeading}
|
|
193
|
+
onDelete={handleDelete}
|
|
194
|
+
/>
|
|
195
|
+
</Form.Root>
|
|
290
196
|
|
|
291
197
|
{!readonly && !expandedField && (
|
|
292
|
-
<div role='none' className=
|
|
198
|
+
<div role='none' className='mlb-cardSpacingBlock'>
|
|
293
199
|
<IconButton
|
|
294
200
|
icon='ph--plus--regular'
|
|
295
201
|
label={t('add property button label')}
|
|
@@ -305,17 +211,180 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
305
211
|
},
|
|
306
212
|
);
|
|
307
213
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
214
|
+
type FieldListProps = Pick<ViewEditorProps, 'schema' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
|
|
215
|
+
|
|
216
|
+
const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
|
|
217
|
+
const atomRegistry = useContext(RegistryContext);
|
|
218
|
+
const schemaReadonly = !isMutable(schema);
|
|
219
|
+
const { t } = useTranslation(translationKey);
|
|
220
|
+
|
|
221
|
+
// Subscribe to view changes for reactivity.
|
|
222
|
+
const [viewSnapshot] = useObject(view);
|
|
223
|
+
|
|
224
|
+
const projectionModel = useMemo(() => {
|
|
225
|
+
// Use reactive and mutable version of json schema when schema is mutable.
|
|
226
|
+
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
227
|
+
|
|
228
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
229
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
230
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
231
|
+
|
|
232
|
+
const model = new ProjectionModel({
|
|
233
|
+
registry: atomRegistry,
|
|
234
|
+
view,
|
|
235
|
+
baseSchema: jsonSchema,
|
|
236
|
+
change,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return model;
|
|
240
|
+
}, [atomRegistry, schema, view]);
|
|
241
|
+
|
|
242
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
243
|
+
|
|
244
|
+
const handleToggleField = useCallback(
|
|
245
|
+
(field: FieldType) => {
|
|
246
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
247
|
+
},
|
|
248
|
+
[readonly],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const handleDelete = useCallback(
|
|
252
|
+
(fieldId: string) => {
|
|
253
|
+
invariant(!readonly);
|
|
254
|
+
if (fieldId === expandedField) {
|
|
255
|
+
setExpandedField(undefined);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
onDelete?.(fieldId);
|
|
259
|
+
},
|
|
260
|
+
[expandedField, onDelete, readonly],
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const handleMove = useCallback(
|
|
264
|
+
(fromIndex: number, toIndex: number) => {
|
|
265
|
+
invariant(!readonly);
|
|
266
|
+
Obj.change(view, (v) => {
|
|
267
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
268
|
+
const fields = [...v.projection.fields];
|
|
269
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
270
|
+
fields.splice(toIndex, 0, moved);
|
|
271
|
+
v.projection.fields = fields;
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
[view, readonly],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
278
|
+
|
|
279
|
+
const handleHide = useCallback(
|
|
280
|
+
(fieldId: string) => {
|
|
281
|
+
setExpandedField(undefined);
|
|
282
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
283
|
+
},
|
|
284
|
+
[projectionModel],
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const handleShow = useCallback(
|
|
288
|
+
(property: string) => {
|
|
289
|
+
setExpandedField(undefined);
|
|
290
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
291
|
+
},
|
|
292
|
+
[projectionModel],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (!viewSnapshot) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<List.Root<FieldType>
|
|
301
|
+
items={viewSnapshot.projection.fields as FieldType[]}
|
|
302
|
+
isItem={Schema.is(FieldSchema)}
|
|
303
|
+
getId={(field) => field.id}
|
|
304
|
+
onMove={readonly ? undefined : handleMove}
|
|
305
|
+
readonly={readonly}
|
|
306
|
+
>
|
|
307
|
+
{({ items: fields }) => (
|
|
308
|
+
<>
|
|
309
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
310
|
+
<div role='list' className={listGrid}>
|
|
311
|
+
{fields?.map((field) => {
|
|
312
|
+
const hidden = field.visible === false;
|
|
313
|
+
return (
|
|
314
|
+
<List.Item<FieldType>
|
|
315
|
+
key={field.id}
|
|
316
|
+
item={field}
|
|
317
|
+
classNames={listItemGrid}
|
|
318
|
+
aria-expanded={expandedField === field.id}
|
|
319
|
+
>
|
|
320
|
+
<div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
|
|
321
|
+
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
322
|
+
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
323
|
+
{field.path}
|
|
324
|
+
</List.ItemTitle>
|
|
325
|
+
<List.ItemButton
|
|
326
|
+
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
327
|
+
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
328
|
+
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
329
|
+
autoHide={false}
|
|
330
|
+
disabled={readonly || (!hidden && projectionModel.getFields().length <= 1)}
|
|
331
|
+
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
332
|
+
/>
|
|
333
|
+
{!readonly && (
|
|
334
|
+
<>
|
|
335
|
+
<List.ItemDeleteButton
|
|
336
|
+
label={t('delete field label')}
|
|
337
|
+
autoHide={false}
|
|
338
|
+
disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
|
|
339
|
+
onClick={() => handleDelete(field.id)}
|
|
340
|
+
data-testid='field.delete'
|
|
341
|
+
/>
|
|
342
|
+
<IconButton
|
|
343
|
+
iconOnly
|
|
344
|
+
variant='ghost'
|
|
345
|
+
label={t('toggle expand label', { ns: osTranslations })}
|
|
346
|
+
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
347
|
+
onClick={() => handleToggleField(field)}
|
|
348
|
+
data-testid='field.toggle'
|
|
349
|
+
/>
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
{expandedField === field.id && !readonly && (
|
|
354
|
+
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
355
|
+
<FieldEditor
|
|
356
|
+
readonly={readonly || schemaReadonly}
|
|
357
|
+
registry={registry}
|
|
358
|
+
projection={projectionModel}
|
|
359
|
+
field={field}
|
|
360
|
+
onSave={handleClose}
|
|
361
|
+
/>
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
</List.Item>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</div>
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
</List.Root>
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const customFields = ({
|
|
375
|
+
types,
|
|
376
|
+
tags,
|
|
377
|
+
}: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, FormFieldComponent> => ({
|
|
378
|
+
query: ({ type, readonly, label, getValue, onValueChange }: FormFieldComponentProps) => {
|
|
379
|
+
const handleChange = useCallback<NonNullable<QueryFormProps['onChange']>>(
|
|
380
|
+
(query) => onValueChange(type, query.ast),
|
|
381
|
+
[onValueChange],
|
|
313
382
|
);
|
|
314
383
|
|
|
315
384
|
return (
|
|
316
385
|
<Input.Root>
|
|
317
|
-
<
|
|
318
|
-
<QueryForm initialQuery={
|
|
386
|
+
<FormFieldLabel readonly={readonly} label={label} />
|
|
387
|
+
<QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
|
|
319
388
|
</Input.Root>
|
|
320
389
|
);
|
|
321
390
|
},
|
|
@@ -5,25 +5,27 @@
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import { Card } from '@dxos/react-ui-
|
|
8
|
+
import { Card } from '@dxos/react-ui-mosaic';
|
|
9
9
|
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
10
|
-
import {
|
|
10
|
+
import { textBlockWidth } from '@dxos/ui-theme';
|
|
11
11
|
|
|
12
12
|
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
13
13
|
|
|
14
14
|
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
15
|
return (
|
|
16
|
-
<div className='is-full bs-full grid grid-cols-[1fr_1fr] gap-
|
|
17
|
-
<
|
|
18
|
-
<
|
|
16
|
+
<div className='is-full bs-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
|
|
17
|
+
<TestPanel classNames={['flex flex-col bs-full overflow-y-auto', classNames]}>{children}</TestPanel>
|
|
18
|
+
<TestPanel classNames={'overflow-hidden'}>
|
|
19
|
+
<JsonFilter testId='debug' data={json} classNames='bs-full text-xs' />
|
|
20
|
+
</TestPanel>
|
|
19
21
|
</div>
|
|
20
22
|
);
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
return <Card.
|
|
27
|
+
const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
28
|
+
return <Card.Root classNames={['bs-full overflow-y-auto', textBlockWidth, classNames]}>{children}</Card.Root>;
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// Symbol for accessing debug objects in tests.
|
package/src/hooks/index.ts
CHANGED