@dxos/react-ui-form 0.8.4-main.84f28bd → 0.8.4-main.9735255
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.
- package/dist/lib/browser/index.mjs +2218 -2062
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +2218 -2062
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +4 -4
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +1 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +84 -5
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -8
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +84 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form-old.stories.d.ts +131 -0
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +81 -25
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +127 -55
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +37 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +78 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +9 -0
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts +19 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts +18 -0
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/NumberField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts +14 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +10 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +5 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +82 -0
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts +6 -0
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/index.d.ts +12 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +2 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +46 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +85 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts +2 -0
- package/dist/types/src/components/ObjectPicker/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +21 -9
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +84 -9
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -3
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +82 -0
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts +6 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -10
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/echo.d.ts +6 -0
- package/dist/types/src/util/echo.d.ts.map +1 -0
- package/dist/types/src/util/field.d.ts +3 -3
- package/dist/types/src/util/field.d.ts.map +1 -1
- package/dist/types/src/util/formatting.d.ts +2 -2
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +7 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +2 -3
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +51 -40
- package/src/components/ControlSection/ControlSection.stories.tsx +11 -14
- package/src/components/ControlSection/ControlSection.tsx +30 -16
- package/src/components/FieldEditor/FieldEditor.stories.tsx +53 -30
- package/src/components/FieldEditor/FieldEditor.test.tsx +14 -13
- package/src/components/FieldEditor/FieldEditor.tsx +87 -92
- package/src/components/Form/Form-old.stories.tsx +314 -0
- package/src/components/Form/Form.stories.tsx +155 -219
- package/src/components/Form/Form.tsx +320 -96
- package/src/components/Form/FormField.tsx +286 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +174 -0
- package/src/components/Form/FormFieldSet.tsx +125 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +151 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/GeoPointField.tsx +120 -0
- package/src/components/Form/fields/MarkdownField.tsx +78 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +181 -0
- package/src/components/Form/fields/SelectField.tsx +65 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +25 -22
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +47 -42
- package/src/components/Form/fields/TextAreaField.tsx +38 -0
- package/src/components/Form/fields/TextField.tsx +38 -0
- package/src/components/Form/{custom/TupleInput.tsx → fields/TupleField.tsx} +16 -9
- package/src/components/Form/fields/index.ts +15 -0
- package/src/components/Form/index.ts +3 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +214 -0
- package/src/components/ObjectPicker/ObjectPicker.tsx +175 -0
- package/src/components/ObjectPicker/index.ts +5 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +141 -59
- package/src/components/ViewEditor/ViewEditor.test.tsx +25 -21
- package/src/components/ViewEditor/ViewEditor.tsx +323 -217
- package/src/components/index.ts +1 -2
- package/src/components/testing.tsx +19 -11
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +362 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/translations.ts +21 -12
- package/src/util/echo.ts +25 -0
- package/src/util/field.ts +33 -43
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.ts +18 -0
- package/src/util/refs.ts +12 -18
- package/dist/types/src/components/Form/ArrayField.d.ts +0 -16
- package/dist/types/src/components/Form/ArrayField.d.ts.map +0 -1
- package/dist/types/src/components/Form/Defaults.d.ts +0 -14
- package/dist/types/src/components/Form/Defaults.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormActions.d.ts +0 -9
- package/dist/types/src/components/Form/FormActions.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContent.d.ts +0 -57
- package/dist/types/src/components/Form/FormContent.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormContext.d.ts +0 -23
- package/dist/types/src/components/Form/FormContext.d.ts.map +0 -1
- package/dist/types/src/components/Form/Input.d.ts +0 -26
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -12
- package/dist/types/src/components/Form/RefField.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/GeoPointInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts +0 -4
- package/dist/types/src/components/Form/custom/SelectOptionsInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts +0 -7
- package/dist/types/src/components/Form/custom/SelectOptionsInput.stories.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/TupleInput.d.ts +0 -6
- package/dist/types/src/components/Form/custom/TupleInput.d.ts.map +0 -1
- package/dist/types/src/components/Form/custom/index.d.ts +0 -4
- package/dist/types/src/components/Form/custom/index.d.ts.map +0 -1
- package/dist/types/src/components/Form/factory.d.ts +0 -8
- package/dist/types/src/components/Form/factory.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.d.ts +0 -15
- package/dist/types/src/components/deprecated/Form.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/Form.stories.d.ts +0 -13
- package/dist/types/src/components/deprecated/Form.stories.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/FormInput.d.ts +0 -16
- package/dist/types/src/components/deprecated/FormInput.d.ts.map +0 -1
- package/dist/types/src/components/deprecated/index.d.ts +0 -2
- package/dist/types/src/components/deprecated/index.d.ts.map +0 -1
- package/dist/types/src/hooks/useForm.d.ts +0 -67
- package/dist/types/src/hooks/useForm.d.ts.map +0 -1
- package/dist/types/src/hooks/useQueryRefOptions.d.ts +0 -23
- package/dist/types/src/hooks/useQueryRefOptions.d.ts.map +0 -1
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts +0 -8
- package/dist/types/src/hooks/useRefQueryLookupHandler.d.ts.map +0 -1
- package/dist/types/src/util/schema.d.ts +0 -3
- package/dist/types/src/util/schema.d.ts.map +0 -1
- package/src/components/Form/ArrayField.tsx +0 -124
- package/src/components/Form/Defaults.tsx +0 -203
- package/src/components/Form/FormActions.tsx +0 -53
- package/src/components/Form/FormContent.tsx +0 -240
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -51
- package/src/components/Form/RefField.tsx +0 -213
- package/src/components/Form/custom/GeoPointInput.tsx +0 -72
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/components/deprecated/Form.stories.tsx +0 -62
- package/src/components/deprecated/Form.tsx +0 -119
- package/src/components/deprecated/FormInput.tsx +0 -71
- package/src/components/deprecated/index.ts +0 -5
- package/src/hooks/useForm.ts +0 -291
- package/src/hooks/useQueryRefOptions.ts +0 -53
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- 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
|
+
};
|
|
@@ -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
|
|
9
|
-
import React, { useCallback, useMemo,
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
8
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
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 {
|
|
16
|
-
import {
|
|
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,
|
|
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:
|
|
26
|
-
projection:
|
|
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 =
|
|
41
|
+
const { space } = useClientStory();
|
|
33
42
|
const [schema, setSchema] = useState<EchoSchema>();
|
|
34
|
-
const [view, setView] = useState<
|
|
35
|
-
const
|
|
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
|
-
|
|
50
|
+
const TestSchema = Schema.Struct({
|
|
39
51
|
name: Schema.String,
|
|
40
52
|
email: Format.Email,
|
|
41
53
|
salary: Format.Currency(),
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
[
|
|
118
|
+
[view, schema],
|
|
68
119
|
);
|
|
69
120
|
|
|
70
|
-
const handleDelete = useCallback(
|
|
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 &&
|
|
75
|
-
(window as any)[VIEW_EDITOR_DEBUG_SYMBOL] = {
|
|
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
|
|
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)
|
|
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
|
|
143
|
+
if (!schema || !view) {
|
|
86
144
|
return <div />;
|
|
87
145
|
}
|
|
88
146
|
|
|
89
147
|
return (
|
|
90
148
|
<TestLayout json={json}>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
165
|
+
const meta = {
|
|
106
166
|
title: 'ui/react-ui-form/ViewEditor',
|
|
107
167
|
render: DefaultStory,
|
|
108
|
-
decorators: [
|
|
109
|
-
|
|
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<
|
|
188
|
+
type Story = StoryObj<typeof meta>;
|
|
115
189
|
|
|
116
190
|
export const Default: Story = {};
|
|
117
191
|
|
|
118
192
|
export const Readonly: Story = {
|
|
119
|
-
args: {
|
|
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 {
|
|
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
|
|
10
|
-
import {
|
|
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).
|
|
23
|
-
expect(debugObjects.projection).toBeInstanceOf(
|
|
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('
|
|
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('
|
|
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('
|
|
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(
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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);
|