@dxos/react-ui-form 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/index.mjs +4064 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +77 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +0 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Card.stories.d.ts +88 -0
- package/dist/types/src/components/Form/Card.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.d.ts +7 -7
- package/dist/types/src/components/Form/Form.stories.d.ts +21 -0
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +6 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormControls.d.ts +7 -7
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +17 -3
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts +0 -6
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts +7 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/default-value.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/ArrayField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/DateField/DateField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts +0 -11
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/numeric-constraints.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts +1 -11
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts +13 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/find-ref-option.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +1 -13
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts +15 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/create-select-field.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextAreaField/TextAreaField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TextField/TextField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormField/fields/TupleField/TupleField.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +0 -27
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts +29 -0
- package/dist/types/src/components/Form/FormLayout/resolve-layout-field.d.ts.map +1 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts +10 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.d.ts.map +1 -1
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts +2 -0
- package/dist/types/src/components/ObjectForm/ObjectForm.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts +8 -23
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.stories.d.ts +1 -0
- 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 +1 -0
- package/dist/types/src/components/ObjectProperties/ObjectProperties.stories.d.ts.map +1 -1
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts +1 -0
- package/dist/types/src/components/ObjectTree/ObjectTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -0
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts +9 -8
- package/dist/types/src/components/RefEditor/ref-editor-extension.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +1 -0
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +6 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.test.d.ts +2 -0
- package/dist/types/src/hooks/useFormHandler.test.d.ts.map +1 -0
- package/dist/types/src/testing/TestLayout.d.ts +1 -1
- package/dist/types/src/testing/TestLayout.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +1 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +39 -42
- package/src/components/Form/Card.stories.tsx +93 -0
- package/src/components/Form/Form.stories.tsx +95 -3
- package/src/components/Form/Form.theme.ts +14 -8
- package/src/components/Form/Form.tsx +2 -2
- package/src/components/Form/FormControls.tsx +32 -13
- package/src/components/Form/FormField/FormField.tsx +11 -3
- package/src/components/Form/FormField/FormRow.tsx +20 -1
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +3 -46
- package/src/components/Form/FormField/fields/ArrayField/default-value.ts +49 -0
- package/src/components/Form/FormField/fields/ArrayField/index.ts +2 -0
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +12 -2
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +10 -3
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +1 -1
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +2 -12
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +1 -1
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +1 -36
- package/src/components/Form/FormField/fields/NumberField/index.ts +2 -0
- package/src/components/Form/FormField/fields/NumberField/numeric-constraints.ts +43 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +1 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +2 -2
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +1 -1
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +3 -45
- package/src/components/Form/FormField/fields/RefField/find-ref-option.ts +56 -0
- package/src/components/Form/FormField/fields/RefField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +2 -67
- package/src/components/Form/FormField/fields/SelectField/create-select-field.tsx +80 -0
- package/src/components/Form/FormField/fields/SelectField/index.ts +2 -0
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +2 -1
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +11 -9
- package/src/components/Form/FormLayout/FormLayout.test.ts +1 -1
- package/src/components/Form/FormLayout/FormLayout.tsx +1 -86
- package/src/components/Form/FormLayout/index.ts +2 -0
- package/src/components/Form/FormLayout/resolve-layout-field.ts +98 -0
- package/src/components/ObjectForm/ObjectForm.stories.tsx +93 -3
- package/src/components/ObjectForm/ObjectForm.tsx +22 -4
- package/src/components/ObjectPicker/ObjectPicker.stories.tsx +1 -1
- package/src/components/ObjectPicker/ObjectPicker.tsx +3 -3
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +7 -1
- package/src/components/ObjectTree/ObjectTree.stories.tsx +1 -1
- package/src/components/RefEditor/RefEditor.stories.tsx +9 -1
- package/src/components/RefEditor/RefEditor.tsx +42 -6
- package/src/components/RefEditor/ref-editor-extension.ts +168 -91
- package/src/components/ViewEditor/ViewEditor.stories.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.tsx +9 -9
- package/src/hooks/useFormHandler.test.ts +85 -0
- package/src/hooks/useFormHandler.ts +118 -31
- package/src/translations.ts +1 -0
- package/src/types.ts +5 -0
- package/src/util/index.ts +0 -1
- package/dist/lib/browser/index.mjs +0 -4675
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -87
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4677
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -89
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/types/src/util/formatting.d.ts +0 -15
- package/dist/types/src/util/formatting.d.ts.map +0 -1
- package/src/util/formatting.ts +0 -171
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { invariant } from '@dxos/invariant';
|
|
8
|
+
import { Select } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { type FormFieldRenderer, type FormFieldRendererProps } from '#types';
|
|
11
|
+
|
|
12
|
+
import { FormRow } from '../../FormRow';
|
|
13
|
+
import { type SelectFieldOption } from './SelectField';
|
|
14
|
+
|
|
15
|
+
// Kept out of `SelectField.tsx`: react-refresh only fast-refreshes a module whose
|
|
16
|
+
// exports are all components, so values exported beside them force a full page reload on every edit.
|
|
17
|
+
|
|
18
|
+
export type CreateSelectFieldOptions = {
|
|
19
|
+
/** Options to display. Strings are used as both value and label. */
|
|
20
|
+
options: ReadonlyArray<string | SelectFieldOption>;
|
|
21
|
+
/** Label for the sentinel option that maps to `undefined`. Defaults to `'Default'`. Pass `null` to omit. */
|
|
22
|
+
defaultLabel?: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Factory for a `fieldMap` entry (keyed by JSON path) that renders a Select over a fixed list of
|
|
27
|
+
* options, with an optional sentinel mapping to `undefined`. Use for fields whose choices come from a
|
|
28
|
+
* runtime list rather than the schema (e.g. available model ids).
|
|
29
|
+
*/
|
|
30
|
+
export const createSelectField = ({
|
|
31
|
+
options,
|
|
32
|
+
defaultLabel = 'Default',
|
|
33
|
+
}: CreateSelectFieldOptions): FormFieldRenderer => {
|
|
34
|
+
const normalized = options.map((option) => (typeof option === 'string' ? { value: option, label: option } : option));
|
|
35
|
+
const hasDefault = defaultLabel !== null;
|
|
36
|
+
// The sentinel maps to `undefined`. Radix forbids an empty-string `Select.Item` value, so use a
|
|
37
|
+
// non-empty placeholder; reserved (a real option with this value would be unrepresentable).
|
|
38
|
+
const sentinel = '__default__';
|
|
39
|
+
invariant(
|
|
40
|
+
!normalized.some((option) => option.value === sentinel),
|
|
41
|
+
`createSelectField: option value '${sentinel}' is reserved.`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return ({ type, readonly, onValueChange, ...props }: FormFieldRendererProps<string | undefined>) => (
|
|
45
|
+
<FormRow<string>
|
|
46
|
+
readonly={readonly}
|
|
47
|
+
renderStatic={(value) => (
|
|
48
|
+
<p className='truncate min-w-0'>
|
|
49
|
+
{normalized.find((option) => option.value === value)?.label ?? String(value ?? '')}
|
|
50
|
+
</p>
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
>
|
|
54
|
+
{({ value }) => (
|
|
55
|
+
<Select.Root
|
|
56
|
+
disabled={!!readonly}
|
|
57
|
+
value={value ?? sentinel}
|
|
58
|
+
onValueChange={(next) => onValueChange(type, hasDefault && next === sentinel ? undefined : next)}
|
|
59
|
+
>
|
|
60
|
+
<Select.TriggerButton classNames='w-full' disabled={!!readonly} />
|
|
61
|
+
{normalized.length > 0 && (
|
|
62
|
+
<Select.Portal>
|
|
63
|
+
<Select.Content>
|
|
64
|
+
<Select.Viewport>
|
|
65
|
+
{hasDefault && <Select.Option value={sentinel}>{defaultLabel}</Select.Option>}
|
|
66
|
+
{normalized.map((option) => (
|
|
67
|
+
<Select.Option key={option.value} value={option.value}>
|
|
68
|
+
{option.label ?? option.value}
|
|
69
|
+
</Select.Option>
|
|
70
|
+
))}
|
|
71
|
+
</Select.Viewport>
|
|
72
|
+
<Select.Arrow />
|
|
73
|
+
</Select.Content>
|
|
74
|
+
</Select.Portal>
|
|
75
|
+
)}
|
|
76
|
+
</Select.Root>
|
|
77
|
+
)}
|
|
78
|
+
</FormRow>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -9,7 +9,8 @@ import { PublicKey } from '@dxos/keys';
|
|
|
9
9
|
import { type ChromaticPalette, IconButton, Input, Tag, ToggleIconButton, useTranslation } from '@dxos/react-ui';
|
|
10
10
|
import { OrderedList } from '@dxos/react-ui-list';
|
|
11
11
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
12
|
-
import {
|
|
12
|
+
import { osTranslations } from '@dxos/ui-theme';
|
|
13
|
+
import { hues } from '@dxos/ui-types';
|
|
13
14
|
|
|
14
15
|
import { translationKey } from '#translations';
|
|
15
16
|
import { type FormFieldRendererProps } from '#types';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { type PropsWithChildren, useState } from 'react';
|
|
5
|
+
import React, { Children, type PropsWithChildren, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type ThemedClassName, ToggleIconButton, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { mx } from '@dxos/ui-theme';
|
|
@@ -10,8 +10,7 @@ import { mx } from '@dxos/ui-theme';
|
|
|
10
10
|
import { translationKey } from '#translations';
|
|
11
11
|
|
|
12
12
|
import { formTheme } from '../Form.theme';
|
|
13
|
-
import { FormFieldHeader } from '../FormField
|
|
14
|
-
import { type FormFieldPresentation } from '../FormField/presentation';
|
|
13
|
+
import { FormFieldHeader, type FormFieldPresentation } from '../FormField';
|
|
15
14
|
|
|
16
15
|
const styles = formTheme.styles();
|
|
17
16
|
|
|
@@ -47,7 +46,10 @@ export const FormFieldSetContainer = ({
|
|
|
47
46
|
const { t } = useTranslation(translationKey);
|
|
48
47
|
// TODO(burdon): Generalize collapse state (cf. useSelection in react-ui-attention, plugin-markdown cursor state).
|
|
49
48
|
const [collapsed, setCollapsed] = useState(false);
|
|
50
|
-
|
|
49
|
+
// An empty object/array renders no sub-fields, so there is nothing to collapse — offering the
|
|
50
|
+
// toggle (and the click target on the header) would be a control that does nothing.
|
|
51
|
+
const canCollapse = collapsible && Children.toArray(children).length > 0;
|
|
52
|
+
const showBody = !(canCollapse && collapsed);
|
|
51
53
|
|
|
52
54
|
const content = (
|
|
53
55
|
<>
|
|
@@ -58,29 +60,29 @@ export const FormFieldSetContainer = ({
|
|
|
58
60
|
readonly={readonly}
|
|
59
61
|
classNames='pl-2'
|
|
60
62
|
actions={
|
|
61
|
-
|
|
63
|
+
canCollapse ? (
|
|
62
64
|
<ToggleIconButton
|
|
63
65
|
active={!collapsed}
|
|
64
66
|
classNames='px-1 mr-0.5'
|
|
65
67
|
variant='ghost'
|
|
66
|
-
density='
|
|
68
|
+
density='sm'
|
|
67
69
|
iconOnly
|
|
68
70
|
icon='ph--caret-right--regular'
|
|
69
71
|
label={t(collapsed ? 'expand-fields.label' : 'collapse-fields.label')}
|
|
70
72
|
/>
|
|
71
73
|
) : undefined
|
|
72
74
|
}
|
|
73
|
-
onClick={
|
|
75
|
+
onClick={canCollapse ? () => setCollapsed((value) => !value) : undefined}
|
|
74
76
|
/>
|
|
75
77
|
)}
|
|
76
|
-
{showBody && (
|
|
78
|
+
{showBody && (canCollapse ? <div className={styles.fieldSetBody()}>{children}</div> : children)}
|
|
77
79
|
</>
|
|
78
80
|
);
|
|
79
81
|
|
|
80
82
|
// Nested groups render inside an indented, bordered container with a collapse toggle. A non-collapsible
|
|
81
83
|
// group only materializes a wrapper when `classNames` is supplied — otherwise the body flows straight
|
|
82
84
|
// into the parent grid (the default, grid-transparent behavior).
|
|
83
|
-
if (
|
|
85
|
+
if (canCollapse) {
|
|
84
86
|
return (
|
|
85
87
|
<div className={styles.fieldSetBoxOuter()}>
|
|
86
88
|
<div className={styles.fieldSetBox({ class: mx(classNames) })}>{content}</div>
|
|
@@ -8,7 +8,7 @@ import { describe, test } from 'vitest';
|
|
|
8
8
|
|
|
9
9
|
import { Annotation } from '@dxos/echo';
|
|
10
10
|
|
|
11
|
-
import { resolveLayoutField } from './
|
|
11
|
+
import { resolveLayoutField } from './resolve-layout-field';
|
|
12
12
|
|
|
13
13
|
const Place = Schema.Struct({
|
|
14
14
|
name: Schema.optional(Schema.String),
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import * as Option from 'effect/Option';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
|
-
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
7
|
import * as String from 'effect/String';
|
|
9
8
|
import React, { Fragment, useMemo } from 'react';
|
|
10
9
|
|
|
@@ -19,6 +18,7 @@ import { useFormContext, useFormFieldState } from '../../../hooks';
|
|
|
19
18
|
import { formTheme } from '../Form.theme';
|
|
20
19
|
import { FormField, FormFieldErrorBoundary, FormFieldLabel, type FormFieldProps, presentationFor } from '../FormField';
|
|
21
20
|
import { type LayoutNode, LayoutParseError, parseLayout } from './parser';
|
|
21
|
+
import { resolveLayoutField } from './resolve-layout-field';
|
|
22
22
|
|
|
23
23
|
const FORM_LAYOUT_NAME = 'Form.Layout';
|
|
24
24
|
|
|
@@ -137,91 +137,6 @@ const RenderNode = ({ node, schema, basePath, ...props }: RenderNodeProps) => {
|
|
|
137
137
|
);
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
export type ResolvedLayoutField = {
|
|
141
|
-
/** Resolved leaf AST node for the field. */
|
|
142
|
-
type: SchemaAST.AST;
|
|
143
|
-
/** Path segments (dotted names split on `.`). */
|
|
144
|
-
segments: string[];
|
|
145
|
-
/** Last path segment; used to derive the field label when there is no `title` annotation. */
|
|
146
|
-
leafName: string;
|
|
147
|
-
/** Resolved `title` annotation for the field, if any. */
|
|
148
|
-
title?: string;
|
|
149
|
-
/**
|
|
150
|
-
* Set when the field resolves to a nested struct carrying a `LabelAnnotation` —
|
|
151
|
-
* the type literal whose computed label should render in place of the struct's
|
|
152
|
-
* sub-fields. Undefined for ordinary (non-auto-labelled) fields.
|
|
153
|
-
*/
|
|
154
|
-
labelType?: SchemaAST.AST;
|
|
155
|
-
/** Whether the resolved property is required (non-optional). */
|
|
156
|
-
required: boolean;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Walks a dotted path of property names through nested struct ASTs, returning the
|
|
161
|
-
* leaf `PropertySignature` (or `undefined` if any segment is missing).
|
|
162
|
-
*/
|
|
163
|
-
const resolvePropertySignature = (ast: SchemaAST.AST, segments: string[]): SchemaAST.PropertySignature | undefined => {
|
|
164
|
-
let node: SchemaAST.AST = ast;
|
|
165
|
-
for (let index = 0; index < segments.length; index++) {
|
|
166
|
-
const typeLiteral = SchemaEx.findNode(node, SchemaAST.isTypeLiteral);
|
|
167
|
-
if (!typeLiteral) {
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
const prop = SchemaAST.getPropertySignatures(typeLiteral).find(
|
|
171
|
-
(p) => globalThis.String(p.name) === segments[index],
|
|
172
|
-
);
|
|
173
|
-
if (!prop) {
|
|
174
|
-
return undefined;
|
|
175
|
-
}
|
|
176
|
-
if (index === segments.length - 1) {
|
|
177
|
-
return prop;
|
|
178
|
-
}
|
|
179
|
-
node = prop.type;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return undefined;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Resolves a (possibly dotted) layout field name against a schema.
|
|
187
|
-
* - `origin.code` drills through nested structs to the leaf field.
|
|
188
|
-
* - A name resolving to a nested struct with a `LabelAnnotation` is flagged via
|
|
189
|
-
* `labelType` so the layout can auto-convert it to a single read-only label.
|
|
190
|
-
* Returns `undefined` when the name does not resolve to a property.
|
|
191
|
-
*/
|
|
192
|
-
export const resolveLayoutField = (schema: Schema.Schema<any>, name: string): ResolvedLayoutField | undefined => {
|
|
193
|
-
const segments = name.split('.');
|
|
194
|
-
const prop = resolvePropertySignature(schema.ast, segments);
|
|
195
|
-
if (!prop) {
|
|
196
|
-
return undefined;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Normalized leaf type (optional unwrapped, refinements stripped, signature-level
|
|
200
|
-
// annotations merged) — matches how `getProperties` feeds `FormField`.
|
|
201
|
-
const { type: baseType } = SchemaEx.getBaseType(prop);
|
|
202
|
-
const type =
|
|
203
|
-
prop.annotations && Reflect.ownKeys(prop.annotations).length > 0
|
|
204
|
-
? ({ ...baseType, annotations: { ...baseType.annotations, ...prop.annotations } } as SchemaAST.AST)
|
|
205
|
-
: baseType;
|
|
206
|
-
|
|
207
|
-
const title = SchemaEx.getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
|
|
208
|
-
|
|
209
|
-
// Label detection reads the *raw* property type: `SchemaEx.getBaseType`'s `encodedBoundAST`
|
|
210
|
-
// strips annotations from non-keyword inner types (e.g. nested structs), which would
|
|
211
|
-
// drop the `LabelAnnotation` we rely on here.
|
|
212
|
-
const labelType = SchemaEx.findNode(prop.type, SchemaAST.isTypeLiteral);
|
|
213
|
-
const labelled = labelType != null && Option.isSome(Annotation.LabelAnnotation.getFromAst(labelType));
|
|
214
|
-
|
|
215
|
-
return {
|
|
216
|
-
type,
|
|
217
|
-
segments,
|
|
218
|
-
leafName: segments[segments.length - 1],
|
|
219
|
-
title,
|
|
220
|
-
labelType: labelled ? labelType : undefined,
|
|
221
|
-
required: !prop.isOptional,
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
|
|
225
140
|
type LabelFieldProps = {
|
|
226
141
|
schema: Schema.Schema<any>;
|
|
227
142
|
label: string;
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export { type ResolvedLayoutField, resolveLayoutField } from './resolve-layout-field';
|
|
6
|
+
|
|
5
7
|
export { FormLayout, type FormLayoutProps } from './FormLayout';
|
|
6
8
|
export { type LayoutNode, LayoutParseError, parseLayout } from './parser';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Option from 'effect/Option';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
import * as SchemaAST from 'effect/SchemaAST';
|
|
8
|
+
|
|
9
|
+
import { Annotation } from '@dxos/echo';
|
|
10
|
+
import { SchemaEx } from '@dxos/effect';
|
|
11
|
+
|
|
12
|
+
// Kept out of `FormLayout.tsx`: react-refresh only fast-refreshes a module whose
|
|
13
|
+
// exports are all components, so values exported beside them force a full page reload on every edit.
|
|
14
|
+
|
|
15
|
+
export type ResolvedLayoutField = {
|
|
16
|
+
/** Resolved leaf AST node for the field. */
|
|
17
|
+
type: SchemaAST.AST;
|
|
18
|
+
/** Path segments (dotted names split on `.`). */
|
|
19
|
+
segments: string[];
|
|
20
|
+
/** Last path segment; used to derive the field label when there is no `title` annotation. */
|
|
21
|
+
leafName: string;
|
|
22
|
+
/** Resolved `title` annotation for the field, if any. */
|
|
23
|
+
title?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Set when the field resolves to a nested struct carrying a `LabelAnnotation` —
|
|
26
|
+
* the type literal whose computed label should render in place of the struct's
|
|
27
|
+
* sub-fields. Undefined for ordinary (non-auto-labelled) fields.
|
|
28
|
+
*/
|
|
29
|
+
labelType?: SchemaAST.AST;
|
|
30
|
+
/** Whether the resolved property is required (non-optional). */
|
|
31
|
+
required: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Walks a dotted path of property names through nested struct ASTs, returning the
|
|
36
|
+
* leaf `PropertySignature` (or `undefined` if any segment is missing).
|
|
37
|
+
*/
|
|
38
|
+
const resolvePropertySignature = (ast: SchemaAST.AST, segments: string[]): SchemaAST.PropertySignature | undefined => {
|
|
39
|
+
let node: SchemaAST.AST = ast;
|
|
40
|
+
for (let index = 0; index < segments.length; index++) {
|
|
41
|
+
const typeLiteral = SchemaEx.findNode(node, SchemaAST.isTypeLiteral);
|
|
42
|
+
if (!typeLiteral) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const prop = SchemaAST.getPropertySignatures(typeLiteral).find(
|
|
46
|
+
(p) => globalThis.String(p.name) === segments[index],
|
|
47
|
+
);
|
|
48
|
+
if (!prop) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
if (index === segments.length - 1) {
|
|
52
|
+
return prop;
|
|
53
|
+
}
|
|
54
|
+
node = prop.type;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves a (possibly dotted) layout field name against a schema.
|
|
62
|
+
* - `origin.code` drills through nested structs to the leaf field.
|
|
63
|
+
* - A name resolving to a nested struct with a `LabelAnnotation` is flagged via
|
|
64
|
+
* `labelType` so the layout can auto-convert it to a single read-only label.
|
|
65
|
+
* Returns `undefined` when the name does not resolve to a property.
|
|
66
|
+
*/
|
|
67
|
+
export const resolveLayoutField = (schema: Schema.Schema<any>, name: string): ResolvedLayoutField | undefined => {
|
|
68
|
+
const segments = name.split('.');
|
|
69
|
+
const prop = resolvePropertySignature(schema.ast, segments);
|
|
70
|
+
if (!prop) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Normalized leaf type (optional unwrapped, refinements stripped, signature-level
|
|
75
|
+
// annotations merged) — matches how `getProperties` feeds `FormField`.
|
|
76
|
+
const { type: baseType } = SchemaEx.getBaseType(prop);
|
|
77
|
+
const type =
|
|
78
|
+
prop.annotations && Reflect.ownKeys(prop.annotations).length > 0
|
|
79
|
+
? ({ ...baseType, annotations: { ...baseType.annotations, ...prop.annotations } } as SchemaAST.AST)
|
|
80
|
+
: baseType;
|
|
81
|
+
|
|
82
|
+
const title = SchemaEx.getAnnotation<string>(SchemaAST.TitleAnnotationId)(type);
|
|
83
|
+
|
|
84
|
+
// Label detection reads the *raw* property type: `SchemaEx.getBaseType`'s `encodedBoundAST`
|
|
85
|
+
// strips annotations from non-keyword inner types (e.g. nested structs), which would
|
|
86
|
+
// drop the `LabelAnnotation` we rely on here.
|
|
87
|
+
const labelType = SchemaEx.findNode(prop.type, SchemaAST.isTypeLiteral);
|
|
88
|
+
const labelled = labelType != null && Option.isSome(Annotation.LabelAnnotation.getFromAst(labelType));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
segments,
|
|
93
|
+
leafName: segments[segments.length - 1],
|
|
94
|
+
title,
|
|
95
|
+
labelType: labelled ? labelType : undefined,
|
|
96
|
+
required: !prop.isOptional,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -3,18 +3,28 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React from 'react';
|
|
6
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
import { expect, waitFor, within } from 'storybook/test';
|
|
7
8
|
|
|
8
9
|
import { Filter, Obj, Ref, Tag } from '@dxos/echo';
|
|
9
|
-
import { useQuery } from '@dxos/react
|
|
10
|
+
import { useObject, useQuery } from '@dxos/echo-react';
|
|
11
|
+
import { random } from '@dxos/random';
|
|
10
12
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
13
|
+
import { Toolbar } from '@dxos/react-ui';
|
|
11
14
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
15
|
import { Person } from '@dxos/types';
|
|
13
16
|
|
|
14
17
|
import { translations } from '#translations';
|
|
15
18
|
|
|
19
|
+
import { TestLayout } from '../../testing';
|
|
16
20
|
import { ObjectForm } from './ObjectForm';
|
|
17
21
|
|
|
22
|
+
// Seed so the first background mutation deterministically targets `fullName` (moving it off 'Alice Carroll'), which
|
|
23
|
+
// the play asserts. Seeded inside the component (below) rather than at module level: `@dxos/random` is a shared
|
|
24
|
+
// singleton, and the full storybook run imports every story up front and runs them in sequence, so a module-level
|
|
25
|
+
// seed gets consumed by other stories before this story's first tick.
|
|
26
|
+
const REACTIVE_SEED = 42;
|
|
27
|
+
|
|
18
28
|
const DefaultStory = () => {
|
|
19
29
|
const { space } = useClientStory();
|
|
20
30
|
const [person] = useQuery(space?.db, Filter.type(Person.Person));
|
|
@@ -30,7 +40,7 @@ const meta = {
|
|
|
30
40
|
render: DefaultStory,
|
|
31
41
|
decorators: [
|
|
32
42
|
withTheme(),
|
|
33
|
-
withLayout({
|
|
43
|
+
withLayout({ classNames: 'p-2', scroll: true }),
|
|
34
44
|
withClientProvider({
|
|
35
45
|
types: [Person.Person, Tag.Tag],
|
|
36
46
|
createIdentity: true,
|
|
@@ -60,3 +70,83 @@ export default meta;
|
|
|
60
70
|
type Story = StoryObj<typeof meta>;
|
|
61
71
|
|
|
62
72
|
export const Default: Story = {};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Exercises the reactive companion form against a live ECHO object. The toolbar mutates the object's fields in the
|
|
76
|
+
* background (outside the form); because `ObjectForm` subscribes via `useObject`, those external changes appear in the
|
|
77
|
+
* inputs immediately. Editing a field holds your (possibly invalid) draft locally without snapping back, while the
|
|
78
|
+
* other fields keep updating from the background mutations.
|
|
79
|
+
*/
|
|
80
|
+
const ReactiveStory = () => {
|
|
81
|
+
const { space } = useClientStory();
|
|
82
|
+
const [person] = useQuery(space?.db, Filter.type(Person.Person));
|
|
83
|
+
// Reactive snapshot for the JSON panel: reflects both background mutations and the form's own writes, so it shows
|
|
84
|
+
// whether a form edit reaches the object immediately.
|
|
85
|
+
const [snapshot] = useObject(person);
|
|
86
|
+
const [running, setRunning] = useState(true);
|
|
87
|
+
|
|
88
|
+
const mutate = useCallback(() => {
|
|
89
|
+
if (!person) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
Obj.update(person, (person) => {
|
|
93
|
+
if (random.datatype.boolean()) {
|
|
94
|
+
person.fullName = random.person.fullName();
|
|
95
|
+
} else {
|
|
96
|
+
person.jobTitle = random.person.jobTitle();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}, [person]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!running || !person) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Seed here — the mounted component is the RNG's sole consumer during its test — so the first tick is deterministic.
|
|
106
|
+
random.seed(REACTIVE_SEED);
|
|
107
|
+
const interval = setInterval(mutate, 1000);
|
|
108
|
+
return () => clearInterval(interval);
|
|
109
|
+
}, [running, person, mutate]);
|
|
110
|
+
|
|
111
|
+
if (!person) {
|
|
112
|
+
return <></>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<TestLayout json={snapshot}>
|
|
117
|
+
<div className='flex flex-col gap-2'>
|
|
118
|
+
<Toolbar.Root>
|
|
119
|
+
<Toolbar.IconButton
|
|
120
|
+
icon={running ? 'ph--pause--regular' : 'ph--play--regular'}
|
|
121
|
+
label={running ? 'Stop background mutations' : 'Start background mutations'}
|
|
122
|
+
onClick={() => setRunning((value) => !value)}
|
|
123
|
+
/>
|
|
124
|
+
<Toolbar.IconButton
|
|
125
|
+
icon='ph--shuffle--regular'
|
|
126
|
+
label='Mutate a random field once'
|
|
127
|
+
disabled={running}
|
|
128
|
+
onClick={mutate}
|
|
129
|
+
/>
|
|
130
|
+
</Toolbar.Root>
|
|
131
|
+
<ObjectForm object={person} type={Person.Person} />
|
|
132
|
+
</div>
|
|
133
|
+
</TestLayout>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const Reactive: Story = {
|
|
138
|
+
render: () => <ReactiveStory />,
|
|
139
|
+
play: async ({ canvasElement }) => {
|
|
140
|
+
const canvas = within(canvasElement);
|
|
141
|
+
|
|
142
|
+
// The form initially reflects the live object (generous timeout: the client provider creates the identity/space
|
|
143
|
+
// asynchronously, which is slow under full-suite browser load).
|
|
144
|
+
await canvas.findByDisplayValue('Alice Carroll', {}, { timeout: 15_000 });
|
|
145
|
+
|
|
146
|
+
// The first background tick mutates `fullName` outside the form; the form reflects it reactively, so the initial
|
|
147
|
+
// value disappears. `waitFor` re-runs the selector until the assertion holds.
|
|
148
|
+
await waitFor(() => expect(canvas.queryByDisplayValue('Alice Carroll')).not.toBeInTheDocument(), {
|
|
149
|
+
timeout: 5_000,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
};
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
5
6
|
import React, { useCallback, useMemo } from 'react';
|
|
6
7
|
|
|
7
8
|
import { Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
9
|
+
import { useObject } from '@dxos/echo-react';
|
|
8
10
|
import { SchemaEx } from '@dxos/effect';
|
|
9
11
|
import { invariant } from '@dxos/invariant';
|
|
10
12
|
import { HuePicker } from '@dxos/react-ui-pickers';
|
|
@@ -12,20 +14,36 @@ import { HuePicker } from '@dxos/react-ui-pickers';
|
|
|
12
14
|
import { translationKey } from '#translations';
|
|
13
15
|
import { type FormFieldMap } from '#types';
|
|
14
16
|
|
|
17
|
+
import { omitId } from '../../util';
|
|
15
18
|
import { Form, META_TAGS_KEY, withMetaTags } from '../Form';
|
|
16
19
|
|
|
17
20
|
export type ObjectFormProps = {
|
|
18
21
|
type: Type.AnyEntity;
|
|
19
22
|
object: Obj.Unknown;
|
|
23
|
+
/**
|
|
24
|
+
* Optional schema override for the rendered fields, e.g. a projection of `type`'s schema
|
|
25
|
+
* (`Type.getSchema(T).pipe(Schema.pick(...))`). Values are still read from and written back to
|
|
26
|
+
* `object` by path, so the picked fields must be paths on `object`. Defaults to `type`'s schema.
|
|
27
|
+
*/
|
|
28
|
+
schema?: Schema.Schema.AnyNoContext;
|
|
29
|
+
/** Render the meta-tags field. Defaults to `true`. */
|
|
30
|
+
showTags?: boolean;
|
|
20
31
|
};
|
|
21
32
|
|
|
22
|
-
export const ObjectForm = ({ object, type }: ObjectFormProps) => {
|
|
33
|
+
export const ObjectForm = ({ object, type, schema, showTags = true }: ObjectFormProps) => {
|
|
23
34
|
const db = Obj.getDatabase(object);
|
|
35
|
+
// Subscribe to the object so external/remote mutations re-render the form: ECHO reactivity is atom-based, so
|
|
36
|
+
// reading the raw object during render does not establish a subscription. `snapshot` is a fresh value on change.
|
|
37
|
+
const [snapshot] = useObject(object);
|
|
24
38
|
const meta = Obj.getMeta(object);
|
|
25
39
|
// `meta.tags` already holds `Ref<Tag>`s (materialized by the database handler).
|
|
26
40
|
const tags = [...meta.tags];
|
|
27
|
-
const values = useMemo(() => ({ [META_TAGS_KEY]: tags, ...
|
|
28
|
-
const formSchema = useMemo(() =>
|
|
41
|
+
const values = useMemo(() => ({ [META_TAGS_KEY]: tags, ...snapshot }), [snapshot, tags]);
|
|
42
|
+
const formSchema = useMemo(() => {
|
|
43
|
+
const base = schema ?? Type.getSchema(type);
|
|
44
|
+
// `withMetaTags` also drops `id`; keep it dropped when tags are omitted.
|
|
45
|
+
return showTags ? withMetaTags(base) : omitId(base);
|
|
46
|
+
}, [schema, type, showTags]);
|
|
29
47
|
|
|
30
48
|
const handleCreate = useCallback((type: Type.AnyEntity, values: any) => {
|
|
31
49
|
invariant(db);
|
|
@@ -79,7 +97,7 @@ export const ObjectForm = ({ object, type }: ObjectFormProps) => {
|
|
|
79
97
|
return (
|
|
80
98
|
<Form.Root
|
|
81
99
|
schema={formSchema}
|
|
82
|
-
|
|
100
|
+
values={values}
|
|
83
101
|
createTypename={Type.getTypename(Tag.Tag)}
|
|
84
102
|
createOptionIcon='ph--plus--regular'
|
|
85
103
|
createOptionLabel={['add-tag.label', { ns: translationKey }]}
|
|
@@ -8,8 +8,8 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
8
8
|
import { expect, fn, userEvent, within } from 'storybook/test';
|
|
9
9
|
|
|
10
10
|
import { Filter, Obj, Type } from '@dxos/echo';
|
|
11
|
+
import { useQuery } from '@dxos/echo-react';
|
|
11
12
|
import { random } from '@dxos/random';
|
|
12
|
-
import { useQuery } from '@dxos/react-client/echo';
|
|
13
13
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { Button } from '@dxos/react-ui';
|
|
15
15
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type * as Schema from 'effect/Schema';
|
|
6
|
-
import React, { type KeyboardEvent,
|
|
6
|
+
import React, { type KeyboardEvent, useCallback, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import { Popover, type ThemedClassName, useTranslation } from '@dxos/react-ui';
|
|
8
|
+
import { Popover, type ThemedClassName, composable, useTranslation } from '@dxos/react-ui';
|
|
9
9
|
import { Combobox } from '@dxos/react-ui-list';
|
|
10
10
|
import { useSearchListResults } from '@dxos/react-ui-search';
|
|
11
11
|
|
|
@@ -31,7 +31,7 @@ export type ObjectPickerContentProps = ThemedClassName<
|
|
|
31
31
|
}
|
|
32
32
|
>;
|
|
33
33
|
|
|
34
|
-
const ObjectPickerContent =
|
|
34
|
+
const ObjectPickerContent = composable<HTMLDivElement, ObjectPickerContentProps>(
|
|
35
35
|
(
|
|
36
36
|
{
|
|
37
37
|
options,
|
|
@@ -8,8 +8,8 @@ import React, { useEffect } from 'react';
|
|
|
8
8
|
import { expect, userEvent, within } from 'storybook/test';
|
|
9
9
|
|
|
10
10
|
import { Annotation, DXN, Filter, Obj, Ref, Tag, Type } from '@dxos/echo';
|
|
11
|
+
import { useQuery } from '@dxos/echo-react';
|
|
11
12
|
import { FormInputAnnotation, LabelAnnotation } from '@dxos/echo/Annotation';
|
|
12
|
-
import { useQuery } from '@dxos/react-client/echo';
|
|
13
13
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
14
14
|
import { Panel } from '@dxos/react-ui';
|
|
15
15
|
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
@@ -30,7 +30,13 @@ export const ObjectProperties = composable<HTMLDivElement, ObjectPropertiesProps
|
|
|
30
30
|
const meta = Obj.getMeta(object);
|
|
31
31
|
// `meta.tags` already holds `Ref<Tag>`s (materialized by the database handler).
|
|
32
32
|
const tags = [...meta.tags];
|
|
33
|
-
const values = useMemo(
|
|
33
|
+
const values = useMemo(
|
|
34
|
+
() => ({
|
|
35
|
+
[META_TAGS_KEY]: tags,
|
|
36
|
+
...object,
|
|
37
|
+
}),
|
|
38
|
+
[object, tags],
|
|
39
|
+
);
|
|
34
40
|
|
|
35
41
|
// Obj.getType fails for database-registered (dynamic) schemas due to DXN mismatch;
|
|
36
42
|
// useType queries by the object's stored type URI, resolving both static and dynamic schemas.
|
|
@@ -40,7 +40,7 @@ const value: ConfigProto = {
|
|
|
40
40
|
subductionReplicator: true,
|
|
41
41
|
signaling: true,
|
|
42
42
|
},
|
|
43
|
-
servicesMode: Runtime.Client.ServicesMode.
|
|
43
|
+
servicesMode: Runtime.Client.ServicesMode.DEDICATED_WORKER,
|
|
44
44
|
enableSnapshots: false,
|
|
45
45
|
snapshotInterval: 60_000,
|
|
46
46
|
},
|
|
@@ -6,8 +6,8 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
|
6
6
|
import React, { Fragment, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type Database, Filter, Obj } from '@dxos/echo';
|
|
9
|
+
import { useQuery } from '@dxos/echo-react';
|
|
9
10
|
import { random } from '@dxos/random';
|
|
10
|
-
import { useQuery } from '@dxos/react-client/echo';
|
|
11
11
|
import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
|
|
12
12
|
import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
13
13
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
@@ -145,3 +145,11 @@ export const WithEmail: Story = {
|
|
|
145
145
|
value: 'someone@example.com ',
|
|
146
146
|
},
|
|
147
147
|
};
|
|
148
|
+
|
|
149
|
+
export const EmailModeWithValues: Story = {
|
|
150
|
+
args: {
|
|
151
|
+
mode: 'email',
|
|
152
|
+
activateOnTyping: true,
|
|
153
|
+
value: 'Alice Smith <alice@example.com>, bob@example.com, ',
|
|
154
|
+
},
|
|
155
|
+
};
|