@dxos/react-ui-form 0.8.4-main.e098934 → 0.8.4-main.e8ec1fe

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 (89) hide show
  1. package/dist/lib/browser/index.mjs +480 -300
  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 +480 -300
  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.stories.d.ts +5 -5
  8. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +5 -5
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.d.ts +4 -3
  14. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form.stories.d.ts +15 -15
  16. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormContent.d.ts +5 -5
  18. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  20. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/Input.d.ts +1 -1
  22. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  23. package/dist/types/src/components/Form/RefField.d.ts +10 -6
  24. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +5 -5
  26. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/factory.d.ts +1 -1
  28. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  29. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +41 -0
  30. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  31. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +81 -0
  32. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  34. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  35. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +20 -7
  36. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  37. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +10 -10
  38. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/index.d.ts +1 -0
  40. package/dist/types/src/components/index.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useForm.d.ts +2 -2
  42. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  43. package/dist/types/src/hooks/useQueryRefOptions.d.ts +9 -3
  44. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  46. package/dist/types/src/translations.d.ts +4 -4
  47. package/dist/types/src/util/field.d.ts +1 -1
  48. package/dist/types/src/util/field.d.ts.map +1 -1
  49. package/dist/types/src/util/formatting.d.ts +1 -1
  50. package/dist/types/src/util/formatting.d.ts.map +1 -1
  51. package/dist/types/src/util/refs.d.ts +1 -1
  52. package/dist/types/src/util/refs.d.ts.map +1 -1
  53. package/dist/types/src/util/schema.d.ts +1 -1
  54. package/dist/types/src/util/schema.d.ts.map +1 -1
  55. package/dist/types/tsconfig.tsbuildinfo +1 -1
  56. package/package.json +40 -39
  57. package/src/components/ControlSection/ControlSection.stories.tsx +4 -5
  58. package/src/components/ControlSection/ControlSection.tsx +1 -1
  59. package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -5
  60. package/src/components/FieldEditor/FieldEditor.tsx +5 -2
  61. package/src/components/Form/ArrayField.tsx +4 -3
  62. package/src/components/Form/Defaults.tsx +1 -1
  63. package/src/components/Form/Form.stories.tsx +35 -35
  64. package/src/components/Form/Form.tsx +16 -6
  65. package/src/components/Form/FormActions.tsx +2 -2
  66. package/src/components/Form/FormContent.tsx +82 -22
  67. package/src/components/Form/FormContext.tsx +3 -3
  68. package/src/components/Form/Input.tsx +1 -1
  69. package/src/components/Form/RefField.tsx +48 -64
  70. package/src/components/Form/custom/GeoPointInput.tsx +1 -1
  71. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +5 -6
  72. package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
  73. package/src/components/Form/factory.tsx +1 -1
  74. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +207 -0
  75. package/src/components/ObjectPicker/ObjectPicker.tsx +161 -0
  76. package/src/components/ObjectPicker/index.ts +5 -0
  77. package/src/components/ViewEditor/ViewEditor.stories.tsx +98 -55
  78. package/src/components/ViewEditor/ViewEditor.test.tsx +6 -5
  79. package/src/components/ViewEditor/ViewEditor.tsx +275 -231
  80. package/src/components/index.ts +1 -0
  81. package/src/components/testing.tsx +2 -2
  82. package/src/hooks/useForm.ts +7 -4
  83. package/src/hooks/useQueryRefOptions.ts +15 -6
  84. package/src/hooks/useRefQueryLookupHandler.ts +3 -3
  85. package/src/translations.ts +4 -4
  86. package/src/util/field.ts +1 -1
  87. package/src/util/formatting.ts +1 -1
  88. package/src/util/refs.ts +7 -7
  89. package/src/util/schema.ts +2 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.8.4-main.e098934",
3
+ "version": "0.8.4-main.e8ec1fe",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -28,54 +28,55 @@
28
28
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
29
29
  "@fluentui/react-tabster": "^9.24.2",
