@dxos/react-ui-form 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae
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 +2314 -2078
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2314 -2078
- 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 +8 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +82 -5
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +81 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +37 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -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/Settings/Settings.d.ts +56 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -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 +22 -10
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -8
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -10
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +2 -3
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +51 -42
- package/src/components/FieldEditor/FieldEditor.stories.tsx +51 -32
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +87 -83
- package/src/components/Form/Form.stories.tsx +156 -219
- package/src/components/Form/Form.tsx +347 -96
- package/src/components/Form/FormField.tsx +294 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +172 -0
- package/src/components/Form/FormFieldSet.tsx +123 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +149 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +189 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/fields/SelectOptionField.stories.tsx +64 -0
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +46 -41
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +210 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
- package/src/components/{ControlSection → ObjectPicker}/index.ts +1 -1
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +230 -0
- package/src/components/Settings/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +140 -62
- package/src/components/ViewEditor/ViewEditor.test.tsx +17 -18
- package/src/components/ViewEditor/ViewEditor.tsx +341 -202
- package/src/components/index.ts +2 -3
- package/src/components/testing.tsx +22 -11
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +21 -12
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +1 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +12 -18
- 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 -8
- 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/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -57
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -26
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -12
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- 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/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -41
- package/src/components/ControlSection/ControlSection.tsx +0 -139
- package/src/components/Form/ArrayField.tsx +0 -124
- package/src/components/Form/Defaults.tsx +0 -203
- package/src/components/Form/FormActions.tsx +0 -53
- package/src/components/Form/FormContent.tsx +0 -240
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -51
- package/src/components/Form/RefField.tsx +0 -213
- package/src/components/Form/custom/GeoPointInput.tsx +0 -72
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +0 -61
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- 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
- package/src/hooks/useForm.ts +0 -291
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -14
|
@@ -2,269 +2,408 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Array from 'effect/Array';
|
|
7
|
+
import * as Match from 'effect/Match';
|
|
8
|
+
import * as Option from 'effect/Option';
|
|
9
|
+
import * as Schema from 'effect/Schema';
|
|
10
|
+
import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMemo, useState } from 'react';
|
|
7
11
|
|
|
8
|
-
import { type SchemaRegistry } from '@dxos/echo
|
|
9
|
-
import {
|
|
12
|
+
import { Entity, Feed, Filter, Format, Obj, Query, QueryAST, Ref, type SchemaRegistry } from '@dxos/echo';
|
|
13
|
+
import { View } from '@dxos/echo';
|
|
14
|
+
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
10
15
|
import { invariant } from '@dxos/invariant';
|
|
11
|
-
import {
|
|
16
|
+
import { useObject, useQuery } from '@dxos/react-client/echo';
|
|
17
|
+
import { IconButton, Input, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
18
|
+
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
12
19
|
import { List } from '@dxos/react-ui-list';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { FieldSchema, type FieldType, VIEW_FIELD_LIMIT, type DataType, ProjectionModel } from '@dxos/schema';
|
|
20
|
+
import { ProjectionModel, VIEW_FIELD_LIMIT, createEchoChangeCallback, getTypenameFromQuery } from '@dxos/schema';
|
|
21
|
+
import { mx, osTranslations, subtleHover } from '@dxos/ui-theme';
|
|
16
22
|
|
|
17
23
|
import { translationKey } from '../../translations';
|
|
18
24
|
import { FieldEditor } from '../FieldEditor';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}).pipe(Schema.mutable);
|
|
28
|
-
|
|
29
|
-
type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
|
|
25
|
+
import {
|
|
26
|
+
Form,
|
|
27
|
+
type FormFieldComponent,
|
|
28
|
+
type FormFieldComponentProps,
|
|
29
|
+
FormFieldLabel,
|
|
30
|
+
type FormFieldMap,
|
|
31
|
+
type FormRootProps,
|
|
32
|
+
} from '../Form';
|
|
30
33
|
|
|
31
34
|
export type ViewEditorProps = ThemedClassName<
|
|
32
35
|
{
|
|
33
36
|
schema: Schema.Schema.AnyNoContext;
|
|
34
|
-
view:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
view: View.View;
|
|
38
|
+
mode?: 'schema' | 'tag';
|
|
39
|
+
registry?: SchemaRegistry.SchemaRegistry;
|
|
37
40
|
showHeading?: boolean;
|
|
38
|
-
|
|
41
|
+
onQueryChanged?: (query: QueryAST.Query, target?: string) => void;
|
|
39
42
|
onDelete?: (fieldId: string) => void;
|
|
40
|
-
} & Pick<
|
|
43
|
+
} & Pick<QueryFormProps, 'types' | 'tags'> &
|
|
44
|
+
Pick<FormRootProps<any>, 'readonly' | 'db'>
|
|
41
45
|
>;
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
48
|
* Schema-based object form.
|
|
45
49
|
*/
|
|
46
|
-
export const ViewEditor = (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
classNames,
|
|
54
|
+
schema,
|
|
55
|
+
view,
|
|
56
|
+
mode = 'schema',
|
|
57
|
+
registry,
|
|
58
|
+
db,
|
|
59
|
+
readonly,
|
|
60
|
+
showHeading = false,
|
|
61
|
+
types,
|
|
62
|
+
tags,
|
|
63
|
+
onQueryChanged,
|
|
64
|
+
onDelete,
|
|
65
|
+
},
|
|
66
|
+
forwardedRef,
|
|
67
|
+
) => {
|
|
68
|
+
const atomRegistry = useContext(RegistryContext);
|
|
69
|
+
const schemaReadonly = !isMutable(schema);
|
|
70
|
+
const { t } = useTranslation(translationKey);
|
|
71
|
+
|
|
72
|
+
const projectionModel = useMemo(() => {
|
|
73
|
+
// Use reactive and mutable version of json schema when schema is mutable.
|
|
74
|
+
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
75
|
+
|
|
76
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
77
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
78
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
79
|
+
|
|
80
|
+
const model = new ProjectionModel({
|
|
81
|
+
registry: atomRegistry,
|
|
82
|
+
view,
|
|
83
|
+
baseSchema: jsonSchema,
|
|
84
|
+
change,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return model;
|
|
88
|
+
}, [atomRegistry, schema, view]);
|
|
89
|
+
|
|
90
|
+
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
91
|
+
|
|
92
|
+
const queueTarget = Match.value(view.query.ast).pipe(
|
|
93
|
+
Match.when({ type: 'from' }, ({ from }) => {
|
|
94
|
+
if (from._tag !== 'scope') {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return Option.fromNullable(from.scope.queues).pipe(
|
|
98
|
+
Option.flatMap((queues) => Array.head(queues)),
|
|
99
|
+
Option.map(String),
|
|
100
|
+
Option.getOrUndefined,
|
|
101
|
+
);
|
|
102
|
+
}),
|
|
103
|
+
Match.orElse(() => undefined),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const feeds = useQuery(db, Filter.type(Feed.Feed));
|
|
107
|
+
|
|
108
|
+
const targetRef = useMemo(() => {
|
|
109
|
+
if (!queueTarget) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const feed = feeds.find((feed) => Feed.getQueueDxn(feed)?.toString() === queueTarget);
|
|
113
|
+
return feed ? Ref.fromDXN(Entity.getDXN(feed)) : undefined;
|
|
114
|
+
}, [queueTarget, feeds]);
|
|
115
|
+
|
|
116
|
+
const viewSchema = useMemo(() => {
|
|
117
|
+
const base = Schema.Struct({
|
|
118
|
+
query:
|
|
119
|
+
mode === 'schema'
|
|
120
|
+
? Format.URL.annotations({ title: 'Record type' })
|
|
121
|
+
: QueryAST.Query.annotations({ title: 'Query' }),
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (mode === 'tag') {
|
|
125
|
+
return Schema.Struct({
|
|
126
|
+
...base.fields,
|
|
127
|
+
target: Schema.optional(Ref.Ref(Feed.Feed).annotations({ title: 'Target Feed' })),
|
|
128
|
+
}).pipe(Schema.mutable);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return base.pipe(Schema.mutable);
|
|
132
|
+
}, [mode]);
|
|
133
|
+
|
|
134
|
+
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
135
|
+
// TODO(burdon): Settings should have domain name owned by user.
|
|
136
|
+
const viewValues = useMemo(
|
|
137
|
+
() => ({
|
|
138
|
+
query: mode === 'schema' ? getTypenameFromQuery(view.query.ast) : view.query.ast,
|
|
139
|
+
target: targetRef,
|
|
140
|
+
}),
|
|
141
|
+
[mode, view.query.ast, targetRef],
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const fieldMap = useMemo<FormFieldMap | undefined>(
|
|
145
|
+
() => (mode === 'tag' ? customFields({ types, tags }) : undefined),
|
|
146
|
+
[mode, types, tags],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const handleUpdate = useCallback(
|
|
150
|
+
(values: any) => {
|
|
151
|
+
const targetValue = values.target;
|
|
152
|
+
let queueDxn: string | undefined;
|
|
153
|
+
|
|
154
|
+
if (Ref.isRef(targetValue)) {
|
|
155
|
+
const feedDxn = targetValue.dxn.toString();
|
|
156
|
+
const feed = feeds.find((feed) => Obj.getDXN(feed).toString() === feedDxn);
|
|
157
|
+
if (feed) {
|
|
158
|
+
queueDxn = Feed.getQueueDxn(feed)?.toString();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// TODO(wittjosiah): Deep-clone the AST to plain JS or ECHO proxy arrays become objects with numeric keys.
|
|
163
|
+
const query =
|
|
164
|
+
mode === 'schema'
|
|
165
|
+
? Query.select(Filter.typename(values.query)).ast
|
|
166
|
+
: JSON.parse(JSON.stringify(values.query));
|
|
167
|
+
onQueryChanged?.(query, queueDxn);
|
|
168
|
+
},
|
|
169
|
+
[onQueryChanged, mode, feeds],
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const handleDelete = useCallback(
|
|
173
|
+
(fieldId: string) => {
|
|
174
|
+
invariant(!readonly);
|
|
175
|
+
onDelete?.(fieldId);
|
|
176
|
+
},
|
|
177
|
+
[onDelete, readonly],
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div role='none' className={mx(classNames)}>
|
|
182
|
+
{/* If readonly is set, then the callout is not needed. */}
|
|
183
|
+
{schemaReadonly && !readonly && (
|
|
184
|
+
<Message.Root valence='info' classNames='my-form-padding'>
|
|
185
|
+
<Message.Title>{t('system schema description')}</Message.Title>
|
|
186
|
+
</Message.Root>
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
190
|
+
<Form.Root schema={viewSchema} values={viewValues} fieldMap={fieldMap} db={db} onValuesChanged={handleUpdate}>
|
|
191
|
+
<Form.FieldSet />
|
|
192
|
+
|
|
193
|
+
<FormFieldLabel label={t('fields label')} asChild />
|
|
194
|
+
<FieldList
|
|
195
|
+
schema={schema}
|
|
196
|
+
view={view}
|
|
197
|
+
registry={registry}
|
|
198
|
+
readonly={readonly}
|
|
199
|
+
showHeading={showHeading}
|
|
200
|
+
onDelete={handleDelete}
|
|
201
|
+
/>
|
|
202
|
+
</Form.Root>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
type FieldListProps = Pick<ViewEditorProps, 'schema' | 'view' | 'registry' | 'readonly' | 'showHeading' | 'onDelete'>;
|
|
209
|
+
|
|
210
|
+
const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDelete }: FieldListProps) => {
|
|
211
|
+
const atomRegistry = useContext(RegistryContext);
|
|
212
|
+
const schemaReadonly = !isMutable(schema);
|
|
57
213
|
const { t } = useTranslation(translationKey);
|
|
58
|
-
|
|
214
|
+
|
|
215
|
+
// Subscribe to view changes for reactivity.
|
|
216
|
+
const [viewSnapshot] = useObject(view);
|
|
217
|
+
|
|
218
|
+
const projectionModel = useMemo(() => {
|
|
59
219
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
60
220
|
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));
|
|
81
|
-
},
|
|
82
|
-
[readonly],
|
|
83
|
-
);
|
|
84
221
|
|
|
85
|
-
|
|
222
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
223
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
224
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
225
|
+
|
|
226
|
+
const model = new ProjectionModel({
|
|
227
|
+
registry: atomRegistry,
|
|
228
|
+
view,
|
|
229
|
+
baseSchema: jsonSchema,
|
|
230
|
+
change,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return model;
|
|
234
|
+
}, [atomRegistry, schema, view]);
|
|
235
|
+
|
|
236
|
+
const [expandedField, setExpandedField] = useState<View.FieldType['id']>();
|
|
86
237
|
|
|
87
238
|
const handleAdd = useCallback(() => {
|
|
88
239
|
invariant(!readonly);
|
|
89
|
-
const field =
|
|
90
|
-
|
|
91
|
-
}, [
|
|
240
|
+
const field = projectionModel.createFieldProjection();
|
|
241
|
+
setExpandedField(field.id);
|
|
242
|
+
}, [projectionModel, readonly]);
|
|
92
243
|
|
|
93
|
-
const
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
requestAnimationFrame(() => {
|
|
97
|
-
if (view.query.typename !== typename && !readonly) {
|
|
98
|
-
onTypenameChanged?.(typename);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
244
|
+
const handleToggleField = useCallback(
|
|
245
|
+
(field: View.FieldType) => {
|
|
246
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
101
247
|
},
|
|
102
|
-
[
|
|
248
|
+
[readonly],
|
|
103
249
|
);
|
|
104
250
|
|
|
105
251
|
const handleDelete = useCallback(
|
|
106
252
|
(fieldId: string) => {
|
|
107
253
|
invariant(!readonly);
|
|
108
|
-
if (fieldId ===
|
|
109
|
-
|
|
254
|
+
if (fieldId === expandedField) {
|
|
255
|
+
setExpandedField(undefined);
|
|
110
256
|
}
|
|
111
257
|
|
|
112
258
|
onDelete?.(fieldId);
|
|
113
259
|
},
|
|
114
|
-
[
|
|
260
|
+
[expandedField, onDelete, readonly],
|
|
115
261
|
);
|
|
116
262
|
|
|
117
263
|
const handleMove = useCallback(
|
|
118
264
|
(fromIndex: number, toIndex: number) => {
|
|
119
265
|
invariant(!readonly);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
266
|
+
Obj.change(view, (v) => {
|
|
267
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
268
|
+
const fields = [...v.projection.fields];
|
|
269
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
270
|
+
fields.splice(toIndex, 0, moved);
|
|
271
|
+
v.projection.fields = fields;
|
|
272
|
+
});
|
|
125
273
|
},
|
|
126
|
-
[view
|
|
274
|
+
[view, readonly],
|
|
127
275
|
);
|
|
128
276
|
|
|
129
|
-
const handleClose = useCallback(() =>
|
|
130
|
-
|
|
131
|
-
const hiddenProperties = projection.getHiddenProperties();
|
|
277
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
132
278
|
|
|
133
279
|
const handleHide = useCallback(
|
|
134
280
|
(fieldId: string) => {
|
|
135
|
-
|
|
136
|
-
|
|
281
|
+
setExpandedField(undefined);
|
|
282
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
137
283
|
},
|
|
138
|
-
[
|
|
284
|
+
[projectionModel],
|
|
139
285
|
);
|
|
140
286
|
|
|
141
287
|
const handleShow = useCallback(
|
|
142
288
|
(property: string) => {
|
|
143
|
-
|
|
144
|
-
|
|
289
|
+
setExpandedField(undefined);
|
|
290
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
145
291
|
},
|
|
146
|
-
[
|
|
292
|
+
[projectionModel],
|
|
147
293
|
);
|
|
148
294
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
<div role='none' className={mx('is-full', outerSpacing && 'plb-cardSpacingBlock pli-cardSpacingInline')}>
|
|
153
|
-
<Callout.Root valence='info'>
|
|
154
|
-
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
155
|
-
</Callout.Root>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
295
|
+
if (!viewSnapshot) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
158
298
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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')}
|
|
299
|
+
return (
|
|
300
|
+
<List.Root<View.FieldType>
|
|
301
|
+
items={viewSnapshot.projection.fields as View.FieldType[]}
|
|
302
|
+
isItem={Schema.is(View.FieldSchema)}
|
|
303
|
+
getId={(field) => field.id}
|
|
304
|
+
onMove={readonly ? undefined : handleMove}
|
|
305
|
+
readonly={readonly}
|
|
306
|
+
>
|
|
307
|
+
{({ items: fields }) => (
|
|
308
|
+
<>
|
|
309
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
310
|
+
<div role='list' className='grid grid-cols-[min-content_1fr_min-content_min-content_min-content]'>
|
|
311
|
+
{fields?.map((field) => {
|
|
312
|
+
const hidden = field.visible === false;
|
|
313
|
+
return (
|
|
314
|
+
<List.Item<View.FieldType>
|
|
315
|
+
key={field.id}
|
|
316
|
+
item={field}
|
|
317
|
+
classNames={'grid grid-cols-subgrid col-span-5'}
|
|
318
|
+
aria-expanded={expandedField === field.id}
|
|
319
|
+
>
|
|
320
|
+
<div
|
|
321
|
+
role='none'
|
|
322
|
+
className={mx(
|
|
323
|
+
subtleHover,
|
|
324
|
+
'grid grid-cols-subgrid col-span-5',
|
|
325
|
+
'rounded-xs cursor-pointer min-h-10',
|
|
326
|
+
)}
|
|
195
327
|
>
|
|
196
|
-
<List.ItemDragHandle />
|
|
197
|
-
<List.ItemTitle onClick={() =>
|
|
328
|
+
<List.ItemDragHandle disabled={readonly || schemaReadonly} />
|
|
329
|
+
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
330
|
+
{field.path}
|
|
331
|
+
</List.ItemTitle>
|
|
198
332
|
<List.ItemButton
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
333
|
+
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
334
|
+
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
335
|
+
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
336
|
+
autoHide={false}
|
|
337
|
+
disabled={readonly || (!hidden && projectionModel.getFields().length <= 1)}
|
|
338
|
+
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
204
339
|
/>
|
|
205
|
-
{/* TODO(burdon): Remove unless implement undo. */}
|
|
206
340
|
{!readonly && (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
341
|
+
<>
|
|
342
|
+
<List.ItemDeleteButton
|
|
343
|
+
label={t('delete field label')}
|
|
344
|
+
autoHide={false}
|
|
345
|
+
disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
|
|
346
|
+
onClick={() => handleDelete(field.id)}
|
|
347
|
+
data-testid='field.delete'
|
|
348
|
+
/>
|
|
349
|
+
<IconButton
|
|
350
|
+
iconOnly
|
|
351
|
+
variant='ghost'
|
|
352
|
+
label={t('toggle expand label', { ns: osTranslations })}
|
|
353
|
+
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
354
|
+
onClick={() => handleToggleField(field)}
|
|
355
|
+
data-testid='field.toggle'
|
|
356
|
+
/>
|
|
357
|
+
</>
|
|
211
358
|
)}
|
|
212
|
-
</
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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'
|
|
359
|
+
</div>
|
|
360
|
+
{expandedField === field.id && !readonly && (
|
|
361
|
+
<div role='none' className='col-span-5 mt-1 mb-1 border border-separator rounded-md'>
|
|
362
|
+
<FieldEditor
|
|
363
|
+
readonly={readonly || schemaReadonly}
|
|
364
|
+
registry={registry}
|
|
365
|
+
projection={projectionModel}
|
|
366
|
+
field={field}
|
|
367
|
+
onSave={handleClose}
|
|
242
368
|
/>
|
|
243
|
-
</
|
|
244
|
-
)
|
|
245
|
-
</
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
onClick={readonly ? undefined : handleAdd}
|
|
262
|
-
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
263
|
-
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
264
|
-
classNames='flex is-full'
|
|
265
|
-
/>
|
|
266
|
-
</div>
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
</List.Item>
|
|
372
|
+
);
|
|
373
|
+
})}
|
|
374
|
+
</div>
|
|
375
|
+
{!readonly && !expandedField && (
|
|
376
|
+
<div role='none' className='my-form-padding'>
|
|
377
|
+
<IconButton
|
|
378
|
+
icon='ph--plus--regular'
|
|
379
|
+
label={t('add property button label')}
|
|
380
|
+
onClick={handleAdd}
|
|
381
|
+
disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
382
|
+
classNames='w-full'
|
|
383
|
+
/>
|
|
384
|
+
</div>
|
|
385
|
+
)}
|
|
386
|
+
</>
|
|
267
387
|
)}
|
|
268
|
-
</
|
|
388
|
+
</List.Root>
|
|
269
389
|
);
|
|
270
390
|
};
|
|
391
|
+
|
|
392
|
+
const customFields = ({
|
|
393
|
+
types,
|
|
394
|
+
tags,
|
|
395
|
+
}: Pick<ViewEditorProps, 'types' | 'tags'>): Record<string, FormFieldComponent> => ({
|
|
396
|
+
query: ({ type, readonly, label, getValue, onValueChange }: FormFieldComponentProps) => {
|
|
397
|
+
const handleChange = useCallback<NonNullable<QueryFormProps['onChange']>>(
|
|
398
|
+
(query) => onValueChange(type, query.ast),
|
|
399
|
+
[onValueChange],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<Input.Root>
|
|
404
|
+
<FormFieldLabel readonly={readonly} label={label} />
|
|
405
|
+
<QueryForm initialQuery={getValue()} types={types} tags={tags} onChange={handleChange} />
|
|
406
|
+
</Input.Root>
|
|
407
|
+
);
|
|
408
|
+
},
|
|
409
|
+
});
|
package/src/components/index.ts
CHANGED
|
@@ -5,20 +5,31 @@
|
|
|
5
5
|
import React, { type PropsWithChildren } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
8
|
-
import { Card } from '@dxos/react-ui-stack';
|
|
9
8
|
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
10
|
-
import { mx, textBlockWidth } from '@dxos/
|
|
9
|
+
import { mx, textBlockWidth } from '@dxos/ui-theme';
|
|
11
10
|
|
|
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
|
-
);
|
|
11
|
+
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
18
12
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<div className='w-full h-full grid grid-cols-[1fr_1fr] p-4 gap-4 overflow-hidden'>
|
|
16
|
+
<TestPanel classNames={['flex flex-col h-full overflow-y-auto', classNames]}>{children}</TestPanel>
|
|
17
|
+
<TestPanel classNames={'overflow-hidden'}>
|
|
18
|
+
<JsonFilter testId='debug' data={json} classNames='h-full text-xs' />
|
|
19
|
+
</TestPanel>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
25
|
+
|
|
26
|
+
const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
27
|
+
return (
|
|
28
|
+
<div role='none' className={mx(['h-full bg-modal-surface rounded-md', textBlockWidth, classNames])}>
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
22
33
|
|
|
23
34
|
// Symbol for accessing debug objects in tests.
|
|
24
35
|
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|