@dxos/react-ui-form 0.8.4-main.fd6878d → 0.8.4-main.fffef41

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 (115) hide show
  1. package/dist/lib/browser/index.mjs +806 -572
  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 +806 -572
  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 +80 -5
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +1 -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 +75 -5
  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 +6 -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 +128 -32
  22. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/FormContent.d.ts +16 -10
  24. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  26. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/Input.d.ts +5 -2
  28. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/RefField.d.ts +14 -4
  30. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +1 -1
  32. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  33. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +1 -1
  34. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +75 -4
  35. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/TupleInput.d.ts +1 -1
  37. package/dist/types/src/components/Form/factory.d.ts +1 -1
  38. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  39. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
  40. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  41. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +81 -0
  42. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  43. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  44. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  45. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +20 -6
  46. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  47. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +79 -8
  48. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/index.d.ts +1 -1
  50. package/dist/types/src/components/index.d.ts.map +1 -1
  51. package/dist/types/src/components/testing.d.ts +6 -3
  52. package/dist/types/src/components/testing.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useForm.d.ts +2 -2
  54. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useQueryRefOptions.d.ts +9 -3
  56. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  58. package/dist/types/src/translations.d.ts +11 -7
  59. package/dist/types/src/translations.d.ts.map +1 -1
  60. package/dist/types/src/util/field.d.ts +1 -1
  61. package/dist/types/src/util/field.d.ts.map +1 -1
  62. package/dist/types/src/util/formatting.d.ts +1 -1
  63. package/dist/types/src/util/formatting.d.ts.map +1 -1
  64. package/dist/types/src/util/refs.d.ts +1 -1
  65. package/dist/types/src/util/refs.d.ts.map +1 -1
  66. package/dist/types/src/util/schema.d.ts +1 -1
  67. package/dist/types/src/util/schema.d.ts.map +1 -1
  68. package/dist/types/tsconfig.tsbuildinfo +1 -1
  69. package/package.json +41 -36
  70. package/src/components/ControlSection/ControlSection.stories.tsx +10 -14
  71. package/src/components/ControlSection/ControlSection.tsx +26 -12
  72. package/src/components/FieldEditor/FieldEditor.stories.tsx +10 -11
  73. package/src/components/FieldEditor/FieldEditor.tsx +12 -14
  74. package/src/components/Form/ArrayField.tsx +6 -6
  75. package/src/components/Form/Defaults.tsx +21 -20
  76. package/src/components/Form/Form.stories.tsx +132 -97
  77. package/src/components/Form/Form.tsx +27 -14
  78. package/src/components/Form/FormActions.tsx +2 -2
  79. package/src/components/Form/FormContent.tsx +166 -45
  80. package/src/components/Form/FormContext.tsx +3 -3
  81. package/src/components/Form/Input.tsx +5 -2
  82. package/src/components/Form/RefField.tsx +107 -38
  83. package/src/components/Form/custom/GeoPointInput.tsx +28 -20
  84. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +9 -10
  85. package/src/components/Form/custom/SelectOptionsInput.tsx +10 -10
  86. package/src/components/Form/custom/TupleInput.tsx +2 -2
  87. package/src/components/Form/factory.tsx +1 -1
  88. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
  89. package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
  90. package/src/components/ObjectPicker/index.ts +5 -0
  91. package/src/components/ViewEditor/ViewEditor.stories.tsx +123 -49
  92. package/src/components/ViewEditor/ViewEditor.test.tsx +8 -11
  93. package/src/components/ViewEditor/ViewEditor.tsx +281 -233
  94. package/src/components/index.ts +1 -2
  95. package/src/components/testing.tsx +15 -9
  96. package/src/hooks/useForm.ts +7 -4
  97. package/src/hooks/useQueryRefOptions.ts +15 -6
  98. package/src/hooks/useRefQueryLookupHandler.ts +3 -3
  99. package/src/translations.ts +13 -8
  100. package/src/util/field.ts +9 -19
  101. package/src/util/formatting.ts +1 -1
  102. package/src/util/refs.ts +7 -7
  103. package/src/util/schema.ts +2 -4
  104. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  105. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  106. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  107. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  108. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  109. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  110. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  111. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  112. package/src/components/deprecated/Form.stories.tsx +0 -63
  113. package/src/components/deprecated/Form.tsx +0 -119
  114. package/src/components/deprecated/FormInput.tsx +0 -71
  115. package/src/components/deprecated/index.ts +0 -5
