@dxos/react-ui-form 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b

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 (153) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +1134 -682
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +82 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +1134 -682
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +84 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  13. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  15. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +44 -16
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +75 -73
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  21. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormFieldSet.d.ts +6 -10
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  33. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  36. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
  38. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  40. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  42. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  45. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  46. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  47. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  48. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  49. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  50. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  54. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  58. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  60. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  61. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  62. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  63. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  64. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  65. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  66. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  68. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  69. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  70. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  71. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  72. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  73. package/dist/types/src/components/Settings/index.d.ts +2 -0
  74. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  75. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -6
  76. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  77. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -64
  78. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  79. package/dist/types/src/components/index.d.ts +2 -0
  80. package/dist/types/src/components/index.d.ts.map +1 -1
  81. package/dist/types/src/components/testing.d.ts +4 -4
  82. package/dist/types/src/components/testing.d.ts.map +1 -1
  83. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  84. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  85. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  86. package/dist/types/src/index.d.ts +0 -2
  87. package/dist/types/src/index.d.ts.map +1 -1
  88. package/dist/types/src/translations.d.ts +63 -61
  89. package/dist/types/src/translations.d.ts.map +1 -1
  90. package/dist/types/src/util/field-type.d.ts +19 -0
  91. package/dist/types/src/util/field-type.d.ts.map +1 -0
  92. package/dist/types/src/util/field.d.ts.map +1 -1
  93. package/dist/types/src/util/formatting.d.ts.map +1 -1
  94. package/dist/types/src/util/index.d.ts +1 -1
  95. package/dist/types/src/util/index.d.ts.map +1 -1
  96. package/dist/types/src/util/properties.d.ts.map +1 -1
  97. package/dist/types/src/util/refs.d.ts +1 -0
  98. package/dist/types/src/util/refs.d.ts.map +1 -1
  99. package/dist/types/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +42 -42
  101. package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -8
  102. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  103. package/src/components/FieldEditor/FieldEditor.tsx +6 -6
  104. package/src/components/Form/AUDIT.md +17 -0
  105. package/src/components/Form/Form.stories.tsx +29 -44
  106. package/src/components/Form/Form.tsx +101 -54
  107. package/src/components/Form/FormField.tsx +39 -19
  108. package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
  109. package/src/components/Form/FormFieldComponent.tsx +11 -11
  110. package/src/components/Form/FormFieldSet.tsx +121 -104
  111. package/src/components/Form/fields/ArrayField.tsx +10 -9
  112. package/src/components/Form/fields/GeoPointField.tsx +8 -7
  113. package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
  114. package/src/components/Form/fields/MarkdownField.tsx +135 -45
  115. package/src/components/Form/fields/RefField.tsx +85 -45
  116. package/src/components/Form/fields/SelectField.tsx +1 -1
  117. package/src/components/Form/fields/SelectOptionField.stories.tsx +10 -8
  118. package/src/components/Form/fields/SelectOptionField.tsx +13 -13
  119. package/src/components/Form/fields/TextField.tsx +2 -2
  120. package/src/components/Form/fields/TupleField.tsx +1 -1
  121. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  122. package/src/components/ObjectForm/index.ts +5 -0
  123. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +16 -18
  124. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  125. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  126. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  127. package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
  128. package/src/components/ObjectProperties/index.ts +5 -0
  129. package/src/components/Settings/AUDIT.md +52 -0
  130. package/src/components/Settings/Settings.stories.tsx +69 -21
  131. package/src/components/Settings/Settings.tsx +33 -187
  132. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  133. package/src/components/Settings/SettingsItem.tsx +58 -0
  134. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  135. package/src/components/Settings/index.ts +2 -0
  136. package/src/components/ViewEditor/ViewEditor.stories.tsx +21 -19
  137. package/src/components/ViewEditor/ViewEditor.test.tsx +2 -2
  138. package/src/components/ViewEditor/ViewEditor.tsx +87 -50
  139. package/src/components/index.ts +2 -0
  140. package/src/components/testing.tsx +23 -12
  141. package/src/hooks/useFormHandler.ts +42 -24
  142. package/src/hooks/useKeyHandler.tsx +7 -7
  143. package/src/index.ts +0 -4
  144. package/src/translations.ts +66 -63
  145. package/src/util/field-type.ts +60 -0
  146. package/src/util/index.ts +1 -1
  147. package/src/util/refs.ts +7 -3
  148. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  149. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  150. package/dist/types/src/util/echo.d.ts +0 -6
  151. package/dist/types/src/util/echo.d.ts.map +0 -1
  152. package/src/components/Form/Form-old.stories.tsx +0 -314
  153. package/src/util/echo.ts +0 -25
