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