@dxos/react-ui-form 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (99) hide show
  1. package/README.md +0 -1
  2. package/dist/lib/browser/index.mjs +620 -583
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +620 -583
  6. package/dist/lib/node-esm/index.mjs.map +4 -4
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +3 -3
  9. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  10. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +61 -61
  11. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/Form/Form.d.ts +18 -15
  13. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.stories.d.ts +67 -67
  15. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormField.d.ts +7 -1
  17. package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormFieldComponent.d.ts +0 -1
  19. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
  21. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormFieldSet.d.ts +9 -10
  23. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/fields/RefField.d.ts +7 -5
  26. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +61 -61
  29. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +10 -2
  30. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
  31. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +61 -61
  32. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/Settings/Settings.d.ts +39 -0
  34. package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
  35. package/dist/types/src/components/Settings/Settings.stories.d.ts +81 -0
  36. package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/Settings/index.d.ts +2 -0
  38. package/dist/types/src/components/Settings/index.d.ts.map +1 -0
  39. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +5 -5
  40. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +65 -64
  42. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/index.d.ts +1 -1
  44. package/dist/types/src/components/index.d.ts.map +1 -1
  45. package/dist/types/src/components/testing.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
  47. package/dist/types/src/translations.d.ts +61 -61
  48. package/dist/types/src/translations.d.ts.map +1 -1
  49. package/dist/types/src/util/index.d.ts +0 -1
  50. package/dist/types/src/util/index.d.ts.map +1 -1
  51. package/dist/types/src/util/refs.d.ts +1 -0
  52. package/dist/types/src/util/refs.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +36 -38
  55. package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -6
  56. package/src/components/FieldEditor/FieldEditor.tsx +3 -4
  57. package/src/components/Form/AUDIT.md +17 -0
  58. package/src/components/Form/Form.stories.tsx +23 -24
  59. package/src/components/Form/Form.tsx +58 -65
  60. package/src/components/Form/FormField.tsx +27 -8
  61. package/src/components/Form/FormFieldComponent.stories.tsx +5 -5
  62. package/src/components/Form/FormFieldComponent.tsx +17 -20
  63. package/src/components/Form/FormFieldSet.tsx +108 -104
  64. package/src/components/Form/fields/ArrayField.tsx +11 -8
  65. package/src/components/Form/fields/GeoPointField.tsx +6 -6
  66. package/src/components/Form/fields/MarkdownField.tsx +1 -1
  67. package/src/components/Form/fields/RefField.tsx +35 -17
  68. package/src/components/Form/fields/SelectField.tsx +1 -1
  69. package/src/components/Form/fields/SelectOptionField.stories.tsx +2 -2
  70. package/src/components/Form/fields/SelectOptionField.tsx +9 -10
  71. package/src/components/Form/fields/TupleField.tsx +1 -1
  72. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +10 -14
  73. package/src/components/ObjectPicker/ObjectPicker.tsx +71 -60
  74. package/src/components/Settings/Settings.stories.tsx +56 -0
  75. package/src/components/Settings/Settings.tsx +129 -0
  76. package/src/components/{ControlSection → Settings}/index.ts +1 -1
  77. package/src/components/ViewEditor/ViewEditor.stories.tsx +22 -20
  78. package/src/components/ViewEditor/ViewEditor.test.tsx +3 -6
  79. package/src/components/ViewEditor/ViewEditor.tsx +99 -70
  80. package/src/components/index.ts +1 -1
  81. package/src/components/testing.tsx +20 -8
  82. package/src/hooks/useFormHandler.ts +42 -24
  83. package/src/translations.ts +62 -63
  84. package/src/util/index.ts +0 -1
  85. package/src/util/refs.ts +7 -3
  86. package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
  87. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
  88. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -87
  89. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
  90. package/dist/types/src/components/ControlSection/index.d.ts +0 -2
  91. package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
  92. package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
  93. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
  94. package/dist/types/src/util/echo.d.ts +0 -6
  95. package/dist/types/src/util/echo.d.ts.map +0 -1
  96. package/src/components/ControlSection/ControlSection.stories.tsx +0 -38
  97. package/src/components/ControlSection/ControlSection.tsx +0 -153
  98. package/src/components/Form/Form-old.stories.tsx +0 -314
  99. package/src/util/echo.ts +0 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-form",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.bcb3aa67d6",
