@dxos/react-ui-form 0.8.2-main.f081794 → 0.8.2-main.f11618f

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 (33) hide show
  1. package/dist/lib/browser/index.mjs +205 -73
  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 +259 -129
  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 +205 -73
  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 +3 -1
  12. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  13. package/dist/types/src/components/Form/Form.stories.d.ts +4 -2
  14. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/FormContent.d.ts +13 -10
  16. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  17. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  18. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/RefField.d.ts +23 -0
  20. package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
  21. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  23. package/dist/types/src/util/formatting.d.ts.map +1 -1
  24. package/package.json +24 -24
  25. package/src/components/Form/ArrayField.tsx +3 -1
  26. package/src/components/Form/Form.stories.tsx +48 -5
  27. package/src/components/Form/Form.tsx +4 -0
  28. package/src/components/Form/FormContent.tsx +59 -15
  29. package/src/components/Form/FormContext.tsx +10 -2
  30. package/src/components/Form/RefField.tsx +151 -0
  31. package/src/components/Form/custom/GeoPointInput.tsx +2 -2
  32. package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
  33. package/src/util/formatting.ts +15 -3
@@ -1,5 +1,5 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  import { pipe } from 'effect';
@@ -17,29 +17,29 @@ import { SelectInput } from './Defaults';
17
17
  import { type ComponentLookup } from './Form';
18
18
  import { useInputProps, useFormValues } from './FormContext';
19
19
  import { type InputComponent } from './Input';
20
+ import { RefField, type QueryRefOptions } from './RefField';
20
21
  import { getInputComponent } from './factory';
21
22
 
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
23
  export type FormFieldProps = {
33
24
  property: SchemaProperty<any>;
34
25
  path?: (string | number)[];
35
26
  readonly?: boolean;
36
27
  /** Used to indicate if input should be presented inline (e.g. for array items). */
37
28
  inline?: boolean;
29
+ onQueryRefOptions?: QueryRefOptions;
38
30
  lookupComponent?: ComponentLookup;
39
31
  Custom?: Partial<Record<string, InputComponent>>;
40
32
  };
41
33
 
42
- export const FormField = ({ property, path, readonly, inline, lookupComponent, Custom }: FormFieldProps) => {
34
+ export const FormField = ({
35
+ property,
36
+ path,
37
+ readonly,
38
+ inline,
39
+ onQueryRefOptions,
40
+ lookupComponent,
41
+ Custom,
42
+ }: FormFieldProps) => {
43
43
  const inputProps = useInputProps(path);
44
44
  const { ast, name, type, format, title, description, options, examples, array } = property;
45
45
 
@@ -120,6 +120,22 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
120
120
  );
121
121
  }
122
122
 