@@ -3,14 +3,25 @@
3
3
  //
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
+ import * as Option from 'effect/Option';
6
7
  import * as Schema from 'effect/Schema';
7
- import type * as SchemaAST from 'effect/SchemaAST';
8
+ import * as SchemaAST from 'effect/SchemaAST';
8
9
  import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
9
10
 
11
+ import { Annotation as EchoAnnotation } from '@dxos/echo';
10
12
  import { type AnyProperties } from '@dxos/echo/internal';
11
13
  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';
14
+ import {
15
+ IconButton,
16
+ type IconButtonProps,
17
+ ScrollArea,
18
+ type ThemedClassName,
19
+ useMergeRefs,
20
+ useTranslation,
21
+ } from '@dxos/react-ui';
22
+ import { composable, composableProps, mx, withColumn } from '@dxos/ui-theme';
23
+
24
+ import { translationKey } from '#translations';
14
25
 
15
26
  import {
16
27
  type FormHandler,
@@ -19,38 +30,35 @@ import {
19
30
  useFormHandler,
20
31
  useKeyHandler,
21
32
  } from '../../hooks';
22
- import { translationKey } from '../../translations';
23
- import { setValueEchoAware } from '../../util';
24
-
25
33
  import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
26
34
  import {
27
35
  FormFieldSet as NaturalFormFieldSet,
28
36
  type FormFieldSetProps as NaturalFormFieldSetProps,
29
37
  } from './FormFieldSet';
30
38
 
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)
39
+ // TODO(burdon): Move styles to form.ts (as with ui-theme).
46
40
 
47
- // TODO(burdon): Move to @dxos/schema (re-export here).
41
+ // TODO(burdon): Reconcile with @dxos/echo.
48
42
  export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
49
43
 
50
44
  // TODO(burdon): Move to @dxos/schema (re-export here).
51
45
  export const omitId = <S extends Schema.Schema.AnyNoContext>(schema: S): Schema.Schema<ExcludeId<S>, ExcludeId<S>> =>
52
46
  schema.pipe(Schema.omit('id')) as any;
53
47
 
48
+ /**
49
+ * Drop fields annotated with `FormInputAnnotation.set(false)` from a schema so
50
+ * the form's validator doesn't trip on required-but-hidden fields. Used by
51
+ * the picker's inline create form, where a `FactoryAnnotation` typically
52
+ * supplies the hidden values (e.g. a backing-object Ref) outside the form.
53
+ */
54
+ export const omitHiddenFormFields = <S extends Schema.Schema.AnyNoContext>(schema: S): S => {
55
+ const properties = SchemaAST.getPropertySignatures(schema.ast);
56
+ const hidden = properties
57
+ .filter((prop) => Option.getOrElse(EchoAnnotation.FormInputAnnotation.getFromAst(prop.type), () => true) === false)
58
+ .map((prop) => prop.name as string);
59
+ return hidden.length === 0 ? schema : (schema.pipe(Schema.omit(...(hidden as [string, ...string[]]))) as any);
60
+ };
61
+
54
62
  //
55
63
  // Context
56
64
  //
@@ -70,7 +78,10 @@ type FormContextValue<T extends AnyProperties = any> = {
70
78
  * Testing.
71
79
  */
72
80
  testId?: string;
73
- } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
81
+ } & Pick<
82
+ NaturalFormFieldSetProps<T>,
83
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap' | 'db'
84
+ >;
74
85
 