4
4
  "description": "Data components.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -30,58 +30,56 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
33
- "@effect-atom/atom-react": "^0.4.6",
33
+ "@effect-atom/atom-react": "^0.5.0",
34
34
  "@fluentui/react-tabster": "9.26.11",
35
35
  "@radix-ui/react-context": "1.1.1",
36
36
  "@radix-ui/react-use-controllable-state": "1.1.0",
37
- "date-fns": "^3.3.1",
38
- "jsonpath": "^1.1.1",
37
+ "date-fns": "^3.6.0",
39
38
  "tabster": "^8.5.5",
40
- "@dxos/async": "0.8.4-main.bc674ce",
41
- "@dxos/debug": "0.8.4-main.bc674ce",
42
- "@dxos/echo": "0.8.4-main.bc674ce",
43
- "@dxos/echo-react": "0.8.4-main.bc674ce",
44
- "@dxos/effect": "0.8.4-main.bc674ce",
45
- "@dxos/invariant": "0.8.4-main.bc674ce",
46
- "@dxos/keys": "0.8.4-main.bc674ce",
47
- "@dxos/echo-query": "0.8.4-main.bc674ce",
48
- "@dxos/lit-ui": "0.8.4-main.bc674ce",
49
- "@dxos/log": "0.8.4-main.bc674ce",
50
- "@dxos/react-client": "0.8.4-main.bc674ce",
51
- "@dxos/react-ui-components": "0.8.4-main.bc674ce",
52
- "@dxos/react-ui-editor": "0.8.4-main.bc674ce",
53
- "@dxos/react-ui-mosaic": "0.8.4-main.bc674ce",
54
- "@dxos/react-ui-pickers": "0.8.4-main.bc674ce",
55
- "@dxos/react-ui-list": "0.8.4-main.bc674ce",
56
- "@dxos/schema": "0.8.4-main.bc674ce",
57
- "@dxos/react-ui-searchlist": "0.8.4-main.bc674ce",
58
- "@dxos/react-ui-stack": "0.8.4-main.bc674ce",
59
- "@dxos/types": "0.8.4-main.bc674ce",
60
- "@dxos/ui-types": "0.8.4-main.bc674ce",
61
- "@dxos/util": "0.8.4-main.bc674ce"
39
+ "@dxos/async": "0.8.4-main.bcb3aa67d6",
40
+ "@dxos/debug": "0.8.4-main.bcb3aa67d6",
41
+ "@dxos/echo": "0.8.4-main.bcb3aa67d6",
42
+ "@dxos/effect": "0.8.4-main.bcb3aa67d6",
43
+ "@dxos/echo-query": "0.8.4-main.bcb3aa67d6",
44
+ "@dxos/invariant": "0.8.4-main.bcb3aa67d6",
45
+ "@dxos/lit-ui": "0.8.4-main.bcb3aa67d6",
46
+ "@dxos/keys": "0.8.4-main.bcb3aa67d6",
47
+ "@dxos/echo-react": "0.8.4-main.bcb3aa67d6",
48
+ "@dxos/log": "0.8.4-main.bcb3aa67d6",
49
+ "@dxos/react-client": "0.8.4-main.bcb3aa67d6",
50
+ "@dxos/react-ui-components": "0.8.4-main.bcb3aa67d6",
51
+ "@dxos/react-ui-editor": "0.8.4-main.bcb3aa67d6",
52
+ "@dxos/react-ui-list": "0.8.4-main.bcb3aa67d6",
53
+ "@dxos/react-ui-pickers": "0.8.4-main.bcb3aa67d6",
54
+ "@dxos/react-ui-mosaic": "0.8.4-main.bcb3aa67d6",
55
+ "@dxos/react-ui-search": "0.8.4-main.bcb3aa67d6",
56
+ "@dxos/react-ui-stack": "0.8.4-main.bcb3aa67d6",
57
+ "@dxos/schema": "0.8.4-main.bcb3aa67d6",
58
+ "@dxos/ui-types": "0.8.4-main.bcb3aa67d6",
59
+ "@dxos/types": "0.8.4-main.bcb3aa67d6",
60
+ "@dxos/util": "0.8.4-main.bcb3aa67d6"
62
61
  },
