@dxos/react-ui-form 0.8.4-main.fffef41 → 0.9.0
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/LICENSE +102 -5
- package/README.md +0 -1
- package/dist/lib/browser/index.mjs +4005 -2402
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +87 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +4005 -2402
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +89 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts +8 -7
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +73 -59
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +135 -27
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +192 -137
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +52 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +98 -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 +26 -0
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts +19 -0
- package/dist/types/src/components/Form/FormTooltipsContext.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts +50 -0
- package/dist/types/src/components/Form/Layout/FormLayout.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts +123 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.stories.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/FormLayout.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts +30 -0
- package/dist/types/src/components/Form/Layout/annotation.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/index.d.ts +4 -0
- package/dist/types/src/components/Form/Layout/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts +31 -0
- package/dist/types/src/components/Form/Layout/parser.d.ts.map +1 -0
- package/dist/types/src/components/Form/Layout/parser.test.d.ts +2 -0
- package/dist/types/src/components/Form/Layout/parser.test.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/DateField.d.ts +4 -0
- package/dist/types/src/components/Form/fields/DateField.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/InlineRefField.d.ts +13 -0
- package/dist/types/src/components/Form/fields/InlineRefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts +11 -0
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.d.ts +89 -0
- package/dist/types/src/components/Form/fields/MarkdownField.stories.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 +34 -0
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/RefField.test.d.ts +2 -0
- package/dist/types/src/components/Form/fields/RefField.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/fields/SelectField.d.ts +13 -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 +89 -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 +14 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/index.d.ts +5 -4
- package/dist/types/src/components/Form/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +30 -0
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts +2 -0
- package/dist/types/src/components/Form/meta-tags.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +8 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +87 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/index.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +39 -12
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +69 -58
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +9 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +132 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.test.d.ts.map +1 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts +2 -0
- package/dist/types/src/components/ObjectProperties/index.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts +9 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +91 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -0
- package/dist/types/src/components/ObjectTree/index.d.ts +2 -0
- package/dist/types/src/components/ObjectTree/index.d.ts.map +1 -0
- package/dist/types/src/components/RefEditor/RefEditor.d.ts +73 -0
- package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -0
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +92 -0
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -0
- package/dist/types/src/components/RefEditor/index.d.ts +3 -0
- package/dist/types/src/components/RefEditor/index.d.ts.map +1 -0
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +47 -0
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.d.ts +51 -0
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts +89 -0
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +43 -0
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts +16 -0
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +1 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +18 -0
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Settings/index.d.ts +4 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +15 -19
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +75 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +5 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +13 -6
- 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 +7 -0
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +70 -59
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/field-type.d.ts +19 -0
- package/dist/types/src/util/field-type.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 +35 -0
- package/dist/types/src/util/properties.d.ts.map +1 -0
- package/dist/types/src/util/properties.test.d.ts +2 -0
- package/dist/types/src/util/properties.test.d.ts.map +1 -0
- package/dist/types/src/util/refs.d.ts +2 -2
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +64 -52
- package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
- package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
- package/src/components/FieldEditor/FieldEditor.tsx +91 -86
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +147 -252
- package/src/components/Form/Form.tsx +461 -108
- package/src/components/Form/FormField.tsx +345 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +253 -0
- package/src/components/Form/FormFieldSet.tsx +213 -0
- package/src/components/Form/FormTooltipsContext.ts +25 -0
- package/src/components/Form/Layout/FormLayout.stories.test.tsx +51 -0
- package/src/components/Form/Layout/FormLayout.stories.tsx +422 -0
- package/src/components/Form/Layout/FormLayout.test.ts +59 -0
- package/src/components/Form/Layout/FormLayout.tsx +251 -0
- package/src/components/Form/Layout/annotation.ts +38 -0
- package/src/components/Form/Layout/index.ts +7 -0
- package/src/components/Form/Layout/parser.test.ts +120 -0
- package/src/components/Form/Layout/parser.ts +167 -0
- package/src/components/Form/README.md +3 -0
- package/src/components/Form/fields/ArrayField.tsx +173 -0
- package/src/components/Form/fields/BooleanField.tsx +22 -0
- package/src/components/Form/fields/DateField.tsx +101 -0
- package/src/components/Form/fields/GeoPointField.tsx +124 -0
- package/src/components/Form/fields/InlineRefField.tsx +138 -0
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +167 -0
- package/src/components/Form/fields/NumberField.tsx +76 -0
- package/src/components/Form/fields/RefField.test.ts +45 -0
- package/src/components/Form/fields/RefField.tsx +266 -0
- package/src/components/Form/fields/SelectField.tsx +77 -0
- package/src/components/Form/{custom/SelectOptionsInput.stories.tsx → fields/SelectOptionField.stories.tsx} +23 -18
- package/src/components/Form/{custom/SelectOptionsInput.tsx → fields/SelectOptionField.tsx} +33 -26
- 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} +17 -9
- package/src/components/Form/fields/index.ts +17 -0
- package/src/components/Form/index.ts +6 -4
- package/src/components/Form/meta-tags.test.ts +47 -0
- package/src/components/Form/meta-tags.ts +33 -0
- package/src/components/ObjectForm/ObjectForm.stories.tsx +62 -0
- package/src/components/ObjectForm/ObjectForm.tsx +111 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +26 -21
- package/src/components/ObjectPicker/ObjectPicker.tsx +123 -82
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +350 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +111 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +146 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/ObjectTree/ObjectTree.stories.tsx +134 -0
- package/src/components/ObjectTree/ObjectTree.tsx +190 -0
- package/src/components/ObjectTree/index.ts +5 -0
- package/src/components/RefEditor/RefEditor.stories.tsx +148 -0
- package/src/components/RefEditor/RefEditor.tsx +275 -0
- package/src/components/RefEditor/index.ts +6 -0
- package/src/components/RefEditor/ref-editor-extension.ts +254 -0
- package/src/components/Settings/AUDIT.md +51 -0
- package/src/components/Settings/Settings.stories.tsx +104 -0
- package/src/components/Settings/Settings.tsx +81 -0
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +58 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +7 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +70 -76
- package/src/components/ViewEditor/ViewEditor.test.tsx +17 -13
- package/src/components/ViewEditor/ViewEditor.tsx +340 -226
- package/src/components/index.ts +5 -1
- package/src/components/testing.tsx +24 -11
- package/src/hooks/index.ts +2 -3
- package/src/hooks/useFormHandler.ts +380 -0
- package/src/hooks/useKeyHandler.tsx +37 -0
- package/src/index.ts +0 -4
- package/src/translations.ts +76 -64
- package/src/util/field-type.ts +60 -0
- package/src/util/field.ts +25 -25
- package/src/util/formatting.ts +22 -22
- package/src/util/index.ts +2 -1
- package/src/util/properties.test.ts +153 -0
- package/src/util/properties.ts +100 -0
- package/src/util/refs.ts +16 -15
- package/src/vite-env.d.ts +10 -0
- package/dist/types/src/components/ControlSection/ControlSection.d.ts +0 -23
- package/dist/types/src/components/ControlSection/ControlSection.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts +0 -83
- package/dist/types/src/components/ControlSection/ControlSection.stories.d.ts.map +0 -1
- package/dist/types/src/components/ControlSection/index.d.ts +0 -2
- package/dist/types/src/components/ControlSection/index.d.ts.map +0 -1
- 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 -63
- 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 -29
- package/dist/types/src/components/Form/Input.d.ts.map +0 -1
- package/dist/types/src/components/Form/RefField.d.ts +0 -22
- 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 -78
- 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/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 -29
- 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/ControlSection/ControlSection.stories.tsx +0 -38
- package/src/components/ControlSection/ControlSection.tsx +0 -153
- package/src/components/Form/ArrayField.tsx +0 -125
- package/src/components/Form/Defaults.tsx +0 -205
- package/src/components/Form/FormActions.tsx +0 -54
- package/src/components/Form/FormContent.tsx +0 -362
- package/src/components/Form/FormContext.tsx +0 -110
- package/src/components/Form/Input.tsx +0 -54
- package/src/components/Form/RefField.tsx +0 -283
- package/src/components/Form/custom/GeoPointInput.tsx +0 -80
- package/src/components/Form/custom/index.ts +0 -7
- package/src/components/Form/factory.tsx +0 -31
- package/src/hooks/useForm.ts +0 -296
- package/src/hooks/useQueryRefOptions.ts +0 -62
- package/src/hooks/useRefQueryLookupHandler.ts +0 -39
- package/src/util/schema.ts +0 -12
|
@@ -3,24 +3,40 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type * as Schema from 'effect/Schema';
|
|
6
|
-
import React, { type KeyboardEvent,
|
|
6
|
+
import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { Combobox } from '@dxos/react-ui-
|
|
8
|
+
import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { Combobox } from '@dxos/react-ui-list';
|
|
10
|
+
import { useSearchListResults } from '@dxos/react-ui-search';
|
|
11
|
+
|
|
12
|
+
import { translationKey } from '#translations';
|
|
10
13
|
|
|
11
|
-
import { type QueryTag } from '../../hooks';
|
|
12
|
-
import { translationKey } from '../../translations';
|
|
13
14
|
import { Form } from '../Form';
|
|
15
|
+
import { type FormFieldMap } from '../Form/FormFieldComponent';
|
|
16
|
+
|
|
17
|
+
export type RefOption = {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
hue?: Palette;
|
|
21
|
+
};
|
|
14
22
|
|
|
15
23
|
export type ObjectPickerContentProps = ThemedClassName<{
|
|
16
|
-
options:
|
|
24
|
+
options: RefOption[];
|
|
17
25
|
selectedIds?: string[];
|
|
18
|
-
|
|
19
|
-
createOptionLabel?: [string, { ns: string }];
|
|
26
|
+
createOptionLabel?: [string, { ns: string | readonly string[] }];
|
|
20
27
|
createOptionIcon?: string;
|
|
28
|
+
createSchema?: Schema.Schema.AnyNoContext;
|
|
21
29
|
createInitialValuePath?: string;
|
|
30
|
+
createFieldMap?: FormFieldMap;
|
|
31
|
+
/**
|
|
32
|
+
* Persist a newly-created object given the form values. May be async (e.g.
|
|
33
|
+
* to write to a database). The Promise is awaited before the inline create
|
|
34
|
+
* form is collapsed back to the picker list, so consumers can complete
|
|
35
|
+
* follow-up work (assign the new ref to a parent slot, close the popover)
|
|
36
|
+
* before the picker UI re-renders.
|
|
37
|
+
*/
|
|
38
|
+
onCreate?: (values: any) => unknown | Promise<unknown>;
|
|
22
39
|
onSelect: (id: string) => void;
|
|
23
|
-
onCreate?: (values: any) => void;
|
|
24
40
|
}>;
|
|
25
41
|
|
|
26
42
|
const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>(
|
|
@@ -32,46 +48,35 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
32
48
|
createOptionLabel,
|
|
33
49
|
createOptionIcon,
|
|
34
50
|
createInitialValuePath,
|
|
51
|
+
createFieldMap,
|
|
35
52
|
onSelect,
|
|
36
53
|
onCreate,
|
|
37
54
|
...props
|
|
38
55
|
},
|
|
39
|
-
|
|
56
|
+
forwardedRef,
|
|
40
57
|
) => {
|
|
41
58
|
const { t } = useTranslation(translationKey);
|
|
42
|
-
|
|
43
|
-
const [searchString, setSearchString] = useState('');
|
|
44
59
|
const [showForm, setShowForm] = useState(false);
|
|
60
|
+
const [formInitialValue, setFormInitialValue] = useState<string>('');
|
|
61
|
+
|
|
62
|
+
const { results, query, handleSearch } = useSearchListResults({
|
|
63
|
+
items: options,
|
|
64
|
+
});
|
|
45
65
|
|
|
46
66
|
const handleFormSave = useCallback(
|
|
47
|
-
(values: any) => {
|
|
48
|
-
onCreate?.(values);
|
|
67
|
+
async (values: any) => {
|
|
68
|
+
await onCreate?.(values);
|
|
49
69
|
setShowForm(false);
|
|
50
|
-
|
|
70
|
+
setFormInitialValue('');
|
|
51
71
|
},
|
|
52
72
|
[onCreate],
|
|
53
73
|
);
|
|
54
74
|
|
|
55
75
|
const handleFormCancel = useCallback(() => {
|
|
56
76
|
setShowForm(false);
|
|
57
|
-
|
|
77
|
+
setFormInitialValue('');
|
|
58
78
|
}, []);
|
|
59
79
|
|
|
60
|
-
// TODO(thure): The following click and keydown handlers are necessary because `onSelect` is called after the
|
|
61
|
-
// Popover is already closed. Augment/refactor CmdK, if possible, to facilitate stopping event default
|
|
62
|
-
// and propagation.
|
|
63
|
-
|
|
64
|
-
const handleClick = useCallback(
|
|
65
|
-
(event: MouseEvent) => {
|
|
66
|
-
if (createSchema && (event.target as HTMLElement).closest('[data-value="__create__"]')) {
|
|
67
|
-
event.stopPropagation();
|
|
68
|
-
event.preventDefault();
|
|
69
|
-
setShowForm(true);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
[createSchema],
|
|
73
|
-
);
|
|
74
|
-
|
|
75
80
|
const handleKeyDown = useCallback(
|
|
76
81
|
(event: KeyboardEvent) => {
|
|
77
82
|
if (
|
|
@@ -83,78 +88,114 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
83
88
|
) {
|
|
84
89
|
event.stopPropagation();
|
|
85
90
|
event.preventDefault();
|
|
91
|
+
// Get the current query from the input element
|
|
92
|
+
const input = (event.currentTarget as HTMLElement).querySelector('input[type="text"]') as HTMLInputElement;
|
|
93
|
+
const currentQuery = input?.value || '';
|
|
94
|
+
setFormInitialValue(currentQuery);
|
|
86
95
|
setShowForm(true);
|
|
87
96
|
}
|
|
88
97
|
},
|
|
89
98
|
[createSchema],
|
|
90
99
|
);
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
(
|
|
94
|
-
|
|
95
|
-
acc[option.id.toLowerCase()] = option.label.toLowerCase();
|
|
96
|
-
return acc;
|
|
97
|
-
}, {}),
|
|
98
|
-
[options],
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<Combobox.Content
|
|
103
|
-
{...props}
|
|
104
|
-
ref={ref}
|
|
105
|
-
filter={(value, search) => (value === '__create__' || labelById[value]?.includes(search.toLowerCase()) ? 1 : 0)}
|
|
106
|
-
onClickCapture={handleClick}
|
|
107
|
-
onKeyDownCapture={handleKeyDown}
|
|
108
|
-
>
|
|
109
|
-
{showForm && createSchema ? (
|
|
101
|
+
if (showForm && createSchema) {
|
|
102
|
+
return (
|
|
103
|
+
<Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
110
104
|
<Popover.Viewport>
|
|
111
|
-
<Form
|
|
105
|
+
<Form.Root
|
|
106
|
+
testId='create-referenced-object-form'
|
|
112
107
|
schema={createSchema}
|
|
113
|
-
|
|
108
|
+
defaultValues={createInitialValuePath ? { [createInitialValuePath]: formInitialValue } : {}}
|
|
109
|
+
fieldMap={createFieldMap}
|
|
114
110
|
onSave={handleFormSave}
|
|
115
111
|
onCancel={handleFormCancel}
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
>
|
|
113
|
+
<Form.Viewport>
|
|
114
|
+
<Form.Content>
|
|
115
|
+
<Form.FieldSet />
|
|
116
|
+
<Form.Actions />
|
|
117
|
+
</Form.Content>
|
|
118
|
+
</Form.Viewport>
|
|
119
|
+
</Form.Root>
|
|
118
120
|
</Popover.Viewport>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
<Combobox.Arrow />
|
|
122
|
+
</Combobox.Content>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
128
|
+
<Combobox.Input
|
|
129
|
+
placeholder={t('ref-field-combobox-input.placeholder')}
|
|
130
|
+
autoFocus
|
|
131
|
+
value={query}
|
|
132
|
+
onValueChange={handleSearch}
|
|
133
|
+
/>
|
|
134
|
+
<Combobox.List>
|
|
135
|
+
{results.map((option) => (
|
|
136
|
+
<Combobox.Item
|
|
137
|
+
key={option.id}
|
|
138
|
+
value={option.id}
|
|
139
|
+
label={option.label}
|
|
140
|
+
checked={selectedIds?.includes(option.id)}
|
|
141
|
+
onSelect={() => onSelect(option.id)}
|
|
142
|
+
classNames='flex items-center gap-2'
|
|
126
143
|
/>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
<Combobox.Item value='__create__' classNames='flex items-center gap-2'>
|
|
141
|
-
<Icon icon={createOptionIcon} />
|
|
142
|
-
{t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: searchString })}
|
|
143
|
-
</Combobox.Item>
|
|
144
|
-
)}
|
|
145
|
-
</Combobox.List>
|
|
146
|
-
</>
|
|
147
|
-
)}
|
|
144
|
+
))}
|
|
145
|
+
{createSchema && onCreate && (
|
|
146
|
+
<CreateItem
|
|
147
|
+
query={query}
|
|
148
|
+
createOptionLabel={createOptionLabel}
|
|
149
|
+
createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
|
|
150
|
+
onCreateItemSelect={(currentQuery: string) => {
|
|
151
|
+
setFormInitialValue(currentQuery);
|
|
152
|
+
setShowForm(true);
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
)}
|
|
156
|
+
</Combobox.List>
|
|
148
157
|
<Combobox.Arrow />
|
|
149
158
|
</Combobox.Content>
|
|
150
159
|
);
|
|
151
160
|
},
|
|
152
161
|
);
|
|
153
162
|
|
|
154
|
-
|
|
163
|
+
const CreateItem = ({
|
|
164
|
+
query,
|
|
165
|
+
createOptionLabel,
|
|
166
|
+
createOptionIcon,
|
|
167
|
+
onCreateItemSelect,
|
|
168
|
+
}: {
|
|
169
|
+
query: string;
|
|
170
|
+
createOptionLabel?: [string, { ns: string | readonly string[] }];
|
|
171
|
+
createOptionIcon: string;
|
|
172
|
+
onCreateItemSelect: (query: string) => void;
|
|
173
|
+
}) => {
|
|
174
|
+
const { t } = useTranslation(translationKey);
|
|
175
|
+
|
|
176
|
+
const label = createOptionLabel
|
|
177
|
+
? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
|
|
178
|
+
: t('create-option.label');
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Combobox.Item
|
|
182
|
+
value='__create__'
|
|
183
|
+
label={label}
|
|
184
|
+
icon={createOptionIcon}
|
|
185
|
+
classNames='flex items-center gap-2'
|
|
186
|
+
closeOnSelect={false}
|
|
187
|
+
onSelect={() => {
|
|
188
|
+
onCreateItemSelect(query);
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
ObjectPickerContent.displayName = 'ObjectPicker.Content';
|
|
155
195
|
|
|
156
196
|
export const ObjectPicker = {
|
|
157
197
|
Root: Combobox.Root,
|
|
198
|
+
Portal: Combobox.Portal,
|
|
158
199
|
Trigger: Combobox.Trigger,
|
|
159
200
|
VirtualTrigger: Combobox.VirtualTrigger,
|
|
160
201
|
Content: ObjectPickerContent,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useEffect } from 'react';
|
|
8
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { DXN, Annotation, Filter, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
11
|
+
import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
|
|
12
|
+
import { useQuery } from '@dxos/react-client/echo';
|
|
13
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
|
+
import { Panel } from '@dxos/react-ui';
|
|
15
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
|
+
import { FactoryAnnotation, type FactoryFn } from '@dxos/schema';
|
|
17
|
+
import { Pipeline } from '@dxos/types';
|
|
18
|
+
|
|
19
|
+
import { translations } from '#translations';
|
|
20
|
+
|
|
21
|
+
import { OBJECT_PROPERTIES_DEBUG_SYMBOL } from '../testing';
|
|
22
|
+
import { ObjectProperties } from './ObjectProperties';
|
|
23
|
+
|
|
24
|
+
//
|
|
25
|
+
// Test-only schemas exercising a non-Tag ref-array. `Article.authors` is the
|
|
26
|
+
// case the generic create flow currently fails on: the picker creates an
|
|
27
|
+
// Author in the DB but never wires the new Ref into the array slot.
|
|
28
|
+
//
|
|
29
|
+
|
|
30
|
+
const Author = Schema.Struct({
|
|
31
|
+
name: Schema.String,
|
|
32
|
+
}).pipe(
|
|
33
|
+
LabelAnnotation.set(['name']),
|
|
34
|
+
Annotation.IconAnnotation.set({ icon: 'ph--user--regular', hue: 'blue' }),
|
|
35
|
+
Type.makeObject(DXN.make('org.dxos.test.author', '0.1.0')),
|
|
36
|
+
);
|
|
37
|
+
type Author = Type.InstanceType<typeof Author>;
|
|
38
|
+
|
|
39
|
+
const Article = Schema.Struct({
|
|
40
|
+
title: Schema.String.pipe(Schema.optional),
|
|
41
|
+
authors: Schema.Array(Ref.Ref(Author)),
|
|
42
|
+
}).pipe(
|
|
43
|
+
LabelAnnotation.set(['title']),
|
|
44
|
+
Annotation.IconAnnotation.set({ icon: 'ph--article--regular', hue: 'green' }),
|
|
45
|
+
Type.makeObject(DXN.make('org.dxos.test.article', '0.1.0')),
|
|
46
|
+
);
|
|
47
|
+
type Article = Type.InstanceType<typeof Article>;
|
|
48
|
+
|
|
49
|
+
//
|
|
50
|
+
// `Note` mirrors the `Subscription.Feed` shape: a required `Ref` field
|
|
51
|
+
// (`backing`) is hidden from the form via `FormInputAnnotation.set(false)`,
|
|
52
|
+
// so `Obj.make(Note, values)` from the picker would reject because the form
|
|
53
|
+
// values can't satisfy the schema. A `FactoryAnnotation` constructs the
|
|
54
|
+
// backing object and links it at create time. `cursor` is an optional hidden
|
|
55
|
+
// field included to exercise the same path for non-required hidden fields.
|
|
56
|
+
//
|
|
57
|
+
const NoteBacking = Schema.Struct({}).pipe(Type.makeObject(DXN.make('org.dxos.test.noteBacking', '0.1.0')));
|
|
58
|
+
type NoteBacking = Type.InstanceType<typeof NoteBacking>;
|
|
59
|
+
|
|
60
|
+
const Note = Schema.Struct({
|
|
61
|
+
title: Schema.String,
|
|
62
|
+
cursor: Schema.String.pipe(FormInputAnnotation.set(false), Schema.optional),
|
|
63
|
+
backing: Ref.Ref(NoteBacking).pipe(FormInputAnnotation.set(false)),
|
|
64
|
+
}).pipe(
|
|
65
|
+
LabelAnnotation.set(['title']),
|
|
66
|
+
Annotation.IconAnnotation.set({ icon: 'ph--note--regular', hue: 'amber' }),
|
|
67
|
+
FactoryAnnotation.set(((values: any) =>
|
|
68
|
+
Obj.make(Note, { ...values, backing: Ref.make(Obj.make(NoteBacking, {})) })) as FactoryFn),
|
|
69
|
+
Type.makeObject(DXN.make('org.dxos.test.note', '0.1.0')),
|
|
70
|
+
);
|
|
71
|
+
type Note = Type.InstanceType<typeof Note>;
|
|
72
|
+
|
|
73
|
+
const Notebook = Schema.Struct({
|
|
74
|
+
name: Schema.String.pipe(Schema.optional),
|
|
75
|
+
notes: Schema.Array(Ref.Ref(Note)),
|
|
76
|
+
}).pipe(
|
|
77
|
+
LabelAnnotation.set(['name']),
|
|
78
|
+
Annotation.IconAnnotation.set({ icon: 'ph--notebook--regular', hue: 'amber' }),
|
|
79
|
+
Type.makeObject(DXN.make('org.dxos.test.notebook', '0.1.0')),
|
|
80
|
+
);
|
|
81
|
+
type Notebook = Type.InstanceType<typeof Notebook>;
|
|
82
|
+
|
|
83
|
+
//
|
|
84
|
+
// Stories.
|
|
85
|
+
//
|
|
86
|
+
|
|
87
|
+
export type ObjectPropertiesDebug = {
|
|
88
|
+
/** Live database the story is rendering against. */
|
|
89
|
+
db: ReturnType<typeof useClientStory>['space'] extends infer S ? (S extends { db: infer D } ? D : never) : never;
|
|
90
|
+
/** Object whose properties are being edited. */
|
|
91
|
+
object: Obj.Unknown;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const PipelineStory = () => {
|
|
95
|
+
const { space } = useClientStory();
|
|
96
|
+
const [object] = useQuery(space?.db, Filter.type(Pipeline.Pipeline));
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (typeof window !== 'undefined' && space?.db && object) {
|
|
100
|
+
(window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
|
|
101
|
+
}
|
|
102
|
+
}, [space, object]);
|
|
103
|
+
|
|
104
|
+
if (!object) {
|
|
105
|
+
return <Loading />;
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
<Panel.Root>
|
|
109
|
+
<Panel.Content asChild>
|
|
110
|
+
<ObjectProperties object={object} />
|
|
111
|
+
</Panel.Content>
|
|
112
|
+
</Panel.Root>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const ArticleStory = () => {
|
|
117
|
+
const { space } = useClientStory();
|
|
118
|
+
const [object] = useQuery(space?.db, Filter.type(Article));
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (typeof window !== 'undefined' && space?.db && object) {
|
|
122
|
+
(window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
|
|
123
|
+
}
|
|
124
|
+
}, [space, object]);
|
|
125
|
+
|
|
126
|
+
if (!object) {
|
|
127
|
+
return <Loading />;
|
|
128
|
+
}
|
|
129
|
+
return (
|
|
130
|
+
<Panel.Root>
|
|
131
|
+
<Panel.Content asChild>
|
|
132
|
+
<ObjectProperties object={object} />
|
|
133
|
+
</Panel.Content>
|
|
134
|
+
</Panel.Root>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const meta = {
|
|
139
|
+
title: 'ui/react-ui-form/ObjectProperties',
|
|
140
|
+
component: ObjectProperties as any,
|
|
141
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
142
|
+
parameters: {
|
|
143
|
+
layout: 'fullscreen',
|
|
144
|
+
translations,
|
|
145
|
+
},
|
|
146
|
+
} satisfies Meta<typeof PipelineStory>;
|
|
147
|
+
|
|
148
|
+
export default meta;
|
|
149
|
+
|
|
150
|
+
type Story = StoryObj<typeof meta>;
|
|
151
|
+
|
|
152
|
+
const pipelineDecorators = [
|
|
153
|
+
withClientProvider({
|
|
154
|
+
createIdentity: true,
|
|
155
|
+
createSpace: true,
|
|
156
|
+
types: [Pipeline.Pipeline, Tag.Tag],
|
|
157
|
+
onCreateSpace: async ({ space }) => {
|
|
158
|
+
space.db.add(Pipeline.make());
|
|
159
|
+
space.db.add(Tag.make({ label: 'Tag 1' }));
|
|
160
|
+
space.db.add(Tag.make({ label: 'Tag 2' }));
|
|
161
|
+
space.db.add(Tag.make({ label: 'Tag 3' }));
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const articleDecorators = [
|
|
167
|
+
withClientProvider({
|
|
168
|
+
createIdentity: true,
|
|
169
|
+
createSpace: true,
|
|
170
|
+
types: [Article, Author, Tag.Tag],
|
|
171
|
+
onCreateSpace: async ({ space }) => {
|
|
172
|
+
space.db.add(Obj.make(Article, { title: 'Untitled article', authors: [] }));
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const NotebookStory = () => {
|
|
178
|
+
const { space } = useClientStory();
|
|
179
|
+
const [object] = useQuery(space?.db, Filter.type(Notebook));
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (typeof window !== 'undefined' && space?.db && object) {
|
|
183
|
+
(window as any)[OBJECT_PROPERTIES_DEBUG_SYMBOL] = { db: space.db, object } satisfies ObjectPropertiesDebug;
|
|
184
|
+
}
|
|
185
|
+
}, [space, object]);
|
|
186
|
+
|
|
187
|
+
if (!object) {
|
|
188
|
+
return <Loading />;
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
<Panel.Root>
|
|
192
|
+
<Panel.Content asChild>
|
|
193
|
+
<ObjectProperties object={object} />
|
|
194
|
+
</Panel.Content>
|
|
195
|
+
</Panel.Root>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const notebookDecorators = [
|
|
200
|
+
withClientProvider({
|
|
201
|
+
createIdentity: true,
|
|
202
|
+
createSpace: true,
|
|
203
|
+
types: [Notebook, Note, NoteBacking, Tag.Tag],
|
|
204
|
+
onCreateSpace: async ({ space }) => {
|
|
205
|
+
space.db.add(Obj.make(Notebook, { name: 'Untitled notebook', notes: [] }));
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Default Pipeline form — manual exploration. Includes the Tags picker (which
|
|
212
|
+
* is the only ref-array path that currently works end-to-end, by virtue of
|
|
213
|
+
* `Tag.Tag` being special-cased in `ObjectProperties.handleCreate`).
|
|
214
|
+
*/
|
|
215
|
+
export const Default: Story = {
|
|
216
|
+
render: PipelineStory,
|
|
217
|
+
decorators: pipelineDecorators,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Tag-creation play. Walks the inline create flow against the Tags picker and
|
|
222
|
+
* leaves the magazine in the post-Save state for assertions to inspect via
|
|
223
|
+
* `OBJECT_PROPERTIES_DEBUG_SYMBOL`.
|
|
224
|
+
*
|
|
225
|
+
* Expected end-state (currently passes):
|
|
226
|
+
* - new Tag.Tag exists in the DB with `label === "PinnedTag"`
|
|
227
|
+
* - `Obj.getMeta(object).tags` includes its DXN.
|
|
228
|
+
*/
|
|
229
|
+
export const CreateTagPlay: Story = {
|
|
230
|
+
render: PipelineStory,
|
|
231
|
+
decorators: pipelineDecorators,
|
|
232
|
+
play: async ({ canvasElement }) => {
|
|
233
|
+
const canvas = within(canvasElement);
|
|
234
|
+
const body = within(document.body);
|
|
235
|
+
|
|
236
|
+
// Tags is `Schema.Array(Ref.Ref(Tag.Tag))` and starts empty, so it renders
|
|
237
|
+
// as an "Add item" button — same shape as any other ref-array. Click it
|
|
238
|
+
// to materialise an empty slot, then open the slot's picker.
|
|
239
|
+
const addButton = await canvas.findByRole('button', { name: /add/i }, { timeout: 10_000 });
|
|
240
|
+
await userEvent.click(addButton);
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
242
|
+
|
|
243
|
+
const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
|
|
244
|
+
await expect(comboboxes.length).toBeGreaterThan(0);
|
|
245
|
+
await userEvent.click(comboboxes[0]);
|
|
246
|
+
|
|
247
|
+
const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
|
|
248
|
+
await userEvent.type(search, 'PinnedTag');
|
|
249
|
+
|
|
250
|
+
// The Tag picker localises the create option as "Add tag" (no text suffix
|
|
251
|
+
// — the translation has no `{{text}}` placeholder), so match on /add/.
|
|
252
|
+
const createOption = await body.findByRole('option', { name: /add/i }, { timeout: 3000 });
|
|
253
|
+
await userEvent.click(createOption);
|
|
254
|
+
|
|
255
|
+
const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
|
|
256
|
+
const labelInput = await within(form).findByLabelText(/^label$/i);
|
|
257
|
+
await userEvent.clear(labelInput);
|
|
258
|
+
await userEvent.type(labelInput, 'PinnedTag');
|
|
259
|
+
await userEvent.click(await within(form).findByTestId('save-button'));
|
|
260
|
+
|
|
261
|
+
// Allow async state updates to flush before the test asserts.
|
|
262
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Ref-array creation play. Walks Add → Open → Create → Save against
|
|
268
|
+
* `Article.authors`. After the fix, the new Author should be persisted AND
|
|
269
|
+
* wired into `article.authors[0]`. Today it persists to the DB but the array
|
|
270
|
+
* slot stays empty.
|
|
271
|
+
*/
|
|
272
|
+
export const CreateRefArrayPlay: Story = {
|
|
273
|
+
render: ArticleStory,
|
|
274
|
+
decorators: articleDecorators,
|
|
275
|
+
play: async ({ canvasElement }) => {
|
|
276
|
+
const canvas = within(canvasElement);
|
|
277
|
+
const body = within(document.body);
|
|
278
|
+
|
|
279
|
+
// ObjectProperties extends Article with the synthetic `_tags` meta-tags
|
|
280
|
+
// array (labelled "Tags"), so the form has two "Add item" buttons (Tags +
|
|
281
|
+
// Authors). Pick the last one — Authors comes after Tags in rendered order.
|
|
282
|
+
const addButtons = await canvas.findAllByRole('button', { name: /add/i }, { timeout: 10_000 });
|
|
283
|
+
await userEvent.click(addButtons[addButtons.length - 1]);
|
|
284
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
285
|
+
|
|
286
|
+
// Open the empty slot's combobox.
|
|
287
|
+
const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
|
|
288
|
+
await expect(comboboxes.length).toBeGreaterThan(0);
|
|
289
|
+
await userEvent.click(comboboxes[0]);
|
|
290
|
+
|
|
291
|
+
const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
|
|
292
|
+
await userEvent.type(search, 'Ada Lovelace');
|
|
293
|
+
|
|
294
|
+
// Author has no `createOptionLabel` → option falls back to "Create".
|
|
295
|
+
const createOption = await body.findByRole('option', { name: /create/i }, { timeout: 3000 });
|
|
296
|
+
await userEvent.click(createOption);
|
|
297
|
+
|
|
298
|
+
const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
|
|
299
|
+
const nameInput = await within(form).findByLabelText(/^name$/i);
|
|
300
|
+
await userEvent.clear(nameInput);
|
|
301
|
+
await userEvent.type(nameInput, 'Ada Lovelace');
|
|
302
|
+
await userEvent.click(await within(form).findByTestId('save-button'));
|
|
303
|
+
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Ref-array creation against a schema with a hidden required field. `Note`
|
|
310
|
+
* mirrors the shape of `Subscription.Feed`: the form omits `backing` (a
|
|
311
|
+
* required `Ref` annotated `FormInputAnnotation.set(false)`) but the schema
|
|
312
|
+
* requires it. Without `omitHiddenFormFields` + `FactoryAnnotation`, the
|
|
313
|
+
* form's validator would reject Save and the popover would never close.
|
|
314
|
+
*
|
|
315
|
+
* Expected end-state after the fix:
|
|
316
|
+
* - new Note exists in DB with a synthesised `backing` ref
|
|
317
|
+
* - `notebook.notes[0]` references the new Note
|
|
318
|
+
* - the create form is gone
|
|
319
|
+
*/
|
|
320
|
+
export const CreateHiddenFieldPlay: Story = {
|
|
321
|
+
render: NotebookStory,
|
|
322
|
+
decorators: notebookDecorators,
|
|
323
|
+
play: async ({ canvasElement }) => {
|
|
324
|
+
const canvas = within(canvasElement);
|
|
325
|
+
const body = within(document.body);
|
|
326
|
+
|
|
327
|
+
// Two "Add item" buttons (Tags + Notes) — Notes comes second.
|
|
328
|
+
const addButtons = await canvas.findAllByRole('button', { name: /add/i }, { timeout: 10_000 });
|
|
329
|
+
await userEvent.click(addButtons[addButtons.length - 1]);
|
|
330
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
331
|
+
|
|
332
|
+
const comboboxes = await canvas.findAllByRole('combobox', undefined, { timeout: 10_000 });
|
|
333
|
+
await expect(comboboxes.length).toBeGreaterThan(0);
|
|
334
|
+
await userEvent.click(comboboxes[0]);
|
|
335
|
+
|
|
336
|
+
const search = await body.findByPlaceholderText(/search/i, undefined, { timeout: 5000 });
|
|
337
|
+
await userEvent.type(search, 'Ideas');
|
|
338
|
+
|
|
339
|
+
const createOption = await body.findByRole('option', { name: /create/i }, { timeout: 3000 });
|
|
340
|
+
await userEvent.click(createOption);
|
|
341
|
+
|
|
342
|
+
const form = await body.findByTestId('create-referenced-object-form', undefined, { timeout: 5000 });
|
|
343
|
+
const titleInput = await within(form).findByLabelText(/^title$/i);
|
|
344
|
+
await userEvent.clear(titleInput);
|
|
345
|
+
await userEvent.type(titleInput, 'Ideas');
|
|
346
|
+
await userEvent.click(await within(form).findByTestId('save-button'));
|
|
347
|
+
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
349
|
+
},
|
|
350
|
+
};
|