123
+ // TODO(ZaymonFC): Extract this to it's own component.
124
+ if (format === 'ref') {
125
+ return (
126
+ <RefField
127
+ ast={ast}
128
+ type={type}
129
+ label={label}
130
+ readonly={readonly}
131
+ placeholder={placeholder}
132
+ inline={inline}
133
+ onQueryRefOptions={onQueryRefOptions}
134
+ inputProps={inputProps}
135
+ />
136
+ );
137
+ }
138
+
123
139
  if (type === 'object') {
124
140
  const baseNode = findNode(ast, isDiscriminatedUnion);
125
141
  const typeLiteral = baseNode
@@ -129,8 +145,15 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
129
145
  if (typeLiteral) {
130
146
  return (
131
147
  <div role='none'>
132
- {!inline && <div>{label}</div>}
133
- <FormFields schema={S.make(typeLiteral)} path={path} readonly={readonly} Custom={Custom} />
148
+ {!inline && <h3 className='text-lg mbs-2 mbe-1'>{label}</h3>}
149
+ <FormFields
150
+ schema={S.make(typeLiteral)}
151
+ path={path}
152
+ readonly={readonly}
153
+ onQueryRefOptions={onQueryRefOptions}
154
+ Custom={Custom}
155
+ lookupComponent={lookupComponent}
156
+ />
134
157
  </div>
135
158
  );
136
159
  }
@@ -139,7 +162,27 @@ export const FormField = ({ property, path, readonly, inline, lookupComponent, C
139
162
  return null;
140
163
  };
141
164
 
142
- export const FormFields = ({ schema, path, filter, sort, readonly, lookupComponent, Custom }: FormContentProps) => {
165
+ export type FormContentProps = {
166
+ schema: S.Schema.All;
167
+ path?: (string | number)[];
168
+ filter?: (props: SchemaProperty<any>[]) => SchemaProperty<any>[];
169
+ sort?: string[];
170
+ readonly?: boolean;
171
+ onQueryRefOptions?: QueryRefOptions;
172
+ lookupComponent?: ComponentLookup;
173
+ Custom?: Partial<Record<string, InputComponent>>;
174
+ };
175
+
176
+ export const FormFields = ({
177
+ schema,
178
+ path,
179
+ filter,
180
+ sort,
181
+ readonly,
182
+ onQueryRefOptions,
183
+ lookupComponent,
184
+ Custom,
185
+ }: FormContentProps) => {
143
186
  const values = useFormValues(path);
144
187
 
145
188
  const properties = useMemo(() => {
@@ -159,6 +202,7 @@ export const FormFields = ({ schema, path, filter, sort, readonly, lookupCompone
159
202
  property={property}
160
203
  path={[...(path ?? []), property.name]}
161
204
  readonly={readonly}
205
+ onQueryRefOptions={onQueryRefOptions}
162
206
  lookupComponent={lookupComponent}
163
207
  Custom={Custom}
164
208
  />
@@ -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,151 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ import {
8
+ type AST,
9
+ Expando,
10
+ getTypeAnnotation,
11
+ ReferenceAnnotationId,
12
+ type ReferenceAnnotationValue,
13
+ type TypeAnnotation,
14
+ } from '@dxos/echo-schema';
15
+ import { findAnnotation, type SimpleType } from '@dxos/effect';
16
+ import { DXN } from '@dxos/keys';
17
+ import { refFromDXN, RefImpl } from '@dxos/live-object';
18
+ import { log } from '@dxos/log';
19
+ import { type MaybePromise } from '@dxos/util';
20
+
21
+ import { SelectInput, TextInput } from './Defaults';
22
+ import { type FormInputStateProps } from './FormContext';
23
+
24
+ export type QueryRefOptions = (type: TypeAnnotation) => MaybePromise<{ dxn: DXN; label?: string }[]>;
25
+
26
+ type RefFieldProps = {
27
+ ast: AST.AST;
28
+ type: SimpleType;
29
+ label: string;
30
+ readonly?: boolean;
31
+ placeholder?: string;
32
+ inline?: boolean;
33
+ onQueryRefOptions?: QueryRefOptions;
34
+ inputProps: FormInputStateProps;
35
+ };
36
+
37
+ export const RefField = ({
38
+ ast,
39
+ type,
40
+ label,
41
+ readonly,
42
+ placeholder,
43
+ inline,
44
+ onQueryRefOptions,
45
+ inputProps,
46
+ }: RefFieldProps) => {
47
+ const { getValue, onValueChange, ...restInputProps } = inputProps;
48
+ const refTypeInfo = findAnnotation<ReferenceAnnotationValue>(ast, ReferenceAnnotationId);
49
+ const [refOptions, setRefOptions] = useState<Array<{ value: string; label?: string }>>([]);
50
+ const [loading, setLoading] = useState(false);
51
+
52
+ useEffect(() => {
53
+ if (!refTypeInfo || !onQueryRefOptions) {
54
+ return;
55
+ }
56
+
57
+ const fetchOptions = async () => {
58
+ setLoading(true);
59
+ try {
60
+ const options = await onQueryRefOptions(refTypeInfo);
61
+ setRefOptions(
62
+ options.map((option) => ({
63
+ label: option.label,
64
+ value: option.dxn.toString(),
65
+ })),
66
+ );
67
+ } catch (error) {
68
+ log.error('Failed to fetch ref options:', error);
69
+ setRefOptions([]);
70
+ } finally {
71
+ setLoading(false);
72
+ }
73
+ };
74
+
75
+ void fetchOptions();
76
+ }, [refTypeInfo, onQueryRefOptions]);
77
+
78
+ if (!refTypeInfo) {
79
+ return null;
80
+ }
81
+
82
+ // If ref type is expando, fall back to taking a DXN in string format.
83
+ if (refTypeInfo.typename === getTypeAnnotation(Expando)?.typename || !onQueryRefOptions) {
84
+ const handleOnValueChange = (_type: any, dxnString: string) => {
85
+ const dxn = DXN.tryParse(dxnString);
86
+ if (dxn) {
87
+ onValueChange?.('object', refFromDXN(dxn));
88
+ } else if (dxnString === '') {
89
+ onValueChange?.('object', undefined);
90
+ } else {
91
+ onValueChange?.('string', dxnString);
92
+ }
93
+ };
94
+
95
+ const handleGetValue = () => {
96
+ const formValue = getValue();
97
+ if (typeof formValue === 'string') {
98
+ return formValue;
99
+ }
100
+ if (formValue instanceof RefImpl) {
101
+ return formValue.dxn.toString();
102
+ }
103
+
104
+ return undefined;
105
+ };
106
+
107
+ return (
108
+ <TextInput
109
+ type={type}
110
+ label={label}
111
+ disabled={readonly}
112
+ placeholder={placeholder}
113
+ inputOnly={inline}
114
+ getValue={handleGetValue as <V>() => V | undefined}
115
+ onValueChange={handleOnValueChange}
116
+ {...restInputProps}
117
+ />
118
+ );
119
+ }
120
+
121
+ const handleGetValue = () => {
122
+ const formValue = getValue();
123
+
124
+ if (formValue instanceof RefImpl) {
125
+ return formValue.dxn.toString();
126
+ }
127
+
128
+ return undefined;
129
+ };
130
+
131
+ const handleValueChanged = (_type: any, dxnString: string) => {
132
+ const dxn = DXN.parse(dxnString);
133
+ const ref = refFromDXN(dxn);
134
+
135
+ onValueChange('object', ref);
136
+ };
137
+
138
+ return (
139
+ <SelectInput
140
+ type={type}
141
+ label={label}
142
+ disabled={readonly || loading}
143
+ placeholder={loading ? 'Loading options...' : placeholder}
144
+ inputOnly={inline}
145
+ getValue={handleGetValue as <V>() => V | undefined}
146
+ onValueChange={handleValueChanged}
147
+ options={refOptions}
148
+ {...restInputProps}
149
+ />
150
+ );
151
+ };
@@ -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 !== '-') {
@@ -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
  }
@@ -64,13 +64,25 @@ export const formatForDisplay = ({ type, format, value, locale = undefined }: Va
64
64
  });
65
65
  }
66
66
  case FormatEnum.Date: {
67
- return formatDate(new Date(value as number), 'yyyy-MM-dd');
67
+ try {
68
+ return formatDate(new Date(value as number), 'yyyy-MM-dd');
69
+ } catch (error) {
70
+ return 'Invalid Date';
71
+ }
68
72
  }
69
73
  case FormatEnum.Time: {
70
- return formatDate(new Date(value as number), 'HH:mm:ss');
74
+ try {
75
+ return formatDate(new Date(value as number), 'HH:mm:ss');
76
+ } catch (error) {
77
+ return 'Invalid Time';
78
+ }
71
79
  }
72
80
  case FormatEnum.DateTime: {
73
- return formatDate(new Date(value as number), 'yyyy-MM-dd HH:mm:ss');
81
+ try {
82
+ return formatDate(new Date(value as number), 'yyyy-MM-dd HH:mm:ss');
83
+ } catch (error) {
84
+ return 'Invalid DateTime';
85
+ }
74
86
  }
75
87
  case FormatEnum.GeoPoint: {
76
88
  if (value === null || value === undefined) {