@dxos/react-ui-form 0.8.4-main.b97322e → 0.8.4-main.dedc0f3

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 (94) hide show
  1. package/dist/lib/browser/index.mjs +601 -528
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +601 -528
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +79 -4
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +74 -4
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/ArrayField.d.ts +2 -2
  16. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts +5 -5
  18. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.d.ts +5 -4
  20. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/Form.stories.d.ts +126 -30
  22. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContent.d.ts +16 -10
  25. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormContext.d.ts +1 -1
  27. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/Input.d.ts +4 -1
  29. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/RefField.d.ts +10 -4
  31. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
  33. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  34. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
  35. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +74 -3
  37. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
  39. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +3 -2
  41. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +76 -5
  43. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/index.d.ts +0 -1
  45. package/dist/types/src/components/index.d.ts.map +1 -1
  46. package/dist/types/src/components/testing.d.ts +6 -3
  47. package/dist/types/src/components/testing.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useForm.d.ts +1 -1
  49. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  50. package/dist/types/src/translations.d.ts +9 -4
  51. package/dist/types/src/translations.d.ts.map +1 -1
  52. package/dist/types/src/util/field.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +35 -30
  55. package/src/components/ControlSection/ControlSection.stories.tsx +8 -10
  56. package/src/components/ControlSection/ControlSection.tsx +26 -12
  57. package/src/components/FieldEditor/FieldEditor.stories.tsx +8 -9
  58. package/src/components/FieldEditor/FieldEditor.test.tsx +3 -2
  59. package/src/components/FieldEditor/FieldEditor.tsx +22 -17
  60. package/src/components/Form/ArrayField.tsx +8 -8
  61. package/src/components/Form/Defaults.tsx +22 -20
  62. package/src/components/Form/Form.stories.tsx +129 -93
  63. package/src/components/Form/Form.tsx +16 -12
  64. package/src/components/Form/FormActions.tsx +2 -1
  65. package/src/components/Form/FormContent.tsx +96 -34
  66. package/src/components/Form/FormContext.tsx +5 -5
  67. package/src/components/Form/Input.tsx +4 -1
  68. package/src/components/Form/RefField.tsx +126 -40
  69. package/src/components/Form/custom/GeoPointInput.tsx +29 -21
  70. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +7 -6
  71. package/src/components/Form/custom/SelectOptionsInput.tsx +29 -30
  72. package/src/components/Form/custom/TupleInput.tsx +2 -2
  73. package/src/components/Form/factory.tsx +1 -1
  74. package/src/components/ViewEditor/ViewEditor.stories.tsx +50 -23
  75. package/src/components/ViewEditor/ViewEditor.test.tsx +7 -10
  76. package/src/components/ViewEditor/ViewEditor.tsx +151 -132
  77. package/src/components/index.ts +0 -2
  78. package/src/components/testing.tsx +15 -9
  79. package/src/hooks/useForm.ts +5 -3
  80. package/src/hooks/useRefQueryLookupHandler.ts +1 -1
  81. package/src/translations.ts +11 -5
  82. package/src/util/field.ts +8 -18
  83. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  84. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  85. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  86. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  87. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  88. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  89. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  90. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  91. package/src/components/deprecated/Form.stories.tsx +0 -62
  92. package/src/components/deprecated/Form.tsx +0 -119
  93. package/src/components/deprecated/FormInput.tsx +0 -71
  94. package/src/components/deprecated/index.ts +0 -5
@@ -9,65 +9,68 @@ import { Schema } from 'effect';
9
9
  import React, { useCallback, useState } from 'react';
10
10
 
11
11
  import { ContactType } from '@dxos/client/testing';
