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

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 (81) hide show
  1. package/dist/lib/browser/index.mjs +731 -573
  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 +748 -598
  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 +731 -573
  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 +2 -2
  34. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/deprecated/Form.d.ts +2 -2
  37. package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
  38. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  40. package/dist/types/src/components/testing.d.ts.map +1 -1
  41. package/dist/types/src/hooks/index.d.ts +2 -0
  42. package/dist/types/src/hooks/index.d.ts.map +1 -1
  43. package/dist/types/src/hooks/useForm.d.ts +2 -2
  44. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  46. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  47. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  48. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  49. package/dist/types/src/util/field.d.ts.map +1 -1
  50. package/dist/types/src/util/formatting.d.ts.map +1 -1
  51. package/dist/types/src/util/index.d.ts +2 -0
  52. package/dist/types/src/util/index.d.ts.map +1 -1
  53. package/dist/types/src/util/refs.d.ts +9 -0
  54. package/dist/types/src/util/refs.d.ts.map +1 -0
  55. package/dist/types/src/util/schema.d.ts +3 -0
  56. package/dist/types/src/util/schema.d.ts.map +1 -0
  57. package/dist/types/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +26 -25
  59. package/src/components/ControlSection/ControlSection.stories.tsx +1 -1
  60. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  61. package/src/components/Form/ArrayField.tsx +10 -16
  62. package/src/components/Form/Defaults.tsx +4 -1
  63. package/src/components/Form/Form.stories.tsx +48 -42
  64. package/src/components/Form/Form.tsx +4 -4
  65. package/src/components/Form/FormContent.tsx +49 -22
  66. package/src/components/Form/FormContext.tsx +3 -1
  67. package/src/components/Form/Input.tsx +3 -9
  68. package/src/components/Form/RefField.tsx +147 -101
  69. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
  70. package/src/components/Form/factory.tsx +1 -1
  71. package/src/components/Form/index.ts +0 -1
  72. package/src/components/ViewEditor/ViewEditor.stories.tsx +5 -4
  73. package/src/components/ViewEditor/ViewEditor.tsx +23 -20
  74. package/src/components/deprecated/Form.tsx +6 -6
  75. package/src/hooks/index.ts +3 -1
  76. package/src/hooks/useForm.ts +4 -3
  77. package/src/hooks/useQueryRefOptions.ts +53 -0
  78. package/src/hooks/useRefQueryLookupHandler.ts +41 -0
  79. package/src/util/index.ts +3 -1
  80. package/src/util/refs.ts +38 -0
  81. package/src/util/schema.ts +14 -0
@@ -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 JsonProp, isMutable, toJsonSchema } 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,19 +19,19 @@ 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
- schema: BaseSchema;
34
+ schema: Schema.Schema.AnyNoContext;
34
35
  view: ViewType;
35
36
  registry?: SchemaRegistry;
36
37
  readonly?: boolean;
@@ -53,8 +54,9 @@ export const ViewEditor = ({
53
54
  onDelete,
54
55
  }: ViewEditorProps) => {
55
56
  const { t } = useTranslation(translationKey);
56
- const projection = useMemo(() => new ViewProjection(schema.jsonSchema, view), [schema, view]);
57
+ const projection = useMemo(() => new ViewProjection(toJsonSchema(schema), view), [schema, view]);
57
58
  const [field, setField] = useState<FieldType>();
59
+ const immutable = readonly || !isMutable(schema);
58
60
 
59
61
  // TODO(burdon): Should be reactive.
60
62
  const viewValues = useMemo(() => {
@@ -73,14 +75,14 @@ export const ViewEditor = ({
73
75
  // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
74
76
 
75
77
  const handleAdd = useCallback(() => {
76
- invariant(!schema.readonly);
78
+ invariant(!immutable);
77
79
  const field = projection.createFieldProjection();
78
80
  setField(field);
79
81
  }, [schema, view]);
80
82
 
81
83
  const handleUpdate = useCallback(
82
84
  ({ name, typename }: ViewMetaType) => {
83
- invariant(!schema.readonly);
85
+ invariant(!immutable);
84
86
  requestAnimationFrame(() => {
85
87
  if (view.name !== name) {
86
88
  view.name = name;
@@ -96,7 +98,7 @@ export const ViewEditor = ({
96
98
 
97
99
  const handleDelete = useCallback(
98
100
  (fieldId: string) => {
99
- invariant(!schema.readonly);
101
+ invariant(!immutable);
100
102
  if (fieldId === field?.id) {
101
103
  setField(undefined);
102
104
  }
@@ -108,7 +110,7 @@ export const ViewEditor = ({
108
110
 
109
111
  const handleMove = useCallback(
110
112
  (fromIndex: number, toIndex: number) => {
111
- invariant(!schema.readonly);
113
+ invariant(!immutable);
112
114
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
113
115
  const fields = [...view.fields];
114
116
  const [moved] = fields.splice(fromIndex, 1);
@@ -139,15 +141,16 @@ export const ViewEditor = ({
139
141
  );
140
142
 
141
143
  return (
142
- <div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
144
+ <div role='none' className={mx('grid grid-rows-[1fr_1fr] min-bs-0', classNames)}>
143
145
  <Form<ViewMetaType>
144
146
  autoSave
145
147
  schema={ViewMetaSchema}
146
148
  values={viewValues}
147
- onSave={schema.readonly ? undefined : handleUpdate}
149
+ onSave={immutable ? undefined : handleUpdate}
150
+ classNames='min-bs-0 overflow-y-auto'
148
151
  />
149
152
 
150
- <div>
153
+ <div role='none' className='border-bs border-separator min-bs-0 overflow-y-auto'>
151
154
  {/* TODO(burdon): Clean up common form ux. */}
152
155
  <div role='none' className='p-2'>
153
156
  <label className={mx(inputTextLabel)}>{t('fields label')}</label>
@@ -155,9 +158,9 @@ export const ViewEditor = ({
155
158
 
156
159
  <List.Root<FieldType>
157
160
  items={view.fields}
158
- isItem={S.is(FieldSchema)}
161
+ isItem={Schema.is(FieldSchema)}
159
162
  getId={(field) => field.id}
160
- onMove={schema.readonly ? undefined : handleMove}
163
+ onMove={immutable ? undefined : handleMove}
161
164
  >
162
165
  {({ items: fields }) => (
163
166
  <>
@@ -179,7 +182,7 @@ export const ViewEditor = ({
179
182
  disabled={view.fields.length <= 1}
180
183
  onClick={() => handleHide(field.id)}
181
184
  />
182
- {!schema.readonly && (
185
+ {!immutable && (
183
186
  <List.ItemDeleteButton
184
187
  icon='ph--trash--regular'
185
188
  disabled={view.fields.length <= 1}
@@ -241,7 +244,7 @@ export const ViewEditor = ({
241
244
  <IconButton
242
245
  icon='ph--plus--regular'
243
246
  label={t('button add property')}
244
- onClick={schema.readonly ? undefined : handleAdd}
247
+ onClick={immutable ? undefined : handleAdd}
245
248
  // TODO(burdon): Show field limit in ux (not tooltip).
246
249
  disabled={view.fields.length >= VIEW_FIELD_LIMIT}
247
250
  />
@@ -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,9 +90,10 @@ 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
97
  useEffect(() => {
97
98
  setValues(initialValues);
98
99
  }, [initialValues]);
@@ -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
+ };