@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.dedc0f3
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 +601 -528
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +601 -528
- 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 +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +74 -4
- 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 +3 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +76 -5
- 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/translations.d.ts +9 -4
- 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 +35 -30
- package/src/components/ControlSection/ControlSection.stories.tsx +8 -10
- package/src/components/ControlSection/ControlSection.tsx +26 -12
- package/src/components/FieldEditor/FieldEditor.stories.tsx +8 -9
- package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
- package/src/components/FieldEditor/FieldEditor.tsx +22 -17
- package/src/components/Form/ArrayField.tsx +8 -8
- package/src/components/Form/Defaults.tsx +22 -20
- package/src/components/Form/Form.stories.tsx +129 -93
- 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 +7 -6
- 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 +50 -23
- package/src/components/ViewEditor/ViewEditor.test.tsx +7 -10
- package/src/components/ViewEditor/ViewEditor.tsx +151 -132
- 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/translations.ts +11 -5
- package/src/util/field.ts +8 -18
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { composeStories } from '@storybook/react';
|
|
6
|
-
import {
|
|
6
|
+
import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
|
|
7
7
|
import { afterEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
|
|
10
10
|
import { DataType, ProjectionModel } from '@dxos/schema';
|
|
11
11
|
|
|
12
|
+
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
13
|
+
|
|
12
14
|
import * as stories from './ViewEditor.stories';
|
|
13
15
|
import { type ViewEditorDebugObjects } from './ViewEditor.stories';
|
|
14
|
-
import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
15
16
|
|
|
16
17
|
const { Default } = composeStories(stories);
|
|
17
18
|
|
|
@@ -41,7 +42,7 @@ describe('ViewEditor', () => {
|
|
|
41
42
|
test('renders view editor', async () => {
|
|
42
43
|
await Default.run();
|
|
43
44
|
await waitForViewEditor();
|
|
44
|
-
expect(screen.getByText('
|
|
45
|
+
expect(screen.getByText('Record type')).toBeInTheDocument();
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
test('change property name', async () => {
|
|
@@ -140,7 +141,7 @@ describe('ViewEditor', () => {
|
|
|
140
141
|
// Find the delete button for the 'name' property.
|
|
141
142
|
const nameProperty = screen.getByText('name');
|
|
142
143
|
const propertyRow = nameProperty.closest('[role="listitem"]');
|
|
143
|
-
const deleteButton = propertyRow?.querySelector('
|
|
144
|
+
const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
|
|
144
145
|
|
|
145
146
|
if (!deleteButton) {
|
|
146
147
|
throw new Error('Delete button not found');
|
|
@@ -188,19 +189,15 @@ describe('ViewEditor', () => {
|
|
|
188
189
|
// Access the live objects directly from window using symbol.
|
|
189
190
|
const debugObjects = getViewEditorDebugObjects();
|
|
190
191
|
|
|
191
|
-
// Check that hiddenFields is non-empty.
|
|
192
|
-
expect(debugObjects.view.projection.hiddenFields).toBeDefined();
|
|
193
|
-
expect(debugObjects.view.projection.hiddenFields!.length).toBeGreaterThan(0);
|
|
194
|
-
|
|
195
192
|
// Click the show button
|
|
196
193
|
fireEvent.click(screen.getByTestId('show-field-button'));
|
|
197
194
|
|
|
198
195
|
// Wait for the data to update and check that hiddenFields is empty.
|
|
199
196
|
await waitFor(() => {
|
|
200
|
-
expect(debugObjects.view.projection.
|
|
197
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
|
|
201
198
|
});
|
|
202
199
|
|
|
203
200
|
// Also verify that the field is back in the visible fields list.
|
|
204
|
-
expect(debugObjects.view.projection.fields.length).toBeGreaterThan(0);
|
|
201
|
+
expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
|
|
205
202
|
});
|
|
206
203
|
});
|
|
@@ -2,40 +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
|
-
typename: Format.URL.annotations({
|
|
25
|
-
title: 'Typename',
|
|
26
|
-
}),
|
|
27
|
-
}).pipe(Schema.mutable);
|
|
28
|
-
|
|
29
|
-
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';
|
|
30
32
|
|
|
31
33
|
export type ViewEditorProps = ThemedClassName<
|
|
32
34
|
{
|
|
33
35
|
schema: Schema.Schema.AnyNoContext;
|
|
34
36
|
view: DataType.View;
|
|
37
|
+
kind?: 'basic' | 'advanced';
|
|
35
38
|
registry?: SchemaRegistry;
|
|
36
39
|
readonly?: boolean;
|
|
37
40
|
showHeading?: boolean;
|
|
38
|
-
|
|
41
|
+
onQueryChanged?: (query: string) => void;
|
|
39
42
|
onDelete?: (fieldId: string) => void;
|
|
40
43
|
} & Pick<FormProps<any>, 'outerSpacing'>
|
|
41
44
|
>;
|
|
@@ -47,37 +50,52 @@ export const ViewEditor = ({
|
|
|
47
50
|
classNames,
|
|
48
51
|
schema,
|
|
49
52
|
view,
|
|
53
|
+
kind = 'basic',
|
|
50
54
|
registry,
|
|
51
|
-
readonly
|
|
55
|
+
readonly,
|
|
52
56
|
showHeading = false,
|
|
53
|
-
|
|
57
|
+
onQueryChanged,
|
|
54
58
|
onDelete,
|
|
55
59
|
outerSpacing = true,
|
|
56
60
|
}: ViewEditorProps) => {
|
|
61
|
+
const space = getSpace(view);
|
|
62
|
+
const schemaReadonly = !isMutable(schema);
|
|
57
63
|
const { t } = useTranslation(translationKey);
|
|
58
|
-
const
|
|
64
|
+
const projectionModel = useMemo(() => {
|
|
59
65
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
60
66
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
61
67
|
return new ProjectionModel(jsonSchema, view.projection);
|
|
62
68
|
}, [schema, view.projection]);
|
|
63
|
-
const [
|
|
64
|
-
|
|
69
|
+
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
70
|
+
|
|
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
|
+
}
|
|
77
|
+
|
|
78
|
+
const serializer = new QuerySerializer();
|
|
79
|
+
return serializer.serialize(createExpression(view.query.filter));
|
|
80
|
+
}),
|
|
81
|
+
Match.exhaustive,
|
|
82
|
+
);
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
73
|
-
}, [
|
|
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]);
|
|
74
95
|
|
|
75
|
-
const
|
|
96
|
+
const handleToggleField = useCallback(
|
|
76
97
|
(field: FieldType) => {
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
setField((f) => (f === field ? undefined : field));
|
|
98
|
+
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
81
99
|
},
|
|
82
100
|
[readonly],
|
|
83
101
|
);
|
|
@@ -86,32 +104,32 @@ export const ViewEditor = ({
|
|
|
86
104
|
|
|
87
105
|
const handleAdd = useCallback(() => {
|
|
88
106
|
invariant(!readonly);
|
|
89
|
-
const field =
|
|
90
|
-
|
|
91
|
-
}, [schema,
|
|
107
|
+
const field = projectionModel.createFieldProjection();
|
|
108
|
+
setExpandedField(field.id);
|
|
109
|
+
}, [schema, projectionModel, readonly]);
|
|
92
110
|
|
|
93
111
|
const handleUpdate = useCallback(
|
|
94
|
-
({
|
|
112
|
+
({ query }: Schema.Schema.Type<typeof viewSchema>) => {
|
|
95
113
|
invariant(!readonly);
|
|
96
114
|
requestAnimationFrame(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
115
|
+
if (serializedQuery !== query && !readonly) {
|
|
116
|
+
onQueryChanged?.(query);
|
|
99
117
|
}
|
|
100
118
|
});
|
|
101
119
|
},
|
|
102
|
-
[
|
|
120
|
+
[serializedQuery, onQueryChanged, readonly],
|
|
103
121
|
);
|
|
104
122
|
|
|
105
123
|
const handleDelete = useCallback(
|
|
106
124
|
(fieldId: string) => {
|
|
107
125
|
invariant(!readonly);
|
|
108
|
-
if (fieldId ===
|
|
109
|
-
|
|
126
|
+
if (fieldId === expandedField) {
|
|
127
|
+
setExpandedField(undefined);
|
|
110
128
|
}
|
|
111
129
|
|
|
112
130
|
onDelete?.(fieldId);
|
|
113
131
|
},
|
|
114
|
-
[
|
|
132
|
+
[expandedField, onDelete, readonly],
|
|
115
133
|
);
|
|
116
134
|
|
|
117
135
|
const handleMove = useCallback(
|
|
@@ -126,134 +144,135 @@ export const ViewEditor = ({
|
|
|
126
144
|
[view.projection.fields, readonly],
|
|
127
145
|
);
|
|
128
146
|
|
|
129
|
-
const handleClose = useCallback(() =>
|
|
130
|
-
|
|
131
|
-
const hiddenProperties = projection.getHiddenProperties();
|
|
147
|
+
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
132
148
|
|
|
133
149
|
const handleHide = useCallback(
|
|
134
150
|
(fieldId: string) => {
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
setExpandedField(undefined);
|
|
152
|
+
projectionModel.hideFieldProjection(fieldId);
|
|
137
153
|
},
|
|
138
|
-
[
|
|
154
|
+
[projectionModel],
|
|
139
155
|
);
|
|
140
156
|
|
|
141
157
|
const handleShow = useCallback(
|
|
142
158
|
(property: string) => {
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
setExpandedField(undefined);
|
|
160
|
+
projectionModel.showFieldProjection(property as JsonProp);
|
|
145
161
|
},
|
|
146
|
-
[
|
|
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
|
+
[],
|
|
147
181
|
);
|
|
148
182
|
|
|
149
183
|
return (
|
|
150
184
|
<div role='none' className={mx(classNames)}>
|
|
151
|
-
{
|
|
152
|
-
<
|
|
153
|
-
<Callout.
|
|
154
|
-
|
|
155
|
-
</Callout.Root>
|
|
156
|
-
</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>
|
|
157
189
|
)}
|
|
158
190
|
|
|
159
191
|
{/* TODO(burdon): Is the form read-only or just the schema? */}
|
|
160
192
|
{/* TODO(burdon): Readonly fields should take up the same space as editable fields (just be ghosted). */}
|
|
161
|
-
<Form<
|
|
193
|
+
<Form<Schema.Schema.Type<typeof viewSchema>>
|
|
162
194
|
autoSave
|
|
163
|
-
schema={
|
|
195
|
+
schema={viewSchema}
|
|
164
196
|
values={viewValues}
|
|
165
|
-
readonly={readonly}
|
|
197
|
+
readonly={readonly ? 'disabled-input' : false}
|
|
166
198
|
onSave={handleUpdate}
|
|
167
199
|
outerSpacing={outerSpacing}
|
|
200
|
+
Custom={custom}
|
|
168
201
|
/>
|
|
169
202
|
|
|
170
203
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
171
|
-
<
|
|
204
|
+
<h2 className={mx(inputTextLabel)}>{t('fields label')}</h2>
|
|
172
205
|
|
|
173
206
|
<List.Root<FieldType>
|
|
174
207
|
items={view.projection.fields}
|
|
175
208
|
isItem={Schema.is(FieldSchema)}
|
|
176
209
|
getId={(field) => field.id}
|
|
177
|
-
readonly={readonly}
|
|
178
210
|
onMove={readonly ? undefined : handleMove}
|
|
211
|
+
readonly={readonly}
|
|
179
212
|
>
|
|
180
213
|
{({ items: fields }) => (
|
|
181
214
|
<>
|
|
182
|
-
{showHeading && (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
})}
|
|
214
269
|
</div>
|
|
215
270
|
</>
|
|
216
271
|
)}
|
|
217
272
|
</List.Root>
|
|
218
|
-
|
|
219
|
-
{hiddenProperties.length > 0 && (
|
|
220
|
-
<>
|
|
221
|
-
<label className={mx(inputTextLabel)}>{t('hidden fields label')}</label>
|
|
222
|
-
|
|
223
|
-
<List.Root<string>
|
|
224
|
-
items={hiddenProperties}
|
|
225
|
-
isItem={(item): item is string => typeof item === 'string'}
|
|
226
|
-
getId={(property) => property}
|
|
227
|
-
>
|
|
228
|
-
{({ items: properties }) => (
|
|
229
|
-
<div role='list' className='flex flex-col is-full'>
|
|
230
|
-
{properties?.map((property) => (
|
|
231
|
-
<List.Item<string>
|
|
232
|
-
key={property}
|
|
233
|
-
item={property}
|
|
234
|
-
classNames={mx(grid, ghostHover, !readonly && 'cursor-pointer')}
|
|
235
|
-
>
|
|
236
|
-
<List.ItemDragHandle disabled />
|
|
237
|
-
<List.ItemTitle>{property}</List.ItemTitle>
|
|
238
|
-
<List.ItemButton
|
|
239
|
-
icon='ph--eye--regular'
|
|
240
|
-
onClick={() => handleShow(property)}
|
|
241
|
-
data-testid='show-field-button'
|
|
242
|
-
/>
|
|
243
|
-
</List.Item>
|
|
244
|
-
))}
|
|
245
|
-
</div>
|
|
246
|
-
)}
|
|
247
|
-
</List.Root>
|
|
248
|
-
</>
|
|
249
|
-
)}
|
|
250
273
|
</div>
|
|
251
274
|
|
|
252
|
-
{
|
|
253
|
-
<FieldEditor key={field.id} projection={projection} field={field} registry={registry} onSave={handleClose} />
|
|
254
|
-
)}
|
|
255
|
-
|
|
256
|
-
{!readonly && !field && (
|
|
275
|
+
{!readonly && !expandedField && (
|
|
257
276
|
<div role='none' className={outerSpacing ? cardSpacing : 'mlb-cardSpacingBlock'}>
|
|
258
277
|
<IconButton
|
|
259
278
|
icon='ph--plus--regular'
|
|
@@ -261,7 +280,7 @@ export const ViewEditor = ({
|
|
|
261
280
|
onClick={readonly ? undefined : handleAdd}
|
|
262
281
|
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
263
282
|
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
264
|
-
classNames='
|
|
283
|
+
classNames='is-full'
|
|
265
284
|
/>
|
|
266
285
|
</div>
|
|
267
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/translations.ts
CHANGED
|
@@ -25,6 +25,11 @@ export const translations = [
|
|
|
25
25
|
'field limit reached': 'Maximum number of fields reached',
|
|
26
26
|
'boolean input true value': 'Yes',
|
|
27
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…',
|
|
28
33
|
|
|
29
34
|
// TODO(burdon): Factor out?
|
|
30
35
|
'button cancel': 'Cancel',
|
|
@@ -60,9 +65,9 @@ export const translations = [
|
|
|
60
65
|
'format url': 'URL',
|
|
61
66
|
'format uuid': 'UUID',
|
|
62
67
|
|
|
63
|
-
//
|
|
64
|
-
'
|
|
65
|
-
'
|
|
68
|
+
// GeoPointInput.
|
|
69
|
+
'latitude label': 'Latitude',
|
|
70
|
+
'longitude label': 'Longitude',
|
|
66
71
|
|
|
67
72
|
// SelectOptionsInput.
|
|
68
73
|
'select option label': 'Label',
|
|
@@ -72,8 +77,9 @@ export const translations = [
|
|
|
72
77
|
'select option add': 'Add option',
|
|
73
78
|
|
|
74
79
|
// System schema message.
|
|
75
|
-
'system schema title': 'System
|
|
76
|
-
'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.',
|
|
77
83
|
},
|
|
78
84
|
},
|
|
79
85
|
},
|
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
|
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type Schema } from 'effect';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { type ThemedClassName } from '@dxos/react-ui';
|
|
4
|
-
import { type Projection } from '@dxos/schema';
|
|
5
|
-
export type DeprecatedFormProps<T extends {} = {}> = ThemedClassName<{
|
|
6
|
-
projection: Projection;
|
|
7
|
-
object: T;
|
|
8
|
-
schema?: Schema.Schema<T>;
|
|
9
|
-
readonly?: boolean;
|
|
10
|
-
}>;
|
|
11
|
-
/**
|
|
12
|
-
* Schema-based object form.
|
|
13
|
-
*/
|
|
14
|
-
export declare const DeprecatedForm: <T extends {} = {}>({ classNames, projection, object, schema, readonly, }: DeprecatedFormProps<T>) => React.JSX.Element;
|
|
15
|
-
//# sourceMappingURL=Form.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../../../../../src/components/deprecated/Form.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9D,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAS,KAAK,eAAe,EAA+C,MAAM,gBAAgB,CAAC;AAE1G,OAAO,EAAgC,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE7E,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,eAAe,CAAC;IACnE,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,CAAC,CAAC;IACV,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,uDAM/C,mBAAmB,CAAC,CAAC,CAAC,sBA0DxB,CAAC"}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import '@dxos-theme';
|
|
2
|
-
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import { type TestType } from '@dxos/schema/testing';
|
|
5
|
-
import { type DeprecatedFormProps } from './Form';
|
|
6
|
-
declare const DefaultStory: (props: DeprecatedFormProps) => React.JSX.Element;
|
|
7
|
-
declare const meta: Meta<typeof DefaultStory>;
|
|
8
|
-
export default meta;
|
|
9
|
-
type Story = StoryObj<DeprecatedFormProps<TestType>>;
|
|
10
|
-
export declare const Default: Story;
|
|
11
|
-
export declare const Empty: Story;
|
|
12
|
-
export declare const Readonly: Story;
|
|
13
|
-
//# sourceMappingURL=Form.stories.d.ts.map
|