@dxos/react-ui-form 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/lib/browser/index.mjs +1003 -668
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1003 -668
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +23 -18
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +7 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +35 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +62 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +25 -26
- package/src/components/Form/Form.tsx +101 -66
- package/src/components/Form/FormField.tsx +32 -14
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +126 -105
- package/src/components/Form/fields/ArrayField.tsx +11 -8
- package/src/components/Form/fields/GeoPointField.tsx +6 -6
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +37 -20
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +113 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/Settings.stories.tsx +55 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/Settings/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
- package/src/components/ViewEditor/ViewEditor.tsx +117 -85
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -18
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/translations.ts +63 -63
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
|
@@ -9,19 +9,18 @@ import * as Option from 'effect/Option';
|
|
|
9
9
|
import * as Schema from 'effect/Schema';
|
|
10
10
|
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
|
|
11
11
|
|
|
12
|
-
import { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
|
|
12
|
+
import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
|
|
13
|
+
import { View } from '@dxos/echo';
|
|
13
14
|
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
14
15
|
import { invariant } from '@dxos/invariant';
|
|
15
|
-
import {
|
|
16
|
+
import { useObject, useQuery } from '@dxos/react-client/echo';
|
|
17
|
+
import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
16
18
|
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
17
19
|
import { List } from '@dxos/react-ui-list';
|
|
18
20
|
import {
|
|
19
|
-
|
|
20
|
-
type FieldType,
|
|
21
|
+
ParentLabelAnnotation,
|
|
21
22
|
ProjectionModel,
|
|
22
23
|
VIEW_FIELD_LIMIT,
|
|
23
|
-
type View,
|
|
24
|
-
createDirectChangeCallback,
|
|
25
24
|
createEchoChangeCallback,
|
|
26
25
|
getTypenameFromQuery,
|
|
27
26
|
} from '@dxos/schema';
|
|
@@ -35,22 +34,20 @@ import {
|
|
|
35
34
|
type FormFieldComponentProps,
|
|
36
35
|
FormFieldLabel,
|
|
37
36
|
type FormFieldMap,
|
|
37
|
+
type FormRootProps,
|
|
38
38
|
} from '../Form';
|
|
39
39
|
|
|
40
|
-
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
41
|
-
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
42
|
-
|
|
43
40
|
export type ViewEditorProps = ThemedClassName<
|
|
44
41
|
{
|
|
45
42
|
schema: Schema.Schema.AnyNoContext;
|
|
46
43
|
view: View.View;
|
|
47
44
|
mode?: 'schema' | 'tag';
|
|
48
45
|
registry?: SchemaRegistry.SchemaRegistry;
|
|
49
|
-
readonly?: boolean;
|
|
50
46
|
showHeading?: boolean;
|
|
51
47
|
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
52
48
|
onDelete?: (fieldId: string) => void;
|
|
53
|
-
} & Pick<QueryFormProps, 'types' | 'tags'>
|
|
49
|
+
} & Pick<QueryFormProps, 'types' | 'tags'> &
|
|
50
|
+
Pick<FormRootProps<any>, 'readonly' | 'db'>
|
|
54
51
|
>;
|
|
55
52
|
|
|
56
53
|
/**
|
|
@@ -64,6 +61,7 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
64
61
|
view,
|
|
65
62
|
mode = 'schema',
|
|
66
63
|
registry,
|
|
64
|
+
db,
|
|
67
65
|
readonly,
|
|
68
66
|
showHeading = false,
|
|
69
67
|
types,
|
|
@@ -81,11 +79,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
81
79
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
82
80
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
83
81
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
? createEchoChangeCallback(view, schema)
|
|
88
|
-
: createDirectChangeCallback(view.projection, jsonSchema);
|
|
82
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
83
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
84
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
89
85
|
|
|
90
86
|
const model = new ProjectionModel({
|
|
91
87
|
registry: atomRegistry,
|
|
@@ -98,18 +94,31 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
98
94
|
}, [atomRegistry, schema, view]);
|
|
99
95
|
|
|
100
96
|
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
101
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
102
97
|
|
|
103
98
|
const queueTarget = Match.value(view.query.ast).pipe(
|
|
104
|
-
Match.when({ type: '
|
|
105
|
-
|
|
99
|
+
Match.when({ type: 'from' }, ({ from }) => {
|
|
100
|
+
if (from._tag !== 'scope') {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return Option.fromNullable(from.scope.queues).pipe(
|
|
106
104
|
Option.flatMap((queues) => Array.head(queues)),
|
|
105
|
+
Option.map(String),
|
|
107
106
|
Option.getOrUndefined,
|
|
108
107
|
);
|
|
109
108
|
}),
|
|
110
109
|
Match.orElse(() => undefined),
|
|
111
110
|
);
|
|
112
111
|
|
|
112
|
+
const feeds = useQuery(db, Filter.type(Feed.Feed));
|
|
113
|
+
|
|
114
|
+
const targetRef = useMemo(() => {
|
|
115
|
+
if (!queueTarget) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
|
|
119
|
+
return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
|
|
120
|
+
}, [queueTarget, feeds]);
|
|
121
|
+
|
|
113
122
|
const viewSchema = useMemo(() => {
|
|
114
123
|
const base = Schema.Struct({
|
|
115
124
|
query:
|
|
@@ -121,7 +130,12 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
121
130
|
if (mode === 'tag') {
|
|
122
131
|
return Schema.Struct({
|
|
123
132
|
...base.fields,
|
|
124
|
-
|
|
133
|
+
// TODO(wittjosiah): Replace Type.Feed with Dataset.Dataset when Ref.Ref supports unions.
|
|
134
|
+
target: Ref.Ref(Feed.Feed).pipe(
|
|
135
|
+
Schema.annotations({ title: 'Target Feed' }),
|
|
136
|
+
ParentLabelAnnotation.set(true),
|
|
137
|
+
Schema.optional,
|
|
138
|
+
),
|
|
125
139
|
}).pipe(Schema.mutable);
|
|
126
140
|
}
|
|
127
141
|
|
|
@@ -133,9 +147,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
133
147
|
const viewValues = useMemo(
|
|
134
148
|
() => ({
|
|
135
149
|
query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
|
|
136
|
-
target:
|
|
150
|
+
target: targetRef,
|
|
137
151
|
}),
|
|
138
|
-
[mode, view.query.ast,
|
|
152
|
+
[mode, view.query.ast, targetRef],
|
|
139
153
|
);
|
|
140
154
|
|
|
141
155
|
const fieldMap = useMemo<FormFieldMap | undefined>(
|
|
@@ -143,49 +157,51 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
143
157
|
[mode, types, tags],
|
|
144
158
|
);
|
|
145
159
|
|
|
146
|
-
// TODO(burdon): Check if mutable.
|
|
147
|
-
const handleAdd = useCallback(() => {
|
|
148
|
-
invariant(!readonly);
|
|
149
|
-
const field = projectionModel.createFieldProjection();
|
|
150
|
-
setExpandedField(field.id);
|
|
151
|
-
}, [projectionModel, readonly]);
|
|
152
|
-
|
|
153
160
|
const handleUpdate = useCallback(
|
|
154
161
|
(values: any) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
const targetValue = values.target;
|
|
163
|
+
let queueDxn: string | undefined;
|
|
164
|
+
|
|
165
|
+
if (Ref.isRef(targetValue)) {
|
|
166
|
+
const feedDxn = targetValue.dxn.toString();
|
|
167
|
+
const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
|
|
168
|
+
if (feed) {
|
|
169
|
+
queueDxn = Feed.getQueueDxn(feed)?.toString();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// TODO(wittjosiah): Deep-clone the AST to plain JS or ECHO proxy arrays become objects with numeric keys.
|
|
174
|
+
const query =
|
|
175
|
+
mode === 'schema'
|
|
176
|
+
? Query.select(Filter.typename(values.query)).ast
|
|
177
|
+
: JSON.parse(JSON.stringify(values.query));
|
|
178
|
+
onQueryChanged?.(query, queueDxn);
|
|
159
179
|
},
|
|
160
|
-
[onQueryChanged,
|
|
180
|
+
[onQueryChanged, mode, feeds],
|
|
161
181
|
);
|
|
162
182
|
|
|
163
183
|
const handleDelete = useCallback(
|
|
164
184
|
(fieldId: string) => {
|
|
165
185
|
invariant(!readonly);
|
|
166
|
-
if (fieldId === expandedField) {
|
|
167
|
-
setExpandedField(undefined);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
186
|
onDelete?.(fieldId);
|
|
171
187
|
},
|
|
172
|
-
[
|
|
188
|
+
[onDelete, readonly],
|
|
173
189
|
);
|
|
174
190
|
|
|
175
191
|
return (
|
|
176
192
|
<div role='none' className={mx(classNames)}>
|
|
177
193
|
{/* If readonly is set, then the callout is not needed. */}
|
|
178
194
|
{schemaReadonly && !readonly && (
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
</
|
|
195
|
+
<Message.Root valence='info' classNames='my-form-padding'>
|
|
196
|
+
<Message.Title>{t('system-schema.description')}</Message.Title>
|
|
197
|
+
</Message.Root>
|
|
182
198
|
)}
|
|
183
199
|
|
|
184
200
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
185
|
-
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap}
|
|
201
|
+
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
|
|
186
202
|
<Form.FieldSet />
|
|
187
203
|
|
|
188
|
-
<FormFieldLabel label={t('fields
|
|
204
|
+
<FormFieldLabel label={t('fields.label')} asChild />
|
|
189
205
|
<FieldList
|
|
190
206
|
schema={schema}
|
|
191
207
|
view={view}
|
|
@@ -195,19 +211,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
195
211
|
onDelete={handleDelete}
|
|
196
212
|
/>
|
|
197
213
|
</Form.Root>
|
|
198
|
-
|
|
199
|
-
{!readonly && !expandedField && (
|
|
200
|
-
<div role='none' className='mlb-cardSpacingBlock'>
|
|
201
|
-
<IconButton
|
|
202
|
-
icon='ph--plus--regular'
|
|
203
|
-
label={t('add property button label')}
|
|
204
|
-
onClick={readonly ? undefined : handleAdd}
|
|
205
|
-
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
206
|
-
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
207
|
-
classNames='is-full'
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
214
|
</div>
|
|
212
215
|
);
|
|
213
216
|
},
|
|
@@ -220,15 +223,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
220
223
|
const schemaReadonly = !isMutable(schema);
|
|
221
224
|
const { t } = useTranslation(translationKey);
|
|
222
225
|
|
|
226
|
+
// Subscribe to view changes for reactivity.
|
|
227
|
+
const [viewSnapshot] = useObject(view);
|
|
228
|
+
|
|
223
229
|
const projectionModel = useMemo(() => {
|
|
224
230
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
225
231
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
226
232
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
? createEchoChangeCallback(view, schema)
|
|
231
|
-
: createDirectChangeCallback(view.projection, jsonSchema);
|
|
233
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
234
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
235
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
232
236
|
|
|
233
237
|
const model = new ProjectionModel({
|
|
234
238
|
registry: atomRegistry,
|
|
@@ -240,10 +244,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
240
244
|
return model;
|
|
241
245
|
}, [atomRegistry, schema, view]);
|
|
242
246
|
|
|
243
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
247
|
+
const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
|
|
248
|
+
|
|
249
|
+
const handleAdd = useCallback(() => {
|
|
250
|
+
invariant(!readonly);
|
|
251
|
+
const field = projectionModel.createFieldProjection();
|
|
252
|
+
setExpandedField(field.id);
|
|
253
|
+
}, [projectionModel, readonly]);
|
|
244
254
|
|
|
245
255
|
const handleToggleField = useCallback(
|
|
246
|
-
(field: FieldType) => {
|
|
256
|
+
(field: View.FieldType) => {
|
|
247
257
|
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
248
258
|
},
|
|
249
259
|
[readonly],
|
|
@@ -264,15 +274,15 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
264
274
|
const handleMove = useCallback(
|
|
265
275
|
(fromIndex: number, toIndex: number) => {
|
|
266
276
|
invariant(!readonly);
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
277
|
+
Obj.change(view, (view) => {
|
|
278
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
279
|
+
const fields = [...view.projection.fields];
|
|
280
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
281
|
+
fields.splice(toIndex, 0, moved);
|
|
282
|
+
view.projection.fields = fields;
|
|
273
283
|
});
|
|
274
284
|
},
|
|
275
|
-
[view
|
|
285
|
+
[view, readonly],
|
|
276
286
|
);
|
|
277
287
|
|
|
278
288
|
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
@@ -293,33 +303,44 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
293
303
|
[projectionModel],
|
|
294
304
|
);
|
|
295
305
|
|
|
306
|
+
if (!viewSnapshot) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
296
310
|
return (
|
|
297
|
-
<List.Root<FieldType>
|
|
298
|
-
items={
|
|
299
|
-
isItem={Schema.is(FieldSchema)}
|
|
311
|
+
<List.Root<View.FieldType>
|
|
312
|
+
items={viewSnapshot.projection.fields as View.FieldType[]}
|
|
313
|
+
isItem={Schema.is(View.FieldSchema)}
|
|
300
314
|
getId={(field) => field.id}
|
|
301
315
|
onMove={readonly ? undefined : handleMove}
|
|
302
316
|
readonly={readonly}
|
|
303
317
|
>
|
|
304
318
|
{({ items: fields }) => (
|
|
305
319
|
<>
|
|
306
|
-
{showHeading && <h3 className='text-sm'>{t('field
|
|
307
|
-
<div role='list' className=
|
|
320
|
+
{showHeading && <h3 className='text-sm'>{t('field-path.label')}</h3>}
|
|
321
|
+
<div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
|
|
308
322
|
{fields?.map((field) => {
|
|
309
323
|
const hidden = field.visible === false;
|
|
310
324
|
return (
|
|
311
|
-
<List.Item<FieldType>
|
|
325
|
+
<List.Item<View.FieldType>
|
|
312
326
|
key={field.id}
|
|
313
327
|
item={field}
|
|
314
|
-
classNames={
|
|
328
|
+
classNames={'grid grid-cols-subgrid col-span-5'}
|
|
315
329
|
aria-expanded={expandedField === field.id}
|
|
316
330
|
>
|
|
317
|
-
<div
|
|
331
|
+
<div
|
|
332
|
+
role='none'
|
|
333
|
+
className={mx(
|
|
334
|
+
subtleHover,
|
|
335
|
+
'grid grid-cols-subgrid col-span-5',
|
|
336
|
+
'rounded-xs cursor-pointer min-h-10',
|
|
337
|
+
)}
|
|
338
|
+
>
|
|
318
339
|
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
319
340
|
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
320
341
|
{field.path}
|
|
321
342
|
</List.ItemTitle>
|
|
322
|
-
<List.
|
|
343
|
+
<List.ItemIconButton
|
|
323
344
|
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
324
345
|
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
325
346
|
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
@@ -330,16 +351,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
330
351
|
{!readonly && (
|
|
331
352
|
<>
|
|
332
353
|
<List.ItemDeleteButton
|
|
333
|
-
label={t('delete
|
|
354
|
+
label={t('delete-field.label')}
|
|
334
355
|
autoHide={false}
|
|
335
|
-
disabled={readonly || schemaReadonly ||
|
|
356
|
+
disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
|
|
336
357
|
onClick={() => handleDelete(field.id)}
|
|
337
358
|
data-testid='field.delete'
|
|
338
359
|
/>
|
|
339
|
-
<
|
|
360
|
+
<List.ItemIconButton
|
|
340
361
|
iconOnly
|
|
341
362
|
variant='ghost'
|
|
342
|
-
label={t('toggle
|
|
363
|
+
label={t('toggle-expand.label', { ns: osTranslations })}
|
|
343
364
|
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
344
365
|
onClick={() => handleToggleField(field)}
|
|
345
366
|
data-testid='field.toggle'
|
|
@@ -348,7 +369,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
348
369
|
)}
|
|
349
370
|
</div>
|
|
350
371
|
{expandedField === field.id && !readonly && (
|
|
351
|
-
<div role='none' className='col-span-
|
|
372
|
+
<div role='none' className='col-span-full mt-1 mb-1 border border-separator rounded-md'>
|
|
352
373
|
<FieldEditor
|
|
353
374
|
readonly={readonly || schemaReadonly}
|
|
354
375
|
registry={registry}
|
|
@@ -362,6 +383,17 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
362
383
|
);
|
|
363
384
|
})}
|
|
364
385
|
</div>
|
|
386
|
+
{!readonly && !expandedField && (
|
|
387
|
+
<div role='none' className='my-form-padding'>
|
|
388
|
+
<IconButton
|
|
389
|
+
icon='ph--plus--regular'
|
|
390
|
+
label={t('add-property-button.label')}
|
|
391
|
+
onClick={handleAdd}
|
|
392
|
+
disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
393
|
+
classNames='w-full'
|
|
394
|
+
/>
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
365
397
|
</>
|
|
366
398
|
)}
|
|
367
399
|
</List.Root>
|
package/src/components/index.ts
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
export * from './ControlSection';
|
|
6
5
|
export * from './FieldEditor';
|
|
7
6
|
export * from './Form';
|
|
7
|
+
export * from './ObjectForm';
|
|
8
8
|
export * from './ObjectPicker';
|
|
9
|
+
export * from './ObjectProperties';
|
|
10
|
+
export * from './Settings';
|
|
9
11
|
export * from './ViewEditor';
|
|
@@ -4,19 +4,27 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { textBlockWidth } from '@dxos/ui-theme';
|
|
7
|
+
import { Column, type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
+
import { Json } from '@dxos/react-ui-syntax-highlighter';
|
|
9
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
10
|
|
|
12
11
|
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
13
12
|
|
|
14
13
|
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
14
|
return (
|
|
16
|
-
<div className='
|
|
17
|
-
<TestPanel classNames={['
|
|
15
|
+
<div className='h-full w-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
|
|
16
|
+
<TestPanel classNames={['dx-container', classNames]}>
|
|
17
|
+
<Column.Root classNames='dx-container' gutter='sm'>
|
|
18
|
+
{children}
|
|
19
|
+
</Column.Root>
|
|
20
|
+
</TestPanel>
|
|
18
21
|
<TestPanel classNames={'overflow-hidden'}>
|
|
19
|
-
<
|
|
22
|
+
<Json.Root data={json}>
|
|
23
|
+
<Json.Content classNames='h-full'>
|
|
24
|
+
<Json.Filter />
|
|
25
|
+
<Json.Data testId='debug' classNames='text-xs' />
|
|
26
|
+
</Json.Content>
|
|
27
|
+
</Json.Root>
|
|
20
28
|
</TestPanel>
|
|
21
29
|
</div>
|
|
22
30
|
);
|
|
@@ -25,7 +33,11 @@ export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
|
25
33
|
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
26
34
|
|
|
27
35
|
const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
28
|
-
return
|
|
36
|
+
return (
|
|
37
|
+
<div role='none' className={mx(['dx-document h-full bg-modal-surface rounded-md', classNames])}>
|
|
38
|
+
{children}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
29
41
|
};
|
|
30
42
|
|
|
31
43
|
// Symbol for accessing debug objects in tests.
|
|
@@ -8,13 +8,7 @@ import type * as SchemaAST from 'effect/SchemaAST';
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
10
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
|
-
import {
|
|
12
|
-
type JsonPath,
|
|
13
|
-
createJsonPath,
|
|
14
|
-
fromEffectValidationPath,
|
|
15
|
-
getValue as getValue$,
|
|
16
|
-
setValue as setValue$,
|
|
17
|
-
} from '@dxos/effect';
|
|
11
|
+
import { type JsonPath, createJsonPath, fromEffectValidationPath, getValue as getValue$ } from '@dxos/effect';
|
|
18
12
|
import { log } from '@dxos/log';
|
|
19
13
|
import { useDefaultValue } from '@dxos/react-ui';
|
|
20
14
|
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
@@ -136,11 +130,15 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
136
130
|
prop: valuesProp,
|
|
137
131
|
defaultProp: defaultValues,
|
|
138
132
|
onChange: () => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
133
|
+
// Only reset when controlled: parent passed new values.
|
|
134
|
+
// Uncontrolled updates come from setValues in onValueChange.
|
|
135
|
+
if (valuesProp !== undefined) {
|
|
136
|
+
setValues(valuesProp ?? defaultValues);
|
|
137
|
+
setChanged({});
|
|
138
|
+
setTouched({});
|
|
139
|
+
setErrors({});
|
|
140
|
+
setSaving(false);
|
|
141
|
+
}
|
|
144
142
|
},
|
|
145
143
|
});
|
|
146
144
|
// TODO(wittjosiah): Upgrade @radix-ui/react-use-controllable-state.
|
|
@@ -256,6 +254,7 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
256
254
|
log('onValueChange', { path, value });
|
|
257
255
|
|
|
258
256
|
const jsonPath = createJsonPath(path);
|
|
257
|
+
const pathArray = path;
|
|
259
258
|
let parsedValue = value as any;
|
|
260
259
|
try {
|
|
261
260
|
if (type._tag === 'NumberKeyword') {
|
|
@@ -266,21 +265,25 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
266
265
|
parsedValue = undefined;
|
|
267
266
|
}
|
|
268
267
|
|
|
269
|
-
//
|
|
270
|
-
const newValues =
|
|
271
|
-
setValues(newValues);
|
|
268
|
+
// Create merged values for validation and callback. Form never mutates; parent applies via onValuesChanged.
|
|
269
|
+
const newValues = mergeAtPath(values, pathArray as (string | number)[], parsedValue) as Partial<T>;
|
|
272
270
|
|
|
273
271
|
// TODO(burdon): Check value has changed from original.
|
|
274
272
|
const newChanged = { [jsonPath]: true };
|
|
275
|
-
setChanged({ ...
|
|
273
|
+
setChanged((prev) => ({ ...prev, ...newChanged }));
|
|
276
274
|
|
|
277
275
|
// Validate.
|
|
278
276
|
const isValid = validate(newValues);
|
|
279
277
|
|
|
280
|
-
//
|
|
278
|
+
// Notify parent; parent is responsible for applying the change.
|
|
281
279
|
onValuesChanged?.(newValues, { isValid, changed: newChanged });
|
|
280
|
+
|
|
281
|
+
// For uncontrolled mode, update internal state. Controlled mode relies on parent to pass new values.
|
|
282
|
+
if (valuesProp === undefined) {
|
|
283
|
+
setValues(newValues);
|
|
284
|
+
}
|
|
282
285
|
},
|
|
283
|
-
[values,
|
|
286
|
+
[values, validate, onValuesChanged, valuesProp],
|
|
284
287
|
);
|
|
285
288
|
|
|
286
289
|
const onBlur = useCallback(
|
|
@@ -341,6 +344,27 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
341
344
|
);
|
|
342
345
|
};
|
|
343
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Creates a new object with value at path. Does not mutate source.
|
|
349
|
+
* Preserves arrays when path segments are numeric indices.
|
|
350
|
+
*/
|
|
351
|
+
const mergeAtPath = (obj: any, path: readonly (string | number)[], value: any): any => {
|
|
352
|
+
if (path.length === 0) {
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const [head, ...rest] = path;
|
|
357
|
+
const child = rest.length === 0 ? value : mergeAtPath(obj?.[head], rest, value);
|
|
358
|
+
|
|
359
|
+
if (Array.isArray(obj)) {
|
|
360
|
+
const copy = [...obj];
|
|
361
|
+
copy[head as number] = child;
|
|
362
|
+
return copy;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { ...obj, [head]: child };
|
|
366
|
+
};
|
|
367
|
+
|
|
344
368
|
const flatMap = (errors: ValidationError[]) => {
|
|
345
369
|
return errors.reduce(
|
|
346
370
|
(result, { path, message }) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { useCallback,
|
|
5
|
+
import { type RefObject, useCallback, useLayoutEffect } from 'react';
|
|
6
6
|
|
|
7
7
|
import { addEventListener } from '@dxos/async';
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ import { type FormHandler } from './useFormHandler';
|
|
|
11
11
|
/**
|
|
12
12
|
* Key handler.
|
|
13
13
|
*/
|
|
14
|
-
export const useKeyHandler = (
|
|
14
|
+
export const useKeyHandler = (elRef: RefObject<HTMLDivElement | null>, form: FormHandler<any>) => {
|
|
15
15
|
const handleKeyDown = useCallback(
|
|
16
16
|
(event: KeyboardEvent) => {
|
|
17
17
|
switch (event.key) {
|
|
@@ -26,12 +26,12 @@ export const useKeyHandler = (formElement: HTMLDivElement | null, form: FormHand
|
|
|
26
26
|
[form.isValid, form.canSave, form.onSave, form.autoSave],
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
useLayoutEffect(() => {
|
|
30
|
+
const el = elRef.current;
|
|
31
|
+
if (!el) {
|
|
31
32
|
return;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}, [formElement, handleKeyDown]);
|
|
35
|
+
return addEventListener(el, 'keydown', handleKeyDown);
|
|
36
|
+
}, [elRef, handleKeyDown]);
|
|
37
37
|
};
|