@dxos/react-ui-form 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/lib/browser/index.mjs +1003 -668
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1003 -668
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +23 -18
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +7 -1
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +35 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +62 -61
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/refs.d.ts +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +3 -4
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +25 -26
- package/src/components/Form/Form.tsx +101 -66
- package/src/components/Form/FormField.tsx +32 -14
- package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
- package/src/components/Form/FormFieldComponent.tsx +17 -20
- package/src/components/Form/FormFieldSet.tsx +126 -105
- package/src/components/Form/fields/ArrayField.tsx +11 -8
- package/src/components/Form/fields/GeoPointField.tsx +6 -6
- package/src/components/Form/fields/MarkdownField.tsx +1 -1
- package/src/components/Form/fields/RefField.tsx +37 -20
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
- package/src/components/Form/fields/SelectOptionField.tsx +9 -10
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +113 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/Settings.stories.tsx +55 -0
- package/src/components/Settings/Settings.tsx +129 -0
- package/src/components/Settings/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
- package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
- package/src/components/ViewEditor/ViewEditor.tsx +117 -85
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -18
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/translations.ts +63 -63
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/Form-old.stories.tsx +0 -314
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import { createContext } from '@radix-ui/react-context';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import
|
|
7
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
8
|
import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
|
|
9
9
|
|
|
10
10
|
import { type AnyProperties } from '@dxos/echo/internal';
|
|
11
|
-
import { createJsonPath, getValue as getValue
|
|
12
|
-
import {
|
|
13
|
-
|
|
11
|
+
import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
|
|
12
|
+
import {
|
|
13
|
+
IconButton,
|
|
14
|
+
type IconButtonProps,
|
|
15
|
+
ScrollArea,
|
|
16
|
+
type ThemedClassName,
|
|
17
|
+
useMergeRefs,
|
|
18
|
+
useTranslation,
|
|
19
|
+
} from '@dxos/react-ui';
|
|
20
|
+
import { composable, composableProps, mx } from '@dxos/ui-theme';
|
|
14
21
|
|
|
15
22
|
import {
|
|
16
23
|
type FormHandler,
|
|
@@ -20,28 +27,13 @@ import {
|
|
|
20
27
|
useKeyHandler,
|
|
21
28
|
} from '../../hooks';
|
|
22
29
|
import { translationKey } from '../../translations';
|
|
23
|
-
|
|
24
30
|
import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
|
|
25
31
|
import {
|
|
26
32
|
FormFieldSet as NaturalFormFieldSet,
|
|
27
33
|
type FormFieldSetProps as NaturalFormFieldSetProps,
|
|
28
34
|
} from './FormFieldSet';
|
|
29
35
|
|
|
30
|
-
//
|
|
31
|
-
// [x] Unify readonly/inline modes
|
|
32
|
-
// [ ] Don't call save/autoSave if value hasn't changed
|
|
33
|
-
// [ ] Fix onCancel (restore values)
|
|
34
|
-
// [ ] Fix useSchema Type.Obj.Any cast
|
|
35
|
-
// [ ] Remove @dxos/echo-db deps
|
|
36
|
-
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
37
|
-
// [ ] Use FormFieldWrapper uniformly
|
|
38
|
-
// [ ] Inline tables for object arrays
|
|
39
|
-
// [ ] Defer query until popover
|
|
40
|
-
// [ ] Omit id from sub properties.
|
|
41
|
-
// [x] Refs
|
|
42
|
-
// [x] Single-select (fix popover)
|
|
43
|
-
// [x] Multi-select (array)
|
|
44
|
-
// [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
|
|
36
|
+
// TODO(burdon): Move styles to form.ts.
|
|
45
37
|
|
|
46
38
|
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
47
39
|
export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
|
|
@@ -69,7 +61,10 @@ type FormContextValue<T extends AnyProperties = any> = {
|
|
|
69
61
|
* Testing.
|
|
70
62
|
*/
|
|
71
63
|
testId?: string;
|
|
72
|
-
} & Pick<
|
|
64
|
+
} & Pick<
|
|
65
|
+
NaturalFormFieldSetProps<T>,
|
|
66
|
+
'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap'
|
|
67
|
+
>;
|
|
73
68
|
|
|
74
69
|
const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
75
70
|
|
|
@@ -83,19 +78,25 @@ const useFormValues: {
|
|
|
83
78
|
path: (string | number)[] | undefined,
|
|
84
79
|
defaultValue: () => T,
|
|
85
80
|
): T | undefined;
|
|
86
|
-
} = (componentName: string, path
|
|
87
|
-
const
|
|
81
|
+
} = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
|
|
82
|
+
const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
|
|
83
|
+
const jsonPath = createJsonPath(stablePath);
|
|
88
84
|
const {
|
|
89
|
-
form: { values },
|
|
85
|
+
form: { values, onValueChange },
|
|
90
86
|
} = useFormContext(componentName);
|
|
91
87
|
|
|
92
88
|
const value = getValue$(values, jsonPath);
|
|
93
89
|
|
|
90
|
+
// Apply default value once when the field has no value. lastAppliedPathRef prevents
|
|
91
|
+
// re-applying on every render (e.g. when defaultValue() returns null) and ensures
|
|
92
|
+
// we apply per path when the hook is used for different fields.
|
|
93
|
+
const lastAppliedPathRef = useRef<string | null>(null);
|
|
94
94
|
useEffect(() => {
|
|
95
|
-
if (
|
|
96
|
-
|
|
95
|
+
if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
|
|
96
|
+
lastAppliedPathRef.current = jsonPath;
|
|
97
|
+
onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
|
|
97
98
|
}
|
|
98
|
-
}, [value, defaultValue]);
|
|
99
|
+
}, [value, defaultValue, onValueChange, stablePath, jsonPath]);
|
|
99
100
|
|
|
100
101
|
return value;
|
|
101
102
|
};
|
|
@@ -163,65 +164,69 @@ FormRoot.displayName = 'Form.Root';
|
|
|
163
164
|
// Viewport
|
|
164
165
|
//
|
|
165
166
|
|
|
166
|
-
|
|
167
|
+
const FORM_VIEWPORT_NAME = 'Form.Viewport';
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
type FormViewportProps = {};
|
|
170
|
+
|
|
171
|
+
const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
169
172
|
return (
|
|
170
|
-
<ScrollArea.Root>
|
|
171
|
-
<ScrollArea.Viewport
|
|
172
|
-
<ScrollArea.Scrollbar orientation='vertical'>
|
|
173
|
-
<ScrollArea.Thumb />
|
|
174
|
-
</ScrollArea.Scrollbar>
|
|
173
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
|
|
174
|
+
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
|
|
175
175
|
</ScrollArea.Root>
|
|
176
176
|
);
|
|
177
|
-
};
|
|
177
|
+
});
|
|
178
178
|
|
|
179
|
-
FormViewport.displayName =
|
|
179
|
+
FormViewport.displayName = FORM_VIEWPORT_NAME;
|
|
180
180
|
|
|
181
181
|
//
|
|
182
182
|
// Content
|
|
183
183
|
//
|
|
184
184
|
|
|
185
|
+
const FORM_CONTENT_NAME = 'Form.Content';
|
|
186
|
+
|
|
185
187
|
type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
186
188
|
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
useKeyHandler(
|
|
189
|
+
const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
|
|
190
|
+
const { form, testId } = useFormContext(FORM_CONTENT_NAME);
|
|
191
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
192
|
+
const mergedRef = useMergeRefs([forwardedRef, localRef]);
|
|
193
|
+
useKeyHandler(localRef, form);
|
|
192
194
|
|
|
193
195
|
return (
|
|
194
196
|
<div
|
|
195
|
-
|
|
196
|
-
role='form'
|
|
197
|
-
className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
|
|
197
|
+
{...composableProps(props, { role: 'form', classNames: 'flex flex-col w-full pb-form-gap' })}
|
|
198
198
|
data-testid={testId}
|
|
199
|
+
ref={mergedRef}
|
|
199
200
|
>
|
|
200
201
|
{children}
|
|
201
202
|
</div>
|
|
202
203
|
);
|
|
203
|
-
};
|
|
204
|
+
});
|
|
204
205
|
|
|
205
|
-
FormContent.displayName =
|
|
206
|
+
FormContent.displayName = FORM_CONTENT_NAME;
|
|
206
207
|
|
|
207
208
|
//
|
|
208
209
|
// FieldSet
|
|
209
210
|
//
|
|
210
211
|
|
|
211
|
-
|
|
212
|
+
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
213
|
+
|
|
214
|
+
type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
|
|
212
215
|
|
|
213
|
-
const FormFieldSet = (
|
|
214
|
-
const { form, ...
|
|
216
|
+
const FormFieldSet = (props: FormFieldSetProps) => {
|
|
217
|
+
const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
|
|
215
218
|
|
|
216
|
-
return <NaturalFormFieldSet
|
|
219
|
+
return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
|
|
217
220
|
};
|
|
218
221
|
|
|
219
|
-
FormFieldSet.displayName =
|
|
222
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
220
223
|
|
|
221
224
|
//
|
|
222
225
|
// Actions
|
|
223
226
|
//
|
|
224
227
|
|
|
228
|
+
const FORM_ACTIONS_NAME = 'Form.Actions';
|
|
229
|
+
|
|
225
230
|
type FormActionsProps = ThemedClassName<{}>;
|
|
226
231
|
|
|
227
232
|
const FormActions = ({ classNames }: FormActionsProps) => {
|
|
@@ -230,7 +235,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
230
235
|
form: { canSave, onSave, onCancel },
|
|
231
236
|
readonly,
|
|
232
237
|
layout,
|
|
233
|
-
} = useFormContext(
|
|
238
|
+
} = useFormContext(FORM_ACTIONS_NAME);
|
|
234
239
|
|
|
235
240
|
if (readonly || layout === 'static') {
|
|
236
241
|
return null;
|
|
@@ -240,12 +245,12 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
240
245
|
// Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
|
|
241
246
|
|
|
242
247
|
return (
|
|
243
|
-
<div role='none' className={mx('grid grid-flow-col auto-cols-fr
|
|
248
|
+
<div role='none' className={mx('grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}>
|
|
244
249
|
{onCancel && (
|
|
245
250
|
<IconButton
|
|
246
251
|
icon='ph--x--regular'
|
|
247
252
|
iconEnd
|
|
248
|
-
label={t('cancel
|
|
253
|
+
label={t('cancel-button.label')}
|
|
249
254
|
onClick={onCancel}
|
|
250
255
|
data-testid='cancel-button'
|
|
251
256
|
/>
|
|
@@ -257,7 +262,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
257
262
|
disabled={!canSave}
|
|
258
263
|
icon='ph--check--regular'
|
|
259
264
|
iconEnd
|
|
260
|
-
label={t('save
|
|
265
|
+
label={t('save-button.label')}
|
|
261
266
|
onClick={onSave}
|
|
262
267
|
data-testid='save-button'
|
|
263
268
|
/>
|
|
@@ -266,35 +271,62 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
266
271
|
);
|
|
267
272
|
};
|
|
268
273
|
|
|
269
|
-
FormActions.displayName =
|
|
274
|
+
FormActions.displayName = FORM_ACTIONS_NAME;
|
|
275
|
+
|
|
276
|
+
//
|
|
277
|
+
// Section
|
|
278
|
+
//
|
|
279
|
+
|
|
280
|
+
const FORM_SECTION_NAME = 'Form.Section';
|
|
281
|
+
|
|
282
|
+
type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
|
|
283
|
+
|
|
284
|
+
const FormSection = composable<HTMLDivElement, FormSectionProps>(
|
|
285
|
+
({ children, label, description, ...props }, forwardedRef) => {
|
|
286
|
+
return (
|
|
287
|
+
<div
|
|
288
|
+
{...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
|
|
289
|
+
ref={forwardedRef}
|
|
290
|
+
>
|
|
291
|
+
<h2 className='text-lg'>{label}</h2>
|
|
292
|
+
{description && <p className='text-description'>{description}</p>}
|
|
293
|
+
{children}
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
},
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
FormSection.displayName = FORM_SECTION_NAME;
|
|
270
300
|
|
|
271
301
|
//
|
|
272
302
|
// Submit
|
|
273
303
|
//
|
|
274
304
|
|
|
275
|
-
|
|
305
|
+
const FORM_SUBMIT_NAME = 'Form.Submit';
|
|
306
|
+
|
|
307
|
+
type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
|
|
276
308
|
|
|
277
|
-
const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
309
|
+
const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
|
|
278
310
|
const { t } = useTranslation(translationKey);
|
|
279
311
|
const {
|
|
280
312
|
form: { canSave, onSave },
|
|
281
313
|
readonly,
|
|
282
314
|
layout,
|
|
283
|
-
} = useFormContext(
|
|
315
|
+
} = useFormContext(FORM_SUBMIT_NAME);
|
|
284
316
|
|
|
285
317
|
if (readonly || layout === 'static') {
|
|
286
318
|
return null;
|
|
287
319
|
}
|
|
288
320
|
|
|
289
321
|
return (
|
|
290
|
-
<div role='none' className={mx('flex
|
|
322
|
+
<div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
|
|
291
323
|
<IconButton
|
|
292
|
-
classNames='
|
|
324
|
+
classNames='w-full'
|
|
293
325
|
type='submit'
|
|
294
326
|
variant='primary'
|
|
295
|
-
disabled={!canSave}
|
|
327
|
+
disabled={disabled ?? !canSave}
|
|
296
328
|
icon={icon ?? 'ph--check--regular'}
|
|
297
|
-
label={label ?? t('save
|
|
329
|
+
label={label ?? t('save-button.label')}
|
|
298
330
|
onClick={onSave}
|
|
299
331
|
data-testid='save-button'
|
|
300
332
|
/>
|
|
@@ -302,7 +334,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
|
302
334
|
);
|
|
303
335
|
};
|
|
304
336
|
|
|
305
|
-
FormSubmit.displayName =
|
|
337
|
+
FormSubmit.displayName = FORM_SUBMIT_NAME;
|
|
306
338
|
|
|
307
339
|
//
|
|
308
340
|
// Form
|
|
@@ -313,10 +345,11 @@ export const Form = {
|
|
|
313
345
|
Root: FormRoot,
|
|
314
346
|
Viewport: FormViewport,
|
|
315
347
|
Content: FormContent,
|
|
348
|
+
Section: FormSection,
|
|
316
349
|
FieldSet: FormFieldSet,
|
|
350
|
+
Label: FormFieldLabel,
|
|
317
351
|
Actions: FormActions,
|
|
318
352
|
Submit: FormSubmit,
|
|
319
|
-
Label: FormFieldLabel,
|
|
320
353
|
};
|
|
321
354
|
|
|
322
355
|
export { useFormContext, useFormValues, useFormFieldState };
|
|
@@ -325,7 +358,9 @@ export type {
|
|
|
325
358
|
FormRootProps,
|
|
326
359
|
FormViewportProps,
|
|
327
360
|
FormContentProps,
|
|
361
|
+
FormSectionProps,
|
|
328
362
|
FormFieldSetProps,
|
|
363
|
+
FormFieldLabelProps,
|
|
329
364
|
FormActionsProps,
|
|
330
|
-
|
|
365
|
+
FormSubmitProps,
|
|
331
366
|
};
|
|
@@ -25,7 +25,6 @@ import { type ProjectionModel } from '@dxos/schema';
|
|
|
25
25
|
|
|
26
26
|
import { translationKey } from '../../translations';
|
|
27
27
|
import { getRefProps } from '../../util';
|
|
28
|
-
|
|
29
28
|
import {
|
|
30
29
|
ArrayField,
|
|
31
30
|
BooleanField,
|
|
@@ -78,12 +77,19 @@ export type FormFieldProps = {
|
|
|
78
77
|
* Function to lookup custom renderers for specific properties.
|
|
79
78
|
*/
|
|
80
79
|
fieldProvider?: FormFieldProvider;
|
|
80
|
+
/**
|
|
81
|
+
* Typename of the ref type that the create props apply to.
|
|
82
|
+
* When set, createOptionLabel/createOptionIcon/createInitialValuePath/createFieldMap/onCreate
|
|
83
|
+
* are only passed to ref fields whose typename matches.
|
|
84
|
+
*/
|
|
85
|
+
createTypename?: string;
|
|
81
86
|
} & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
|
|
82
87
|
Pick<
|
|
83
88
|
RefFieldProps,
|
|
84
89
|
| 'createOptionLabel'
|
|
85
90
|
| 'createOptionIcon'
|
|
86
91
|
| 'createInitialValuePath'
|
|
92
|
+
| 'createFieldMap'
|
|
87
93
|
| 'db'
|
|
88
94
|
| 'schemaHook'
|
|
89
95
|
| 'getOptions'
|
|
@@ -102,12 +108,14 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
102
108
|
layout,
|
|
103
109
|
|
|
104
110
|
// RefFieldProps
|
|
111
|
+
createTypename,
|
|
105
112
|
createOptionLabel,
|
|
106
113
|
createOptionIcon,
|
|
107
114
|
createInitialValuePath,
|
|
115
|
+
createFieldMap,
|
|
108
116
|
db,
|
|
109
117
|
schemaHook,
|
|
110
|
-
getOptions
|
|
118
|
+
getOptions,
|
|
111
119
|
onCreate,
|
|
112
120
|
} = props;
|
|
113
121
|
const { t } = useTranslation(translationKey);
|
|
@@ -117,7 +125,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
117
125
|
|
|
118
126
|
const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
|
|
119
127
|
const placeholder = useMemo(
|
|
120
|
-
() => (examples?.length ? `${t('example
|
|
128
|
+
() => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
|
|
121
129
|
[examples, description, label],
|
|
122
130
|
);
|
|
123
131
|
|
|
@@ -169,15 +177,23 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
169
177
|
// Select field.
|
|
170
178
|
//
|
|
171
179
|
|
|
172
|
-
const options =
|
|
180
|
+
const options = getSelectOptions(type);
|
|
173
181
|
if (options) {
|
|
182
|
+
// Resolve labels from projection metadata when available.
|
|
183
|
+
const fieldProjections = projection?.getFieldProjections();
|
|
184
|
+
const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
|
|
185
|
+
const selectOptions = fieldProjection?.props.options;
|
|
186
|
+
|
|
174
187
|
return (
|
|
175
188
|
<SelectField
|
|
176
189
|
{...fieldProps}
|
|
177
|
-
options={options.map((option) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
options={options.map((option) => {
|
|
191
|
+
const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
|
|
192
|
+
return {
|
|
193
|
+
value: option,
|
|
194
|
+
label: selectOption?.title ?? option.toString(),
|
|
195
|
+
};
|
|
196
|
+
})}
|
|
181
197
|
/>
|
|
182
198
|
);
|
|
183
199
|
}
|
|
@@ -188,16 +204,18 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
188
204
|
|
|
189
205
|
const refProps = getRefProps(type);
|
|
190
206
|
if (refProps) {
|
|
207
|
+
const isCreateTarget = !createTypename || refProps.typename === createTypename;
|
|
191
208
|
return (
|
|
192
209
|
<RefField
|
|
193
210
|
{...fieldProps}
|
|
194
211
|
{...refProps}
|
|
195
|
-
createOptionLabel={createOptionLabel}
|
|
196
|
-
createOptionIcon={createOptionIcon}
|
|
197
|
-
createInitialValuePath={createInitialValuePath}
|
|
212
|
+
createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
|
|
213
|
+
createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
|
|
214
|
+
createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
|
|
215
|
+
createFieldMap={isCreateTarget ? createFieldMap : undefined}
|
|
198
216
|
db={db}
|
|
199
217
|
schemaHook={schemaHook}
|
|
200
|
-
getOptions={
|
|
218
|
+
getOptions={getOptions}
|
|
201
219
|
onCreate={onCreate}
|
|
202
220
|
/>
|
|
203
221
|
);
|
|
@@ -230,7 +248,7 @@ export const FormField = (props: FormFieldProps) => {
|
|
|
230
248
|
createInitialValuePath={createInitialValuePath}
|
|
231
249
|
db={db}
|
|
232
250
|
schemaHook={schemaHook}
|
|
233
|
-
getOptions={
|
|
251
|
+
getOptions={getOptions}
|
|
234
252
|
onCreate={onCreate}
|
|
235
253
|
/>
|
|
236
254
|
);
|
|
@@ -277,7 +295,7 @@ const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldCompo
|
|
|
277
295
|
}
|
|
278
296
|
};
|
|
279
297
|
|
|
280
|
-
const
|
|
298
|
+
const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
|
|
281
299
|
if (isLiteralUnion(ast)) {
|
|
282
300
|
return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
|
|
283
301
|
}
|
|
@@ -10,7 +10,7 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
10
10
|
|
|
11
11
|
import { type FormFieldComponentProps, FormFieldWrapper } from './FormFieldComponent';
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const DefaultStory = ({
|
|
14
14
|
type,
|
|
15
15
|
placeholder,
|
|
16
16
|
readonly,
|
|
@@ -24,7 +24,7 @@ const Component = ({
|
|
|
24
24
|
);
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<div className='
|
|
27
|
+
<div className='p-form-padding'>
|
|
28
28
|
<FormFieldWrapper<string> {...props}>
|
|
29
29
|
{({ value = '' }) => (
|
|
30
30
|
<Input.TextInput
|
|
@@ -41,10 +41,10 @@ const Component = ({
|
|
|
41
41
|
);
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const meta: Meta<typeof
|
|
44
|
+
const meta: Meta<typeof DefaultStory> = {
|
|
45
45
|
title: 'ui/react-ui-form/FormFieldComponent',
|
|
46
|
-
component:
|
|
47
|
-
decorators: [withTheme, withLayout({ layout: 'column' })],
|
|
46
|
+
component: DefaultStory,
|
|
47
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
48
48
|
parameters: {
|
|
49
49
|
layout: 'fullscreen',
|
|
50
50
|
},
|
|
@@ -15,12 +15,10 @@ import React, {
|
|
|
15
15
|
|
|
16
16
|
import { type Format } from '@dxos/echo/internal';
|
|
17
17
|
import { Icon, Input, Tooltip } from '@dxos/react-ui';
|
|
18
|
-
import {
|
|
18
|
+
import { inputTextLabel } from '@dxos/ui-theme';
|
|
19
19
|
|
|
20
20
|
import { type FormFieldStatus } from '../../hooks';
|
|
21
21
|
|
|
22
|
-
const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
|
|
23
|
-
|
|
24
22
|
/**
|
|
25
23
|
* Dynamic props passed to input components.
|
|
26
24
|
*/
|
|
@@ -33,7 +31,6 @@ export type FormFieldStateProps<T = any> = {
|
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Presentation mode.
|
|
36
|
-
*
|
|
37
34
|
* - full: Show label, control, and status.
|
|
38
35
|
* - compact: Show label and control.
|
|
39
36
|
* - inline: Control only.
|
|
@@ -79,14 +76,12 @@ export type FormFieldLabelProps = {
|
|
|
79
76
|
|
|
80
77
|
export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
|
|
81
78
|
const Label = readonly || asChild ? 'span' : Input.Label;
|
|
82
|
-
const labelProps = readonly || asChild ? { className: 'text-description text-sm' } : { classNames: '!mlb-0 text-sm' };
|
|
83
|
-
|
|
84
79
|
return (
|
|
85
|
-
<div role='none' className=
|
|
86
|
-
<Label {
|
|
80
|
+
<div role='none' className='flex items-center justify-between'>
|
|
81
|
+
<Label className={inputTextLabel}>{label}</Label>
|
|
87
82
|
{error && (
|
|
88
83
|
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
89
|
-
<Icon icon='ph--warning--regular' size={4} classNames=
|
|
84
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
|
|
90
85
|
</Tooltip.Trigger>
|
|
91
86
|
)}
|
|
92
87
|
</div>
|
|
@@ -118,15 +113,17 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
|
|
|
118
113
|
const str = String(value ?? '');
|
|
119
114
|
|
|
120
115
|
return (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<Input.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
116
|
+
<div role='none' className='contents'>
|
|
117
|
+
<Input.Root validationValence={status}>
|
|
118
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
119
|
+
{layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
|
|
120
|
+
{layout === 'full' && (
|
|
121
|
+
<Input.DescriptionAndValidation>
|
|
122
|
+
<Input.Validation>{error}</Input.Validation>
|
|
123
|
+
</Input.DescriptionAndValidation>
|
|
124
|
+
)}
|
|
125
|
+
</Input.Root>
|
|
126
|
+
</div>
|
|
130
127
|
);
|
|
131
128
|
};
|
|
132
129
|
|
|
@@ -158,8 +155,8 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
|
|
|
158
155
|
override render() {
|
|
159
156
|
if (this.state.error) {
|
|
160
157
|
return (
|
|
161
|
-
<div className='flex gap-2 border border-
|
|
162
|
-
<span className='bg-
|
|
158
|
+
<div className='flex gap-2 border border-rose-fill font-mono text-sm'>
|
|
159
|
+
<span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
|
|
163
160
|
{String(this.props.path?.join('.'))}
|
|
164
161
|
</div>
|
|
165
162
|
);
|