@dxos/react-ui-form 0.8.1-staging.9eaf14f → 0.8.2-main.10c050d

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 (112) hide show
  1. package/dist/lib/browser/index.mjs +1818 -1309
  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 +1774 -1273
  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 +1818 -1309
  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.d.ts → ControlSection/ControlSection.d.ts} +9 -6
  11. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -0
  12. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +8 -0
  13. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/ControlSection/index.d.ts +2 -0
  15. package/dist/types/src/components/ControlSection/index.d.ts.map +1 -0
  16. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  17. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts +2 -0
  18. package/dist/types/src/components/FieldEditor/FieldEditor.test.d.ts.map +1 -0
  19. package/dist/types/src/components/Form/ArrayField.d.ts.map +1 -1
  20. package/dist/types/src/components/Form/Defaults.d.ts +4 -4
  21. package/dist/types/src/components/Form/Defaults.d.ts.map +1 -1
  22. package/dist/types/src/components/Form/Form.d.ts +6 -4
  23. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  24. package/dist/types/src/components/Form/Form.stories.d.ts +38 -36
  25. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Form/FormActions.d.ts +1 -1
  27. package/dist/types/src/components/Form/FormActions.d.ts.map +1 -1
  28. package/dist/types/src/components/Form/FormContent.d.ts +16 -12
  29. package/dist/types/src/components/Form/FormContent.d.ts.map +1 -1
  30. package/dist/types/src/components/Form/FormContext.d.ts +2 -2
  31. package/dist/types/src/components/Form/FormContext.d.ts.map +1 -1
  32. package/dist/types/src/components/Form/Input.d.ts.map +1 -1
  33. package/dist/types/src/components/Form/RefField.d.ts +12 -0
  34. package/dist/types/src/components/Form/RefField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +1 -1
  36. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +1 -1
  37. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +1 -1
  39. package/dist/types/src/components/Form/factory.d.ts +1 -1
  40. package/dist/types/src/components/Form/factory.d.ts.map +1 -1
  41. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  42. package/dist/types/src/components/ViewEditor/Test.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +2 -2
  44. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +5 -3
  46. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/deprecated/Form.d.ts +2 -2
  48. package/dist/types/src/components/deprecated/Form.d.ts.map +1 -1
  49. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +1 -1
  50. package/dist/types/src/components/deprecated/FormInput.d.ts.map +1 -1
  51. package/dist/types/src/components/index.d.ts.map +1 -1
  52. package/dist/types/src/components/testing.d.ts.map +1 -1
  53. package/dist/types/src/hooks/index.d.ts +2 -0
  54. package/dist/types/src/hooks/index.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useForm.d.ts +2 -2
  56. package/dist/types/src/hooks/useForm.d.ts.map +1 -1
  57. package/dist/types/src/hooks/useQueryRefOptions.d.ts +23 -0
  58. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +1 -0
  59. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +8 -0
  60. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +1 -0
  61. package/dist/types/src/translations.d.ts +5 -0
  62. package/dist/types/src/translations.d.ts.map +1 -1
  63. package/dist/types/src/util/field.d.ts.map +1 -1
  64. package/dist/types/src/util/formatting.d.ts.map +1 -1
  65. package/dist/types/src/util/index.d.ts +2 -0
  66. package/dist/types/src/util/index.d.ts.map +1 -1
  67. package/dist/types/src/util/refs.d.ts +9 -0
  68. package/dist/types/src/util/refs.d.ts.map +1 -0
  69. package/dist/types/src/util/schema.d.ts +3 -0
  70. package/dist/types/src/util/schema.d.ts.map +1 -0
  71. package/dist/types/src/vitest-setup.d.ts +2 -0
  72. package/dist/types/src/vitest-setup.d.ts.map +1 -0
  73. package/dist/types/tsconfig.tsbuildinfo +1 -1
  74. package/package.json +28 -25
  75. package/src/components/ControlSection/ControlSection.stories.tsx +29 -0
  76. package/src/components/{ControlSection.tsx → ControlSection/ControlSection.tsx} +40 -16
  77. package/src/components/ControlSection/index.ts +5 -0
  78. package/src/components/FieldEditor/FieldEditor.stories.tsx +1 -1
  79. package/src/components/FieldEditor/FieldEditor.test.tsx +49 -0
  80. package/src/components/FieldEditor/FieldEditor.tsx +3 -2
  81. package/src/components/Form/ArrayField.tsx +63 -50
  82. package/src/components/Form/Defaults.tsx +77 -48
  83. package/src/components/Form/Form.stories.tsx +134 -109
  84. package/src/components/Form/Form.tsx +8 -5
  85. package/src/components/Form/FormActions.tsx +15 -18
  86. package/src/components/Form/FormContent.tsx +116 -46
  87. package/src/components/Form/FormContext.tsx +24 -10
  88. package/src/components/Form/Input.tsx +4 -10
  89. package/src/components/Form/RefField.tsx +217 -0
  90. package/src/components/Form/custom/GeoPointInput.tsx +2 -2
  91. package/src/components/Form/custom/SelectOptionsInput.stories.tsx +6 -5
  92. package/src/components/Form/custom/SelectOptionsInput.tsx +2 -2
  93. package/src/components/Form/factory.tsx +1 -1
  94. package/src/components/Form/index.ts +0 -1
  95. package/src/components/ViewEditor/ViewEditor.stories.tsx +19 -9
  96. package/src/components/ViewEditor/ViewEditor.tsx +65 -35
  97. package/src/components/deprecated/Form.stories.tsx +6 -6
  98. package/src/components/deprecated/Form.tsx +6 -6
  99. package/src/components/deprecated/FormInput.tsx +1 -1
  100. package/src/components/index.ts +1 -0
  101. package/src/components/testing.tsx +5 -9
  102. package/src/hooks/index.ts +3 -1
  103. package/src/hooks/useForm.ts +4 -3
  104. package/src/hooks/useQueryRefOptions.ts +53 -0
  105. package/src/hooks/useRefQueryLookupHandler.ts +39 -0
  106. package/src/translations.ts +9 -2
  107. package/src/util/formatting.ts +15 -3
  108. package/src/util/index.ts +3 -1
  109. package/src/util/refs.ts +38 -0
  110. package/src/util/schema.ts +14 -0
  111. package/src/vitest-setup.ts +11 -0
  112. package/dist/types/src/components/ControlSection.d.ts.map +0 -1
