@dxos/react-ui-form 0.8.2-main.5885341 → 0.8.2-main.600d381

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 (80) hide show
  1. package/dist/lib/browser/index.mjs +720 -557
  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 +734 -579
  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 +720 -557
  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/ControlSection.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.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/Defaults.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/Form.d.ts +4 -4
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +33 -32
  17. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/FormContent.d.ts +3 -3
  20. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormContext.d.ts.map +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 +7 -18
  24. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  25. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  27. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  29. package/dist/types/src/components/Form/factory.d.ts +1 -1
  30. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  31. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  32. package/dist/types/src/components/ViewEditor/Test.stories.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/deprecated/Form.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  39. package/dist/types/src/components/testing.d.ts.map +1 -1
  40. package/dist/types/src/hooks/index.d.ts +2 -0
  41. package/dist/types/src/hooks/index.d.ts.map +1 -1
  42. package/dist/types/src/hooks/useForm.d.ts +2 -2
  43. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  45. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  46. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  47. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  48. package/dist/types/src/util/field.d.ts.map +1 -1
  49. package/dist/types/src/util/formatting.d.ts.map +1 -1
  50. package/dist/types/src/util/index.d.ts +2 -0
  51. package/dist/types/src/util/index.d.ts.map +1 -1
  52. package/dist/types/src/util/refs.d.ts +9 -0
  53. package/dist/types/src/util/refs.d.ts.map +1 -0
  54. package/dist/types/src/util/schema.d.ts +3 -0
  55. package/dist/types/src/util/schema.d.ts.map +1 -0
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +25 -24
  58. package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
  59. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  60. package/src/components/Form/ArrayField.tsx +5 -6
  61. package/src/components/Form/Defaults.tsx +4 -1
  62. package/src/components/Form/Form.stories.tsx +48 -42
  63. package/src/components/Form/Form.tsx +4 -4
  64. package/src/components/Form/FormContent.tsx +49 -22
  65. package/src/components/Form/FormContext.tsx +3 -1
  66. package/src/components/Form/Input.tsx +3 -9
  67. package/src/components/Form/RefField.tsx +145 -100
  68. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
  69. package/src/components/Form/factory.tsx +1 -1
  70. package/src/components/Form/index.ts +0 -1
  71. package/src/components/ViewEditor/ViewEditor.stories.tsx +4 -3
  72. package/src/components/ViewEditor/ViewEditor.tsx +9 -8
  73. package/src/components/deprecated/Form.tsx +6 -6
  74. package/src/hooks/index.ts +3 -1
  75. package/src/hooks/useForm.ts +3 -3
  76. package/src/hooks/useQueryRefOptions.ts +53 -0
  77. package/src/hooks/useRefQueryLookupHandler.ts +41 -0
  78. package/src/util/index.ts +3 -1
  79. package/src/util/refs.ts +38 -0
  80. package/src/util/schema.ts +14 -0
@@ -2,126 +2,179 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { useEffect, useState } from 'react';
5
+ import { type SchemaAST } from 'effect';
6
+ import React, { useCallback, useMemo, type FocusEvent } from 'react';
6
7
 
7
8
  import {
8
- type AST,
9
9
  Expando,
10
10
  getTypeAnnotation,
11
11
  Ref,
12
12
  ReferenceAnnotationId,
13
13
  type ReferenceAnnotationValue,
14
- type TypeAnnotation,
15
14
  } from '@dxos/echo-schema';
16
- import { findAnnotation, type SimpleType } from '@dxos/effect';
15
+ import { findAnnotation } from '@dxos/effect';
17
16
  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: AST.AST;
29
- type: SimpleType;
30
- label: string;
31
- readonly?: boolean;
32
- placeholder?: string;
33
- inline?: boolean;
17
+ import { Input } from '@dxos/react-ui';
18
+ import { TagPicker, type TagPickerMode, type TagPickerItemData } from '@dxos/react-ui-tag-picker';
19
+ import { isNonNullable } from '@dxos/util';
20
+
21
+ import { TextInput } from './Defaults';
22
+ import { InputHeader, type InputProps } from './Input';
23
+ import { type QueryRefOptions, useQueryRefOptions } from '../../hooks';
24
+
25
+ type RefFieldProps = InputProps & {
26
+ ast?: SchemaAST.AST;
27
+ array?: boolean;
34
28
  onQueryRefOptions?: QueryRefOptions;
35
- inputProps: FormInputStateProps;
36
29
  };
