@dxos/react-ui-form 0.8.4-main.1da679c → 0.8.4-main.21d9917

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 (180) hide show
  1. package/dist/lib/browser/index.mjs +2188 -2119
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +2188 -2119
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/ControlSection/ControlSection.d.ts +2 -2
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +14 -10
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +6 -5
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +16 -9
  14. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
  16. package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Form/Form.d.ts +81 -26
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +63 -87
  20. package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/Form/FormField.d.ts +37 -0
  22. package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
  23. package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
  24. package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
  25. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
  26. package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
  27. package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
  28. package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
  29. package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
  30. package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
  31. package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
  32. package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
  33. package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
  34. package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
  35. package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
  36. package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
  37. package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
  38. package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
  39. package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
  40. package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
  41. package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
  42. package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
  43. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
  44. package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
  45. package/dist/types/src/components/Form/{custom/SelectOptionsInput.stories.d.ts → fields/SelectOptionField.stories.d.ts} +13 -9
  46. package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
  47. package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
  48. package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
  49. package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
  50. package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
  51. package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
  52. package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
  53. package/dist/types/src/components/Form/fields/index.d.ts +12 -0
  54. package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
  55. package/dist/types/src/components/Form/index.d.ts +2 -4
  56. package/dist/types/src/components/Form/index.d.ts.map +1 -1
  57. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
  58. package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
  59. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
  60. package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
  61. package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
  62. package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
  63. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -10
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +17 -13
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +1 -0
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/components/testing.d.ts +0 -2
  70. package/dist/types/src/components/testing.d.ts.map +1 -1
  71. package/dist/types/src/hooks/index.d.ts +2 -3
  72. package/dist/types/src/hooks/index.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
  74. package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
  75. package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
  76. package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +12 -8
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/util/field.d.ts +3 -3
  80. package/dist/types/src/util/field.d.ts.map +1 -1
  81. package/dist/types/src/util/formatting.d.ts +2 -2
  82. package/dist/types/src/util/formatting.d.ts.map +1 -1
  83. package/dist/types/src/util/index.d.ts +1 -1
  84. package/dist/types/src/util/index.d.ts.map +1 -1
  85. package/dist/types/src/util/properties.d.ts +7 -0
  86. package/dist/types/src/util/properties.d.ts.map +1 -0
  87. package/dist/types/src/util/refs.d.ts +2 -3
  88. package/dist/types/src/util/refs.d.ts.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +50 -44
  91. package/src/components/ControlSection/ControlSection.stories.tsx +4 -5
  92. package/src/components/ControlSection/ControlSection.tsx +8 -8
  93. package/src/components/FieldEditor/FieldEditor.stories.tsx +46 -24
  94. package/src/components/FieldEditor/FieldEditor.tsx +77 -77
  95. package/src/components/Form/Form-old.stories.tsx +314 -0
  96. package/src/components/Form/Form.stories.tsx +151 -251
  97. package/src/components/Form/Form.tsx +319 -100
  98. package/src/components/Form/FormField.tsx +286 -0
  99. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  100. package/src/components/Form/FormFieldComponent.tsx +174 -0
  101. package/src/components/Form/FormFieldSet.tsx +125 -0
  102. package/src/components/Form/README.md +3 -0
  103. package/src/components/Form/fields/ArrayField.tsx +151 -0
  104. package/src/components/Form/fields/BooleanField.tsx +22 -0
  105. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  106. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  107. package/src/components/Form/fields/NumberField.tsx +39 -0
  108. package/src/components/Form/fields/RefField.tsx +181 -0
  109. package/src/components/Form/fields/SelectField.tsx +65 -0
  110. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +21 -19
  111. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +22 -16
  112. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  113. package/src/components/Form/fields/TextField.tsx +38 -0
  114. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +15 -8
  115. package/src/components/Form/fields/index.ts +15 -0
  116. package/src/components/Form/index.ts +3 -4
  117. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
  118. package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
  119. package/src/components/ObjectPicker/index.ts +5 -0
  120. package/src/components/ViewEditor/ViewEditor.stories.tsx +112 -68
  121. package/src/components/ViewEditor/ViewEditor.test.tsx +12 -7
  122. package/src/components/ViewEditor/ViewEditor.tsx +283 -184
  123. package/src/components/index.ts +1 -0
  124. package/src/components/testing.tsx +9 -7
  125. package/src/hooks/index.ts +2 -3
  126. package/src/hooks/useFormHandler.ts +356 -0
  127. package/src/hooks/useKeyHandler.tsx +37 -0
  128. package/src/translations.ts +14 -11
  129. package/src/util/field.ts +25 -25
  130. package/src/util/formatting.ts +22 -22
  131. package/src/util/index.ts +1 -1
  132. package/src/util/properties.ts +18 -0
  133. package/src/util/refs.ts +12 -18
  134. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  135. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  136. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  137. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  138. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  139. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  140. package/dist/types/src/components/Form/FormContent.d.ts +0 -63
  141. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  142. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  143. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  144. package/dist/types/src/components/Form/Input.d.ts +0 -29
  145. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  146. package/dist/types/src/components/Form/RefField.d.ts +0 -18
  147. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  148. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  149. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  150. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  151. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  152. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  154. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  156. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  157. package/dist/types/src/components/Form/factory.d.ts +0 -8
  158. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  159. package/dist/types/src/hooks/useForm.d.ts +0 -67
  160. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  161. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  162. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  163. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  164. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  165. package/dist/types/src/util/schema.d.ts +0 -3
  166. package/dist/types/src/util/schema.d.ts.map +0 -1
  167. package/src/components/Form/ArrayField.tsx +0 -124
  168. package/src/components/Form/Defaults.tsx +0 -205
  169. package/src/components/Form/FormActions.tsx +0 -54
  170. package/src/components/Form/FormContent.tsx +0 -302
  171. package/src/components/Form/FormContext.tsx +0 -110
  172. package/src/components/Form/Input.tsx +0 -54
  173. package/src/components/Form/RefField.tsx +0 -299
  174. package/src/components/Form/custom/GeoPointInput.tsx +0 -80
  175. package/src/components/Form/custom/index.ts +0 -7
  176. package/src/components/Form/factory.tsx +0 -31
  177. package/src/hooks/useForm.ts +0 -293
  178. package/src/hooks/useQueryRefOptions.ts +0 -53
  179. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  180. package/src/util/schema.ts +0 -14
