@dxos/react-ui-form 0.8.1 → 0.8.2-main.2f9c567

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 (70) hide show
  1. package/dist/lib/browser/index.mjs +338 -149
  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 +326 -138
  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 +338 -149
  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/{ControlSection.d.ts → ControlSection/ControlSection.d.ts} +8 -5
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -0
  12. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +8 -0
  13. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/ControlSection/index.d.ts +2 -0
  15. package/dist/types/src/components/ControlSection/index.d.ts.map +1 -0
  16. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/Form.d.ts +6 -3
  19. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Form.stories.d.ts +37 -34
  21. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/FormContent.d.ts +14 -11
  23. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  25. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/RefField.d.ts +24 -0
  27. package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
  28. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/index.d.ts +1 -0
  32. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  33. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  34. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/deprecated/Form.d.ts +2 -2
  36. package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
  37. package/dist/types/src/components/index.d.ts.map +1 -1
  38. package/dist/types/src/hooks/index.d.ts +1 -0
  39. package/dist/types/src/hooks/index.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useForm.d.ts +2 -2
  41. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  42. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  43. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  44. package/dist/types/src/util/formatting.d.ts.map +1 -1
  45. package/package.json +25 -25
  46. package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
  47. package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +38 -14
  48. package/src/components/ControlSection/index.ts +5 -0
  49. package/src/components/Form/ArrayField.tsx +8 -7
  50. package/src/components/Form/Defaults.tsx +4 -1
  51. package/src/components/Form/Form.stories.tsx +90 -42
  52. package/src/components/Form/Form.tsx +7 -2
  53. package/src/components/Form/FormContent.tsx +62 -19
  54. package/src/components/Form/FormContext.tsx +10 -2
  55. package/src/components/Form/RefField.tsx +152 -0
  56. package/src/components/Form/custom/GeoPointInput.tsx +2 -2
  57. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +5 -4
  58. package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
  59. package/src/components/Form/index.ts +1 -1
  60. package/src/components/ViewEditor/ViewEditor.stories.tsx +3 -2
  61. package/src/components/ViewEditor/ViewEditor.tsx +12 -10
  62. package/src/components/deprecated/Form.stories.tsx +6 -6
  63. package/src/components/deprecated/Form.tsx +6 -6
  64. package/src/components/deprecated/FormInput.tsx +1 -1
  65. package/src/components/index.ts +1 -0
  66. package/src/hooks/index.ts +1 -0
  67. package/src/hooks/useForm.ts +3 -3
  68. package/src/hooks/useRefQueryLookupHandler.ts +43 -0
  69. package/src/util/formatting.ts +15 -3
  70. package/dist/types/src/components/ControlSection.d.ts.map +0 -1
@@ -1,12 +1,11 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { pipe } from 'effect';
5
+ import { Schema, SchemaAST, pipe } from 'effect';
6
6
  import { capitalize } from 'effect/String';
7
7
  import React, { useMemo } from 'react';
8
8
 
9
- import { AST, S } from '@dxos/echo-schema';
10
9
  import { createJsonPath, findNode, getDiscriminatedType, isDiscriminatedUnion } from '@dxos/effect';
11
10
  import { mx } from '@dxos/react-ui-theme';
12
11
  import { getSchemaProperties, type SchemaProperty } from '@dxos/schema';
@@ -17,29 +16,29 @@ import { SelectInput } from './Defaults';
17
16
  import { type ComponentLookup } from './Form';
18
17
  import { useInputProps, useFormValues } from './FormContext';
19
18
  import { type InputComponent } from './Input';
19
+ import { RefField, type QueryRefOptions } from './RefField';
20
20
  import { getInputComponent } from './factory';
21
21
 
