@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.ae835ea
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 +826 -621
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +826 -621
- 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 +4 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +78 -7
- 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 +7 -6
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +129 -32
- 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 +17 -11
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +3 -3
- 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 +15 -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 +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 +20 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +81 -9
- 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 +3 -3
- 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 -5
- 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 +11 -14
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +26 -24
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +21 -23
- package/src/components/Form/ArrayField.tsx +13 -12
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +135 -99
- package/src/components/Form/Form.tsx +27 -15
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +121 -39
- package/src/components/Form/FormContext.tsx +8 -8
- package/src/components/Form/Input.tsx +5 -2
- package/src/components/Form/RefField.tsx +111 -41
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +11 -11
- package/src/components/Form/custom/SelectOptionsInput.tsx +30 -31
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +2 -2
- 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 +129 -49
- package/src/components/ViewEditor/ViewEditor.test.tsx +19 -19
- package/src/components/ViewEditor/ViewEditor.tsx +278 -233
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +11 -6
- package/src/hooks/useQueryRefOptions.ts +15 -6
- package/src/hooks/useRefQueryLookupHandler.ts +3 -3
- package/src/translations.ts +13 -6
- package/src/util/field.ts +9 -19
- package/src/util/formatting.ts +1 -1
- package/src/util/refs.ts +2 -2
- 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 -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,284 +2,329 @@
|
|
|
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
|
-
import {
|
|
15
|
-
import {
|
|
19
|
+
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
20
|
+
import {
|
|
21
|
+
type DataType,
|
|
22
|
+
FieldSchema,
|
|
23
|
+
type FieldType,
|
|
24
|
+
ProjectionModel,
|
|
25
|
+
VIEW_FIELD_LIMIT,
|
|
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
|
-
const
|
|
22
|
-
|
|
23
|
-
const ViewMetaSchema = Schema.Struct({
|
|
24
|
-
name: Schema.String.annotations({
|
|
25
|
-
title: 'View',
|
|
26
|
-
}),
|
|
27
|
-
typename: Format.URL.annotations({
|
|
28
|
-
title: 'Typename',
|
|
29
|
-
}),
|
|
30
|
-
}).pipe(Schema.mutable);
|
|
31
|
-
|
|
32
|
-
type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
33
|
+
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
34
|
+
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
33
35
|
|
|
34
36
|
export type ViewEditorProps = ThemedClassName<
|
|
35
37
|
{
|
|
36
38
|
schema: Schema.Schema.AnyNoContext;
|
|
37
|
-
view:
|
|
39
|
+
view: DataType.View;
|
|
40
|
+
mode?: 'schema' | 'query';
|
|
38
41
|
registry?: SchemaRegistry;
|
|
39
42
|
readonly?: boolean;
|
|
40
43
|
showHeading?: boolean;
|
|
41
|
-
|
|
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
|
-
|
|
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']>();
|
|
80
|
+
|
|
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
|
+
);
|
|
68
90
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
}, [view]);
|
|
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
|
+
});
|
|
78
98
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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);
|
|
83
105
|
}
|
|
84
|
-
setField((f) => (f === field ? undefined : field));
|
|
85
|
-
},
|
|
86
|
-
[readonly],
|
|
87
|
-
);
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
return base.pipe(Schema.mutable);
|
|
108
|
+
}, [mode]);
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
);
|
|
96
120
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
121
|
+
const handleToggleField = useCallback(
|
|
122
|
+
(field: FieldType) => {
|
|
123
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
124
|
+
},
|
|
125
|
+
[readonly],
|
|
126
|
+
);
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
onTypenameChanged?.(typename);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
[schema, view, onTypenameChanged, readonly],
|
|
111
|
-
);
|
|
128
|
+
// TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
(fieldId: string) => {
|
|
130
|
+
const handleAdd = useCallback(() => {
|
|
115
131
|
invariant(!readonly);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
132
|
+
const field = projectionModel.createFieldProjection();
|
|
133
|
+
setExpandedField(field.id);
|
|
134
|
+
}, [schema, projectionModel, readonly]);
|
|
119
135
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
+
}
|
|
124
144
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
);
|
|
151
|
+
|
|
152
|
+
const handleDelete = useCallback(
|
|
153
|
+
(fieldId: string) => {
|
|
154
|
+
invariant(!readonly);
|
|
155
|
+
if (fieldId === expandedField) {
|
|
156
|
+
setExpandedField(undefined);
|
|
157
|
+
}
|
|
136
158
|
|
|
137
|
-
|
|
159
|
+
onDelete?.(fieldId);
|
|
160
|
+
},
|
|
161
|
+
[expandedField, onDelete, readonly],
|
|
162
|
+
);
|
|
138
163
|
|
|
139
|
-
|
|
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
|
+
);
|
|
140
175
|
|
|
141
|
-
|
|
142
|
-
(fieldId: string) => {
|
|
143
|
-
setField(undefined);
|
|
144
|
-
projection.hideFieldProjection(fieldId);
|
|
145
|
-
},
|
|
146
|
-
[projection],
|
|
147
|
-
);
|
|
176
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
148
177
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
+
);
|
|
156
193
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
194
|
+
const custom = useMemo(() => (mode === 'query' ? customFields({ types, tags }) : undefined), [types, tags, mode]);
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div role='none' className={mx(classNames)}>
|
|
198
|
+
{schemaReadonly && mode === 'schema' && (
|
|
199
|
+
<Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
|
|
162
200
|
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
163
201
|
</Callout.Root>
|
|
164
|
-
|
|
165
|
-
)}
|
|
202
|
+
)}
|
|
166
203
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
/>
|
|
177
215
|
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
217
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
180
218
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
<div role='
|
|
192
|
-
|
|
193
|
-
|
|
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))}
|
|
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
|
+
)}
|
|
278
|
+
</div>
|
|
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
|
+
})}
|
|
194
293
|
</div>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
<List.Item<FieldType>
|
|
200
|
-
key={field.id}
|
|
201
|
-
item={field}
|
|
202
|
-
classNames={mx(grid, ghostHover, 'overflow-hidden', !readonly && 'cursor-pointer')}
|
|
203
|
-
>
|
|
204
|
-
<List.ItemDragHandle />
|
|
205
|
-
<List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
|
|
206
|
-
<List.ItemButton
|
|
207
|
-
data-testid='hide-field-button'
|
|
208
|
-
icon='ph--eye-slash--regular'
|
|
209
|
-
// TDOO(burdon): Is this the correct test?
|
|
210
|
-
disabled={view.fields.length <= 1}
|
|
211
|
-
onClick={() => handleHide(field.id)}
|
|
212
|
-
/>
|
|
213
|
-
{/* TODO(burdon): Remove unless implement undo. */}
|
|
214
|
-
{!readonly && (
|
|
215
|
-
<List.ItemDeleteButton
|
|
216
|
-
disabled={view.fields.length <= 1}
|
|
217
|
-
onClick={() => handleDelete(field.id)}
|
|
218
|
-
/>
|
|
219
|
-
)}
|
|
220
|
-
</List.Item>
|
|
221
|
-
))}
|
|
222
|
-
</div>
|
|
223
|
-
</>
|
|
224
|
-
)}
|
|
225
|
-
</List.Root>
|
|
226
|
-
|
|
227
|
-
{hiddenProperties.length > 0 && (
|
|
228
|
-
<>
|
|
229
|
-
<label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
|
|
294
|
+
</>
|
|
295
|
+
)}
|
|
296
|
+
</List.Root>
|
|
297
|
+
</div>
|
|
230
298
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
|
|
243
|
-
>
|
|
244
|
-
<List.ItemDragHandle disabled />
|
|
245
|
-
<List.ItemTitle>{property}</List.ItemTitle>
|
|
246
|
-
<List.ItemButton
|
|
247
|
-
icon='ph--eye--regular'
|
|
248
|
-
onClick={() => handleShow(property)}
|
|
249
|
-
data-testid='show-field-button'
|
|
250
|
-
/>
|
|
251
|
-
</List.Item>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
</List.Root>
|
|
256
|
-
</>
|
|
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>
|
|
257
310
|
)}
|
|
258
311
|
</div>
|
|
312
|
+
);
|
|
313
|
+
},
|
|
314
|
+
);
|
|
259
315
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
registry={registry}
|
|
267
|
-
onSave={handleClose}
|
|
268
|
-
/>
|
|
269
|
-
)}
|
|
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
|
+
);
|
|
270
322
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
classNames='flex is-full'
|
|
280
|
-
/>
|
|
281
|
-
</div>
|
|
282
|
-
)}
|
|
283
|
-
</div>
|
|
284
|
-
);
|
|
285
|
-
};
|
|
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
|
@@ -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,14 +2,15 @@
|
|
|
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
|
|
9
|
+
import { type BaseObject, getValue, setValue } from '@dxos/echo/internal';
|
|
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';
|
|
12
|
-
import {
|
|
13
|
+
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
13
14
|
import { type MaybePromise } from '@dxos/util';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -26,6 +27,8 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
26
27
|
changed: Record<JsonPath, boolean>;
|
|
27
28
|
canSave: boolean;
|
|
28
29
|
formIsValid: boolean;
|
|
30
|
+
|
|
31
|
+
// TODO(burdon): Return onSave.
|
|
29
32
|
handleSave: () => void;
|
|
30
33
|
|
|
31
34
|
//
|
|
@@ -201,6 +204,7 @@ export const useForm = <T extends BaseObject>({
|
|
|
201
204
|
[values],
|
|
202
205
|
);
|
|
203
206
|
|
|
207
|
+
// TODO(wittjosiah): This is causing the entire form to re-render on every change.
|
|
204
208
|
const onValueChange = useCallback<FormHandler<T>['onValueChange']>(
|
|
205
209
|
(path: (string | number)[], type: SimpleType, value: any) => {
|
|
206
210
|
const jsonPath = createJsonPath(path);
|
|
@@ -216,12 +220,13 @@ export const useForm = <T extends BaseObject>({
|
|
|
216
220
|
|
|
217
221
|
const newValues = { ...setValue(values, jsonPath, parsedValue) };
|
|
218
222
|
setValues(newValues);
|
|
219
|
-
|
|
223
|
+
const newChanged = { ...changed, [jsonPath]: true };
|
|
224
|
+
setChanged(newChanged);
|
|
220
225
|
onValuesChanged?.(newValues);
|
|
221
226
|
|
|
222
227
|
const isValid = validate(newValues);
|
|
223
228
|
if (isValid && onValid) {
|
|
224
|
-
onValid(newValues, { changed });
|
|
229
|
+
onValid(newValues, { changed: newChanged });
|
|
225
230
|
}
|
|
226
231
|
},
|
|
227
232
|
[values, onValuesChanged, validate, onValid, changed],
|