@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.937b3ca

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 (194) hide show
  1. package/dist/lib/browser/index.mjs +2203 -2067
  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 +2203 -2067
  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 +4 -4
  8. package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
  9. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +84 -5
  10. package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
  12. package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
  13. package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -7
  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 -25
  18. package/dist/types/src/components/Form/Form.d.ts.map +1 -1
  19. package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
  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/fields/SelectOptionField.stories.d.ts +82 -0
  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 -9
  64. package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
  65. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -9
  66. package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
  67. package/dist/types/src/components/index.d.ts +1 -1
  68. package/dist/types/src/components/index.d.ts.map +1 -1
  69. package/dist/types/src/components/testing.d.ts +4 -3
  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 +19 -10
  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 +51 -40
  91. package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
  92. package/src/components/ControlSection/ControlSection.tsx +30 -16
  93. package/src/components/FieldEditor/FieldEditor.stories.tsx +53 -30
  94. package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
  95. package/src/components/FieldEditor/FieldEditor.tsx +87 -92
  96. package/src/components/Form/Form-old.stories.tsx +314 -0
  97. package/src/components/Form/Form.stories.tsx +155 -219
  98. package/src/components/Form/Form.tsx +319 -96
  99. package/src/components/Form/FormField.tsx +286 -0
  100. package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
  101. package/src/components/Form/FormFieldComponent.tsx +174 -0
  102. package/src/components/Form/FormFieldSet.tsx +125 -0
  103. package/src/components/Form/README.md +3 -0
  104. package/src/components/Form/fields/ArrayField.tsx +151 -0
  105. package/src/components/Form/fields/BooleanField.tsx +22 -0
  106. package/src/components/Form/fields/GeoPointField.tsx +120 -0
  107. package/src/components/Form/fields/MarkdownField.tsx +78 -0
  108. package/src/components/Form/fields/NumberField.tsx +39 -0
  109. package/src/components/Form/fields/RefField.tsx +181 -0
  110. package/src/components/Form/fields/SelectField.tsx +65 -0
  111. package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +25 -22
  112. package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +47 -42
  113. package/src/components/Form/fields/TextAreaField.tsx +38 -0
  114. package/src/components/Form/fields/TextField.tsx +38 -0
  115. package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
  116. package/src/components/Form/fields/index.ts +15 -0
  117. package/src/components/Form/index.ts +3 -4
  118. package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
  119. package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
  120. package/src/components/ObjectPicker/index.ts +5 -0
  121. package/src/components/ViewEditor/ViewEditor.stories.tsx +141 -59
  122. package/src/components/ViewEditor/ViewEditor.test.tsx +25 -21
  123. package/src/components/ViewEditor/ViewEditor.tsx +317 -214
  124. package/src/components/index.ts +1 -2
  125. package/src/components/testing.tsx +19 -11
  126. package/src/hooks/index.ts +2 -3
  127. package/src/hooks/useFormHandler.ts +356 -0
  128. package/src/hooks/useKeyHandler.tsx +37 -0
  129. package/src/translations.ts +21 -12
  130. package/src/util/field.ts +33 -43
  131. package/src/util/formatting.ts +22 -22
  132. package/src/util/index.ts +1 -1
  133. package/src/util/properties.ts +18 -0
  134. package/src/util/refs.ts +12 -18
  135. package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
  136. package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
  137. package/dist/types/src/components/Form/Defaults.d.ts +0 -14
  138. package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
  139. package/dist/types/src/components/Form/FormActions.d.ts +0 -9
  140. package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
  141. package/dist/types/src/components/Form/FormContent.d.ts +0 -57
  142. package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
  143. package/dist/types/src/components/Form/FormContext.d.ts +0 -23
  144. package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
  145. package/dist/types/src/components/Form/Input.d.ts +0 -26
  146. package/dist/types/src/components/Form/Input.d.ts.map +0 -1
  147. package/dist/types/src/components/Form/RefField.d.ts +0 -12
  148. package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
  149. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
  150. package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
  151. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
  152. package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
  153. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
  154. package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
  155. package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
  156. package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
  157. package/dist/types/src/components/Form/custom/index.d.ts +0 -4
  158. package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
  159. package/dist/types/src/components/Form/factory.d.ts +0 -8
  160. package/dist/types/src/components/Form/factory.d.ts.map +0 -1
  161. package/dist/types/src/components/deprecated/Form.d.ts +0 -15
  162. package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
  163. package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
  164. package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
  165. package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
  166. package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
  167. package/dist/types/src/components/deprecated/index.d.ts +0 -2
  168. package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
  169. package/dist/types/src/hooks/useForm.d.ts +0 -67
  170. package/dist/types/src/hooks/useForm.d.ts.map +0 -1
  171. package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
  172. package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
  173. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
  174. package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
  175. package/dist/types/src/util/schema.d.ts +0 -3
  176. package/dist/types/src/util/schema.d.ts.map +0 -1
  177. package/src/components/Form/ArrayField.tsx +0 -124
  178. package/src/components/Form/Defaults.tsx +0 -203
  179. package/src/components/Form/FormActions.tsx +0 -53
  180. package/src/components/Form/FormContent.tsx +0 -240
  181. package/src/components/Form/FormContext.tsx +0 -110
  182. package/src/components/Form/Input.tsx +0 -51
  183. package/src/components/Form/RefField.tsx +0 -213
  184. package/src/components/Form/custom/GeoPointInput.tsx +0 -72
  185. package/src/components/Form/custom/index.ts +0 -7
  186. package/src/components/Form/factory.tsx +0 -31
  187. package/src/components/deprecated/Form.stories.tsx +0 -62
  188. package/src/components/deprecated/Form.tsx +0 -119
  189. package/src/components/deprecated/FormInput.tsx +0 -71
  190. package/src/components/deprecated/index.ts +0 -5
  191. package/src/hooks/useForm.ts +0 -291
  192. package/src/hooks/useQueryRefOptions.ts +0 -53
  193. package/src/hooks/useRefQueryLookupHandler.ts +0 -39
  194. 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,119 +2,201 @@
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, useMemo, useState, useEffect } from 'react';
6
+ import * as Schema from 'effect/Schema';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
10
8
 
