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