@dxos/react-ui-form 0.8.2-main.5ca3450 → 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 (55) hide show
  1. package/dist/lib/browser/index.mjs +672 -543
  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 +564 -442
  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 +672 -543
  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/Form/ArrayField.d.ts.map +1 -1
  11. package/dist/types/src/components/Form/Form.d.ts +1 -2
  12. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Form/FormContent.d.ts +1 -1
  15. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  18. package/dist/types/src/components/Form/RefField.d.ts +6 -18
  19. package/dist/types/src/components/Form/RefField.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/factory.d.ts +1 -1
  21. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/index.d.ts +0 -1
  23. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  24. package/dist/types/src/hooks/index.d.ts +1 -0
  25. package/dist/types/src/hooks/index.d.ts.map +1 -1
  26. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  27. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  28. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +1 -1
  29. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -1
  30. package/dist/types/src/util/index.d.ts +2 -0
  31. package/dist/types/src/util/index.d.ts.map +1 -1
  32. package/dist/types/src/util/refs.d.ts +9 -0
  33. package/dist/types/src/util/refs.d.ts.map +1 -0
  34. package/dist/types/src/util/schema.d.ts +3 -0
  35. package/dist/types/src/util/schema.d.ts.map +1 -0
  36. package/package.json +25 -24
  37. package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
  38. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  39. package/src/components/Form/ArrayField.tsx +2 -2
  40. package/src/components/Form/Form.stories.tsx +2 -1
  41. package/src/components/Form/Form.tsx +1 -2
  42. package/src/components/Form/FormContent.tsx +44 -16
  43. package/src/components/Form/FormContext.tsx +3 -1
  44. package/src/components/Form/Input.tsx +3 -9
  45. package/src/components/Form/RefField.tsx +144 -99
  46. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +1 -1
  47. package/src/components/Form/factory.tsx +1 -1
  48. package/src/components/Form/index.ts +0 -1
  49. package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
  50. package/src/hooks/index.ts +2 -1
  51. package/src/hooks/useQueryRefOptions.ts +53 -0
  52. package/src/hooks/useRefQueryLookupHandler.ts +2 -4
  53. package/src/util/index.ts +3 -1
  54. package/src/util/refs.ts +38 -0
  55. package/src/util/schema.ts +14 -0
@@ -16,8 +16,10 @@ import { SelectInput } from './Defaults';
16
16
  import { type ComponentLookup } from './Form';
17
17
  import { useInputProps, useFormValues } from './FormContext';
18
18
  import { type InputComponent } from './Input';
19
- import { RefField, type QueryRefOptions } from './RefField';
19
+ import { RefField } from './RefField';
20
20
  import { getInputComponent } from './factory';
21
+ import { type QueryRefOptions } from '../../hooks';
22
+ import { getRefProps } from '../../util';
21
23
 