37
30
 
38
31
  export const RefField = ({
39
- ast,
40
32
  type,
41
33
  label,
42
- readonly,
34
+ disabled,
43
35
  placeholder,
44
- inline,
36
+ inputOnly,
37
+ array,
38
+ ast,
39
+ getValue,
40
+ onBlur,
45
41
  onQueryRefOptions,
46
- inputProps,
42
+ onValueChange,
43
+ ...restInputProps
47
44
  }: 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
- }
45
+ const refTypeInfo = useMemo(
46
+ () => (ast ? findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId) : undefined),
47
+ [ast],
48
+ );
49
+ const { options: availableOptions, loading: _loading } = useQueryRefOptions({ refTypeInfo, onQueryRefOptions });
50
+
51
+ if ((refTypeInfo && refTypeInfo?.typename === getTypeAnnotation(Expando)?.typename) || !onQueryRefOptions) {
52
+ // If ref type is expando, fall back to taking a DXN in string format.
53
+ return (
54
+ <RefFieldFallback
55
+ {...{ type, label, placeholder, disabled, inputOnly, getValue, onBlur, onValueChange, ...restInputProps }}
56
+ />
57
+ );
58
+ }
59
+
60
+ const handleGetValue = useCallback((): TagPickerItemData[] => {
61
+ const formValue = getValue();
57
62
 
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);
63
+ const unknownToRefOption = (val: unknown) => {
64
+ if (Ref.isRef(val)) {
65
+ const dxnString = val.dxn.toString();
66
+ const matchingOption = availableOptions.find((option) => option.id === dxnString);
67
+ if (matchingOption) {
68
+ return matchingOption;
69
+ }
73
70
  }
71
+ return undefined;
74
72
  };
75
73
 
76
- void fetchOptions();
77
- }, [refTypeInfo, onQueryRefOptions]);
74
+ if (array && Array.isArray(formValue)) {
75
+ return formValue.map(unknownToRefOption).filter(isNonNullable) ?? [];
76
+ }
78
77
 
79
- if (!refTypeInfo) {
80
- return null;
81
- }
78
+ const option = unknownToRefOption(formValue);
79
+ if (option) {
80
+ return [option];
81
+ }
82
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
- };
83
+ return [];
84
+ }, [getValue, availableOptions, array]);
95
85
 
