@dxos/react-ui-form 0.8.2-main.36232bc → 0.8.2-main.5ca3450

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 (63) hide show
  1. package/dist/lib/browser/index.mjs +275 -117
  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 +258 -103
  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 +275 -117
  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 +6 -3
  15. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  16. package/dist/types/src/components/Form/Form.stories.d.ts +35 -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 +14 -11
  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 +24 -0
  24. package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
  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.map +1 -1
  30. package/dist/types/src/components/Form/index.d.ts +1 -0
  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 +1 -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/useRefQueryLookupHandler.d.ts +8 -0
  45. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  46. package/dist/types/src/util/field.d.ts.map +1 -1
  47. package/dist/types/src/util/formatting.d.ts.map +1 -1
  48. package/dist/types/tsconfig.tsbuildinfo +1 -1
  49. package/package.json +25 -25
  50. package/src/components/Form/ArrayField.tsx +5 -6
  51. package/src/components/Form/Defaults.tsx +4 -1
  52. package/src/components/Form/Form.stories.tsx +86 -38
  53. package/src/components/Form/Form.tsx +7 -2
  54. package/src/components/Form/FormContent.tsx +62 -19
  55. package/src/components/Form/RefField.tsx +152 -0
  56. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +5 -4
  57. package/src/components/Form/index.ts +1 -1
  58. package/src/components/ViewEditor/ViewEditor.stories.tsx +3 -2
  59. package/src/components/ViewEditor/ViewEditor.tsx +12 -10
  60. package/src/components/deprecated/Form.tsx +6 -6
  61. package/src/hooks/index.ts +1 -0
  62. package/src/hooks/useForm.ts +3 -3
  63. package/src/hooks/useRefQueryLookupHandler.ts +43 -0
@@ -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
+ };
@@ -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[] }>({
@@ -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
  >
@@ -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);
@@ -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
+ };