11
- import { Format, type EchoSchema, toJsonSchema, TypedObject } from '@dxos/echo-schema';
12
- import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
13
- 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';
14
13
  import { useAsyncEffect } from '@dxos/react-ui';
15
- import { ViewProjection, ViewType, createView } from '@dxos/schema';
16
- import { withTheme, withLayout } 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';
17
17
 
18
- import { ViewEditor, type ViewEditorProps } from './ViewEditor';
19
18
  import { translations } from '../../translations';
20
- import { TestLayout, TestPanel, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
19
+ import { TestLayout, VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
20
+
21
+ import { ViewEditor, type ViewEditorProps } from './ViewEditor';
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
+ ];
21
30
 
22
31
  // Type definition for debug objects exposed to tests.
23
32
  export type ViewEditorDebugObjects = {
24
33
  schema: EchoSchema;
25
- view: ViewType;
26
- projection: ViewProjection;
34
+ view: View.View;
35
+ projection: ProjectionModel;
27
36
  };
28
37
 
29
- type StoryProps = Pick<ViewEditorProps, 'readonly'>;
38
+ type StoryProps = Pick<ViewEditorProps, 'readonly' | 'mode'>;
30
39
 
31
40
  const DefaultStory = (props: StoryProps) => {
32
- const space = useSpace();
41
+ const { space } = useClientStory();
33
42
  const [schema, setSchema] = useState<EchoSchema>();
34
- const [view, setView] = useState<ViewType>();
35
- const [projection, setProjection] = useState<ViewProjection>();
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
+
36
48
  useAsyncEffect(async () => {
37
49
  if (space) {
38
- class TestSchema extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
50
+ const TestSchema = Schema.Struct({
39
51
  name: Schema.String,
40
52
  email: Format.Email,
41
53
  salary: Format.Currency(),
42
- }) {}
43
-
44
- const [schema] = await space.db.schemaRegistry.register([TestSchema]);
45
- const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: toJsonSchema(TestSchema) });
46
- const projection = new ViewProjection(schema.jsonSchema, view);
47
-
48
- setSchema(schema);
54
+ }).pipe(
55
+ Type.object({
56
+ typename: 'example.com/type/Test',
57
+ version: '0.1.0',
58
+ }),
59
+ );
60
+
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({
74
+ name: 'Test',
75
+ query: Query.select(Filter.type(TestSchema)),
76
+ jsonSchema: Type.toJsonSchema(TestSchema),
77
+ });
78
+
79
+ setSchema(testSchema);
49
80
  setView(view);
50
- setProjection(projection);
51
81
  }