96
- const handleGetValue = () => {
97
- const formValue = getValue();
98
- if (typeof formValue === 'string') {
99
- return formValue;
86
+ const handleSearch = useCallback(
87
+ (text: string, ids: string[]): TagPickerItemData[] =>
88
+ availableOptions
89
+ .filter((option) => !ids.includes(option.id))
90
+ .filter((option) => option.label.toLowerCase().includes(text.toLowerCase()))
91
+ .map((option) => ({ id: option.id, label: option.label, hue: option.hue as any })),
92
+ [availableOptions],
93
+ );
94
+
95
+ const handleUpdate = useCallback(
96
+ (ids: string[]) => {
97
+ if (ids.length === 0) {
98
+ onValueChange('object', undefined);
99
+ return;
100
100
  }
101
- if (Ref.isRef(formValue)) {
102
- return formValue.dxn.toString();
101
+
102
+ const refs = ids
103
+ .map((id) => {
104
+ const item = availableOptions.find((option) => option.id === id);
105
+ if (item) {
106
+ const dxn = DXN.parse(item.id);
107
+ return Ref.fromDXN(dxn);
108
+ }
109
+ return null;
110
+ })
111
+ .filter(isNonNullable);
112
+
113
+ if (array) {
114
+ onValueChange('object', refs);
115
+ } else {
116
+ onValueChange('object', refs[0]);
103
117
  }
118
+ },
119
+ [availableOptions, array, onValueChange],
120
+ );
104
121
 
105
- return undefined;
106
- };
122
+ const tagPickerMode: TagPickerMode = useMemo(() => (array ? 'multi-select' : 'single-select'), [array]);
107
123
 
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
- );
124
+ if (!refTypeInfo) {
125
+ return null;
120
126
  }
121
127
 
128
+ const { status, error } = restInputProps.getStatus();
129
+
130
+ return (
131
+ <Input.Root validationValence={status}>
132
+ {!inputOnly && (
133
+ <InputHeader error={error}>
134
+ <Input.Label>{label}</Input.Label>
135
+ </InputHeader>
136
+ )}
137
+ <div data-no-submit>
138
+ <TagPicker
139
+ items={handleGetValue()}
140
+ mode={tagPickerMode}
141
+ onBlur={(event) => onBlur(event as unknown as FocusEvent<HTMLElement>)}
142
+ onUpdate={handleUpdate}
143
+ onSearch={handleSearch}
144
+ classNames='rounded-sm bg-input p-1.5'
145
+ />
146
+ </div>
147
+ {inputOnly && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
148
+ </Input.Root>
149
+ );
150
+ };
151
+
152
+ const RefFieldFallback = ({
153
+ type,
154
+ label,
155
+ disabled,
156
+ placeholder,
157
+ inputOnly,
158
+ getValue,
159
+ onValueChange,
160
+ ...restInputProps
161
+ }: InputProps) => {
162
+ const handleOnValueChange = (_type: any, dxnString: string) => {
163
+ const dxn = DXN.tryParse(dxnString);
164
+ if (dxn) {
165
+ onValueChange?.('object', Ref.fromDXN(dxn));
166
+ } else if (dxnString === '') {
167
+ onValueChange?.('object', undefined);
168
+ } else {
169
+ onValueChange?.('string', dxnString);
170
+ }
171
+ };
172
+
122
173
  const handleGetValue = () => {
123
174
  const formValue = getValue();
124
-
175
+ if (typeof formValue === 'string') {
176
+ return formValue;
177
+ }
125
178
  if (Ref.isRef(formValue)) {
126
179
  return formValue.dxn.toString();
127
180
  }
@@ -129,23 +182,15 @@ export const RefField = ({
129
182
  return undefined;
130
183
  };
131
184
 
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
185
  return (
140
- <SelectInput
186
+ <TextInput
141
187
  type={type}
142
188
  label={label}
143
- disabled={readonly || loading}
144
- placeholder={loading ? 'Loading options...' : placeholder}
145
- inputOnly={inline}
189
+ disabled={disabled}
190
+ placeholder={placeholder}
191
+ inputOnly={inputOnly}
146
192
  getValue={handleGetValue as <V>() => V | undefined}
147
- onValueChange={handleValueChanged}
148
- options={refOptions}
193
+ onValueChange={handleOnValueChange}
149
194
  {...restInputProps}
150
195
  />
151
196
  );
@@ -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[] }>({
@@ -46,7 +47,7 @@ const DefaultStory = () => {
46
47
  const meta: Meta = {
47
48
  title: 'ui/react-ui-form/SelectOptionsInput',
48
49
  component: SelectOptionInput,
49
- decorators: [withTheme, withLayout({ tooltips: true })],
50
+ decorators: [withTheme, withLayout()],
50
51
  parameters: { translations },
51
52
  };
52
53
 
@@ -12,7 +12,7 @@ import { GeoPointInput } from './custom';
12
12
  /**
13
13
  * Get property input component.
14
14
  */
15
- export const getInputComponent = (type: SimpleType, format?: FormatEnum): InputComponent | undefined => {
15
+ export const getInputComponent = (type: SimpleType, format?: FormatEnum | string): InputComponent | undefined => {
16
16
  switch (format) {
17
17
  case FormatEnum.GeoPoint:
18
18
  return GeoPointInput;
@@ -3,7 +3,6 @@
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';
@@ -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
  }) {}
@@ -81,7 +82,7 @@ const meta: Meta<typeof ViewEditor> = {
81
82
  title: 'ui/react-ui-form/ViewEditor',
82
83
  component: ViewEditor,
83
84
  render: DefaultStory,
84
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true, tooltips: true }), withTheme],
85
+ decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
85
86
  parameters: {
86
87
  translations,
87
88
  },
@@ -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;
@@ -156,7 +157,7 @@ export const ViewEditor = ({
156
157
 
157
158
  <List.Root<FieldType>
158
159
  items={view.fields}
159
- isItem={S.is(FieldSchema)}
160
+ isItem={Schema.is(FieldSchema)}
160
161
  getId={(field) => field.id}
161
162
  onMove={schema.readonly ? undefined : handleMove}
162
163
  >
@@ -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);
@@ -1,5 +1,7 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './useForm';
6
+ export * from './useRefQueryLookupHandler';
7
+ export * from './useQueryRefOptions';
@@ -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,53 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { type TypeAnnotation } from '@dxos/echo-schema';
8
+ import { type DXN } from '@dxos/keys';
9
+ import { log } from '@dxos/log';
10
+ import { type TagPickerItemData } from '@dxos/react-ui-tag-picker';
11
+ import { type MaybePromise } from '@dxos/util';
12
+
13
+ export type RefOption = { dxn: DXN; label?: string };
14
+ export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<RefOption[]>;
15
+
16
+ type UseQueryRefOptionsProps = { refTypeInfo: TypeAnnotation | undefined; onQueryRefOptions?: QueryRefOptions };
17
+
18
+ /**
19
+ * Hook to query reference options based on type information.
20
+ * Used internally within forms to fetch and format reference options for reference fields.
21
+ */
22
+ export const useQueryRefOptions = ({ refTypeInfo, onQueryRefOptions }: UseQueryRefOptionsProps) => {
23
+ const [options, setOptions] = useState<TagPickerItemData[]>([]);
24
+ const [loading, setLoading] = useState(false);
25
+
26
+ useEffect(() => {
27
+ if (!refTypeInfo || !onQueryRefOptions) {
28
+ return;
29
+ }
30
+
31
+ const fetchOptions = async () => {
32
+ setLoading(true);
33
+ try {
34
+ const fetchedOptions = await onQueryRefOptions(refTypeInfo);
35
+ setOptions(
36
+ fetchedOptions.map((option) => {
37
+ const dxn = option.dxn.toString() as string;
38
+ return { id: dxn, label: option.label ?? dxn, hue: 'neutral' as any };
39
+ }),
40
+ );
41
+ } catch (error) {
42
+ log.error('Failed to fetch ref options:', error);
43
+ setOptions([]);
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+
49
+ void fetchOptions();
50
+ }, [refTypeInfo, onQueryRefOptions]);
51
+
52
+ return { options, loading };
53
+ };
@@ -0,0 +1,41 @@
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 './useQueryRefOptions';
12
+
13
+ type UseRefQueryLookupProps = { space?: Space };
14
+
15
+ export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
16
+ const handleRefQueryLookup = useCallback(
17
+ async (typeInfo: TypeAnnotation) => {
18
+ if (!space) {
19
+ return [];
20
+ }
21
+ const query = space.db.query(Filter.typename(typeInfo.typename));
22
+ const { objects } = await query.run();
23
+
24
+ return objects
25
+ .map((object) => {
26
+ const dxn = getDXN(object);
27
+ if (!dxn) {
28
+ return undefined;
29
+ }
30
+
31
+ // TODO(Zaymon): Better fallback object names?
32
+ const item = { dxn, label: object?.name ?? object?.id ?? '' };
33
+ return item;
34
+ })
35
+ .filter(isNonNullable);
36
+ },
37
+ [space],
38
+ );
39
+
40
+ return handleRefQueryLookup;
41
+ };
package/src/util/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './field';
6
6
  export * from './formatting';
7
+ export * from './refs';
8
+ export * from './schema';
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type SchemaAST } from 'effect';
6
+
7
+ import { FormatEnum, ReferenceAnnotationId } from '@dxos/echo-schema';
8
+ import { findAnnotation } from '@dxos/effect';
9
+ import { type SchemaProperty } from '@dxos/schema';
10
+
11
+ import { findArrayElementType } from './schema';
12
+
13
+ type RefProps = {
14
+ ast: SchemaAST.AST;
15
+ isArray: boolean;
16
+ };
17
+
18
+ export const getRefProps = (property: SchemaProperty<any>): RefProps | undefined => {
19
+ const { ast, format, array } = property;
20
+
21
+ // Direct reference.
22
+ if (format === FormatEnum.Ref) {
23
+ return { ast, isArray: false };
24
+ }
25
+
26
+ // Array of references.
27
+ if (array) {
28
+ const elementType = findArrayElementType(ast);
29
+ if (elementType) {
30
+ const annotation = findAnnotation(elementType, ReferenceAnnotationId);
31
+ if (annotation) {
32
+ return { ast: elementType, isArray: true };
33
+ }
34
+ }
35
+ }
36
+
37
+ return undefined;
38
+ };
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { SchemaAST } from 'effect';
6
+
7
+ import { findNode } from '@dxos/effect';
8
+
9
+ export const findArrayElementType = (ast: SchemaAST.AST) => {
10
+ const tupleType = findNode(ast, SchemaAST.isTupleType);
11
+ const elementType = (tupleType as SchemaAST.TupleType | undefined)?.rest[0]?.type;
12
+
13
+ return elementType;
14
+ };