22
- export type FormContentProps = {
23
- schema: S.Schema.All;
24
- path?: (string | number)[];
25
- filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
26
- sort?: string[];
27
- readonly?: boolean;
28
- lookupComponent?: ComponentLookup;
29
- Custom?: Partial<Record<string, InputComponent>>;
30
- };
31
-
32
22
  export type FormFieldProps = {
33
23
  property: SchemaProperty<any>;
34
24
  path?: (string | number)[];
35
25
  readonly?: boolean;
36
26
  /** Used to indicate if input should be presented inline (e.g. for array items). */
37
27
  inline?: boolean;
28
+ onQueryRefOptions?: QueryRefOptions;
38
29
  lookupComponent?: ComponentLookup;
39
30
  Custom?: Partial<Record<string, InputComponent>>;
40
31
  };
41
32
 
42
- export const FormField = ({ property, path, readonly, inline, lookupComponent, Custom }: FormFieldProps) => {
33
+ export const FormField = ({
34
+ property,
35
+ path,
36
+ readonly,
37
+ inline,
38
+ onQueryRefOptions,
39
+ lookupComponent,
40
+ Custom,
41
+ }: FormFieldProps) => {
43
42
  const inputProps = useInputProps(path);
44
43
  const { ast, name, type, format, title, description, options, examples, array } = property;
45
44
 
@@ -51,7 +50,7 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
51
50
 
52
51
  const FoundComponent = lookupComponent?.({
53
52
  prop: name,
54
- schema: S.make(ast),
53
+ schema: Schema.make(ast),
55
54
  inputProps: {
56
55
  type,
57
56
  format,
@@ -120,17 +119,40 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
120
119
  );
121
120
  }
122
121
 
122
+ // TODO(ZaymonFC): Extract this to it's own component.
123
+ if (format === 'ref') {
124
+ return (
125
+ <RefField
126
+ ast={ast}
127
+ type={type}
128
+ label={label}
129
+ readonly={readonly}
130
+ placeholder={placeholder}
131
+ inline={inline}
132
+ onQueryRefOptions={onQueryRefOptions}
133
+ inputProps={inputProps}
134
+ />
135
+ );
136
+ }
137
+
123
138
  if (type === 'object') {
124
139
  const baseNode = findNode(ast, isDiscriminatedUnion);
125
140
  const typeLiteral = baseNode
126
141
  ? getDiscriminatedType(baseNode, inputProps.getValue() as any)
127
- : findNode(ast, AST.isTypeLiteral);
142
+ : findNode(ast, SchemaAST.isTypeLiteral);
128
143
 
129
144
  if (typeLiteral) {
130
145
  return (
131
146
  <div role='none'>
132
- {!inline && <div>{label}</div>}
133
- <FormFields schema={S.make(typeLiteral)} path={path} readonly={readonly} Custom={Custom} />
147
+ {!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
148
+ <FormFields
149
+ schema={Schema.make(typeLiteral)}
150
+ path={path}
151
+ readonly={readonly}
152
+ onQueryRefOptions={onQueryRefOptions}
153
+ Custom={Custom}
154
+ lookupComponent={lookupComponent}
155
+ />
134
156
  </div>
135
157
  );
136
158
  }
@@ -139,7 +161,27 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
139
161
  return null;
140
162
  };
141
163
 
142
- export const FormFields = ({ schema, path, filter, sort, readonly, lookupComponent, Custom }: FormContentProps) => {
164
+ export type FormContentProps = {
165
+ schema: Schema.Schema.All;
166
+ path?: (string | number)[];
167
+ filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
168
+ sort?: string[];
169
+ readonly?: boolean;
170
+ onQueryRefOptions?: QueryRefOptions;
171
+ lookupComponent?: ComponentLookup;
172
+ Custom?: Partial<Record<string, InputComponent>>;
173
+ };
174
+
175
+ export const FormFields = ({
176
+ schema,
177
+ path,
178
+ filter,
179
+ sort,
180
+ readonly,
181
+ onQueryRefOptions,
182
+ lookupComponent,
183
+ Custom,
184
+ }: FormContentProps) => {
143
185
  const values = useFormValues(path);
144
186
 
145
187
  const properties = useMemo(() => {
@@ -159,6 +201,7 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
159
201
  property={property}
160
202
  path={[...(path ?? []), property.name]}
161
203
  readonly={readonly}
204
+ onQueryRefOptions={onQueryRefOptions}
162
205
  lookupComponent={lookupComponent}
163
206
  Custom={Custom}
164
207
  />
@@ -2,7 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { createContext, useContext, useEffect, useMemo, type FocusEvent, type PropsWithChildren } from 'react';
5
+ import React, {
6
+ createContext,
7
+ type RefObject,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ type FocusEvent,
12
+ type PropsWithChildren,
13
+ } from 'react';
6
14
 
7
15
  import { raise } from '@dxos/debug';
8
16
  import { type BaseObject, getValue } from '@dxos/echo-schema';
@@ -53,7 +61,7 @@ export const FormProvider = ({
53
61
  ...formOptions
54
62
  }: PropsWithChildren<
55
63
  FormOptions<any> & {
56
- formRef?: React.RefObject<HTMLDivElement>;
64
+ formRef?: RefObject<HTMLDivElement>;
57
65
  autoSave?: boolean;
58
66
  }
59
67
  >) => {
@@ -0,0 +1,152 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type SchemaAST } from 'effect';
6
+ import React, { useEffect, useState } from 'react';
7
+
8
+ import {
9
+ Expando,
10
+ getTypeAnnotation,
11
+ Ref,
12
+ ReferenceAnnotationId,
13
+ type ReferenceAnnotationValue,
14
+ type TypeAnnotation,
15
+ } from '@dxos/echo-schema';
16
+ import { findAnnotation, type SimpleType } from '@dxos/effect';
17
+ import { DXN } from '@dxos/keys';
18
+ import { refFromDXN } from '@dxos/live-object';
19
+ import { log } from '@dxos/log';
20
+ import { type MaybePromise } from '@dxos/util';
21
+
22
+ import { SelectInput, TextInput } from './Defaults';
23
+ import { type FormInputStateProps } from './FormContext';
24
+
25
+ export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<{ dxn: DXN; label?: string }[]>;
26
+
27
+ type RefFieldProps = {
28
+ ast: SchemaAST.AST;
29
+ type: SimpleType;
30
+ label: string;
31
+ readonly?: boolean;
32
+ placeholder?: string;
33
+ inline?: boolean;
34
+ onQueryRefOptions?: QueryRefOptions;
35
+ inputProps: FormInputStateProps;
36
+ };
37
+
38
+ export const RefField = ({
39
+ ast,
40
+ type,
41
+ label,
42
+ readonly,
43
+ placeholder,
44
+ inline,
45
+ onQueryRefOptions,
46
+ inputProps,
47
+ }: RefFieldProps) => {
48
+ const { getValue, onValueChange, ...restInputProps } = inputProps;
49
+ const refTypeInfo = findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId);
50
+ const [refOptions, setRefOptions] = useState<Array<{ value: string; label?: string }>>([]);
51
+ const [loading, setLoading] = useState(false);
52
+
53
+ useEffect(() => {
54
+ if (!refTypeInfo || !onQueryRefOptions) {
55
+ return;
56
+ }
57
+
58
+ const fetchOptions = async () => {
59
+ setLoading(true);
60
+ try {
61
+ const options = await onQueryRefOptions(refTypeInfo);
62
+ setRefOptions(
63
+ options.map((option) => ({
64
+ label: option.label,
65
+ value: option.dxn.toString(),
66
+ })),
67
+ );
68
+ } catch (error) {
69
+ log.error('Failed to fetch ref options:', error);
70
+ setRefOptions([]);
71
+ } finally {
72
+ setLoading(false);
73
+ }
74
+ };
75
+
76
+ void fetchOptions();
77
+ }, [refTypeInfo, onQueryRefOptions]);
78
+
79
+ if (!refTypeInfo) {
80
+ return null;
81
+ }
82
+
83
+ // If ref type is expando, fall back to taking a DXN in string format.
84
+ if (refTypeInfo.typename === getTypeAnnotation(Expando)?.typename || !onQueryRefOptions) {
85
+ const handleOnValueChange = (_type: any, dxnString: string) => {
86
+ const dxn = DXN.tryParse(dxnString);
87
+ if (dxn) {
88
+ onValueChange?.('object', refFromDXN(dxn));
89
+ } else if (dxnString === '') {
90
+ onValueChange?.('object', undefined);
91
+ } else {
92
+ onValueChange?.('string', dxnString);
93
+ }
94
+ };
95
+
96
+ const handleGetValue = () => {
97
+ const formValue = getValue();
98
+ if (typeof formValue === 'string') {
99
+ return formValue;
100
+ }
101
+ if (Ref.isRef(formValue)) {
102
+ return formValue.dxn.toString();
103
+ }
104
+
105
+ return undefined;
106
+ };
107
+
108
+ return (
109
+ <TextInput
110
+ type={type}
111
+ label={label}
112
+ disabled={readonly}
113
+ placeholder={placeholder}
114
+ inputOnly={inline}
115
+ getValue={handleGetValue as <V>() => V | undefined}
116
+ onValueChange={handleOnValueChange}
117
+ {...restInputProps}
118
+ />
119
+ );
120
+ }
121
+
122
+ const handleGetValue = () => {
123
+ const formValue = getValue();
124
+
125
+ if (Ref.isRef(formValue)) {
126
+ return formValue.dxn.toString();
127
+ }
128
+
129
+ return undefined;
130
+ };
131
+
132
+ const handleValueChanged = (_type: any, dxnString: string) => {
133
+ const dxn = DXN.parse(dxnString);
134
+ const ref = refFromDXN(dxn);
135
+
136
+ onValueChange('object', ref);
137
+ };
138
+
139
+ return (
140
+ <SelectInput
141
+ type={type}
142
+ label={label}
143
+ disabled={readonly || loading}
144
+ placeholder={loading ? 'Loading options...' : placeholder}
145
+ inputOnly={inline}
146
+ getValue={handleGetValue as <V>() => V | undefined}
147
+ onValueChange={handleValueChanged}
148
+ options={refOptions}
149
+ {...restInputProps}
150
+ />
151
+ );
152
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useState, useEffect, useMemo } from 'react';
5
+ import React, { useCallback, useState, useEffect, useMemo, type ChangeEvent } from 'react';
6
6
 
7
7
  import { type GeoPoint, GeoLocation } from '@dxos/echo-schema';
8
8
  import { Input, useTranslation } from '@dxos/react-ui';
@@ -28,7 +28,7 @@ export const GeoPointInput = ({ type, label, disabled, getStatus, getValue, onVa
28
28
 
29
29
  const handleCoordinateChange = useCallback(
30
30
  (coordinateType: keyof Pick<GeoLocation, 'longitude' | 'latitude'>, setText: (text: string) => void) =>
31
- (event: React.ChangeEvent<HTMLInputElement>) => {
31
+ (event: ChangeEvent<HTMLInputElement>) => {
32
32
  const inputText = event.target.value;
33
33
  setText(inputText);
34
34
  if (inputText !== '' && inputText !== '-') {
@@ -5,9 +5,10 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
+ import { Schema } from 'effect';
8
9
  import React, { useState } from 'react';
9
10
 
10
- import { S, SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
11
+ import { SelectOptionSchema, type SelectOption } from '@dxos/echo-schema';
11
12
  import { withLayout, withTheme } from '@dxos/storybook-utils';
12
13
 
13
14
  import { SelectOptionInput } from './SelectOptionsInput';
@@ -15,9 +16,9 @@ import translations from '../../../translations';
15
16
  import { TestLayout, TestPanel } from '../../testing';
16
17
  import { Form } from '../Form';
17
18
 
18
- const schema = S.Struct({
19
- options: S.Array(SelectOptionSchema).pipe(S.mutable),
20
- }).pipe(S.mutable);
19
+ const schema = Schema.Struct({
20
+ options: Schema.Array(SelectOptionSchema).pipe(Schema.mutable),
21
+ }).pipe(Schema.mutable);
21
22
 
22
23
  const DefaultStory = () => {
23
24
  const [values, setValues] = useState<{ options: SelectOption[] }>({
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useEffect, useState, type ChangeEvent, useRef } from 'react';
5
+ import React, { type ChangeEvent, type KeyboardEvent, useCallback, useEffect, useState, useRef } from 'react';
6
6
 
7
7
  import { type SelectOption } from '@dxos/echo-schema';
8
8
  import { PublicKey } from '@dxos/keys';
@@ -89,7 +89,7 @@ export const SelectOptionInput = ({ type, label, disabled, getStatus, getValue,
89
89
  [options, type, onValueChange],
90
90
  );
91
91
 
92
- const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
92
+ const handleKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
93
93
  if (e.key === 'Enter') {
94
94
  setSelectedId(null);
95
95
  }
@@ -3,8 +3,8 @@
3
3
  //
4
4
 
5
5
  export * from './custom';
6
-
7
6
  export * from './Defaults';
8
7
  export * from './Form';
9
8
  export * from './FormContext';
10
9
  export * from './Input';
10
+ export { type QueryRefOptions } from './RefField';
@@ -5,9 +5,10 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
+ import { Schema } from 'effect';
8
9
  import React, { useCallback, useMemo, useState } from 'react';
9
10
 
10
- import { Format, type EchoSchema, S, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
+ import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
11
12
  import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
12
13
  import { withClientProvider } from '@dxos/react-client/testing';
13
14
  import { useAsyncEffect } from '@dxos/react-ui';
@@ -26,7 +27,7 @@ const DefaultStory = () => {
26
27
  useAsyncEffect(async () => {
27
28
  if (space) {
28
29
  class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
29
- name: S.String,
30
+ name: Schema.String,
30
31
  email: Format.Email,
31
32
  salary: Format.Currency(),
32
33
  }) {}
@@ -2,10 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Schema, SchemaAST } from 'effect';
5
6
  import React, { useCallback, useMemo, useState } from 'react';
6
7
 
7
8
  import { type SchemaRegistry } from '@dxos/echo-db';
8
- import { AST, Format, type BaseSchema, S, type JsonProp } from '@dxos/echo-schema';
9
+ import { Format, type BaseSchema, type JsonProp } from '@dxos/echo-schema';
9
10
  import { invariant } from '@dxos/invariant';
10
11
  import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
12
  import { List } from '@dxos/react-ui-list';
@@ -18,16 +19,16 @@ import { Form } from '../Form';
18
19
 
19
20
  const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2.5rem]';
20
21
 
21
- const ViewMetaSchema = S.Struct({
22
- name: S.String.annotations({
23
- [AST.TitleAnnotationId]: 'View',
22
+ const ViewMetaSchema = Schema.Struct({
23
+ name: Schema.String.annotations({
24
+ [SchemaAST.TitleAnnotationId]: 'View',
24
25
  }),
25
26
  typename: Format.URL.annotations({
26
- [AST.TitleAnnotationId]: 'Typename',
27
+ [SchemaAST.TitleAnnotationId]: 'Typename',
27
28
  }),
28
- }).pipe(S.mutable);
29
+ }).pipe(Schema.mutable);
29
30
 
30
- type ViewMetaType = S.Schema.Type<typeof ViewMetaSchema>;
31
+ type ViewMetaType = Schema.Schema.Type<typeof ViewMetaSchema>;
31
32
 
32
33
  export type ViewEditorProps = ThemedClassName<{
33
34
  schema: BaseSchema;
@@ -139,15 +140,16 @@ export const ViewEditor = ({
139
140
  );
140
141
 
141
142
  return (
142
- <div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
143
+ <div role='none' className={mx('grid grid-rows-[1fr_1fr] min-bs-0', classNames)}>
143
144
  <Form<ViewMetaType>
144
145
  autoSave
145
146
  schema={ViewMetaSchema}
146
147
  values={viewValues}
147
148
  onSave={schema.readonly ? undefined : handleUpdate}
149
+ classNames='min-bs-0 overflow-y-auto'
148
150
  />
149
151
 
150
- <div>
152
+ <div role='none' className='border-bs border-separator min-bs-0 overflow-y-auto'>
151
153
  {/* TODO(burdon): Clean up common form ux. */}
152
154
  <div role='none' className='p-2'>
153
155
  <label className={mx(inputTextLabel)}>{t('fields label')}</label>
@@ -155,7 +157,7 @@ export const ViewEditor = ({
155
157
 
156
158
  <List.Root<FieldType>
157
159
  items={view.fields}
158
- isItem={S.is(FieldSchema)}
160
+ isItem={Schema.is(FieldSchema)}
159
161
  getId={(field) => field.id}
160
162
  onMove={schema.readonly ? undefined : handleMove}
161
163
  >
@@ -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 { create } from '@dxos/live-object';
10
+ import { live } from '@dxos/live-object';
11
11
  import { testData, TestSchema, testView, type TestType } from '@dxos/schema/testing';
12
12
  import { withLayout, withTheme } from '@dxos/storybook-utils';
13
13
 
@@ -39,23 +39,23 @@ type Story = StoryObj<DeprecatedFormProps<TestType>>;
39
39
 
40
40
  export const Default: Story = {
41
41
  args: {
42
- object: create(testData),
43
- view: create(testView),
42
+ object: live(testData),
43
+ view: live(testView),
44
44
  schema: TestSchema,
45
45
  },
46
46
  };
47
47
 
48
48
  export const Empty: Story = {
49
49
  args: {
50
- view: create(testView),
50
+ view: live(testView),
51
51
  schema: TestSchema,
52
52
  },
53
53
  };
54
54
 
55
55
  export const Readonly: Story = {
56
56
  args: {
57
- object: create(testData),
58
- view: create(testView),
57
+ object: live(testData),
58
+ view: live(testView),
59
59
  schema: TestSchema,
60
60
  readonly: true,
61
61
  },
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { SchemaAST as AST, type Schema as S, Option, pipe } from 'effect';
5
+ import { SchemaAST, type Schema, Option, pipe } from 'effect';
6
6
  import React, { useState } from 'react';
7
7
 
8
8
  import { findProperty } from '@dxos/effect';
@@ -13,7 +13,7 @@ import { type ViewType, getFieldValue, setFieldValue } from '@dxos/schema';
13
13
  export type DeprecatedFormProps<T extends {} = {}> = ThemedClassName<{
14
14
  view: ViewType;
15
15
  object: T;
16
- schema?: S.Schema<T>;
16
+ schema?: Schema.Schema<T>;
17
17
  readonly?: boolean;
18
18
  }>;
19
19
 
@@ -31,16 +31,16 @@ export const DeprecatedForm = <T extends {} = {}>({
31
31
  <div role='none' className={mx('flex flex-col w-full gap-2 p-2', classNames)}>
32
32
  {view.fields.map((field) => {
33
33
  const prop = schema && findProperty(schema, field.path);
34
- const title = (prop && pipe(AST.getTitleAnnotation(prop), Option.getOrUndefined)) ?? '';
35
- const description = (prop && pipe(AST.getDescriptionAnnotation(prop), Option.getOrUndefined)) ?? title;
34
+ const title = (prop && pipe(SchemaAST.getTitleAnnotation(prop), Option.getOrUndefined)) ?? '';
35
+ const description = (prop && pipe(SchemaAST.getDescriptionAnnotation(prop), Option.getOrUndefined)) ?? title;
36
36
  // const format =
37
- // (prop && (getPatternAnnotation(prop) ?? (AST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
37
+ // (prop && (getPatternAnnotation(prop) ?? (SchemaAST.isNumberKeyword(prop) && RealNumberFormat))) || undefined;
38
38
 
39
39
  //
40
40
  // Boolean
41
41
  //
42
42
 
43
- if (prop && AST.isBooleanKeyword(prop)) {
43
+ if (prop && SchemaAST.isBooleanKeyword(prop)) {
44
44
  const value = object && getFieldValue(object, field);
45
45
  const onChange = (checked: boolean) => {
46
46
  setFieldValue(object, field, checked);
@@ -12,7 +12,7 @@ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
12
12
  */
13
13
  export const DeprecatedFormContainer = ({ children, classNames }: ThemedClassName<PropsWithChildren>) => {
14
14
  return (
15
- <div role='none' className='w-full p-4 justify-center overflow-x-hidden overflow-y-auto'>
15
+ <div role='none' className='container-max-width p-4 justify-center overflow-x-hidden overflow-y-auto'>
16
16
  <div role='form' className={mx('flex flex-col', textBlockWidth, classNames)}>
17
17
  {children}
18
18
  </div>
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './deprecated';
6
+
6
7
  export * from './ControlSection';
7
8
  export * from './FieldEditor';
8
9
  export * from './Form';
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './useForm';
6
+ export * from './useRefQueryLookupHandler';
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { SchemaAST as AST, type Schema as S } from 'effect';
5
+ import { SchemaAST, type Schema } from 'effect';
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react';
7
7
 
8
8
  import { type BaseObject, getValue, setValue } from '@dxos/echo-schema';
@@ -45,7 +45,7 @@ export interface FormOptions<T extends BaseObject> {
45
45
  /**
46
46
  * Effect schema (Type literal).
47
47
  */
48
- schema: S.Schema<T>;
48
+ schema: Schema.Schema<T, any>;
49
49
 
50
50
  /**
51
51
  * Initial values (which may not pass validation).
@@ -90,7 +90,7 @@ export const useForm = <T extends BaseObject>({
90
90
  onValid,
91
91
  onSave,
92
92
  }: FormOptions<T>): FormHandler<T> => {
93
- invariant(AST.isTypeLiteral(schema.ast));
93
+ invariant(SchemaAST.isTypeLiteral(schema.ast));
94
94
 
95
95
  const [values, setValues] = useState<Partial<T>>(initialValues);
96
96
  useEffect(() => {
@@ -0,0 +1,43 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useCallback } from 'react';
6
+
7
+ import { getDXN, type TypeAnnotation } from '@dxos/echo-schema';
8
+ import { Filter, type Space } from '@dxos/react-client/echo';
9
+ import { isNonNullable } from '@dxos/util';
10
+
11
+ import { type QueryRefOptions } from '../components';
12
+
13
+ type UseRefQueryLookupProps = {
14
+ space?: Space;
15
+ };
16
+
17
+ export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
18
+ const handleRefQueryLookup = useCallback(
19
+ async (typeInfo: TypeAnnotation) => {
20
+ if (!space) {
21
+ return [];
22
+ }
23
+ const query = space.db.query(Filter.typename(typeInfo.typename));
24
+ const { objects } = await query.run();
25
+
26
+ return objects
27
+ .map((object) => {
28
+ const dxn = getDXN(object);
29
+ if (!dxn) {
30
+ return undefined;
31
+ }
32
+
33
+ // TODO(Zaymon): Better fallback object names?
34
+ const item = { dxn, label: object?.name ?? object?.id ?? '' };
35
+ return item;
36
+ })
37
+ .filter(isNonNullable);
38
+ },
39
+ [space],
40
+ );
41
+
42
+ return handleRefQueryLookup;
43
+ };