52
82
  }, [space]);
53
83
 
54
- const views = useQuery(space, Filter.type(ViewType));
55
- const currentTypename = useMemo(() => view?.query?.typename, [view]);
56
- const updateViewTypename = useCallback(
57
- (newTypename: string) => {
58
- if (!schema) {
84
+ const updateViewQuery = useCallback(
85
+ async (newQuery: QueryAST.Query, target?: string) => {
86
+ if (!schema || !view || !space) {
59
87
  return;
60
88
  }
61
- const matchingViews = views.filter((view) => view.query.typename === currentTypename);
62
- for (const view of matchingViews) {
63
- view.query.typename = newTypename;
89
+
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);
111
+ } else {
112
+ Obj.change(view, (v) => {
113
+ v.query.ast = newQuery as Mutable<typeof newQuery>;
114
+ });
115
+ schema.updateTypename(getTypenameFromQuery(newQuery));
64
116
  }
65
- schema.updateTypename(newTypename);
66
117
  },
67
- [views, schema],
118
+ [view, schema],
68
119
  );
69
120
 
70
- const handleDelete = useCallback((property: string) => projection?.deleteFieldProjection(property), [projection]);
121
+ const handleDelete = useCallback(
122
+ (property: string) => projectionRef.current?.deleteFieldProjection(property),
123
+ [projectionRef],
124
+ );
71
125
 
72
126
  // Expose objects on window for test access.
73
127
  useEffect(() => {
74
- if (typeof window !== 'undefined' && schema && view && projection) {
75
- (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;
76
134
  }
77
- }, [schema, view, projection]);
135
+ }, [schema, view]);
78
136
 
79
137
  // NOTE(ZaymonFC): This looks awkward but it resolves an infinite parsing issue with sb.
80
138
  const json = useMemo(
81
- () => JSON.parse(JSON.stringify({ schema, view, projection })),
82
- [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)],
83
141
  );
84
142
 
85
- if (!schema || !view || !projection) {
143
+ if (!schema || !view) {
86
144
  return <div />;
87
145
  }
88
146
 
89
147
  return (
90
148
  <TestLayout json={json}>
91
- <TestPanel>
92
- <ViewEditor
93
- schema={schema}
94
- view={view}
95
- registry={space?.db.schemaRegistry}
96
- readonly={props.readonly}
97
- onTypenameChanged={updateViewTypename}
98
- onDelete={handleDelete}
99
- />
100
- </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
+ />
101
161
  </TestLayout>
102
162
  );
103
163
  };
104
164
 
105
- const meta: Meta<StoryProps> = {
165
+ const meta = {
106
166
  title: 'ui/react-ui-form/ViewEditor',
107
167
  render: DefaultStory,
108
- decorators: [withClientProvider({ createSpace: true }), withLayout({ fullscreen: true }), withTheme],
109
- parameters: { translations },
110
- };
168
+ decorators: [
169
+ withClientProvider({
170
+ createSpace: 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
+ },
177
+ }),
178
+ withTheme,
179
+ ],
180
+ parameters: {
181
+ layout: 'fullscreen',
182
+ translations,
183
+ },
184
+ } satisfies Meta<typeof DefaultStory>;
111
185
 
