@dxos/react-ui-form 0.8.4-main.f9ba587 → 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 (118) hide show
  1. package/dist/lib/browser/index.mjs +884 -637
  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 +884 -637
  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 +4 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +77 -7
  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 +8 -6
  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/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 +3 -3
  27. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/Input.d.ts +5 -2
  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 +75 -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 +1 -1
  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 +81 -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 -9
  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 +3 -3
  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 +13 -8
  61. package/dist/types/src/translations.d.ts.map +1 -1
  62. package/dist/types/src/util/field.d.ts +1 -1
  63. package/dist/types/src/util/field.d.ts.map +1 -1
  64. package/dist/types/src/util/formatting.d.ts +1 -1
  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 +42 -36
  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 +26 -24
  75. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  76. package/src/components/FieldEditor/FieldEditor.tsx +21 -23
  77. package/src/components/Form/ArrayField.tsx +13 -12
  78. package/src/components/Form/Defaults.tsx +23 -21
  79. package/src/components/Form/Form.stories.tsx +135 -99
  80. package/src/components/Form/Form.tsx +30 -16
  81. package/src/components/Form/FormActions.tsx +4 -3
  82. package/src/components/Form/FormContent.tsx +171 -49
  83. package/src/components/Form/FormContext.tsx +8 -8
  84. package/src/components/Form/Input.tsx +5 -2
  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 +2 -2
  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 +130 -49
  95. package/src/components/ViewEditor/ViewEditor.test.tsx +19 -19
  96. package/src/components/ViewEditor/ViewEditor.tsx +281 -233
  97. package/src/components/index.ts +1 -2
  98. package/src/components/testing.tsx +15 -9
  99. package/src/hooks/useForm.ts +11 -6
  100. package/src/hooks/useQueryRefOptions.ts +15 -6
  101. package/src/hooks/useRefQueryLookupHandler.ts +3 -3
  102. package/src/translations.ts +15 -9
  103. package/src/util/field.ts +9 -19
  104. package/src/util/formatting.ts +1 -1
  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
@@ -6,14 +6,15 @@ import React, { useCallback, useEffect, useRef } from 'react';
6
6
 
7
7
  import { Input, Select, useTranslation } from '@dxos/react-ui';
8
8
 
9
- import { type InputProps, InputHeader } from './Input';
10
9
  import { translationKey } from '../../translations';
11
10
 
