@dxos/react-ui-form 0.7.4 → 0.7.5-labs.5f04cf6

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 (65) hide show
  1. package/dist/lib/browser/index.mjs +574 -496
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +629 -565
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +574 -496
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +2 -2
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/ArrayField.d.ts +16 -0
  13. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -0
  14. package/dist/types/src/components/Form/Defaults.d.ts +5 -6
  15. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.d.ts +13 -17
  17. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.stories.d.ts +7 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormActions.d.ts +7 -0
  21. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -0
  22. package/dist/types/src/components/Form/FormContent.d.ts +26 -0
  23. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -0
  24. package/dist/types/src/components/Form/FormContext.d.ts +20 -0
  25. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -0
  26. package/dist/types/src/components/Form/Input.d.ts +5 -6
  27. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +4 -0
  29. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -0
  30. package/dist/types/src/components/Form/custom/TupleInput.d.ts +6 -0
  31. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -0
  32. package/dist/types/src/components/Form/custom/index.d.ts +3 -0
  33. package/dist/types/src/components/Form/custom/index.d.ts.map +1 -0
  34. package/dist/types/src/components/Form/factory.d.ts +2 -2
  35. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/index.d.ts +2 -2
  37. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  38. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +6 -5
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useForm.d.ts +13 -15
  42. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  43. package/dist/types/tsconfig.tsbuildinfo +1 -0
  44. package/package.json +26 -26
  45. package/src/components/FieldEditor/FieldEditor.stories.tsx +2 -2
  46. package/src/components/FieldEditor/FieldEditor.tsx +75 -50
  47. package/src/components/Form/ArrayField.tsx +111 -0
  48. package/src/components/Form/Defaults.tsx +20 -33
  49. package/src/components/Form/Form.stories.tsx +48 -6
  50. package/src/components/Form/Form.tsx +39 -333
  51. package/src/components/Form/FormActions.tsx +41 -0
  52. package/src/components/Form/FormContent.tsx +155 -0
  53. package/src/components/Form/FormContext.tsx +48 -0
  54. package/src/components/Form/Input.tsx +5 -6
  55. package/src/components/Form/custom/GeoPointInput.tsx +44 -0
  56. package/src/components/Form/custom/TupleInput.tsx +49 -0
  57. package/src/components/Form/custom/index.ts +6 -0
  58. package/src/components/Form/factory.tsx +4 -7
  59. package/src/components/Form/index.ts +3 -2
  60. package/src/components/ViewEditor/ViewEditor.stories.tsx +30 -8
  61. package/src/components/ViewEditor/ViewEditor.tsx +13 -8
  62. package/src/hooks/useForm.ts +67 -78
  63. package/dist/types/src/components/Form/Custom.d.ts +0 -5
  64. package/dist/types/src/components/Form/Custom.d.ts.map +0 -1
  65. package/src/components/Form/Custom.tsx +0 -65
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.7.4",
3
+ "version": "0.7.5-labs.5f04cf6",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -27,47 +27,47 @@
27
27
  "@effect/schema": "^0.75.5",
28
28
  "@radix-ui/react-context": "^1.0.0",
29
29
  "date-fns": "^3.3.1",
30
- "effect": "^3.9.2",
31
30
  "jsonpath": "^1.1.1",
