@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.
Files changed (119) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +989 -674
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +989 -674
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +62 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +23 -18
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +68 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +8 -6
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +62 -61
  29. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  31. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  32. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  33. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  34. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  35. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  36. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +62 -61
  37. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  39. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  40. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +83 -0
  41. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  43. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  44. package/dist/types/src/components/Settings/Settings.d.ts +35 -0
  45. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  46. package/dist/types/src/components/Settings/Settings.stories.d.ts +82 -0
  47. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/Settings/index.d.ts +2 -0
  49. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  50. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  51. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  52. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +66 -64
  53. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/index.d.ts +3 -1
  55. package/dist/types/src/components/index.d.ts.map +1 -1
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  58. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  59. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +62 -61
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/index.d.ts +0 -1
  63. package/dist/types/src/util/index.d.ts.map +1 -1
  64. package/dist/types/src/util/refs.d.ts +1 -0
  65. package/dist/types/src/util/refs.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +37 -38
  68. package/src/components/FieldEditor/FieldEditor.stories.tsx +7 -8
  69. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  70. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  71. package/src/components/Form/AUDIT.md +17 -0
  72. package/src/components/Form/Form.stories.tsx +25 -26
  73. package/src/components/Form/Form.tsx +100 -66
  74. package/src/components/Form/FormField.tsx +32 -14
  75. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  76. package/src/components/Form/FormFieldComponent.tsx +17 -20
  77. package/src/components/Form/FormFieldSet.tsx +126 -105
  78. package/src/components/Form/fields/ArrayField.tsx +11 -8
  79. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  80. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  81. package/src/components/Form/fields/RefField.tsx +37 -20
  82. package/src/components/Form/fields/SelectField.tsx +1 -1
  83. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -3
  84. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  85. package/src/components/Form/fields/TupleField.tsx +1 -1
  86. package/src/components/ObjectForm/ObjectForm.tsx +113 -0
  87. package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
  88. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -18
  89. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  90. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +63 -0
  91. package/src/components/ObjectProperties/ObjectProperties.tsx +129 -0
  92. package/src/components/ObjectProperties/index.ts +5 -0
  93. package/src/components/Settings/Settings.stories.tsx +55 -0
  94. package/src/components/Settings/Settings.tsx +129 -0
  95. package/src/components/Settings/index.ts +5 -0
  96. package/src/components/ViewEditor/ViewEditor.stories.tsx +36 -33
  97. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -7
  98. package/src/components/ViewEditor/ViewEditor.tsx +99 -70
  99. package/src/components/index.ts +3 -1
  100. package/src/components/testing.tsx +20 -8
  101. package/src/hooks/useFormHandler.ts +42 -24
  102. package/src/hooks/useKeyHandler.tsx +7 -7
  103. package/src/translations.ts +63 -63
  104. package/src/util/index.ts +0 -1
  105. package/src/util/refs.ts +7 -3
  106. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  107. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  108. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  109. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  110. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  111. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  112. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  113. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  114. package/dist/types/src/util/echo.d.ts +0 -6
  115. package/dist/types/src/util/echo.d.ts.map +0 -1
  116. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  117. package/src/components/ControlSection/ControlSection.tsx +0 -153
  118. package/src/components/Form/Form-old.stories.tsx +0 -314
  119. 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 type * as SchemaAST from 'effect/SchemaAST';
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 { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
- import { mx } from '@dxos/ui-theme';
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
- // New features/polish
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<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
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: (string | number)[] = [], defaultValue?: () => any) => {
88
- const jsonPath = createJsonPath(path);
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 (!value && defaultValue) {
97
- setValueEchoAware(values, jsonPath, defaultValue());
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
- type FormViewportProps = ThemedClassName<PropsWithChildren<{}>>;
167
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
168
168
 
169
- const FormViewport = ({ classNames, children }: FormViewportProps) => {
169
+ type FormViewportProps = {};
170
+
171
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
170
172
  return (
171
- <ScrollArea.Root>
172
- <ScrollArea.Viewport classNames={['plb-cardSpacingBlock', classNames]}>{children}</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 = 'Form.Viewport';
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
- // TOOD(burdon): Figure out nesting (indent and testId).
189
- const FormContent = ({ classNames, children }: FormContentProps) => {
190
- const { form, testId } = useFormContext(FormContent.displayName);
191
- const ref = useRef<HTMLDivElement>(null);
192
- useKeyHandler(ref.current, form);
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
- ref={ref}
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 = 'Form.Content';
206
+ FormContent.displayName = FORM_CONTENT_NAME;
207
207
 
208
208
  //
209
209
  // FieldSet
210
210
  //
211
211
 
212
- type FormFieldSetProps = ThemedClassName<{}>;
212
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
213
+
214
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
213
215
 
214
- const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
215
- const { form, ...props } = useFormContext(FormFieldSet.displayName);
216
+ const FormFieldSet = (props: FormFieldSetProps) => {
217
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
216
218
 
217
- return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
219
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
218
220
  };
219
221
 
220
- FormFieldSet.displayName = 'Form.FieldSet';
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(FormActions.displayName);
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 gap-2 pbs-cardSpacingBlock', classNames)}>
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 button label')}
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 button label')}
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 = 'Form.Actions';
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
- type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label'>>>;
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(FormSubmit.displayName);
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 is-full pbs-cardSpacingBlock', classNames)}>
322
+ <div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
292
323
  <IconButton
293
- classNames='is-full'
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 button label')}
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 = 'Form.Submit';
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
- FormFieldLabelProps as LabelProps,
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: getRefOptions,
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 placeholder')}: ${examples[0]}` : (description ?? label)),
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 = getOptions(type);
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
- value: option,
179
- label: option.toString(),
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={getRefOptions}
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={getRefOptions}
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 getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
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 Component = ({
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='plb-cardSpacingBlock pli-cardSpacingInline'>
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 Component> = {
44
+ const meta: Meta<typeof DefaultStory> = {
45
45
  title: 'ui/react-ui-form/FormFieldComponent',
46
- component: 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 { errorText, mx } from '@dxos/ui-theme';
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={mx('flex justify-between items-center', labelSpacing)}>
86
- <Label {...labelProps}>{label}</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={errorText} />
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
- <Input.Root validationValence={status}>
122
- {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
123
- {layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
124
- {layout === 'full' && (
125
- <Input.DescriptionAndValidation>
126
- <Input.Validation>{error}</Input.Validation>
127
- </Input.DescriptionAndValidation>
128
- )}
129
- </Input.Root>
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-roseFill font-mono text-sm'>
162
- <span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
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
  );