11
+ import { InputHeader, type InputProps } from './Input';
12
+
12
13
  export const TextInput = ({
13
14
  type,
14
15
  label,
15
16
  inputOnly,
16
- disabled,
17
+ readonly,
17
18
  placeholder,
18
19
  getStatus,
19
20
  getValue,
@@ -22,16 +23,16 @@ export const TextInput = ({
22
23
  }: InputProps) => {
23
24
  const { status, error } = getStatus();
24
25
 
25
- return disabled && !getValue() ? null : disabled && inputOnly ? (
26
+ return readonly && !getValue() ? null : readonly === 'static' && inputOnly ? (
26
27
  <p>{getValue() ?? ''}</p>
27
28
  ) : (
28
29
  <Input.Root validationValence={status}>
29
30
  {!inputOnly && <InputHeader error={error} label={label} />}
30
- {disabled ? (
31
+ {readonly === 'static' ? (
31
32
  <p>{getValue() ?? ''}</p>
32
33
  ) : (
33
34
  <Input.TextInput
34
- disabled={disabled}
35
+ disabled={!!readonly}
35
36
  placeholder={placeholder}
36
37
  value={getValue() ?? ''}
37
38
  onChange={(event) => onValueChange(type, event.target.value)}
@@ -47,7 +48,7 @@ export const NumberInput = ({
47
48
  type,
48
49
  label,
49
50
  inputOnly,
50
- disabled,
51
+ readonly,
51
52
  placeholder,
52
53
  getStatus,
53
54
  getValue,
@@ -56,19 +57,19 @@ export const NumberInput = ({
56
57
  }: InputProps) => {
57
58
  const { status, error } = getStatus();
58
59
 
59
- return disabled && !getValue() ? null : disabled && inputOnly ? (
60
+ return readonly && !getValue() ? null : readonly === 'static' && inputOnly ? (
60
61
  <p>{getValue() ?? ''}</p>
61
62
  ) : (
62
63
  <Input.Root validationValence={status}>
63
64
  {!inputOnly && <InputHeader error={error} label={label} />}
64
- {disabled ? (
65
+ {readonly === 'static' ? (
65
66
  <p>{getValue() ?? ''}</p>
66
67
  ) : (
67
68
  <Input.TextInput
68
69
  type='number'
69
- disabled={disabled}
70
+ disabled={!!readonly}
70
71
  placeholder={placeholder}
71
- value={getValue() ?? 0}
72
+ value={getValue()}
72
73
  onChange={(event) => onValueChange(type, event.target.value)}
73
74
  onBlur={onBlur}
74
75
  />
@@ -78,7 +79,7 @@ export const NumberInput = ({
78
79
  );
79
80
  };
80
81
 
81
- export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onValueChange, disabled }: InputProps) => {
82
+ export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onValueChange, readonly }: InputProps) => {
82
83
  const { status, error } = getStatus();
83
84
  const checked = Boolean(getValue());
84
85
  const { t } = useTranslation(translationKey);
@@ -86,10 +87,10 @@ export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onVa
86
87
  return (
87
88
  <Input.Root validationValence={status}>
88
89
  {!inputOnly && <InputHeader error={error} label={label} />}
89
- {disabled ? (
90
+ {readonly === 'static' ? (
90
91
  <p>{t(checked ? 'boolean input true value' : 'boolean input false value')}</p>
91
92
  ) : (
92
- <Input.Switch checked={checked} onCheckedChange={(value) => onValueChange(type, value)} />
93
+ <Input.Switch disabled={!!readonly} checked={checked} onCheckedChange={(value) => onValueChange(type, value)} />
93
94
  )}
94
95
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
95
96
  </Input.Root>
@@ -104,7 +105,7 @@ export const SelectInput = ({
104
105
  type,
105
106
  label,
106
107
  inputOnly,
107
- disabled,
108
+ readonly,
108
109
  placeholder,
109
110
  options,
110
111
  getStatus,
@@ -116,15 +117,15 @@ export const SelectInput = ({
116
117
  const value = getValue() as string | undefined;
117
118
  const handleValueChange = useCallback((value: string | number) => onValueChange(type, value), [type, onValueChange]);
118
119
 
119
- return disabled && !value ? null : (
120
+ return readonly && !value ? null : (
120
121
  <Input.Root validationValence={status}>
121
122
  {!inputOnly && <InputHeader error={error} label={label} />}
122
- {disabled ? (
123
+ {readonly === 'static' ? (
123
124
  <p>{options?.find(({ value: optionValue }) => optionValue === value)?.label ?? String(value)}</p>
124
125
  ) : (
125
126
  <Select.Root value={value} onValueChange={handleValueChange}>
126
127
  {/* TODO(burdon): Placeholder not working? */}
127
- <Select.TriggerButton classNames='is-full' disabled={disabled} placeholder={placeholder} />
128
+ <Select.TriggerButton classNames='is-full' disabled={!!readonly} placeholder={placeholder} />
128
129
  <Select.Portal>
129
130
  <Select.Content>
130
131
  <Select.Viewport>
@@ -135,6 +136,7 @@ export const SelectInput = ({
135
136
  </Select.Option>
136
137
  ))}
137
138
  </Select.Viewport>
139
+ <Select.Arrow />
138
140
  </Select.Content>
139
141
  </Select.Portal>
140
142
  </Select.Root>
@@ -148,7 +150,7 @@ export const MarkdownInput = ({
148
150
  type,
149
151
  label,
150
152
  inputOnly,
151
- disabled,
153
+ readonly,
152
154
  placeholder,
153
155
  getStatus,
154
156
  getValue,
@@ -182,15 +184,15 @@ export const MarkdownInput = ({
182
184
  return (
183
185
  <Input.Root validationValence={status}>
184
186
  {!inputOnly && <InputHeader error={error} label={label} />}
185
- {disabled ? (
187
+ {readonly === 'static' ? (
186
188
  <p>{getValue() ?? ''}</p>
187
189
  ) : (
188
190
  <Input.TextArea
189
191
  ref={textareaRef}
190
- disabled={disabled}
192
+ disabled={!!readonly}
191
193
  placeholder={placeholder}
192
194
  value={getValue() ?? ''}
193
- classNames={'min-bs-auto max-h-40 overflow-auto'}
195
+ classNames={'min-bs-auto max-bs-40 overflow-auto'}
194
196
  onChange={(event) => onValueChange(type, event.target.value)}
195
197
  onBlur={onBlur}
196
198
  style={{ resize: 'none' }}
@@ -2,24 +2,71 @@
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, getObjectDXN, Ref, type TypeAnnotation } 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
- import { SelectInput } from './Defaults';
19
- import { Form, type FormProps } from './Form';
20
17
  import { translations } from '../../translations';
21
18
  import { TestLayout, TestPanel } from '../testing';
22
19
 
20
+ import { SelectInput } from './Defaults';
21
+ import { Form, type FormProps } from './Form';
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
+
23
70
  const AddressSchema = Schema.Struct({
24
71
  street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
25
72
  city: Schema.optional(Schema.String.annotations({ title: 'City' })),
@@ -27,8 +74,7 @@ const AddressSchema = Schema.Struct({
27
74
  location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
28
75
  }).annotations({ title: 'Address' });
29
76
 
30
- // TODO(burdon): Translations?
31
- const TestSchema = Schema.Struct({
77
+ const ContactSchema = Schema.Struct({
32
78
  name: Schema.optional(Schema.String.annotations({ title: 'Name' })),
33
79
  active: Schema.optional(Schema.Boolean.annotations({ title: 'Active' })),
34
80
  rank: Schema.optional(Schema.Number.annotations({ title: 'Rank' })),
@@ -36,40 +82,15 @@ const TestSchema = Schema.Struct({
36
82
  address: Schema.optional(AddressSchema),
37
83
  }).pipe(Schema.mutable);
38
84
 
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
- };
51
-
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
- };
85
+ type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
66
86
 
67
- const meta: Meta<StoryProps<any>> = {
87
+ const meta = {
68
88
  title: 'ui/react-ui-form/Form',
69
- component: Form<any>,
70
- render: DebugStory,
71
- decorators: [withLayout({ fullscreen: true }), withTheme],
89
+ component: Form as any,
90
+ render: DefaultStory,
91
+ decorators: [withTheme, withLayoutVariants()],
72
92
  parameters: {
93
+ layout: 'fullscreen',
73
94
  translations,
74
95
  },
75
96
  argTypes: {
@@ -78,17 +99,15 @@ const meta: Meta<StoryProps<any>> = {
78
99
  description: 'Readonly',
79
100
  },
80
101
  },
81
- };
102
+ } satisfies Meta<StoryProps<any>>;
82
103
 
83
104
  export default meta;
84
105
 
85
- type Story<T extends BaseObject> = StoryObj<StoryProps<T>>;
106
+ type Story = StoryObj<typeof meta>;
86
107
 
87
- export const Default: Story<TestType> = {
88
- render: DefaultStory,
89
- decorators: [withSurfaceVariantsLayout(), withTheme],
108
+ export const Default: Story = {
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 Person: Story = {
151
+ args: {
152
+ debug: true,
153
+ schema: Testing.Person,
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 { 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';
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
21
  export type PropsFilter<T extends BaseObject> = (props: SchemaProperty<T>[]) => SchemaProperty<T>[];
20
22
 
@@ -27,29 +29,33 @@ export type ComponentLookup = (args: {
27
29
  export type CustomInputMap = Partial<Record<string, InputComponent>>;
28
30
 
29
31
  export type FormProps<T extends BaseObject> = 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
48
  export const Form = <T extends BaseObject>({
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,7 @@ 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 data-testid='cancel-button' icon='ph--x--regular' label={t('cancel button')} onClick={onCancel} />
40
41
  )}
41
42
  {handleSave && (
42
43
  <IconButton
@@ -44,7 +45,7 @@ export const FormActions = ({ readonly, onCancel, outerSpacing = true }: FormAct
44
45
  data-testid='save-button'
45
46
  disabled={!canSave}
46
47
  icon='ph--check--regular'
47
- label={t('button save')}
48
+ label={t('save button')}
48
49
  onClick={handleSave}
49
50
  />
50
51
  )}