63
62
  "devDependencies": {
64
- "@types/jsonpath": "^0.2.4",
65
63
  "@types/react": "~19.2.7",
66
64
  "@types/react-dom": "~19.2.3",
67
- "effect": "3.19.11",
65
+ "effect": "3.20.0",
68
66
  "fast-check": "^3.19.0",
69
67
  "react": "~19.2.3",
70
68
  "react-dom": "~19.2.3",
71
- "vite": "7.1.9",
72
- "@dxos/react-client": "0.8.4-main.bc674ce",
73
- "@dxos/react-ui": "0.8.4-main.bc674ce",
74
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.bc674ce",
75
- "@dxos/random": "0.8.4-main.bc674ce",
76
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
77
- "@dxos/storybook-utils": "0.8.4-main.bc674ce"
69
+ "vite": "^7.1.11",
70
+ "@dxos/react-client": "0.8.4-main.bcb3aa67d6",
71
+ "@dxos/random": "0.8.4-main.bcb3aa67d6",
72
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
73
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.bcb3aa67d6",
74
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
75
+ "@dxos/storybook-utils": "0.8.4-main.bcb3aa67d6"
78
76
  },
79
77
  "peerDependencies": {
80
- "effect": "3.19.11",
78
+ "effect": "3.20.0",
81
79
  "react": "~19.2.3",
82
80
  "react-dom": "~19.2.3",
83
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
84
- "@dxos/react-ui": "0.8.4-main.bc674ce"
81
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
82
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6"
85
83
  },