@@ -2,18 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
9
7
  import React, { useCallback, useState } from 'react';
10
8
 
11
9
  import { ContactType } from '@dxos/client/testing';
12
- import { type BaseObject, Expando, Format, Ref, type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
13
- import { live } from '@dxos/live-object';
14
- import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
10
+ import { type Type } from '@dxos/echo';
11
+ import { type BaseObject, Expando, Format, Ref, type TypeAnnotation, getObjectDXN } from '@dxos/echo/internal';
12
+ import { live } from '@dxos/echo/internal';
13
+ import { Tooltip } from '@dxos/react-ui';
14
+ import { withLayoutVariants, withTheme } from '@dxos/react-ui/testing';
15
15
  import { Testing } from '@dxos/schema/testing';
16
- import { withLayout, withTheme } from '@dxos/storybook-utils';
17
16
 
18
17
  import { translations } from '../../translations';
19
18
  import { TestLayout, TestPanel } from '../testing';
@@ -21,6 +20,53 @@ import { TestLayout, TestPanel } from '../testing';
21
20
  import { SelectInput } from './Defaults';
22
21
  import { Form, type FormProps } from './Form';
23
22
 
23
+ type StoryProps<T extends BaseObject> = {
24
+ debug?: boolean;
25
+ schema: Type.Obj.Any;
26
+ } & FormProps<T>;
27
+
28
+ const DefaultStory = <T extends BaseObject = any>({
29
+ debug,
30
+ schema,
31
+ values: initialValues,
32
+ ...props
33
+ }: StoryProps<T>) => {
34
+ const [values, setValues] = useState(initialValues);
35
+ const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
36
+ setValues(values);
37
+ }, []);
38
+
39
+ if (debug) {
40
+ return (
41
+ <Tooltip.Provider>
42
+ <TestLayout json={{ values, schema: schema.ast }}>
43
+ <TestPanel>
44
+ <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
45
+ </TestPanel>
46
+ </TestLayout>
47
+ </Tooltip.Provider>
48
+ );
49
+ }
50
+
51
+ return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
52
+ };
53
+
54
+ const RefStory = <T extends BaseObject = any>(props: StoryProps<T>) => {
55
+ const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
56
+ switch (typeInfo.typename) {
57
+ case Testing.Person.typename:
58
+ return [
59
+ { dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
60
+ { dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
61
+ ];
62
+ default:
63
+ return [];
64
+ }
65
+ }, []);
66
+
67
+ return <DefaultStory<T> onQueryRefOptions={onQueryRefOptions} {...props} />;
68
+ };
69
+
24
70
  const AddressSchema = Schema.Struct({
25
71
  street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
26
72
  city: Schema.optional(Schema.String.annotations({ title: 'City' })),
@@ -28,8 +74,7 @@ const AddressSchema = Schema.Struct({
28
74
  location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
29
75
  }).annotations({ title: 'Address' });
30
76
 
31
- // TODO(burdon): Translations?
32
- const TestSchema = Schema.Struct({
77
+ const ContactSchema = Schema.Struct({
33
78
  name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
34
79
  active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
35
80
  rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
@@ -37,40 +82,15 @@ const TestSchema = Schema.Struct({
37
82
  address: Schema.optional(AddressSchema),
38
83
  }).pipe(Schema.mutable);
39
84
 
40
- type TestType = Schema.Schema.Type<typeof TestSchema>;
85
+ type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
41
86
 
42
- type StoryProps<T extends BaseObject> = { schema: Schema.Schema<T> } & FormProps<T>;
43
-
44
- const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
45
- const [values, setValues] = useState(initialValues);
46
- const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
47
- setValues(values);
48
- }, []);
49
-
50
- return <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />;
51
- };
52
-
53
- const DebugStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
54
- const [values, setValues] = useState(initialValues);
55
- const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
56
- setValues(values);
57
- }, []);
58
-
59
- return (
60
- <TestLayout json={{ values, schema: schema.ast.toJSON() }}>
61
- <TestPanel>
62
- <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
63
- </TestPanel>
64
- </TestLayout>
65
- );
66
- };
67
-
68
- const meta: Meta<StoryProps<any>> = {
87
+ const meta = {
69
88
  title: 'ui/react-ui-form/Form',
70
- component: Form<any>,
71
- render: DebugStory,
72
- decorators: [withLayout({ fullscreen: true }), withTheme],
89
+ component: Form as any,
90
+ render: DefaultStory,
91
+ decorators: [withTheme, withLayoutVariants()],
73
92
  parameters: {
93
+ layout: 'fullscreen',
74
94
  translations,
75
95
  },
76
96
  argTypes: {
@@ -79,17 +99,15 @@ const meta: Meta<StoryProps<any>> = {
79
99
  description: 'Readonly',
80
100
  },
81
101
  },
82
- };
102
+ } satisfies Meta<StoryProps<any>>;
83
103
 