75
86
  const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
76
87
 
@@ -84,19 +95,25 @@ const useFormValues: {
84
95
  path: (string | number)[] | undefined,
85
96
  defaultValue: () => T,
86
97
  ): T | undefined;
87
- } = (componentName: string, path: (string | number)[] = [], defaultValue?: () => any) => {
88
- const jsonPath = createJsonPath(path);
98
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
99
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
100
+ const jsonPath = createJsonPath(stablePath);
89
101
  const {
90
- form: { values },
102
+ form: { values, onValueChange },
91
103
  } = useFormContext(componentName);
92
104
 
93
105
  const value = getValue$(values, jsonPath);
94
106
 
107
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
108
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
109
+ // we apply per path when the hook is used for different fields.
110
+ const lastAppliedPathRef = useRef<string | null>(null);
95
111
  useEffect(() => {
96
- if (!value && defaultValue) {
97
- setValueEchoAware(values, jsonPath, defaultValue());
112
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
113
+ lastAppliedPathRef.current = jsonPath;
114
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
98
115
  }
99
- }, [value, defaultValue]);
116
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
100
117
 
101
118
  return value;
102
119
  };
@@ -166,16 +183,15 @@ FormRoot.displayName = 'Form.Root';
166
183
 
167
184
  const FORM_VIEWPORT_NAME = 'Form.Viewport';
168
185
 
169
- type FormViewportProps = PropsWithChildren<{}>;
186
+ type FormViewportProps = {};
170
187
 
171
- // TODO(burdon): Ref and props (allow asChild).
172
- const FormViewport = ({ children }: FormViewportProps) => {
188
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
173
189
  return (
174
- <ScrollArea.Root orientation='vertical'>
190
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
175
191
  <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
176
192
  </ScrollArea.Root>
177
193
  );
178
- };
194
+ });
179
195
 
180
196
  FormViewport.displayName = FORM_VIEWPORT_NAME;
181
197
 
@@ -187,23 +203,25 @@ const FORM_CONTENT_NAME = 'Form.Content';
187
203
 
188
204
  type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
189
205
 
190
- // TOOD(burdon): Figure out nesting (indent and testId).
191
- const FormContent = ({ classNames, children }: FormContentProps) => {
206
+ const FormContent = composable<HTMLDivElement, FormContentProps>(({ children, ...props }, forwardedRef) => {
192
207
  const { form, testId } = useFormContext(FORM_CONTENT_NAME);
193
- const ref = useRef<HTMLDivElement>(null);
194
- useKeyHandler(ref.current, form);
208
+ const localRef = useRef<HTMLDivElement>(null);
209
+ const mergedRef = useMergeRefs([forwardedRef, localRef]);
210
+ useKeyHandler(localRef, form);
195
211
 
196
212
  return (
197
213
  <div
198
- ref={ref}
199
- role='form'
200
- className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
214
+ {...composableProps(props, {
215
+ role: 'form',
216
+ classNames: mx(withColumn.center(), 'flex flex-col w-full pb-form-gap'),
217
+ })}
201
218
  data-testid={testId}
219
+ ref={mergedRef}
202
220
  >
203
221
  {children}
204
222
  </div>
205
223
  );
206
- };
224
+ });
207
225
 
208
226
  FormContent.displayName = FORM_CONTENT_NAME;
209
227
 
@@ -213,12 +231,12 @@ FormContent.displayName = FORM_CONTENT_NAME;
213
231
 
214
232
  const FORM_FIELDSET_NAME = 'Form.FieldSet';
215
233
 
216
- type FormFieldSetProps = ThemedClassName<{}>;
234
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
217
235
 
218
- const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
219
- const { form, ...props } = useFormContext(FORM_FIELDSET_NAME);
236
+ const FormFieldSet = (props: FormFieldSetProps) => {
237
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
220
238
 
221
- return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
239
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
222
240
  };
223
241
 
224
242
  FormFieldSet.displayName = FORM_FIELDSET_NAME;
