@dxos/react-ui-form 0.8.3 → 0.8.4-main.1f223c7
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 +648 -581
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +648 -581
- 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 +79 -4
- 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 +76 -6
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
- package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/Defaults.d.ts +5 -5
- package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +5 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +126 -30
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContent.d.ts +16 -10
- package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts +1 -1
- package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
- package/dist/types/src/components/Form/Input.d.ts +4 -1
- package/dist/types/src/components/Form/Input.d.ts.map +1 -1
- package/dist/types/src/components/Form/RefField.d.ts +10 -4
- 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 +74 -3
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
- package/dist/types/src/components/Form/factory.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -4
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +78 -7
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +6 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useForm.d.ts +1 -1
- package/dist/types/src/hooks/useForm.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +61 -57
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -33
- package/src/components/ControlSection/ControlSection.stories.tsx +10 -12
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +21 -20
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +17 -22
- package/src/components/Form/ArrayField.tsx +9 -9
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +131 -95
- package/src/components/Form/Form.tsx +16 -12
- package/src/components/Form/FormActions.tsx +2 -1
- package/src/components/Form/FormContent.tsx +96 -34
- package/src/components/Form/FormContext.tsx +5 -5
- package/src/components/Form/Input.tsx +4 -1
- package/src/components/Form/RefField.tsx +126 -40
- package/src/components/Form/custom/GeoPointInput.tsx +29 -21
- package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -8
- package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
- package/src/components/Form/custom/TupleInput.tsx +2 -2
- package/src/components/Form/factory.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +70 -32
- package/src/components/ViewEditor/ViewEditor.test.tsx +38 -39
- package/src/components/ViewEditor/ViewEditor.tsx +159 -155
- package/src/components/index.ts +0 -2
- package/src/components/testing.tsx +15 -9
- package/src/hooks/useForm.ts +5 -3
- package/src/hooks/useRefQueryLookupHandler.ts +1 -1
- package/src/index.ts +1 -1
- package/src/translations.ts +15 -7
- package/src/util/field.ts +8 -18
- package/dist/lib/node/index.cjs +0 -2416
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/types/src/components/ViewEditor/Test.stories.d.ts +0 -10
- package/dist/types/src/components/ViewEditor/Test.stories.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/src/components/ViewEditor/Test.stories.tsx +0 -45
- 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,43 +2,43 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Schema } from 'effect';
|
|
5
|
+
import { Match, Schema } from 'effect';
|
|
6
6
|
import React, { useCallback, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type SchemaRegistry } from '@dxos/echo-db';
|
|
9
9
|
import { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { getSpace } from '@dxos/react-client/echo';
|
|
11
12
|
import { Callout, IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
13
|
+
import { QueryEditor, QuerySerializer, createExpression } from '@dxos/react-ui-components';
|
|
12
14
|
import { List } from '@dxos/react-ui-list';
|
|
13
15
|
import { cardSpacing } from '@dxos/react-ui-stack';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
+
import { inputTextLabel, mx, subtleHover } from '@dxos/react-ui-theme';
|
|
17
|
+
import {
|
|
18
|
+
type DataType,
|
|
19
|
+
FieldSchema,
|
|
20
|
+
type FieldType,
|
|
21
|
+
ProjectionModel,
|
|
22
|
+
VIEW_FIELD_LIMIT,
|
|
23
|
+
typenameFromQuery,
|
|
24
|
+
} from '@dxos/schema';
|
|
16
25
|
|
|
17
26
|
import { translationKey } from '../../translations';
|
|
18
27
|
import { FieldEditor } from '../FieldEditor';
|
|
19
|
-
import { Form, type FormProps } from '../Form';
|
|
28
|
+
import { Form, type FormProps, type InputComponent, type InputProps } from '../Form';
|
|
20
29
|
|
|
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>;
|
|
30
|
+
const listGrid = 'grid grid-cols-[min-content_1fr_min-content_min-content_min-content]';
|
|
31
|
+
const listItemGrid = 'grid grid-cols-subgrid col-span-5';
|
|
33
32
|
|
|
34
33
|
export type ViewEditorProps = ThemedClassName<
|
|
35
34
|
{
|
|
36
35
|
schema: Schema.Schema.AnyNoContext;
|
|
37
|
-
view:
|
|
36
|
+
view: DataType.View;
|
|
37
|
+
kind?: 'basic' | 'advanced';
|
|
38
38
|
registry?: SchemaRegistry;
|
|
39
39
|
readonly?: boolean;
|
|
40
40
|
showHeading?: boolean;
|
|
41
|
-
|
|
41
|
+
onQueryChanged?: (query: string) => void;
|
|
42
42
|
onDelete?: (fieldId: string) => void;
|
|
43
43
|
} & Pick<FormProps<any>, 'outerSpacing'>
|
|
44
44
|
>;
|
|
@@ -50,38 +50,52 @@ export const ViewEditor = ({
|
|
|
50
50
|
classNames,
|
|
51
51
|
schema,
|
|
52
52
|
view,
|
|
53
|
+
kind = 'basic',
|
|
53
54
|
registry,
|
|
54
|
-
readonly
|
|
55
|
+
readonly,
|
|
55
56
|
showHeading = false,
|
|
56
|
-
|
|
57
|
+
onQueryChanged,
|
|
57
58
|
onDelete,
|
|
58
59
|
outerSpacing = true,
|
|
59
60
|
}: ViewEditorProps) => {
|
|
61
|
+
const space = getSpace(view);
|
|
62
|
+
const schemaReadonly = !isMutable(schema);
|
|
60
63
|
const { t } = useTranslation(translationKey);
|
|
61
|
-
const
|
|
64
|
+
const projectionModel = useMemo(() => {
|
|
62
65
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
63
66
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
64
|
-
return new
|
|
65
|
-
}, [schema, view]);
|
|
66
|
-
const [
|
|
67
|
-
const readonly = _readonly || !isMutable(schema);
|
|
67
|
+
return new ProjectionModel(jsonSchema, view.projection);
|
|
68
|
+
}, [schema, view.projection]);
|
|
69
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
68
70
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
typename: view.query.typename,
|
|
76
|
-
};
|
|
77
|
-
}, [view]);
|
|
71
|
+
const serializedQuery = Match.value(kind).pipe(
|
|
72
|
+
Match.when('basic', () => typenameFromQuery(view.query)),
|
|
73
|
+
Match.when('advanced', () => {
|
|
74
|
+
if (view.query.type !== 'select') {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
const serializer = new QuerySerializer();
|
|
79
|
+
return serializer.serialize(createExpression(view.query.filter));
|
|
80
|
+
}),
|
|
81
|
+
Match.exhaustive,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const viewSchema = useMemo(() => {
|
|
85
|
+
return Schema.Struct({
|
|
86
|
+
query:
|
|
87
|
+
kind === 'basic'
|
|
88
|
+
? Format.URL.annotations({ title: 'Record type' })
|
|
89
|
+
: Schema.String.annotations({ title: 'Query' }),
|
|
90
|
+
}).pipe(Schema.mutable);
|
|
91
|
+
}, [kind]);
|
|
92
|
+
// TODO(burdon): Need to warn user of possible consequences of editing.
|
|
93
|
+
// TODO(burdon): Settings should have domain name owned by user.
|
|
94
|
+
const viewValues = useMemo(() => ({ query: serializedQuery }), [serializedQuery]);
|
|
95
|
+
|
|
96
|
+
const handleToggleField = useCallback(
|
|
80
97
|
(field: FieldType) => {
|
|
81
|
-
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
setField((f) => (f === field ? undefined : field));
|
|
98
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
85
99
|
},
|
|
86
100
|
[readonly],
|
|
87
101
|
);
|
|
@@ -90,193 +104,183 @@ export const ViewEditor = ({
|
|
|
90
104
|
|
|
91
105
|
const handleAdd = useCallback(() => {
|
|
92
106
|
invariant(!readonly);
|
|
93
|
-
const field =
|
|
94
|
-
|
|
95
|
-
}, [schema,
|
|
107
|
+
const field = projectionModel.createFieldProjection();
|
|
108
|
+
setExpandedField(field.id);
|
|
109
|
+
}, [schema, projectionModel, readonly]);
|
|
96
110
|
|
|
97
111
|
const handleUpdate = useCallback(
|
|
98
|
-
({
|
|
112
|
+
({ query }: Schema.Schema.Type<typeof viewSchema>) => {
|
|
99
113
|
invariant(!readonly);
|
|
100
114
|
requestAnimationFrame(() => {
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (view.query.typename !== typename && !readonly) {
|
|
106
|
-
onTypenameChanged?.(typename);
|
|
115
|
+
if (serializedQuery !== query && !readonly) {
|
|
116
|
+
onQueryChanged?.(query);
|
|
107
117
|
}
|
|
108
118
|
});
|
|
109
119
|
},
|
|
110
|
-
[
|
|
120
|
+
[serializedQuery, onQueryChanged, readonly],
|
|
111
121
|
);
|
|
112
122
|
|
|
113
123
|
const handleDelete = useCallback(
|
|
114
124
|
(fieldId: string) => {
|
|
115
125
|
invariant(!readonly);
|
|
116
|
-
if (fieldId ===
|
|
117
|
-
|
|
126
|
+
if (fieldId === expandedField) {
|
|
127
|
+
setExpandedField(undefined);
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
onDelete?.(fieldId);
|
|
121
131
|
},
|
|
122
|
-
[
|
|
132
|
+
[expandedField, onDelete, readonly],
|
|
123
133
|
);
|
|
124
134
|
|
|
125
135
|
const handleMove = useCallback(
|
|
126
136
|
(fromIndex: number, toIndex: number) => {
|
|
127
137
|
invariant(!readonly);
|
|
128
138
|
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
129
|
-
const fields = [...view.fields];
|
|
139
|
+
const fields = [...view.projection.fields];
|
|
130
140
|
const [moved] = fields.splice(fromIndex, 1);
|
|
131
141
|
fields.splice(toIndex, 0, moved);
|
|
132
|
-
view.fields = fields;
|
|
142
|
+
view.projection.fields = fields;
|
|
133
143
|
},
|
|
134
|
-
[
|
|
144
|
+
[view.projection.fields, readonly],
|
|
135
145
|
);
|
|
136
146
|
|
|
137
|
-
const handleClose = useCallback(() =>
|
|
138
|
-
|
|
139
|
-
const hiddenProperties = projection.getHiddenProperties();
|
|
147
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
140
148
|
|
|
141
149
|
const handleHide = useCallback(
|
|
142
150
|
(fieldId: string) => {
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
setExpandedField(undefined);
|
|
152
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
145
153
|
},
|
|
146
|
-
[
|
|
154
|
+
[projectionModel],
|
|
147
155
|
);
|
|
148
156
|
|
|
149
157
|
const handleShow = useCallback(
|
|
150
158
|
(property: string) => {
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
setExpandedField(undefined);
|
|
160
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
153
161
|
},
|
|
154
|
-
[
|
|
162
|
+
[projectionModel],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const custom: Partial<Record<string, InputComponent>> = useMemo(
|
|
166
|
+
() =>
|
|
167
|
+
kind === 'advanced'
|
|
168
|
+
? {
|
|
169
|
+
['query' satisfies keyof Schema.Schema.Type<typeof viewSchema>]: (props: InputProps) => {
|
|
170
|
+
const handleChange = useCallback(
|
|
171
|
+
(text: string) => props.onValueChange('string', text),
|
|
172
|
+
[props.onValueChange],
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// TODO(wittjosiah): Add label & input styles.
|
|
176
|
+
return <QueryEditor space={space} initialValue={props.getValue()} onChange={handleChange} />;
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
: {},
|
|
180
|
+
[],
|
|
155
181
|
);
|
|
156
182
|
|
|
157
183
|
return (
|
|
158
184
|
<div role='none' className={mx(classNames)}>
|
|
159
|
-
{
|
|
160
|
-
<
|
|
161
|
-
<Callout.
|
|
162
|
-
|
|
163
|
-
</Callout.Root>
|
|
164
|
-
</div>
|
|
185
|
+
{schemaReadonly && (
|
|
186
|
+
<Callout.Root valence='info' classNames={['mlb-cardSpacingBlock', outerSpacing && 'mli-cardSpacingInline']}>
|
|
187
|
+
<Callout.Title>{t('system schema description')}</Callout.Title>
|
|
188
|
+
</Callout.Root>
|
|
165
189
|
)}
|
|
166
190
|
|
|
167
191
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
168
192
|
{/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
|
|
169
|
-
<Form<
|
|
193
|
+
<Form<Schema.Schema.Type<typeof viewSchema>>
|
|
170
194
|
autoSave
|
|
171
|
-
schema={
|
|
195
|
+
schema={viewSchema}
|
|
172
196
|
values={viewValues}
|
|
173
|
-
readonly={readonly}
|
|
197
|
+
readonly={readonly ? 'disabled-input' : false}
|
|
174
198
|
onSave={handleUpdate}
|
|
175
199
|
outerSpacing={outerSpacing}
|
|
200
|
+
Custom={custom}
|
|
176
201
|
/>
|
|
177
202
|
|
|
178
203
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
179
|
-
<
|
|
204
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
180
205
|
|
|
181
206
|
<List.Root<FieldType>
|
|
182
|
-
items={view.fields}
|
|
207
|
+
items={view.projection.fields}
|
|
183
208
|
isItem={Schema.is(FieldSchema)}
|
|
184
209
|
getId={(field) => field.id}
|
|
185
|
-
readonly={readonly}
|
|
186
210
|
onMove={readonly ? undefined : handleMove}
|
|
211
|
+
readonly={readonly}
|
|
187
212
|
>
|
|
188
213
|
{({ items: fields }) => (
|
|
189
214
|
<>
|
|
190
|
-
{showHeading && (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
{showHeading && <h3 className='text-sm'>{t('field path label')}</h3>}
|
|
216
|
+
<div role='list' className={listGrid}>
|
|
217
|
+
{fields?.map((field) => {
|
|
218
|
+
const hidden = field.visible === false;
|
|
219
|
+
return (
|
|
220
|
+
<List.Item<FieldType>
|
|
221
|
+
key={field.id}
|
|
222
|
+
item={field}
|
|
223
|
+
classNames={listItemGrid}
|
|
224
|
+
aria-expanded={expandedField === field.id}
|
|
225
|
+
>
|
|
226
|
+
<div role='none' className={mx(subtleHover, listItemGrid, 'rounded-sm cursor-pointer min-bs-10')}>
|
|
227
|
+
<List.ItemDragHandle disabled={readonly} />
|
|
228
|
+
<List.ItemTitle classNames={hidden && 'text-subdued'} onClick={() => handleToggleField(field)}>
|
|
229
|
+
{field.path}
|
|
230
|
+
</List.ItemTitle>
|
|
231
|
+
<List.ItemButton
|
|
232
|
+
label={t(hidden ? 'show field label' : 'hide field label')}
|
|
233
|
+
data-testid={hidden ? 'show-field-button' : 'hide-field-button'}
|
|
234
|
+
icon={hidden ? 'ph--eye-closed--regular' : 'ph--eye--regular'}
|
|
235
|
+
autoHide={false}
|
|
236
|
+
disabled={readonly || (!hidden && projectionModel.fields.length <= 1)}
|
|
237
|
+
onClick={() => (hidden ? handleShow(field.path) : handleHide(field.id))}
|
|
238
|
+
/>
|
|
239
|
+
{/* TODO(burdon): Remove unless implement undo. */}
|
|
240
|
+
<List.ItemDeleteButton
|
|
241
|
+
label={t('delete field label')}
|
|
242
|
+
autoHide={false}
|
|
243
|
+
disabled={readonly || schemaReadonly || view.projection.fields.length <= 1}
|
|
244
|
+
onClick={() => handleDelete(field.id)}
|
|
245
|
+
data-testid='field.delete'
|
|
246
|
+
/>
|
|
247
|
+
<IconButton
|
|
248
|
+
iconOnly
|
|
249
|
+
variant='ghost'
|
|
250
|
+
label={t('toggle expand label', { ns: 'os' })}
|
|
251
|
+
icon={expandedField === field.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
252
|
+
onClick={() => handleToggleField(field)}
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
{expandedField === field.id && (
|
|
256
|
+
<div role='none' className='col-span-5 mbs-1 mbe-1 border border-separator rounded-md'>
|
|
257
|
+
<FieldEditor
|
|
258
|
+
readonly={readonly || schemaReadonly ? 'disabled-input' : false}
|
|
259
|
+
projection={projectionModel}
|
|
260
|
+
field={field}
|
|
261
|
+
registry={registry}
|
|
262
|
+
onSave={handleClose}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</List.Item>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
222
269
|
</div>
|
|
223
270
|
</>
|
|
224
271
|
)}
|
|
225
272
|
</List.Root>
|
|
226
|
-
|
|
227
|
-
{hiddenProperties.length > 0 && (
|
|
228
|
-
<>
|
|
229
|
-
<label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
|
|
230
|
-
|
|
231
|
-
<List.Root<string>
|
|
232
|
-
items={hiddenProperties}
|
|
233
|
-
isItem={(item): item is string => typeof item === 'string'}
|
|
234
|
-
getId={(property) => property}
|
|
235
|
-
>
|
|
236
|
-
{({ items: properties }) => (
|
|
237
|
-
<div role='list' className='flex flex-col is-full'>
|
|
238
|
-
{properties?.map((property) => (
|
|
239
|
-
<List.Item<string>
|
|
240
|
-
key={property}
|
|
241
|
-
item={property}
|
|
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
|
-
</>
|
|
257
|
-
)}
|
|
258
273
|
</div>
|
|
259
274
|
|
|
260
|
-
{
|
|
261
|
-
<FieldEditor
|
|
262
|
-
key={field.id}
|
|
263
|
-
view={view}
|
|
264
|
-
projection={projection}
|
|
265
|
-
field={field}
|
|
266
|
-
registry={registry}
|
|
267
|
-
onSave={handleClose}
|
|
268
|
-
/>
|
|
269
|
-
)}
|
|
270
|
-
|
|
271
|
-
{!readonly && !field && (
|
|
275
|
+
{!readonly && !expandedField && (
|
|
272
276
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
273
277
|
<IconButton
|
|
274
278
|
icon='ph--plus--regular'
|
|
275
279
|
label={t('button add property')}
|
|
276
280
|
onClick={readonly ? undefined : handleAdd}
|
|
277
281
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
278
|
-
disabled={view.fields.length >= VIEW_FIELD_LIMIT}
|
|
279
|
-
classNames='
|
|
282
|
+
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
283
|
+
classNames='is-full'
|
|
280
284
|
/>
|
|
281
285
|
</div>
|
|
282
286
|
)}
|
package/src/components/index.ts
CHANGED
|
@@ -9,16 +9,22 @@ import { Card } from '@dxos/react-ui-stack';
|
|
|
9
9
|
import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
|
|
10
10
|
import { mx, textBlockWidth } from '@dxos/react-ui-theme';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
<div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
14
|
-
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
15
|
-
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
12
|
+
type TestLayoutProps = ThemedClassName<PropsWithChildren<{ json?: any }>>;
|
|
18
13
|
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
export const TestLayout = ({ classNames, children, json }: TestLayoutProps) => {
|
|
15
|
+
return (
|
|
16
|
+
<div className='w-full h-full grid grid-cols-[1fr_1fr] gap-2 overflow-hidden'>
|
|
17
|
+
<div className={mx('p-4 justify-center overflow-y-auto', classNames)}>{children}</div>
|
|
18
|
+
<JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TestPanelProps = ThemedClassName<PropsWithChildren>;
|
|
24
|
+
|
|
25
|
+
export const TestPanel = ({ classNames, children }: TestPanelProps) => {
|
|
26
|
+
return <Card.StaticRoot classNames={[textBlockWidth, classNames]}>{children}</Card.StaticRoot>;
|
|
27
|
+
};
|
|
22
28
|
|
|
23
29
|
// Symbol for accessing debug objects in tests.
|
|
24
30
|
export const VIEW_EDITOR_DEBUG_SYMBOL = Symbol('viewEditorDebug');
|
package/src/hooks/useForm.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type Schema, SchemaAST } from 'effect';
|
|
6
6
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
|
|
9
|
-
import { type
|
|
9
|
+
import { type JsonPath, type SimpleType, createJsonPath, fromEffectValidationPath } from '@dxos/effect';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
11
|
import { log } from '@dxos/log';
|
|
12
|
-
import {
|
|
12
|
+
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
13
13
|
import { type MaybePromise } from '@dxos/util';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -26,6 +26,8 @@ export type FormHandler<T extends BaseObject> = {
|
|
|
26
26
|
changed: Record<JsonPath, boolean>;
|
|
27
27
|
canSave: boolean;
|
|
28
28
|
formIsValid: boolean;
|
|
29
|
+
|
|
30
|
+
// TODO(burdon): Return onSave.
|
|
29
31
|
handleSave: () => void;
|
|
30
32
|
|
|
31
33
|
//
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
|
|
8
8
|
import { Filter, type Space } from '@dxos/react-client/echo';
|
|
9
9
|
import { isNonNullable } from '@dxos/util';
|
|
10
10
|
|
package/src/index.ts
CHANGED
package/src/translations.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Resource } from '@dxos/react-ui';
|
|
6
|
+
|
|
5
7
|
export const translationKey = 'react-ui-form';
|
|
6
8
|
|
|
7
|
-
export
|
|
9
|
+
export const translations = [
|
|
8
10
|
{
|
|
9
11
|
'en-US': {
|
|
10
12
|
[translationKey]: {
|
|
@@ -23,6 +25,11 @@ export default [
|
|
|
23
25
|
'field limit reached': 'Maximum number of fields reached',
|
|
24
26
|
'boolean input true value': 'Yes',
|
|
25
27
|
'boolean input false value': 'No',
|
|
28
|
+
'show field label': 'Show field',
|
|
29
|
+
'hide field label': 'Hide field',
|
|
30
|
+
'delete field label': 'Delete field',
|
|
31
|
+
'ref field placeholder_one': 'Select…',
|
|
32
|
+
'ref field placeholder_other': 'Select items…',
|
|
26
33
|
|
|
27
34
|
// TODO(burdon): Factor out?
|
|
28
35
|
'button cancel': 'Cancel',
|
|
@@ -58,9 +65,9 @@ export default [
|
|
|
58
65
|
'format url': 'URL',
|
|
59
66
|
'format uuid': 'UUID',
|
|
60
67
|
|
|
61
|
-
//
|
|
62
|
-
'
|
|
63
|
-
'
|
|
68
|
+
// GeoPointInput.
|
|
69
|
+
'latitude label': 'Latitude',
|
|
70
|
+
'longitude label': 'Longitude',
|
|
64
71
|
|
|
65
72
|
// SelectOptionsInput.
|
|
66
73
|
'select option label': 'Label',
|
|
@@ -70,9 +77,10 @@ export default [
|
|
|
70
77
|
'select option add': 'Add option',
|
|
71
78
|
|
|
72
79
|
// System schema message.
|
|
73
|
-
'system schema title': 'System
|
|
74
|
-
'system schema description':
|
|
80
|
+
'system schema title': 'System record type',
|
|
81
|
+
'system schema description':
|
|
82
|
+
'This is a system record type and cannot be modified, though the view can still be configured.',
|
|
75
83
|
},
|
|
76
84
|
},
|
|
77
85
|
},
|
|
78
|
-
];
|
|
86
|
+
] as const satisfies Resource[];
|
package/src/util/field.ts
CHANGED
|
@@ -112,24 +112,7 @@ export type CellClassesForFieldTypeProps = {
|
|
|
112
112
|
};
|
|
113
113
|
|
|
114
114
|
export const cellClassesForFieldType = ({ type, format }: CellClassesForFieldTypeProps): string[] | undefined => {
|
|
115
|
-
|
|
116
|
-
switch (type) {
|
|
117
|
-
case TypeEnum.Number:
|
|
118
|
-
return ['text-right', 'font-mono'];
|
|
119
|
-
case TypeEnum.Boolean:
|
|
120
|
-
return ['text-right', 'font-mono'];
|
|
121
|
-
case TypeEnum.String:
|
|
122
|
-
return undefined;
|
|
123
|
-
case TypeEnum.Ref:
|
|
124
|
-
return undefined;
|
|
125
|
-
|
|
126
|
-
default: {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
switch (format) {
|
|
115
|
+
switch (format || type) {
|
|
133
116
|
case FormatEnum.Markdown:
|
|
134
117
|
return undefined;
|
|
135
118
|
case FormatEnum.Time:
|
|
@@ -143,6 +126,13 @@ export const cellClassesForFieldType = ({ type, format }: CellClassesForFieldTyp
|
|
|
143
126
|
case FormatEnum.JSON:
|
|
144
127
|
case FormatEnum.DID:
|
|
145
128
|
return ['font-mono'];
|
|
129
|
+
case TypeEnum.Number:
|
|
130
|
+
case TypeEnum.Boolean:
|
|
131
|
+
return ['text-right', 'font-mono'];
|
|
132
|
+
case TypeEnum.String:
|
|
133
|
+
return undefined;
|
|
134
|
+
case TypeEnum.Ref:
|
|
135
|
+
return undefined;
|
|
146
136
|
default:
|
|
147
137
|
return undefined;
|
|
148
138
|
}
|