@dxos/react-ui-form 0.8.4-main.9735255 → 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 +989 -674
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +989 -674
- 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/index.d.ts +0 -1
- package/dist/types/src/util/index.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 +100 -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 +99 -70
- package/src/components/index.ts +3 -1
- package/src/components/testing.tsx +20 -8
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/translations.ts +63 -63
- package/src/util/index.ts +0 -1
- 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/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.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
- package/src/util/echo.ts +0 -25
|
@@ -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
11
|
import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
|
|
12
|
-
import {
|
|
13
|
-
|
|
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,29 +27,13 @@ import {
|
|
|
20
27
|
useKeyHandler,
|
|
21
28
|
} from '../../hooks';
|
|
22
29
|
import { translationKey } from '../../translations';
|
|
23
|
-
import { setValueEchoAware } from '../../util';
|
|
24
|
-
|
|
25
30
|
import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
|
|
26
31
|
import {
|
|
27
32
|
FormFieldSet as NaturalFormFieldSet,
|
|
28
33
|
type FormFieldSetProps as NaturalFormFieldSetProps,
|
|
29
34
|
} from './FormFieldSet';
|
|
30
35
|
|
|
31
|
-
//
|
|
32
|
-
// [x] Unify readonly/inline modes
|
|
33
|
-
// [ ] Don't call save/autoSave if value hasn't changed
|
|
34
|
-
// [ ] Fix onCancel (restore values)
|
|
35
|
-
// [ ] Fix useSchema Type.Obj.Any cast
|
|
36
|
-
// [ ] Remove @dxos/echo-db deps
|
|
37
|
-
// [ ] TableCellEditor (handleEnter/ModalController).
|
|
38
|
-
// [ ] Use FormFieldWrapper uniformly
|
|
39
|
-
// [ ] Inline tables for object arrays
|
|
40
|
-
// [ ] Defer query until popover
|
|
41
|
-
// [ ] Omit id from sub properties.
|
|
42
|
-
// [x] Refs
|
|
43
|
-
// [x] Single-select (fix popover)
|
|
44
|
-
// [x] Multi-select (array)
|
|
45
|
-
// [ ] 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.
|
|
46
37
|
|
|
47
38
|
// TODO(burdon): Move to @dxos/schema (re-export here).
|
|
48
39
|
export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
|
|
@@ -70,7 +61,10 @@ type FormContextValue<T extends AnyProperties = any> = {
|
|
|
70
61
|
* Testing.
|
|
71
62
|
*/
|
|
72
63
|
testId?: string;
|
|
73
|
-
} & Pick<
|
|
64
|
+
} & Pick<
|
|
65
|
+
NaturalFormFieldSetProps<T>,
|
|
66
|
+
'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap'
|
|
67
|
+
>;
|
|
74
68
|
|
|
75
69
|
const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
|
|
76
70
|
|
|
@@ -84,19 +78,25 @@ const useFormValues: {
|
|
|
84
78
|
path: (string | number)[] | undefined,
|
|
85
79
|
defaultValue: () => T,
|
|
86
80
|
): T | undefined;
|
|
87
|
-
} = (componentName: string, path
|
|
88
|
-
const
|
|
81
|
+
} = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
|
|
82
|
+
const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
|
|
83
|
+
const jsonPath = createJsonPath(stablePath);
|
|
89
84
|
const {
|
|
90
|
-
form: { values },
|
|
85
|
+
form: { values, onValueChange },
|
|
91
86
|
} = useFormContext(componentName);
|
|
92
87
|
|
|
93
88
|
const value = getValue$(values, jsonPath);
|
|
94
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);
|
|
95
94
|
useEffect(() => {
|
|
96
|
-
if (
|
|
97
|
-
|
|
95
|
+
if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
|
|
96
|
+
lastAppliedPathRef.current = jsonPath;
|
|
97
|
+
onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
|
|
98
98
|
}
|
|
99
|
-
}, [value, defaultValue]);
|
|
99
|
+
}, [value, defaultValue, onValueChange, stablePath, jsonPath]);
|
|
100
100
|
|
|
101
101
|
return value;
|
|
102
102
|
};
|
|
@@ -164,65 +164,69 @@ FormRoot.displayName = 'Form.Root';
|
|
|
164
164
|
// Viewport
|
|
165
165
|
//
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
const FORM_VIEWPORT_NAME = 'Form.Viewport';
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
type FormViewportProps = {};
|
|
170
|
+
|
|
171
|
+
const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
170
172
|
return (
|
|
171
|
-
<ScrollArea.Root>
|
|
172
|
-
<ScrollArea.Viewport
|
|
173
|
-
<ScrollArea.Scrollbar orientation='vertical'>
|
|
174
|
-
<ScrollArea.Thumb />
|
|
175
|
-
</ScrollArea.Scrollbar>
|
|
173
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
|
|
174
|
+
<ScrollArea.Viewport>{children}</ScrollArea.Viewport>
|
|
176
175
|
</ScrollArea.Root>
|
|
177
176
|
);
|
|
178
|
-
};
|
|
177
|
+
});
|
|
179
178
|
|
|
180
|
-
FormViewport.displayName =
|
|
179
|
+
FormViewport.displayName = FORM_VIEWPORT_NAME;
|
|
181
180
|
|
|
182
181
|
//
|
|
183
182
|
// Content
|
|
184
183
|
//
|
|
185
184
|
|
|
185
|
+
const FORM_CONTENT_NAME = 'Form.Content';
|
|
186
|
+
|
|
186
187
|
type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
|
|
187
188
|
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
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);
|
|
193
194
|
|
|
194
195
|
return (
|
|
195
196
|
<div
|
|
196
|
-
|
|
197
|
-
role='form'
|
|
198
|
-
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' })}
|
|
199
198
|
data-testid={testId}
|
|
199
|
+
ref={mergedRef}
|
|
200
200
|
>
|
|
201
201
|
{children}
|
|
202
202
|
</div>
|
|
203
203
|
);
|
|
204
|
-
};
|
|
204
|
+
});
|
|
205
205
|
|
|
206
|
-
FormContent.displayName =
|
|
206
|
+
FormContent.displayName = FORM_CONTENT_NAME;
|
|
207
207
|
|
|
208
208
|
//
|
|
209
209
|
// FieldSet
|
|
210
210
|
//
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
const FORM_FIELDSET_NAME = 'Form.FieldSet';
|
|
213
|
+
|
|
214
|
+
type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
|
|
213
215
|
|
|
214
|
-
const FormFieldSet = (
|
|
215
|
-
const { form, ...
|
|
216
|
+
const FormFieldSet = (props: FormFieldSetProps) => {
|
|
217
|
+
const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
|
|
216
218
|
|
|
217
|
-
return <NaturalFormFieldSet
|
|
219
|
+
return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
|
|
218
220
|
};
|
|
219
221
|
|
|
220
|
-
FormFieldSet.displayName =
|
|
222
|
+
FormFieldSet.displayName = FORM_FIELDSET_NAME;
|
|
221
223
|
|
|
222
224
|
//
|
|
223
225
|
// Actions
|
|
224
226
|
//
|
|
225
227
|
|
|
228
|
+
const FORM_ACTIONS_NAME = 'Form.Actions';
|
|
229
|
+
|
|
226
230
|
type FormActionsProps = ThemedClassName<{}>;
|
|
227
231
|
|
|
228
232
|
const FormActions = ({ classNames }: FormActionsProps) => {
|
|
@@ -231,7 +235,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
231
235
|
form: { canSave, onSave, onCancel },
|
|
232
236
|
readonly,
|
|
233
237
|
layout,
|
|
234
|
-
} = useFormContext(
|
|
238
|
+
} = useFormContext(FORM_ACTIONS_NAME);
|
|
235
239
|
|
|
236
240
|
if (readonly || layout === 'static') {
|
|
237
241
|
return null;
|
|
@@ -241,12 +245,12 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
241
245
|
// Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
|
|
242
246
|
|
|
243
247
|
return (
|
|
244
|
-
<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)}>
|
|
245
249
|
{onCancel && (
|
|
246
250
|
<IconButton
|
|
247
251
|
icon='ph--x--regular'
|
|
248
252
|
iconEnd
|
|
249
|
-
label={t('cancel
|
|
253
|
+
label={t('cancel-button.label')}
|
|
250
254
|
onClick={onCancel}
|
|
251
255
|
data-testid='cancel-button'
|
|
252
256
|
/>
|
|
@@ -258,7 +262,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
258
262
|
disabled={!canSave}
|
|
259
263
|
icon='ph--check--regular'
|
|
260
264
|
iconEnd
|
|
261
|
-
label={t('save
|
|
265
|
+
label={t('save-button.label')}
|
|
262
266
|
onClick={onSave}
|
|
263
267
|
data-testid='save-button'
|
|
264
268
|
/>
|
|
@@ -267,35 +271,62 @@ const FormActions = ({ classNames }: FormActionsProps) => {
|
|
|
267
271
|
);
|
|
268
272
|
};
|
|
269
273
|
|
|
270
|
-
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;
|
|
271
300
|
|
|
272
301
|
//
|
|
273
302
|
// Submit
|
|
274
303
|
//
|
|
275
304
|
|
|
276
|
-
|
|
305
|
+
const FORM_SUBMIT_NAME = 'Form.Submit';
|
|
306
|
+
|
|
307
|
+
type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
|
|
277
308
|
|
|
278
|
-
const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
309
|
+
const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
|
|
279
310
|
const { t } = useTranslation(translationKey);
|
|
280
311
|
const {
|
|
281
312
|
form: { canSave, onSave },
|
|
282
313
|
readonly,
|
|
283
314
|
layout,
|
|
284
|
-
} = useFormContext(
|
|
315
|
+
} = useFormContext(FORM_SUBMIT_NAME);
|
|
285
316
|
|
|
286
317
|
if (readonly || layout === 'static') {
|
|
287
318
|
return null;
|
|
288
319
|
}
|
|
289
320
|
|
|
290
321
|
return (
|
|
291
|
-
<div role='none' className={mx('flex
|
|
322
|
+
<div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
|
|
292
323
|
<IconButton
|
|
293
|
-
classNames='
|
|
324
|
+
classNames='w-full'
|
|
294
325
|
type='submit'
|
|
295
326
|
variant='primary'
|
|
296
|
-
disabled={!canSave}
|
|
327
|
+
disabled={disabled ?? !canSave}
|
|
297
328
|
icon={icon ?? 'ph--check--regular'}
|
|
298
|
-
label={label ?? t('save
|
|
329
|
+
label={label ?? t('save-button.label')}
|
|
299
330
|
onClick={onSave}
|
|
300
331
|
data-testid='save-button'
|
|
301
332
|
/>
|
|
@@ -303,7 +334,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
|
|
|
303
334
|
);
|
|
304
335
|
};
|
|
305
336
|
|
|
306
|
-
FormSubmit.displayName =
|
|
337
|
+
FormSubmit.displayName = FORM_SUBMIT_NAME;
|
|
307
338
|
|
|
308
339
|
//
|
|
309
340
|
// Form
|
|
@@ -314,10 +345,11 @@ export const Form = {
|
|
|
314
345
|
Root: FormRoot,
|
|
315
346
|
Viewport: FormViewport,
|
|
316
347
|
Content: FormContent,
|
|
348
|
+
Section: FormSection,
|
|
317
349
|
FieldSet: FormFieldSet,
|
|
350
|
+
Label: FormFieldLabel,
|
|
318
351
|
Actions: FormActions,
|
|
319
352
|
Submit: FormSubmit,
|
|
320
|
-
Label: FormFieldLabel,
|
|
321
353
|
};
|
|
322
354
|
|
|
323
355
|
export { useFormContext, useFormValues, useFormFieldState };
|
|
@@ -326,7 +358,9 @@ export type {
|
|
|
326
358
|
FormRootProps,
|
|
327
359
|
FormViewportProps,
|
|
328
360
|
FormContentProps,
|
|
361
|
+
FormSectionProps,
|
|
329
362
|
FormFieldSetProps,
|
|
363
|
+
FormFieldLabelProps,
|
|
330
364
|
FormActionsProps,
|
|
331
|
-
|
|
365
|
+
FormSubmitProps,
|
|
332
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
|
);
|