@dxos/react-ui-form 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b
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/README.md +0 -1
- package/dist/lib/browser/index.mjs +1134 -682
- 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 +1134 -682
- 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 +3 -3
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +63 -61
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +44 -16
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +75 -73
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField.d.ts +11 -5
- package/dist/types/src/components/Form/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.d.ts +8 -7
- package/dist/types/src/components/Form/FormFieldComponent.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts +2 -2
- package/dist/types/src/components/Form/FormFieldComponent.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet.d.ts +6 -10
- 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 +15 -7
- 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 -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.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +9 -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 +26 -4
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +63 -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 +127 -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/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 +64 -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 +6 -6
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +67 -64
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -0
- package/dist/types/src/components/index.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 +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 +63 -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 -1
- 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 +1 -0
- package/dist/types/src/util/refs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +42 -42
- package/src/components/FieldEditor/FieldEditor.stories.tsx +6 -8
- package/src/components/FieldEditor/FieldEditor.test.tsx +0 -1
- package/src/components/FieldEditor/FieldEditor.tsx +6 -6
- package/src/components/Form/AUDIT.md +17 -0
- package/src/components/Form/Form.stories.tsx +29 -44
- package/src/components/Form/Form.tsx +101 -54
- package/src/components/Form/FormField.tsx +39 -19
- package/src/components/Form/FormFieldComponent.stories.tsx +4 -4
- package/src/components/Form/FormFieldComponent.tsx +11 -11
- package/src/components/Form/FormFieldSet.tsx +121 -104
- package/src/components/Form/fields/ArrayField.tsx +10 -9
- package/src/components/Form/fields/GeoPointField.tsx +8 -7
- 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 +85 -45
- package/src/components/Form/fields/SelectField.tsx +1 -1
- package/src/components/Form/fields/SelectOptionField.stories.tsx +10 -8
- package/src/components/Form/fields/SelectOptionField.tsx +13 -13
- package/src/components/Form/fields/TextField.tsx +2 -2
- package/src/components/Form/fields/TupleField.tsx +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +114 -0
- package/src/components/ObjectForm/index.ts +5 -0
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +16 -18
- package/src/components/ObjectPicker/ObjectPicker.tsx +92 -65
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +362 -0
- package/src/components/ObjectProperties/ObjectProperties.test.tsx +101 -0
- package/src/components/ObjectProperties/ObjectProperties.tsx +141 -0
- package/src/components/ObjectProperties/index.ts +5 -0
- package/src/components/Settings/AUDIT.md +52 -0
- package/src/components/Settings/Settings.stories.tsx +69 -21
- package/src/components/Settings/Settings.tsx +33 -187
- 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 +21 -19
- package/src/components/ViewEditor/ViewEditor.test.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.tsx +87 -50
- package/src/components/index.ts +2 -0
- package/src/components/testing.tsx +23 -12
- package/src/hooks/useFormHandler.ts +42 -24
- package/src/hooks/useKeyHandler.tsx +7 -7
- package/src/index.ts +0 -4
- package/src/translations.ts +66 -63
- package/src/util/field-type.ts +60 -0
- package/src/util/index.ts +1 -1
- package/src/util/refs.ts +7 -3
- package/dist/types/src/components/Form/Form-old.stories.d.ts +0 -131
- package/dist/types/src/components/Form/Form-old.stories.d.ts.map +0 -1
- package/dist/types/src/util/echo.d.ts +0 -6
- package/dist/types/src/util/echo.d.ts.map +0 -1
- package/src/components/Form/Form-old.stories.tsx +0 -314
- package/src/util/echo.ts +0 -25
|
@@ -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-bs-auto max-bs-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 '
|
|
6
|
-
|
|
7
|
-
import React, { useCallback, useMemo } from 'react';
|
|
5
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
8
6
|
|
|
9
|
-
import
|
|
7
|
+
import '@dxos/lit-ui/dx-tag-picker.pcss';
|
|
8
|
+
import { type Database, Entity, Filter, Obj, Ref, Type } from '@dxos/echo';
|
|
9
|
+
import { useQuery, useSchema as defaultUseSchema } from '@dxos/echo-react';
|
|
10
10
|
import { ReferenceAnnotationId, type ReferenceAnnotationValue } from '@dxos/echo/internal';
|
|
11
|
-
import { useQuery, useSchema as useSchema$ } from '@dxos/echo-react';
|
|
12
11
|
import { findAnnotation } from '@dxos/effect';
|
|
13
12
|
import { DXN } from '@dxos/keys';
|
|
14
13
|
import { DxAnchor } from '@dxos/lit-ui/react';
|
|
15
14
|
import { Button, Icon, Input, useTranslation } from '@dxos/react-ui';
|
|
16
|
-
import {
|
|
15
|
+
import { ParentLabelAnnotationId } from '@dxos/schema';
|
|
16
|
+
|
|
17
|
+
import { translationKey } from '#translations';
|
|
17
18
|
|
|
18
|
-
import { translationKey } from '../../../translations';
|
|
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.
|
|
@@ -25,31 +25,42 @@ const isRefSnapshot = (val: any): val is { '/': string } => {
|
|
|
25
25
|
return typeof val === 'object' && typeof (val as any)?.['/'] === 'string';
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results) =>
|
|
28
|
+
const defaultGetOptions: NonNullable<RefFieldProps['getOptions']> = (results, { parentLabel } = {}) =>
|
|
29
29
|
results.map((result) => {
|
|
30
30
|
const id = Entity.getDXN(result).toString();
|
|
31
|
-
const
|
|
31
|
+
const parent = parentLabel ? Obj.getParent(result as Obj.Unknown) : undefined;
|
|
32
|
+
const label = parent ? Entity.getLabel(parent) : Entity.getLabel(result);
|
|
32
33
|
return { id, label: label ?? id };
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
const
|
|
36
|
+
const defaultUseResults: NonNullable<RefFieldProps['useResults']> = (db, typename) =>
|
|
36
37
|
useQuery(
|
|
37
38
|
db,
|
|
38
39
|
typename
|
|
39
|
-
? // For
|
|
40
|
-
typename === Type.getTypename(
|
|
40
|
+
? // For Ref.Ref(Obj.Unknown) we want to show all objects.
|
|
41
|
+
typename === Type.getTypename(Obj.Unknown)
|
|
41
42
|
? Filter.everything()
|
|
42
43
|
: Filter.typename(typename)
|
|
43
44
|
: Filter.nothing(),
|
|
44
45
|
);
|
|
45
46
|
|
|
46
47
|
export type RefFieldProps = FormFieldComponentProps &
|
|
47
|
-
Pick<
|
|
48
|
+
Pick<
|
|
49
|
+
ObjectPickerContentProps,
|
|
50
|
+
'createOptionLabel' | 'createOptionIcon' | 'createInitialValuePath' | 'createFieldMap'
|
|
51
|
+
> & {
|
|
48
52
|
db?: Database.Database;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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[];
|
|
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;
|
|
53
64
|
};
|
|
54
65
|
|
|
55
66
|
export const RefField = (props: RefFieldProps) => {
|
|
@@ -64,9 +75,10 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
64
75
|
createOptionLabel,
|
|
65
76
|
createOptionIcon,
|
|
66
77
|
createInitialValuePath,
|
|
78
|
+
createFieldMap,
|
|
67
79
|
db,
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
useSchema = defaultUseSchema,
|
|
81
|
+
useResults = defaultUseResults,
|
|
70
82
|
getOptions = defaultGetOptions,
|
|
71
83
|
onCreate,
|
|
72
84
|
onValueChange,
|
|
@@ -80,46 +92,70 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
80
92
|
);
|
|
81
93
|
|
|
82
94
|
const results = useResults(db, typename);
|
|
83
|
-
const options = useMemo(() =>
|
|
95
|
+
const options = useMemo(() => {
|
|
96
|
+
const parentLabel = type ? findAnnotation<boolean>(type, ParentLabelAnnotationId) === true : false;
|
|
97
|
+
return getOptions(results, { parentLabel });
|
|
98
|
+
}, [results, getOptions, type]);
|
|
84
99
|
|
|
85
100
|
const handleGetValue = useCallback(() => {
|
|
86
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;
|
|
87
110
|
|
|
88
111
|
const unknownToRefOption = (value: unknown) => {
|
|
89
112
|
const isRef = Ref.isRef(value);
|
|
90
113
|
if (isRef || isRefSnapshot(value)) {
|
|
91
114
|
const dxnString = isRef ? value.dxn.toString() : value['/'];
|
|
92
|
-
const
|
|
115
|
+
const objectId = dxnToObjectId(dxnString);
|
|
116
|
+
const matchingOption = options.find((option) => dxnToObjectId(option.id) === objectId);
|
|
93
117
|
if (matchingOption) {
|
|
94
118
|
return matchingOption;
|
|
95
119
|
}
|
|
96
120
|
}
|
|
121
|
+
|
|
97
122
|
return undefined;
|
|
98
123
|
};
|
|
99
124
|
|
|
100
125
|
return unknownToRefOption(formValue);
|
|
101
126
|
}, [options, getValue]);
|
|
102
127
|
|
|
103
|
-
const handleUpdate = useCallback(
|
|
104
|
-
(id: string | undefined) => {
|
|
105
|
-
const item = options.find((option) => option.id === id);
|
|
106
|
-
const ref = item ? Ref.fromDXN(DXN.parse(item.id)) : undefined;
|
|
107
|
-
onValueChange(type, ref);
|
|
108
|
-
},
|
|
109
|
-
[options, type, onValueChange],
|
|
110
|
-
);
|
|
111
|
-
|
|
112
128
|
const item = handleGetValue();
|
|
113
129
|
const selectedIds = useMemo(() => (item ? [item.id] : []), [item]);
|
|
114
130
|
const createSchema = useSchema(db, typename);
|
|
115
131
|
|
|
132
|
+
// Lift the popover open state so we can dismiss it after a successful create.
|
|
133
|
+
const [open, setOpen] = useState(false);
|
|
134
|
+
|
|
116
135
|
const handleCreate = useCallback(
|
|
117
|
-
(values: any) => {
|
|
118
|
-
if (createSchema
|
|
119
|
-
|
|
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));
|
|
120
146
|
}
|
|
147
|
+
setOpen(false);
|
|
148
|
+
},
|
|
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);
|
|
121
157
|
},
|
|
122
|
-
[
|
|
158
|
+
[options, type, onValueChange],
|
|
123
159
|
);
|
|
124
160
|
|
|
125
161
|
const handleSelect = useCallback(
|
|
@@ -143,25 +179,25 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
143
179
|
<div>
|
|
144
180
|
{readonly ? (
|
|
145
181
|
!item ? (
|
|
146
|
-
<p className=
|
|
182
|
+
<p className='text-description mb-2'>{t('empty-readonly-ref-field.label')}</p>
|
|
147
183
|
) : (
|
|
148
|
-
<DxAnchor key={item.id} dxn={item.id} rootclassname='
|
|
184
|
+
<DxAnchor key={item.id} dxn={item.id} rootclassname='me-1'>
|
|
149
185
|
{item.label}
|
|
150
186
|
</DxAnchor>
|
|
151
187
|
)
|
|
152
188
|
) : (
|
|
153
|
-
<ObjectPicker.Root>
|
|
189
|
+
<ObjectPicker.Root open={open} onOpenChange={setOpen}>
|
|
154
190
|
<ObjectPicker.Trigger asChild classNames='p-0'>
|
|
155
191
|
{item ? (
|
|
156
|
-
<div className='flex gap-
|
|
192
|
+
<div className='flex gap-form-gap w-full'>
|
|
157
193
|
<Input.Root key={item.id}>
|
|
158
|
-
<Input.TextInput value={item.label} readOnly classNames='
|
|
194
|
+
<Input.TextInput value={item.label} readOnly classNames='w-full' />
|
|
159
195
|
</Input.Root>
|
|
160
196
|
</div>
|
|
161
197
|
) : (
|
|
162
|
-
<Button classNames='
|
|
163
|
-
<div
|
|
164
|
-
<span className='
|
|
198
|
+
<Button classNames='w-full text-start gap-form-gap'>
|
|
199
|
+
<div className='grow overflow-hidden'>
|
|
200
|
+
<span className='truncate text-description'>{placeholder ?? t('ref-field.placeholder')}</span>
|
|
165
201
|
</div>
|
|
166
202
|
<Icon size={3} icon='ph--caret-down--bold' />
|
|
167
203
|
</Button>
|
|
@@ -169,13 +205,17 @@ export const RefField = (props: RefFieldProps) => {
|
|
|
169
205
|
</ObjectPicker.Trigger>
|
|
170
206
|
<ObjectPicker.Portal>
|
|
171
207
|
<ObjectPicker.Content
|
|
172
|
-
classNames='
|
|
208
|
+
classNames='dx-card-popover-width'
|
|
173
209
|
options={options}
|
|
174
210
|
selectedIds={selectedIds}
|
|
175
|
-
|
|
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))}
|
|
176
215
|
createOptionLabel={createOptionLabel}
|
|
177
216
|
createOptionIcon={createOptionIcon}
|
|
178
217
|
createInitialValuePath={createInitialValuePath}
|
|
218
|
+
createFieldMap={createFieldMap}
|
|
179
219
|
onCreate={handleCreate}
|
|
180
220
|
onSelect={handleSelect}
|
|
181
221
|
/>
|
|
@@ -43,7 +43,7 @@ export const SelectField = ({
|
|
|
43
43
|
) : (
|
|
44
44
|
<Select.Root value={value} onValueChange={handleValueChange}>
|
|
45
45
|
{/* TODO(burdon): Placeholder not working? */}
|
|
46
|
-
<Select.TriggerButton classNames='
|
|
46
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} placeholder={placeholder} />
|
|
47
47
|
<Select.Portal>
|
|
48
48
|
<Select.Content>
|
|
49
49
|
<Select.Viewport>
|
|
@@ -7,12 +7,12 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { SelectOption } from '@dxos/echo/internal';
|
|
10
|
-
import { withTheme } from '@dxos/react-ui/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { translations } from '#translations';
|
|
11
13
|
|
|
12
|
-
import { translations } from '../../../translations';
|
|
13
14
|
import { TestLayout } from '../../testing';
|
|
14
15
|
import { Form } from '../Form';
|
|
15
|
-
|
|
16
16
|
import { SelectOptionField } from './SelectOptionField';
|
|
17
17
|
|
|
18
18
|
const schema = Schema.Struct({
|
|
@@ -37,10 +37,12 @@ const DefaultStory = () => {
|
|
|
37
37
|
fieldMap={{ options: SelectOptionField }}
|
|
38
38
|
onSave={(values) => setValues(values)}
|
|
39
39
|
>
|
|
40
|
-
<Form.
|
|
41
|
-
<Form.
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
<Form.Viewport>
|
|
41
|
+
<Form.Content>
|
|
42
|
+
<Form.FieldSet />
|
|
43
|
+
<Form.Actions />
|
|
44
|
+
</Form.Content>
|
|
45
|
+
</Form.Viewport>
|
|
44
46
|
</Form.Root>
|
|
45
47
|
</TestLayout>
|
|
46
48
|
);
|
|
@@ -50,7 +52,7 @@ const meta = {
|
|
|
50
52
|
title: 'ui/react-ui-form/SelectOptionField',
|
|
51
53
|
component: SelectOptionField as any,
|
|
52
54
|
render: DefaultStory,
|
|
53
|
-
decorators: [withTheme()],
|
|
55
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
54
56
|
parameters: {
|
|
55
57
|
layout: 'fullscreen',
|
|
56
58
|
translations,
|
|
@@ -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 = ({
|
|
@@ -119,7 +120,7 @@ export const SelectOptionField = ({
|
|
|
119
120
|
return (
|
|
120
121
|
<Input.Root validationValence={status}>
|
|
121
122
|
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
122
|
-
<div
|
|
123
|
+
<div>
|
|
123
124
|
{options && (
|
|
124
125
|
<List.Root
|
|
125
126
|
items={options}
|
|
@@ -128,13 +129,13 @@ export const SelectOptionField = ({
|
|
|
128
129
|
readonly={!!readonly}
|
|
129
130
|
>
|
|
130
131
|
{({ items }) => (
|
|
131
|
-
<div role='list' className='
|
|
132
|
+
<div role='list' className='w-full overflow-auto'>
|
|
132
133
|
{items?.map((item) => (
|
|
133
134
|
<div key={item.id}>
|
|
134
135
|
<List.Item
|
|
135
136
|
role='button'
|
|
136
137
|
item={item}
|
|
137
|
-
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-
|
|
138
|
+
classNames={[subtleHover, 'flex flex-col cursor-pointer rounded-xs']}
|
|
138
139
|
aria-expanded={selected === item.id}
|
|
139
140
|
>
|
|
140
141
|
<div className='flex items-center'>
|
|
@@ -146,19 +147,18 @@ 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
|
/>
|
|
153
154
|
</div>
|
|
154
155
|
{selected === item.id && (
|
|
155
|
-
<div className='
|
|
156
|
-
{
|
|
157
|
-
<
|
|
158
|
-
<div className='grid grid-cols-[1fr_min-content_min-content] gap-1'>
|
|
156
|
+
<div className='flex flex-col p-form-padding gap-form-gap dx-density-fine'>
|
|
157
|
+
<Input.Label classNames='text-xs'>{t('select-option.label')}</Input.Label>
|
|
158
|
+
<div className='grid grid-cols-[1fr_min-content_min-content] gap-form-gap'>
|
|
159
159
|
<Input.TextInput
|
|
160
160
|
disabled={!!readonly}
|
|
161
|
-
placeholder={t('select
|
|
161
|
+
placeholder={t('select-option-label.placeholder')}
|
|
162
162
|
ref={selected === item.id ? inputRef : undefined}
|
|
163
163
|
value={item.title}
|
|
164
164
|
onChange={handleTitleChange(item.id)}
|
|
@@ -170,7 +170,7 @@ export const SelectOptionField = ({
|
|
|
170
170
|
disabled={!!readonly}
|
|
171
171
|
icon='ph--trash--fill'
|
|
172
172
|
iconOnly
|
|
173
|
-
label={t('select
|
|
173
|
+
label={t('select-option-delete.button')}
|
|
174
174
|
onClick={() => handleDelete(item.id)}
|
|
175
175
|
/>
|
|
176
176
|
</div>
|
|
@@ -182,10 +182,10 @@ export const SelectOptionField = ({
|
|
|
182
182
|
<IconButton
|
|
183
183
|
variant='ghost'
|
|
184
184
|
icon='ph--plus--regular'
|
|
185
|
-
label={t('select
|
|
185
|
+
label={t('select-option-add.button')}
|
|
186
186
|
onClick={handleAdd}
|
|
187
187
|
disabled={!!readonly}
|
|
188
|
-
classNames='
|
|
188
|
+
classNames='w-full'
|
|
189
189
|
/>
|
|
190
190
|
</div>
|
|
191
191
|
)}
|
|
@@ -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>
|
|
@@ -32,7 +32,7 @@ export const TupleField = ({
|
|
|
32
32
|
return (
|
|
33
33
|
<Input.Root validationValence={status}>
|
|
34
34
|
<FormFieldLabel error={error} readonly={readonly} label={label} />
|
|
35
|
-
<div className={mx('grid gap-
|
|
35
|
+
<div className={mx('grid gap-form-gap', gridCols[binding.length - 1])}>
|
|
36
36
|
{binding.map((prop) => (
|
|
37
37
|
<Input.TextInput
|
|
38
38
|
key={prop}
|