86
84
  "publishConfig": {
87
85
  "access": "public"
@@ -8,8 +8,8 @@ import React, { useEffect, useMemo } from 'react';
8
8
  import { Filter, Query } from '@dxos/echo';
9
9
  import { createEchoSchema } from '@dxos/echo/testing';
10
10
  import { log } from '@dxos/log';
11
- import { withTheme } from '@dxos/react-ui/testing';
12
- import { ProjectionModel, View, createEchoChangeCallback } from '@dxos/schema';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+ import { ProjectionModel, ViewModel, createEchoChangeCallback } from '@dxos/schema';
13
13
  import { Example } from '@dxos/schema/testing';
14
14
 
15
15
  import { translations } from '../../translations';
@@ -17,17 +17,15 @@ import { FIELD_EDITOR_DEBUG_SYMBOL, TestLayout } from '../testing';
17
17
 
18
18
  import { FieldEditor, type FieldEditorProps } from './FieldEditor';
19
19
 
20
- // Type definition for debug objects exposed to tests.
21
20
  export type FieldEditorDebugObjects = {
22
21
  props: FieldEditorProps;
23
22
  projection: ProjectionModel;
24
23
  };
25
24
 
26
- // TODO(wittjosiah): ECHO objects don't work when passed via Storybook args.
27
25
  const useTestProjection = () => {
28
26
  return useMemo(() => {
29
27
  const schema = createEchoSchema(Example);
30
- const view = View.make({
28
+ const view = ViewModel.make({
31
29
  name: 'Test',
32
30
  query: Query.select(Filter.type(Example)),
33
31
  jsonSchema: schema.jsonSchema,
@@ -82,7 +80,7 @@ const meta = {
82
80
  title: 'ui/react-ui-form/FieldEditor',
83
81
  component: FieldEditor as any,
84
82
  render: DefaultStory,
85
- decorators: [withTheme],
83
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
86
84
  parameters: {
87
85
  layout: 'fullscreen',
88
86
  translations,
@@ -4,13 +4,12 @@
4
4
 
5
5
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
6
 
7
- import { Obj, type SchemaRegistry } from '@dxos/echo';
7
+ import { Obj, type SchemaRegistry, type View } from '@dxos/echo';
8
8
  import { type EchoSchema, Format, FormatEnums, formatToType } from '@dxos/echo/internal';
9
9
  import { type SchemaProperty } from '@dxos/effect';
10
10
  import { log } from '@dxos/log';
11
11
  import { useAsyncEffect, useTranslation } from '@dxos/react-ui';
12
12
  import {
13
- type FieldType,
14
13
  type ProjectionModel,
15
14
  type PropertyType,
16
15
  formatToAdditionalPropertyAttributes,
@@ -23,7 +22,7 @@ import { Form, type FormFieldMap, type FormRootProps, SelectField, SelectOptionF
23
22
 
24
23
  export type FieldEditorProps = {
25
24
  projection: ProjectionModel;
26
- field: FieldType;
25
+ field: View.FieldType;
27
26
  registry?: SchemaRegistry.SchemaRegistry;
28
27
  view?: Obj.Unknown;
29
28
  onSave: () => void;
@@ -69,7 +68,7 @@ export const FieldEditor = ({ readonly, projection, field, registry, view, onSav
69
68
  {...props}
70
69
  options={FormatEnums.filter((value) => value !== Format.TypeFormat.None).map((value) => ({
71
70
  value,
72
- label: t(`format ${value}`),
71
+ label: t(`format.${value}.label`),
73
72
  }))}
74
73
  />
75
74
  ),
@@ -0,0 +1,17 @@
1
+ # Form
2
+
3
+ ## New features/polish
4
+
5
+ - [x] Unify readonly/inline modes.
6
+ - [x] Refs
7
+ - [x] Single-select (fix popover)
8
+ - [x] Multi-select (array)
9
+ - [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged).
10
+ - [ ] Don't call save/autoSave if value hasn't changed.
11
+ - [ ] Fix onCancel (restore values).
12
+ - [ ] Fix useSchema Type.AnyObj cast.
13
+ - [ ] TableCellEditor (handleEnter/ModalController).
14
+ - [ ] Use FormFieldWrapper uniformly
15
+ - [ ] Inline tables.
16
+ - [ ] Defer query until popover.
17
+ - [ ] Omit id from sub properties (tune Omit)
@@ -6,13 +6,13 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback, useState } from 'react';
8
8
 
9
- import { Annotation, Format, Obj, Tag, Type } from '@dxos/echo';
9
+ import { Annotation, Format, Obj, Ref, Tag, Type } from '@dxos/echo';
10
10
  import { type AnyProperties } from '@dxos/echo/internal';
11
11
  import { log } from '@dxos/log';
12
- import { useClient } from '@dxos/react-client';
12
+ import { useSpaces } from '@dxos/react-client/echo';
13
13
  import { withClientProvider } from '@dxos/react-client/testing';
14
14
  import { Tooltip } from '@dxos/react-ui';
15
- import { withTheme } from '@dxos/react-ui/testing';
15
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
16
16
 
17
17
  import { translations } from '../../translations';
18
18
  import { TestLayout } from '../testing';
@@ -23,7 +23,7 @@ const Organization = Schema.Struct({
23
23
  name: Schema.String.pipe(Schema.minLength(1)).annotations({ title: 'Full name' }),
24
24
  }).pipe(
25
25
  Type.object({
26
- typename: 'example.com/type/Organization', // TODO(burdon): Change all types to /schema
26
+ typename: 'com.example.type.organization',
27
27
  version: '0.1.0',
28
28
  }),
29
29
  );
@@ -46,8 +46,8 @@ const Person = Schema.Struct({
46
46
  zip: Schema.Number,
47
47
  }).annotations({ title: 'Address' }),
48
48
  ),
49
- employer: Schema.optional(Type.Ref(Organization).annotations({ title: 'Employer' })),
50
- tags: Schema.optional(Schema.Array(Type.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
49
+ employer: Schema.optional(Ref.Ref(Organization).annotations({ title: 'Employer' })),
50
+ tags: Schema.optional(Schema.Array(Ref.Ref(Tag.Tag)).annotations({ title: 'Tags' })),
51
51
  status: Schema.optional(Schema.Literal('active', 'inactive').annotations({ title: 'Status' })),
52
52
  notes: Schema.optional(Format.Text.annotations({ title: 'Notes' })),
53
53
  location: Schema.optional(Format.GeoPoint.annotations({ title: 'Location' })),
@@ -65,14 +65,14 @@ const Person = Schema.Struct({
65
65
  ),
66
66
  }).pipe(
67
67
  Type.object({
68
- typename: 'dxos.org/type/Person', // TODO(burdon): Change all types to /schema
68
+ typename: 'org.dxos.type.person', // TODO(burdon): Change all types to /schema
69
69
  version: '0.1.0',
70
70
  }),
71
71
  );
72
72
 
73
73
  export interface Person extends Schema.Schema.Type<typeof Person> {}
74
74
 
75
- type StoryProps<T extends AnyProperties> = {
75
+ type DefaultStoryProps<T extends AnyProperties> = {
76
76
  debug?: boolean;
77
77
  schema: Schema.Schema<T>;
78
78
  } & FormRootProps<T>;
@@ -82,10 +82,10 @@ const DefaultStory = <T extends AnyProperties = AnyProperties>({
82
82
  schema,
83
83
  values: valuesProp,
84
84
  ...props
85
- }: StoryProps<T>) => {
85
+ }: DefaultStoryProps<T>) => {
86
86
  const [values, setValues] = useState<Partial<T>>(valuesProp ?? {});
87
- const client = useClient();
88
- const space = client.spaces.default;
87
+ const spaces = useSpaces();
88
+ const space = spaces[0];
89
89
 
90
90
  const handleSave = useCallback<NonNullable<FormRootProps<T>['onSave']>>((values) => {
91
91
  log.info('save', { values, meta });
@@ -97,13 +97,17 @@ const DefaultStory = <T extends AnyProperties = AnyProperties>({
97
97
  setValues(valuesProp ?? {});
98
98
  }, []);
99
99
 
100
+ if (!space) {
101
+ return <></>;
102
+ }
103
+
100
104
  return (
101
105
  <Tooltip.Provider>
102
106
  <TestLayout json={{ values, schema: schema.ast }}>
103
107
  <Form.Root
104
108
  debug={debug}
105
109
  schema={schema}
106
- values={values}
110
+ defaultValues={values}
107
111
  db={space.db}
108
112
  onSave={handleSave}
109
113
  onCancel={handleCancel}
@@ -127,21 +131,16 @@ const meta = {
127
131
  render: DefaultStory,
128
132
 
129
133
  decorators: [
130
- withTheme,
134
+ withTheme(),
135
+ withLayout({ layout: 'fullscreen' }),
131
136
  withClientProvider({
132
137
  createIdentity: true,
133
138
  createSpace: true,
134
139
  types: [Tag.Tag, Organization, Person],
135
- onCreateIdentity: ({ client }) => {
136
- const space = client.spaces.default;
140
+ onCreateSpace: ({ space }) => {
137
141
  [
138
- Obj.make(Organization, { name: 'DXOS' }),
139
- Obj.make(Organization, { name: 'BlueYard' }),
140
- Obj.make(Organization, { name: 'Backed' }),
141
- Obj.make(Organization, { name: 'BCV' }),
142
- Tag.make({ label: 'Tag 1' }),
143
- Tag.make({ label: 'Tag 2' }),
144
- Tag.make({ label: 'Tag 3' }),
142
+ ...Array.from({ length: 3 }).map((_, i) => Obj.make(Tag.Tag, { label: `Tag ${i}` })),
143
+ ...Array.from({ length: 50 }).map((_, i) => Obj.make(Organization, { name: `Organization ${i}` })),
145
144
  ].map((obj) => space.db.add(obj));
146
145
  },
147
146
  }),
@@ -150,11 +149,11 @@ const meta = {
150
149
  layout: 'fullscreen',
151
150
  translations,
152
151
  },
153
- } satisfies Meta<StoryProps<any>>;
152
+ } satisfies Meta<DefaultStoryProps<any>>;
154
153
 
155
154
  export default meta;
156
155
 
157
- type Story<T extends AnyProperties> = StoryObj<StoryProps<T>>;
156
+ type Story<T extends AnyProperties> = StoryObj<DefaultStoryProps<T>>;
158
157
 
159
158
  const values: Partial<Person> = {
160
159
  name: 'Alice',
@@ -4,13 +4,13 @@
4
4
 
5
5
  import { createContext } from '@radix-ui/react-context';
6
6
  import * as Schema from 'effect/Schema';
7
- import type * as SchemaAST from 'effect/SchemaAST';
7
+ import * as SchemaAST from 'effect/SchemaAST';
8
8
  import React, { type PropsWithChildren, useEffect, useMemo, useRef } from 'react';
9
9
 
10
10
  import { type AnyProperties } from '@dxos/echo/internal';
11
11
  import { createJsonPath, getValue as getValue$ } from '@dxos/effect';
12
12
  import { IconButton, type IconButtonProps, ScrollArea, type ThemedClassName, useTranslation } from '@dxos/react-ui';
13
- import { mx } from '@dxos/ui-theme';
13
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
14
14
 
15
15
  import {
16
16
  type FormHandler,
@@ -20,7 +20,6 @@ import {
20
20
  useKeyHandler,
21
21
  } from '../../hooks';
22
22
  import { translationKey } from '../../translations';
23
- import { setValueEchoAware } from '../../util';
24
23
 
25
24
  import { FormFieldLabel, type FormFieldLabelProps, type FormFieldStateProps } from './FormFieldComponent';
26
25
  import {
@@ -28,22 +27,6 @@ import {
28
27
  type FormFieldSetProps as NaturalFormFieldSetProps,
29
28
  } from './FormFieldSet';
30
29
 
31
- // New features/polish
32
- // [x] Unify readonly/inline modes
33
- // [ ] Don't call save/autoSave if value hasn't changed
34
- // [ ] Fix onCancel (restore values)
35
- // [ ] Fix useSchema Type.Obj.Any cast
36
- // [ ] Remove @dxos/echo-db deps
37
- // [ ] TableCellEditor (handleEnter/ModalController).
38
- // [ ] Use FormFieldWrapper uniformly
39
- // [ ] Inline tables for object arrays
40
- // [ ] Defer query until popover
41
- // [ ] Omit id from sub properties.
42
- // [x] Refs
43
- // [x] Single-select (fix popover)
44
- // [x] Multi-select (array)
45
- // [ ] auto save doesn't work for combobox + select due to only firing on blur (workaround is to use onValuesChanged)
46
-
47
30
  // TODO(burdon): Move to @dxos/schema (re-export here).
48
31
  export type ExcludeId<S extends Schema.Schema.AnyNoContext> = Omit<Schema.Schema.Type<S>, 'id'>;
49
32
 
@@ -70,7 +53,10 @@ type FormContextValue<T extends AnyProperties = any> = {
70
53
  * Testing.
71
54
  */
72
55
  testId?: string;
73
- } & Pick<NaturalFormFieldSetProps<T>, 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider'>;
56
+ } & Pick<
57
+ NaturalFormFieldSetProps<T>,
58
+ 'readonly' | 'layout' | 'fieldMap' | 'fieldProvider' | 'projection' | 'createTypename' | 'createFieldMap'
59
+ >;
74
60
 
75
61
  const [FormContextProvider, useFormContext] = createContext<FormContextValue>('Form');
76
62
 
@@ -84,19 +70,25 @@ const useFormValues: {
84
70
  path: (string | number)[] | undefined,
85
71
  defaultValue: () => T,
86
72
  ): T | undefined;
87
- } = (componentName: string, path: (string | number)[] = [], defaultValue?: () => any) => {
88
- const jsonPath = createJsonPath(path);
73
+ } = (componentName: string, path?: (string | number)[], defaultValue?: () => any) => {
74
+ const stablePath = useMemo(() => path ?? [], [path ? path.join('.') : undefined]);
75
+ const jsonPath = createJsonPath(stablePath);
89
76
  const {
90
- form: { values },
77
+ form: { values, onValueChange },
91
78
  } = useFormContext(componentName);
92
79
 
93
80
  const value = getValue$(values, jsonPath);
94
81
 
82
+ // Apply default value once when the field has no value. lastAppliedPathRef prevents
83
+ // re-applying on every render (e.g. when defaultValue() returns null) and ensures
84
+ // we apply per path when the hook is used for different fields.
85
+ const lastAppliedPathRef = useRef<string | null>(null);
95
86
  useEffect(() => {
96
- if (!value && defaultValue) {
97
- setValueEchoAware(values, jsonPath, defaultValue());
87
+ if (value == null && defaultValue && lastAppliedPathRef.current !== jsonPath) {
88
+ lastAppliedPathRef.current = jsonPath;
89
+ onValueChange(stablePath, SchemaAST.stringKeyword, defaultValue());
98
90
  }
99
- }, [value, defaultValue]);
91
+ }, [value, defaultValue, onValueChange, stablePath, jsonPath]);
100
92
 
101
93
  return value;
102
94
  };
@@ -164,65 +156,64 @@ FormRoot.displayName = 'Form.Root';
164
156
  // Viewport
165
157
  //
166
158
 
167
- type FormViewportProps = ThemedClassName<PropsWithChildren<{}>>;
159
+ const FORM_VIEWPORT_NAME = 'Form.Viewport';
160
+
161
+ type FormViewportProps = {};
168
162
 
169
- const FormViewport = ({ classNames, children }: FormViewportProps) => {
163
+ const FormViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
170
164
  return (
171
- <ScrollArea.Root>
172
- <ScrollArea.Viewport classNames={['plb-cardSpacingBlock', classNames]}>{children}</ScrollArea.Viewport>
173
- <ScrollArea.Scrollbar orientation='vertical'>
174
- <ScrollArea.Thumb />
175
- </ScrollArea.Scrollbar>
165
+ <ScrollArea.Root {...composableProps(props)} orientation='vertical' centered padding thin ref={forwardedRef}>
166
+ <ScrollArea.Viewport>{children}</ScrollArea.Viewport>
176
167
  </ScrollArea.Root>
177
168
  );
178
- };
169
+ });
179
170
 
180
- FormViewport.displayName = 'Form.Viewport';
171
+ FormViewport.displayName = FORM_VIEWPORT_NAME;
181
172
 
182
173
  //
183
174
  // Content
184
175
  //
185
176
 
177
+ const FORM_CONTENT_NAME = 'Form.Content';
178
+
186
179
  type FormContentProps = ThemedClassName<PropsWithChildren<{}>>;
187
180
 
188
- // TOOD(burdon): Figure out nesting (indent and testId).
189
181
  const FormContent = ({ classNames, children }: FormContentProps) => {
190
- const { form, testId } = useFormContext(FormContent.displayName);
182
+ const { form, testId } = useFormContext(FORM_CONTENT_NAME);
191
183
  const ref = useRef<HTMLDivElement>(null);
192
184
  useKeyHandler(ref.current, form);
193
185
 
194
186
  return (
195
- <div
196
- ref={ref}
197
- role='form'
198
- className={mx('flex flex-col is-full pli-cardSpacingInline density-fine', classNames)}
199
- data-testid={testId}
200
- >
187
+ <div ref={ref} role='form' className={mx('flex flex-col w-full', classNames)} data-testid={testId}>
201
188
  {children}
202
189
  </div>
203
190
  );
204
191
  };
205
192
 
206
- FormContent.displayName = 'Form.Content';
193
+ FormContent.displayName = FORM_CONTENT_NAME;
207
194
 
208
195
  //
209
196
  // FieldSet
210
197
  //
211
198
 
212
- type FormFieldSetProps = ThemedClassName<{}>;
199
+ const FORM_FIELDSET_NAME = 'Form.FieldSet';
200
+
201
+ type FormFieldSetProps = ThemedClassName<NaturalFormFieldSetProps<any>>;
213
202
 
214
- const FormFieldSet = ({ classNames }: FormFieldSetProps) => {
215
- const { form, ...props } = useFormContext(FormFieldSet.displayName);
203
+ const FormFieldSet = (props: FormFieldSetProps) => {
204
+ const { form, ...contextProps } = useFormContext(FORM_FIELDSET_NAME);
216
205
 
217
- return <NaturalFormFieldSet classNames={classNames} schema={form.schema} {...props} />;
206
+ return <NaturalFormFieldSet schema={form.schema} {...contextProps} {...props} />;
218
207
  };
219
208
 
220
- FormFieldSet.displayName = 'Form.FieldSet';
209
+ FormFieldSet.displayName = FORM_FIELDSET_NAME;
221
210
 
222
211
  //
223
212
  // Actions
224
213
  //
225
214
 
215
+ const FORM_ACTIONS_NAME = 'Form.Actions';
216
+
226
217
  type FormActionsProps = ThemedClassName<{}>;
227
218
 
228
219
  const FormActions = ({ classNames }: FormActionsProps) => {
@@ -231,22 +222,21 @@ const FormActions = ({ classNames }: FormActionsProps) => {
231
222
  form: { canSave, onSave, onCancel },
232
223
  readonly,
233
224
  layout,
234
- } = useFormContext(FormActions.displayName);
225
+ } = useFormContext(FORM_ACTIONS_NAME);
235
226
 
236
227
  if (readonly || layout === 'static') {
237
228
  return null;
238
229
  }
239
-
240
230
  // TODO(burdon): Currently onCancel is a no-op; implement "revert values".
241
231
  // Deprecate FormSubmit ans use FormActions without Cancel button if no callback is supplied.
242
232
 
243
233
  return (
244
- <div role='none' className={mx('grid grid-flow-col auto-cols-fr gap-2 pbs-cardSpacingBlock', classNames)}>
234
+ <div role='none' className={mx('grid grid-flow-col gap-form-gap auto-cols-fr py-form-padding', classNames)}>
245
235
  {onCancel && (
246
236
  <IconButton
247
237
  icon='ph--x--regular'
248
238
  iconEnd
249
- label={t('cancel button label')}
239
+ label={t('cancel-button.label')}
250
240
  onClick={onCancel}
251
241
  data-testid='cancel-button'
252
242
  />
@@ -258,7 +248,7 @@ const FormActions = ({ classNames }: FormActionsProps) => {
258
248
  disabled={!canSave}
259
249
  icon='ph--check--regular'
260
250
  iconEnd
261
- label={t('save button label')}
251
+ label={t('save-button.label')}
262
252
  onClick={onSave}
263
253
  data-testid='save-button'
264
254
  />
@@ -267,35 +257,37 @@ const FormActions = ({ classNames }: FormActionsProps) => {
267
257
  );
268
258
  };
269
259
 
270
- FormActions.displayName = 'Form.Actions';
260
+ FormActions.displayName = FORM_ACTIONS_NAME;
271
261
 
272
262
  //
273
263
  // Submit
274
264
  //
275
265
 
276
- type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label'>>>;
266
+ const FORM_SUBMIT_NAME = 'Form.Submit';
267
+
268
+ type FormSubmitProps = ThemedClassName<Partial<Pick<IconButtonProps, 'icon' | 'label' | 'disabled'>>>;
277
269
 
278
- const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
270
+ const FormSubmit = ({ classNames, label, icon, disabled }: FormSubmitProps) => {
279
271
  const { t } = useTranslation(translationKey);
280
272
  const {
281
273
  form: { canSave, onSave },
282
274
  readonly,
283
275
  layout,
284
- } = useFormContext(FormSubmit.displayName);
276
+ } = useFormContext(FORM_SUBMIT_NAME);
285
277
 
286
278
  if (readonly || layout === 'static') {
287
279
  return null;
288
280
  }
289
281
 
290
282
  return (
291
- <div role='none' className={mx('flex is-full pbs-cardSpacingBlock', classNames)}>
283
+ <div role='none' className={mx('flex w-full pt-form-padding', classNames)}>
292
284
  <IconButton
293
- classNames='is-full'
285
+ classNames='w-full'
294
286
  type='submit'
295
287
  variant='primary'
296
- disabled={!canSave}
288
+ disabled={disabled ?? !canSave}
297
289
  icon={icon ?? 'ph--check--regular'}
298
- label={label ?? t('save button label')}
290
+ label={label ?? t('save-button.label')}
299
291
  onClick={onSave}
300
292
  data-testid='save-button'
301
293
  />
@@ -303,7 +295,7 @@ const FormSubmit = ({ classNames, label, icon }: FormSubmitProps) => {
303
295
  );
304
296
  };
305
297
 
306
- FormSubmit.displayName = 'Form.Submit';
298
+ FormSubmit.displayName = FORM_SUBMIT_NAME;
307
299
 
308
300
  //
309
301
  // Form
@@ -315,9 +307,9 @@ export const Form = {
315
307
  Viewport: FormViewport,
316
308
  Content: FormContent,
317
309
  FieldSet: FormFieldSet,
310
+ Label: FormFieldLabel,
318
311
  Actions: FormActions,
319
312
  Submit: FormSubmit,
320
- Label: FormFieldLabel,
321
313
  };
322
314
 
323
315
  export { useFormContext, useFormValues, useFormFieldState };
@@ -327,6 +319,7 @@ export type {
327
319
  FormViewportProps,
328
320
  FormContentProps,
329
321
  FormFieldSetProps,
322
+ FormFieldLabelProps,
330
323
  FormActionsProps,
331
- FormFieldLabelProps as LabelProps,
324
+ FormSubmitProps,
332
325
  };