22
24
  export type FormFieldProps = {
23
25
  property: SchemaProperty<any>;
@@ -48,6 +50,10 @@ export const FormField = ({
48
50
  [examples, description],
49
51
  );
50
52
 
53
+ //
54
+ // Registry and Custom.
55
+ //
56
+
51
57
  const FoundComponent = lookupComponent?.({
52
58
  prop: name,
53
59
  schema: Schema.make(ast),
@@ -81,10 +87,40 @@ export const FormField = ({
81
87
  );
82
88
  }
83
89
 
90
+ //
91
+ // Refs.
92
+ //
93
+
94
+ const refProps = getRefProps(property);
95
+ if (refProps) {
96
+ return (
97
+ <RefField
98
+ ast={refProps.ast}
99
+ array={refProps.isArray}
100
+ type={type}
101
+ format={format}
102
+ label={label}
103
+ placeholder={placeholder}
104
+ disabled={readonly}
105
+ inputOnly={inline}
106
+ onQueryRefOptions={onQueryRefOptions}
107
+ {...inputProps}
108
+ />
109
+ );
110
+ }
111
+
112
+ //
113
+ // Arrays.
114
+ //
115
+
84
116
  if (array) {
85
117
  return <ArrayField property={property} path={path} inputProps={inputProps} readonly={readonly} Custom={Custom} />;
86
118
  }
87
119
 
120
+ //
121
+ // Standard Inputs.
122
+ //
123
+
88
124
  const InputComponent = getInputComponent(type, format);
89
125
  if (InputComponent) {
90
126
  return (
@@ -102,6 +138,10 @@ export const FormField = ({
102
138
  );
103
139
  }
104
140
 
141
+ //
142
+ // Select.
143
+ //
144
+
105
145
  if (options) {
106
146
  return (
107
147
  <div role='none'>
@@ -119,21 +159,9 @@ export const FormField = ({
119
159
  );
120
160
  }
121
161
 
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
- }
162
+ //
163
+ // Nested Objects.
164
+ //
137
165
 
138
166
  if (type === 'object') {
139
167
  const baseNode = findNode(ast, isDiscriminatedUnion);
@@ -76,7 +76,9 @@ export const FormProvider = ({
76
76
  const keyIsEnter = event.key === 'Enter';
77
77
  const modifierUsed = event.ctrlKey || event.altKey || event.metaKey || event.shiftKey;
78
78
  const inputIsTextarea = (event.target as HTMLElement).tagName.toLowerCase() === 'textarea';
79
- const inputOptOut = (event.target as HTMLElement).hasAttribute('data-no-submit');
79
+ const inputOptOut =
80
+ (event.target as HTMLElement).hasAttribute('data-no-submit') ||
81
+ (event.target as HTMLElement).closest('[data-no-submit]') !== null;
80
82
 
81
83
  // Regular inputs: Submit on Enter (no modifiers).
82
84
  const shouldSubmitRegularInput = !inputIsTextarea && keyIsEnter && !modifierUsed;
@@ -37,15 +37,9 @@ export const InputHeader = ({ children, error }: InputHeaderProps) => {
37
37
  <div role='none' className='flex justify-between items-center mb-1'>
38
38
  {children}
39
39
  {error && (
40
- <Tooltip.Root>
41
- <Tooltip.Trigger asChild>
42
- <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
43
- </Tooltip.Trigger>
44
- <Tooltip.Content side='bottom'>
45
- <Tooltip.Arrow />
46
- {error}
47
- </Tooltip.Content>
48
- </Tooltip.Root>
40
+ <Tooltip.Trigger asChild content={error} side='bottom'>
41
+ <Icon icon='ph--warning--regular' size={4} classNames={errorText} />
42
+ </Tooltip.Trigger>
49
43
  )}
50
44
  </div>
51
45
  );
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type SchemaAST } from 'effect';
6
- import React, { useEffect, useState } from 'react';
6
+ import React, { useCallback, useMemo, type FocusEvent } from 'react';
7
7
 
8
8
  import {
9
9
  Expando,
@@ -11,117 +11,170 @@ import {
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: SchemaAST.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
  );
@@ -47,7 +47,7 @@ const DefaultStory = () => {
47
47
  const meta: Meta = {
48
48
  title: 'ui/react-ui-form/SelectOptionsInput',
49
49
  component: SelectOptionInput,
50
- decorators: [withTheme, withLayout({ tooltips: true })],
50
+ decorators: [withTheme, withLayout()],
51
51
  parameters: { translations },
52
52
  };
53
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;
@@ -7,4 +7,3 @@ export * from './Defaults';
7
7
  export * from './Form';
8
8
  export * from './FormContext';
9
9
  export * from './Input';
10
- export { type QueryRefOptions } from './RefField';
@@ -82,7 +82,7 @@ const meta: Meta<typeof ViewEditor> = {
82
82
  title: 'ui/react-ui-form/ViewEditor',
83
83
  component: ViewEditor,
84
84
  render: DefaultStory,
85
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true, tooltips: true }), withTheme],
85
+ decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
86
86
  parameters: {
87
87
  translations,
88
88
  },
@@ -1,6 +1,7 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  export * from './useForm';
6
6
  export * from './useRefQueryLookupHandler';
7
+ export * from './useQueryRefOptions';
@@ -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
+ };
@@ -8,11 +8,9 @@ import { getDXN, type TypeAnnotation } from '@dxos/echo-schema';
8
8
  import { Filter, type Space } from '@dxos/react-client/echo';
9
9
  import { isNonNullable } from '@dxos/util';
10
10
 
11
- import { type QueryRefOptions } from '../components';
11
+ import { type QueryRefOptions } from './useQueryRefOptions';
12
12
 
13
- type UseRefQueryLookupProps = {
14
- space?: Space;
15
- };
13
+ type UseRefQueryLookupProps = { space?: Space };
16
14
 
17
15
  export const useRefQueryLookupHandler = ({ space }: UseRefQueryLookupProps): QueryRefOptions => {
18
16
  const handleRefQueryLookup = useCallback(
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
+ };