30
30
  "@preact-signals/safe-react": "^0.9.0",
31
+ "@preact/signals-react": "^3.2.0",
31
32
  "@radix-ui/react-context": "1.1.1",
32
33
  "date-fns": "^3.3.1",
33
34
  "jsonpath": "^1.1.1",
34
- "@dxos/async": "0.8.4-main.e098934",
35
- "@dxos/debug": "0.8.4-main.e098934",
36
- "@dxos/echo": "0.8.4-main.e098934",
37
- "@dxos/echo-schema": "0.8.4-main.e098934",
38
- "@dxos/echo-db": "0.8.4-main.e098934",
39
- "@dxos/effect": "0.8.4-main.e098934",
40
- "@dxos/invariant": "0.8.4-main.e098934",
41
- "@dxos/lit-ui": "0.8.4-main.e098934",
42
- "@dxos/keys": "0.8.4-main.e098934",
43
- "@dxos/live-object": "0.8.4-main.e098934",
44
- "@dxos/log": "0.8.4-main.e098934",
45
- "@dxos/react-client": "0.8.4-main.e098934",
46
- "@dxos/react-ui-components": "0.8.4-main.e098934",
47
- "@dxos/react-ui-editor": "0.8.4-main.e098934",
48
- "@dxos/react-ui-pickers": "0.8.4-main.e098934",
49
- "@dxos/react-ui-list": "0.8.4-main.e098934",
50
- "@dxos/react-ui-searchlist": "0.8.4-main.e098934",
51
- "@dxos/react-ui-stack": "0.8.4-main.e098934",
52
- "@dxos/schema": "0.8.4-main.e098934",
53
- "@dxos/react-ui-types": "0.8.4-main.e098934",
54
- "@dxos/util": "0.8.4-main.e098934",
55
- "@dxos/react-ui-tag-picker": "0.8.4-main.e098934"
35
+ "@dxos/async": "0.8.4-main.e8ec1fe",
36
+ "@dxos/echo": "0.8.4-main.e8ec1fe",
37
+ "@dxos/debug": "0.8.4-main.e8ec1fe",
38
+ "@dxos/echo-db": "0.8.4-main.e8ec1fe",
39
+ "@dxos/effect": "0.8.4-main.e8ec1fe",
40
+ "@dxos/echo-query": "0.8.4-main.e8ec1fe",
41
+ "@dxos/keys": "0.8.4-main.e8ec1fe",
42
+ "@dxos/lit-ui": "0.8.4-main.e8ec1fe",
43
+ "@dxos/live-object": "0.8.4-main.e8ec1fe",
44
+ "@dxos/invariant": "0.8.4-main.e8ec1fe",
45
+ "@dxos/react-client": "0.8.4-main.e8ec1fe",
46
+ "@dxos/react-ui-components": "0.8.4-main.e8ec1fe",
47
+ "@dxos/react-ui-editor": "0.8.4-main.e8ec1fe",
48
+ "@dxos/react-ui-pickers": "0.8.4-main.e8ec1fe",
49
+ "@dxos/log": "0.8.4-main.e8ec1fe",
50
+ "@dxos/react-ui-list": "0.8.4-main.e8ec1fe",
51
+ "@dxos/react-ui-searchlist": "0.8.4-main.e8ec1fe",
52
+ "@dxos/react-ui-stack": "0.8.4-main.e8ec1fe",
53
+ "@dxos/schema": "0.8.4-main.e8ec1fe",
54
+ "@dxos/react-ui-types": "0.8.4-main.e8ec1fe",
55
+ "@dxos/types": "0.8.4-main.e8ec1fe",
56
+ "@dxos/util": "0.8.4-main.e8ec1fe"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@types/jsonpath": "^0.2.4",
59
- "@types/react": "~18.2.0",
60
- "@types/react-dom": "~18.2.0",
61
- "effect": "3.17.7",
60
+ "@types/react": "~19.2.2",
61
+ "@types/react-dom": "~19.2.2",
62
+ "effect": "3.18.3",
62
63
  "fast-check": "^3.19.0",
