@dxos/react-ui-form 0.8.4-main.67995b8 → 0.8.4-main.70d3990

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