12
- import { type BaseObject, Expando, Format, getObjectDXN, Ref, type TypeAnnotation } from '@dxos/echo-schema';
12
+ import { type Type } from '@dxos/echo';
13
+ import { type BaseObject, Expando, Format, Ref, type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
13
14
  import { live } from '@dxos/live-object';
14
15
  import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
15
16
  import { Testing } from '@dxos/schema/testing';
16
17
  import { withLayout, withTheme } from '@dxos/storybook-utils';
17
18
 
18
- import { SelectInput } from './Defaults';
19
- import { Form, type FormProps } from './Form';
20
19
  import { translations } from '../../translations';
21
20
  import { TestLayout, TestPanel } from '../testing';
22
21
 
23
- const AddressSchema = Schema.Struct({
24
- street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
25
- city: Schema.optional(Schema.String.annotations({ title: 'City' })),
26
- zip: Schema.optional(Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ title: 'ZIP' })),
27
- location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
28
- }).annotations({ title: 'Address' });
29
-
30
- // TODO(burdon): Translations?
31
- const TestSchema = Schema.Struct({
32
- name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
33
- active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
34
- rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
35
- website: Schema.optional(Format.URL.annotations({ title: 'Website' })),
36
- address: Schema.optional(AddressSchema),
37
- }).pipe(Schema.mutable);
38
-
39
- type TestType = Schema.Schema.Type<typeof TestSchema>;
40
-
41
- type StoryProps<T extends BaseObject> = { schema: Schema.Schema<T> } & FormProps<T>;
22
+ import { SelectInput } from './Defaults';
23
+ import { Form, type FormProps } from './Form';
42
24
 
43
- const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
25
+ type StoryProps<T extends BaseObject> = {
26
+ debug?: boolean;
27
+ schema: Type.Obj.Any;
28
+ } & FormProps<T>;
29
+
30
+ const DefaultStory = <T extends BaseObject = any>({
31
+ debug,
32
+ schema,
33
+ values: initialValues,
34
+ ...props
35
+ }: StoryProps<T>) => {
44
36
  const [values, setValues] = useState(initialValues);
45
37
  const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
46
38
  setValues(values);
47
39
  }, []);
48
40
 
41
+ if (debug) {
42
+ return (
43
+ <TestLayout json={{ values, schema: schema.ast }}>
44
+ <TestPanel>
45
+ <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
46
+ </TestPanel>
47
+ </TestLayout>
48
+ );
49
+ }
50
+
49
51
  return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
50
52
  };
51
53
 
52
- const DebugStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
53
- const [values, setValues] = useState(initialValues);
54
- const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
55
- setValues(values);
54
+ const RefStory = <T extends BaseObject = any>(props: StoryProps<T>) => {
55
+ const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
56
+ switch (typeInfo.typename) {
57
+ case ContactType.typename:
58
+ return [
59
+ { dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
60
+ { dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
61
+ ];
62
+ default:
63
+ return [];
64
+ }
56
65
  }, []);
57
66
 
58
- return (
59
- <TestLayout json={{ values, schema: schema.ast.toJSON() }}>
60
- <TestPanel>
61
- <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
62
- </TestPanel>
63
- </TestLayout>
64
- );
67
+ return <DefaultStory<T> onQueryRefOptions={onQueryRefOptions} {...props} />;
65
68
  };
66
69
 
67
- const meta: Meta<StoryProps<any>> = {
70
+ const meta = {
68
71
  title: 'ui/react-ui-form/Form',
69
- component: Form<any>,
70
- render: DebugStory,
72
+ component: Form as any,
73
+ render: DefaultStory,
71
74
  decorators: [withLayout({ fullscreen: true }), withTheme],
72
75
  parameters: {
73
76
  translations,
@@ -78,17 +81,33 @@ const meta: Meta<StoryProps<any>> = {
78
81
  description: 'Readonly',
79
82
  },
80
83
  },
81
- };
84
+ } satisfies Meta<StoryProps<any>>;
82
85
 
83
86
  export default meta;
84
87
 
85
- type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
88
+ const AddressSchema = Schema.Struct({
89
+ street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
90
+ city: Schema.optional(Schema.String.annotations({ title: 'City' })),
91
+ zip: Schema.optional(Schema.String.pipe(Schema.pattern(/^\d{5}(-\d{4})?$/)).annotations({ title: 'ZIP' })),
92
+ location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
93
+ }).annotations({ title: 'Address' });
86
94
 