63
- "react": "~18.2.0",
64
- "react-dom": "~18.2.0",
65
- "vite": "7.1.1",
66
- "@dxos/echo-signals": "0.8.4-main.e098934",
67
- "@dxos/random": "0.8.4-main.e098934",
68
- "@dxos/react-ui": "0.8.4-main.e098934",
69
- "@dxos/react-ui-theme": "0.8.4-main.e098934",
70
- "@dxos/storybook-utils": "0.8.4-main.e098934",
71
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.e098934"
64
+ "react": "~19.2.0",
65
+ "react-dom": "~19.2.0",
66
+ "vite": "7.1.9",
67
+ "@dxos/echo-signals": "0.8.4-main.e8ec1fe",
68
+ "@dxos/random": "0.8.4-main.e8ec1fe",
69
+ "@dxos/react-ui": "0.8.4-main.e8ec1fe",
70
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.e8ec1fe",
71
+ "@dxos/react-ui-theme": "0.8.4-main.e8ec1fe",
72
+ "@dxos/storybook-utils": "0.8.4-main.e8ec1fe"
72
73
  },
73
74
  "peerDependencies": {
74
75
  "effect": "^3.13.3",
75
- "react": "~18.2.0",
76
- "react-dom": "~18.2.0",
77
- "@dxos/react-ui": "0.8.4-main.e098934",
78
- "@dxos/react-ui-theme": "0.8.4-main.e098934"
76
+ "react": "^19.0.0",
77
+ "react-dom": "^19.0.0",
78
+ "@dxos/react-ui": "0.8.4-main.e8ec1fe",
79
+ "@dxos/react-ui-theme": "0.8.4-main.e8ec1fe"
79
80
  },