@@ -247,12 +265,14 @@ const FormActions = ({ classNames }: FormActionsProps) => {
247
265
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
248
266
 
249
267
  return (
250
- <div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
268
+ <div
269
+ className={mx(withColumn.center(), 'grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}
270
+ >
251
271
  {onCancel && (
252
272
  <IconButton
253
273
  icon='ph--x--regular'
254
274
  iconEnd
255
- label={t('cancel button label')}
275
+ label={t('cancel-button.label')}
256
276
  onClick={onCancel}
257
277
  data-testid='cancel-button'
258
278
  />
@@ -264,7 +284,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
264
284
  disabled={!canSave}
265
285
  icon='ph--check--regular'
266
286
  iconEnd
267
- label={t('save button label')}
287
+ label={t('save-button.label')}
268
288
  onClick={onSave}
269
289
  data-testid='save-button'
270
290
  />
@@ -275,6 +295,31 @@ const FormActions = ({ classNames }: FormActionsProps) => {
275
295
 
276
296
  FormActions.displayName = FORM_ACTIONS_NAME;
277
297
 
298
+ //
299
+ // Section
300
+ //
301
+
302
+ const FORM_SECTION_NAME = 'Form.Section';
303
+
304
+ type FormSectionProps = ThemedClassName<{ label: string; description?: string }>;
305
+
306
+ const FormSection = composable<HTMLDivElement, FormSectionProps>(
307
+ ({ children, label, description, ...props }, forwardedRef) => {
308
+ return (
309
+ <div
310
+ {...composableProps(props, { classNames: 'flex flex-col pt-form-section-gap first:pt-0' })}
311
+ ref={forwardedRef}
312
+ >
313
+ <h2 className='text-lg'>{label}</h2>
314
+ {description && <p className='text-description'>{description}</p>}
315
+ {children}
316
+ </div>
317
+ );
318
+ },
319
+ );
320
+
321
+ FormSection.displayName = FORM_SECTION_NAME;
322
+
278
323
  //
279
324
  // Submit
280
325
  //
@@ -296,14 +341,14 @@ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
296
341
  }
297
342
 
298
343
  return (
299
- <div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
344
+ <div className={mx('flex w-full pt-form-padding', classNames)}>
300
345
  <IconButton
301
- classNames='is-full'
346
+ classNames='w-full'
302
347
  type='submit'
303
348
  variant='primary'
304
349
  disabled={disabled ?? !canSave}
305
350
  icon={icon ?? 'ph--check--regular'}
306
- label={label ?? t('save button label')}
351
+ label={label ?? t('save-button.label')}
307
352
  onClick={onSave}
308
353
  data-testid='save-button'
309
354
  />
@@ -322,10 +367,11 @@ export const Form = {
322
367
  Root: FormRoot,
323
368
  Viewport: FormViewport,
324
369
  Content: FormContent,
370
+ Section: FormSection,
325
371
  FieldSet: FormFieldSet,
372
+ Label: FormFieldLabel,
326
373
  Actions: FormActions,
327
374
  Submit: FormSubmit,
328
- Label: FormFieldLabel,
329
375
  };
330
376
 
331
377
  export { useFormContext, useFormValues, useFormFieldState };
@@ -334,8 +380,9 @@ export type {
334
380
  FormRootProps,
335
381
  FormViewportProps,
336
382
  FormContentProps,
383
+ FormSectionProps,
337
384
  FormFieldSetProps,
385
+ FormFieldLabelProps,
338
386
  FormActionsProps,
339
387
  FormSubmitProps,
340
- FormFieldLabelProps as LabelProps,
341
388
  };
@@ -23,9 +23,9 @@ import {
23
23
  import { useTranslation } from '@dxos/react-ui';
24
24
  import { type ProjectionModel } from '@dxos/schema';
25
25
 
26
- import { translationKey } from '../../translations';
27
- import { getRefProps } from '../../util';
26
+ import { translationKey } from '#translations';
28
27
 
28
+ import { getRefProps } from '../../util';
29
29
  import {
30
30
  ArrayField,
31
31
  BooleanField,
@@ -78,14 +78,21 @@ export type FormFieldProps = {
78
78
  * Function to lookup custom renderers for specific properties.
79
79
  */
80
80
  fieldProvider?: FormFieldProvider;
81
+ /**
82
+ * Typename of the ref type that the create props apply to.
83
+ * When set, createOptionLabel/createOptionIcon/createInitialValuePath/createFieldMap/onCreate
84
+ * are only passed to ref fields whose typename matches.
85
+ */
86
+ createTypename?: string;
81
87
  } & Pick<FormFieldComponentProps, 'autoFocus' | 'readonly' | 'layout'> &
82
88
  Pick<
83
89
  RefFieldProps,
84
90
  | 'createOptionLabel'
85
91
  | 'createOptionIcon'
86
92
  | 'createInitialValuePath'
93
+ | 'createFieldMap'
87
94
  | 'db'
88
- | 'schemaHook'
95
+ | 'useSchema'
89
96
  | 'getOptions'
90
97
  | 'onCreate'
91
98
  >;
@@ -102,12 +109,14 @@ export const FormField = (props: FormFieldProps) => {
102
109
  layout,
103
110
 
104
111
  // RefFieldProps
112
+ createTypename,
105
113
  createOptionLabel,
106
114
  createOptionIcon,
107
115
  createInitialValuePath,
116
+ createFieldMap,
108
117
  db,
109
- schemaHook,
110
- getOptions: getRefOptions,
118
+ useSchema: schemaHook,
119
+ getOptions,
111
120
  onCreate,
112
121
  } = props;
113
122
  const { t } = useTranslation(translationKey);
@@ -117,7 +126,7 @@ export const FormField = (props: FormFieldProps) => {
117
126
 
118
127
  const label = useMemo(() => title ?? String.capitalize(name), [title, name]);
119
128
  const placeholder = useMemo(
120
- () => (examples?.length ? `${t('example placeholder')}: ${examples[0]}` : (description ?? label)),
129
+ () => (examples?.length ? `${t('example.placeholder')}: ${examples[0]}` : (description ?? label)),
121
130
  [examples, description, label],
122
131
  );
123
132
 
@@ -129,6 +138,7 @@ export const FormField = (props: FormFieldProps) => {
129
138
  label,
130
139
  placeholder,
131
140
  layout,
141
+ db,
132
142
  ...fieldState,
133
143
  };
134
144
 
@@ -169,15 +179,23 @@ export const FormField = (props: FormFieldProps) => {
169
179
  // Select field.
170
180
  //
171
181
 
172
- const options = getOptions(type);
182
+ const options = getSelectOptions(type);
173
183
  if (options) {
184
+ // Resolve labels from projection metadata when available.
185
+ const fieldProjections = projection?.getFieldProjections();
186
+ const fieldProjection = fieldProjections?.find((fp) => fp.field.path === name);
187
+ const selectOptions = fieldProjection?.props.options;
188
+
174
189
  return (
175
190
  <SelectField
176
191
  {...fieldProps}
177
- options={options.map((option) => ({
178
- value: option,
179
- label: option.toString(),
180
- }))}
192
+ options={options.map((option) => {
193
+ const selectOption = selectOptions?.find((so) => so.id === globalThis.String(option));
194
+ return {
195
+ value: option,
196
+ label: selectOption?.title ?? option.toString(),
197
+ };
198
+ })}
181
199
  />
182
200
  );
183
201
  }
@@ -188,16 +206,18 @@ export const FormField = (props: FormFieldProps) => {
188
206
 
189
207
  const refProps = getRefProps(type);
190
208
  if (refProps) {
209
+ const isCreateTarget = !createTypename || refProps.typename === createTypename;
191
210
  return (
192
211
  <RefField
193
212
  {...fieldProps}
194
213
  {...refProps}
195
- createOptionLabel={createOptionLabel}
196
- createOptionIcon={createOptionIcon}
197
- createInitialValuePath={createInitialValuePath}
214
+ createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
215
+ createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
216
+ createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
217
+ createFieldMap={isCreateTarget ? createFieldMap : undefined}
198
218
  db={db}
199
- schemaHook={schemaHook}
200
- getOptions={getRefOptions}
219
+ useSchema={schemaHook}
220
+ getOptions={getOptions}
201
221
  onCreate={onCreate}
202
222
  />
203
223
  );
@@ -229,8 +249,8 @@ export const FormField = (props: FormFieldProps) => {
229
249
  createOptionIcon={createOptionIcon}
230
250
  createInitialValuePath={createInitialValuePath}
231
251
  db={db}
232
- schemaHook={schemaHook}
233
- getOptions={getRefOptions}
252
+ useSchema={schemaHook}
253
+ getOptions={getOptions}
234
254
  onCreate={onCreate}
235
255
  />
236
256
  );
@@ -277,7 +297,7 @@ const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldCompo
277
297
  }
278
298
  };
279
299
 
280
- const getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
300
+ const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
281
301
  if (isLiteralUnion(ast)) {
282
302
  return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
283
303
  }
@@ -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,9 +41,9 @@ 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,
46
+ component: DefaultStory,
47
47
  decorators: [withTheme(), withLayout({ layout: 'column' })],
48
48
  parameters: {
49
49
  layout: 'fullscreen',
@@ -13,14 +13,13 @@ import React, {
13
13
  type ReactNode,
14
14
  } from 'react';
15
15
 
16
+ import { type Database } from '@dxos/echo';
16
17
  import { type Format } from '@dxos/echo/internal';
17
18
  import { Icon, Input, Tooltip } from '@dxos/react-ui';
18
- import { errorText } from '@dxos/ui-theme';
19
+ import { inputTextLabel } from '@dxos/ui-theme';
19
20
 
20
21
  import { type FormFieldStatus } from '../../hooks';
21
22
 
22
- const labelSpacing = 'mbs-inputSpacingBlock mbe-labelSpacingBlock';
23
-
24
23
  /**
25
24
  * Dynamic props passed to input components.
26
25
  */
@@ -33,7 +32,6 @@ export type FormFieldStateProps<T = any> = {
33
32
 
34
33
  /**
35
34
  * Presentation mode.
36
- *
37
35
  * - full: Show label, control, and status.
38
36
  * - compact: Show label and control.
39
37
  * - inline: Control only.
@@ -53,6 +51,8 @@ export type FormFieldComponentProps<T = any> = {
53
51
  placeholder?: string;
54
52
  autoFocus?: boolean;
55
53
  layout?: Presentation;
54
+ /** Database the form is editing against; populated for fields whose value is an ECHO object/Ref. */
55
+ db?: Database.Database;
56
56
  } & FormFieldStateProps<T>;
57
57
 
58
58
  /**
@@ -80,14 +80,14 @@ export type FormFieldLabelProps = {
80
80
  export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
81
81
  const Label = readonly || asChild ? 'span' : Input.Label;
82
82
  return (
83
- <>
84
- <Label>{label}</Label>
83
+ <div className='flex items-center justify-between'>
84
+ <Label className={inputTextLabel}>{label}</Label>
85
85
  {error && (
86
86
  <Tooltip.Trigger asChild content={error} side='bottom'>
87
- <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
87
+ <Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
88
88
  </Tooltip.Trigger>
89
89
  )}
90
- </>
90
+ </div>
91
91
  );
92
92
  };
93
93
 
@@ -116,7 +116,7 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
116
116
  const str = String(value ?? '');
117
117
 
118
118
  return (
119
- <div role='none' className='contents'>
119
+ <div className='contents'>
120
120
  <Input.Root validationValence={status}>
121
121
  {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
122
122
  {layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
@@ -158,8 +158,8 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
158
158
  override render() {
159
159
  if (this.state.error) {
160
160
  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>
161
+ <div className='flex gap-2 border border-rose-fill font-mono text-sm'>
162
+ <span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
163
163
  {String(this.props.path?.join('.'))}
164
164
  </div>
165
165
  );