87
- export const Default: Story<TestType> = {
88
- render: DefaultStory,
95
+ const ContactSchema = Schema.Struct({
96
+ name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
97
+ active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
98
+ rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
99
+ website: Schema.optional(Format.URL.annotations({ title: 'Website' })),
100
+ address: Schema.optional(AddressSchema),
101
+ }).pipe(Schema.mutable);
102
+
103
+ type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
104
+
105
+ type Story = StoryObj<StoryProps<any>>;
106
+
107
+ export const Default: Story = {
89
108
  decorators: [withSurfaceVariantsLayout(), withTheme],
90
109
  args: {
91
- schema: TestSchema,
110
+ schema: ContactSchema,
92
111
  values: {
93
112
  name: 'DXOS',
94
113
  active: true,
@@ -100,8 +119,9 @@ export const Default: Story<TestType> = {
100
119
  },
101
120
  };
102
121
 
103
- export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationSchema>> = {
122
+ export const Organization: Story = {
104
123
  args: {
124
+ debug: true,
105
125
  schema: Testing.OrganizationSchema,
106
126
  values: {
107
127
  name: 'DXOS',
@@ -111,8 +131,9 @@ export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationS
111
131
  },
112
132
  };
113
133
 
114
- export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.OrganizationSchema>> = {
134
+ export const OrganizationAutoSave: Story = {
115
135
  args: {
136
+ debug: true,
116
137
  schema: Testing.OrganizationSchema,
117
138
  values: {
118
139
  name: 'DXOS',
@@ -125,15 +146,20 @@ export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.Organ
125
146
 
126
147
  // TODO(burdon): Type issue with employer reference.
127
148
  // TODO(burdon): Test table/form with compound values (e.g., address).
128
- // export const Contact: Story<Testing.ContactSchemaType> = {
129
- // args: {
130
- // // Property name is missing in type EncodedReference but required in type
131
- // schema: Testing.ContactSchema,
132
- // values: {
133
- // name: 'Bot',
134
- // },
135
- // },
136
- // };
149
+ // Property name is missing in type EncodedReference but required in type
150
+ export const Contact: Story = {
151
+ args: {
152
+ debug: true,
153
+ schema: Testing.ContactSchema,
154
+ values: {
155
+ name: 'Bot',
156
+ },
157
+ },
158
+ };
159
+
160
+ //
161
+ // Union
162
+ //
137
163
 
138
164
  const ShapeSchema = Schema.Struct({
139
165
  shape: Schema.optional(
@@ -150,10 +176,11 @@ const ShapeSchema = Schema.Struct({
150
176
  ),
151
177
  }).pipe(Schema.mutable);
152
178
 
153
- type ShapeType = Schema.Schema.Type<typeof ShapeSchema>;
179
+ type ShapeSchema = Schema.Schema.Type<typeof ShapeSchema>;
154
180
 
155
- export const DiscriminatedShape: Story<ShapeType> = {
181
+ export const DiscriminatedShape: StoryObj<StoryProps<ShapeSchema>> = {
156
182
  args: {
183
+ debug: true,
157
184
  schema: ShapeSchema,
158
185
  readonly: false,
159
186
  values: {
@@ -176,15 +203,20 @@ export const DiscriminatedShape: Story<ShapeType> = {
176
203
  },
177
204
  };
178
205
 
206
+ //
207
+ // Arrays
208
+ //
209
+
179
210
  const ArraysSchema = Schema.Struct({
180
211
  names: Schema.Array(Schema.String.pipe(Schema.nonEmptyString())),
181
212
  addresses: Schema.Array(AddressSchema),
182
213
  }).pipe(Schema.mutable);
183
214
 
184
- type ArraysType = Schema.Schema.Type<typeof ArraysSchema>;
215
+ type ArraysSchema = Schema.Schema.Type<typeof ArraysSchema>;
185
216
 
186
- export const Arrays: StoryObj<FormProps<ArraysType>> = {
217
+ export const Arrays: StoryObj<StoryProps<ArraysSchema>> = {
187
218
  args: {
219
+ debug: true,
188
220
  schema: ArraysSchema,
189
221
  readonly: false,
190
222
  values: {
@@ -194,6 +226,34 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
194
226
  },
195
227
  };
196
228
 
229
+ //
230
+ // Tuple
231
+ //
232
+
233
+ // TODO(wittjosiah): Only GeoPoint is works currently.
234
+ const TupleSchema = Schema.Struct({
235
+ tuple: Schema.Tuple(Schema.Number, Schema.String, Schema.Boolean),
236
+ geopoint: Format.GeoPoint,
237
+ }).pipe(Schema.mutable);
238
+
239
+ type TupleSchema = Schema.Schema.Type<typeof TupleSchema>;
240
+
241
+ export const Tuple: StoryObj<StoryProps<TupleSchema>> = {
242
+ args: {
243
+ debug: true,
244
+ schema: TupleSchema,
245
+ readonly: false,
246
+ values: {
247
+ tuple: [1, 'a', true],
248
+ geopoint: [-122.41941, 37.77492],
249
+ },
250
+ },
251
+ };
252
+
253
+ //
254
+ // Enum
255
+ //
256
+
197
257
  const ColorSchema = Schema.Struct({
198
258
  color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
199
259
  title: 'Color',
@@ -202,8 +262,9 @@ const ColorSchema = Schema.Struct({
202
262
 
203
263
  type ColorType = Schema.Schema.Type<typeof ColorSchema>;
204
264
 
205
- export const Enum: StoryObj<FormProps<ColorType>> = {
265
+ export const Enum: StoryObj<StoryProps<ColorType>> = {
206
266
  args: {
267
+ debug: true,
207
268
  schema: ColorSchema,
208
269
  readonly: false,
209
270
  values: {
@@ -212,6 +273,10 @@ export const Enum: StoryObj<FormProps<ColorType>> = {
212
273
  },
213
274
  };
214
275
 
276
+ //
277
+ // Refs
278
+ //
279
+
215
280
  const RefSchema = Schema.Struct({
216
281
  contact: Ref(ContactType).annotations({ title: 'Contact Reference' }),
217
282
  optionalContact: Schema.optional(Ref(ContactType).annotations({ title: 'Optional Contact Reference' })),
@@ -219,48 +284,19 @@ const RefSchema = Schema.Struct({
219
284
  unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
220
285
  });
221
286
 
287
+ type RefSchema = Schema.Schema.Type<typeof RefSchema>;
288
+
222
289
  const contact1 = live(ContactType, { identifiers: [] });
223
290
  const contact2 = live(ContactType, { identifiers: [] });
224
291
 
225
- const RefStory = ({ values: initialValues, readonly }: FormProps<any>) => {
226
- const [values, setValues] = useState(initialValues);
227
- const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
228
- setValues(values);
229
- }, []);
230
-
231
- const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
232
- switch (typeInfo.typename) {
233
- case ContactType.typename:
234
- return [
235
- { dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
236
- { dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
237
- ];
238
- default:
239
- return [];
240
- }
241
- }, []);
242
-
243
- return (
244
- <TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
245
- <TestPanel>
246
- <Form
247
- schema={RefSchema}
248
- values={values}
249
- readonly={readonly}
250
- onSave={handleSave}
251
- onQueryRefOptions={onQueryRefOptions}
252
- />
253
- </TestPanel>
254
- </TestLayout>
255
- );
256
- };
257
-
258
- export const Refs: StoryObj<FormProps<ContactType>> = {
292
+ export const Refs: StoryObj<StoryProps<RefSchema>> = {
259
293
  render: RefStory,
260
294
  args: {
295
+ debug: true,
296
+ schema: RefSchema,
261
297
  readonly: false,
262
298
  values: {
263
299
  refArray: [Ref.make(contact1), Ref.make(contact2)],
264
- } as any,
300
+ },
265
301
  },
266
302
  };
@@ -2,19 +2,21 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { useFocusFinders } from '@fluentui/react-tabster';
5
6
  import { type Schema } from 'effect';
6
7
  import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
7
8
 
8
9
  import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
9
10
  import { type ThemedClassName } from '@dxos/react-ui';
10
11
  import { cardDialogOverflow, cardSpacing } from '@dxos/react-ui-stack';
11
- import { type SchemaProperty } from '@dxos/schema';
12
+ import { type ProjectionModel, type SchemaProperty } from '@dxos/schema';
13
+
14
+ import { type FormOptions } from '../../hooks';
12
15
 
13
16
  import { FormActions, type FormOuterSpacing } from './FormActions';
14
17
  import { FormFields, type FormFieldsProps } from './FormContent';
15
18
  import { FormProvider } from './FormContext';
16
- import { type InputProps, type InputComponent } from './Input';
17
- import { type FormOptions } from '../../hooks';
19
+ import { type InputComponent, type InputProps } from './Input';
18
20
 
19
21
  export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
20
22
 
@@ -33,6 +35,7 @@ export type FormProps<T extends BaseObject> = ThemedClassName<{
33
35
  // TODO(burdon): Change to JsonPath includes/excludes.
34
36
  filter?: PropsFilter<T>;
35
37
  sort?: PropertyKey<T>[];
38
+ projection?: ProjectionModel;
36
39
  autoSave?: boolean;
37
40
  outerSpacing?: FormOuterSpacing;
38
41
  onCancel?: () => void;
@@ -48,8 +51,8 @@ export const Form = <T extends BaseObject>({
48
51
  readonly,
49
52
  autoSave,
50
53
  outerSpacing = true,
51
- onCancel,
52
54
  schema,
55
+ onCancel,
53
56
  onValuesChanged,
54
57
  onValidate,
55
58
  onSave,
@@ -57,19 +60,20 @@ export const Form = <T extends BaseObject>({
57
60
  }: FormProps<T>) => {
58
61
  const formRef = useRef<HTMLDivElement>(null);
59
62
 
60
- // TODO(burdon): Rename.
61
- const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
62
-
63
- // Focus the first input element within this form.
63
+ // Focus the first focusable element within this form.
64
+ const { findFirstFocusable } = useFocusFinders();
64
65
  useEffect(() => {
65
66
  if (autoFocus && formRef.current) {
66
- const input = formRef.current.querySelector('input');
67
- if (input) {
68
- input.focus();
67
+ const firstFocusable = findFirstFocusable(formRef.current);
68
+ if (firstFocusable) {
69
+ firstFocusable.focus();
69
70
  }
70
71
  }
71
72
  }, [autoFocus]);
72
73
 
74
+ // TODO(burdon): Name?
75
+ const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
76
+
73
77
  return (
74
78
  <FormProvider
75
79
  formRef={formRef}
@@ -99,7 +103,7 @@ export const Form = <T extends BaseObject>({
99
103
  readonly={readonly}
100
104
  schema={schema}
101
105
  />
102
- {(onCancel || onSave) && !autoSave && (
106
+ {(onCancel || onSave) && !autoSave && !readonly && (
103
107
  <FormActions readonly={readonly} onCancel={onCancel} outerSpacing={outerSpacing} />
104
108
  )}
105
109
  </div>
@@ -8,9 +8,10 @@ import { IconButton, useTranslation } from '@dxos/react-ui';
8
8
  import { cardSpacing } from '@dxos/react-ui-stack';
9
9
  import { mx } from '@dxos/react-ui-theme';
10
10
 
11
- import { useFormContext } from './FormContext';
12
11
  import { translationKey } from '../../translations';
13
12
 
13
+ import { useFormContext } from './FormContext';
14
+
14
15
  export type FormOuterSpacing = boolean | 'blockStart-0' | 'scroll-fields';
15
16
 
16
17
  export type FormActionsProps = {
@@ -9,36 +9,43 @@ import React, { forwardRef, useMemo } from 'react';
9
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
11
11
  import { mx } from '@dxos/react-ui-theme';
12
- import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
12
+ import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
13
13
  import { isNotFalsy } from '@dxos/util';
14
14
 
15
+ import { type QueryRefOptions } from '../../hooks';
16
+ import { getRefProps } from '../../util';
17
+
15
18
  import { ArrayField } from './ArrayField';
16
19
  import { SelectInput } from './Defaults';
20
+ import { getInputComponent } from './factory';
17
21
  import { type ComponentLookup } from './Form';
18
- import { useInputProps, useFormValues } from './FormContext';
22
+ import { useFormValues, useInputProps } from './FormContext';
19
23
  import { type InputComponent } from './Input';
20
- import { RefField } from './RefField';
21
- import { getInputComponent } from './factory';
22
- import { type QueryRefOptions } from '../../hooks';
23
- import { getRefProps } from '../../util';
24
+ import { RefField, type RefFieldProps } from './RefField';
24
25
 
25
26
  export type FormFieldProps = {
26
27
  property: SchemaProperty<any>;
27
28
  path?: (string | number)[];
28
- readonly?: boolean;
29
29
  /** Used to indicate if input should be presented inline (e.g. for array items). */
30
30
  inline?: boolean;
31
- onQueryRefOptions?: QueryRefOptions;
31
+ projection?: ProjectionModel;
32
32
  lookupComponent?: ComponentLookup;
33
33
  Custom?: Partial<Record<string, InputComponent>>;
34
- };
34
+ } & Pick<
35
+ RefFieldProps,
36
+ 'readonly' | 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'
37
+ >;
35
38
 
36
39
  export const FormField = ({
37
40
  property,
38
41
  path,
39
42
  readonly,
40
43
  inline,
44
+ projection,
41
45
  onQueryRefOptions,
46
+ createOptionLabel,
47
+ createOptionIcon,
48
+ onCreateFromQuery,
42
49
  lookupComponent,
43
50
  Custom,
44
51
  }: FormFieldProps) => {
@@ -62,7 +69,7 @@ export const FormField = ({
62
69
  type,
63
70
  format,
64
71
  label,
65
- disabled: readonly,
72
+ readonly,
66
73
  placeholder,
67
74
  ...inputProps,
68
75
  },
@@ -82,7 +89,7 @@ export const FormField = ({
82
89
  label={label}
83
90
  inputOnly={inline}
84
91
  placeholder={placeholder}
85
- disabled={readonly}
92
+ readonly={readonly}
86
93
  {...inputProps}
87
94
  />
88
95
  );
@@ -102,9 +109,12 @@ export const FormField = ({
102
109
  format={format}
103
110
  label={label}
104
111
  placeholder={placeholder}
105
- disabled={readonly}
112
+ readonly={readonly}
106
113
  inputOnly={inline}
107
114
  onQueryRefOptions={onQueryRefOptions}
115
+ createOptionLabel={createOptionLabel}
116
+ createOptionIcon={createOptionIcon}
117
+ onCreateFromQuery={onCreateFromQuery}
108
118
  {...inputProps}
109
119
  />
110
120
  );
@@ -131,7 +141,7 @@ export const FormField = ({
131
141
  label={label}
132
142
  inputOnly={inline}
133
143
  placeholder={placeholder}
134
- disabled={readonly}
144
+ readonly={readonly}
135
145
  {...inputProps}
136
146
  />
137
147
  );
@@ -146,7 +156,7 @@ export const FormField = ({
146
156
  <SelectInput
147
157
  type={type}
148
158
  format={format}
149
- disabled={readonly}
159
+ readonly={readonly}
150
160
  inputOnly={inline}
151
161
  label={label}
152
162
  options={options.map((option) => ({ value: option, label: String(option) }))}
@@ -174,7 +184,11 @@ export const FormField = ({
174
184
  schema={Schema.make(typeLiteral)}
175
185
  path={path}
176
186
  readonly={readonly}
187
+ projection={projection}
177
188
  onQueryRefOptions={onQueryRefOptions}
189
+ createOptionLabel={createOptionLabel}
190
+ createOptionIcon={createOptionIcon}
191
+ onCreateFromQuery={onCreateFromQuery}
178
192
  Custom={Custom}
179
193
  lookupComponent={lookupComponent}
180
194
  />
@@ -186,36 +200,82 @@ export const FormField = ({
186
200
  return null;
187
201
  };
188
202
 
189
- export type FormFieldsProps = ThemedClassName<{
190
- testId?: string;
191
- readonly?: boolean;
192
- schema: Schema.Schema.All;
193
- /**
194
- * Path to the current object from the root. Used with nested forms.
195
- */
196
- path?: (string | number)[];
197
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
198
- sort?: string[];
199
- lookupComponent?: ComponentLookup;
200
- /**
201
- * Map of custom renderers for specific properties.
202
- * Prefer lookupComponent for plugin specific input surfaces.
203
- */
204
- Custom?: Partial<Record<string, InputComponent>>;
205
- onQueryRefOptions?: QueryRefOptions;
206
- }>;
203
+ export type FormFieldsProps = ThemedClassName<
204
+ {
205
+ testId?: string;
206
+ schema: Schema.Schema.All;
207
+ /**
208
+ * Path to the current object from the root. Used with nested forms.
209
+ */
210
+ path?: (string | number)[];
211
+ filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
212
+ sort?: string[];
213
+ /**
214
+ * Optional projection for projection-based field management
215
+ */
216
+ projection?: ProjectionModel;
217
+ lookupComponent?: ComponentLookup;
218
+ /**
219
+ * Map of custom renderers for specific properties.
220
+ * Prefer lookupComponent for plugin specific input surfaces.
221
+ */
222
+ Custom?: Partial<Record<string, InputComponent>>;
223
+ onQueryRefOptions?: QueryRefOptions;
224
+ } & Pick<FormFieldProps, 'readonly'> &
225
+ Pick<RefFieldProps, 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'>
226
+ >;
207
227
 
208
228
  export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
209
229
  (
210
- { classNames, testId, schema, path, filter, sort, readonly, lookupComponent, Custom, onQueryRefOptions },
230
+ {
231
+ classNames,
232
+ schema,
233
+ path,
234
+ filter,
235
+ sort,
236
+ projection,
237
+ readonly,
238
+ lookupComponent,
239
+ Custom,
240
+ onQueryRefOptions,
241
+ ...props
242
+ },
211
243
  forwardRef,
212
244
  ) => {
213
245
  const values = useFormValues(path);
214
246
  const properties = useMemo(() => {
215
247
  const props = getSchemaProperties(schema.ast, values);
248
+
249
+ // Use projection-based field management when view and projection are available
250
+ if (projection) {
251
+ const fieldProjections = projection.getFieldProjections();
252
+ const hiddenProperties = new Set(projection.getHiddenProperties());
253
+
254
+ // Filter properties to only include visible ones and order by projection
255
+ const visibleProps = props.filter((prop) => !hiddenProperties.has(prop.name));
256
+ const orderedProps: SchemaProperty<any>[] = [];
257
+
258
+ // Add properties in projection field order
259
+ for (const fieldProjection of fieldProjections) {
260
+ const fieldPath = String(fieldProjection.field.path);
261
+ const prop = visibleProps.find((p) => p.name === fieldPath);
262
+ if (prop) {
263
+ orderedProps.push(prop);
264
+ }
265
+ }
266
+
267
+ // Add any remaining properties not in projection
268
+ const projectionPaths = new Set(fieldProjections.map((fp) => String(fp.field.path)));
269
+ const remainingProps = visibleProps.filter((prop) => !projectionPaths.has(prop.name));
270
+ orderedProps.push(...remainingProps);
271
+
272
+ return orderedProps;
273
+ }
274
+
275
+ // Fallback to legacy filter/sort behavior
216
276
  const filtered = filter ? filter(props) : props;
217
277
  return sort ? filtered.sort((a, b) => sort.indexOf(a.name) - sort.indexOf(b.name)) : filtered;
218
- }, [schema, values, filter, sort]);
278
+ }, [schema, values, filter, sort, projection?.fields]);
219
279
 
220
280
  return (
221
281
  <div role='form' className={mx('is-full', classNames)} ref={forwardRef}>
@@ -227,9 +287,11 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
227
287
  property={property}
228
288
  path={[...(path ?? []), property.name]}
229
289
  readonly={readonly}
290
+ projection={projection}
230
291
  onQueryRefOptions={onQueryRefOptions}
231
292
  lookupComponent={lookupComponent}
232
293
  Custom={Custom}
294
+ {...props}
233
295
  />
234
296
  );
235
297
  })
@@ -3,19 +3,19 @@
3
3
  //
4
4
 
5
5
  import React, {
6
- createContext,
6
+ type FocusEvent,
7
+ type PropsWithChildren,
7
8
  type RefObject,
9
+ createContext,
10
+ useCallback,
8
11
  useContext,
9
12
  useEffect,
10
13
  useMemo,
11
- type FocusEvent,
12
- type PropsWithChildren,
13
- useCallback,
14
14
  } from 'react';
15
15
 
16
16
  import { raise } from '@dxos/debug';
17
17
  import { type BaseObject, getValue } from '@dxos/echo-schema';
18
- import { createJsonPath, type SimpleType } from '@dxos/effect';
18
+ import { type SimpleType, createJsonPath } from '@dxos/effect';
19
19
 
20
20
  import { type FormHandler, type FormOptions, useForm } from '../../hooks';
21
21