@dxos/react-ui-form 0.8.4-main.d05673bc65 → 0.8.4-main.d9fc60f731
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/dist/lib/browser/index.mjs +2008 -840
- 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 +2008 -840
- 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 +2 -2
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +64 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +71 -17
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +148 -86
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +24 -8
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +26 -13
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +11 -9
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- 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 +4 -4
- package/dist/types/src/components/Form/fields/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/BooleanField.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/components/Form/fields/GeoPointField.d.ts.map +1 -1
- 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 +8 -1
- package/dist/types/src/components/Form/fields/MarkdownField.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/types/src/components/Form/fields/RefField.d.ts +12 -6
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts +4 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts +64 -61
- package/dist/types/src/components/Form/fields/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextAreaField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TextField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts +1 -1
- package/dist/types/src/components/Form/fields/TupleField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/index.d.ts +2 -0
- package/dist/types/src/components/Form/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/index.d.ts +3 -0
- 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 +28 -6
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +64 -61
- 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 +28 -33
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +65 -61
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +1 -1
- 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 +2 -0
- package/dist/types/src/components/Settings/index.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts +9 -11
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +68 -66
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +13 -4
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts +2 -1
- package/dist/types/src/hooks/useKeyHandler.d.ts.map +1 -1
- 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 +64 -61
- 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.map +1 -1
- package/dist/types/src/util/formatting.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/properties.d.ts +18 -1
- package/dist/types/src/util/properties.d.ts.map +1 -1
- 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 +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +48 -41
- package/src/components/FieldEditor/FieldEditor.stories.tsx +4 -4
- package/src/components/FieldEditor/FieldEditor.test.tsx +4 -2
- package/src/components/FieldEditor/FieldEditor.tsx +23 -17
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +46 -65
- package/src/components/Form/Form.tsx +176 -60
- package/src/components/Form/FormField.tsx +107 -38
- package/src/components/Form/FormFieldComponent.tsx +77 -20
- package/src/components/Form/FormFieldSet.tsx +128 -78
- 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/fields/ArrayField.tsx +64 -35
- package/src/components/Form/fields/DateField.tsx +101 -0
- package/src/components/Form/fields/GeoPointField.tsx +12 -8
- 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 +135 -45
- package/src/components/Form/fields/RefField.tsx +83 -42
- package/src/components/Form/fields/SelectField.tsx +17 -5
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -6
- package/src/components/Form/fields/SelectOptionField.tsx +14 -13
- package/src/components/Form/fields/TextAreaField.tsx +1 -1
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +3 -2
- package/src/components/Form/fields/index.ts +2 -0
- package/src/components/Form/index.ts +3 -0
- 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/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +13 -11
- package/src/components/ObjectPicker/ObjectPicker.tsx +93 -66
- 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 +69 -21
- package/src/components/Settings/Settings.tsx +32 -181
- 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 +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +45 -51
- package/src/components/ViewEditor/ViewEditor.test.tsx +11 -9
- package/src/components/ViewEditor/ViewEditor.tsx +45 -55
- package/src/components/index.ts +3 -0
- package/src/components/testing.tsx +19 -11
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +67 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
- package/src/util/properties.test.ts +121 -0
- package/src/util/properties.ts +47 -4
- package/src/util/refs.ts +7 -3
- package/src/vite-env.d.ts +10 -0
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Option from 'effect/Option';
|
|
6
6
|
import * as SchemaAST from 'effect/SchemaAST';
|
|
7
|
-
import React, {
|
|
7
|
+
import React, { useCallback } from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { Ref } from '@dxos/echo';
|
|
10
|
+
import {
|
|
11
|
+
createJsonPath,
|
|
12
|
+
findNode,
|
|
13
|
+
getArrayElementType,
|
|
14
|
+
getDiscriminatedType,
|
|
15
|
+
isDiscriminatedUnion,
|
|
16
|
+
isNestedType,
|
|
17
|
+
} from '@dxos/effect';
|
|
18
|
+
import { useTranslation } from '@dxos/react-ui';
|
|
19
|
+
|
|
20
|
+
import { translationKey } from '#translations';
|
|
11
21
|
|
|
12
|
-
import { translationKey } from '../../../translations';
|
|
13
22
|
import { getFormProperties } from '../../../util';
|
|
14
23
|
import { useFormValues } from '../Form';
|
|
15
|
-
import { FormField, type FormFieldProps } from '../FormField';
|
|
24
|
+
import { FormField, IconBlock, type FormFieldProps } from '../FormField';
|
|
16
25
|
import { FormFieldLabel, type FormFieldStateProps } from '../FormFieldComponent';
|
|
17
26
|
|
|
18
27
|
export type ArrayFieldProps = {
|
|
@@ -54,7 +63,13 @@ export const ArrayField = ({
|
|
|
54
63
|
const handleAdd = useCallback(() => {
|
|
55
64
|
const defaultValue =
|
|
56
65
|
isNestedType(type) && elementType ? getDefaultObjectValue(elementType) : getDefaultValue(elementType);
|
|
57
|
-
values
|
|
66
|
+
// `values` is `undefined` on first render for arrays whose parent path
|
|
67
|
+
// hasn't been materialised in the form values yet (e.g. `package.repos`
|
|
68
|
+
// when the form starts with no `package`). `useFormValues`'s default-
|
|
69
|
+
// value effect would eventually backfill it to `[]`, but until then the
|
|
70
|
+
// old `values && ...` guard silently swallowed the click. Treat a
|
|
71
|
+
// missing array as empty so Add always produces `[defaultValue]`.
|
|
72
|
+
onValueChange(type, [...(values ?? []), defaultValue]);
|
|
58
73
|
}, [onValueChange, type, elementType, values]);
|
|
59
74
|
|
|
60
75
|
const handleDelete = useCallback(
|
|
@@ -71,48 +86,62 @@ export const ArrayField = ({
|
|
|
71
86
|
return null;
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
const renderItemAsObject = elementType && isNestedType(elementType) && !Ref.isRefType(elementType);
|
|
90
|
+
|
|
74
91
|
return (
|
|
75
92
|
<>
|
|
76
93
|
{(layout !== 'static' || (values && values.length > 0)) && (
|
|
77
|
-
<
|
|
94
|
+
<div className='flex items-center gap-2'>
|
|
95
|
+
<div className='flex-1 min-w-0'>
|
|
96
|
+
<FormFieldLabel readonly={readonly} label={label} path={createJsonPath(path ?? [])} asChild />
|
|
97
|
+
</div>
|
|
98
|
+
{!readonly && layout !== 'static' && (
|
|
99
|
+
<IconBlock inline icon='ph--plus--regular' label={t('add-item.button')} onClick={handleAdd} />
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
78
102
|
)}
|
|
79
103
|
|
|
80
|
-
<div
|
|
104
|
+
<div className='flex flex-col'>
|
|
81
105
|
{values?.map((_, index) => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
const isLast = index === values.length - 1;
|
|
107
|
+
|
|
108
|
+
// Object items: each row contains a recursively-rendered FormField
|
|
109
|
+
// (multiple sub-rows for the object's fields) wrapped in a border,
|
|
110
|
+
// with the delete button bottom-aligned next to it.
|
|
111
|
+
// Scalar items: a single-row inline FormField with a center-aligned
|
|
112
|
+
// delete button. Refs and primitive arrays use this layout.
|
|
113
|
+
const fieldField = (
|
|
114
|
+
<FormField
|
|
115
|
+
{...props}
|
|
116
|
+
autoFocus={isLast}
|
|
117
|
+
type={elementType}
|
|
118
|
+
// Suppress the per-item header for object items only — the recursive
|
|
119
|
+
// form already renders labels for each sub-field. Scalar items
|
|
120
|
+
// (refs, primitives) keep the parent name so inline-layout
|
|
121
|
+
// children (e.g. RefField) have a real label to use as a
|
|
122
|
+
// fallback placeholder.
|
|
123
|
+
{...(renderItemAsObject && { name: null })}
|
|
124
|
+
path={[...(path ?? []), index]}
|
|
125
|
+
readonly={readonly || layout === 'static'}
|
|
126
|
+
layout={renderItemAsObject ? (layout === 'static' ? 'static' : undefined) : 'inline'}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
92
129
|
|
|
130
|
+
return (
|
|
131
|
+
<div key={index} className='grid grid-cols-[1fr_min-content] items-center mb-1 last:mb-form-gap'>
|
|
132
|
+
{fieldField}
|
|
93
133
|
{!readonly && layout !== 'static' && (
|
|
94
|
-
<
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
label={t('button remove')}
|
|
101
|
-
onClick={() => handleDelete(index)}
|
|
102
|
-
classNames='self-center'
|
|
103
|
-
/>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
134
|
+
<IconBlock
|
|
135
|
+
inline={!renderItemAsObject}
|
|
136
|
+
icon='ph--x--regular'
|
|
137
|
+
label={t('remove-item.button')}
|
|
138
|
+
onClick={() => handleDelete(index)}
|
|
139
|
+
/>
|
|
106
140
|
)}
|
|
107
141
|
</div>
|
|
108
142
|
);
|
|
109
143
|
})}
|
|
110
144
|
</div>
|
|
111
|
-
|
|
112
|
-
{/* TODO(burdon): Get label from schema. */}
|
|
113
|
-
{!readonly && layout !== 'static' && (
|
|
114
|
-
<IconButton classNames='flex w-full' icon='ph--plus--regular' label={t('add field')} onClick={handleAdd} />
|
|
115
|
-
)}
|
|
116
145
|
</>
|
|
117
146
|
);
|
|
118
147
|
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { format as formatDate } from 'date-fns';
|
|
6
|
+
import React, { useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Format } from '@dxos/echo';
|
|
9
|
+
import { Input } from '@dxos/react-ui';
|
|
10
|
+
|
|
11
|
+
import { type FormFieldComponentProps, FormFieldWrapper } from '../FormFieldComponent';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stored value shapes:
|
|
15
|
+
* - `Format.DateTime` -> ISO 8601 (`2018-11-13T20:20:39.000Z`).
|
|
16
|
+
* - `Format.Date` -> `YYYY-MM-DD`.
|
|
17
|
+
* - `Format.Time` -> `HH:mm:ss`.
|
|
18
|
+
*
|
|
19
|
+
* Segmented input value shapes (react-aria-components-backed):
|
|
20
|
+
* - `Input.DateTime` -> `YYYY-MM-DDTHH:mm` in local time.
|
|
21
|
+
* - `Input.Date` -> `YYYY-MM-DD`.
|
|
22
|
+
* - `Input.Time` -> `HH:mm`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** ISO 8601 → `YYYY-MM-DDTHH:mm` in the user's local timezone. */
|
|
26
|
+
const isoToLocalDateTime = (value: string | undefined): string => {
|
|
27
|
+
if (!value) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
const date = new Date(value);
|
|
31
|
+
if (Number.isNaN(date.getTime())) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
return formatDate(date, "yyyy-MM-dd'T'HH:mm");
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** `YYYY-MM-DDTHH:mm` (local) → ISO 8601 with timezone. */
|
|
38
|
+
const localDateTimeToIso = (value: string): string | undefined => {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
// Treat the input as local time. `new Date('YYYY-MM-DDTHH:mm')` parses as local.
|
|
43
|
+
const date = new Date(value);
|
|
44
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const DateField = ({
|
|
48
|
+
type,
|
|
49
|
+
format,
|
|
50
|
+
readonly,
|
|
51
|
+
placeholder: _placeholder,
|
|
52
|
+
onValueChange,
|
|
53
|
+
onBlur: _onBlur,
|
|
54
|
+
...props
|
|
55
|
+
}: FormFieldComponentProps<string>) => {
|
|
56
|
+
const handleSimpleChange = useCallback((next: string) => onValueChange(type, next), [type, onValueChange]);
|
|
57
|
+
|
|
58
|
+
const handleDateTimeChange = useCallback(
|
|
59
|
+
(next: string) => {
|
|
60
|
+
const iso = localDateTimeToIso(next);
|
|
61
|
+
onValueChange(type, iso as string);
|
|
62
|
+
},
|
|
63
|
+
[type, onValueChange],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<FormFieldWrapper<string> readonly={readonly} format={format} {...props}>
|
|
68
|
+
{({ value }) => {
|
|
69
|
+
switch (format) {
|
|
70
|
+
case Format.TypeFormat.Date:
|
|
71
|
+
return (
|
|
72
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] items-stretch gap-1'>
|
|
73
|
+
<Input.Date
|
|
74
|
+
classNames='min-w-0 overflow-hidden'
|
|
75
|
+
disabled={!!readonly}
|
|
76
|
+
value={value ?? ''}
|
|
77
|
+
onValueChange={handleSimpleChange}
|
|
78
|
+
/>
|
|
79
|
+
<Input.TriggerIcon classNames='h-full' />
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
case Format.TypeFormat.Time:
|
|
83
|
+
return <Input.Time disabled={!!readonly} value={value ?? ''} onValueChange={handleSimpleChange} />;
|
|
84
|
+
case Format.TypeFormat.DateTime:
|
|
85
|
+
default:
|
|
86
|
+
return (
|
|
87
|
+
<div className='grid grid-cols-[minmax(0,1fr)_min-content] items-stretch gap-1'>
|
|
88
|
+
<Input.DateTime
|
|
89
|
+
classNames='min-w-0 overflow-hidden'
|
|
90
|
+
disabled={!!readonly}
|
|
91
|
+
value={isoToLocalDateTime(value)}
|
|
92
|
+
onValueChange={handleDateTimeChange}
|
|
93
|
+
/>
|
|
94
|
+
<Input.TriggerIcon classNames='h-full' />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
</FormFieldWrapper>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
@@ -8,12 +8,14 @@ import { GeoLocation, type GeoPoint } from '@dxos/echo/internal';
|
|
|
8
8
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { safeParseFloat } from '@dxos/util';
|
|
10
10
|
|
|
11
|
-
import { translationKey } from '
|
|
11
|
+
import { translationKey } from '#translations';
|
|
12
|
+
|
|
12
13
|
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
13
14
|
|
|
14
15
|
export const GeoPointField = ({
|
|
15
16
|
type,
|
|
16
17
|
label,
|
|
18
|
+
jsonPath,
|
|
17
19
|
readonly,
|
|
18
20
|
layout,
|
|
19
21
|
getStatus,
|
|
@@ -58,21 +60,23 @@ export const GeoPointField = ({
|
|
|
58
60
|
|
|
59
61
|
return (
|
|
60
62
|
<Input.Root validationValence={status}>
|
|
61
|
-
{layout !== 'inline' &&
|
|
63
|
+
{layout !== 'inline' && (
|
|
64
|
+
<FormFieldLabel error={error} readonly={readonly} label={label} path={jsonPath} asChild />
|
|
65
|
+
)}
|
|
62
66
|
{layout === 'static' ? (
|
|
63
67
|
<LatLng {...value} />
|
|
64
68
|
) : (
|
|
65
|
-
<div
|
|
69
|
+
<div className='grid grid-cols-2 gap-form-gap'>
|
|
66
70
|
<div>
|
|
67
71
|
<Input.Root>
|
|
68
|
-
{layout !== 'inline' && <Input.Label>{t('latitude
|
|
72
|
+
{layout !== 'inline' && <Input.Label>{t('latitude.label')}</Input.Label>}
|
|
69
73
|
<Input.TextInput
|
|
70
74
|
type='number'
|
|
71
75
|
step='0.00001'
|
|
72
76
|
min='-90'
|
|
73
77
|
max='90'
|
|
74
78
|
disabled={!!readonly}
|
|
75
|
-
placeholder={t('latitude
|
|
79
|
+
placeholder={t('latitude.placeholder')}
|
|
76
80
|
value={latitudeText ?? ''}
|
|
77
81
|
onChange={handleChange('latitude', setLatitudeText)}
|
|
78
82
|
onBlur={onBlur}
|
|
@@ -81,14 +85,14 @@ export const GeoPointField = ({
|
|
|
81
85
|
</div>
|
|
82
86
|
<div>
|
|
83
87
|
<Input.Root>
|
|
84
|
-
{layout !== 'inline' && <Input.Label>{t('longitude
|
|
88
|
+
{layout !== 'inline' && <Input.Label>{t('longitude.label')}</Input.Label>}
|
|
85
89
|
<Input.TextInput
|
|
86
90
|
type='number'
|
|
87
91
|
step='0.00001'
|
|
88
92
|
min='-180'
|
|
89
93
|
max='180'
|
|
90
94
|
disabled={!!readonly}
|
|
91
|
-
placeholder={t('longitude
|
|
95
|
+
placeholder={t('longitude.placeholder')}
|
|
92
96
|
value={longitudeText ?? ''}
|
|
93
97
|
onChange={handleChange('longitude', setLongitudeText)}
|
|
94
98
|
onBlur={onBlur}
|
|
@@ -106,7 +110,7 @@ const LatLng = ({ latitude = 0, longitude = 0 }: GeoLocation) => {
|
|
|
106
110
|
const lngHem = longitude >= 0 ? 'E' : 'W';
|
|
107
111
|
|
|
108
112
|
return (
|
|
109
|
-
<span className='inline-flex items-center gap-
|
|
113
|
+
<span className='inline-flex items-center gap-form-gap'>
|
|
110
114
|
<span>
|
|
111
115
|
<span>{Math.abs(latitude).toFixed(5)}</span>
|
|
112
116
|
<span className='text-subdued'>°{latHem}</span>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 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, Type } from '@dxos/echo';
|
|
9
|
+
import { AtomRef } from '@dxos/echo-atom';
|
|
10
|
+
import { useType as defaultUseType } from '@dxos/echo-react';
|
|
11
|
+
import {
|
|
12
|
+
type JsonPath,
|
|
13
|
+
ReferenceAnnotationId,
|
|
14
|
+
type ReferenceAnnotationValue,
|
|
15
|
+
splitJsonPath,
|
|
16
|
+
} from '@dxos/echo/internal';
|
|
17
|
+
import { findAnnotation } from '@dxos/effect';
|
|
18
|
+
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
19
|
+
|
|
20
|
+
import { translationKey } from '#translations';
|
|
21
|
+
|
|
22
|
+
import { Form, omitId } from '../Form';
|
|
23
|
+
import { Nesting } from '../FormField';
|
|
24
|
+
import { FormFieldLabel } from '../FormFieldComponent';
|
|
25
|
+
import { type RefFieldProps } from './RefField';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Renders a referenced object's own fields inline (a nested form bound to the
|
|
29
|
+
* target) rather than the {@link RefField} picker. Activated by
|
|
30
|
+
* `FormInlineAnnotation.set(true)` on the `Ref` property.
|
|
31
|
+
*
|
|
32
|
+
* The nested form edits the target object directly: each changed path is
|
|
33
|
+
* written back via `Obj.setValue` within an `Obj.update`, mirroring
|
|
34
|
+
* `ObjectProperties` (without the synthetic meta-tags row).
|
|
35
|
+
*/
|
|
36
|
+
export const InlineRefField = (props: RefFieldProps) => {
|
|
37
|
+
const {
|
|
38
|
+
type,
|
|
39
|
+
readonly,
|
|
40
|
+
label,
|
|
41
|
+
jsonPath,
|
|
42
|
+
layout,
|
|
43
|
+
db,
|
|
44
|
+
getValue,
|
|
45
|
+
onValueChange,
|
|
46
|
+
onCreate,
|
|
47
|
+
useType = defaultUseType,
|
|
48
|
+
} = props;
|
|
49
|
+
const { t } = useTranslation(translationKey);
|
|
50
|
+
|
|
51
|
+
const reference = getValue() as Ref.Ref<any> | undefined;
|
|
52
|
+
const typename = useMemo(
|
|
53
|
+
() => (type ? findAnnotation<ReferenceAnnotationValue>(type, ReferenceAnnotationId)?.typename : undefined),
|
|
54
|
+
[type],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Empty ref: offer to create the referenced object so the inline form has a target.
|
|
58
|
+
const createType = useType(db, typename);
|
|
59
|
+
const handleCreate = useCallback(async () => {
|
|
60
|
+
if (!createType || !onCreate) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const newObject = await onCreate(createType, {});
|
|
64
|
+
if (newObject) {
|
|
65
|
+
onValueChange(type, Ref.make(newObject));
|
|
66
|
+
}
|
|
67
|
+
}, [createType, onCreate, type, onValueChange]);
|
|
68
|
+
|
|
69
|
+
if (readonly && !reference) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Input.Root>
|
|
75
|
+
{layout !== 'inline' && <FormFieldLabel readonly={readonly} label={label} path={jsonPath} />}
|
|
76
|
+
{reference ? (
|
|
77
|
+
<InlineForm reference={reference} db={db} readonly={readonly} useType={useType} />
|
|
78
|
+
) : (
|
|
79
|
+
!readonly &&
|
|
80
|
+
onCreate && (
|
|
81
|
+
<Button classNames='w-full gap-form-gap' disabled={!createType || !db} onClick={() => void handleCreate()}>
|
|
82
|
+
<Icon icon='ph--plus--regular' size={4} />
|
|
83
|
+
<span>{label || t('ref-field.placeholder')}</span>
|
|
84
|
+
</Button>
|
|
85
|
+
)
|
|
86
|
+
)}
|
|
87
|
+
</Input.Root>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
type InlineFormProps = {
|
|
92
|
+
reference: Ref.Ref<any>;
|
|
93
|
+
db?: Database.Database;
|
|
94
|
+
readonly?: boolean;
|
|
95
|
+
// Accepts both the generic `defaultUseType` and `RefFieldProps['useType']`.
|
|
96
|
+
useType?: (db?: Database.Database, typename?: string) => Type.AnyEntity | undefined;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const InlineForm = ({ reference, db, readonly, useType = defaultUseType }: InlineFormProps) => {
|
|
100
|
+
const target = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
|
|
101
|
+
const typename = target ? (Obj.getTypename(target) ?? undefined) : undefined;
|
|
102
|
+
const typeFromRegistry = useType(db, typename);
|
|
103
|
+
const targetType = (target && Obj.getType(target)) || typeFromRegistry;
|
|
104
|
+
// Drop the ECHO `id` property (mirrors `ObjectProperties` via `withMetaTags`);
|
|
105
|
+
// otherwise the nested form renders an "Id" field. Hidden fields
|
|
106
|
+
// (`FormInputAnnotation.set(false)`) are already filtered by the field set.
|
|
107
|
+
const formSchema = useMemo(() => (targetType ? omitId(Type.getSchema(targetType)) : undefined), [targetType]);
|
|
108
|
+
const defaultValues = useMemo(() => (target ? { ...target } : {}), [target]);
|
|
109
|
+
|
|
110
|
+
// `formSchema` is resolved at runtime (the referenced object's schema), so the
|
|
111
|
+
// form's value type is not statically known here — `any` at this form-value
|
|
112
|
+
// boundary mirrors `ObjectProperties`. Each changed path is written back to the
|
|
113
|
+
// live target via `Obj.setValue`.
|
|
114
|
+
const handleChange = useCallback(
|
|
115
|
+
(values: any, { isValid, changed }: { isValid: boolean; changed: Record<JsonPath, boolean> }) => {
|
|
116
|
+
if (!isValid || !target) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const changedPaths = (Object.keys(changed) as JsonPath[]).filter((path) => changed[path]);
|
|
120
|
+
if (changedPaths.length === 0) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
Obj.update(target, () => {
|
|
124
|
+
for (const path of changedPaths) {
|
|
125
|
+
const parts = splitJsonPath(path);
|
|
126
|
+
Obj.setValue(target, parts, Obj.getValue(values, parts));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
[target],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (!target || !formSchema) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Nesting>
|
|
139
|
+
<Form.Root schema={formSchema} defaultValues={defaultValues as any} db={db} onValuesChanged={handleChange}>
|
|
140
|
+
<Form.Content>
|
|
141
|
+
<Form.FieldSet readonly={readonly} />
|
|
142
|
+
</Form.Content>
|
|
143
|
+
</Form.Root>
|
|
144
|
+
</Nesting>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
@@ -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
|
+
};
|