112
186
  export default meta;
113
187
 
114
- type Story = StoryObj<StoryProps>;
188
+ type Story = StoryObj<typeof meta>;
115
189
 
116
190
  export const Default: Story = {};
117
191
 
118
192
  export const Readonly: Story = {
119
- args: { readonly: true },
193
+ args: {
194
+ readonly: true,
195
+ },
196
+ };
197
+
198
+ export const TagQueryMode: Story = {
199
+ args: {
200
+ mode: 'tag',
201
+ },
120
202
  };
@@ -3,15 +3,16 @@
3
3
  //
4
4
 
5
5
  import { composeStories } from '@storybook/react';
6
- import { screen, cleanup, fireEvent, waitFor } from '@testing-library/react';
6
+ import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
7
7
  import { afterEach, describe, expect, test } from 'vitest';
8
8
 
9
- import { EchoSchema } from '@dxos/echo-schema';
10
- import { ViewProjection, ViewType } from '@dxos/schema';
9
+ import { EchoSchema, isInstanceOf } from '@dxos/echo/internal';
10
+ import { ProjectionModel, View } from '@dxos/schema';
11
+
12
+ import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
11
13
 
12
14
  import * as stories from './ViewEditor.stories';
13
15
  import { type ViewEditorDebugObjects } from './ViewEditor.stories';
14
- import { VIEW_EDITOR_DEBUG_SYMBOL } from '../testing';
15
16
 
16
17
  const { Default } = composeStories(stories);
17
18
 
@@ -19,8 +20,8 @@ const getViewEditorDebugObjects = (): ViewEditorDebugObjects => {
19
20
  const debugObjects = (window as any)[VIEW_EDITOR_DEBUG_SYMBOL] as ViewEditorDebugObjects;
20
21
  expect(debugObjects).toBeDefined();
21
22
  expect(debugObjects.schema).toBeInstanceOf(EchoSchema);
22
- expect(debugObjects.view).toBeInstanceOf(ViewType);
23
- expect(debugObjects.projection).toBeInstanceOf(ViewProjection);
23
+ expect(isInstanceOf(View.View, debugObjects.view)).toBeTruthy();
24
+ expect(debugObjects.projection).toBeInstanceOf(ProjectionModel);
24
25
  return debugObjects;
25
26
  };
26
27
 