@@ -5,20 +5,23 @@
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';
14
15
  import { ViewProjection, ViewType, createView } from '@dxos/schema';
15
16
  import { withTheme, withLayout } from '@dxos/storybook-utils';
16
17
 
17
- import { ViewEditor } from './ViewEditor';
18
+ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
18
19
  import translations from '../../translations';
19
20
  import { TestLayout, TestPanel } from '../testing';
20
21
 
21
- const DefaultStory = () => {
22
+ type StoryProps = Pick<ViewEditorProps, 'readonly'>;
23
+
24
+ const DefaultStory = (props: StoryProps) => {
22
25
  const space = useSpace();
23
26
  const [schema, setSchema] = useState<EchoSchema>();
24
27
  const [view, setView] = useState<ViewType>();
@@ -26,7 +29,7 @@ const DefaultStory = () => {
26
29
  useAsyncEffect(async () => {
27
30
  if (space) {
28
31
  class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
29
- name: S.String,
32
+ name: Schema.String,
30
33
  email: Format.Email,
31
34
  salary: Format.Currency(),
32
35
  }) {}
@@ -41,7 +44,7 @@ const DefaultStory = () => {
41
44
  }
42
45
  }, [space]);
43
46
 
44
- const views = useQuery(space, Filter.schema(ViewType));
47
+ const views = useQuery(space, Filter.type(ViewType));
45
48
  const currentTypename = useMemo(() => view?.query?.typename, [view]);
46
49
  const updateViewTypename = useCallback(
47
50
  (newTypename: string) => {
@@ -56,6 +59,7 @@ const DefaultStory = () => {
56
59
  },
57
60
  [views, schema],
58
61
  );
62
+
59
63
  const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
60
64
 
61
65
  if (!schema || !view || !projection) {
@@ -69,6 +73,7 @@ const DefaultStory = () => {
69
73
  schema={schema}
70
74
  view={view}
71
75
  registry={space?.db.schemaRegistry}
76
+ readonly={props.readonly}
72
77
  onTypenameChanged={updateViewTypename}
73
78
  onDelete={handleDelete}
74
79
  />
@@ -77,11 +82,10 @@ const DefaultStory = () => {
77
82
  );
78
83
  };
79
84
 
80
- const meta: Meta<typeof ViewEditor> = {
85
+ const meta: Meta<StoryProps> = {
81
86
  title: 'ui/react-ui-form/ViewEditor',
82
- component: ViewEditor,
83
87
  render: DefaultStory,
84
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true, tooltips: true }), withTheme],
88
+ decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
85
89
  parameters: {
86
90
  translations,
87
91
  },
@@ -89,6 +93,12 @@ const meta: Meta<typeof ViewEditor> = {
89
93
 
90
94
  export default meta;
91
95
 
92
- type Story = StoryObj;
96
+ type Story = StoryObj<StoryProps>;
93
97
 
94
98
  export const Default: Story = {};
99
+
100
+ export const Readonly: Story = {
101
+ args: {
102
+ readonly: true,
103
+ },
104
+ };
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Schema } 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 { EchoSchema, Format, type JsonProp, isMutable, toJsonSchema } from '@dxos/echo-schema';
9
10
  import { invariant } from '@dxos/invariant';
10
- import { IconButton, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
+ import { Icon, IconButton, Message, type ThemedClassName, useTranslation } from '@dxos/react-ui';
11
12
  import { List } from '@dxos/react-ui-list';
12
13
  import { ghostHover, inputTextLabel, mx } from '@dxos/react-ui-theme';
13
14
  import { FieldSchema, type FieldType, type ViewType, ViewProjection, VIEW_FIELD_LIMIT } from '@dxos/schema';
@@ -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
+ title: 'View',
24
25
  }),
25
26
  typename: Format.URL.annotations({
26
- [AST.TitleAnnotationId]: 'Typename',
27
+ title: '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,13 @@ 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(() => {
58
+ // Use reactive and mutable version of json schema when schema is mutable.
59
+ const jsonSchema = schema instanceof EchoSchema ? schema.jsonSchema : toJsonSchema(schema);
60
+ return new ViewProjection(jsonSchema, view);
61
+ }, [schema, view]);
57
62
  const [field, setField] = useState<FieldType>();
63
+ const immutable = readonly || !isMutable(schema);
58
64
 
59
65
  // TODO(burdon): Should be reactive.
60
66
  const viewValues = useMemo(() => {
@@ -66,37 +72,43 @@ export const ViewEditor = ({
66
72
  };
67
73
  }, [view]);
68
74
 
69
- const handleSelect = useCallback((field: FieldType) => {
70
- setField((f) => (f === field ? undefined : field));
71
- }, []);
75
+ const handleSelect = useCallback(
76
+ (field: FieldType) => {
77
+ if (immutable) {
78
+ return;
79
+ }
80
+ setField((f) => (f === field ? undefined : field));
81
+ },
82
+ [immutable],
83
+ );
72
84
 
73
85
  // TODO(burdon): Check if mutable; variant of useCallback that return undefined if readonly?
74
86
 
75
87
  const handleAdd = useCallback(() => {
76
- invariant(!schema.readonly);
88
+ invariant(!immutable);
77
89
  const field = projection.createFieldProjection();
78
90
  setField(field);
79
91
  }, [schema, view]);
80
92
 
81
93
  const handleUpdate = useCallback(
82
94
  ({ name, typename }: ViewMetaType) => {
83
- invariant(!schema.readonly);
95
+ invariant(!immutable);
84
96
  requestAnimationFrame(() => {
85
97
  if (view.name !== name) {
86
98
  view.name = name;
87
99
  }
88
100
 
89
- if (view.query.typename !== typename) {
101
+ if (view.query.typename !== typename && !immutable) {
90
102
  onTypenameChanged?.(typename);
91
103
  }
92
104
  });
93
105
  },
94
- [schema, view, onTypenameChanged],
106
+ [schema, view, onTypenameChanged, immutable],
95
107
  );
96
108
 
97
109
  const handleDelete = useCallback(
98
110
  (fieldId: string) => {
99
- invariant(!schema.readonly);
111
+ invariant(!immutable);
100
112
  if (fieldId === field?.id) {
101
113
  setField(undefined);
102
114
  }
@@ -108,7 +120,7 @@ export const ViewEditor = ({
108
120
 
109
121
  const handleMove = useCallback(
110
122
  (fromIndex: number, toIndex: number) => {
111
- invariant(!schema.readonly);
123
+ invariant(!immutable);
112
124
  // NOTE(ZaymonFC): Using arrayMove here causes a race condition with the kanban model.
113
125
  const fields = [...view.fields];
114
126
  const [moved] = fields.splice(fromIndex, 1);
@@ -139,15 +151,28 @@ export const ViewEditor = ({
139
151
  );
140
152
 
141
153
  return (
142
- <div role='none' className={mx('flex flex-col w-full divide-y divide-separator', classNames)}>
143
- <Form<ViewMetaType>
144
- autoSave
145
- schema={ViewMetaSchema}
146
- values={viewValues}
147
- onSave={schema.readonly ? undefined : handleUpdate}
148
- />
149
-
150
- <div>
154
+ <div role='none' className={mx('overflow-y-auto', classNames)}>
155
+ <div role='none' className='p-2'>
156
+ <div role='none' className='mbe-2'>
157
+ {immutable && (
158
+ <Message.Root valence='neutral' className='rounded'>
159
+ <Message.Title>
160
+ <Icon icon='ph--info--regular' size={5} classNames='inline' /> {t('system schema title')}
161
+ </Message.Title>
162
+ <Message.Body>{t('system schema description')}</Message.Body>
163
+ </Message.Root>
164
+ )}
165
+ </div>
166
+ <Form<ViewMetaType>
167
+ autoSave
168
+ schema={ViewMetaSchema}
169
+ values={viewValues}
170
+ onSave={handleUpdate}
171
+ classNames='min-bs-0 overflow-y-auto'
172
+ />
173
+ </div>
174
+
175
+ <div role='none' className='min-bs-0 overflow-y-auto'>
151
176
  {/* TODO(burdon): Clean up common form ux. */}
152
177
  <div role='none' className='p-2'>
153
178
  <label className={mx(inputTextLabel)}>{t('fields label')}</label>
@@ -155,9 +180,9 @@ export const ViewEditor = ({
155
180
 
156
181
  <List.Root<FieldType>
157
182
  items={view.fields}
158
- isItem={S.is(FieldSchema)}
183
+ isItem={Schema.is(FieldSchema)}
159
184
  getId={(field) => field.id}
160
- onMove={schema.readonly ? undefined : handleMove}
185
+ onMove={immutable ? undefined : handleMove}
161
186
  >
162
187
  {({ items: fields }) => (
163
188
  <>
@@ -170,7 +195,11 @@ export const ViewEditor = ({
170
195
 
171
196
  <div role='list' className='flex flex-col w-full'>
172
197
  {fields?.map((field) => (
173
- <List.Item<FieldType> key={field.id} item={field} classNames={mx(grid, ghostHover, 'cursor-pointer')}>
198
+ <List.Item<FieldType>
199
+ key={field.id}
200
+ item={field}
201
+ classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
202
+ >
174
203
  <List.ItemDragHandle />
175
204
  <List.ItemTitle onClick={() => handleSelect(field)}>{field.path}</List.ItemTitle>
176
205
  <div className='flex items-center gap-2 -ml-4'>
@@ -179,7 +208,7 @@ export const ViewEditor = ({
179
208
  disabled={view.fields.length <= 1}
180
209
  onClick={() => handleHide(field.id)}
181
210
  />
182
- {!schema.readonly && (
211
+ {!immutable && (
183
212
  <List.ItemDeleteButton
184
213
  icon='ph--trash--regular'
185
214
  disabled={view.fields.length <= 1}
@@ -211,7 +240,7 @@ export const ViewEditor = ({
211
240
  <List.Item<string>
212
241
  key={property}
213
242
  item={property}
214
- classNames={mx(grid, ghostHover, 'cursor-pointer')}
243
+ classNames={mx(grid, ghostHover, !immutable && 'cursor-pointer')}
215
244
  >
216
245
  <div />
217
246
  <List.ItemTitle>{property}</List.ItemTitle>
@@ -237,13 +266,14 @@ export const ViewEditor = ({
237
266
  )}
238
267
 
239
268
  {!readonly && !field && (
240
- <div className='flex p-2 justify-center'>
269
+ <div role='none' className='p-2'>
241
270
  <IconButton
242
271
  icon='ph--plus--regular'
243
272
  label={t('button add property')}
244
- onClick={schema.readonly ? undefined : handleAdd}
273
+ onClick={immutable ? undefined : handleAdd}
245
274
  // TODO(burdon): Show field limit in ux (not tooltip).
246
275
  disabled={view.fields.length >= VIEW_FIELD_LIMIT}
276
+ classNames='flex is-full'
247
277
  />
248
278
  </div>
249
279
  )}
@@ -7,7 +7,7 @@ import '@dxos-theme';
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
8
  import React from 'react';
9
9
 
10
- import { create } from '@dxos/live-object';
10
+ import { live } from '@dxos/live-object';
11
11
  import { testData, TestSchema, testView, type TestType } from '@dxos/schema/testing';
12
12
  import { withLayout, withTheme } from '@dxos/storybook-utils';
13
13
 
@@ -39,23 +39,23 @@ type Story = StoryObj<DeprecatedFormProps<TestType>>;
39
39
 
40
40
  export const Default: Story = {
41
41
  args: {
42
- object: create(testData),
43
- view: create(testView),
42
+ object: live(testData),
43
+ view: live(testView),
44
44
  schema: TestSchema,
45
45
  },
46
46
  };
47
47
 
48
48
  export const Empty: Story = {
49
49
  args: {
50
- view: create(testView),
50
+ view: live(testView),
51
51
  schema: TestSchema,
52
52
  },
53
53
  };
54
54
 
55
55
  export const Readonly: Story = {
56
56
  args: {
57
- object: create(testData),
58
- view: create(testView),
57
+ object: live(testData),
58
+ view: live(testView),
59
59
  schema: TestSchema,
60
60
  readonly: true,
61
61
  },
@@ -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);
@@ -12,7 +12,7 @@ import { mx, textBlockWidth } from '@dxos/react-ui-theme';
12
12
  */
13
13
  export const DeprecatedFormContainer = ({ children, classNames }: ThemedClassName<PropsWithChildren>) => {
14
14
  return (
15
- <div role='none' className='w-full p-4 justify-center overflow-x-hidden overflow-y-auto'>
15
+ <div role='none' className='container-max-width p-4 justify-center overflow-x-hidden overflow-y-auto'>
16
16
  <div role='form' className={mx('flex flex-col', textBlockWidth, classNames)}>
17
17
  {children}
18
18
  </div>
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  export * from './deprecated';
6
+
6
7
  export * from './ControlSection';
7
8
  export * from './FieldEditor';
8
9
  export * from './Form';
@@ -5,20 +5,16 @@
5
5
  import React, { type PropsWithChildren } from 'react';
6
6
 
7
7
  import { type ThemedClassName } from '@dxos/react-ui';
8
- import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
8
+ import { JsonFilter } from '@dxos/react-ui-syntax-highlighter';
9
9
  import { mx } from '@dxos/react-ui-theme';
10
10
 
11
11
  export const TestPanel = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
12
- <div className='flex flex-col p-4'>
13
- <div className={mx('flex flex-col border border-separator rounded w-[300px]', classNames)}>{children}</div>
14
- </div>
12
+ <div className={mx('w-full max-w-[30rem] border border-separator p-2 rounded', classNames)}>{children}</div>
15
13
  );
16
14
 
17
15
  export const TestLayout = ({ classNames, json, children }: ThemedClassName<PropsWithChildren<{ json?: any }>>) => (
18
- <div className='w-full h-full grid grid-cols-3'>
19
- <div className={mx('flex col-span-2 w-full justify-center overflow-y-auto', classNames)}>{children}</div>
20
- <SyntaxHighlighter language='json' className='w-full text-xs font-thin'>
21
- {JSON.stringify(json, null, 2)}
22
- </SyntaxHighlighter>
16
+ <div className='w-full h-full grid grid-cols-[1fr_1fr]'>
17
+ <div className={mx('m-2 justify-center overflow-y-auto', classNames)}>{children}</div>
18
+ <JsonFilter data={json} classNames='w-full text-xs' testId='debug' />
23
19
  </div>
24
20
  );
@@ -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,39 @@
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
+ return 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
+ };
@@ -10,6 +10,7 @@ export default [
10
10
  [translationKey]: {
11
11
  'fields label': 'Fields',
12
12
  'hidden fields label': 'Hidden Fields',
13
+ 'empty readonly ref field label': '(none)',
13
14
 
14
15
  // TODO(burdon): Standardize field/property.
15
16
  'button add property': 'Add property',
@@ -20,6 +21,8 @@ export default [
20
21
  'add field': 'Add field',
21
22
  'delete field': 'Delete field',
22
23
  'field limit reached': 'Maximum number of fields reached',
24
+ 'boolean input true value': 'Yes',
25
+ 'boolean input false value': 'No',
23
26
 
24
27
  // TODO(burdon): Factor out?
25
28
  'button cancel': 'Cancel',
@@ -52,16 +55,20 @@ export default [
52
55
  'format uri': 'URI',
53
56
  'format uuid': 'UUID',
54
57
 
55
- // Placeholders
58
+ // Placeholders.
56
59
  'placeholder latitude': 'Latitude',
57
60
  'placeholder longitude': 'Longitude',
58
61
 
59
- // SelectOptionsInput
62
+ // SelectOptionsInput.
60
63
  'select option label': 'Label',
61
64
  'select option label placeholder': 'Option label',
62
65
  'select option color': 'Color',
63
66
  'select option delete': 'Delete',
64
67
  'select option add': 'Add option',
68
+
69
+ // System schema message.
70
+ 'system schema title': 'System Schema',
71
+ 'system schema description': 'This schema is managed by the system and cannot be modified.',
65
72
  },
66
73
  },
67
74
  },
@@ -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) {
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';