@dxos/react-ui-form 0.8.4-main.c85a9c8dae → 0.8.4-main.d05539e30a

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 (147) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +1028 -625
  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 +1028 -625
  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.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.d.ts +40 -12
  16. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Form.stories.d.ts +76 -74
  18. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormField.d.ts +11 -5
  20. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
  22. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormFieldSet.d.ts +5 -9
  25. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/fields/ArrayField.d.ts +4 -4
  27. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +8 -1
  31. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +84 -0
  33. package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -1
  35. package/dist/types/src/components/Form/fields/RefField.d.ts +15 -7
  36. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +63 -61
  40. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
  42. package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
  43. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
  44. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
  45. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -0
  46. package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
  47. package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
  48. package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
  49. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +26 -4
  50. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  51. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -61
  52. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
  54. package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
  55. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +127 -0
  56. package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
  57. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
  58. package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
  60. package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
  61. package/dist/types/src/components/Settings/Settings.d.ts +28 -33
  62. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
  63. package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -61
  64. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
  66. package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
  67. package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
  68. package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
  69. package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
  70. package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
  71. package/dist/types/src/components/Settings/index.d.ts +2 -0
  72. package/dist/types/src/components/Settings/index.d.ts.map +1 -1
  73. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -4
  74. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  75. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -63
  76. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  77. package/dist/types/src/components/index.d.ts +2 -0
  78. package/dist/types/src/components/index.d.ts.map +1 -1
  79. package/dist/types/src/components/testing.d.ts +4 -4
  80. package/dist/types/src/components/testing.d.ts.map +1 -1
  81. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  82. package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
  83. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
  84. package/dist/types/src/index.d.ts +0 -2
  85. package/dist/types/src/index.d.ts.map +1 -1
  86. package/dist/types/src/translations.d.ts +63 -61
  87. package/dist/types/src/translations.d.ts.map +1 -1
  88. package/dist/types/src/util/field-type.d.ts +19 -0
  89. package/dist/types/src/util/field-type.d.ts.map +1 -0
  90. package/dist/types/src/util/field.d.ts.map +1 -1
  91. package/dist/types/src/util/formatting.d.ts.map +1 -1
  92. package/dist/types/src/util/index.d.ts +1 -0
  93. package/dist/types/src/util/index.d.ts.map +1 -1
  94. package/dist/types/src/util/properties.d.ts +7 -1
  95. package/dist/types/src/util/properties.d.ts.map +1 -1
  96. package/dist/types/src/util/properties.test.d.ts +2 -0
  97. package/dist/types/src/util/properties.test.d.ts.map +1 -0
  98. package/dist/types/src/util/refs.d.ts +1 -0
  99. package/dist/types/src/util/refs.d.ts.map +1 -1
  100. package/dist/types/tsconfig.tsbuildinfo +1 -1
  101. package/package.json +43 -41
  102. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  103. package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
  104. package/src/components/FieldEditor/FieldEditor.tsx +4 -3
  105. package/src/components/Form/AUDIT.md +17 -0
  106. package/src/components/Form/Form.stories.tsx +25 -38
  107. package/src/components/Form/Form.tsx +77 -48
  108. package/src/components/Form/FormField.tsx +27 -15
  109. package/src/components/Form/FormFieldComponent.tsx +6 -4
  110. package/src/components/Form/FormFieldSet.tsx +98 -77
  111. package/src/components/Form/fields/ArrayField.tsx +14 -8
  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 +79 -39
  116. package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
  117. package/src/components/Form/fields/SelectOptionField.tsx +12 -12
  118. package/src/components/Form/fields/TextField.tsx +2 -2
  119. package/src/components/Form/fields/TupleField.tsx +1 -1
  120. package/src/components/ObjectForm/ObjectForm.tsx +114 -0
  121. package/src/components/ObjectForm/index.ts +5 -0
  122. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +12 -10
  123. package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
  124. package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
  125. package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
  126. package/src/components/ObjectProperties/ObjectProperties.tsx +148 -0
  127. package/src/components/ObjectProperties/index.ts +5 -0
  128. package/src/components/Settings/AUDIT.md +51 -0
  129. package/src/components/Settings/Settings.stories.tsx +69 -21
  130. package/src/components/Settings/Settings.tsx +33 -183
  131. package/src/components/Settings/SettingsFieldSet.tsx +257 -0
  132. package/src/components/Settings/SettingsItem.tsx +58 -0
  133. package/src/components/Settings/SettingsSelectField.tsx +59 -0
  134. package/src/components/Settings/index.ts +2 -0
  135. package/src/components/ViewEditor/ViewEditor.stories.tsx +14 -15
  136. package/src/components/ViewEditor/ViewEditor.test.tsx +0 -1
  137. package/src/components/ViewEditor/ViewEditor.tsx +40 -36
  138. package/src/components/index.ts +2 -0
  139. package/src/components/testing.tsx +19 -11
  140. package/src/hooks/useKeyHandler.tsx +7 -7
  141. package/src/index.ts +0 -4
  142. package/src/translations.ts +66 -63
  143. package/src/util/field-type.ts +60 -0
  144. package/src/util/index.ts +1 -0
  145. package/src/util/properties.test.ts +63 -0
  146. package/src/util/properties.ts +14 -3
  147. package/src/util/refs.ts +7 -3