32
- "@dxos/async": "0.7.4",
33
- "@dxos/echo-db": "0.7.4",
34
- "@dxos/debug": "0.7.4",
35
- "@dxos/echo-schema": "0.7.4",
36
- "@dxos/invariant": "0.7.4",
37
- "@dxos/effect": "0.7.4",
38
- "@dxos/live-object": "0.7.4",
39
- "@dxos/keys": "0.7.4",
40
- "@dxos/log": "0.7.4",
41
- "@dxos/react-client": "0.7.4",
42
- "@dxos/react-ui-list": "0.7.4",
43
- "@dxos/schema": "0.7.4",
44
- "@dxos/react-ui-types": "0.7.4",
45
- "@dxos/util": "0.7.4"
31
+ "@dxos/async": "0.7.5-labs.5f04cf6",
32
+ "@dxos/debug": "0.7.5-labs.5f04cf6",
33
+ "@dxos/echo-schema": "0.7.5-labs.5f04cf6",
34
+ "@dxos/echo-db": "0.7.5-labs.5f04cf6",
35
+ "@dxos/effect": "0.7.5-labs.5f04cf6",
36
+ "@dxos/keys": "0.7.5-labs.5f04cf6",
37
+ "@dxos/live-object": "0.7.5-labs.5f04cf6",
38
+ "@dxos/invariant": "0.7.5-labs.5f04cf6",
39
+ "@dxos/log": "0.7.5-labs.5f04cf6",
40
+ "@dxos/react-client": "0.7.5-labs.5f04cf6",
41
+ "@dxos/react-ui-list": "0.7.5-labs.5f04cf6",
42
+ "@dxos/schema": "0.7.5-labs.5f04cf6",
43
+ "@dxos/util": "0.7.5-labs.5f04cf6",
44
+ "@dxos/react-ui-types": "0.7.5-labs.5f04cf6"
46
45
  },
47
46
  "devDependencies": {
48
47
  "@phosphor-icons/react": "^2.1.5",
49
- "@types/jsonpath": "~0.2.4",
48
+ "@types/jsonpath": "^0.2.4",
50
49
  "@types/react": "~18.2.0",
51
50
  "@types/react-dom": "~18.2.0",
51
+ "effect": "^3.12.3",
52
52
  "fast-check": "^3.19.0",
53
53
  "react": "~18.2.0",
54
54
  "react-dom": "~18.2.0",
55
55
  "vite": "5.4.7",
56
- "@dxos/echo-signals": "0.7.4",
57
- "@dxos/react-ui": "0.7.4",
58
- "@dxos/react-ui-syntax-highlighter": "0.7.4",
59
- "@dxos/random": "0.7.4",
60
- "@dxos/react-ui-theme": "0.7.4",
61
- "@dxos/storybook-utils": "0.7.4"
56
+ "@dxos/random": "0.7.5-labs.5f04cf6",
57
+ "@dxos/echo-signals": "0.7.5-labs.5f04cf6",
58
+ "@dxos/react-ui": "0.7.5-labs.5f04cf6",
59
+ "@dxos/react-ui-syntax-highlighter": "0.7.5-labs.5f04cf6",
60
+ "@dxos/react-ui-theme": "0.7.5-labs.5f04cf6",
61
+ "@dxos/storybook-utils": "0.7.5-labs.5f04cf6"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@effect/schema": "^0.75.5",
65
65
  "@phosphor-icons/react": "^2.1.5",
66
- "effect": "^3.9.2",
66
+ "effect": "^3.12.3",
67
67
  "react": "~18.2.0",
68
68
  "react-dom": "~18.2.0",
69
- "@dxos/react-ui": "0.7.4",
70
- "@dxos/react-ui-theme": "0.7.4"
69
+ "@dxos/react-ui": "0.7.5-labs.5f04cf6",
70
+ "@dxos/react-ui-theme": "0.7.5-labs.5f04cf6"
71
71
  },
