@dxos/react-ui-form 0.8.4-main.fd6878d → 0.8.4-main.fffef41
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 +806 -572
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +806 -572
- 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 +80 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +1 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +75 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +5 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +6 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +128 -32
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -10
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +2 -2
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +5 -2
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +14 -4
- package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +75 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +81 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +20 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +79 -8
- 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 +6 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +2 -2
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +9 -3
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +11 -7
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -1
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -36
- package/src/components/ControlSection/ControlSection.stories.tsx +10 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +10 -11
- package/src/components/FieldEditor/FieldEditor.tsx +12 -14
- package/src/components/Form/ArrayField.tsx +6 -6
- package/src/components/Form/Defaults.tsx +21 -20
- package/src/components/Form/Form.stories.tsx +132 -97
- package/src/components/Form/Form.tsx +27 -14
- package/src/components/Form/FormActions.tsx +2 -2
- package/src/components/Form/FormContent.tsx +166 -45
- package/src/components/Form/FormContext.tsx +3 -3
- package/src/components/Form/Input.tsx +5 -2
- package/src/components/Form/RefField.tsx +107 -38
- package/src/components/Form/custom/GeoPointInput.tsx +28 -20
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -10
- package/src/components/Form/custom/SelectOptionsInput.tsx +10 -10
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +123 -49
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -11
- package/src/components/ViewEditor/ViewEditor.tsx +281 -233
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +7 -4
- package/src/hooks/useQueryRefOptions.ts +15 -6
- package/src/hooks/useRefQueryLookupHandler.ts +3 -3
- package/src/translations.ts +13 -8
- package/src/util/field.ts +9 -19
- package/src/util/formatting.ts +1 -1
- package/src/util/refs.ts +7 -7
- package/src/util/schema.ts +2 -4
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/src/components/deprecated/Form.stories.tsx +0 -63
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
|
@@ -2,284 +2,332 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import * as Array from 'effect/Array';
|
|
6
|
+
import * as Match from 'effect/Match';
|
|
7
|
+
import * as Option from 'effect/Option';
|
|
8
|
+
import * as Schema from 'effect/Schema';
|
|
9
|
+
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useState } from 'react';
|
|
7
10
|
|
|
8
|
-
import {
|
|
9
|
-
import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo
|
|
11
|
+
import { QueryAST } from '@dxos/echo';
|
|
12
|
+
import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
13
|
+
import { Filter, Query, type SchemaRegistry } from '@dxos/echo-db';
|
|
10
14
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
15
|
+
import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
16
|
+
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
12
17
|
import { List } from '@dxos/react-ui-list';
|
|
13
18
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
14
19
|
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
15
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
FieldSchema,
|
|
22
|
+
type FieldType,
|
|
23
|
+
ProjectionModel,
|
|
24
|
+
VIEW_FIELD_LIMIT,
|
|
25
|
+
type View,
|
|
26
|
+
getTypenameFromQuery,
|
|
27
|
+
} from '@dxos/schema';
|
|
16
28
|
|
|
17
29
|
import { translationKey } from '../../translations';
|
|
18
30
|
import { FieldEditor } from '../FieldEditor';
|
|
19
|
-
import { Form, type FormProps } from '../Form';
|
|
31
|
+
import { Form, type FormProps, type InputComponent, InputHeader, type InputProps } from '../Form';
|
|
20
32
|
|
|
21
33
|
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
22
34
|
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
23
35
|
|
|
24
|
-
const ViewMetaSchema = Schema.Struct({
|
|
25
|
-
typename: Format.URL.annotations({
|
|
26
|
-
title: 'Record type',
|
|
27
|
-
}),
|
|
28
|
-
}).pipe(Schema.mutable);
|
|
29
|
-
|
|
30
|
-
type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
31
|
-
|
|
32
36
|
export type ViewEditorProps = ThemedClassName<
|
|
33
37
|
{
|
|
34
38
|
schema: Schema.Schema.AnyNoContext;
|
|
35
|
-
view:
|
|
39
|
+
view: View.View;
|
|
40
|
+
mode?: 'schema' | 'query' | 'named-query';
|
|
36
41
|
registry?: SchemaRegistry;
|
|
37
42
|
readonly?: boolean;
|
|
38
43
|
showHeading?: boolean;
|
|
39
|
-
|
|
44
|
+
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
40
45
|
onDelete?: (fieldId: string) => void;
|
|
41
|
-
} & Pick<FormProps<any>, 'outerSpacing'>
|
|
46
|
+
} & Pick<FormProps<any>, 'outerSpacing'> &
|
|
47
|
+
Pick<QueryFormProps, 'types' | 'tags'>
|
|
42
48
|
>;
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* Schema-based object form.
|
|
46
52
|
*/
|
|
47
|
-
export const ViewEditor = (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return new ProjectionModel(jsonSchema, view.projection);
|
|
63
|
-
}, [schema, view.projection]);
|
|
64
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
65
|
-
const readonly = _readonly || !isMutable(schema);
|
|
66
|
-
|
|
67
|
-
// TODO(burdon): Should be reactive.
|
|
68
|
-
const viewValues = useMemo(() => {
|
|
69
|
-
return {
|
|
70
|
-
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
71
|
-
// TODO(burdon): Settings should have domain name owned by user.
|
|
72
|
-
typename: view.query.typename,
|
|
73
|
-
};
|
|
74
|
-
}, [view.query.typename]);
|
|
75
|
-
|
|
76
|
-
const handleToggleField = useCallback(
|
|
77
|
-
(field: FieldType) => {
|
|
78
|
-
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
53
|
+
export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
54
|
+
(
|
|
55
|
+
{
|
|
56
|
+
classNames,
|
|
57
|
+
schema,
|
|
58
|
+
view,
|
|
59
|
+
mode = 'schema',
|
|
60
|
+
registry,
|
|
61
|
+
readonly,
|
|
62
|
+
showHeading = false,
|
|
63
|
+
types,
|
|
64
|
+
tags,
|
|
65
|
+
onQueryChanged,
|
|
66
|
+
onDelete,
|
|
67
|
+
outerSpacing = true,
|
|
79
68
|
},
|
|
80
|
-
|
|
81
|
-
)
|
|
69
|
+
forwardedRef,
|
|
70
|
+
) => {
|
|
71
|
+
const schemaReadonly = !isMutable(schema);
|
|
72
|
+
const { t } = useTranslation(translationKey);
|
|
73
|
+
const projectionModel = useMemo(() => {
|
|
74
|
+
// Use reactive and mutable version of json schema when schema is mutable.
|
|
75
|
+
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
76
|
+
return new ProjectionModel(jsonSchema, view.projection);
|
|
77
|
+
}, [schema, JSON.stringify(view.projection)]);
|
|
78
|
+
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
79
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
const queueTarget = Match.value(view.query.ast).pipe(
|
|
82
|
+
Match.when({ type: 'options' }, ({ options }) => {
|
|
83
|
+
return Option.fromNullable(options.queues).pipe(
|
|
84
|
+
Option.flatMap((queues) => Array.head(queues)),
|
|
85
|
+
Option.getOrUndefined,
|
|
86
|
+
);
|
|
87
|
+
}),
|
|
88
|
+
Match.orElse(() => undefined),
|
|
89
|
+
);
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const handleUpdate = useCallback(
|
|
92
|
-
({ typename }: ViewMetaType) => {
|
|
93
|
-
invariant(!readonly);
|
|
94
|
-
requestAnimationFrame(() => {
|
|
95
|
-
if (view.query.typename !== typename && !readonly) {
|
|
96
|
-
onTypenameChanged?.(typename);
|
|
97
|
-
}
|
|
91
|
+
const viewSchema = useMemo(() => {
|
|
92
|
+
const base = Schema.Struct({
|
|
93
|
+
query:
|
|
94
|
+
mode === 'schema'
|
|
95
|
+
? Format.URL.annotations({ title: 'Record type' })
|
|
96
|
+
: QueryAST.Query.annotations({ title: 'Query' }),
|
|
98
97
|
});
|
|
99
|
-
},
|
|
100
|
-
[view.query.typename, onTypenameChanged, readonly],
|
|
101
|
-
);
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
if (mode === 'query' || mode === 'named-query') {
|
|
100
|
+
const name = Schema.Struct({
|
|
101
|
+
name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return Schema.Struct({
|
|
105
|
+
...(mode === 'named-query' ? { ...name.fields, ...base.fields } : base.fields),
|
|
106
|
+
target: Schema.optional(Schema.String.annotations({ title: 'Target Queue' })),
|
|
107
|
+
}).pipe(Schema.mutable);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
return base.pipe(Schema.mutable);
|
|
111
|
+
}, [mode]);
|
|
112
|
+
|
|
113
|
+
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
114
|
+
// TODO(burdon): Settings should have domain name owned by user.
|
|
115
|
+
const viewValues = useMemo(
|
|
116
|
+
() => ({
|
|
117
|
+
name: view.name,
|
|
118
|
+
query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
|
|
119
|
+
target: queueTarget,
|
|
120
|
+
}),
|
|
121
|
+
[view.name, mode, view.query.ast, queueTarget],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const handleToggleField = useCallback(
|
|
125
|
+
(field: FieldType) => {
|
|
126
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
127
|
+
},
|
|
128
|
+
[readonly],
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
|
|
114
132
|
|
|
115
|
-
|
|
116
|
-
(fromIndex: number, toIndex: number) => {
|
|
133
|
+
const handleAdd = useCallback(() => {
|
|
117
134
|
invariant(!readonly);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
fields.splice(toIndex, 0, moved);
|
|
122
|
-
view.projection.fields = fields;
|
|
123
|
-
},
|
|
124
|
-
[view.projection.fields, readonly],
|
|
125
|
-
);
|
|
135
|
+
const field = projectionModel.createFieldProjection();
|
|
136
|
+
setExpandedField(field.id);
|
|
137
|
+
}, [schema, projectionModel, readonly]);
|
|
126
138
|
|
|
127
|
-
|
|
139
|
+
const handleUpdate = useCallback(
|
|
140
|
+
(values: any) => {
|
|
141
|
+
invariant(!readonly);
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
if ('name' in values && view.name !== values.name) {
|
|
144
|
+
view.name = values.name;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
128
147
|
|
|
129
|
-
|
|
148
|
+
const query = mode === 'schema' ? Query.select(Filter.typename(values.query)).ast : values.query;
|
|
149
|
+
onQueryChanged?.(query, values.target);
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
[onQueryChanged, readonly, view, queueTarget, mode],
|
|
153
|
+
);
|
|
130
154
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
);
|
|
155
|
+
const handleDelete = useCallback(
|
|
156
|
+
(fieldId: string) => {
|
|
157
|
+
invariant(!readonly);
|
|
158
|
+
if (fieldId === expandedField) {
|
|
159
|
+
setExpandedField(undefined);
|
|
160
|
+
}
|
|
138
161
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
[projection],
|
|
145
|
-
);
|
|
162
|
+
onDelete?.(fieldId);
|
|
163
|
+
},
|
|
164
|
+
[expandedField, onDelete, readonly],
|
|
165
|
+
);
|
|
146
166
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
const handleMove = useCallback(
|
|
168
|
+
(fromIndex: number, toIndex: number) => {
|
|
169
|
+
invariant(!readonly);
|
|
170
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
171
|
+
const fields = [...view.projection.fields];
|
|
172
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
173
|
+
fields.splice(toIndex, 0, moved);
|
|
174
|
+
view.projection.fields = fields;
|
|
175
|
+
},
|
|
176
|
+
[view.projection.fields, readonly],
|
|
177
|
+
);
|
|
157
178
|
|
|
158
|
-
|
|
159
|
-
{/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
|
|
160
|
-
<Form<ViewMetaType>
|
|
161
|
-
autoSave
|
|
162
|
-
schema={ViewMetaSchema}
|
|
163
|
-
values={viewValues}
|
|
164
|
-
readonly={readonly}
|
|
165
|
-
onSave={handleUpdate}
|
|
166
|
-
outerSpacing={outerSpacing}
|
|
167
|
-
/>
|
|
179
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
168
180
|
|
|
169
|
-
|
|
170
|
-
|
|
181
|
+
const handleHide = useCallback(
|
|
182
|
+
(fieldId: string) => {
|
|
183
|
+
setExpandedField(undefined);
|
|
184
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
185
|
+
},
|
|
186
|
+
[projectionModel],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const handleShow = useCallback(
|
|
190
|
+
(property: string) => {
|
|
191
|
+
setExpandedField(undefined);
|
|
192
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
193
|
+
},
|
|
194
|
+
[projectionModel],
|
|
195
|
+
);
|
|
171
196
|
|
|
172
|
-
|
|
173
|
-
items={view.projection.fields}
|
|
174
|
-
isItem={Schema.is(FieldSchema)}
|
|
175
|
-
getId={(field) => field.id}
|
|
176
|
-
onMove={readonly ? undefined : handleMove}
|
|
177
|
-
readonly={readonly}
|
|
178
|
-
>
|
|
179
|
-
{({ items: fields }) => (
|
|
180
|
-
<>
|
|
181
|
-
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
197
|
+
const custom = useMemo(() => (mode === 'query' ? customFields({ types, tags }) : undefined), [types, tags, mode]);
|
|
182
198
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
>
|
|
191
|
-
<div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
|
|
192
|
-
<List.ItemDragHandle disabled={readonly} />
|
|
193
|
-
<List.ItemTitle onClick={() => handleToggleField(field)}>{field.path}</List.ItemTitle>
|
|
194
|
-
<List.ItemButton
|
|
195
|
-
label={t('hide field label')}
|
|
196
|
-
data-testid='hide-field-button'
|
|
197
|
-
icon='ph--eye-slash--regular'
|
|
198
|
-
autoHide={false}
|
|
199
|
-
// TDOO(burdon): Is this the correct test?
|
|
200
|
-
disabled={readonly || view.projection.fields.length <= 1}
|
|
201
|
-
onClick={() => handleHide(field.id)}
|
|
202
|
-
/>
|
|
203
|
-
{/* TODO(burdon): Remove unless implement undo. */}
|
|
204
|
-
<List.ItemDeleteButton
|
|
205
|
-
label={t('delete field label')}
|
|
206
|
-
autoHide={false}
|
|
207
|
-
disabled={readonly || view.projection.fields.length <= 1}
|
|
208
|
-
onClick={() => handleDelete(field.id)}
|
|
209
|
-
data-testid='field.delete'
|
|
210
|
-
/>
|
|
211
|
-
<IconButton
|
|
212
|
-
iconOnly
|
|
213
|
-
variant='ghost'
|
|
214
|
-
label={t('toggle expand label', { ns: 'os' })}
|
|
215
|
-
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
216
|
-
onClick={() => handleToggleField(field)}
|
|
217
|
-
/>
|
|
218
|
-
</div>
|
|
219
|
-
{expandedField === field.id && (
|
|
220
|
-
<div role='none' className='col-span-5'>
|
|
221
|
-
<FieldEditor
|
|
222
|
-
readonly={readonly}
|
|
223
|
-
projection={projection}
|
|
224
|
-
field={field}
|
|
225
|
-
registry={registry}
|
|
226
|
-
onSave={handleClose}
|
|
227
|
-
/>
|
|
228
|
-
</div>
|
|
229
|
-
)}
|
|
230
|
-
</List.Item>
|
|
231
|
-
))}
|
|
232
|
-
</div>
|
|
233
|
-
</>
|
|
234
|
-
)}
|
|
235
|
-
</List.Root>
|
|
199
|
+
return (
|
|
200
|
+
<div role='none' className={mx(classNames)}>
|
|
201
|
+
{schemaReadonly && mode === 'schema' && (
|
|
202
|
+
<Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
|
|
203
|
+
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
204
|
+
</Callout.Root>
|
|
205
|
+
)}
|
|
236
206
|
|
|
237
|
-
{
|
|
238
|
-
|
|
239
|
-
|
|
207
|
+
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
208
|
+
{/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
|
|
209
|
+
<Form<Schema.Schema.Type<typeof viewSchema>>
|
|
210
|
+
autoSave
|
|
211
|
+
schema={viewSchema}
|
|
212
|
+
values={viewValues}
|
|
213
|
+
readonly={readonly ? 'disabled-input' : false}
|
|
214
|
+
onSave={handleUpdate}
|
|
215
|
+
outerSpacing={outerSpacing}
|
|
216
|
+
Custom={custom}
|
|
217
|
+
/>
|
|
240
218
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
219
|
+
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
220
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
221
|
+
|
|
222
|
+
<List.Root<FieldType>
|
|
223
|
+
items={view.projection.fields}
|
|
224
|
+
isItem={Schema.is(FieldSchema)}
|
|
225
|
+
getId={(field) => field.id}
|
|
226
|
+
onMove={readonly ? undefined : handleMove}
|
|
227
|
+
readonly={readonly}
|
|
228
|
+
>
|
|
229
|
+
{({ items: fields }) => (
|
|
230
|
+
<>
|
|
231
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
232
|
+
<div role='list' className={listGrid}>
|
|
233
|
+
{fields?.map((field) => {
|
|
234
|
+
const hidden = field.visible === false;
|
|
235
|
+
return (
|
|
236
|
+
<List.Item<FieldType>
|
|
237
|
+
key={field.id}
|
|
238
|
+
item={field}
|
|
239
|
+
classNames={listItemGrid}
|
|
240
|
+
aria-expanded={expandedField === field.id}
|
|
241
|
+
>
|
|
242
|
+
<div
|
|
243
|
+
role='none'
|
|
244
|
+
className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}
|
|
245
|
+
>
|
|
246
|
+
<List.ItemDragHandle disabled={readonly} />
|
|
247
|
+
<List.ItemTitle
|
|
248
|
+
classNames={hidden && 'text-subdued'}
|
|
249
|
+
onClick={() => handleToggleField(field)}
|
|
250
|
+
>
|
|
251
|
+
{field.path}
|
|
252
|
+
</List.ItemTitle>
|
|
253
|
+
<List.ItemButton
|
|
254
|
+
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
255
|
+
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
256
|
+
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
257
|
+
autoHide={false}
|
|
258
|
+
disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
|
|
259
|
+
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
260
|
+
/>
|
|
261
|
+
{mode === 'schema' && (
|
|
262
|
+
<>
|
|
263
|
+
<List.ItemDeleteButton
|
|
264
|
+
label={t('delete field label')}
|
|
265
|
+
autoHide={false}
|
|
266
|
+
disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
|
|
267
|
+
onClick={() => handleDelete(field.id)}
|
|
268
|
+
data-testid='field.delete'
|
|
269
|
+
/>
|
|
270
|
+
<IconButton
|
|
271
|
+
iconOnly
|
|
272
|
+
variant='ghost'
|
|
273
|
+
label={t('toggle expand label', { ns: 'os' })}
|
|
274
|
+
icon={
|
|
275
|
+
expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'
|
|
276
|
+
}
|
|
277
|
+
onClick={() => handleToggleField(field)}
|
|
278
|
+
/>
|
|
279
|
+
</>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
{expandedField === field.id && mode === 'schema' && (
|
|
283
|
+
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
284
|
+
<FieldEditor
|
|
285
|
+
readonly={readonly || schemaReadonly ? 'disabled-input' : false}
|
|
286
|
+
projection={projectionModel}
|
|
287
|
+
field={field}
|
|
288
|
+
registry={registry}
|
|
289
|
+
onSave={handleClose}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</List.Item>
|
|
294
|
+
);
|
|
295
|
+
})}
|
|
264
296
|
</div>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</List.Root>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{!readonly && !expandedField && mode === 'schema' && (
|
|
303
|
+
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
304
|
+
<IconButton
|
|
305
|
+
icon='ph--plus--regular'
|
|
306
|
+
label={t('add property button')}
|
|
307
|
+
onClick={readonly ? undefined : handleAdd}
|
|
308
|
+
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
309
|
+
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
310
|
+
classNames='is-full'
|
|
311
|
+
/>
|
|
312
|
+
</div>
|
|
268
313
|
)}
|
|
269
314
|
</div>
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
270
318
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
};
|
|
319
|
+
const customFields = ({ types, tags }: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, InputComponent> => ({
|
|
320
|
+
query: (props: InputProps) => {
|
|
321
|
+
const handleChange = useCallback(
|
|
322
|
+
(query: Query.Any) => props.onValueChange('object', query.ast),
|
|
323
|
+
[props.onValueChange],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<Input.Root>
|
|
328
|
+
<InputHeader label={props.label} />
|
|
329
|
+
<QueryForm initialQuery={props.getValue()} types={types} tags={tags} onChange={handleChange} />
|
|
330
|
+
</Input.Root>
|
|
331
|
+
);
|
|
332
|
+
},
|
|
333
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -9,16 +9,22 @@ import { Card } from '@dxos/react-ui-stack';
|
|
|
9
9
|
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
10
10
|
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
<div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
14
|
-
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
15
|
-
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
12
|
+
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
18
13
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className='is-full bs-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
17
|
+
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
18
|
+
<JsonFilter data={json} classNames='is-full text-xs' testId='debug' />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
24
|
+
|
|
25
|
+
export const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
26
|
+
return <Card.StaticRoot classNames={[textBlockWidth, classNames]}>{children}</Card.StaticRoot>;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
// Symbol for accessing debug objects in tests.
|
|
24
30
|
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
6
7
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
8
|
|
|
8
|
-
import { type BaseObject, getValue, setValue } from '@dxos/echo
|
|
9
|
+
import { type BaseObject, getValue, setValue } from '@dxos/echo/internal';
|
|
9
10
|
import { type JsonPath, type SimpleType, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
|
|
10
11
|
import { invariant } from '@dxos/invariant';
|
|
11
12
|
import { log } from '@dxos/log';
|
|
@@ -203,6 +204,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
203
204
|
[values],
|
|
204
205
|
);
|
|
205
206
|
|
|
207
|
+
// TODO(wittjosiah): This is causing the entire form to re-render on every change.
|
|
206
208
|
const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
|
|
207
209
|
(path: (string | number)[], type: SimpleType, value: any) => {
|
|
208
210
|
const jsonPath = createJsonPath(path);
|
|
@@ -218,12 +220,13 @@ export const useForm = <T extends BaseObject>({
|
|
|
218
220
|
|
|
219
221
|
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
220
222
|
setValues(newValues);
|
|
221
|
-
|
|
223
|
+
const newChanged = { ...changed, [jsonPath]: true };
|
|
224
|
+
setChanged(newChanged);
|
|
222
225
|
onValuesChanged?.(newValues);
|
|
223
226
|
|
|
224
227
|
const isValid = validate(newValues);
|
|
225
228
|
if (isValid && onValid) {
|
|
226
|
-
onValid(newValues, { changed });
|
|
229
|
+
onValid(newValues, { changed: newChanged });
|
|
227
230
|
}
|
|
228
231
|
},
|
|
229
232
|
[values, onValuesChanged, validate, onValid, changed],
|