@@ -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,7 +179,7 @@ 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) {
174
184
  // Resolve labels from projection metadata when available.
175
185
  const fieldProjections = projection?.getFieldProjections();
@@ -196,16 +206,18 @@ export const FormField = (props: FormFieldProps) => {
196
206
 
197
207
  const refProps = getRefProps(type);
198
208
  if (refProps) {
209
+ const isCreateTarget = !createTypename || refProps.typename === createTypename;
199
210
  return (
200
211
  <RefField
201
212
  {...fieldProps}
202
213
  {...refProps}
203
- createOptionLabel={createOptionLabel}
204
- createOptionIcon={createOptionIcon}
205
- createInitialValuePath={createInitialValuePath}
214
+ createOptionLabel={isCreateTarget ? createOptionLabel : undefined}
215
+ createOptionIcon={isCreateTarget ? createOptionIcon : undefined}
216
+ createInitialValuePath={isCreateTarget ? createInitialValuePath : undefined}
217
+ createFieldMap={isCreateTarget ? createFieldMap : undefined}
206
218
  db={db}
207
- schemaHook={schemaHook}
208
- getOptions={getRefOptions}
219
+ useSchema={schemaHook}
220
+ getOptions={getOptions}
209
221
  onCreate={onCreate}
210
222
  />
211
223
  );
@@ -237,8 +249,8 @@ export const FormField = (props: FormFieldProps) => {
237
249
  createOptionIcon={createOptionIcon}
238
250
  createInitialValuePath={createInitialValuePath}
239
251
  db={db}
240
- schemaHook={schemaHook}
241
- getOptions={getRefOptions}
252
+ useSchema={schemaHook}
253
+ getOptions={getOptions}
242
254
  onCreate={onCreate}
243
255
  />
244
256
  );
@@ -285,7 +297,7 @@ const getFormField = ({ type, format }: FormFieldComponentProps): FormFieldCompo
285
297
  }
286
298
  };
287
299
 
288
- const getOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
300
+ const getSelectOptions = (ast: SchemaAST.AST): Format.Options[] | undefined => {
289
301
  if (isLiteralUnion(ast)) {
290
302
  return ast.types.map((type) => type.literal).filter((v): v is string | number => v !== null);
291
303
  }
@@ -13,6 +13,7 @@ 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
19
  import { inputTextLabel } from '@dxos/ui-theme';
@@ -31,7 +32,6 @@ export type FormFieldStateProps<T = any> = {
31
32
 
32
33
  /**
33
34
  * Presentation mode.
34
- *
35
35
  * - full: Show label, control, and status.
36
36
  * - compact: Show label and control.
37
37
  * - inline: Control only.
@@ -51,6 +51,8 @@ export type FormFieldComponentProps<T = any> = {
51
51
  placeholder?: string;
52
52
  autoFocus?: boolean;
53
53
  layout?: Presentation;
54
+ /** Database the form is editing against; populated for fields whose value is an ECHO object/Ref. */
55
+ db?: Database.Database;
54
56
  } & FormFieldStateProps<T>;
55
57
 
56
58
  /**
@@ -78,7 +80,7 @@ export type FormFieldLabelProps = {
78
80
  export const FormFieldLabel = ({ label, error, readonly, asChild }: FormFieldLabelProps) => {
79
81
  const Label = readonly || asChild ? 'span' : Input.Label;
80
82
  return (
81
- <div role='none' className='flex items-center justify-between'>
83
+ <div className='flex items-center justify-between'>
82
84
  <Label className={inputTextLabel}>{label}</Label>
83
85
  {error && (
84
86
  <Tooltip.Trigger asChild content={error} side='bottom'>
@@ -114,7 +116,7 @@ export const FormFieldWrapper = <T,>(props: FormFieldWrapperProps<T>) => {
114
116
  const str = String(value ?? '');
115
117
 
116
118
  return (
117
- <div role='none' className='contents'>
119
+ <div className='contents'>
118
120
  <Input.Root validationValence={status}>
119
121
  {layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
120
122
  {layout === 'static' ? <p>{str}</p> : children ? children({ value }) : null}
@@ -157,7 +159,7 @@ export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProp
157
159
  if (this.state.error) {
158
160
  return (
159
161
  <div className='flex gap-2 border border-rose-fill font-mono text-sm'>
160
- <span className='bg-rose-fill text-surface-text px-1 font-thin'>ERROR</span>
162
+ <span className='bg-rose-fill text-base-foreground px-1 font-thin'>ERROR</span>
161
163
  {String(this.props.path?.join('.'))}
162
164
  </div>
163
165
  );
@@ -2,16 +2,13 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { forwardRef, useMemo } from 'react';
5
+ import React, { useMemo } from 'react';
6
6
 
7
7
  import { type AnyProperties } from '@dxos/echo/internal';
8
8
  import { type SchemaProperty } from '@dxos/effect';
9
- import { mx } from '@dxos/ui-theme';
10
- import { isTruthy } from '@dxos/util';
11
9
 
12
10
  import { type FormHandlerProps } from '../../hooks';
13
11
  import { getFormProperties } from '../../util';
14
-
15
12
  import { useFormValues } from './Form';
16
13
  import { FormField, type FormFieldProps } from './FormField';
17
14
  import { FormFieldErrorBoundary, FormFieldLabel } from './FormFieldComponent';
@@ -20,8 +17,8 @@ const FORM_FIELDSET_NAME = 'Form.FieldSet';
20
17
 
21
18
  export type FormFieldSetProps<T extends AnyProperties> = {
22
19
  label?: string;
23
- exclude?: (props: SchemaProperty[]) => SchemaProperty[];
24
20
  sort?: string[];
21
+ exclude?: (props: SchemaProperty[]) => SchemaProperty[];
25
22
  } & Pick<FormHandlerProps<T>, 'schema'> &
26
23
  Pick<
27
24
  FormFieldProps,
@@ -32,11 +29,13 @@ export type FormFieldSetProps<T extends AnyProperties> = {
32
29
  | 'projection'
33
30
  | 'fieldMap'
34
31
  | 'fieldProvider'
32
+ | 'createTypename'
35
33
  | 'createOptionLabel'
36
34
  | 'createOptionIcon'
37
35
  | 'createInitialValuePath'
36
+ | 'createFieldMap'
38
37
  | 'db'
39
- | 'schemaHook'
38
+ | 'useSchema'
40
39
  | 'getOptions'
41
40
  | 'onCreate'
42
41
  >;
@@ -44,80 +43,102 @@ export type FormFieldSetProps<T extends AnyProperties> = {
44
43
  /**
45
44
  * Renders a set of form fields derived from a schema object.
46
45
  */
47
- export const FormFieldSet = forwardRef<HTMLDivElement, FormFieldSetProps<any>>(
48
- ({ label, schema, readonly, path, exclude, sort, projection, layout, ...props }, forwardRef) => {
49
- const values = useFormValues(FORM_FIELDSET_NAME, path);
50
-
51
- // TODO(burdon): Updates on every value change.
52
- // Remove values dep if can remove from getSchemaProperties.
53
- const properties = useMemo(() => {
54
- if (!schema) {
55
- return [];
56
- }
46
+ export const FormFieldSet = ({
47
+ label,
48
+ schema,
49
+ readonly,
50
+ path,
51
+ sort,
52
+ exclude,
53
+ projection,
54
+ layout,
55
+ ...props
56
+ }: FormFieldSetProps<any>) => {
57
+ const values = useFormValues(FORM_FIELDSET_NAME, path);
58
+ const properties = useFormFieldSetProperties({ schema, values, exclude, sort, projection });
59
+ if ((readonly || layout === 'static') && values == null) {
60
+ return null;
61
+ }
62
+
63
+ return (
64
+ <>
65
+ {layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
66
+ {properties.map((property) => {
67
+ const name = property.name.toString();
68
+ return (
69
+ <FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
70
+ <FormField
71
+ type={property.type}
72
+ name={name}
73
+ path={[...(path ?? []), name]}
74
+ readonly={readonly}
75
+ layout={layout}
76
+ projection={projection}
77
+ {...props}
78
+ />
79
+ </FormFieldErrorBoundary>
80
+ );
81
+ })}
82
+ </>
83
+ );
84
+ };
57
85
 
58
- // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
59
- const props = getFormProperties(schema.ast);
60
-
61
- // Use projection-based field management when view and projection are available.
62
- if (projection) {
63
- const fieldProjections = projection.getFieldProjections();
64
- const hiddenProperties = new Set(projection.getHiddenProperties());
65
-
66
- // Filter properties to only include visible ones and order by projection.
67
- const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name.toString()));
68
- const orderedProps: SchemaProperty[] = [];
69
-
70
- // Add properties in projection field order.
71
- for (const fieldProjection of fieldProjections) {
72
- const fieldPath = String(fieldProjection.field.path);
73
- const prop = visibleProps.find((p) => p.name === fieldPath);
74
- if (prop) {
75
- orderedProps.push(prop);
76
- }
77
- }
78
-
79
- // Add any remaining properties not in projection.
80
- const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
81
- const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
82
- orderedProps.push(...remainingProps);
83
- return orderedProps;
84
- }
86
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
85
87
 
86
- // Fallback to legacy filter/sort behavior.
87
- const filtered = exclude ? exclude(props) : props;
88
- return sort
89
- ? filtered.sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
90
- : filtered;
91
- }, [schema, values, exclude, sort, projection?.fields]);
88
+ type UseFormFieldSetPropertiesParams = Pick<FormFieldSetProps<any>, 'schema' | 'exclude' | 'projection' | 'sort'> & {
89
+ values: AnyProperties | undefined;
90
+ sort?: string[];
91
+ };
92
92
 
93
- if ((readonly || layout === 'static') && values == null) {
94
- return null;
93
+ /**
94
+ * Resolves ordered schema properties for a field set (projection order, exclude, or sort).
95
+ */
96
+ const useFormFieldSetProperties = ({
97
+ schema,
98
+ exclude,
99
+ projection,
100
+ values,
101
+ sort,
102
+ }: UseFormFieldSetPropertiesParams): SchemaProperty[] => {
103
+ // TODO(burdon): Updates on every value change.
104
+ // Remove values dep if can remove from getSchemaProperties.
105
+ return useMemo(() => {
106
+ if (!schema) {
107
+ return [];
95
108
  }
96
109
 
97
- return (
98
- <div role='form' className={mx('w-full', layout === 'inline' && 'flex flex-col gap-2')} ref={forwardRef}>
99
- {layout !== 'inline' && label && <FormFieldLabel label={label} asChild />}
100
- {properties
101
- .map((property) => {
102
- const name = property.name.toString();
103
- return (
104
- <FormFieldErrorBoundary key={name} path={[...(path ?? []), name]}>
105
- <FormField
106
- type={property.type}
107
- name={name}
108
- path={[...(path ?? []), name]}
109
- readonly={readonly}
110
- layout={layout}
111
- projection={projection}
112
- {...props}
113
- />
114
- </FormFieldErrorBoundary>
115
- );
116
- })
117
- .filter(isTruthy)}
118
- </div>
119
- );
120
- },
121
- );
110
+ // TODO(wittjosiah): Reconcile FormInputAnnotation with projection hidden properties & exclude function.
111
+ const schemaProps = getFormProperties(schema.ast);
112
+ const filteredProps = exclude ? exclude(schemaProps) : schemaProps;
113
+
114
+ // Use projection-based field management when view and projection are available.
115
+ if (projection) {
116
+ const fieldProjections = projection.getFieldProjections();
117
+ const hiddenProperties = new Set(projection.getHiddenProperties());
118
+
119
+ // Filter properties to only include visible ones and order by projection.
120
+ const visibleProps = filteredProps.filter((prop) => !hiddenProperties.has(prop.name.toString()));
121
+ const orderedProps: SchemaProperty[] = [];
122
+
123
+ // Add properties in projection field order.
124
+ for (const fieldProjection of fieldProjections) {
125
+ const fieldPath = String(fieldProjection.field.path);
126
+ const prop = visibleProps.find((prop) => prop.name === fieldPath);
127
+ if (prop) {
128
+ orderedProps.push(prop);
129
+ }
130
+ }
131
+
132
+ // Add any remaining properties not in projection.
133
+ const projectionPaths = new Set(fieldProjections.map((projection) => String(projection.field.path)));
134
+ const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name.toString()));
135
+ orderedProps.push(...remainingProps);
136
+ return orderedProps;
137
+ }
122
138
 
123
- FormFieldSet.displayName = FORM_FIELDSET_NAME;
139
+ // Fallback to legacy filter/sort behavior.
140
+ return sort
141
+ ? [...filteredProps].sort(({ name: a }, { name: b }) => sort.indexOf(a.toString()) - sort.indexOf(b.toString()))
142
+ : filteredProps;
143
+ }, [schema, values, exclude, sort, projection]);
144
+ };
@@ -4,12 +4,13 @@
4
4
 
