@dxos/react-ui-form 0.8.4-main.21d9917 → 0.8.4-main.2244d791bb
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 +400 -357
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +400 -357
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Form/Form.d.ts +5 -5
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- 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/{ControlSection/ControlSection.stories.d.ts → Settings/Settings.stories.d.ts} +2 -8
- 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.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/util/echo.d.ts +6 -0
- package/dist/types/src/util/echo.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +34 -34
- package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
- package/src/components/Form/Form-old.stories.tsx +1 -1
- package/src/components/Form/Form.stories.tsx +5 -2
- package/src/components/Form/Form.tsx +31 -21
- package/src/components/Form/FormFieldComponent.stories.tsx +1 -1
- package/src/components/Form/FormFieldComponent.tsx +15 -15
- package/src/components/Form/FormFieldSet.tsx +4 -2
- package/src/components/Form/fields/ArrayField.tsx +3 -0
- package/src/components/Form/fields/RefField.tsx +11 -3
- package/src/components/Form/fields/SelectOptionField.stories.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
- package/src/components/Settings/Settings.stories.tsx +56 -0
- package/src/components/Settings/Settings.tsx +234 -0
- package/src/components/{ControlSection → Settings}/index.ts +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +3 -3
- package/src/components/ViewEditor/ViewEditor.test.tsx +1 -5
- package/src/components/ViewEditor/ViewEditor.tsx +39 -44
- package/src/components/index.ts +1 -1
- package/src/hooks/useFormHandler.ts +7 -1
- package/src/util/echo.ts +25 -0
- package/src/util/index.ts +1 -0
- 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.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/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
type ButtonProps,
|
|
10
|
+
Input,
|
|
11
|
+
type Label,
|
|
12
|
+
ScrollArea,
|
|
13
|
+
type ThemedClassName,
|
|
14
|
+
toLocalizedString,
|
|
15
|
+
useTranslation,
|
|
16
|
+
} from '@dxos/react-ui';
|
|
17
|
+
import { mx } from '@dxos/ui-theme';
|
|
18
|
+
|
|
19
|
+
import { translationKey } from '../../translations';
|
|
20
|
+
|
|
21
|
+
const SETTINGS_ROOT_NAME = 'Settings.Root';
|
|
22
|
+
const SETTINGS_SECTION_NAME = 'Settings.Section';
|
|
23
|
+
const SETTINGS_SECTION_HEADING_NAME = 'Settings.SectionHeading';
|
|
24
|
+
const SETTINGS_GROUP_NAME = 'Settings.Group';
|
|
25
|
+
const SETTINGS_GROUP_BUTTON_NAME = 'Settings.GroupButton';
|
|
26
|
+
const SETTINGS_CONTAINER_NAME = 'Settings.Container';
|
|
27
|
+
const SETTINGS_FRAME_NAME = 'Settings.Frame';
|
|
28
|
+
const SETTINGS_FRAME_ITEM_NAME = 'Settings.FrameItem';
|
|
29
|
+
const SETTINGS_ITEM_NAME = 'Settings.Item';
|
|
30
|
+
const SETTINGS_ITEM_INPUT_NAME = 'Settings.ItemInput';
|
|
31
|
+
|
|
32
|
+
const styles = {
|
|
33
|
+
title: 'pbe-trimMd text-baseText text-lg',
|
|
34
|
+
description: 'text-base text-description',
|
|
35
|
+
grid: 'grid grid-cols-1 md:grid-cols-[3fr_2fr] gap-x-trimLg',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
//
|
|
39
|
+
// Root
|
|
40
|
+
//
|
|
41
|
+
|
|
42
|
+
type SettingsRootProps = PropsWithChildren;
|
|
43
|
+
|
|
44
|
+
const SettingsRoot = ({ children }: SettingsRootProps) => {
|
|
45
|
+
return (
|
|
46
|
+
<ScrollArea.Root orientation='vertical'>
|
|
47
|
+
<ScrollArea.Viewport classNames='p-trimMd'>{children}</ScrollArea.Viewport>
|
|
48
|
+
</ScrollArea.Root>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
SettingsRoot.displayName = SETTINGS_ROOT_NAME;
|
|
53
|
+
|
|
54
|
+
//
|
|
55
|
+
// Section
|
|
56
|
+
//
|
|
57
|
+
|
|
58
|
+
type SettingsSectionProps = PropsWithChildren<{
|
|
59
|
+
title: Label;
|
|
60
|
+
description?: Label;
|
|
61
|
+
}>;
|
|
62
|
+
|
|
63
|
+
const SettingsSection = ({ title, description, children }: SettingsSectionProps) => {
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<SettingsSectionHeading title={title} description={description} />
|
|
67
|
+
<div className='is-full pbs-trimMd space-y-trimMd'>{children}</div>
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
SettingsSection.displayName = SETTINGS_SECTION_NAME;
|
|
73
|
+
|
|
74
|
+
//
|
|
75
|
+
// Section Heading
|
|
76
|
+
//
|
|
77
|
+
|
|
78
|
+
const SettingsSectionHeading = ({ title, description }: Omit<SettingsSectionProps, 'children'>) => {
|
|
79
|
+
const { t } = useTranslation(translationKey);
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<h2 className='pli-trimMd container-max-width text-xl mbs-trimMd mbe-trimMd'>{toLocalizedString(title, t)}</h2>
|
|
83
|
+
{description && (
|
|
84
|
+
<p className='pli-trimMd mlb-trimMd container-max-width text-description'>
|
|
85
|
+
{toLocalizedString(description, t)}
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
SettingsSectionHeading.displayName = SETTINGS_SECTION_HEADING_NAME;
|
|
93
|
+
|
|
94
|
+
//
|
|
95
|
+
// Group Button
|
|
96
|
+
//
|
|
97
|
+
|
|
98
|
+
const SettingsGroupButton = ({ classNames, ...props }: ButtonProps) => {
|
|
99
|
+
return <Button {...props} classNames={['md:col-span-2', classNames]} />;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
SettingsGroupButton.displayName = SETTINGS_GROUP_BUTTON_NAME;
|
|
103
|
+
|
|
104
|
+
//
|
|
105
|
+
// Group
|
|
106
|
+
//
|
|
107
|
+
|
|
108
|
+
type SettingsGroupProps = ThemedClassName<PropsWithChildren>;
|
|
109
|
+
|
|
110
|
+
const SettingsGroup = ({ children, classNames }: SettingsGroupProps) => (
|
|
111
|
+
<div role='none' className={mx('group container-max-width space-y-trimMd', classNames)}>
|
|
112
|
+
{children}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
SettingsGroup.displayName = SETTINGS_GROUP_NAME;
|
|
117
|
+
|
|
118
|
+
//
|
|
119
|
+
// Frame
|
|
120
|
+
//
|
|
121
|
+
|
|
122
|
+
const SettingsFrame = ({ children }: SettingsGroupProps) => (
|
|
123
|
+
<div role='none' className={mx('container-max-width p-trimMd', 'border border-separator rounded-md')}>
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
SettingsFrame.displayName = SETTINGS_FRAME_NAME;
|
|
129
|
+
|
|
130
|
+
//
|
|
131
|
+
// Frame Item
|
|
132
|
+
//
|
|
133
|
+
|
|
134
|
+
const SettingsFrameItem = ({ title, description, children }: SettingsItemProps) => {
|
|
135
|
+
const { t } = useTranslation(translationKey);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div role='group' className='min-is-0'>
|
|
139
|
+
<h3 className='text-lg mbe-2'>{toLocalizedString(title, t)}</h3>
|
|
140
|
+
{description && <p className='mlb-trimSm md:mbe-0 text-description'>{toLocalizedString(description, t)}</p>}
|
|
141
|
+
{children}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
SettingsFrameItem.displayName = SETTINGS_FRAME_ITEM_NAME;
|
|
147
|
+
|
|
148
|
+
//
|
|
149
|
+
// Container
|
|
150
|
+
//
|
|
151
|
+
|
|
152
|
+
const SettingsContainer = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => {
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
role='none'
|
|
156
|
+
className={mx([
|
|
157
|
+
'container-max-width',
|
|
158
|
+
'*:first:!mbs-0 *:last:!mbe-0 pli-trimMd plb-trimMd',
|
|
159
|
+
'border border-separator rounded-md',
|
|
160
|
+
classNames,
|
|
161
|
+
])}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
SettingsContainer.displayName = SETTINGS_CONTAINER_NAME;
|
|
169
|
+
|
|
170
|
+
//
|
|
171
|
+
// Item
|
|
172
|
+
//
|
|
173
|
+
|
|
174
|
+
type SettingsItemProps = PropsWithChildren<{
|
|
175
|
+
title: Label;
|
|
176
|
+
description?: Label;
|
|
177
|
+
}>;
|
|
178
|
+
|
|
179
|
+
const SettingsItem = ({ title, description = '', children }: SettingsItemProps) => {
|
|
180
|
+
const { t } = useTranslation(translationKey);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<SettingsContainer classNames={styles.grid}>
|
|
184
|
+
<h3 className={mx(styles.title, 'md:col-span-2')}>{toLocalizedString(title, t)}</h3>
|
|
185
|
+
<p className={styles.description}>{toLocalizedString(description, t)}</p>
|
|
186
|
+
<div role='none' className='text-end plb-1'>
|
|
187
|
+
{children}
|
|
188
|
+
</div>
|
|
189
|
+
</SettingsContainer>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
SettingsItem.displayName = SETTINGS_ITEM_NAME;
|
|
194
|
+
|
|
195
|
+
//
|
|
196
|
+
// Item Input
|
|
197
|
+
//
|
|
198
|
+
|
|
199
|
+
const SettingsItemInput = ({ title, description = '', children }: SettingsItemProps) => {
|
|
200
|
+
const { t } = useTranslation(translationKey);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<Input.Root>
|
|
204
|
+
<SettingsContainer classNames={styles.grid}>
|
|
205
|
+
<Input.Label classNames={mx(styles.title, 'md:col-span-2')}>{toLocalizedString(title, t)}</Input.Label>
|
|
206
|
+
<Input.DescriptionAndValidation>
|
|
207
|
+
<Input.Description classNames={styles.description}>{toLocalizedString(description, t)}</Input.Description>
|
|
208
|
+
</Input.DescriptionAndValidation>
|
|
209
|
+
<div role='none' className='text-end plb-1'>
|
|
210
|
+
{children}
|
|
211
|
+
</div>
|
|
212
|
+
</SettingsContainer>
|
|
213
|
+
</Input.Root>
|
|
214
|
+
);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
SettingsItemInput.displayName = SETTINGS_ITEM_INPUT_NAME;
|
|
218
|
+
|
|
219
|
+
//
|
|
220
|
+
// Settings
|
|
221
|
+
//
|
|
222
|
+
|
|
223
|
+
export const Settings = {
|
|
224
|
+
Root: SettingsRoot,
|
|
225
|
+
Section: SettingsSection,
|
|
226
|
+
SectionHeading: SettingsSectionHeading,
|
|
227
|
+
Group: SettingsGroup,
|
|
228
|
+
GroupButton: SettingsGroupButton,
|
|
229
|
+
Container: SettingsContainer,
|
|
230
|
+
Frame: SettingsFrame,
|
|
231
|
+
FrameItem: SettingsFrameItem,
|
|
232
|
+
Item: SettingsItem,
|
|
233
|
+
ItemInput: SettingsItemInput,
|
|
234
|
+
};
|
|
@@ -13,7 +13,7 @@ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
|
13
13
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
14
14
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
15
15
|
import { type ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
|
|
16
|
-
import { Employer, Organization, Person,
|
|
16
|
+
import { Employer, Organization, Person, Pipeline } from '@dxos/types';
|
|
17
17
|
|
|
18
18
|
import { translations } from '../../translations';
|
|
19
19
|
import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
|
|
@@ -24,7 +24,7 @@ const types = [
|
|
|
24
24
|
// TODO(burdon): Get label from annotation.
|
|
25
25
|
{ value: Organization.Organization.typename, label: 'Organization' },
|
|
26
26
|
{ value: Person.Person.typename, label: 'Person' },
|
|
27
|
-
{ value:
|
|
27
|
+
{ value: Pipeline.Pipeline.typename, label: 'Project' },
|
|
28
28
|
{ value: Employer.Employer.typename, label: 'Employer' },
|
|
29
29
|
];
|
|
30
30
|
|
|
@@ -175,7 +175,7 @@ const meta = {
|
|
|
175
175
|
space.db.add(Tag.make({ label: 'New' }));
|
|
176
176
|
},
|
|
177
177
|
}),
|
|
178
|
-
withTheme,
|
|
178
|
+
withTheme(),
|
|
179
179
|
],
|
|
180
180
|
parameters: {
|
|
181
181
|
layout: 'fullscreen',
|
|
@@ -89,14 +89,10 @@ describe('ViewEditor', () => {
|
|
|
89
89
|
await Default.run();
|
|
90
90
|
await waitForViewEditor();
|
|
91
91
|
|
|
92
|
-
// Click the add property button.
|
|
92
|
+
// Click the add property button. The new field is auto-expanded by handleAdd.
|
|
93
93
|
const addButton = screen.getByText('Add property');
|
|
94
94
|
fireEvent.click(addButton);
|
|
95
95
|
|
|
96
|
-
// Open the last field.
|
|
97
|
-
const buttons = screen.getAllByTestId('field.toggle');
|
|
98
|
-
fireEvent.click(buttons.at(-1)!);
|
|
99
|
-
|
|
100
96
|
// Fill out the property field.
|
|
101
97
|
const fieldInput = screen.getByPlaceholderText('Property name');
|
|
102
98
|
fireEvent.change(fieldInput, { target: { value: 'added_property' } });
|
|
@@ -12,6 +12,7 @@ import React, { forwardRef, useCallback, useContext, useImperativeHandle, useMem
|
|
|
12
12
|
import { Filter, Format, Obj, Query, QueryAST, type SchemaRegistry } from '@dxos/echo';
|
|
13
13
|
import { EchoSchema, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo/internal';
|
|
14
14
|
import { invariant } from '@dxos/invariant';
|
|
15
|
+
import { useObject } from '@dxos/react-client/echo';
|
|
15
16
|
import { Callout, IconButton, Input, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
16
17
|
import { QueryForm, type QueryFormProps } from '@dxos/react-ui-components';
|
|
17
18
|
import { List } from '@dxos/react-ui-list';
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
ProjectionModel,
|
|
22
23
|
VIEW_FIELD_LIMIT,
|
|
23
24
|
type View,
|
|
24
|
-
createDirectChangeCallback,
|
|
25
25
|
createEchoChangeCallback,
|
|
26
26
|
getTypenameFromQuery,
|
|
27
27
|
} from '@dxos/schema';
|
|
@@ -81,11 +81,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
81
81
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
82
82
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
83
83
|
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
? createEchoChangeCallback(view, schema)
|
|
88
|
-
: createDirectChangeCallback(view.projection, jsonSchema);
|
|
84
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
85
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
86
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
89
87
|
|
|
90
88
|
const model = new ProjectionModel({
|
|
91
89
|
registry: atomRegistry,
|
|
@@ -98,7 +96,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
98
96
|
}, [atomRegistry, schema, view]);
|
|
99
97
|
|
|
100
98
|
useImperativeHandle(forwardedRef, () => projectionModel, [projectionModel]);
|
|
101
|
-
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
102
99
|
|
|
103
100
|
const queueTarget = Match.value(view.query.ast).pipe(
|
|
104
101
|
Match.when({ type: 'options' }, ({ options }) => {
|
|
@@ -143,13 +140,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
143
140
|
[mode, types, tags],
|
|
144
141
|
);
|
|
145
142
|
|
|
146
|
-
// TODO(burdon): Check if mutable.
|
|
147
|
-
const handleAdd = useCallback(() => {
|
|
148
|
-
invariant(!readonly);
|
|
149
|
-
const field = projectionModel.createFieldProjection();
|
|
150
|
-
setExpandedField(field.id);
|
|
151
|
-
}, [projectionModel, readonly]);
|
|
152
|
-
|
|
153
143
|
const handleUpdate = useCallback(
|
|
154
144
|
(values: any) => {
|
|
155
145
|
requestAnimationFrame(() => {
|
|
@@ -163,13 +153,9 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
163
153
|
const handleDelete = useCallback(
|
|
164
154
|
(fieldId: string) => {
|
|
165
155
|
invariant(!readonly);
|
|
166
|
-
if (fieldId === expandedField) {
|
|
167
|
-
setExpandedField(undefined);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
156
|
onDelete?.(fieldId);
|
|
171
157
|
},
|
|
172
|
-
[
|
|
158
|
+
[onDelete, readonly],
|
|
173
159
|
);
|
|
174
160
|
|
|
175
161
|
return (
|
|
@@ -195,19 +181,6 @@ export const ViewEditor = forwardRef<ProjectionModel, ViewEditorProps>(
|
|
|
195
181
|
onDelete={handleDelete}
|
|
196
182
|
/>
|
|
197
183
|
</Form.Root>
|
|
198
|
-
|
|
199
|
-
{!readonly && !expandedField && (
|
|
200
|
-
<div role='none' className='mlb-cardSpacingBlock'>
|
|
201
|
-
<IconButton
|
|
202
|
-
icon='ph--plus--regular'
|
|
203
|
-
label={t('add property button label')}
|
|
204
|
-
onClick={readonly ? undefined : handleAdd}
|
|
205
|
-
// TODO(burdon): Show field limit in ux (not tooltip).
|
|
206
|
-
disabled={view.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
207
|
-
classNames='is-full'
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
)}
|
|
211
184
|
</div>
|
|
212
185
|
);
|
|
213
186
|
},
|
|
@@ -220,15 +193,16 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
220
193
|
const schemaReadonly = !isMutable(schema);
|
|
221
194
|
const { t } = useTranslation(translationKey);
|
|
222
195
|
|
|
196
|
+
// Subscribe to view changes for reactivity.
|
|
197
|
+
const [viewSnapshot] = useObject(view);
|
|
198
|
+
|
|
223
199
|
const projectionModel = useMemo(() => {
|
|
224
200
|
// Use reactive and mutable version of json schema when schema is mutable.
|
|
225
201
|
const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
|
|
226
202
|
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
? createEchoChangeCallback(view, schema)
|
|
231
|
-
: createDirectChangeCallback(view.projection, jsonSchema);
|
|
203
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
204
|
+
// Pass schema only when mutable to allow schema mutations.
|
|
205
|
+
const change = createEchoChangeCallback(view, schema instanceof EchoSchema ? schema : undefined);
|
|
232
206
|
|
|
233
207
|
const model = new ProjectionModel({
|
|
234
208
|
registry: atomRegistry,
|
|
@@ -242,6 +216,12 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
242
216
|
|
|
243
217
|
const [expandedField, setExpandedField] = useState<FieldType['id']>();
|
|
244
218
|
|
|
219
|
+
const handleAdd = useCallback(() => {
|
|
220
|
+
invariant(!readonly);
|
|
221
|
+
const field = projectionModel.createFieldProjection();
|
|
222
|
+
setExpandedField(field.id);
|
|
223
|
+
}, [projectionModel, readonly]);
|
|
224
|
+
|
|
245
225
|
const handleToggleField = useCallback(
|
|
246
226
|
(field: FieldType) => {
|
|
247
227
|
setExpandedField((prevExpandedFieldId) => (prevExpandedFieldId === field.id ? undefined : field.id));
|
|
@@ -264,15 +244,15 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
264
244
|
const handleMove = useCallback(
|
|
265
245
|
(fromIndex: number, toIndex: number) => {
|
|
266
246
|
invariant(!readonly);
|
|
267
|
-
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
268
|
-
const fields = [...view.projection.fields];
|
|
269
|
-
const [moved] = fields.splice(fromIndex, 1);
|
|
270
|
-
fields.splice(toIndex, 0, moved);
|
|
271
247
|
Obj.change(view, (v) => {
|
|
248
|
+
// NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
|
|
249
|
+
const fields = [...v.projection.fields];
|
|
250
|
+
const [moved] = fields.splice(fromIndex, 1);
|
|
251
|
+
fields.splice(toIndex, 0, moved);
|
|
272
252
|
v.projection.fields = fields;
|
|
273
253
|
});
|
|
274
254
|
},
|
|
275
|
-
[view
|
|
255
|
+
[view, readonly],
|
|
276
256
|
);
|
|
277
257
|
|
|
278
258
|
const handleClose = useCallback(() => setExpandedField(undefined), []);
|
|
@@ -293,9 +273,13 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
293
273
|
[projectionModel],
|
|
294
274
|
);
|
|
295
275
|
|
|
276
|
+
if (!viewSnapshot) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
296
280
|
return (
|
|
297
281
|
<List.Root<FieldType>
|
|
298
|
-
items={
|
|
282
|
+
items={viewSnapshot.projection.fields as FieldType[]}
|
|
299
283
|
isItem={Schema.is(FieldSchema)}
|
|
300
284
|
getId={(field) => field.id}
|
|
301
285
|
onMove={readonly ? undefined : handleMove}
|
|
@@ -332,7 +316,7 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
332
316
|
<List.ItemDeleteButton
|
|
333
317
|
label={t('delete field label')}
|
|
334
318
|
autoHide={false}
|
|
335
|
-
disabled={readonly || schemaReadonly ||
|
|
319
|
+
disabled={readonly || schemaReadonly || viewSnapshot.projection.fields.length <= 1}
|
|
336
320
|
onClick={() => handleDelete(field.id)}
|
|
337
321
|
data-testid='field.delete'
|
|
338
322
|
/>
|
|
@@ -362,6 +346,17 @@ const FieldList = ({ schema, view, registry, readonly, showHeading = false, onDe
|
|
|
362
346
|
);
|
|
363
347
|
})}
|
|
364
348
|
</div>
|
|
349
|
+
{!readonly && !expandedField && (
|
|
350
|
+
<div role='none' className='mlb-cardSpacingBlock'>
|
|
351
|
+
<IconButton
|
|
352
|
+
icon='ph--plus--regular'
|
|
353
|
+
label={t('add property button label')}
|
|
354
|
+
onClick={handleAdd}
|
|
355
|
+
disabled={viewSnapshot.projection.fields.length >= VIEW_FIELD_LIMIT}
|
|
356
|
+
classNames='is-full'
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
)}
|
|
365
360
|
</>
|
|
366
361
|
)}
|
|
367
362
|
</List.Root>
|
package/src/components/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type * as Schema from 'effect/Schema';
|
|
|
7
7
|
import type * as SchemaAST from 'effect/SchemaAST';
|
|
8
8
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
+
import { Obj } from '@dxos/echo';
|
|
10
11
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
12
|
import {
|
|
12
13
|
type JsonPath,
|
|
@@ -20,6 +21,8 @@ import { useDefaultValue } from '@dxos/react-ui';
|
|
|
20
21
|
import { type ValidationError, validateSchema } from '@dxos/schema';
|
|
21
22
|
import { type MaybePromise } from '@dxos/util';
|
|
22
23
|
|
|
24
|
+
import { setValueEchoAware } from '../util';
|
|
25
|
+
|
|
23
26
|
export type FormFieldStatus = {
|
|
24
27
|
status?: 'error';
|
|
25
28
|
error?: string;
|
|
@@ -267,7 +270,10 @@ export const useFormHandler = <T extends AnyProperties>({
|
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
// Update.
|
|
270
|
-
|
|
273
|
+
// For ECHO objects, mutate in place via Obj.change(); for plain objects, spread to create new reference.
|
|
274
|
+
const newValues = Obj.isObject(values)
|
|
275
|
+
? (setValueEchoAware(values, jsonPath, parsedValue), values)
|
|
276
|
+
: { ...setValue$(values, jsonPath, parsedValue) };
|
|
271
277
|
setValues(newValues);
|
|
272
278
|
|
|
273
279
|
// TODO(burdon): Check value has changed from original.
|
package/src/util/echo.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Obj } from '@dxos/echo';
|
|
6
|
+
import { type JsonPath, setValue as setValuePlain, splitJsonPath } from '@dxos/effect';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sets a value on an object, using Obj.change() if it's an ECHO object.
|
|
10
|
+
*/
|
|
11
|
+
// TODO(wittjosiah): Consider refactoring Form to not know about ECHO at all.
|
|
12
|
+
// Instead of directly mutating the values object, Form could work with a plain
|
|
13
|
+
// object copy and communicate all mutations through callbacks (e.g., onValuesChanged).
|
|
14
|
+
// The caller would then be responsible for applying changes to ECHO objects via
|
|
15
|
+
// Obj.change(). This would keep Form agnostic to the underlying data layer and
|
|
16
|
+
// avoid coupling react-ui-form to @dxos/echo.
|
|
17
|
+
export const setValueEchoAware = <T extends object>(obj: T, path: JsonPath, value: any): T => {
|
|
18
|
+
if (Obj.isObject(obj)) {
|
|
19
|
+
Obj.change(obj, (mutableObj) => {
|
|
20
|
+
Obj.setValue(mutableObj, splitJsonPath(path), value);
|
|
21
|
+
});
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
return setValuePlain(obj, path, value);
|
|
25
|
+
};
|
package/src/util/index.ts
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import React, { type ComponentPropsWithoutRef, type PropsWithChildren } from 'react';
|
|
2
|
-
import { type ButtonProps, type Label, type ThemedClassName } from '@dxos/react-ui';
|
|
3
|
-
export type ControlPageParams = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
|
|
4
|
-
export declare const ControlPage: ({ children, classNames, ...props }: ControlPageParams) => React.JSX.Element;
|
|
5
|
-
export type ControlSectionProps = PropsWithChildren<{
|
|
6
|
-
title: Label;
|
|
7
|
-
description?: Label;
|
|
8
|
-
}>;
|
|
9
|
-
export declare const ControlSection: ({ title, description, children }: ControlSectionProps) => React.JSX.Element;
|
|
10
|
-
export declare const ControlSectionHeading: ({ title, description }: Omit<ControlSectionProps, "children">) => React.JSX.Element;
|
|
11
|
-
export declare const ControlGroupButton: ({ classNames, ...props }: ButtonProps) => React.JSX.Element;
|
|
12
|
-
export type ControlGroupProps = ThemedClassName<PropsWithChildren>;
|
|
13
|
-
export declare const ControlGroup: ({ children, classNames }: ControlGroupProps) => React.JSX.Element;
|
|
14
|
-
export declare const ControlFrame: ({ children }: ControlGroupProps) => React.JSX.Element;
|
|
15
|
-
export declare const controlItemClasses: string;
|
|
16
|
-
export type ControlItemProps = PropsWithChildren<{
|
|
17
|
-
title: Label;
|
|
18
|
-
description?: Label;
|
|
19
|
-
}>;
|
|
20
|
-
export declare const ControlItem: ({ title, description, children }: ControlItemProps) => React.JSX.Element;
|
|
21
|
-
export declare const ControlItemInput: ({ title, description, children }: ControlItemProps) => React.JSX.Element;
|
|
22
|
-
export declare const ControlFrameItem: ({ title, description, children }: ControlItemProps) => React.JSX.Element;
|
|
23
|
-
//# sourceMappingURL=ControlSection.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ControlSection.d.ts","sourceRoot":"","sources":["../../../../../src/components/ControlSection/ControlSection.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,KAAK,wBAAwB,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAErF,OAAO,EAEL,KAAK,WAAW,EAEhB,KAAK,KAAK,EACV,KAAK,eAAe,EAGrB,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC,wBAAwB,CAAC,KAAK,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,WAAW,GAAI,oCAAoC,iBAAiB,sBAQhF,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;IAClD,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,CAAC,EAAE,KAAK,CAAC;CACrB,CAAC,CAAC;AAEH,eAAO,MAAM,cAAc,GAAI,kCAAkC,mBAAmB,sBAOnF,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,wBAAwB,IAAI,CAAC,mBAAmB,EAAE,UAAU,CAAC,sBAYlG,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,0BAA0B,WAAW,sBAEvE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC,iBAAiB,CAAC,CAAC;AAEnE,eAAO,MAAM,YAAY,GAAI,0BAA0B,iBAAiB,sBAWvE,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,cAAc,iBAAiB,sBAU3D,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAK7B,CAAC;AAKH,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;IAC/C,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,CAAC,EAAE,KAAK,CAAC;CACrB,CAAC,CAAC;AAEH,eAAO,MAAM,WAAW,GAAI,kCAAkC,gBAAgB,sBAY7E,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,kCAAkC,gBAAgB,sBAoBlF,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,kCAAkC,gBAAgB,sBAUlF,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ControlSection.stories.d.ts","sourceRoot":"","sources":["../../../../../src/components/ControlSection/ControlSection.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,MAAM,OAAO,CAAC;AAQ1B,QAAA,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAa6B,CAAC;AAExC,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/components/ControlSection/index.ts"],"names":[],"mappings":"AAIA,cAAc,kBAAkB,CAAC"}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2024 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React from 'react';
|
|
7
|
-
|
|
8
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
9
|
-
|
|
10
|
-
import { translations } from '../../translations';
|
|
11
|
-
|
|
12
|
-
import { ControlPage, ControlSection } from './ControlSection';
|
|
13
|
-
|
|
14
|
-
const meta = {
|
|
15
|
-
title: 'ui/react-ui-form/ControlSection',
|
|
16
|
-
component: ControlSection,
|
|
17
|
-
render: (args) => (
|
|
18
|
-
<ControlPage classNames='is-[40rem]'>
|
|
19
|
-
<ControlSection {...args} />
|
|
20
|
-
</ControlPage>
|
|
21
|
-
),
|
|
22
|
-
decorators: [withTheme],
|
|
23
|
-
parameters: {
|
|
24
|
-
layout: 'centered',
|
|
25
|
-
translations,
|
|
26
|
-
},
|
|
27
|
-
} satisfies Meta<typeof ControlSection>;
|
|
28
|
-
|
|
29
|
-
export default meta;
|
|
30
|
-
|
|
31
|
-
type Story = StoryObj<typeof meta>;
|
|
32
|
-
|
|
33
|
-
export const Default: Story = {
|
|
34
|
-
args: {
|
|
35
|
-
title: 'Control Section',
|
|
36
|
-
description: 'This is a control section',
|
|
37
|
-
},
|
|
38
|
-
};
|