@dxos/react-ui-form 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +620 -440
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +82 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +620 -440
- 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 +84 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -62
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +27 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +73 -72
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +5 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -6
- 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 +5 -8
- package/dist/types/src/components/Form/FormFieldSet.d.ts.map +1 -1
- 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/GeoPointField.d.ts.map +1 -1
- 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 +84 -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 +10 -4
- package/dist/types/src/components/Form/fields/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/fields/SelectField.d.ts.map +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 +63 -62
- 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.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +17 -3
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -62
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts +108 -64
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- 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/Settings/Settings.d.ts +24 -8
- package/dist/types/src/components/Settings/Settings.d.ts.map +1 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts +64 -62
- 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 +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +63 -62
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +4 -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.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 +63 -62
- 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.map +1 -1
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -38
- package/src/components/FieldEditor/FieldEditor.stories.tsx +3 -4
- package/src/components/FieldEditor/FieldEditor.tsx +3 -2
- package/src/components/Form/Form.stories.tsx +5 -16
- package/src/components/Form/Form.tsx +30 -7
- package/src/components/Form/FormField.tsx +7 -5
- package/src/components/Form/FormFieldComponent.tsx +3 -0
- package/src/components/Form/FormFieldSet.tsx +1 -3
- package/src/components/Form/fields/ArrayField.tsx +8 -3
- package/src/components/Form/fields/GeoPointField.tsx +2 -1
- 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 +61 -30
- package/src/components/Form/fields/SelectOptionField.stories.tsx +8 -5
- package/src/components/Form/fields/SelectOptionField.tsx +3 -2
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/ObjectForm/ObjectForm.tsx +6 -5
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +5 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +28 -12
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +321 -22
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +26 -14
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +50 -1
- package/src/components/Settings/Settings.tsx +6 -53
- package/src/components/Settings/SettingsFieldSet.tsx +257 -0
- package/src/components/Settings/SettingsItem.tsx +62 -0
- package/src/components/Settings/SettingsSelectField.tsx +59 -0
- package/src/components/Settings/index.ts +2 -0
- package/src/components/ViewEditor/ViewEditor.stories.tsx +17 -18
- package/src/components/ViewEditor/ViewEditor.tsx +4 -3
- package/src/components/testing.tsx +20 -21
- package/src/index.ts +0 -4
- package/src/translations.ts +7 -4
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -0
|
@@ -2,77 +2,167 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import React, { useCallback, useMemo } from 'react';
|
|
6
7
|
|
|
7
|
-
import {
|
|
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';
|
|
8
17
|
|
|
9
18
|
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
10
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
|
+
*/
|
|
11
30
|
export const MarkdownField = ({
|
|
12
31
|
type,
|
|
13
32
|
readonly,
|
|
14
33
|
label,
|
|
15
34
|
placeholder,
|
|
16
35
|
layout,
|
|
36
|
+
db,
|
|
17
37
|
getStatus,
|
|
18
38
|
getValue,
|
|
19
39
|
onValueChange,
|
|
20
|
-
onBlur,
|
|
21
40
|
}: FormFieldComponentProps) => {
|
|
22
41
|
const { status, error } = getStatus();
|
|
23
|
-
const
|
|
42
|
+
const isRef = Ref.isRefType(type);
|
|
43
|
+
const value = getValue();
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
if ((readonly || layout === 'static') && value == null) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
27
48
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}, []);
|
|
49
|
+
const renderEditor = () => {
|
|
50
|
+
if (isRef) {
|
|
51
|
+
const reference = value as Ref.Ref<any> | undefined;
|
|
52
|
+
if (reference) {
|
|
53
|
+
return <RefMarkdownEditor reference={reference} placeholder={placeholder} />;
|
|
54
|
+
}
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}, [adjustHeight]);
|
|
56
|
+
if (readonly) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
40
59
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
adjustHeight();
|
|
44
|
-
}, [getValue(), adjustHeight]);
|
|
60
|
+
return <CreateTextButton db={db} disabled={!db} onCreate={(ref) => onValueChange(type, ref)} />;
|
|
61
|
+
}
|
|
45
62
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
return (
|
|
64
|
+
<StringMarkdownEditor
|
|
65
|
+
value={(value as string | undefined) ?? ''}
|
|
66
|
+
placeholder={placeholder}
|
|
67
|
+
readonly={!!readonly}
|
|
68
|
+
onChange={(next) => onValueChange(type, next)}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
50
72
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
const renderStatic = () => {
|
|
74
|
+
if (isRef) {
|
|
75
|
+
const reference = value as Ref.Ref<any> | undefined;
|
|
76
|
+
return reference ? <RefStaticText reference={reference} /> : null;
|
|
77
|
+
}
|
|
78
|
+
return <p className='whitespace-pre-wrap'>{(value as string | undefined) ?? ''}</p>;
|
|
79
|
+
};
|
|
55
80
|
|
|
56
|
-
// TODO(burdon): Use Markdown Editor.
|
|
57
81
|
return (
|
|
58
82
|
<Input.Root validationValence={status}>
|
|
59
83
|
{layout !== 'inline' && <FormFieldLabel error={error} readonly={readonly} label={label} />}
|
|
60
|
-
{layout === 'static' ? (
|
|
61
|
-
<p>{value}</p>
|
|
62
|
-
) : (
|
|
63
|
-
<Input.TextArea
|
|
64
|
-
ref={textareaRef}
|
|
65
|
-
disabled={!!readonly}
|
|
66
|
-
placeholder={placeholder}
|
|
67
|
-
value={value ?? ''}
|
|
68
|
-
classNames={'min-h-auto max-h-40 overflow-auto'}
|
|
69
|
-
onChange={handleChange}
|
|
70
|
-
onBlur={onBlur}
|
|
71
|
-
style={{ resize: 'none' }}
|
|
72
|
-
spellCheck={false}
|
|
73
|
-
/>
|
|
74
|
-
)}
|
|
84
|
+
{layout === 'static' ? renderStatic() : renderEditor()}
|
|
75
85
|
{layout === 'full' && <Input.Validation>{error}</Input.Validation>}
|
|
76
86
|
</Input.Root>
|
|
77
87
|
);
|
|
78
88
|
};
|
|
89
|
+
|
|
90
|
+
/** Read-only static rendering for a `Ref<Text>` value: resolve the ref and print its content. */
|
|
91
|
+
const RefStaticText = ({ reference }: { reference: Ref.Ref<any> }) => {
|
|
92
|
+
const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
|
|
93
|
+
if (!text?.content) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return <p className='whitespace-pre-wrap'>{text.content}</p>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
type RefMarkdownEditorProps = {
|
|
100
|
+
reference: Ref.Ref<any>;
|
|
101
|
+
placeholder?: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const RefMarkdownEditor = ({ reference, placeholder }: RefMarkdownEditorProps) => {
|
|
105
|
+
const text = useAtomValue(useMemo(() => AtomRef.make(reference), [reference]));
|
|
106
|
+
const dataExtensions = useMemo(
|
|
107
|
+
() =>
|
|
108
|
+
text ? [createDataExtensions({ id: reference.dxn.toString(), 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
|
+
};
|
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { useCallback, useMemo } from 'react';
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
8
8
|
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
9
|
-
import { useQuery, useSchema as
|
|
9
|
+
import { useQuery, useSchema as defaultUseSchema } from '@dxos/echo-react';
|
|
10
10
|
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
11
|
import { findAnnotation } from '@dxos/effect';
|
|
12
12
|
import { DXN } from '@dxos/keys';
|
|
13
13
|
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
14
14
|
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
15
15
|
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
|
-
import { mx } from '@dxos/ui-theme';
|
|
17
16
|
|
|
18
|
-
import { translationKey } from '
|
|
17
|
+
import { translationKey } from '#translations';
|
|
18
|
+
|
|
19
19
|
import { ObjectPicker, type ObjectPickerContentProps, type RefOption } from '../../ObjectPicker';
|
|
20
|
-
import { omitId } from '../Form';
|
|
20
|
+
import { omitHiddenFormFields, omitId } from '../Form';
|
|
21
21
|
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
22
22
|
|
|
23
23
|
// TODO(burdon): Factor out.
|
|
@@ -33,7 +33,7 @@ const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, {
|
|
|
33
33
|
return { id, label: label ?? id };
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
const
|
|
36
|
+
const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
|
|
37
37
|
useQuery(
|
|
38
38
|
db,
|
|
39
39
|
typename
|
|
@@ -50,10 +50,17 @@ export type RefFieldProps = FormFieldComponentProps &
|
|
|
50
50
|
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
51
51
|
> & {
|
|
52
52
|
db?: Database.Database;
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// TODO(burdon): Replace hooks with callbacks.
|
|
54
|
+
useSchema?: (db?: Database.Database, typename?: string) => Type.AnyEntity;
|
|
55
|
+
useResults?: (db?: Database.Database, typename?: string) => Entity.Any[];
|
|
55
56
|
getOptions?: (objects: Entity.Any[], options?: { parentLabel?: boolean }) => RefOption[];
|
|
56
|
-
|
|
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;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
export const RefField = (props: RefFieldProps) => {
|
|
@@ -70,8 +77,8 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
70
77
|
createInitialValuePath,
|
|
71
78
|
createFieldMap,
|
|
72
79
|
db,
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
useSchema = defaultUseSchema,
|
|
81
|
+
useResults = defaultUseResults,
|
|
75
82
|
getOptions = defaultGetOptions,
|
|
76
83
|
onCreate,
|
|
77
84
|
onValueChange,
|
|
@@ -92,42 +99,63 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
92
99
|
|
|
93
100
|
const handleGetValue = useCallback(() => {
|
|
94
101
|
const formValue = getValue();
|
|
102
|
+
// Match form-value Refs against options by the bare object id (last DXN
|
|
103
|
+
// segment), not by full DXN string. Ref.make uses
|
|
104
|
+
// `DXN.fromLocalObjectId(id)` (`dxn:echo:@:<id>`), but
|
|
105
|
+
// `Entity.getDXN(obj)` on a registered object produces the space-scoped
|
|
106
|
+
// form (`dxn:echo:<spaceId>:<id>`). String-comparing the two never
|
|
107
|
+
// matches, so the just-created Ref's option lookup fails and the slot
|
|
108
|
+
// displays as empty even though the underlying form value IS set.
|
|
109
|
+
const dxnToObjectId = (dxn: string): string => dxn.split(':').pop() ?? dxn;
|
|
95
110
|
|
|
96
111
|
const unknownToRefOption = (value: unknown) => {
|
|
97
112
|
const isRef = Ref.isRef(value);
|
|
98
113
|
if (isRef || isRefSnapshot(value)) {
|
|
99
114
|
const dxnString = isRef ? value.dxn.toString() : value['/'];
|
|
100
|
-
const
|
|
115
|
+
const objectId = dxnToObjectId(dxnString);
|
|
116
|
+
const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
|
|
101
117
|
if (matchingOption) {
|
|
102
118
|
return matchingOption;
|
|
103
119
|
}
|
|
104
120
|
}
|
|
121
|
+
|
|
105
122
|
return undefined;
|
|
106
123
|
};
|
|
107
124
|
|
|
108
125
|
return unknownToRefOption(formValue);
|
|
109
126
|
}, [options, getValue]);
|
|
110
127
|
|
|
111
|
-
const handleUpdate = useCallback(
|
|
112
|
-
(id: string | undefined) => {
|
|
113
|
-
const item = options.find((option) => option.id === id);
|
|
114
|
-
const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
|
|
115
|
-
onValueChange(type, ref);
|
|
116
|
-
},
|
|
117
|
-
[options, type, onValueChange],
|
|
118
|
-
);
|
|
119
|
-
|
|
120
128
|
const item = handleGetValue();
|
|
121
129
|
const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
|
|
122
130
|
const createSchema = useSchema(db, typename);
|
|
123
131
|
|
|
132
|
+
// Lift the popover open state so we can dismiss it after a successful create.
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
|
|
124
135
|
const handleCreate = useCallback(
|
|
125
|
-
(values: any) => {
|
|
126
|
-
if (createSchema
|
|
127
|
-
|
|
136
|
+
async (values: any) => {
|
|
137
|
+
if (!createSchema || !onCreate) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const newObject = await onCreate(createSchema, values);
|
|
141
|
+
if (newObject) {
|
|
142
|
+
// Wire the newly-created object into this slot's form value. ArrayField
|
|
143
|
+
// owns the array; this RefField represents a single slot, so writing a
|
|
144
|
+
// Ref via `onValueChange` populates that slot.
|
|
145
|
+
onValueChange(type, Ref.make(newObject));
|
|
128
146
|
}
|
|
147
|
+
setOpen(false);
|
|
129
148
|
},
|
|
130
|
-
[createSchema, onCreate],
|
|
149
|
+
[createSchema, onCreate, type, onValueChange],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const handleUpdate = useCallback(
|
|
153
|
+
(id: string | undefined) => {
|
|
154
|
+
const item = options.find((option) => option.id === id);
|
|
155
|
+
const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
|
|
156
|
+
onValueChange(type, ref);
|
|
157
|
+
},
|
|
158
|
+
[options, type, onValueChange],
|
|
131
159
|
);
|
|
132
160
|
|
|
133
161
|
const handleSelect = useCallback(
|
|
@@ -151,17 +179,17 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
151
179
|
<div>
|
|
152
180
|
{readonly ? (
|
|
153
181
|
!item ? (
|
|
154
|
-
<p className=
|
|
182
|
+
<p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
|
|
155
183
|
) : (
|
|
156
184
|
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
157
185
|
{item.label}
|
|
158
186
|
</DxAnchor>
|
|
159
187
|
)
|
|
160
188
|
) : (
|
|
161
|
-
<ObjectPicker.Root>
|
|
189
|
+
<ObjectPicker.Root open={open} onOpenChange={setOpen}>
|
|
162
190
|
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
163
191
|
{item ? (
|
|
164
|
-
<div className='flex gap-form-gap w-full'>
|
|
192
|
+
<div role='none' className='flex gap-form-gap w-full'>
|
|
165
193
|
<Input.Root key={item.id}>
|
|
166
194
|
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
167
195
|
</Input.Root>
|
|
@@ -169,7 +197,7 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
169
197
|
) : (
|
|
170
198
|
<Button classNames='w-full text-start gap-form-gap'>
|
|
171
199
|
<div role='none' className='grow overflow-hidden'>
|
|
172
|
-
<span className='
|
|
200
|
+
<span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
|
|
173
201
|
</div>
|
|
174
202
|
<Icon size={3} icon='ph--caret-down--bold' />
|
|
175
203
|
</Button>
|
|
@@ -180,7 +208,10 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
180
208
|
classNames='dx-card-popover-width'
|
|
181
209
|
options={options}
|
|
182
210
|
selectedIds={selectedIds}
|
|
183
|
-
|
|
211
|
+
// Strip hidden (`FormInputAnnotation.set(false)`) fields so the
|
|
212
|
+
// form's validator doesn't reject required-but-hidden fields
|
|
213
|
+
// such as backing-object refs supplied by a `FactoryAnnotation`.
|
|
214
|
+
createSchema={createSchema && omitHiddenFormFields(omitId(createSchema))}
|
|
184
215
|
createOptionLabel={createOptionLabel}
|
|
185
216
|
createOptionIcon={createOptionIcon}
|
|
186
217
|
createInitialValuePath={createInitialValuePath}
|
|
@@ -9,7 +9,8 @@ import React, { useState } from 'react';
|
|
|
9
9
|
import { SelectOption } from '@dxos/echo/internal';
|
|
10
10
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
11
|
|
|
12
|
-
import { translations } from '
|
|
12
|
+
import { translations } from '#translations';
|
|
13
|
+
|
|
13
14
|
import { TestLayout } from '../../testing';
|
|
14
15
|
import { Form } from '../Form';
|
|
15
16
|
import { SelectOptionField } from './SelectOptionField';
|
|
@@ -36,10 +37,12 @@ const DefaultStory = () => {
|
|
|
36
37
|
fieldMap={{ options: SelectOptionField }}
|
|
37
38
|
onSave={(values) => setValues(values)}
|
|
38
39
|
>
|
|
39
|
-
<Form.
|
|
40
|
-
<Form.
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
<Form.Viewport>
|
|
41
|
+
<Form.Content>
|
|
42
|
+
<Form.FieldSet />
|
|
43
|
+
<Form.Actions />
|
|
44
|
+
</Form.Content>
|
|
45
|
+
</Form.Viewport>
|
|
43
46
|
</Form.Root>
|
|
44
47
|
</TestLayout>
|
|
45
48
|
);
|
|
@@ -11,7 +11,8 @@ import { List } from '@dxos/react-ui-list';
|
|
|
11
11
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
12
|
import { hues, osTranslations, subtleHover } from '@dxos/ui-theme';
|
|
13
13
|
|
|
14
|
-
import { translationKey } from '
|
|
14
|
+
import { translationKey } from '#translations';
|
|
15
|
+
|
|
15
16
|
import { type FormFieldComponentProps, FormFieldLabel } from '../FormFieldComponent';
|
|
16
17
|
|
|
17
18
|
export const SelectOptionField = ({
|
|
@@ -146,7 +147,7 @@ export const SelectOptionField = ({
|
|
|
146
147
|
<IconButton
|
|
147
148
|
iconOnly
|
|
148
149
|
variant='ghost'
|
|
149
|
-
label={t(selected === item.id ? 'collapse
|
|
150
|
+
label={t(selected === item.id ? 'collapse.label' : 'expand.label', { ns: osTranslations })}
|
|
150
151
|
icon={selected === item.id ? 'ph--caret-down--regular' : 'ph--caret-right--regular'}
|
|
151
152
|
onClick={() => handleClick(item.id)}
|
|
152
153
|
/>
|
|
@@ -12,8 +12,8 @@ export const TextField = ({
|
|
|
12
12
|
type,
|
|
13
13
|
readonly,
|
|
14
14
|
placeholder,
|
|
15
|
-
onValueChange,
|
|
16
15
|
onBlur,
|
|
16
|
+
onValueChange,
|
|
17
17
|
...props
|
|
18
18
|
}: FormFieldComponentProps<string>) => {
|
|
19
19
|
const handleChange = useCallback<NonNullable<TextInputProps['onChange']>>(
|
|
@@ -29,8 +29,8 @@ export const TextField = ({
|
|
|
29
29
|
disabled={!!readonly}
|
|
30
30
|
placeholder={placeholder}
|
|
31
31
|
value={value}
|
|
32
|
-
onChange={handleChange}
|
|
33
32
|
onBlur={onBlur}
|
|
33
|
+
onChange={handleChange}
|
|
34
34
|
/>
|
|
35
35
|
)}
|
|
36
36
|
</FormFieldWrapper>
|
|
@@ -11,7 +11,8 @@ import { invariant } from '@dxos/invariant';
|
|
|
11
11
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
12
|
import { isNonNullable } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
import { translationKey } from '
|
|
14
|
+
import { translationKey } from '#translations';
|
|
15
|
+
|
|
15
16
|
import { Form, type FormFieldMap, omitId } from '../Form';
|
|
16
17
|
|
|
17
18
|
export type ObjectFormProps = {
|
|
@@ -52,7 +53,7 @@ export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
|
|
|
52
53
|
invariant(Type.isObjectSchema(schema));
|
|
53
54
|
const newObject = db.add(Obj.make(schema, values));
|
|
54
55
|
if (Obj.instanceOf(Tag.Tag, newObject)) {
|
|
55
|
-
Obj.
|
|
56
|
+
Obj.update(object, (object) => {
|
|
56
57
|
Obj.getMeta(object).tags = [...(Obj.getMeta(object).tags ?? []), Obj.getDXN(newObject).toString()];
|
|
57
58
|
});
|
|
58
59
|
}
|
|
@@ -67,10 +68,10 @@ export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
|
|
|
67
68
|
|
|
68
69
|
const changedPaths = Object.keys(changed).filter((path) => changed[path as JsonPath]) as JsonPath[];
|
|
69
70
|
|
|
70
|
-
// Handle tags separately using Obj.
|
|
71
|
+
// Handle tags separately using Obj.update.
|
|
71
72
|
const hasTagsChange = changedPaths.some((path) => splitJsonPath(path)[0] === 'tags');
|
|
72
73
|
if (hasTagsChange) {
|
|
73
|
-
Obj.
|
|
74
|
+
Obj.update(object, (object) => {
|
|
74
75
|
Obj.getMeta(object).tags = tags?.map((tag: Ref.Ref<Tag.Tag>) => tag.dxn.toString()) ?? [];
|
|
75
76
|
});
|
|
76
77
|
}
|
|
@@ -78,7 +79,7 @@ export const ObjectForm = ({ object, schema }: ObjectFormProps) => {
|
|
|
78
79
|
// Handle other property changes.
|
|
79
80
|
const nonTagPaths = changedPaths.filter((path) => splitJsonPath(path)[0] !== 'tags');
|
|
80
81
|
if (nonTagPaths.length > 0) {
|
|
81
|
-
Obj.
|
|
82
|
+
Obj.update(object, () => {
|
|
82
83
|
for (const path of nonTagPaths) {
|
|
83
84
|
const parts = splitJsonPath(path);
|
|
84
85
|
const value = Obj.getValue(values, parts);
|
|
@@ -16,7 +16,8 @@ import { withTheme } from '@dxos/react-ui/testing';
|
|
|
16
16
|
import { Person } from '@dxos/types';
|
|
17
17
|
import { osTranslations } from '@dxos/ui-theme';
|
|
18
18
|
|
|
19
|
-
import { translations } from '
|
|
19
|
+
import { translations } from '#translations';
|
|
20
|
+
|
|
20
21
|
import { ObjectPicker } from './ObjectPicker';
|
|
21
22
|
|
|
22
23
|
random.seed(1);
|
|
@@ -64,7 +65,9 @@ const DefaultStory = () => {
|
|
|
64
65
|
const handleCreateCallback = useCallback(
|
|
65
66
|
(values: any) => {
|
|
66
67
|
console.log('[on create]', values);
|
|
67
|
-
if (!space)
|
|
68
|
+
if (!space) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
68
71
|
const newPerson = space.db.add(Obj.make(Person.Person, values));
|
|
69
72
|
mockHandleCreate(values);
|
|
70
73
|
return newPerson;
|
|
@@ -6,9 +6,11 @@ import type * as Schema from 'effect/Schema';
|
|
|
6
6
|
import React, { type KeyboardEvent, forwardRef, useCallback, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type Palette, Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
9
|
-
import { Combobox
|
|
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 { translationKey } from '../../translations';
|
|
12
14
|
import { Form } from '../Form';
|
|
13
15
|
import { type FormFieldMap } from '../Form/FormFieldComponent';
|
|
14
16
|
|
|
@@ -26,7 +28,14 @@ export type ObjectPickerContentProps = ThemedClassName<{
|
|
|
26
28
|
createSchema?: Schema.Schema.AnyNoContext;
|
|
27
29
|
createInitialValuePath?: string;
|
|
28
30
|
createFieldMap?: FormFieldMap;
|
|
29
|
-
|
|
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>;
|
|
30
39
|
onSelect: (id: string) => void;
|
|
31
40
|
}>;
|
|
32
41
|
|
|
@@ -50,13 +59,13 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
50
59
|
const [showForm, setShowForm] = useState(false);
|
|
51
60
|
const [formInitialValue, setFormInitialValue] = useState<string>('');
|
|
52
61
|
|
|
53
|
-
const { results, handleSearch } = useSearchListResults({
|
|
62
|
+
const { results, query, handleSearch } = useSearchListResults({
|
|
54
63
|
items: options,
|
|
55
64
|
});
|
|
56
65
|
|
|
57
66
|
const handleFormSave = useCallback(
|
|
58
|
-
(values: any) => {
|
|
59
|
-
onCreate?.(values);
|
|
67
|
+
async (values: any) => {
|
|
68
|
+
await onCreate?.(values);
|
|
60
69
|
setShowForm(false);
|
|
61
70
|
setFormInitialValue('');
|
|
62
71
|
},
|
|
@@ -91,7 +100,7 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
91
100
|
|
|
92
101
|
if (showForm && createSchema) {
|
|
93
102
|
return (
|
|
94
|
-
<Combobox.Content {...props}
|
|
103
|
+
<Combobox.Content {...props} onKeyDownCapture={handleKeyDown} ref={forwardedRef}>
|
|
95
104
|
<Popover.Viewport>
|
|
96
105
|
<Form.Root
|
|
97
106
|
testId='create-referenced-object-form'
|
|
@@ -115,8 +124,13 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
115
124
|
}
|
|
116
125
|
|
|
117
126
|
return (
|
|
118
|
-
<Combobox.Content {...props}
|
|
119
|
-
<Combobox.Input
|
|
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
|
+
/>
|
|
120
134
|
<Combobox.List>
|
|
121
135
|
{results.map((option) => (
|
|
122
136
|
<Combobox.Item
|
|
@@ -130,10 +144,11 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
130
144
|
))}
|
|
131
145
|
{createSchema && onCreate && (
|
|
132
146
|
<CreateItem
|
|
147
|
+
query={query}
|
|
133
148
|
createOptionLabel={createOptionLabel}
|
|
134
149
|
createOptionIcon={createOptionIcon ?? 'ph--plus--regular'}
|
|
135
|
-
onCreateItemSelect={(
|
|
136
|
-
setFormInitialValue(
|
|
150
|
+
onCreateItemSelect={(currentQuery: string) => {
|
|
151
|
+
setFormInitialValue(currentQuery);
|
|
137
152
|
setShowForm(true);
|
|
138
153
|
}}
|
|
139
154
|
/>
|
|
@@ -146,16 +161,17 @@ const ObjectPickerContent = forwardRef<HTMLDivElement, ObjectPickerContentProps>
|
|
|
146
161
|
);
|
|
147
162
|
|
|
148
163
|
const CreateItem = ({
|
|
164
|
+
query,
|
|
149
165
|
createOptionLabel,
|
|
150
166
|
createOptionIcon,
|
|
151
167
|
onCreateItemSelect,
|
|
152
168
|
}: {
|
|
169
|
+
query: string;
|
|
153
170
|
createOptionLabel?: [string, { ns: string }];
|
|
154
171
|
createOptionIcon: string;
|
|
155
172
|
onCreateItemSelect: (query: string) => void;
|
|
156
173
|
}) => {
|
|
157
174
|
const { t } = useTranslation(translationKey);
|
|
158
|
-
const { query } = useSearchListInput();
|
|
159
175
|
|
|
160
176
|
const label = createOptionLabel
|
|
161
177
|
? t(createOptionLabel[0], { ns: createOptionLabel[1].ns, text: query })
|