5
5
  import * as Option from 'effect/Option';
6
6
  import * as SchemaAST from 'effect/SchemaAST';
7
- import React, { Fragment, useCallback } from 'react';
7
+ import React, { useCallback } from 'react';
8
8
 
9
9
  import { findNode, getArrayElementType, getDiscriminatedType, isDiscriminatedUnion, isNestedType } from '@dxos/effect';
10
10
  import { IconButton, useTranslation } from '@dxos/react-ui';
11
11
 
12
- import { translationKey } from '../../../translations';
12
+ import { translationKey } from '#translations';
13
+
13
14
  import { getFormProperties } from '../../../util';
14
15
  import { useFormValues } from '../Form';
15
16
  import { FormField, type FormFieldProps } from '../FormField';
@@ -77,10 +78,10 @@ export const ArrayField = ({
77
78
  <FormFieldLabel readonly={readonly} label={label} asChild />
78
79
  )}
79
80
 
80
- <div role='none' className='flex flex-col gap-2'>
81
+ <div className='flex flex-col gap-form-gap'>
81
82
  {values?.map((_, index) => {
82
83
  return (
83
- <div role='none' key={index} className='grid grid-cols-[1fr_min-content] gap-2 last:mb-3'>
84
+ <div key={index} className='grid grid-cols-[1fr_min-content] gap-form-gap last:mb-form-gap items-center'>
84
85
  <FormField
85
86
  autoFocus={index === values.length - 1}
86
87
  type={elementType}
@@ -91,13 +92,13 @@ export const ArrayField = ({
91
92
  />
92
93
 
93
94
  {!readonly && layout !== 'static' && (
94
- <div role='none' className='flex flex-col h-full justify-end'>
95
+ <div className='flex flex-col h-full justify-end'>
95
96
  {/* NOTE: Aligns with center of last field if multi-field object. */}
96
- <div role='none' className='flex items-center h-[2rem]'>
97
+ <div className='flex items-center h-[2rem]'>
97
98
  <IconButton
98
99
  icon='ph--x--regular'
99
100
  iconOnly
100
- label={t('button remove')}
101
+ label={t('remove.button')}
101
102
  onClick={() => handleDelete(index)}
102
103
  classNames='self-center'
103
104
  />
@@ -111,7 +112,12 @@ export const ArrayField = ({
111
112
 
112
113
  {/* TODO(burdon): Get label from schema. */}
113
114
  {!readonly && layout !== 'static' && (
114
- <IconButton classNames='flex w-full' icon='ph--plus--regular' label={t('add field')} onClick={handleAdd} />
115
+ <IconButton
116
+ classNames='flex w-full'
117
+ icon='ph--plus--regular'
118
+ label={t('add-item.button')}
119
+ onClick={handleAdd}
120
+ />
115
121
  )}
116
122
  </>
117
123
  );
@@ -8,7 +8,8 @@ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
9
9
  import { safeParseFloat } from '@dxos/util';
10
10
 
11
- import { translationKey } from '../../../translations';
11
+ import { translationKey } from '#translations';
12
+
12
13
  import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
13
14
 
14
15
  export const GeoPointField = ({
@@ -62,17 +63,17 @@ export const GeoPointField = ({
62
63
  {layout === 'static' ? (
63
64
  <LatLng {...value} />
64
65
  ) : (
65
- <div role='none' className='grid grid-cols-2 gap-2'>
66
+ <div className='grid grid-cols-2 gap-form-gap'>
66
67
  <div>
67
68
  <Input.Root>
68
- {layout !== 'inline' && <Input.Label>{t('latitude label')}</Input.Label>}
69
+ {layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
69
70
  <Input.TextInput
70
71
  type='number'
71
72
  step='0.00001'
72
73
  min='-90'
73
74
  max='90'
74
75
  disabled={!!readonly}
75
- placeholder={t('latitude placeholder')}
76
+ placeholder={t('latitude.placeholder')}
76
77
  value={latitudeText ?? ''}
77
78
  onChange={handleChange('latitude', setLatitudeText)}
78
79
  onBlur={onBlur}
@@ -81,14 +82,14 @@ export const GeoPointField = ({
81
82
  </div>
82
83
  <div>
83
84
  <Input.Root>
84
- {layout !== 'inline' && <Input.Label>{t('longitude label')}</Input.Label>}
85
+ {layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
85
86
  <Input.TextInput
86
87
  type='number'
87
88
  step='0.00001'
88
89
  min='-180'
89
90
  max='180'
90
91
  disabled={!!readonly}
91
- placeholder={t('longitude placeholder')}
92
+ placeholder={t('longitude.placeholder')}
92
93
  value={longitudeText ?? ''}
93
94
  onChange={handleChange('longitude', setLongitudeText)}
94
95
  onBlur={onBlur}
@@ -106,7 +107,7 @@ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
106
107
  const lngHem = longitude >= 0 ? 'E' : 'W';
107
108
 
108
109
  return (
109
- <span className='inline-flex items-center gap-1'>
110
+ <span className='inline-flex items-center gap-form-gap'>
110
111
  <span>
111
112
  <span>{Math.abs(latitude).toFixed(5)}</span>
112
113
  <span className='text-subdued'>°{latHem}</span>