80
81
  "publishConfig": {
81
82
  "access": "public"
@@ -2,12 +2,10 @@
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
6
  import React from 'react';
9
7
 
10
- import { withLayout, withTheme } from '@dxos/storybook-utils';
8
+ import { withTheme } from '@dxos/react-ui/testing';
11
9
 
12
10
  import { translations } from '../../translations';
13
11
 
@@ -17,12 +15,13 @@ const meta = {
17
15
  title: 'ui/react-ui-form/ControlSection',
18
16
  component: ControlSection,
19
17
  render: (args) => (
20
- <ControlPage classNames='w-[40rem]'>
18
+ <ControlPage classNames='is-[40rem]'>
21
19
  <ControlSection {...args} />
22
20
  </ControlPage>
23
21
  ),
24
- decorators: [withLayout({ fullscreen: true, classNames: 'justify-center' }), withTheme],
22
+ decorators: [withTheme],
25
23
  parameters: {
24
+ layout: 'centered',
26
25
  translations,
27
26
  },
28
27
  } satisfies Meta<typeof ControlSection>;
@@ -22,7 +22,7 @@ export type ControlPageProps = ThemedClassName<ComponentPropsWithoutRef<'div'>>;
22
22
 
23
23
  export const ControlPage = ({ children, classNames, ...props }: ControlPageProps) => {
24
24
  return (
25
- <StackItem.Content classNames='block overflow-y-auto [--control-spacing:var(--dx-trimMd)]'>
25
+ <StackItem.Content scrollable classNames='[--control-spacing:var(--dx-trimMd)]'>
26
26
  <div role='none' className={mx('pli-cardSpacingInline pbe-trimLg', classNames)} {...props}>
27
27
  {children}
28
28
  </div>
@@ -2,16 +2,14 @@
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
6
  import React, { useEffect, useMemo } from 'react';
9
7
 
10
- import { createEchoSchema } from '@dxos/live-object/testing';
8
+ import { createEchoSchema } from '@dxos/echo/testing';
11
9
  import { log } from '@dxos/log';
10
+ import { withTheme } from '@dxos/react-ui/testing';
12
11
  import { ProjectionModel } from '@dxos/schema';
13
12
  import { TestSchema, testView } from '@dxos/schema/testing';
14
- import { withLayout, withTheme } from '@dxos/storybook-utils';
15
13
 
16
14
  import { translations } from '../../translations';
17
15
  import { FIELD_EDITOR_DEBUG_SYMBOL, TestLayout, TestPanel } from '../testing';
@@ -31,6 +29,8 @@ const DefaultStory = (props: FieldEditorProps) => {
31
29
 
32
30
  // Expose objects on window for test access.
33
31
  useEffect(() => {
32
+ props.projection.normalizeView();
33
+
34
34
  if (typeof window !== 'undefined') {
35
35
  (window as any)[FIELD_EDITOR_DEBUG_SYMBOL] = {
36
36
  props,
@@ -58,8 +58,9 @@ const meta = {
58
58
  title: 'ui/react-ui-form/FieldEditor',
59
59
  component: FieldEditor as any,
60
60
  render: DefaultStory,
61
- decorators: [withLayout({ fullscreen: true }), withTheme],
61
+ decorators: [withTheme],
62
62
  parameters: {
63
+ layout: 'fullscreen',
63
64
  translations,
64
65
  },
65
66
  } satisfies Meta<typeof DefaultStory>;
@@ -4,8 +4,8 @@
4
4
 
5
5
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
+ import { type EchoSchema, FormatEnum, FormatEnums, formatToType } from '@dxos/echo/internal';
7
8
  import { type SchemaRegistry } from '@dxos/echo-db';
8
- import { type EchoSchema, FormatEnum, FormatEnums, formatToType } from '@dxos/echo-schema';
9
9
  import { log } from '@dxos/log';
10
10
  import { useAsyncEffect, useTranslation } from '@dxos/react-ui';
11
11
  import {
@@ -13,6 +13,7 @@ import {
13
13
  type ProjectionModel,
14
14
  type PropertyType,
15
15
  type SchemaProperty,
16
+ formatToAdditionalPropertyAttributes,
16
17
  getFormatSchema,
17
18
  getSchemaProperties,
18
19
  sortProperties,
@@ -94,8 +95,10 @@ export const FieldEditor = ({
94
95
 
95
96
  setProps((props) => {
96
97
  const type = formatToType[_props.format as keyof typeof formatToType];
98
+ const additionalProps =
99
+ formatToAdditionalPropertyAttributes[_props.format as keyof typeof formatToAdditionalPropertyAttributes];
97
100
  if (props.type !== type) {
98
- return { ...props, ..._props, type };
101
+ return { ...props, ..._props, ...additionalProps, type } as PropertyType;
99
102
  }
100
103
 
101
104
  return _props as PropertyType;
@@ -2,8 +2,9 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { SchemaAST, pipe } from 'effect';
6
- import { capitalize } from 'effect/String';
5
+ import * as Function from 'effect/Function';
6
+ import * as SchemaAST from 'effect/SchemaAST';
7
+ import * as String from 'effect/String';
7
8
  import React, { Fragment, useCallback } from 'react';
8
9
 
9
10
  import { SimpleType, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
@@ -34,7 +35,7 @@ export const ArrayField = ({ property, readonly, path, inputProps, Custom, looku
34
35
  // It doesn't become live until another field is touched, but that's better than the whole form crashing.
35
36
  const values = (useFormValues(path ?? []) ?? []) as any[];
36
37
  invariant(Array.isArray(values), `Values at path ${path?.join('.')} must be an array.`);
37
- const label = title ?? pipe(name, capitalize);
38
+ const label = title ?? Function.pipe(name, String.capitalize);
38
39
 
39
40
  const elementType = findArrayElementType(ast);
40
41
 
@@ -192,7 +192,7 @@ export const MarkdownInput = ({
192
192
  disabled={!!readonly}
193
193
  placeholder={placeholder}
194
194
  value={getValue() ?? ''}
195
- classNames={'min-bs-auto max-h-40 overflow-auto'}
195
+ classNames={'min-bs-auto max-bs-40 overflow-auto'}
196
196
  onChange={(event) => onValueChange(type, event.target.value)}
197
197
  onBlur={onBlur}
198
198
  style={{ resize: 'none' }}
@@ -2,19 +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
10
  import { type Type } from '@dxos/echo';
13
- import { type BaseObject, Expando, Format, Ref, type TypeAnnotation, getObjectDXN } from '@dxos/echo-schema';
14
- import { live } from '@dxos/live-object';
15
- import { withSurfaceVariantsLayout } from '@dxos/react-ui/testing';
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';
16
15
  import { Testing } from '@dxos/schema/testing';
17
- import { withLayout, withTheme } from '@dxos/storybook-utils';
18
16
 
19
17
  import { translations } from '../../translations';
20
18
  import { TestLayout, TestPanel } from '../testing';
@@ -40,11 +38,13 @@ const DefaultStory = <T extends BaseObject = any>({
40
38
 
41
39
  if (debug) {
42
40
  return (
43
- <TestLayout json={{ values, schema: schema.ast }}>
44
- <TestPanel>
45
- <Form<T> schema={schema} values={values} onSave={handleSave} {...props} />
46
- </TestPanel>
47
- </TestLayout>
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
48
  );
49
49
  }
50
50
 
@@ -54,7 +54,7 @@ const DefaultStory = <T extends BaseObject = any>({
54
54
  const RefStory = <T extends BaseObject = any>(props: StoryProps<T>) => {
55
55
  const onQueryRefOptions = useCallback((typeInfo: TypeAnnotation) => {
56
56
  switch (typeInfo.typename) {
57
- case ContactType.typename:
57
+ case Testing.Person.typename:
58
58
  return [
59
59
  { dxn: getObjectDXN(contact1)!, label: 'John Coltraine' },
60
60
  { dxn: getObjectDXN(contact2)!, label: 'Erykah Badu' },
@@ -67,24 +67,6 @@ const RefStory = <T extends BaseObject = any>(props: StoryProps<T>) => {
67
67
  return <DefaultStory<T> onQueryRefOptions={onQueryRefOptions} {...props} />;
68
68
  };
69
69
 
70
- const meta = {
71
- title: 'ui/react-ui-form/Form',
72
- component: Form as any,
73
- render: DefaultStory,
74
- decorators: [withLayout({ fullscreen: true }), withTheme],
75
- parameters: {
76
- translations,
77
- },
78
- argTypes: {
79
- readonly: {
80
- control: 'boolean',
81
- description: 'Readonly',
82
- },
83
- },
84
- } satisfies Meta<StoryProps<any>>;
85
-
86
- export default meta;
87
-
88
70
  const AddressSchema = Schema.Struct({
89
71
  street: Schema.optional(Schema.String.annotations({ title: 'Street' })),
90
72
  city: Schema.optional(Schema.String.annotations({ title: 'City' })),
@@ -102,10 +84,28 @@ const ContactSchema = Schema.Struct({
102
84
 
103
85
  type ContactSchema = Schema.Schema.Type<typeof ContactSchema>;
104
86
 
105
- type Story = StoryObj<StoryProps<any>>;
87
+ const meta = {
88
+ title: 'ui/react-ui-form/Form',
89
+ component: Form as any,
90
+ render: DefaultStory,
91
+ decorators: [withTheme, withLayoutVariants()],
92
+ parameters: {
93
+ layout: 'fullscreen',
94
+ translations,
95
+ },
96
+ argTypes: {
97
+ readonly: {
98
+ control: 'boolean',
99
+ description: 'Readonly',
100
+ },
101
+ },
102
+ } satisfies Meta<StoryProps<any>>;
103
+
104
+ export default meta;
105
+
106
+ type Story = StoryObj<typeof meta>;
106
107
 
107
108
  export const Default: Story = {
108
- decorators: [withSurfaceVariantsLayout(), withTheme],
109
109
  args: {
110
110
  schema: ContactSchema,
111
111
  values: {
@@ -147,10 +147,10 @@ export const OrganizationAutoSave: Story = {
147
147
  // TODO(burdon): Type issue with employer reference.
148
148
  // TODO(burdon): Test table/form with compound values (e.g., address).
149
149
  // Property name is missing in type EncodedReference but required in type
150
- export const Contact: Story = {
150
+ export const Person: Story = {
151
151
  args: {
152
152
  debug: true,
153
- schema: Testing.ContactSchema,
153
+ schema: Testing.Person,
154
154
  values: {
155
155
  name: 'Bot',
156
156
  },
@@ -3,15 +3,15 @@
3
3
  //
4
4
 
5
5
  import { useFocusFinders } from '@fluentui/react-tabster';
6
- import { type Schema } from 'effect';
7
- import React, { type ReactElement, useEffect, useMemo, useRef } from 'react';
6
+ import type * as Schema from 'effect/Schema';
7
+ import React, { type ReactElement, useCallback, useEffect, useRef } from 'react';
8
8
 
9
- import { type BaseObject, type PropertyKey } from '@dxos/echo-schema';
9
+ import { type BaseObject, type PropertyKey } from '@dxos/echo/internal';
10
10
  import { type ThemedClassName } from '@dxos/react-ui';
11
11
  import { cardDialogOverflow, cardSpacing } from '@dxos/react-ui-stack';
12
12
  import { type ProjectionModel, type SchemaProperty } from '@dxos/schema';
13
13
 
14
- import { type FormOptions } from '../../hooks';
14
+ import { type FormHandler, type FormOptions } from '../../hooks';
15
15
 
16
16
  import { FormActions, type FormOuterSpacing } from './FormActions';
17
17
  import { FormFields, type FormFieldsProps } from './FormContent';
@@ -29,6 +29,7 @@ export type ComponentLookup = (args: {
29
29
  export type CustomInputMap = Partial<Record<string, InputComponent>>;
30
30
 
31
31
  export type FormProps<T extends BaseObject> = ThemedClassName<{
32
+ id?: string;
32
33
  values: Partial<T>;
33
34
  // TODO(burdon): Autofocus first input.
34
35
  autoFocus?: boolean;
@@ -41,10 +42,12 @@ export type FormProps<T extends BaseObject> = ThemedClassName<{
41
42
  onCancel?: () => void;
42
43
  }> &
43
44
  Pick<FormOptions<T>, 'schema' | 'onValuesChanged' | 'onValidate' | 'onSave'> &
45
+ // TODO(wittjosiah): This needs to support different ref field options per field.
44
46
  FormFieldsProps;
45
47
 
46
48
  export const Form = <T extends BaseObject>({
47
49
  classNames,
50
+ id,
48
51
  testId,
49
52
  values: initialValues,
50
53
  autoFocus,
@@ -72,7 +75,14 @@ export const Form = <T extends BaseObject>({
72
75
  }, [autoFocus]);
73
76
 
74
77
  // TODO(burdon): Name?
75
- const handleValid = useMemo(() => (autoSave ? onSave : undefined), [autoSave, onSave]);
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
+ );
76
86
 
77
87
  return (
78
88
  <FormProvider
@@ -85,7 +95,7 @@ export const Form = <T extends BaseObject>({
85
95
  onValid={handleValid}
86
96
  onSave={onSave}
87
97
  >
88
- <div role='none' className='contents' data-testid={testId}>
98
+ <div role='none' className='contents' {...(id && { 'data-object-id': id })} data-testid={testId}>
89
99
  <FormFields
90
100
  {...props}
91
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
  )}
@@ -2,15 +2,17 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, SchemaAST, pipe } from 'effect';
6
- import { capitalize } from 'effect/String';
7
- import React, { forwardRef, useMemo } from 'react';
5
+ import * as Function from 'effect/Function';
6
+ import * as Schema from 'effect/Schema';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
+ import * as StringEffect from 'effect/String';
9
+ import React, { Component, type PropsWithChildren, type ReactNode, forwardRef, useMemo } from 'react';
8
10
 
9
11
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
10
12
  import { type ThemedClassName } from '@dxos/react-ui';
11
13
  import { mx } from '@dxos/react-ui-theme';
12
14
  import { type ProjectionModel, type SchemaProperty, getSchemaProperties } from '@dxos/schema';
13
- import { isNotFalsy } from '@dxos/util';
15
+ import { isTruthy } from '@dxos/util';
14
16
 
15
17
  import { type QueryRefOptions } from '../../hooks';
16
18
  import { getRefProps } from '../../util';
@@ -33,7 +35,13 @@ export type FormFieldProps = {
33
35
  Custom?: Partial<Record<string, InputComponent>>;
34
36
  } & Pick<
35
37
  RefFieldProps,
36
- 'readonly' | 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'
38
+ | 'readonly'
39
+ | 'onQueryRefOptions'
40
+ | 'createOptionLabel'
41
+ | 'createOptionIcon'
42
+ | 'onCreate'
43
+ | 'createSchema'
44
+ | 'createInitialValuePath'
37
45
  >;
38
46
 
39
47
  export const FormField = ({
@@ -45,14 +53,16 @@ export const FormField = ({
45
53
  onQueryRefOptions,
46
54
  createOptionLabel,
47
55
  createOptionIcon,
48
- onCreateFromQuery,
56
+ onCreate,
57
+ createSchema,
58
+ createInitialValuePath,
49
59
  lookupComponent,
50
60
  Custom,
51
61
  }: FormFieldProps) => {
52
62
  const { ast, name, type, format, title, description, options, examples, array } = property;
53
63
  const inputProps = useInputProps(path);
54
64
 
55
- const label = useMemo(() => title ?? pipe(name, capitalize), [title, name]);
65
+ const label = useMemo(() => title ?? Function.pipe(name, StringEffect.capitalize), [title, name]);
56
66
  const placeholder = useMemo(
57
67
  () => (examples?.length ? `Example: "${examples[0]}"` : description),
58
68
  [examples, description],
@@ -114,7 +124,9 @@ export const FormField = ({
114
124
  onQueryRefOptions={onQueryRefOptions}
115
125
  createOptionLabel={createOptionLabel}
116
126
  createOptionIcon={createOptionIcon}
117
- onCreateFromQuery={onCreateFromQuery}
127
+ onCreate={onCreate}
128
+ createSchema={createSchema}
129
+ createInitialValuePath={createInitialValuePath}
118
130
  {...inputProps}
119
131
  />
120
132
  );
@@ -188,7 +200,7 @@ export const FormField = ({
188
200
  onQueryRefOptions={onQueryRefOptions}
189
201
  createOptionLabel={createOptionLabel}
190
202
  createOptionIcon={createOptionIcon}
191
- onCreateFromQuery={onCreateFromQuery}
203
+ onCreate={onCreate}
192
204
  Custom={Custom}
193
205
  lookupComponent={lookupComponent}
194
206
  />
@@ -222,7 +234,15 @@ export type FormFieldsProps = ThemedClassName<
222
234
  Custom?: Partial<Record<string, InputComponent>>;
223
235
  onQueryRefOptions?: QueryRefOptions;
224
236
  } & Pick<FormFieldProps, 'readonly'> &
225
- Pick<RefFieldProps, 'onQueryRefOptions' | 'createOptionLabel' | 'createOptionIcon' | 'onCreateFromQuery'>
237
+ Pick<
238
+ RefFieldProps,
239
+ | 'onQueryRefOptions'
240
+ | 'createOptionLabel'
241
+ | 'createOptionIcon'
242
+ | 'onCreate'
243
+ | 'createSchema'
244
+ | 'createInitialValuePath'
245
+ >
226
246
  >;
227
247
 
228
248
  export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
@@ -282,21 +302,61 @@ export const FormFields = forwardRef<HTMLDivElement, FormFieldsProps>(
282
302
  {properties
283
303
  .map((property) => {
284
304
  return (
285
- <FormField
286
- key={property.name}
287
- property={property}
288
- path={[...(path ?? []), property.name]}
289
- readonly={readonly}
290
- projection={projection}
291
- onQueryRefOptions={onQueryRefOptions}
292
- lookupComponent={lookupComponent}
293
- Custom={Custom}
294
- {...props}
295
- />
305
+ <ErrorBoundary key={property.name} path={[...(path ?? []), property.name]}>
306
+ <FormField
307
+ property={property}
308
+ path={[...(path ?? []), property.name]}
309
+ readonly={readonly}
310
+ projection={projection}
311
+ onQueryRefOptions={onQueryRefOptions}
312
+ lookupComponent={lookupComponent}
313
+ Custom={Custom}
314
+ {...props}
315
+ />
316
+ </ErrorBoundary>
296
317
  );
297
318
  })
298
- .filter(isNotFalsy)}
319
+ .filter(isTruthy)}
299
320
  </div>
300
321
  );
301
322
  },
302
323
  );
324
+
325
+ type ErrorState = {
326
+ error: Error | undefined;
327
+ };
328
+
329
+ type ErrorBoundaryProps = PropsWithChildren<{
330
+ path?: (string | number)[];
331
+ }>;
332
+
333
+ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorState> {
334
+ static getDerivedStateFromError(error: Error): { error: Error } {
335
+ return { error };
336
+ }
337
+
338
+ override state = { error: undefined };
339
+
340
+ override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
341
+ if (prevProps.path !== this.props.path) {
342
+ this.resetError();
343
+ }
344
+ }
345
+
346
+ override render(): ReactNode {
347
+ if (this.state.error) {
348
+ return (
349
+ <div className='flex gap-2 border border-roseFill font-mono text-sm'>
350
+ <span className='bg-roseFill text-surfaceText pli-1 font-thin'>ERROR</span>
351
+ {String(this.props.path?.join('.'))}
352
+ </div>
353
+ );
354
+ }
355
+
356
+ return this.props.children;
357
+ }
358
+
359
+ private resetError(): void {
360
+ this.setState({ error: undefined });
361
+ }
362
+ }
@@ -14,7 +14,7 @@ import React, {
14
14
  } from 'react';
15
15
 
16
16
  import { raise } from '@dxos/debug';
17
- import { type BaseObject, getValue } from '@dxos/echo-schema';
17
+ import { type BaseObject, getValue } from '@dxos/echo/internal';
18
18
  import { type SimpleType, createJsonPath } from '@dxos/effect';
19
19
 
20
20
  import { type FormHandler, type FormOptions, useForm } from '../../hooks';
@@ -62,7 +62,7 @@ export const FormProvider = ({
62
62
  ...formOptions
63
63
  }: PropsWithChildren<
64
64
  FormOptions<any> & {
65
- formRef?: RefObject<HTMLDivElement>;
65
+ formRef?: RefObject<HTMLDivElement | null>;
66
66
  autoSave?: boolean;
67
67
  }
68
68
  >) => {
@@ -92,7 +92,7 @@ export const FormProvider = ({
92
92
  }
93
93
  }
94
94
  },
95
- [form, autoSave],
95
+ [form.canSave, form.formIsValid, form.handleSave, autoSave],
96
96
  );
97
97
 
98
98
  useEffect(() => {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { type FC } from 'react';
6
6
 
7
- import { type FormatEnum } from '@dxos/echo-schema';
7
+ import { type FormatEnum } from '@dxos/echo/internal';
8
8
  import { type SimpleType } from '@dxos/effect';
9
9
  import { Icon, Input, Tooltip } from '@dxos/react-ui';
10
10
  import { labelSpacing } from '@dxos/react-ui-stack';