@dxos/react-ui-form 0.8.4-main.406dc2a → 0.8.4-main.4668b7de9b
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 +3504 -2302
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +83 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +3504 -2302
- 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 +85 -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 +69 -60
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +135 -26
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +175 -138
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +53 -0
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +91 -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 +21 -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 +119 -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 +85 -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 +22 -0
- package/dist/types/src/components/Form/fields/RefField.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 +85 -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/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 +65 -59
- 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 +128 -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 +87 -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/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 +85 -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 +16 -20
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +72 -66
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +4 -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 +66 -60
- 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 +24 -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 +62 -51
- package/src/components/FieldEditor/FieldEditor.stories.tsx +44 -25
- package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
- package/src/components/FieldEditor/FieldEditor.tsx +88 -86
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +134 -252
- package/src/components/Form/Form.tsx +461 -106
- package/src/components/Form/FormField.tsx +363 -0
- package/src/components/Form/FormFieldComponent.stories.tsx +65 -0
- package/src/components/Form/FormFieldComponent.tsx +229 -0
- package/src/components/Form/FormFieldSet.tsx +173 -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 +178 -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 +146 -0
- package/src/components/Form/fields/MarkdownField.stories.tsx +206 -0
- package/src/components/Form/fields/MarkdownField.tsx +168 -0
- package/src/components/Form/fields/NumberField.tsx +39 -0
- package/src/components/Form/fields/RefField.tsx +235 -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} +31 -24
- 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.tsx +109 -0
- package/src/components/{ControlSection → ObjectForm}/index.ts +1 -1
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +30 -25
- 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 +144 -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/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 +75 -77
- package/src/components/ViewEditor/ViewEditor.test.tsx +16 -13
- package/src/components/ViewEditor/ViewEditor.tsx +295 -215
- package/src/components/index.ts +4 -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 +71 -65
- 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 +121 -0
- package/src/util/properties.ts +61 -0
- package/src/util/refs.ts +16 -18
- 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 -84
- 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 -79
- 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 -322
- 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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
import { expect, userEvent } from 'storybook/test';
|
|
9
|
+
|
|
10
|
+
import { Filter, Format, Obj, Ref } from '@dxos/echo';
|
|
11
|
+
import { useQuery } from '@dxos/react-client/echo';
|
|
12
|
+
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
+
import { Panel } from '@dxos/react-ui';
|
|
14
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
15
|
+
import { Text } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { translations } from '#translations';
|
|
18
|
+
|
|
19
|
+
import { TestLayout } from '../../testing';
|
|
20
|
+
import { Form } from '../Form';
|
|
21
|
+
|
|
22
|
+
//
|
|
23
|
+
// String-backed markdown.
|
|
24
|
+
//
|
|
25
|
+
|
|
26
|
+
const StringSchema = Schema.Struct({
|
|
27
|
+
notes: Schema.String.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown)).annotations({ title: 'Notes' }),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const StringStory = () => (
|
|
31
|
+
<TestLayout>
|
|
32
|
+
<Form.Root schema={StringSchema} defaultValues={{ notes: '# Hello\n\nMarkdown text *here*.' }}>
|
|
33
|
+
<Form.Viewport>
|
|
34
|
+
<Form.Content>
|
|
35
|
+
<Form.FieldSet />
|
|
36
|
+
</Form.Content>
|
|
37
|
+
</Form.Viewport>
|
|
38
|
+
</Form.Root>
|
|
39
|
+
</TestLayout>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
//
|
|
43
|
+
// Ref<Text>-backed markdown.
|
|
44
|
+
//
|
|
45
|
+
|
|
46
|
+
const TextNoteHolder = Schema.Struct({
|
|
47
|
+
instructions: Ref.Ref(Text.Text)
|
|
48
|
+
.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
|
|
49
|
+
.annotations({ title: 'Instructions' }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const RefStory = () => {
|
|
53
|
+
const { space } = useClientStory();
|
|
54
|
+
const [text] = useQuery(space?.db, Filter.type(Text.Text));
|
|
55
|
+
const values = useMemo(() => (text ? { instructions: Ref.make(text) } : undefined), [text]);
|
|
56
|
+
|
|
57
|
+
if (!space || !values) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<TestLayout>
|
|
63
|
+
<Panel.Root>
|
|
64
|
+
<Panel.Content asChild>
|
|
65
|
+
<Form.Root schema={TextNoteHolder} values={values as any} db={space.db}>
|
|
66
|
+
<Form.Viewport>
|
|
67
|
+
<Form.Content>
|
|
68
|
+
<Form.FieldSet />
|
|
69
|
+
</Form.Content>
|
|
70
|
+
</Form.Viewport>
|
|
71
|
+
</Form.Root>
|
|
72
|
+
</Panel.Content>
|
|
73
|
+
</Panel.Root>
|
|
74
|
+
</TestLayout>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//
|
|
79
|
+
// Ref<Text> empty — shows the create button.
|
|
80
|
+
//
|
|
81
|
+
|
|
82
|
+
const EmptyRefSchema = Schema.Struct({
|
|
83
|
+
instructions: Schema.optional(
|
|
84
|
+
Ref.Ref(Text.Text)
|
|
85
|
+
.pipe(Format.FormatAnnotation.set(Format.TypeFormat.Markdown))
|
|
86
|
+
.annotations({ title: 'Instructions' }),
|
|
87
|
+
),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const EmptyRefStory = () => {
|
|
91
|
+
const { space } = useClientStory();
|
|
92
|
+
if (!space) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<TestLayout>
|
|
98
|
+
<Panel.Root>
|
|
99
|
+
<Panel.Content asChild>
|
|
100
|
+
<Form.Root schema={EmptyRefSchema} defaultValues={{}} db={space.db}>
|
|
101
|
+
<Form.Viewport>
|
|
102
|
+
<Form.Content>
|
|
103
|
+
<Form.FieldSet />
|
|
104
|
+
</Form.Content>
|
|
105
|
+
</Form.Viewport>
|
|
106
|
+
</Form.Root>
|
|
107
|
+
</Panel.Content>
|
|
108
|
+
</Panel.Root>
|
|
109
|
+
</TestLayout>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const meta = {
|
|
114
|
+
title: 'ui/react-ui-form/MarkdownField',
|
|
115
|
+
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
116
|
+
parameters: {
|
|
117
|
+
layout: 'fullscreen',
|
|
118
|
+
translations,
|
|
119
|
+
},
|
|
120
|
+
} satisfies Meta;
|
|
121
|
+
|
|
122
|
+
export default meta;
|
|
123
|
+
|
|
124
|
+
type Story = StoryObj<typeof meta>;
|
|
125
|
+
|
|
126
|
+
export const StringBacked: Story = {
|
|
127
|
+
render: () => <StringStory />,
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Regression test: keystrokes into a string-backed markdown field must not
|
|
131
|
+
// blur the editor. Previously the EditorView was destroyed and recreated on
|
|
132
|
+
// every change because `onChange` was a useTextEditor dep and form callbacks
|
|
133
|
+
// are unstable across renders.
|
|
134
|
+
export const StringBackedKeepsFocus: Story = {
|
|
135
|
+
render: () => (
|
|
136
|
+
<TestLayout>
|
|
137
|
+
<Form.Root schema={StringSchema} defaultValues={{ notes: '' }}>
|
|
138
|
+
<Form.Viewport>
|
|
139
|
+
<Form.Content>
|
|
140
|
+
<Form.FieldSet />
|
|
141
|
+
</Form.Content>
|
|
142
|
+
</Form.Viewport>
|
|
143
|
+
</Form.Root>
|
|
144
|
+
</TestLayout>
|
|
145
|
+
),
|
|
146
|
+
play: async ({ canvasElement }) => {
|
|
147
|
+
const waitFor = async <T,>(predicate: () => T | null | undefined, message: string): Promise<T> => {
|
|
148
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
149
|
+
const value = predicate();
|
|
150
|
+
if (value) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
154
|
+
}
|
|
155
|
+
throw new Error(message);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const editorRoot = await waitFor(
|
|
159
|
+
() => canvasElement.querySelector<HTMLElement>('.cm-editor'),
|
|
160
|
+
'CodeMirror editor root (.cm-editor) not found',
|
|
161
|
+
);
|
|
162
|
+
const editorContent = editorRoot.querySelector<HTMLElement>('.cm-content')!;
|
|
163
|
+
await expect(editorContent).toBeVisible();
|
|
164
|
+
|
|
165
|
+
// Focus editor and wait for focus to settle inside the editor root.
|
|
166
|
+
editorContent.focus();
|
|
167
|
+
await waitFor(() => (editorRoot.contains(document.activeElement) ? true : null), 'Editor did not receive focus');
|
|
168
|
+
|
|
169
|
+
// Type several characters; focus must be preserved inside the editor root
|
|
170
|
+
// after every keystroke. Regression: previously the EditorView was
|
|
171
|
+
// destroyed and recreated on every change, which detached the focused
|
|
172
|
+
// contentDOM and blurred the input.
|
|
173
|
+
const phrase = 'hello world';
|
|
174
|
+
for (const ch of phrase) {
|
|
175
|
+
await userEvent.keyboard(ch);
|
|
176
|
+
await expect(editorRoot.contains(document.activeElement)).toBe(true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await expect(editorContent).toHaveTextContent(phrase);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const RefPopulated: Story = {
|
|
184
|
+
render: () => <RefStory />,
|
|
185
|
+
decorators: [
|
|
186
|
+
withClientProvider({
|
|
187
|
+
createIdentity: true,
|
|
188
|
+
createSpace: true,
|
|
189
|
+
types: [Text.Text],
|
|
190
|
+
onCreateSpace: async ({ space }) => {
|
|
191
|
+
space.db.add(Obj.make(Text.Text, { content: '# Magazine brief\n\nLong-form text describing what to gather.' }));
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const RefEmpty: Story = {
|
|
198
|
+
render: () => <EmptyRefStory />,
|
|
199
|
+
decorators: [
|
|
200
|
+
withClientProvider({
|
|
201
|
+
createIdentity: true,
|
|
202
|
+
createSpace: true,
|
|
203
|
+
types: [Text.Text],
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { useCallback, useMemo } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type Database, Obj, Ref } from '@dxos/echo';
|
|
9
|
+
import { AtomRef } from '@dxos/echo-atom';
|
|
10
|
+
import { createDocAccessor } from '@dxos/echo-db';
|
|
11
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
12
|
+
import { Editor, useBasicMarkdownExtensions } from '@dxos/react-ui-editor';
|
|
13
|
+
import { Text } from '@dxos/schema';
|
|
14
|
+
import { createDataExtensions } from '@dxos/ui-editor';
|
|
15
|
+
|
|
16
|
+
import { translationKey } from '#translations';
|
|
17
|
+
|
|
18
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
19
|
+
|
|
20
|
+
const editorClassNames =
|
|
21
|
+
'transition-colors bg-input-surface focus-within:bg-focus-surface border border-separator rounded-xs p-1 px-2';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Form field that edits a markdown value in a CodeMirror editor.
|
|
25
|
+
* Supports two backings:
|
|
26
|
+
* - `Schema.String` annotated with `Format.TypeFormat.Markdown`: edited as plain text via `getValue`/`onValueChange`.
|
|
27
|
+
* - `Ref<Text>` annotated with `Format.TypeFormat.Markdown`: edited in-place via an Automerge doc accessor.
|
|
28
|
+
* When the ref is empty and a database is available, a create button is shown to allocate a new Text.
|
|
29
|
+
*/
|
|
30
|
+
export const MarkdownField = ({
|
|
31
|
+
type,
|
|
32
|
+
readonly,
|
|
33
|
+
label,
|
|
34
|
+
jsonPath,
|
|
35
|
+
placeholder,
|
|
36
|
+
layout,
|
|
37
|
+
db,
|
|
38
|
+
getStatus,
|
|
39
|
+
getValue,
|
|
40
|
+
onValueChange,
|
|
41
|
+
}: FormFieldComponentProps) => {
|
|
42
|
+
const { status, error } = getStatus();
|
|
43
|
+
const isRef = Ref.isRefType(type);
|
|
44
|
+
const value = getValue();
|
|
45
|
+
|
|
46
|
+
if ((readonly || layout === 'static') && value == null) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const renderEditor = () => {
|
|
51
|
+
if (isRef) {
|
|
52
|
+
const reference = value as Ref.Ref<any> | undefined;
|
|
53
|
+
if (reference) {
|
|
54
|
+
return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (readonly) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<StringMarkdownEditor
|
|
66
|
+
value={(value as string | undefined) ?? ''}
|
|
67
|
+
placeholder={placeholder}
|
|
68
|
+
readonly={!!readonly}
|
|
69
|
+
onChange={(next) => onValueChange(type, next)}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const renderStatic = () => {
|
|
75
|
+
if (isRef) {
|
|
76
|
+
const reference = value as Ref.Ref<any> | undefined;
|
|
77
|
+
return reference ? <RefStaticText reference={reference} /> : null;
|
|
78
|
+
}
|
|
79
|
+
return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Input.Root validationValence={status}>
|
|
84
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
|
|
85
|
+
{layout === 'static' ? renderStatic() : renderEditor()}
|
|
86
|
+
{layout === 'full' && <Input.Validation>{error}</Input.Validation>}
|
|
87
|
+
</Input.Root>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
|
|
92
|
+
const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
|
|
93
|
+
const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
|
|
94
|
+
if (!text?.content) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return <p className='whitespace-pre-wrap'>{text.content}</p>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type RefMarkdownEditorProps = {
|
|
101
|
+
reference: Ref.Ref<any>;
|
|
102
|
+
placeholder?: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
|
|
106
|
+
const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
|
|
107
|
+
const dataExtensions = useMemo(
|
|
108
|
+
() => (text ? [createDataExtensions({ id: reference.uri, text: createDocAccessor(text, ['content']) })] : []),
|
|
109
|
+
[text, reference],
|
|
110
|
+
);
|
|
111
|
+
const extensions = useBasicMarkdownExtensions({ placeholder, extensions: dataExtensions });
|
|
112
|
+
if (!text) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Editor.Root>
|
|
118
|
+
<Editor.View classNames={editorClassNames} extensions={extensions} />
|
|
119
|
+
</Editor.Root>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
type StringMarkdownEditorProps = {
|
|
124
|
+
value: string;
|
|
125
|
+
placeholder?: string;
|
|
126
|
+
readonly?: boolean;
|
|
127
|
+
onChange: (value: string) => void;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const StringMarkdownEditor = ({ value, placeholder, readonly, onChange }: StringMarkdownEditorProps) => {
|
|
131
|
+
const extensions = useBasicMarkdownExtensions({ placeholder });
|
|
132
|
+
const handleChange = useCallback((next: string) => onChange(next), [onChange]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Editor.Root>
|
|
136
|
+
<Editor.View
|
|
137
|
+
classNames={editorClassNames}
|
|
138
|
+
extensions={extensions}
|
|
139
|
+
value={value}
|
|
140
|
+
onChange={readonly ? undefined : handleChange}
|
|
141
|
+
/>
|
|
142
|
+
</Editor.Root>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type CreateTextButtonProps = {
|
|
147
|
+
db?: Database.Database;
|
|
148
|
+
disabled?: boolean;
|
|
149
|
+
onCreate: (ref: Ref.Ref<Text.Text>) => void;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const CreateTextButton = ({ db, disabled, onCreate }: CreateTextButtonProps) => {
|
|
153
|
+
const { t } = useTranslation(translationKey);
|
|
154
|
+
const handleClick = useCallback(() => {
|
|
155
|
+
if (!db) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const text = db.add(Obj.make(Text.Text, { content: '' }));
|
|
159
|
+
onCreate(Ref.make(text));
|
|
160
|
+
}, [db, onCreate]);
|
|
161
|
+
|
|
162
|
+
return (
|
|
163
|
+
<Button onClick={handleClick} disabled={disabled}>
|
|
164
|
+
<Icon icon='ph--plus--regular' size={4} />
|
|
165
|
+
<span>{t('create-text.label')}</span>
|
|
166
|
+
</Button>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Input, type TextInputProps } from '@dxos/react-ui';
|
|
8
|
+
import { safeParseFloat } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
11
|
+
|
|
12
|
+
export const NumberField = ({
|
|
13
|
+
type,
|
|
14
|
+
readonly,
|
|
15
|
+
placeholder,
|
|
16
|
+
onValueChange,
|
|
17
|
+
onBlur,
|
|
18
|
+
...props
|
|
19
|
+
}: FormFieldComponentProps<number>) => {
|
|
20
|
+
const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
|
|
21
|
+
(event) => onValueChange(type, safeParseFloat(event.target.value) || 0),
|
|
22
|
+
[type, onValueChange],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<FormFieldWrapper<number> readonly={readonly} {...props}>
|
|
27
|
+
{({ value = '' }) => (
|
|
28
|
+
<Input.TextInput
|
|
29
|
+
type='number'
|
|
30
|
+
disabled={!!readonly}
|
|
31
|
+
placeholder={placeholder}
|
|
32
|
+
value={value}
|
|
33
|
+
onChange={handleChange}
|
|
34
|
+
onBlur={onBlur}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</FormFieldWrapper>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
8
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
9
|
+
import { useQuery, useType as defaultUseType } from '@dxos/echo-react';
|
|
10
|
+
import { ANY_OBJECT_TYPENAME, ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
|
+
import { findAnnotation } from '@dxos/effect';
|
|
12
|
+
import { URI } from '@dxos/keys';
|
|
13
|
+
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
14
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
15
|
+
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { translationKey } from '#translations';
|
|
18
|
+
|
|
19
|
+
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
20
|
+
import { omitHiddenFormFields, omitId } from '../Form';
|
|
21
|
+
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
22
|
+
|
|
23
|
+
// TODO(burdon): Factor out.
|
|
24
|
+
const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
25
|
+
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
|
|
29
|
+
results.map((result) => {
|
|
30
|
+
const id = Entity.getURI(result);
|
|
31
|
+
const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
|
|
32
|
+
const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
|
|
33
|
+
return { id, label: label ?? id };
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
|
|
37
|
+
useQuery(
|
|
38
|
+
db,
|
|
39
|
+
typename
|
|
40
|
+
? // For Ref.Ref(Obj.Unknown) we want to show all objects.
|
|
41
|
+
typename === ANY_OBJECT_TYPENAME
|
|
42
|
+
? Filter.everything()
|
|
43
|
+
: Filter.typename(typename)
|
|
44
|
+
: Filter.nothing(),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type RefFieldProps = FormFieldComponentProps &
|
|
48
|
+
Pick<
|
|
49
|
+
ObjectPickerContentProps,
|
|
50
|
+
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
51
|
+
> & {
|
|
52
|
+
db?: Database.Database;
|
|
53
|
+
// TODO(burdon): Replace hooks with callbacks.
|
|
54
|
+
useType?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
55
|
+
useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
56
|
+
getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
|
|
57
|
+
/**
|
|
58
|
+
* Persist a newly-created object. Called after the user fills out the
|
|
59
|
+
* inline create form and clicks Save. Should add the object to the
|
|
60
|
+
* database and return it (sync or async). The returned object is then
|
|
61
|
+
* wired into this slot's form value as a Ref.
|
|
62
|
+
*/
|
|
63
|
+
onCreate?: (schema: Type.AnyEntity, values: any) => Obj.Unknown | Promise<Obj.Unknown> | undefined | void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const RefField = (props: RefFieldProps) => {
|
|
67
|
+
const {
|
|
68
|
+
type,
|
|
69
|
+
readonly,
|
|
70
|
+
label,
|
|
71
|
+
jsonPath,
|
|
72
|
+
placeholder,
|
|
73
|
+
layout,
|
|
74
|
+
getStatus,
|
|
75
|
+
getValue,
|
|
76
|
+
createOptionLabel,
|
|
77
|
+
createOptionIcon,
|
|
78
|
+
createInitialValuePath,
|
|
79
|
+
createFieldMap,
|
|
80
|
+
db,
|
|
81
|
+
useType = defaultUseType,
|
|
82
|
+
useResults = defaultUseResults,
|
|
83
|
+
getOptions = defaultGetOptions,
|
|
84
|
+
onCreate,
|
|
85
|
+
onValueChange,
|
|
86
|
+
} = props;
|
|
87
|
+
const { t } = useTranslation(translationKey);
|
|
88
|
+
const { status, error } = getStatus();
|
|
89
|
+
|
|
90
|
+
const typename = useMemo(
|
|
91
|
+
() => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
|
|
92
|
+
[type],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const results = useResults(db, typename);
|
|
96
|
+
const options = useMemo(() => {
|
|
97
|
+
const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
|
|
98
|
+
return getOptions(results, { parentLabel });
|
|
99
|
+
}, [results, getOptions, type]);
|
|
100
|
+
|
|
101
|
+
const handleGetValue = useCallback(() => {
|
|
102
|
+
const formValue = getValue();
|
|
103
|
+
// Match form-value Refs against options by the bare entity id, not by full
|
|
104
|
+
// URI string. A just-created object's Ref (`Ref.make`) carries the LOCAL
|
|
105
|
+
// EID `echo:/<id>`, while options derived from `Entity.getURI` carry the
|
|
106
|
+
// qualified EID `echo://<spaceId>/<id>`; encoded snapshots use the DXN
|
|
107
|
+
// form `dxn:echo:<space|@>:<id>`. The entity id (a ULID) contains neither
|
|
108
|
+
// `:` nor `/`, so it is always the final delimiter-separated segment —
|
|
109
|
+
// splitting on both reconciles all three forms. Comparing full strings (or
|
|
110
|
+
// splitting on `:` alone, which leaves `/`-delimited EIDs intact) makes the
|
|
111
|
+
// just-created Ref's lookup fail and the slot render empty even though the
|
|
112
|
+
// underlying form value IS set.
|
|
113
|
+
const dxnToEntityId = (dxn: string): string => dxn.split(/[:/]/).filter(Boolean).pop() ?? dxn;
|
|
114
|
+
|
|
115
|
+
const unknownToRefOption = (value: unknown) => {
|
|
116
|
+
const isRef = Ref.isRef(value);
|
|
117
|
+
if (isRef || isRefSnapshot(value)) {
|
|
118
|
+
const dxnString = isRef ? value.uri : value['/'];
|
|
119
|
+
const objectId = dxnToEntityId(dxnString);
|
|
120
|
+
const matchingOption = options.find((option) => dxnToEntityId(option.id) === objectId);
|
|
121
|
+
if (matchingOption) {
|
|
122
|
+
return matchingOption;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return unknownToRefOption(formValue);
|
|
130
|
+
}, [options, getValue]);
|
|
131
|
+
|
|
132
|
+
const item = handleGetValue();
|
|
133
|
+
const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
|
|
134
|
+
const createSchema = useType(db, typename);
|
|
135
|
+
|
|
136
|
+
// Lift the popover open state so we can dismiss it after a successful create.
|
|
137
|
+
const [open, setOpen] = useState(false);
|
|
138
|
+
|
|
139
|
+
const handleCreate = useCallback(
|
|
140
|
+
async (values: any) => {
|
|
141
|
+
if (!createSchema || !onCreate) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const newObject = await onCreate(createSchema, values);
|
|
145
|
+
if (newObject) {
|
|
146
|
+
// Wire the newly-created object into this slot's form value. ArrayField
|
|
147
|
+
// owns the array; this RefField represents a single slot, so writing a
|
|
148
|
+
// Ref via `onValueChange` populates that slot.
|
|
149
|
+
onValueChange(type, Ref.make(newObject));
|
|
150
|
+
}
|
|
151
|
+
setOpen(false);
|
|
152
|
+
},
|
|
153
|
+
[createSchema, onCreate, type, onValueChange],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const handleUpdate = useCallback(
|
|
157
|
+
(id: string | undefined) => {
|
|
158
|
+
const item = options.find((option) => option.id === id);
|
|
159
|
+
const ref = item ? Ref.fromURI(URI.make(item.id)) : undefined;
|
|
160
|
+
onValueChange(type, ref);
|
|
161
|
+
},
|
|
162
|
+
[options, type, onValueChange],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleSelect = useCallback(
|
|
166
|
+
(id: string) => {
|
|
167
|
+
if (item?.id === id) {
|
|
168
|
+
handleUpdate(undefined);
|
|
169
|
+
} else {
|
|
170
|
+
handleUpdate(id);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
[item, handleUpdate],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
if (!typename || ((readonly || layout === 'static') && !item)) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Input.Root validationValence={status}>
|
|
182
|
+
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} />}
|
|
183
|
+
<div>
|
|
184
|
+
{readonly ? (
|
|
185
|
+
!item ? (
|
|
186
|
+
<p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
|
|
187
|
+
) : (
|
|
188
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
189
|
+
{item.label}
|
|
190
|
+
</DxAnchor>
|
|
191
|
+
)
|
|
192
|
+
) : (
|
|
193
|
+
<ObjectPicker.Root open={open} onOpenChange={setOpen}>
|
|
194
|
+
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
195
|
+
{item ? (
|
|
196
|
+
<div className='flex gap-form-gap w-full'>
|
|
197
|
+
<Input.Root key={item.id}>
|
|
198
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
199
|
+
</Input.Root>
|
|
200
|
+
</div>
|
|
201
|
+
) : (
|
|
202
|
+
<Button classNames='w-full text-start gap-form-gap'>
|
|
203
|
+
<div className='grow overflow-hidden'>
|
|
204
|
+
<span className='truncate text-description'>
|
|
205
|
+
{placeholder || label || t('ref-field.placeholder')}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
<Icon size={3} icon='ph--caret-down--bold' />
|
|
209
|
+
</Button>
|
|
210
|
+
)}
|
|
211
|
+
</ObjectPicker.Trigger>
|
|
212
|
+
<ObjectPicker.Portal>
|
|
213
|
+
<ObjectPicker.Content
|
|
214
|
+
classNames='dx-card-popover-width'
|
|
215
|
+
options={options}
|
|
216
|
+
selectedIds={selectedIds}
|
|
217
|
+
// Strip hidden (`FormInputAnnotation.set(false)`) fields so the
|
|
218
|
+
// form's validator doesn't reject required-but-hidden fields
|
|
219
|
+
// such as backing-object refs supplied by a `FactoryAnnotation`.
|
|
220
|
+
createSchema={createSchema && omitHiddenFormFields(omitId(Type.getSchema(createSchema)))}
|
|
221
|
+
createOptionLabel={createOptionLabel}
|
|
222
|
+
createOptionIcon={createOptionIcon}
|
|
223
|
+
createInitialValuePath={createInitialValuePath}
|
|
224
|
+
createFieldMap={createFieldMap}
|
|
225
|
+
onCreate={handleCreate}
|
|
226
|
+
onSelect={handleSelect}
|
|
227
|
+
/>
|
|
228
|
+
</ObjectPicker.Portal>
|
|
229
|
+
</ObjectPicker.Root>
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
{layout === 'full' && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
233
|
+
</Input.Root>
|
|
234
|
+
);
|
|
235
|
+
};
|