@@ -0,0 +1,175 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type * as Schema from 'effect/Schema';
6
+ import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
7
+
8
+ import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
9
+ import { Combobox, useSearchListInput, useSearchListResults } from '@dxos/react-ui-searchlist';
10
+
11
+ import { translationKey } from '../../translations';
12
+ import { Form } from '../Form';
13
+
14
+ export type RefOption = {
15
+ id: string;
16
+ label: string;
17
+ hue?: Palette;
18
+ };
19
+
20
+ export type ObjectPickerContentProps = ThemedClassName<{
21
+ options: RefOption[];
22
+ selectedIds?: string[];
23
+ createOptionLabel?: [string, { ns: string }];
24
+ createOptionIcon?: string;
25
+ createSchema?: Schema.Schema.AnyNoContext;
26
+ createInitialValuePath?: string;
27
+ onCreate?: (values: any) => void;
28
+ onSelect: (id: string) => void;
29
+ }>;
30
+
31
+ const CreateItem = ({
32
+ createOptionLabel,
33
+ createOptionIcon,
34
+ onCreateItemSelect,
35
+ }: {
36
+ createOptionLabel: [string, { ns: string }];
37
+ createOptionIcon: string;
38
+ onCreateItemSelect: (query: string) => void;
39
+ }) => {
40
+ const { t } = useTranslation(translationKey);
41
+ const { query } = useSearchListInput();
42
+
43
+ return (
44
+ <Combobox.Item
45
+ value='__create__'
46
+ label={t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })}
47
+ icon={createOptionIcon}
48
+ classNames='flex items-center gap-2'
49
+ closeOnSelect={false}
50
+ onSelect={() => {
51
+ onCreateItemSelect(query);
52
+ }}
53
+ />
54
+ );
55
+ };
56
+
57
+ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
58
+ (
59
+ {
60
+ options,
61
+ selectedIds,
62
+ createSchema,
63
+ createOptionLabel,
64
+ createOptionIcon,
65
+ createInitialValuePath,
66
+ onSelect,
67
+ onCreate,
68
+ ...props
69
+ },
70
+ ref,
71
+ ) => {
72
+ const { t } = useTranslation(translationKey);
73
+ const [showForm, setShowForm] = useState(false);
74
+ const [formInitialValue, setFormInitialValue] = useState<string>('');
75
+
76
+ const { results, handleSearch } = useSearchListResults({
77
+ items: options,
78
+ });
79
+
80
+ const handleFormSave = useCallback(
81
+ (values: any) => {
82
+ onCreate?.(values);
83
+ setShowForm(false);
84
+ setFormInitialValue('');
85
+ },
86
+ [onCreate],
87
+ );
88
+
89
+ const handleFormCancel = useCallback(() => {
90
+ setShowForm(false);
91
+ setFormInitialValue('');
92
+ }, []);
93
+
94
+ const handleKeyDown = useCallback(
95
+ (event: KeyboardEvent) => {
96
+ if (
97
+ event.key === 'Enter' &&
98
+ createSchema &&
99
+ (event.currentTarget as HTMLElement).querySelector(
100
+ '[role="option"][aria-selected="true"][data-value="__create__"]',
101
+ )
102
+ ) {
103
+ event.stopPropagation();
104
+ event.preventDefault();
105
+ // Get the current query from the input element
106
+ const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
107
+ const currentQuery = input?.value || '';
108
+ setFormInitialValue(currentQuery);
109
+ setShowForm(true);
110
+ }
111
+ },
112
+ [createSchema],
113
+ );
114
+
115
+ return (
116
+ <Combobox.Content {...props} ref={ref} onSearch={handleSearch} onKeyDownCapture={handleKeyDown}>
117
+ {showForm && createSchema ? (
118
+ <Popover.Viewport>
119
+ <Form.Root
120
+ testId='create-referenced-object-form'
121
+ schema={createSchema}
122
+ values={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
123
+ onSave={handleFormSave}
124
+ onCancel={handleFormCancel}
125
+ >
126
+ <Form.Viewport>
127
+ <Form.Content>
128
+ <Form.FieldSet />
129
+ <Form.Actions />
130
+ </Form.Content>
131
+ </Form.Viewport>
132
+ </Form.Root>
133
+ </Popover.Viewport>
134
+ ) : (
135
+ <>
136
+ <Combobox.Input placeholder={t('ref field combobox input placeholder')} autoFocus />
137
+ <Combobox.List>
138
+ {results.map((option) => (
139
+ <Combobox.Item
140
+ key={option.id}
141
+ value={option.id}
142
+ label={option.label}
143
+ checked={selectedIds?.includes(option.id)}
144
+ onSelect={() => onSelect(option.id)}
145
+ classNames='flex items-center gap-2'
146
+ />
147
+ ))}
148
+ {createOptionLabel && createOptionIcon && createSchema && onCreate && (
149
+ <CreateItem
150
+ createOptionLabel={createOptionLabel}
151
+ createOptionIcon={createOptionIcon}
152
+ onCreateItemSelect={(query: string) => {
153
+ setFormInitialValue(query);
154
+ setShowForm(true);
155
+ }}
156
+ />
157
+ )}
158
+ </Combobox.List>
159
+ </>
160
+ )}
161
+ <Combobox.Arrow />
162
+ </Combobox.Content>
163
+ );
164
+ },
165
+ );
166
+
167
+ ObjectPickerContent.displayName = 'ObjectPickerContent';
168
+
169
+ export const ObjectPicker = {
170
+ Root: Combobox.Root,
171
+ Portal: Combobox.Portal,
172
+ Trigger: Combobox.Trigger,
173
+ VirtualTrigger: Combobox.VirtualTrigger,
174
+ Content: ObjectPickerContent,
175
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './ObjectPicker';
@@ -2,40 +2,49 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
9
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
8
 
11
- import { Filter, Obj, Query, Type } from '@dxos/echo';
12
- import { type EchoSchema, Format, toJsonSchema } from '@dxos/echo-schema';
13
- import { useSpace } from '@dxos/react-client/echo';
14
- import { withClientProvider } from '@dxos/react-client/testing';
9
+ import { DXN, Filter, Obj, Query, type QueryAST, Tag, Type } from '@dxos/echo';
10
+ import { type EchoSchema, Format, type Mutable } from '@dxos/echo/internal';
11
+ import { useQuery } from '@dxos/react-client/echo';
12
+ import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
15
13
  import { useAsyncEffect } from '@dxos/react-ui';
16
- import { QueryParser, createFilter } from '@dxos/react-ui-components';
17
- import { type DataType, ProjectionModel, createView, typenameFromQuery } from '@dxos/schema';
18
- import { withLayout, withTheme } from '@dxos/storybook-utils';
14
+ import { withTheme } from '@dxos/react-ui/testing';
15
+ import { type ProjectionModel, View, getTypenameFromQuery } from '@dxos/schema';
16
+ import { Employer, Organization, Person, Project } from '@dxos/types';
19
17
 
20
18
  import { translations } from '../../translations';
21
- import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
19
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
22
20
 
23
21
  import { ViewEditor, type ViewEditorProps } from './ViewEditor';
24
22
 
23
+ const types = [
24
+ // TODO(burdon): Get label from annotation.
25
+ { value: Organization.Organization.typename, label: 'Organization' },
26
+ { value: Person.Person.typename, label: 'Person' },
27
+ { value: Project.Project.typename, label: 'Project' },
28
+ { value: Employer.Employer.typename, label: 'Employer' },
29
+ ];
30
+
25
31
  // Type definition for debug objects exposed to tests.
26
32
  export type ViewEditorDebugObjects = {
27
33
  schema: EchoSchema;
28
- view: DataType.View;
34
+ view: View.View;
29
35
  projection: ProjectionModel;
30
36
  };
31
37
 
32
- type StoryProps = Pick<ViewEditorProps, 'readonly' | 'kind'>;
38
+ type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
33
39
 
34
40
  const DefaultStory = (props: StoryProps) => {
35
- const space = useSpace();
41
+ const { space } = useClientStory();
36
42
  const [schema, setSchema] = useState<EchoSchema>();
37
- const [view, setView] = useState<DataType.View>();
38
- const [projection, setProjection] = useState<ProjectionModel>();
43
+ const [view, setView] = useState<View.View>();
44
+ const projectionRef = useRef<ProjectionModel>(null);
45
+
46
+ const tags = useQuery(space?.db, Filter.type(Tag.Tag));
47
+
39
48
  useAsyncEffect(async () => {
40
49
  if (space) {
41
50
  const TestSchema = Schema.Struct({
@@ -43,87 +52,112 @@ const DefaultStory = (props: StoryProps) => {
43
52
  email: Format.Email,
44
53
  salary: Format.Currency(),
45
54
  }).pipe(
46
- Type.Obj({
55
+ Type.object({
47
56
  typename: 'example.com/type/Test',
48
57
  version: '0.1.0',
49
58
  }),
50
59
  );
51
60
 
52
- const [schema] = await space.db.schemaRegistry.register([TestSchema]);
53
- const view = createView({
61
+ const AlternateSchema = Schema.Struct({
62
+ title: Schema.String,
63
+ description: Schema.String,
64
+ completed: Schema.Boolean,
65
+ }).pipe(
66
+ Type.object({
67
+ typename: 'example.com/type/Alternate',
68
+ version: '0.1.0',
69
+ }),
70
+ );
71
+
72
+ const [testSchema] = await space.db.schemaRegistry.register([TestSchema, AlternateSchema]);
73
+ const view = View.make({
54
74
  name: 'Test',
55
75
  query: Query.select(Filter.type(TestSchema)),
56
- jsonSchema: toJsonSchema(TestSchema),
57
- presentation: Obj.make(Type.Expando, {}),
76
+ jsonSchema: Type.toJsonSchema(TestSchema),
58
77
  });
59
- const projection = new ProjectionModel(schema.jsonSchema, view.projection);
60
78
 
61
- setSchema(schema);
79
+ setSchema(testSchema);
62
80
  setView(view);
63
- setProjection(projection);
64
81
  }
65
82
  }, [space]);
66
83
 
67
84
  const updateViewQuery = useCallback(
68
- (newQueryString: string) => {
69
- if (!schema || !view) {
85
+ async (newQuery: QueryAST.Query, target?: string) => {
86
+ if (!schema || !view || !space) {
70
87
  return;
71
88
  }
72
89
 
73
- if (props.kind === 'advanced') {
74
- try {
75
- const parser = new QueryParser(newQueryString);
76
- // TODO(wittjosiah): When this fails it should show validation errors in the UI.
77
- const newQuery = Query.select(createFilter(parser.parse()));
78
- view.query = newQuery.ast;
79
-
80
- const typename = typenameFromQuery(newQuery.ast);
81
- if (typename) {
82
- schema.updateTypename(typename);
83
- }
84
- } catch {}
90
+ if (props.mode === 'tag') {
91
+ const queue = target && DXN.tryParse(target) ? target : undefined;
92
+ const query = queue ? Query.fromAst(newQuery).options({ queues: [queue] }) : Query.fromAst(newQuery);
93
+ Obj.change(view, (v) => {
94
+ v.query.ast = query.ast as Mutable<typeof query.ast>;
95
+ });
96
+
97
+ const typename = getTypenameFromQuery(query.ast);
98
+ const [newSchema] = await space.db.schemaRegistry.query({ typename }).run();
99
+ if (!newSchema) {
100
+ return;
101
+ }
102
+
103
+ const newView = View.make({
104
+ query,
105
+ jsonSchema: newSchema.jsonSchema,
106
+ });
107
+ Obj.change(view, (v) => {
108
+ v.projection = Obj.getSnapshot(newView).projection as Mutable<typeof v.projection>;
109
+ });
110
+ setSchema(() => newSchema);
85
111
  } else {
86
- const typename = newQueryString;
87
- const newQuery = Query.select(Filter.typename(typename));
88
- view.query = newQuery.ast;
89
- schema.updateTypename(typename);
112
+ Obj.change(view, (v) => {
113
+ v.query.ast = newQuery as Mutable<typeof newQuery>;
114
+ });
115
+ schema.updateTypename(getTypenameFromQuery(newQuery));
90
116
  }
91
117
  },
92
118
  [view, schema],
93
119
  );
94
120
 
95
- const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
121
+ const handleDelete = useCallback(
122
+ (property: string) => projectionRef.current?.deleteFieldProjection(property),
123
+ [projectionRef],
124
+ );
96
125
 
97
126
  // Expose objects on window for test access.
98
127
  useEffect(() => {
99
- if (typeof window !== 'undefined' && schema && view && projection) {
100
- (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = { schema, view, projection } satisfies ViewEditorDebugObjects;
128
+ if (typeof window !== 'undefined' && schema && view && projectionRef.current) {
129
+ (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
130
+ schema,
131
+ view,
132
+ projection: projectionRef.current,
133
+ } satisfies ViewEditorDebugObjects;
101
134
  }
102
- }, [schema, view, projection]);
135
+ }, [schema, view]);
103
136
 
104
137
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
105
138
  const json = useMemo(
106
- () => JSON.parse(JSON.stringify({ schema, view, projection })),
107
- [JSON.stringify(schema), JSON.stringify(view), JSON.stringify(projection)],
139
+ () => JSON.parse(JSON.stringify({ schema, view, projection: projectionRef.current })),
140
+ [JSON.stringify(schema), JSON.stringify(view)],
108
141
  );
109
142
 
110
- if (!schema || !view || !projection) {
143
+ if (!schema || !view) {
111
144
  return <div />;
112
145
  }
113
146
 
114
147
  return (
115
148
  <TestLayout json={json}>
116
- <TestPanel>
117
- <ViewEditor
118
- schema={schema}
119
- view={view}
120
- registry={space?.db.schemaRegistry}
121
- kind={props.kind}
122
- readonly={props.readonly}
123
- onQueryChanged={updateViewQuery}
124
- onDelete={handleDelete}
125
- />
126
- </TestPanel>
149
+ <ViewEditor
150
+ ref={projectionRef}
151
+ schema={schema}
152
+ view={view}
153
+ registry={space?.db.schemaRegistry}
154
+ mode={props.mode}
155
+ readonly={props.readonly}
156
+ types={types}
157
+ tags={tags}
158
+ onQueryChanged={updateViewQuery}
159
+ onDelete={handleDelete}
160
+ />
127
161
  </TestLayout>
128
162
  );
129
163
  };
@@ -134,13 +168,19 @@ const meta = {
134
168
  decorators: [
135
169
  withClientProvider({
136
170
  createSpace: true,
137
- }),
138
- withLayout({
139
- fullscreen: true,
171
+ types: [Tag.Tag],
172
+ onCreateSpace: ({ space }) => {
173
+ space.db.add(Tag.make({ label: 'Important' }));
174
+ space.db.add(Tag.make({ label: 'Investor' }));
175
+ space.db.add(Tag.make({ label: 'New' }));
176
+ },
140
177
  }),
141
178
  withTheme,
142
179
  ],
143
- parameters: { translations },
180
+ parameters: {
181
+ layout: 'fullscreen',
182
+ translations,
183
+ },
144
184
  } satisfies Meta<typeof DefaultStory>;
145
185
 
146
186
  export default meta;
@@ -150,9 +190,13 @@ type Story = StoryObj<typeof meta>;
150
190
  export const Default: Story = {};
151
191
 
152
192
  export const Readonly: Story = {
153
- args: { readonly: true },
193
+ args: {
194
+ readonly: true,
195
+ },
154
196
  };
155
197
 
156
- export const Advanced: Story = {
157
- args: { kind: 'advanced' },
198
+ export const TagQueryMode: Story = {
199
+ args: {
200
+ mode: 'tag',
201
+ },
158
202
  };
@@ -6,8 +6,8 @@ import { composeStories } from '@storybook/react';
6
6
  import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
- import { EchoSchema, isInstanceOf } from '@dxos/echo-schema';
10
- import { DataType, ProjectionModel } from '@dxos/schema';
9
+ import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
10
+ import { ProjectionModel, View } from '@dxos/schema';
11
11
 
12
12
  import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
13
13
 
@@ -20,7 +20,7 @@ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
20
20
  const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
21
21
  expect(debugObjects).toBeDefined();
22
22
  expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
23
- expect(isInstanceOf(DataType.View, debugObjects.view)).toBeTruthy();
23
+ expect(isInstanceOf(View.View, debugObjects.view)).toBeTruthy();
24
24
  expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
25
25
  return debugObjects;
26
26
  };
@@ -54,7 +54,7 @@ describe('ViewEditor', () => {
54
54
  const nameField = screen.getByText('name');
55
55
  fireEvent.click(nameField);
56
56
 
57
- const fieldInput = screen.getByPlaceholderText('Field name.');
57
+ const fieldInput = screen.getByPlaceholderText('Property name');
58
58
  fireEvent.change(fieldInput, { target: { value: 'new_property' } });
59
59
 
60
60
  fireEvent.click(screen.getByTestId('save-button'));
@@ -93,8 +93,12 @@ describe('ViewEditor', () => {
93
93
  const addButton = screen.getByText('Add property');
94
94
  fireEvent.click(addButton);
95
95
 
96
+ // Open the last field.
97
+ const buttons = screen.getAllByTestId('field.toggle');
98
+ fireEvent.click(buttons.at(-1)!);
99
+
96
100
  // Fill out the property field.
97
- const fieldInput = screen.getByPlaceholderText('Field name.');
101
+ const fieldInput = screen.getByPlaceholderText('Property name');
98
102
  fireEvent.change(fieldInput, { target: { value: 'added_property' } });
99
103
 
100
104
  // Click the format combo box and select the first option.
@@ -134,7 +138,8 @@ describe('ViewEditor', () => {
134
138
  expect(addedPropertyProjection!.props.property).toBe('added_property');
135
139
  });
136
140
 
137
- test('delete property', async () => {
141
+ // TODO(mykola): Fix flaky test.
142
+ test('delete property', { retry: 2 }, async () => {
138
143
  await Default.run();
139
144
  await waitForViewEditor();
140
145
 
@@ -200,4 +205,4 @@ describe('ViewEditor', () => {
200
205
  // Also verify that the field is back in the visible fields list.
201
206
  expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
202
207
  });
203
- });
208
+ }, 30_000);