@@ -41,7 +42,7 @@ describe('ViewEditor', () => {
41
42
  test('renders view editor', async () => {
42
43
  await Default.run();
43
44
  await waitForViewEditor();
44
- expect(screen.getByText('View')).toBeInTheDocument();
45
+ expect(screen.getByText('Record type')).toBeInTheDocument();
45
46
  });
46
47
 
47
48
  test('change property name', async () => {
@@ -53,7 +54,7 @@ describe('ViewEditor', () => {
53
54
  const nameField = screen.getByText('name');
54
55
  fireEvent.click(nameField);
55
56
 
56
- const fieldInput = screen.getByPlaceholderText('Field name.');
57
+ const fieldInput = screen.getByPlaceholderText('Property name');
57
58
  fireEvent.change(fieldInput, { target: { value: 'new_property' } });
58
59
 
59
60
  fireEvent.click(screen.getByTestId('save-button'));
@@ -73,7 +74,7 @@ describe('ViewEditor', () => {
73
74
  expect(schemaProperties!.name).toBeUndefined();
74
75
 
75
76
  // Check view contains new_property field.
76
- const newPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'new_property');
77
+ const newPropertyField = debugObjects.view.projection.fields.find((field: any) => field.path === 'new_property');
77
78
  expect(newPropertyField).toBeDefined();
78
79
  expect(newPropertyField!.path).toBe('new_property');
79
80
 
@@ -92,8 +93,12 @@ describe('ViewEditor', () => {
92
93
  const addButton = screen.getByText('Add property');
93
94
  fireEvent.click(addButton);
94
95
 
96
+ // Open the last field.
97
+ const buttons = screen.getAllByTestId('field.toggle');
98
+ fireEvent.click(buttons.at(-1)!);
99
+
95
100
  // Fill out the property field.
96
- const fieldInput = screen.getByPlaceholderText('Field name.');
101
+ const fieldInput = screen.getByPlaceholderText('Property name');
97
102
  fireEvent.change(fieldInput, { target: { value: 'added_property' } });
98
103
 
99
104
  // Click the format combo box and select the first option.
@@ -120,7 +125,9 @@ describe('ViewEditor', () => {
120
125
  expect(schemaProperties!.added_property).toBeDefined();
121
126
 
122
127
  // Check view contains added_property field.
123
- const addedPropertyField = debugObjects.view.fields.find((field: any) => field.path === 'added_property');
128
+ const addedPropertyField = debugObjects.view.projection.fields.find(
129
+ (field: any) => field.path === 'added_property',
130
+ );
124
131
  expect(addedPropertyField).toBeDefined();
125
132
  expect(addedPropertyField!.path).toBe('added_property');
126
133
 
@@ -131,14 +138,15 @@ describe('ViewEditor', () => {
131
138
  expect(addedPropertyProjection!.props.property).toBe('added_property');
132
139
  });
133
140
 
134
- test('delete property', async () => {
141
+ // TODO(mykola): Fix flaky test.
142
+ test('delete property', { retry: 2 }, async () => {
135
143
  await Default.run();
136
144
  await waitForViewEditor();
137
145
 
138
146
  // Find the delete button for the 'name' property.
139
147
  const nameProperty = screen.getByText('name');
140
148
  const propertyRow = nameProperty.closest('[role="listitem"]');
141
- const deleteButton = propertyRow?.querySelector('button:last-child');
149
+ const deleteButton = propertyRow?.querySelector('[data-testid="field.delete"]');
142
150
 
143
151
  if (!deleteButton) {
144
152
  throw new Error('Delete button not found');
@@ -161,7 +169,7 @@ describe('ViewEditor', () => {
161
169
  expect(schemaProperties!.name).toBeUndefined();
162
170
 
163
171
  // Check view no longer contains the name field.
164
- const nameField = debugObjects.view.fields.find((field: any) => field.path === 'name');
172
+ const nameField = debugObjects.view.projection.fields.find((field: any) => field.path === 'name');
165
173
  expect(nameField).toBeUndefined();
166
174
 
167
175
  // Check projection no longer contains the name property.
@@ -186,19 +194,15 @@ describe('ViewEditor', () => {
186
194
  // Access the live objects directly from window using symbol.
187
195
  const debugObjects = getViewEditorDebugObjects();
188
196
 
189
- // Check that hiddenFields is non-empty.
190
- expect(debugObjects.view.hiddenFields).toBeDefined();
191
- expect(debugObjects.view.hiddenFields!.length).toBeGreaterThan(0);
192
-
193
197
  // Click the show button
194
198
  fireEvent.click(screen.getByTestId('show-field-button'));
195
199
 
196
200
  // Wait for the data to update and check that hiddenFields is empty.
197
201
  await waitFor(() => {
198
- expect(debugObjects.view.hiddenFields!.length).toBe(0);
202
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible === false)!.length).toBe(0);
199
203
  });
200
204
 
201
205
  // Also verify that the field is back in the visible fields list.
202
- expect(debugObjects.view.fields.length).toBeGreaterThan(0);
206
+ expect(debugObjects.view.projection.fields.filter(({ visible }) => visible !== false).length).toBeGreaterThan(0);
203
207
  });
204
- });
208
+ }, 30_000);