84
104
  export default meta;
85
105
 
86
- type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
106
+ type Story = StoryObj<typeof meta>;
87
107
 
88
- export const Default: Story<TestType> = {
89
- render: DefaultStory,
90
- decorators: [withSurfaceVariantsLayout(), withTheme],
108
+ export const Default: Story = {
91
109
  args: {
92
- schema: TestSchema,
110
+ schema: ContactSchema,
93
111
  values: {
94
112
  name: 'DXOS',
95
113
  active: true,
@@ -101,8 +119,9 @@ export const Default: Story<TestType> = {
101
119
  },
102
120
  };
103
121
 
104
- export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationSchema>> = {
122
+ export const Organization: Story = {
105
123
  args: {
124
+ debug: true,
106
125
  schema: Testing.OrganizationSchema,
107
126
  values: {
108
127
  name: 'DXOS',
@@ -112,8 +131,9 @@ export const Organization: Story<Schema.Schema.Type<typeof Testing.OrganizationS
112
131
  },
113
132
  };
114
133
 
115
- export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.OrganizationSchema>> = {
134
+ export const OrganizationAutoSave: Story = {
116
135
  args: {
136
+ debug: true,
117
137
  schema: Testing.OrganizationSchema,
118
138
  values: {
119
139
  name: 'DXOS',
@@ -126,15 +146,20 @@ export const OrganizationAutoSave: Story<Schema.Schema.Type<typeof Testing.Organ
126
146
 
127
147
  // TODO(burdon): Type issue with employer reference.
128
148
  // TODO(burdon): Test table/form with compound values (e.g., address).
129
- // export const Contact: Story<Testing.ContactSchemaType> = {
130
- // args: {
131
- // // Property name is missing in type EncodedReference but required in type
132
- // schema: Testing.ContactSchema,
133
- // values: {
134
- // name: 'Bot',
135
- // },
136
- // },
137
- // };
149
+ // Property name is missing in type EncodedReference but required in type
150
+ export const Person: Story = {
151
+ args: {
152
+ debug: true,
153
+ schema: Testing.Person,
154
+ values: {
155
+ name: 'Bot',
156
+ },
157
+ },
158
+ };
159
+
160
+ //
161
+ // Union
162
+ //
138
163
 
139
164
  const ShapeSchema = Schema.Struct({
140
165
  shape: Schema.optional(
@@ -151,10 +176,11 @@ const ShapeSchema = Schema.Struct({
151
176
  ),
152
177
  }).pipe(Schema.mutable);
153
178
 
154
- type ShapeType = Schema.Schema.Type<typeof ShapeSchema>;
179
+ type ShapeSchema = Schema.Schema.Type<typeof ShapeSchema>;
155
180
 
156
- export const DiscriminatedShape: Story<ShapeType> = {
181
+ export const DiscriminatedShape: StoryObj<StoryProps<ShapeSchema>> = {
157
182
  args: {
183
+ debug: true,
158
184
  schema: ShapeSchema,
159
185
  readonly: false,
160
186
  values: {
@@ -177,15 +203,20 @@ export const DiscriminatedShape: Story<ShapeType> = {
177
203
  },
178
204
  };
179
205
 
206
+ //
207
+ // Arrays
208
+ //
209
+
180
210
  const ArraysSchema = Schema.Struct({
181
211
  names: Schema.Array(Schema.String.pipe(Schema.nonEmptyString())),
182
212
  addresses: Schema.Array(AddressSchema),
183
213
  }).pipe(Schema.mutable);
184
214
 
185
- type ArraysType = Schema.Schema.Type<typeof ArraysSchema>;
215
+ type ArraysSchema = Schema.Schema.Type<typeof ArraysSchema>;
186
216
 
187
- export const Arrays: StoryObj<FormProps<ArraysType>> = {
217
+ export const Arrays: StoryObj<StoryProps<ArraysSchema>> = {
188
218
  args: {
219
+ debug: true,
189
220
  schema: ArraysSchema,
190
221
  readonly: false,
191
222
  values: {
@@ -195,6 +226,34 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
195
226
  },
196
227
  };
197
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
+
198
257
  const ColorSchema = Schema.Struct({
199
258
  color: Schema.Union(Schema.Literal('red'), Schema.Literal('green'), Schema.Literal('blue')).annotations({
200
259
  title: 'Color',
@@ -203,8 +262,9 @@ const ColorSchema = Schema.Struct({
203
262
 
204
263
  type ColorType = Schema.Schema.Type<typeof ColorSchema>;
205
264
 
206
- export const Enum: StoryObj<FormProps<ColorType>> = {
265
+ export const Enum: StoryObj<StoryProps<ColorType>> = {
207
266
  args: {
267
+ debug: true,
208
268
  schema: ColorSchema,
209
269
  readonly: false,
210
270
  values: {
@@ -213,6 +273,10 @@ export const Enum: StoryObj<FormProps<ColorType>> = {
213
273
  },
214
274
  };
215
275
 
276
+ //
277
+ // Refs
278
+ //
279
+
216
280
  const RefSchema = Schema.Struct({
217
281
  contact: Ref(ContactType).annotations({ title: 'Contact Reference' }),
218
282
  optionalContact: Schema.optional(Ref(ContactType).annotations({ title: 'Optional Contact Reference' })),
@@ -220,48 +284,19 @@ const RefSchema = Schema.Struct({
220
284
  unknownExpando: Schema.optional(Ref(Expando).annotations({ title: 'Optional Ref to an Expando (DXN Input)' })),
221
285
  });
222
286
 
287
+ type RefSchema = Schema.Schema.Type<typeof RefSchema>;
288
+
223
289
  const contact1 = live(ContactType, { identifiers: [] });
224
290
  const contact2 = live(ContactType, { identifiers: [] });
225
291
 
226
- const RefStory = ({ values: initialValues, readonly }: FormProps<any>) => {
227
- const [values, setValues] = useState(initialValues);
228
- const handleSave = useCallback<NonNullable<FormProps<any>['onSave']>>((values) => {
229
- setValues(values);
230
- }, []);
231
-
232
- const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
233
- switch (typeInfo.typename) {
234
- case ContactType.typename:
235
- return [
236
- { dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
237
- { dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
238
- ];
239
- default:
240
- return [];
241
- }
242
- }, []);
243
-
244
- return (
245
- <TestLayout json={{ values, schema: RefSchema.ast.toJSON() }}>
246
- <TestPanel>
247
- <Form
248
- schema={RefSchema}
249
- values={values}
250
- readonly={readonly}
251
- onSave={handleSave}
252
- onQueryRefOptions={onQueryRefOptions}
253
- />
254
- </TestPanel>
255
- </TestLayout>
256
- );
257
- };
258
-
259
- export const Refs: StoryObj<FormProps<ContactType>> = {
292
+ export const Refs: StoryObj<StoryProps<RefSchema>> = {
260
293
  render: RefStory,
261
294
  args: {
295
+ debug: true,
296
+ schema: RefSchema,
262
297
  readonly: false,
263
298
  values: {
264
299
  refArray: [Ref.make(contact1), Ref.make(contact2)],
265
- } as any,
300
+ },
266
301
  },
267
302
  };
@@ -2,15 +2,16 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect';
6
- import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
5
+ import { useFocusFinders } from '@fluentui/react-tabster';
6
+ import type * as Schema from 'effect/Schema';
7
+ import React, { type ReactElement, useCallback, useEffect, useRef } from 'react';
7
8
 
8
- import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
9
+ import { type BaseObject, type PropertyKey } from '@dxos/echo/internal';
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';
12
13
 
13
- import { type FormOptions } from '../../hooks';
14
+ import { type FormHandler, type FormOptions } from '../../hooks';
14
15
 
15
16
  import { FormActions, type FormOuterSpacing } from './FormActions';
16
17
  import { FormFields, type FormFieldsProps } from './FormContent';
@@ -28,29 +29,33 @@ export type ComponentLookup = (args: {
28
29
  export type CustomInputMap = Partial<Record<string, InputComponent>>;
29
30
 
30
31
  export type FormProps<T extends BaseObject> = ThemedClassName<{
32
+ id?: string;
31
33
  values: Partial<T>;
32
34
  // TODO(burdon): Autofocus first input.
33
35
  autoFocus?: boolean;
34
36
  // TODO(burdon): Change to JsonPath includes/excludes.
35
37
  filter?: PropsFilter<T>;
36
38
  sort?: PropertyKey<T>[];
39
+ projection?: ProjectionModel;
37
40
  autoSave?: boolean;
38
41
  outerSpacing?: FormOuterSpacing;
39
42
  onCancel?: () => void;
40
43
  }> &
41
44
  Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'> &
45
+ // TODO(wittjosiah): This needs to support different ref field options per field.
42
46
  FormFieldsProps;
43
47
 
44
48
  export const Form = <T extends BaseObject>({
45
49
  classNames,
50
+ id,
46
51
  testId,
47
52
  values: initialValues,
48
53
  autoFocus,
49
54
  readonly,
50
55
  autoSave,
51
56
  outerSpacing = true,
52
- onCancel,
53
57
  schema,
58
+ onCancel,
54
59
  onValuesChanged,
55
60
  onValidate,
56
61
  onSave,
@@ -58,19 +63,27 @@ export const Form = <T extends BaseObject>({
58
63
  }: FormProps<T>) => {
59
64
  const formRef = useRef<HTMLDivElement>(null);
60
65
 
61
- // TODO(burdon): Rename.
62
- const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
63
-
64
- // Focus the first input element within this form.
66
+ // Focus the first focusable element within this form.
67
+ const { findFirstFocusable } = useFocusFinders();
65
68
  useEffect(() => {
66
69
  if (autoFocus && formRef.current) {
67
- const input = formRef.current.querySelector('input');
68
- if (input) {
69
- input.focus();
70
+ const firstFocusable = findFirstFocusable(formRef.current);
71
+ if (firstFocusable) {
72
+ firstFocusable.focus();
70
73
  }
71
74
  }
72
75
  }, [autoFocus]);
73
76
 
77
+ // TODO(burdon): Name?
78
+ const handleValid = useCallback(
79
+ async (values: any, meta: { changed: FormHandler<T>['changed'] }) => {
80
+ if (autoSave) {
81
+ await onSave?.(values, meta);
82
+ }
83
+ },
84
+ [autoSave, onSave],
85
+ );
86
+
74
87
  return (
75
88
  <FormProvider
76
89
  formRef={formRef}
@@ -82,7 +95,7 @@ export const Form = <T extends BaseObject>({
82
95
  onValid={handleValid}
83
96
  onSave={onSave}
84
97
  >
85
- <div role='none' className='contents' data-testid={testId}>
98
+ <div role='none' className='contents' {...(id && { 'data-object-id': id })} data-testid={testId}>
86
99
  <FormFields
87
100
  {...props}
88
101
  ref={formRef}
@@ -37,7 +37,7 @@ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormAct
37
37
  )}
38
38
  >
39
39
  {onCancel && !readonly && (
40
- <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('button cancel')} onClick={onCancel} />
40
+ <IconButton data-testid='cancel-button' icon='ph--x--regular' label={t('cancel button')} onClick={onCancel} />
41
41
  )}
42
42
  {handleSave && (
43
43
  <IconButton
@@ -45,7 +45,7 @@ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormAct
45
45
  data-testid='save-button'
46
46
  disabled={!canSave}
47
47
  icon='ph--check--regular'
48
- label={t('button save')}
48
+ label={t('save button')}
49
49
  onClick={handleSave}
50
50
  />
51
51
  )}