@dxos/react-ui-form 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6
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/README.md +0 -1
- package/dist/lib/browser/index.mjs +620 -583
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +620 -583
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +61 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +18 -15
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +67 -67
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +7 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +7 -5
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +61 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +61 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.d.ts +39 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +61 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +23 -24
- package/src/components/Form/Form.tsx +58 -65
- package/src/components/Form/FormField.tsx +27 -8
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +108 -104
- package/src/components/Form/fields/ArrayField.tsx +11 -8
- package/src/components/Form/fields/GeoPointField.tsx +6 -6
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +35 -17
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +10 -14
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/{ControlSection → Settings}/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +22 -20
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
- package/src/components/ViewEditor/ViewEditor.tsx +99 -70
- package/src/components/index.ts +1 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/translations.ts +62 -63
- package/src/util/index.ts +0 -1
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
|
@@ -6,14 +6,15 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
|
|
9
|
+
import { DXN, Filter, JsonSchema, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
|
|
10
|
+
import { type View } from '@dxos/echo';
|
|
10
11
|
import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
|
|
11
12
|
import { useQuery } from '@dxos/react-client/echo';
|
|
12
13
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
14
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
14
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
15
|
-
import { type ProjectionModel,
|
|
16
|
-
import { Employer, Organization, Person,
|
|
15
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
|
+
import { type ProjectionModel, ViewModel, getTypenameFromQuery } from '@dxos/schema';
|
|
17
|
+
import { Employer, Organization, Person, Pipeline } from '@dxos/types';
|
|
17
18
|
|
|
18
19
|
import { translations } from '../../translations';
|
|
19
20
|
import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
@@ -24,7 +25,7 @@ const types = [
|
|
|
24
25
|
// TODO(burdon): Get label from annotation.
|
|
25
26
|
{ value: Organization.Organization.typename, label: 'Organization' },
|
|
26
27
|
{ value: Person.Person.typename, label: 'Person' },
|
|
27
|
-
{ value:
|
|
28
|
+
{ value: Pipeline.Pipeline.typename, label: 'Project' },
|
|
28
29
|
{ value: Employer.Employer.typename, label: 'Employer' },
|
|
29
30
|
];
|
|
30
31
|
|
|
@@ -35,9 +36,9 @@ export type ViewEditorDebugObjects = {
|
|
|
35
36
|
projection: ProjectionModel;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
type
|
|
39
|
+
type DefaultStoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
|
|
39
40
|
|
|
40
|
-
const DefaultStory = (props:
|
|
41
|
+
const DefaultStory = (props: DefaultStoryProps) => {
|
|
41
42
|
const { space } = useClientStory();
|
|
42
43
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
43
44
|
const [view, setView] = useState<View.View>();
|
|
@@ -53,7 +54,7 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
53
54
|
salary: Format.Currency(),
|
|
54
55
|
}).pipe(
|
|
55
56
|
Type.object({
|
|
56
|
-
typename: 'example.
|
|
57
|
+
typename: 'com.example.type.test',
|
|
57
58
|
version: '0.1.0',
|
|
58
59
|
}),
|
|
59
60
|
);
|
|
@@ -64,16 +65,16 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
64
65
|
completed: Schema.Boolean,
|
|
65
66
|
}).pipe(
|
|
66
67
|
Type.object({
|
|
67
|
-
typename: 'example.
|
|
68
|
+
typename: 'com.example.type.alternate',
|
|
68
69
|
version: '0.1.0',
|
|
69
70
|
}),
|
|
70
71
|
);
|
|
71
72
|
|
|
72
73
|
const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
|
|
73
|
-
const view =
|
|
74
|
+
const view = ViewModel.make({
|
|
74
75
|
name: 'Test',
|
|
75
76
|
query: Query.select(Filter.type(TestSchema)),
|
|
76
|
-
jsonSchema:
|
|
77
|
+
jsonSchema: JsonSchema.toJsonSchema(TestSchema),
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
setSchema(testSchema);
|
|
@@ -89,9 +90,9 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
89
90
|
|
|
90
91
|
if (props.mode === 'tag') {
|
|
91
92
|
const queue = target && DXN.tryParse(target) ? target : undefined;
|
|
92
|
-
const query = queue ? Query.fromAst(newQuery).
|
|
93
|
-
Obj.change(view, (
|
|
94
|
-
|
|
93
|
+
const query = queue ? Query.fromAst(newQuery).from({ queues: [queue] }) : Query.fromAst(newQuery);
|
|
94
|
+
Obj.change(view, (obj) => {
|
|
95
|
+
obj.query.ast = query.ast as Mutable<typeof query.ast>;
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
const typename = getTypenameFromQuery(query.ast);
|
|
@@ -100,17 +101,17 @@ const DefaultStory = (props: StoryProps) => {
|
|
|
100
101
|
return;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
const newView =
|
|
104
|
+
const newView = ViewModel.make({
|
|
104
105
|
query,
|
|
105
106
|
jsonSchema: newSchema.jsonSchema,
|
|
106
107
|
});
|
|
107
|
-
Obj.change(view, (
|
|
108
|
-
|
|
108
|
+
Obj.change(view, (obj) => {
|
|
109
|
+
obj.projection = Obj.getSnapshot(newView).projection as Mutable<typeof obj.projection>;
|
|
109
110
|
});
|
|
110
111
|
setSchema(() => newSchema);
|
|
111
112
|
} else {
|
|
112
|
-
Obj.change(view, (
|
|
113
|
-
|
|
113
|
+
Obj.change(view, (obj) => {
|
|
114
|
+
obj.query.ast = newQuery as Mutable<typeof newQuery>;
|
|
114
115
|
});
|
|
115
116
|
schema.updateTypename(getTypenameFromQuery(newQuery));
|
|
116
117
|
}
|
|
@@ -175,7 +176,8 @@ const meta = {
|
|
|
175
176
|
space.db.add(Tag.make({ label: 'New' }));
|
|
176
177
|
},
|
|
177
178
|
}),
|
|
178
|
-
withTheme,
|
|
179
|
+
withTheme(),
|
|
180
|
+
withLayout({ layout: 'fullscreen' }),
|
|
179
181
|
],
|
|
180
182
|
parameters: {
|
|
181
183
|
layout: 'fullscreen',
|
|
@@ -6,8 +6,9 @@ import { composeStories } from '@storybook/react';
|
|
|
6
6
|
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
7
7
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
|
+
import { View } from '@dxos/echo';
|
|
9
10
|
import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
|
|
10
|
-
import { ProjectionModel
|
|
11
|
+
import { ProjectionModel } from '@dxos/schema';
|
|
11
12
|
|
|
12
13
|
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
13
14
|
|
|
@@ -89,14 +90,10 @@ describe('ViewEditor', () => {
|
|
|
89
90
|
await Default.run();
|
|
90
91
|
await waitForViewEditor();
|
|
91
92
|
|
|
92
|
-
// Click the add property button.
|
|
93
|
+
// Click the add property button. The new field is auto-expanded by handleAdd.
|
|
93
94
|
const addButton = screen.getByText('Add property');
|
|
94
95
|
fireEvent.click(addButton);
|
|
95
96
|
|
|
96
|
-
// Open the last field.
|
|
97
|
-
const buttons = screen.getAllByTestId('field.toggle');
|
|
98
|
-
fireEvent.click(buttons.at(-1)!);
|
|
99
|
-
|
|
100
97
|
// Fill out the property field.
|
|
101
98
|
const fieldInput = screen.getByPlaceholderText('Property name');
|
|
102
99
|
fireEvent.change(fieldInput, { target: { value: 'added_property' } });
|
|
@@ -9,19 +9,18 @@ import * as Option from 'effect/Option';
|
|
|
9
9
|
import * as Schema from 'effect/Schema';
|
|
10
10
|
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
|
|
11
11
|
|
|
12
|
-
import { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
|
|
12
|
+
import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
|
|
13
|
+
import { View } from '@dxos/echo';
|
|
13
14
|
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
14
15
|
import { invariant } from '@dxos/invariant';
|
|
15
|
-
import { useObject } from '@dxos/react-client/echo';
|
|
16
|
-
import {
|
|
16
|
+
import { useObject, useQuery } from '@dxos/react-client/echo';
|
|
17
|
+
import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
17
18
|
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
18
19
|
import { List } from '@dxos/react-ui-list';
|
|
19
20
|
import {
|
|
20
|
-
|
|
21
|
-
type FieldType,
|
|
21
|
+
ParentLabelAnnotation,
|
|
22
22
|
ProjectionModel,
|
|
23
23
|
VIEW_FIELD_LIMIT,
|
|
24
|
-
type View,
|
|
25
24
|
createEchoChangeCallback,
|
|
26
25
|
getTypenameFromQuery,
|
|
27
26
|
} from '@dxos/schema';
|
|
@@ -35,22 +34,20 @@ import {
|
|
|
35
34
|
type FormFieldComponentProps,
|
|
36
35
|
FormFieldLabel,
|
|
37
36
|
type FormFieldMap,
|
|
37
|
+
type FormRootProps,
|
|
38
38
|
} from '../Form';
|
|
39
39
|
|
|
40
|
-
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
41
|
-
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
42
|
-
|
|
43
40
|
export type ViewEditorProps = ThemedClassName<
|
|
44
41
|
{
|
|
45
42
|
schema: Schema.Schema.AnyNoContext;
|
|
46
43
|
view: View.View;
|
|
47
44
|
mode?: 'schema' | 'tag';
|
|
48
45
|
registry?: SchemaRegistry.SchemaRegistry;
|
|
49
|
-
readonly?: boolean;
|
|
50
46
|
showHeading?: boolean;
|
|
51
47
|
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
52
48
|
onDelete?: (fieldId: string) => void;
|
|
53
|
-
} & Pick<QueryFormProps, 'types' | 'tags'>
|
|
49
|
+
} & Pick<QueryFormProps, 'types' | 'tags'> &
|
|
50
|
+
Pick<FormRootProps<any>, 'readonly' | 'db'>
|
|
54
51
|
>;
|
|
55
52
|
|
|
56
53
|
/**
|
|
@@ -64,6 +61,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
64
61
|
view,
|
|
65
62
|
mode = 'schema',
|
|
66
63
|
registry,
|
|
64
|
+
db,
|
|
67
65
|
readonly,
|
|
68
66
|
showHeading = false,
|
|
69
67
|
types,
|
|
@@ -96,18 +94,31 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
96
94
|
}, [atomRegistry, schema, view]);
|
|
97
95
|
|
|
98
96
|
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
99
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
100
97
|
|
|
101
98
|
const queueTarget = Match.value(view.query.ast).pipe(
|
|
102
|
-
Match.when({ type: '
|
|
103
|
-
|
|
99
|
+
Match.when({ type: 'from' }, ({ from }) => {
|
|
100
|
+
if (from._tag !== 'scope') {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return Option.fromNullable(from.scope.queues).pipe(
|
|
104
104
|
Option.flatMap((queues) => Array.head(queues)),
|
|
105
|
+
Option.map(String),
|
|
105
106
|
Option.getOrUndefined,
|
|
106
107
|
);
|
|
107
108
|
}),
|
|
108
109
|
Match.orElse(() => undefined),
|
|
109
110
|
);
|
|
110
111
|
|
|
112
|
+
const feeds = useQuery(db, Filter.type(Feed.Feed));
|
|
113
|
+
|
|
114
|
+
const targetRef = useMemo(() => {
|
|
115
|
+
if (!queueTarget) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
|
|
119
|
+
return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
|
|
120
|
+
}, [queueTarget, feeds]);
|
|
121
|
+
|
|
111
122
|
const viewSchema = useMemo(() => {
|
|
112
123
|
const base = Schema.Struct({
|
|
113
124
|
query:
|
|
@@ -119,7 +130,12 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
119
130
|
if (mode === 'tag') {
|
|
120
131
|
return Schema.Struct({
|
|
121
132
|
...base.fields,
|
|
122
|
-
|
|
133
|
+
// TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
|
|
134
|
+
target: Ref.Ref(Feed.Feed).pipe(
|
|
135
|
+
Schema.annotations({ title: 'Target Feed' }),
|
|
136
|
+
ParentLabelAnnotation.set(true),
|
|
137
|
+
Schema.optional,
|
|
138
|
+
),
|
|
123
139
|
}).pipe(Schema.mutable);
|
|
124
140
|
}
|
|
125
141
|
|
|
@@ -131,9 +147,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
131
147
|
const viewValues = useMemo(
|
|
132
148
|
() => ({
|
|
133
149
|
query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
|
|
134
|
-
target:
|
|
150
|
+
target: targetRef,
|
|
135
151
|
}),
|
|
136
|
-
[mode, view.query.ast,
|
|
152
|
+
[mode, view.query.ast, targetRef],
|
|
137
153
|
);
|
|
138
154
|
|
|
139
155
|
const fieldMap = useMemo<FormFieldMap | undefined>(
|
|
@@ -141,49 +157,51 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
141
157
|
[mode, types, tags],
|
|
142
158
|
);
|
|
143
159
|
|
|
144
|
-
// TODO(burdon): Check if mutable.
|
|
145
|
-
const handleAdd = useCallback(() => {
|
|
146
|
-
invariant(!readonly);
|
|
147
|
-
const field = projectionModel.createFieldProjection();
|
|
148
|
-
setExpandedField(field.id);
|
|
149
|
-
}, [projectionModel, readonly]);
|
|
150
|
-
|
|
151
160
|
const handleUpdate = useCallback(
|
|
152
161
|
(values: any) => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
162
|
+
const targetValue = values.target;
|
|
163
|
+
let queueDxn: string | undefined;
|
|
164
|
+
|
|
165
|
+
if (Ref.isRef(targetValue)) {
|
|
166
|
+
const feedDxn = targetValue.dxn.toString();
|
|
167
|
+
const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
|
|
168
|
+
if (feed) {
|
|
169
|
+
queueDxn = Feed.getQueueDxn(feed)?.toString();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TODO(wittjosiah): Deep-clone the AST to plain JS or ECHO proxy arrays become objects with numeric keys.
|
|
174
|
+
const query =
|
|
175
|
+
mode === 'schema'
|
|
176
|
+
? Query.select(Filter.typename(values.query)).ast
|
|
177
|
+
: JSON.parse(JSON.stringify(values.query));
|
|
178
|
+
onQueryChanged?.(query, queueDxn);
|
|
157
179
|
},
|
|
158
|
-
[onQueryChanged,
|
|
180
|
+
[onQueryChanged, mode, feeds],
|
|
159
181
|
);
|
|
160
182
|
|
|
161
183
|
const handleDelete = useCallback(
|
|
162
184
|
(fieldId: string) => {
|
|
163
185
|
invariant(!readonly);
|
|
164
|
-
if (fieldId === expandedField) {
|
|
165
|
-
setExpandedField(undefined);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
186
|
onDelete?.(fieldId);
|
|
169
187
|
},
|
|
170
|
-
[
|
|
188
|
+
[onDelete, readonly],
|
|
171
189
|
);
|
|
172
190
|
|
|
173
191
|
return (
|
|
174
192
|
<div role='none' className={mx(classNames)}>
|
|
175
193
|
{/* If readonly is set, then the callout is not needed. */}
|
|
176
194
|
{schemaReadonly && !readonly && (
|
|
177
|
-
<
|
|
178
|
-
<
|
|
179
|
-
</
|
|
195
|
+
<Message.Root valence='info' classNames='my-form-padding'>
|
|
196
|
+
<Message.Title>{t('system-schema.description')}</Message.Title>
|
|
197
|
+
</Message.Root>
|
|
180
198
|
)}
|
|
181
199
|
|
|
182
200
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
183
|
-
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap}
|
|
201
|
+
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
|
|
184
202
|
<Form.FieldSet />
|
|
185
203
|
|
|
186
|
-
<FormFieldLabel label={t('fields
|
|
204
|
+
<FormFieldLabel label={t('fields.label')} asChild />
|
|
187
205
|
<FieldList
|
|
188
206
|
schema={schema}
|
|
189
207
|
view={view}
|
|
@@ -193,19 +211,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
193
211
|
onDelete={handleDelete}
|
|
194
212
|
/>
|
|
195
213
|
</Form.Root>
|
|
196
|
-
|
|
197
|
-
{!readonly && !expandedField && (
|
|
198
|
-
<div role='none' className='mlb-cardSpacingBlock'>
|
|
199
|
-
<IconButton
|
|
200
|
-
icon='ph--plus--regular'
|
|
201
|
-
label={t('add property button label')}
|
|
202
|
-
onClick={readonly ? undefined : handleAdd}
|
|
203
|
-
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
204
|
-
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
205
|
-
classNames='is-full'
|
|
206
|
-
/>
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
214
|
</div>
|
|
210
215
|
);
|
|
211
216
|
},
|
|
@@ -239,10 +244,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
239
244
|
return model;
|
|
240
245
|
}, [atomRegistry, schema, view]);
|
|
241
246
|
|
|
242
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
247
|
+
const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
|
|
248
|
+
|
|
249
|
+
const handleAdd = useCallback(() => {
|
|
250
|
+
invariant(!readonly);
|
|
251
|
+
const field = projectionModel.createFieldProjection();
|
|
252
|
+
setExpandedField(field.id);
|
|
253
|
+
}, [projectionModel, readonly]);
|
|
243
254
|
|
|
244
255
|
const handleToggleField = useCallback(
|
|
245
|
-
(field: FieldType) => {
|
|
256
|
+
(field: View.FieldType) => {
|
|
246
257
|
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
247
258
|
},
|
|
248
259
|
[readonly],
|
|
@@ -263,12 +274,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
263
274
|
const handleMove = useCallback(
|
|
264
275
|
(fromIndex: number, toIndex: number) => {
|
|
265
276
|
invariant(!readonly);
|
|
266
|
-
Obj.change(view, (
|
|
277
|
+
Obj.change(view, (obj) => {
|
|
267
278
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
268
|
-
const fields = [...
|
|
279
|
+
const fields = [...obj.projection.fields];
|
|
269
280
|
const [moved] = fields.splice(fromIndex, 1);
|
|
270
281
|
fields.splice(toIndex, 0, moved);
|
|
271
|
-
|
|
282
|
+
obj.projection.fields = fields;
|
|
272
283
|
});
|
|
273
284
|
},
|
|
274
285
|
[view, readonly],
|
|
@@ -297,32 +308,39 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
return (
|
|
300
|
-
<List.Root<FieldType>
|
|
301
|
-
items={viewSnapshot.projection.fields as FieldType[]}
|
|
302
|
-
isItem={Schema.is(FieldSchema)}
|
|
311
|
+
<List.Root<View.FieldType>
|
|
312
|
+
items={viewSnapshot.projection.fields as View.FieldType[]}
|
|
313
|
+
isItem={Schema.is(View.FieldSchema)}
|
|
303
314
|
getId={(field) => field.id}
|
|
304
315
|
onMove={readonly ? undefined : handleMove}
|
|
305
316
|
readonly={readonly}
|
|
306
317
|
>
|
|
307
318
|
{({ items: fields }) => (
|
|
308
319
|
<>
|
|
309
|
-
{showHeading && <h3 className='text-sm'>{t('field
|
|
310
|
-
<div role='list' className=
|
|
320
|
+
{showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
|
|
321
|
+
<div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
|
|
311
322
|
{fields?.map((field) => {
|
|
312
323
|
const hidden = field.visible === false;
|
|
313
324
|
return (
|
|
314
|
-
<List.Item<FieldType>
|
|
325
|
+
<List.Item<View.FieldType>
|
|
315
326
|
key={field.id}
|
|
316
327
|
item={field}
|
|
317
|
-
classNames={
|
|
328
|
+
classNames={'grid grid-cols-subgrid col-span-5'}
|
|
318
329
|
aria-expanded={expandedField === field.id}
|
|
319
330
|
>
|
|
320
|
-
<div
|
|
331
|
+
<div
|
|
332
|
+
role='none'
|
|
333
|
+
className={mx(
|
|
334
|
+
subtleHover,
|
|
335
|
+
'grid grid-cols-subgrid col-span-5',
|
|
336
|
+
'rounded-xs cursor-pointer min-h-10',
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
321
339
|
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
322
340
|
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
323
341
|
{field.path}
|
|
324
342
|
</List.ItemTitle>
|
|
325
|
-
<List.
|
|
343
|
+
<List.ItemIconButton
|
|
326
344
|
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
327
345
|
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
328
346
|
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
@@ -333,16 +351,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
333
351
|
{!readonly && (
|
|
334
352
|
<>
|
|
335
353
|
<List.ItemDeleteButton
|
|
336
|
-
label={t('delete
|
|
354
|
+
label={t('delete-field.label')}
|
|
337
355
|
autoHide={false}
|
|
338
356
|
disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
|
|
339
357
|
onClick={() => handleDelete(field.id)}
|
|
340
358
|
data-testid='field.delete'
|
|
341
359
|
/>
|
|
342
|
-
<
|
|
360
|
+
<List.ItemIconButton
|
|
343
361
|
iconOnly
|
|
344
362
|
variant='ghost'
|
|
345
|
-
label={t('toggle
|
|
363
|
+
label={t('toggle-expand.label', { ns: osTranslations })}
|
|
346
364
|
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
347
365
|
onClick={() => handleToggleField(field)}
|
|
348
366
|
data-testid='field.toggle'
|
|
@@ -351,7 +369,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
351
369
|
)}
|
|
352
370
|
</div>
|
|
353
371
|
{expandedField === field.id && !readonly && (
|
|
354
|
-
<div role='none' className='col-span-5
|
|
372
|
+
<div role='none' className='col-span-5 mt-1 mb-1 border border-separator rounded-md'>
|
|
355
373
|
<FieldEditor
|
|
356
374
|
readonly={readonly || schemaReadonly}
|
|
357
375
|
registry={registry}
|
|
@@ -365,6 +383,17 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
365
383
|
);
|
|
366
384
|
})}
|
|
367
385
|
</div>
|
|
386
|
+
{!readonly && !expandedField && (
|
|
387
|
+
<div role='none' className='my-form-padding'>
|
|
388
|
+
<IconButton
|
|
389
|
+
icon='ph--plus--regular'
|
|
390
|
+
label={t('add-property-button.label')}
|
|
391
|
+
onClick={handleAdd}
|
|
392
|
+
disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
393
|
+
classNames='w-full'
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
368
397
|
</>
|
|
369
398
|
)}
|
|
370
399
|
</List.Root>
|
package/src/components/index.ts
CHANGED
|
@@ -4,19 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { textBlockWidth } from '@dxos/ui-theme';
|
|
7
|
+
import { Column, type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { Json } from '@dxos/react-ui-syntax-highlighter';
|
|
9
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
10
|
|
|
12
11
|
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
13
12
|
|
|
14
13
|
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
14
|
return (
|
|
16
|
-
<div className='
|
|
17
|
-
<TestPanel classNames={['
|
|
15
|
+
<div className='h-full w-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
|
|
16
|
+
<TestPanel classNames={['dx-container', classNames]}>
|
|
17
|
+
<Column.Root classNames='dx-container' gutter='sm'>
|
|
18
|
+
{children}
|
|
19
|
+
</Column.Root>
|
|
20
|
+
</TestPanel>
|
|
18
21
|
<TestPanel classNames={'overflow-hidden'}>
|
|
19
|
-
<
|
|
22
|
+
<Json.Root data={json}>
|
|
23
|
+
<Json.Content classNames='h-full'>
|
|
24
|
+
<Json.Filter />
|
|
25
|
+
<Json.Data testId='debug' classNames='text-xs' />
|
|
26
|
+
</Json.Content>
|
|
27
|
+
</Json.Root>
|
|
20
28
|
</TestPanel>
|
|
21
29
|
</div>
|
|
22
30
|
);
|
|
@@ -25,7 +33,11 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
|
25
33
|
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
26
34
|
|
|
27
35
|
const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
28
|
-
return
|
|
36
|
+
return (
|
|
37
|
+
<div role='none' className={mx(['dx-document h-full bg-modal-surface rounded-md', classNames])}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
29
41
|
};
|
|
30
42
|
|
|
31
43
|
// Symbol for accessing debug objects in tests.
|
|
@@ -7,22 +7,13 @@ import type * as Schema from 'effect/Schema';
|
|
|
7
7
|
import type * as SchemaAST from 'effect/SchemaAST';
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
-
import { Obj } from '@dxos/echo';
|
|
11
10
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
12
|
-
import {
|
|
13
|
-
type JsonPath,
|
|
14
|
-
createJsonPath,
|
|
15
|
-
fromEffectValidationPath,
|
|
16
|
-
getValue as getValue$,
|
|
17
|
-
setValue as setValue$,
|
|
18
|
-
} from '@dxos/effect';
|
|
11
|
+
import { type JsonPath, createJsonPath, fromEffectValidationPath, getValue as getValue$ } from '@dxos/effect';
|
|
19
12
|
import { log } from '@dxos/log';
|
|
20
13
|
import { useDefaultValue } from '@dxos/react-ui';
|
|
21
14
|
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
22
15
|
import { type MaybePromise } from '@dxos/util';
|
|
23
16
|
|
|
24
|
-
import { setValueEchoAware } from '../util';
|
|
25
|
-
|
|
26
17
|
export type FormFieldStatus = {
|
|
27
18
|
status?: 'error';
|
|
28
19
|
error?: string;
|
|
@@ -139,11 +130,15 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
139
130
|
prop: valuesProp,
|
|
140
131
|
defaultProp: defaultValues,
|
|
141
132
|
onChange: () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
// Only reset when controlled: parent passed new values.
|
|
134
|
+
// Uncontrolled updates come from setValues in onValueChange.
|
|
135
|
+
if (valuesProp !== undefined) {
|
|
136
|
+
setValues(valuesProp ?? defaultValues);
|
|
137
|
+
setChanged({});
|
|
138
|
+
setTouched({});
|
|
139
|
+
setErrors({});
|
|
140
|
+
setSaving(false);
|
|
141
|
+
}
|
|
147
142
|
},
|
|
148
143
|
});
|
|
149
144
|
// TODO(wittjosiah): Upgrade @radix-ui/react-use-controllable-state.
|
|
@@ -259,6 +254,7 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
259
254
|
log('onValueChange', { path, value });
|
|
260
255
|
|
|
261
256
|
const jsonPath = createJsonPath(path);
|
|
257
|
+
const pathArray = path;
|
|
262
258
|
let parsedValue = value as any;
|
|
263
259
|
try {
|
|
264
260
|
if (type._tag === 'NumberKeyword') {
|
|
@@ -269,24 +265,25 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
269
265
|
parsedValue = undefined;
|
|
270
266
|
}
|
|
271
267
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
const newValues = Obj.isObject(values)
|
|
275
|
-
? (setValueEchoAware(values, jsonPath, parsedValue), values)
|
|
276
|
-
: { ...setValue$(values, jsonPath, parsedValue) };
|
|
277
|
-
setValues(newValues);
|
|
268
|
+
// Create merged values for validation and callback. Form never mutates; parent applies via onValuesChanged.
|
|
269
|
+
const newValues = mergeAtPath(values, pathArray as (string | number)[], parsedValue) as Partial<T>;
|
|
278
270
|
|
|
279
271
|
// TODO(burdon): Check value has changed from original.
|
|
280
272
|
const newChanged = { [jsonPath]: true };
|
|
281
|
-
setChanged({ ...
|
|
273
|
+
setChanged((prev) => ({ ...prev, ...newChanged }));
|
|
282
274
|
|
|
283
275
|
// Validate.
|
|
284
276
|
const isValid = validate(newValues);
|
|
285
277
|
|
|
286
|
-
//
|
|
278
|
+
// Notify parent; parent is responsible for applying the change.
|
|
287
279
|
onValuesChanged?.(newValues, { isValid, changed: newChanged });
|
|
280
|
+
|
|
281
|
+
// For uncontrolled mode, update internal state. Controlled mode relies on parent to pass new values.
|
|
282
|
+
if (valuesProp === undefined) {
|
|
283
|
+
setValues(newValues);
|
|
284
|
+
}
|
|
288
285
|
},
|
|
289
|
-
[values,
|
|
286
|
+
[values, validate, onValuesChanged, valuesProp],
|
|
290
287
|
);
|
|
291
288
|
|
|
292
289
|
const onBlur = useCallback(
|
|
@@ -347,6 +344,27 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
347
344
|
);
|
|
348
345
|
};
|
|
349
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Creates a new object with value at path. Does not mutate source.
|
|
349
|
+
* Preserves arrays when path segments are numeric indices.
|
|
350
|
+
*/
|
|
351
|
+
const mergeAtPath = (obj: any, path: readonly (string | number)[], value: any): any => {
|
|
352
|
+
if (path.length === 0) {
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const [head, ...rest] = path;
|
|
357
|
+
const child = rest.length === 0 ? value : mergeAtPath(obj?.[head], rest, value);
|
|
358
|
+
|
|
359
|
+
if (Array.isArray(obj)) {
|
|
360
|
+
const copy = [...obj];
|
|
361
|
+
copy[head as number] = child;
|
|
362
|
+
return copy;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { ...obj, [head]: child };
|
|
366
|
+
};
|
|
367
|
+
|
|
350
368
|
const flatMap = (errors: ValidationError[]) => {
|
|
351
369
|
return errors.reduce(
|
|
352
370
|
(result, { path, message }) => {
|