72
72
  "publishConfig": {
73
73
  "access": "public"
@@ -7,7 +7,7 @@ import '@dxos-theme';
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
8
  import React from 'react';
9
9
 
10
- import { createMutableSchema } from '@dxos/live-object/testing';
10
+ import { createEchoSchema } from '@dxos/live-object/testing';
11
11
  import { log } from '@dxos/log';
12
12
  import { ViewProjection } from '@dxos/schema';
13
13
  import { TestSchema, testView } from '@dxos/schema/testing';
@@ -49,7 +49,7 @@ type Story = StoryObj<StoryProps>;
49
49
 
50
50
  export const Default: Story = {
51
51
  args: {
52
- projection: new ViewProjection(createMutableSchema(TestSchema), testView),
52
+ projection: new ViewProjection(createEchoSchema(TestSchema), testView),
53
53
  view: testView,
54
54
  field: testView.fields[0],
55
55
  },
@@ -2,10 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useEffect, useState } from 'react';
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { type SchemaResolver } from '@dxos/echo-db';
8
- import { FormatEnum, FormatEnums, formatToType, type MutableSchema } from '@dxos/echo-schema';
7
+ import { type SchemaRegistry } from '@dxos/echo-db';
8
+ import { FormatEnum, FormatEnums, formatToType, type EchoSchema } from '@dxos/echo-schema';
9
9
  import { log } from '@dxos/log';
10
10
  import { useTranslation } from '@dxos/react-ui';
11
11
  import {
@@ -16,16 +16,17 @@ import {
16
16
  type PropertyType,
17
17
  type ViewType,
18
18
  type ViewProjection,
19
+ type SchemaProperty,
19
20
  } from '@dxos/schema';
20
21
 
21
22
  import { translationKey } from '../../translations';
22
- import { Form, type FormProps, SelectInput } from '../Form';
23
+ import { Form, type FormProps, type InputComponent, SelectInput } from '../Form';
23
24
 
24
25
  export type FieldEditorProps = {
25
26
  view: ViewType;
26
27
  projection: ViewProjection;
27
28
  field: FieldType;
28
- registry?: SchemaResolver;
29
+ registry?: SchemaRegistry;
29
30
  onSave: () => void;
30
31
  onCancel?: () => void;
31
32
  };
@@ -38,15 +39,17 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
38
39
  const [props, setProps] = useState<PropertyType>(projection.getFieldProjection(field.id).props);
39
40
  useEffect(() => setProps(projection.getFieldProjection(field.id).props), [field, projection]);
40
41
 
41
- const [schemas, setSchemas] = useState<MutableSchema[]>([]);
42
+ const [schemas, setSchemas] = useState<EchoSchema[]>([]);
42
43
  useEffect(() => {
43
44
  if (!registry) {
44
45
  return;
45
46
  }
46
47
 
47
- const subscription = registry.subscribe(setSchemas);
48
+ const subscription = registry.query().subscribe((query) => setSchemas(query.results), { fire: true });
49
+
50
+ // TODO(dmaretskyi): This shouldn't be needed.
48
51
  const t = setTimeout(async () => {
49
- const schemas = await registry.query();
52
+ const schemas = await registry.query().run();
50
53
  setSchemas(schemas);
51
54
  });
52
55
  return () => {
@@ -55,24 +58,36 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
55
58
  };
56
59
  }, [registry]);
57
60
 
58
- const [schema, setSchema] = useState<MutableSchema>();
61
+ const [referenceSchema, setReferenceSchema] = useState<EchoSchema>();
59
62
  useEffect(() => {
60
- setSchema(schemas.find((schema) => schema.typename === props?.referenceSchema));
63
+ setReferenceSchema(schemas.find((schema) => schema.typename === props?.referenceSchema));
61
64
  }, [schemas, props?.referenceSchema]);
62
65
 
63
66
  // TODO(burdon): Need to wrap otherwise throws error:
64
67
  // Class constructor SchemaClass cannot be invoked without 'new'.
65
68
  const [{ fieldSchema }, setFieldSchema] = useState({ fieldSchema: getFormatSchema(props?.format) });
69
+
66
70
  const handleValuesChanged = useCallback<NonNullable<FormProps<PropertyType>['onValuesChanged']>>(
67
71
  (_props) => {
68
- // Update schema if format changed.
69
72
  // TODO(burdon): Callback should pass `changed` to indicate which fields have changed.
70
- if (_props.format !== props.format) {
71
- setFieldSchema({ fieldSchema: getFormatSchema(_props.format) });
72
- }
73
- if (_props.referenceSchema !== props.referenceSchema) {
74
- setSchema(schemas.find((schema) => schema.typename === _props.referenceSchema));
75
- }
73
+ // TODO(Zaymon): Workout why old and new format values are the same sometimes even when
74
+ // selecting novel format values.
75
+ setFieldSchema((prev) => {
76
+ const fieldSchema = getFormatSchema(_props.format);
77
+ if (prev.fieldSchema === fieldSchema) {
78
+ return prev;
79
+ }
80
+ return { fieldSchema };
81
+ });
82
+ setReferenceSchema((prev) => {
83
+ if (_props.referenceSchema !== prev?.typename) {
84
+ const newSchema = schemas.find((schema) => schema.typename === _props.referenceSchema);
85
+ if (newSchema) {
86
+ return newSchema;
87
+ }
88
+ }
89
+ return prev;
90
+ });
76
91
 
77
92
  setProps((props) => {
78
93
  const type = formatToType[_props.format as keyof typeof formatToType];
@@ -83,7 +98,7 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
83
98
  return props;
84
99
  });
85
100
  },
86
- [schemas, props],
101
+ [schemas, props.format, props.referenceSchema],
87
102
  );
88
103
 
89
104
  const handleValidate = useCallback<NonNullable<FormProps<PropertyType>['onValidate']>>(
@@ -114,6 +129,46 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
114
129
  onSave();
115
130
  }, [onSave]);
116
131
 
132
+ const Custom: Partial<Record<string, InputComponent>> = useMemo(
133
+ () => ({
134
+ ['format' satisfies keyof PropertyType]: (props) => (
135
+ <SelectInput
136
+ {...props}
137
+ options={FormatEnums.filter((value) => value !== FormatEnum.None).map((value) => ({
138
+ value,
139
+ label: t(`format ${value}`),
140
+ }))}
141
+ />
142
+ ),
143
+ ['referenceSchema' satisfies keyof PropertyType]: (props) => (
144
+ <SelectInput
145
+ {...props}
146
+ options={schemas.map((schema) => ({
147
+ value: schema.typename,
148
+ }))}
149
+ />
150
+ ),
151
+ ['referencePath' satisfies keyof PropertyType]: (props) => (
152
+ <SelectInput
153
+ {...props}
154
+ options={
155
+ referenceSchema
156
+ ? getSchemaProperties(referenceSchema.ast)
157
+ .sort(sortProperties)
158
+ .map((p) => ({ value: p.name }))
159
+ : []
160
+ }
161
+ />
162
+ ),
163
+ }),
164
+ [t, schemas, referenceSchema],
165
+ );
166
+
167
+ const propIsNotType = useCallback(
168
+ (props: SchemaProperty<PropertyType>[]) => props.filter((p) => p.name !== 'type'),
169
+ [],
170
+ );
171
+
117
172
  if (!fieldSchema) {
118
173
  log.warn('invalid format', { props });
119
174
  return null;
@@ -125,43 +180,13 @@ export const FieldEditor = ({ view, projection, field, registry, onSave, onCance
125
180
  autoFocus
126
181
  values={props}
127
182
  schema={fieldSchema}
128
- filter={(props) => props.filter((p) => p.name !== 'type')}
183
+ filter={propIsNotType}
129
184
  sort={['property', 'format']}
130
185
  onValuesChanged={handleValuesChanged}
131
186
  onValidate={handleValidate}
132
187
  onSave={handleSave}
133
188
  onCancel={handleCancel}
134
- Custom={{
135
- ['format' satisfies keyof PropertyType]: (props) => (
136
- <SelectInput<PropertyType>
137
- {...props}
138
- options={FormatEnums.filter((value) => value !== FormatEnum.None).map((value) => ({
139
- value,
140
- label: t(`format ${value}`),
141
- }))}
142
- />
143
- ),
144
- ['referenceSchema' satisfies keyof PropertyType]: (props) => (
145
- <SelectInput<PropertyType>
146
- {...props}
147
- options={schemas.map((schema) => ({
148
- value: schema.typename,
149
- }))}
150
- />
151
- ),
152
- ['referencePath' satisfies keyof PropertyType]: (props) => (
153
- <SelectInput<PropertyType>
154
- {...props}
155
- options={
156
- schema
157
- ? getSchemaProperties(schema.schema.ast)
158
- .sort(sortProperties)
159
- .map((p) => ({ value: p.name }))
160
- : []
161
- }
162
- />
163
- ),
164
- }}
189
+ Custom={Custom}
165
190
  />
166
191
  );
167
192
  };
@@ -0,0 +1,111 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { pipe } from 'effect';
6
+ import { capitalize } from 'effect/String';
7
+ import React, { useCallback, useMemo } from 'react';
8
+
9
+ import { AST } from '@dxos/echo-schema';
10
+ import { findNode, getDiscriminatedType, isDiscriminatedUnion, SimpleType } from '@dxos/effect';
11
+ import { invariant } from '@dxos/invariant';
12
+ import { IconButton, useTranslation } from '@dxos/react-ui';
13
+ import { mx } from '@dxos/react-ui-theme';
14
+ import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
15
+
16
+ import { type ComponentLookup } from './Form';
17
+ import { FormField } from './FormContent';
18
+ import { useFormValues, type FormInputStateProps } from './FormContext';
19
+ import { InputHeader, type InputComponent } from './Input';
20
+ import { translationKey } from '../../translations';
21
+
22
+ const padding = 'px-2';
23
+
24
+ type ArrayFieldProps = {
25
+ property: SchemaProperty<any>;
26
+ readonly?: boolean;
27
+ inputProps: FormInputStateProps;
28
+ path?: (string | number)[];
29
+ Custom?: Partial<Record<string, InputComponent>>;
30
+ lookupComponent?: ComponentLookup;
31
+ };
32
+
33
+ export const ArrayField = ({ property, readonly, path, inputProps, Custom, lookupComponent }: ArrayFieldProps) => {
34
+ const { t } = useTranslation(translationKey);
35
+ const { ast, name, type, title } = property;
36
+ const values = useFormValues(path ?? []) as any[];
37
+ invariant(Array.isArray(values));
38
+ const label = title ?? pipe(name, capitalize);
39
+
40
+ const tupleType = findNode(ast, AST.isTupleType);
41
+ const elementType = (tupleType as AST.TupleType | undefined)?.rest[0]?.type;
42
+
43
+ const getDefaultObjectValue = (typeNode: AST.AST): any => {
44
+ const baseNode = findNode(typeNode, isDiscriminatedUnion);
45
+ const typeLiteral = baseNode ? getDiscriminatedType(baseNode, {}) : findNode(typeNode, AST.isTypeLiteral);
46
+ if (!typeLiteral) {
47
+ return {};
48
+ }
49
+
50
+ return Object.fromEntries(getSchemaProperties(typeLiteral, {}).map((prop) => [prop.name, prop.defaultValue]));
51
+ };
52
+
53
+ const newValue = useMemo(() => {
54
+ if (type === 'object' && elementType) {
55
+ return getDefaultObjectValue(elementType);
56
+ } else {
57
+ return SimpleType.getDefaultValue(type);
58
+ }
59
+ }, [type, elementType]);
60
+
61
+ const handleAdd = useCallback(() => {
62
+ inputProps.onValueChange(type, [...values, newValue]);
63
+ }, [type, elementType, inputProps, newValue, values]);
64
+
65
+ const handleRemove = useCallback(
66
+ (index: number) => {
67
+ const newValues = values.filter((_, i) => i !== index);
68
+ inputProps.onValueChange(type, newValues);
69
+ },
70
+ [type, inputProps, values],
71
+ );
72
+
73
+ if (!elementType) {
74
+ return null;
75
+ }
76
+
77
+ return (
78
+ <div role='none' className={mx(padding)}>
79
+ <InputHeader>{label}</InputHeader>
80
+ <div role='none' className='flex flex-col gap-1'>
81
+ {values.map((_value, index) => (
82
+ <div key={index} role='none' className='flex items-center gap-1'>
83
+ <div role='none' className='flex-1'>
84
+ <FormField
85
+ property={{
86
+ ...property,
87
+ array: false, // NOTE(ZaymonFC): This breaks arrays of arrays but ¯\_(ツ)_/¯. Ping me if you need that.
88
+ ast: elementType,
89
+ }}
90
+ path={[...(path ?? []), index]}
91
+ readonly={readonly}
92
+ inline
93
+ Custom={Custom}
94
+ lookupComponent={lookupComponent}
95
+ />
96
+ </div>
97
+ <IconButton
98
+ icon='ph--trash--regular'
99
+ iconOnly
100
+ label={t('button remove')}
101
+ onClick={() => handleRemove(index)}
102
+ />
103
+ </div>
104
+ ))}
105
+ </div>
106
+ <div role='none' className='flex justify-between items-center plb-1'>
107
+ <IconButton icon='ph--plus--regular' iconOnly label={t('button add')} onClick={handleAdd} />
108
+ </div>
109
+ </div>
110
+ );
111
+ };
@@ -4,13 +4,11 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type BaseObject } from '@dxos/echo-schema';
8
7
  import { Input, Select } from '@dxos/react-ui';
9
8
 
10
9
  import { type InputProps, InputHeader } from './Input';
11
10
 
12
- export const TextInput = <T extends BaseObject>({
13
- property,
11
+ export const TextInput = ({
14
12
  type,
15
13
  label,
16
14
  inputOnly,
@@ -20,8 +18,8 @@ export const TextInput = <T extends BaseObject>({
20
18
  getValue,
21
19
  onValueChange,
22
20
  onBlur,
23
- }: InputProps<T>) => {
24
- const { status, error } = getStatus?.(property);
21
+ }: InputProps) => {
22
+ const { status, error } = getStatus();
25
23
 
26
24
  return (
27
25
  <Input.Root validationValence={status}>
@@ -33,8 +31,8 @@ export const TextInput = <T extends BaseObject>({
33
31
  <Input.TextInput
34
32
  disabled={disabled}
35
33
  placeholder={placeholder}
36
- value={getValue(property) ?? ''}
37
- onChange={(event) => onValueChange(property, type, event.target.value)}
34
+ value={getValue() ?? ''}
35
+ onChange={(event) => onValueChange(type, event.target.value)}
38
36
  onBlur={onBlur}
39
37
  />
40
38
  {inputOnly && <Input.Validation>{error}</Input.Validation>}
@@ -42,8 +40,7 @@ export const TextInput = <T extends BaseObject>({
42
40
  );
43
41
  };
44
42
 
45
- export const NumberInput = <T extends BaseObject>({
46
- property,
43
+ export const NumberInput = ({
47
44
  type,
48
45
  label,
49
46
  inputOnly,
@@ -53,8 +50,8 @@ export const NumberInput = <T extends BaseObject>({
53
50
  getValue,
54
51
  onValueChange,
55
52
  onBlur,
56
- }: InputProps<T>) => {
57
- const { status, error } = getStatus?.(property);
53
+ }: InputProps) => {
54
+ const { status, error } = getStatus();
58
55
 
59
56
  return (
60
57
  <Input.Root validationValence={status}>
@@ -67,8 +64,8 @@ export const NumberInput = <T extends BaseObject>({
67
64
  type='number'
68
65
  disabled={disabled}
69
66
  placeholder={placeholder}
70
- value={getValue(property) ?? 0}
71
- onChange={(event) => onValueChange(property, type, event.target.value)}
67
+ value={getValue() ?? 0}
68
+ onChange={(event) => onValueChange(type, event.target.value)}
72
69
  onBlur={onBlur}
73
70
  />
74
71
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
@@ -76,17 +73,11 @@ export const NumberInput = <T extends BaseObject>({
76
73
  );
77
74
  };
78
75
 
79
- export const BooleanInput = <T extends BaseObject>({
80
- property,
81
- type,
82
- label,
83
- inputOnly,
84
- getStatus,
85
- getValue,
86
- onValueChange,
87
- }: InputProps<T>) => {
88
- const { status, error } = getStatus?.(property);
76
+ export const BooleanInput = ({ type, label, inputOnly, getStatus, getValue, onValueChange }: InputProps) => {
77
+ const { status, error } = getStatus();
89
78
 
79
+ // TODO(burdon): Runtime error:
80
+ // Warning: Unknown event handler property `onCheckedChange`. It will be ignored.
90
81
  return (
91
82
  <Input.Root validationValence={status}>
92
83
  {!inputOnly && (
@@ -94,21 +85,17 @@ export const BooleanInput = <T extends BaseObject>({
94
85
  <Input.Label>{label}</Input.Label>
95
86
  </InputHeader>
96
87
  )}
97
- <Input.Switch
98
- checked={getValue<boolean>(property)}
99
- onCheckedChange={(value) => onValueChange(property, type, value)}
100
- />
88
+ <Input.Switch checked={getValue()} onCheckedChange={(value) => onValueChange(type, value)} />
101
89
  {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
102
90
  </Input.Root>
103
91
  );
104
92
  };
105
93
 
106
- export type SelectInputOptions<T extends BaseObject> = InputProps<T> & {
94
+ export type SelectInputOptions = InputProps & {
107
95
  options?: Array<{ value: string | number; label?: string }>;
108
96
  };
109
97
 
110
- export const SelectInput = <T extends BaseObject>({
111
- property,
98
+ export const SelectInput = ({
112
99
  type,
113
100
  label,
114
101
  inputOnly,
@@ -118,8 +105,8 @@ export const SelectInput = <T extends BaseObject>({
118
105
  getStatus,
119
106
  getValue,
120
107
  onValueChange,
121
- }: SelectInputOptions<T>) => {
122
- const { status, error } = getStatus?.(property);
108
+ }: SelectInputOptions) => {
109
+ const { status, error } = getStatus();
123
110
 
124
111
  return (
125
112
  <Input.Root validationValence={status}>
@@ -128,7 +115,7 @@ export const SelectInput = <T extends BaseObject>({
128
115
  <Input.Label>{label}</Input.Label>
129
116
  </InputHeader>
130
117
  )}
131
- <Select.Root value={getValue(property)} onValueChange={(value) => onValueChange(property, type, value)}>
118
+ <Select.Root value={getValue()} onValueChange={(value) => onValueChange(type, value)}>
132
119
  {/* TODO(burdon): Placeholder not working? */}
133
120
  <Select.TriggerButton classNames='is-full' disabled={disabled} placeholder={placeholder} />
134
121
  <Select.Portal>
@@ -36,7 +36,7 @@ type TestType = S.Schema.Type<typeof TestSchema>;
36
36
 
37
37
  type StoryProps<T extends BaseObject> = { schema: S.Schema<T> } & FormProps<T>;
38
38
 
39
- const DefaultStory = <T extends BaseObject>({ schema, values: initialValues }: StoryProps<T>) => {
39
+ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues, ...props }: StoryProps<T>) => {
40
40
  const [values, setValues] = useState(initialValues);
41
41
  const handleSave = useCallback<NonNullable<FormProps<T>['onSave']>>((values) => {
42
42
  setValues(values);
@@ -45,7 +45,7 @@ const DefaultStory = <T extends BaseObject>({ schema, values: initialValues }: S
45
45
  return (
46
46
  <TestLayout json={{ values, schema: schema.ast.toJSON() }}>
47
47
  <TestPanel>
48
- <Form<T> schema={schema} values={values} onSave={handleSave} />
48
+ <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
49
49
  </TestPanel>
50
50
  </TestLayout>
51
51
  );
@@ -78,17 +78,27 @@ export const Default: Story<TestType> = {
78
78
  },
79
79
  };
80
80
 
81
- // TODO(burdon): Should accept partial values.
82
81
  export const Org: Story<Testing.OrgSchemaType> = {
83
82
  args: {
84
83
  schema: Testing.OrgSchema,
85
84
  values: {
86
- // name: 'DXOS',
85
+ name: 'DXOS',
87
86
  // website: 'https://dxos.org',
88
87
  },
89
88
  },
90
89
  };
91
90
 
91
+ export const OrgAutoSave: Story<Testing.OrgSchemaType> = {
92
+ args: {
93
+ schema: Testing.OrgSchema,
94
+ values: {
95
+ name: 'DXOS',
96
+ // website: 'https://dxos.org',
97
+ },
98
+ autoSave: true,
99
+ },
100
+ };
101
+
92
102
  // TODO(burdon): Type issue with employer reference.
93
103
  // TODO(burdon): Test table/form with compound values (e.g., address).
94
104
  // export const Contact: Story<Testing.ContactSchemaType> = {
@@ -114,7 +124,7 @@ const ShapeSchema = S.Struct({
114
124
  }),
115
125
  S.Struct({
116
126
  type: S.Literal('square').annotations({ [AST.TitleAnnotationId]: 'Type' }),
117
- size: S.optional(S.Number.annotations({ [AST.TitleAnnotationId]: 'Size' })),
127
+ size: S.optional(S.Number.pipe(S.nonNegative()).annotations({ [AST.TitleAnnotationId]: 'Size' })),
118
128
  }),
119
129
  ).annotations({ [AST.TitleAnnotationId]: 'Shape' }),
120
130
  ),
@@ -139,7 +149,7 @@ const DiscriminatedUnionStory = ({ values: initialValues }: DiscriminatedUnionSt
139
149
  onSave={handleSave}
140
150
  Custom={{
141
151
  ['shape.type' as const]: (props) => (
142
- <SelectInput<ShapeType>
152
+ <SelectInput
143
153
  {...props}
144
154
  options={['circle', 'square'].map((value) => ({
145
155
  value,
@@ -197,3 +207,35 @@ export const Arrays: StoryObj<FormProps<ArraysType>> = {
197
207
  },
198
208
  },
199
209
  };
210
+
211
+ const ColorSchema = S.Struct({
212
+ color: S.Union(S.Literal('red'), S.Literal('green'), S.Literal('blue')).annotations({
213
+ [AST.TitleAnnotationId]: 'Color',
214
+ }),
215
+ }).pipe(S.mutable);
216
+
217
+ type ColorType = S.Schema.Type<typeof ColorSchema>;
218
+
219
+ const EnumStory = ({ values: initialValues }: FormProps<ColorType>) => {
220
+ const [values, setValues] = useState(initialValues);
221
+ const handleSave = useCallback<NonNullable<FormProps<ColorType>['onSave']>>((values) => {
222
+ setValues(values);
223
+ }, []);
224
+
225
+ return (
226
+ <TestLayout json={{ values, schema: ColorSchema.ast.toJSON() }}>
227
+ <TestPanel>
228
+ <Form<ColorType> schema={ColorSchema} values={values} onSave={handleSave} />
229
+ </TestPanel>
230
+ </TestLayout>
231
+ );
232
+ };
233
+
234
+ export const Enum: StoryObj<FormProps<ColorType>> = {
235
+ render: EnumStory,
236
+ args: {
237
+ values: {
238
+ color: 'red',
239
+ },
240
+ },
241
+ };