@dxos/react-ui-form 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a
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 +1883 -1539
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1883 -1539
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/annotations.d.ts +47 -0
- package/dist/types/src/annotations.d.ts.map +1 -0
- package/dist/types/src/components/FieldEditor/FieldEditor.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.d.ts +12 -4
- package/dist/types/src/components/Form/Form.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.stories.d.ts +46 -65
- package/dist/types/src/components/Form/Form.stories.d.ts.map +1 -1
- package/dist/types/src/components/Form/Form.theme.d.ts +252 -0
- package/dist/types/src/components/Form/Form.theme.d.ts.map +1 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts +2 -0
- package/dist/types/src/components/Form/Form.theme.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormControls.d.ts +17 -8
- package/dist/types/src/components/Form/FormControls.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormField.d.ts +16 -2
- package/dist/types/src/components/Form/FormField/FormField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts +27 -0
- package/dist/types/src/components/Form/FormField/FormFieldHeader.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts +85 -0
- package/dist/types/src/components/Form/FormField/FormRow.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/{FormFieldWrapper.stories.d.ts → FormRow.stories.d.ts} +4 -1
- package/dist/types/src/components/Form/FormField/FormRow.stories.d.ts.map +1 -0
- 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/AsyncSelectField/AsyncSelectField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AsyncSelectField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/AutofillField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/AutofillField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/BooleanField/BooleanField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts +12 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/ComboboxField.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/ComboboxField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts +1 -1
- package/dist/types/src/components/Form/FormField/fields/GeoPointField/GeoPointField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/MarkdownField/MarkdownField.d.ts +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/NumberField/NumberField.d.ts +11 -0
- 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.test.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/NumberField/NumberField.test.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.d.ts.map +1 -0
- package/dist/types/src/components/{Settings/Settings.stories.d.ts → Form/FormField/fields/PasswordField/PasswordField.stories.d.ts} +1 -2
- package/dist/types/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts +2 -0
- package/dist/types/src/components/Form/FormField/fields/PasswordField/index.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormField/fields/RefField/InlineRefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/RefField/RefField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts +18 -2
- package/dist/types/src/components/Form/FormField/fields/SelectField/SelectField.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/fields/index.d.ts +4 -0
- package/dist/types/src/components/Form/FormField/fields/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/index.d.ts +4 -3
- package/dist/types/src/components/Form/FormField/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormField/presentation.d.ts +22 -0
- package/dist/types/src/components/Form/FormField/presentation.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts +8 -5
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSet.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts +22 -0
- package/dist/types/src/components/Form/FormFieldSet/FormFieldSetContainer.d.ts.map +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts +1 -0
- package/dist/types/src/components/Form/FormFieldSet/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts +2 -0
- package/dist/types/src/components/Form/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/types/src/components/Form/FormLayout/FormLayout.stories.d.ts +5 -5
- 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 -1
- package/dist/types/src/components/Form/FormLayout/index.d.ts.map +1 -1
- package/dist/types/src/components/Form/meta-tags.d.ts +1 -12
- package/dist/types/src/components/Form/meta-tags.d.ts.map +1 -1
- package/dist/types/src/components/ObjectPicker/ObjectPicker.d.ts.map +1 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts +2 -1
- package/dist/types/src/components/ObjectProperties/ObjectProperties.d.ts.map +1 -1
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts +2 -2
- package/dist/types/src/components/RefEditor/RefEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.d.ts.map +1 -1
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts +2 -2
- package/dist/types/src/components/ViewEditor/ViewEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts +24 -0
- package/dist/types/src/hooks/useAsyncFieldEffect.d.ts.map +1 -0
- package/dist/types/src/hooks/useFormContext.d.ts +8 -3
- package/dist/types/src/hooks/useFormContext.d.ts.map +1 -1
- package/dist/types/src/hooks/useFormHandler.d.ts +8 -2
- package/dist/types/src/hooks/useFormHandler.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- 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/testing/schema.d.ts +13 -16
- package/dist/types/src/testing/schema.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +50 -1
- 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/src/util/omit.d.ts +1 -1
- package/dist/types/src/util/omit.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -40
- package/src/annotations.ts +63 -0
- package/src/components/FieldEditor/FieldEditor.tsx +5 -2
- package/src/components/Form/DESIGN.md +24 -0
- package/src/components/Form/Form.stories.tsx +257 -13
- package/src/components/Form/Form.theme.test.ts +33 -0
- package/src/components/Form/Form.theme.ts +86 -0
- package/src/components/Form/Form.tsx +15 -9
- package/src/components/Form/FormControls.tsx +94 -37
- package/src/components/Form/FormField/FormField.tsx +83 -32
- package/src/components/Form/FormField/FormFieldHeader.tsx +68 -0
- package/src/components/Form/FormField/{FormFieldWrapper.stories.tsx → FormRow.stories.tsx} +34 -9
- package/src/components/Form/FormField/FormRow.tsx +298 -0
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.stories.tsx +25 -9
- package/src/components/Form/FormField/fields/ArrayField/ArrayField.tsx +83 -24
- package/src/components/Form/FormField/fields/AsyncSelectField/AsyncSelectField.tsx +59 -0
- package/src/components/Form/FormField/fields/AsyncSelectField/index.ts +5 -0
- package/src/components/Form/FormField/fields/AutofillField/AutofillField.tsx +67 -0
- package/src/components/Form/FormField/fields/AutofillField/index.ts +5 -0
- package/src/components/Form/FormField/fields/BooleanField/BooleanField.tsx +5 -6
- package/src/components/Form/FormField/fields/ComboboxField/ComboboxField.tsx +104 -0
- package/src/components/Form/FormField/fields/ComboboxField/index.ts +5 -0
- package/src/components/Form/FormField/fields/DateField/DateField.tsx +9 -9
- package/src/components/Form/FormField/fields/GeoPointField/GeoPointField.tsx +17 -20
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.stories.tsx +2 -2
- package/src/components/Form/FormField/fields/MarkdownField/MarkdownField.tsx +39 -40
- package/src/components/Form/FormField/fields/NumberField/NumberField.test.ts +29 -0
- package/src/components/Form/FormField/fields/NumberField/NumberField.tsx +60 -21
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.stories.tsx +53 -0
- package/src/components/Form/FormField/fields/PasswordField/PasswordField.tsx +41 -0
- package/src/components/Form/FormField/fields/PasswordField/index.ts +5 -0
- package/src/components/Form/FormField/fields/RefField/InlineRefField.tsx +16 -10
- package/src/components/Form/FormField/fields/RefField/RefField.test.ts +8 -0
- package/src/components/Form/FormField/fields/RefField/RefField.tsx +50 -23
- package/src/components/Form/FormField/fields/SelectField/SelectField.tsx +104 -47
- package/src/components/Form/FormField/fields/SelectOptionField/SelectOptionField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextAreaField/TextAreaField.tsx +3 -3
- package/src/components/Form/FormField/fields/TextField/TextField.tsx +3 -3
- package/src/components/Form/FormField/fields/TupleField/TupleField.tsx +1 -1
- package/src/components/Form/FormField/fields/index.ts +4 -0
- package/src/components/Form/FormField/index.ts +5 -3
- package/src/components/Form/FormField/presentation.tsx +32 -0
- package/src/components/Form/FormFieldSet/FormFieldSet.tsx +49 -70
- package/src/components/Form/FormFieldSet/FormFieldSetContainer.tsx +93 -0
- package/src/components/Form/FormFieldSet/index.ts +1 -0
- package/src/components/Form/FormLayout/FormLayout.stories.tsx +7 -5
- package/src/components/Form/FormLayout/FormLayout.tsx +24 -14
- package/src/components/Form/FormLayout/index.ts +1 -1
- package/src/components/Form/meta-tags.test.ts +1 -1
- package/src/components/ObjectForm/ObjectForm.tsx +2 -2
- package/src/components/ObjectPicker/ObjectPicker.tsx +1 -0
- package/src/components/ObjectProperties/ObjectProperties.stories.tsx +1 -1
- package/src/components/ObjectProperties/ObjectProperties.tsx +12 -8
- package/src/components/RefEditor/RefEditor.stories.tsx +2 -2
- package/src/components/RefEditor/RefEditor.tsx +1 -1
- package/src/components/ViewEditor/ViewEditor.stories.tsx +2 -2
- package/src/components/ViewEditor/ViewEditor.test.tsx +8 -2
- package/src/components/ViewEditor/ViewEditor.tsx +3 -3
- package/src/components/index.ts +0 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAsyncFieldEffect.ts +81 -0
- package/src/hooks/useFormContext.ts +6 -3
- package/src/hooks/useFormHandler.ts +11 -6
- package/src/index.ts +1 -0
- package/src/testing/TestLayout.tsx +15 -12
- package/src/testing/schema.ts +43 -43
- package/src/types.ts +53 -1
- package/src/util/index.ts +0 -1
- package/src/util/omit.ts +25 -11
- package/src/util/properties.test.ts +110 -28
- package/src/util/refs.ts +1 -1
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts +0 -19
- package/dist/types/src/components/Form/FormField/FieldHeader.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts +0 -47
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.d.ts.map +0 -1
- package/dist/types/src/components/Form/FormField/FormFieldWrapper.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.d.ts +0 -51
- package/dist/types/src/components/Settings/Settings.d.ts.map +0 -1
- package/dist/types/src/components/Settings/Settings.stories.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts +0 -43
- package/dist/types/src/components/Settings/SettingsFieldSet.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsItem.d.ts +0 -16
- package/dist/types/src/components/Settings/SettingsItem.d.ts.map +0 -1
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts +0 -18
- package/dist/types/src/components/Settings/SettingsSelectField.d.ts.map +0 -1
- package/dist/types/src/components/Settings/index.d.ts +0 -4
- package/dist/types/src/components/Settings/index.d.ts.map +0 -1
- package/dist/types/src/util/field-type.d.ts +0 -19
- package/dist/types/src/util/field-type.d.ts.map +0 -1
- package/src/components/Form/FormField/FieldHeader.tsx +0 -36
- package/src/components/Form/FormField/FormFieldWrapper.tsx +0 -179
- package/src/components/Settings/AUDIT.md +0 -51
- package/src/components/Settings/Settings.stories.tsx +0 -104
- package/src/components/Settings/Settings.tsx +0 -81
- package/src/components/Settings/SettingsFieldSet.tsx +0 -257
- package/src/components/Settings/SettingsItem.tsx +0 -58
- package/src/components/Settings/SettingsSelectField.tsx +0 -59
- package/src/components/Settings/index.ts +0 -7
- package/src/util/field-type.ts +0 -60
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
import { CompactIconButton } from './FormField';
|
|
8
|
+
import { FormFieldLabel } from './FormRow';
|
|
9
|
+
|
|
10
|
+
export type FormFieldHeaderProps = {
|
|
11
|
+
label: string;
|
|
12
|
+
/** JSON path forwarded to the label as field metadata. */
|
|
13
|
+
path?: string;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
readonly?: boolean;
|
|
16
|
+
classNames?: string;
|
|
17
|
+
/** Trailing inline add affordance; omit to hide it. */
|
|
18
|
+
add?: { icon?: string; label: string; disabled?: boolean; onClick: () => void };
|
|
19
|
+
/** Extra trailing controls placed after the add affordance (e.g. a collapse toggle). */
|
|
20
|
+
actions?: ReactNode;
|
|
21
|
+
/** Header row click (e.g. toggle collapse). The add affordance stops propagation so it doesn't trigger this. */
|
|
22
|
+
onClick?: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Header row for a labelled group or list: a label with optional trailing controls (an inline add
|
|
27
|
+
* affordance and/or arbitrary `actions`). Shared by array/list fields ({@link ArrayField}, the view
|
|
28
|
+
* editor) and {@link FormFieldSetContainer}'s collapse header, so all group/list headers render identically.
|
|
29
|
+
*/
|
|
30
|
+
export const FormFieldHeader = ({
|
|
31
|
+
label,
|
|
32
|
+
path,
|
|
33
|
+
required,
|
|
34
|
+
readonly,
|
|
35
|
+
classNames,
|
|
36
|
+
add,
|
|
37
|
+
actions,
|
|
38
|
+
onClick,
|
|
39
|
+
}: FormFieldHeaderProps) => (
|
|
40
|
+
<FormFieldLabel
|
|
41
|
+
standalone
|
|
42
|
+
classNames={classNames}
|
|
43
|
+
label={label}
|
|
44
|
+
required={required}
|
|
45
|
+
readonly={readonly}
|
|
46
|
+
path={path}
|
|
47
|
+
button={
|
|
48
|
+
(!readonly && add) || actions ? (
|
|
49
|
+
<>
|
|
50
|
+
{!readonly && add && (
|
|
51
|
+
<CompactIconButton
|
|
52
|
+
disabled={add.disabled}
|
|
53
|
+
icon={add.icon ?? 'ph--plus--regular'}
|
|
54
|
+
label={add.label}
|
|
55
|
+
onClick={(event) => {
|
|
56
|
+
// Don't let the add click bubble to the header's onClick (e.g. collapse toggle).
|
|
57
|
+
event.stopPropagation();
|
|
58
|
+
add.onClick();
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
{actions}
|
|
63
|
+
</>
|
|
64
|
+
) : undefined
|
|
65
|
+
}
|
|
66
|
+
onClick={onClick}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
@@ -11,7 +11,7 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
|
11
11
|
import { type FormFieldRendererProps } from '#types';
|
|
12
12
|
|
|
13
13
|
import { Form } from '../Form';
|
|
14
|
-
import {
|
|
14
|
+
import { FormRow } from './FormRow';
|
|
15
15
|
|
|
16
16
|
const DefaultStory = ({
|
|
17
17
|
type,
|
|
@@ -30,7 +30,7 @@ const DefaultStory = ({
|
|
|
30
30
|
<Form.Root>
|
|
31
31
|
<Form.Viewport>
|
|
32
32
|
<Form.Content>
|
|
33
|
-
<
|
|
33
|
+
<FormRow<string> {...props}>
|
|
34
34
|
{({ value = '' }) => (
|
|
35
35
|
<Input.TextInput
|
|
36
36
|
disabled={!!readonly}
|
|
@@ -41,7 +41,7 @@ const DefaultStory = ({
|
|
|
41
41
|
onBlur={onBlur}
|
|
42
42
|
/>
|
|
43
43
|
)}
|
|
44
|
-
</
|
|
44
|
+
</FormRow>
|
|
45
45
|
</Form.Content>
|
|
46
46
|
</Form.Viewport>
|
|
47
47
|
</Form.Root>
|
|
@@ -49,7 +49,7 @@ const DefaultStory = ({
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
const meta: Meta<typeof DefaultStory> = {
|
|
52
|
-
title: 'ui/react-ui-form/
|
|
52
|
+
title: 'ui/react-ui-form/FormRow',
|
|
53
53
|
component: DefaultStory,
|
|
54
54
|
decorators: [withTheme(), withLayout({ layout: 'column' })],
|
|
55
55
|
parameters: {
|
|
@@ -61,12 +61,37 @@ export default meta;
|
|
|
61
61
|
|
|
62
62
|
type Story = StoryObj<typeof meta>;
|
|
63
63
|
|
|
64
|
+
const props: Story['args'] = {
|
|
65
|
+
label: 'Label',
|
|
66
|
+
required: true,
|
|
67
|
+
getStatus: () => ({ status: 'error', error: 'Required field' }),
|
|
68
|
+
getValue: () => 'DXOS',
|
|
69
|
+
};
|
|
70
|
+
|
|
64
71
|
export const Default: Story = {
|
|
65
72
|
args: {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
...props,
|
|
74
|
+
presentation: 'full',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const Compact: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
...props,
|
|
81
|
+
presentation: 'compact',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const Inline: Story = {
|
|
86
|
+
args: {
|
|
87
|
+
...props,
|
|
88
|
+
presentation: 'inline',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Static: Story = {
|
|
93
|
+
args: {
|
|
94
|
+
...props,
|
|
95
|
+
presentation: 'static',
|
|
71
96
|
},
|
|
72
97
|
};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { format as formatDate } from 'date-fns';
|
|
6
|
+
import React, { Component, type PropsWithChildren, type ReactNode, type Ref } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Format } from '@dxos/echo';
|
|
9
|
+
import { Icon, Input, type ThemedClassName, Tooltip } from '@dxos/react-ui';
|
|
10
|
+
import { mx } from '@dxos/ui-theme';
|
|
11
|
+
|
|
12
|
+
import { type FormFieldRendererProps } from '#types';
|
|
13
|
+
|
|
14
|
+
import { useFormContext } from '../../../hooks';
|
|
15
|
+
import { type FormVariant, formTheme } from '../Form.theme';
|
|
16
|
+
import { type FormFieldPresentation, presentationFor } from './presentation';
|
|
17
|
+
|
|
18
|
+
//
|
|
19
|
+
// FormFieldLabel
|
|
20
|
+
//
|
|
21
|
+
|
|
22
|
+
export type FormFieldLabelProps = ThemedClassName<
|
|
23
|
+
{
|
|
24
|
+
/** Render a plain `<span>` instead of an input-associated `Input.Label`, for labels used outside an `Input.Root` (e.g. section/group headers). */
|
|
25
|
+
standalone?: boolean;
|
|
26
|
+
/** Class applied to the inner label text node, overriding the default size/color (e.g. `text-lg`). */
|
|
27
|
+
labelClassName?: string;
|
|
28
|
+
/** Form variant; selects the label's chrome (e.g. `settings` enlarges the text and places it in the field grid). */
|
|
29
|
+
variant?: FormVariant;
|
|
30
|
+
error?: string;
|
|
31
|
+
/**
|
|
32
|
+
* JSON path of the field this label describes (e.g. `runtime.client.storage.persistent`).
|
|
33
|
+
* Field metadata; accepted by callers but not currently rendered.
|
|
34
|
+
*/
|
|
35
|
+
path?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Trailing button rendered at the end of the label row (third grid column).
|
|
38
|
+
* Used by nested-object field sets to surface a collapse toggle.
|
|
39
|
+
*/
|
|
40
|
+
button?: ReactNode;
|
|
41
|
+
onClick?: () => void;
|
|
42
|
+
} & Pick<FormFieldRendererProps, 'label' | 'readonly' | 'required'>
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export const FormFieldLabel = ({
|
|
46
|
+
classNames,
|
|
47
|
+
labelClassName,
|
|
48
|
+
variant = 'default',
|
|
49
|
+
label,
|
|
50
|
+
error,
|
|
51
|
+
readonly,
|
|
52
|
+
required,
|
|
53
|
+
standalone,
|
|
54
|
+
button,
|
|
55
|
+
onClick,
|
|
56
|
+
}: FormFieldLabelProps) => {
|
|
57
|
+
const styles = formTheme.styles({ variant });
|
|
58
|
+
// Render the required asterisk via a `::after` pseudo-element rather than a DOM node: it keeps the
|
|
59
|
+
// label's `textContent` exactly `label`, so fields stay locatable by their exact label text
|
|
60
|
+
// (`getByLabelText('Name')`), which the DOM-text-based query would otherwise miss as `Name *`.
|
|
61
|
+
// The `fieldLabelText` slot is applied last so a variant/caller size/color (e.g. `text-lg`) wins over
|
|
62
|
+
const labelClassNames = mx(
|
|
63
|
+
'text-sm text-description',
|
|
64
|
+
required && "after:content-['*'] after:ms-0.5 after:text-warning-text",
|
|
65
|
+
styles.fieldLabelText({ class: labelClassName }),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// `Input.Label` is a themed primitive that reads `classNames` (and ignores `className`), whereas the
|
|
69
|
+
// plain `span` used for read-only/standalone labels reads `className`.
|
|
70
|
+
const labelNode =
|
|
71
|
+
readonly || standalone ? (
|
|
72
|
+
<span className={labelClassNames}>{label}</span>
|
|
73
|
+
) : (
|
|
74
|
+
<Input.Label classNames={labelClassNames}>{label}</Input.Label>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={styles.fieldLabel({ class: mx(onClick && 'cursor-pointer', classNames) })} onClick={onClick}>
|
|
79
|
+
{labelNode}
|
|
80
|
+
{error ? (
|
|
81
|
+
<Tooltip.Trigger asChild content={error} side='bottom'>
|
|
82
|
+
<Icon icon='ph--warning--regular' size={4} classNames='text-error-text' />
|
|
83
|
+
</Tooltip.Trigger>
|
|
84
|
+
) : (
|
|
85
|
+
<span />
|
|
86
|
+
)}
|
|
87
|
+
{button}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
FormFieldLabel.displayName = 'Form.FieldLabel';
|
|
93
|
+
|
|
94
|
+
//
|
|
95
|
+
// FormRow
|
|
96
|
+
//
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Formats a value for `static` (read-only, plain-DOM) presentation based on its
|
|
100
|
+
* type `format`. Dates/times are rendered human-readable; everything else falls
|
|
101
|
+
* back to `String(value)`.
|
|
102
|
+
*/
|
|
103
|
+
const formatStaticValue = (value: unknown, format?: Format.TypeFormat): string => {
|
|
104
|
+
if (value == null) {
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (format) {
|
|
109
|
+
case Format.TypeFormat.DateTime:
|
|
110
|
+
case Format.TypeFormat.Date:
|
|
111
|
+
case Format.TypeFormat.Time: {
|
|
112
|
+
const date = new Date(value as string);
|
|
113
|
+
if (Number.isNaN(date.getTime())) {
|
|
114
|
+
return String(value);
|
|
115
|
+
}
|
|
116
|
+
const pattern = format === Format.TypeFormat.DateTime ? 'PPp' : format === Format.TypeFormat.Date ? 'PP' : 'p';
|
|
117
|
+
return formatDate(date, pattern);
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return String(value);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const FORM_ROW_NAME = 'Form.Row';
|
|
125
|
+
|
|
126
|
+
export type FormRowProps<T = any> = ThemedClassName<
|
|
127
|
+
Pick<FormFieldRendererProps<T>, 'readonly' | 'label' | 'description' | 'presentation' | 'required'> &
|
|
128
|
+
Partial<Pick<FormFieldRendererProps<T>, 'getStatus' | 'getValue' | 'jsonPath' | 'format'>> & {
|
|
129
|
+
/**
|
|
130
|
+
* Ref to the row's outer element. Enables measurement/positioning (e.g. a future virtualized
|
|
131
|
+
* field set) without forwarding through the generic component.
|
|
132
|
+
*/
|
|
133
|
+
rootRef?: Ref<HTMLDivElement>;
|
|
134
|
+
/**
|
|
135
|
+
* Render the label as a standalone `<span>` (group/multi-input fields with no single associated control).
|
|
136
|
+
*/
|
|
137
|
+
standalone?: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Override the read-only/`static` rendering of the value. Fields whose value is not plain text
|
|
140
|
+
* (refs, selects, markdown) supply this; the default formats scalars/dates via `formatStaticValue`.
|
|
141
|
+
* Return `null` to render nothing (e.g. an empty/unresolved value).
|
|
142
|
+
*/
|
|
143
|
+
renderStatic?: (value: T | undefined) => ReactNode;
|
|
144
|
+
/**
|
|
145
|
+
* Validation/error content rendered in the validation slot. Supplied directly in action mode; in
|
|
146
|
+
* field mode it is derived from `getStatus`.
|
|
147
|
+
*/
|
|
148
|
+
validation?: ReactNode;
|
|
149
|
+
/**
|
|
150
|
+
* The control. A render-prop binds to the form value (field mode: `Input.Root` validation, static
|
|
151
|
+
* rendering, value via `getValue`). Plain nodes render an arbitrary control with no value wiring
|
|
152
|
+
* (action mode, e.g. a button) — the labeled-card escape hatch for an arbitrary control.
|
|
153
|
+
*
|
|
154
|
+
* `value` is `T | undefined` because `getValue()` returns no value when the field is unset (optional
|
|
155
|
+
* schema properties, freshly-added array items); renderers default it (e.g. `{ value = '' }`).
|
|
156
|
+
*/
|
|
157
|
+
children?: ReactNode | ((props: { value: T | undefined; presentation: FormFieldPresentation }) => ReactNode);
|
|
158
|
+
}
|
|
159
|
+
>;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A labeled card row — the single shell behind both schema fields and free-form "action" rows. Field
|
|
163
|
+
* renderers pass a render-prop `children` (field mode: bound to the form value, with validation and
|
|
164
|
+
* static rendering); consumers pass plain `children` for an arbitrary control (action mode), the
|
|
165
|
+
* affordance for an arbitrary labeled control (action rows, settings cards).
|
|
166
|
+
*/
|
|
167
|
+
export const FormRow = <T,>({
|
|
168
|
+
classNames,
|
|
169
|
+
children,
|
|
170
|
+
readonly,
|
|
171
|
+
presentation,
|
|
172
|
+
label,
|
|
173
|
+
description,
|
|
174
|
+
jsonPath,
|
|
175
|
+
format,
|
|
176
|
+
required,
|
|
177
|
+
standalone,
|
|
178
|
+
renderStatic,
|
|
179
|
+
validation,
|
|
180
|
+
getStatus,
|
|
181
|
+
getValue,
|
|
182
|
+
rootRef,
|
|
183
|
+
}: FormRowProps<T>) => {
|
|
184
|
+
const { variant = 'default' } = useFormContext(FORM_ROW_NAME);
|
|
185
|
+
const styles = formTheme.styles({ variant });
|
|
186
|
+
const { showDescription } = formTheme.behavior[variant];
|
|
187
|
+
const resolved = presentationFor(presentation);
|
|
188
|
+
|
|
189
|
+
//
|
|
190
|
+
// Field mode: a render-prop control bound to the form value.
|
|
191
|
+
//
|
|
192
|
+
if (typeof children === 'function' && getStatus && getValue) {
|
|
193
|
+
const { status, error } = getStatus();
|
|
194
|
+
const value: T | undefined = getValue();
|
|
195
|
+
if (resolved.isStatic && value == null) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const control = resolved.isStatic
|
|
200
|
+
? (renderStatic?.(value) ?? <p className='truncate min-w-0'>{formatStaticValue(value, format)}</p>)
|
|
201
|
+
: children({ value, presentation: resolved });
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Input.Root validationValence={status}>
|
|
205
|
+
<div className={styles.field({ class: classNames })} ref={rootRef}>
|
|
206
|
+
{resolved.showLabel && (
|
|
207
|
+
<FormFieldLabel
|
|
208
|
+
variant={variant}
|
|
209
|
+
error={error}
|
|
210
|
+
readonly={readonly}
|
|
211
|
+
required={required}
|
|
212
|
+
standalone={standalone}
|
|
213
|
+
label={label}
|
|
214
|
+
path={jsonPath}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
{showDescription && description && (
|
|
218
|
+
<Input.Description classNames={styles.fieldDescription()}>{description}</Input.Description>
|
|
219
|
+
)}
|
|
220
|
+
<div className={styles.fieldControl()}>{control}</div>
|
|
221
|
+
{resolved.showError && error && (
|
|
222
|
+
<div className={styles.fieldValidation()}>
|
|
223
|
+
<Input.DescriptionAndValidation>
|
|
224
|
+
<Input.Validation>{error}</Input.Validation>
|
|
225
|
+
</Input.DescriptionAndValidation>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
</Input.Root>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//
|
|
234
|
+
// Action mode: an arbitrary control with a standalone label; no value/`Input.Root` wiring.
|
|
235
|
+
//
|
|
236
|
+
return (
|
|
237
|
+
<div ref={rootRef} className={styles.field({ class: classNames })}>
|
|
238
|
+
{resolved.showLabel && label && (
|
|
239
|
+
<FormFieldLabel
|
|
240
|
+
variant={variant}
|
|
241
|
+
readonly={readonly}
|
|
242
|
+
required={required}
|
|
243
|
+
standalone
|
|
244
|
+
label={label}
|
|
245
|
+
path={jsonPath}
|
|
246
|
+
/>
|
|
247
|
+
)}
|
|
248
|
+
{showDescription && description && <p className={styles.fieldDescription()}>{description}</p>}
|
|
249
|
+
<div className={styles.fieldControl()}>{typeof children === 'function' ? null : children}</div>
|
|
250
|
+
{validation && <div className={styles.fieldValidation()}>{validation}</div>}
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
FormRow.displayName = FORM_ROW_NAME;
|
|
256
|
+
|
|
257
|
+
//
|
|
258
|
+
// FormFieldErrorBoundary
|
|
259
|
+
//
|
|
260
|
+
|
|
261
|
+
type FormFieldErrorState = {
|
|
262
|
+
error: Error | undefined;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
type FormFieldErrorBoundaryProps = PropsWithChildren<{
|
|
266
|
+
path?: (string | number)[];
|
|
267
|
+
}>;
|
|
268
|
+
|
|
269
|
+
export class FormFieldErrorBoundary extends Component<FormFieldErrorBoundaryProps, FormFieldErrorState> {
|
|
270
|
+
static getDerivedStateFromError(error: Error): { error: Error } {
|
|
271
|
+
return { error };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
override state = { error: undefined };
|
|
275
|
+
|
|
276
|
+
override componentDidUpdate(prevProps: FormFieldErrorBoundaryProps) {
|
|
277
|
+
if (prevProps.path !== this.props.path) {
|
|
278
|
+
this.resetError();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
override render() {
|
|
283
|
+
if (this.state.error) {
|
|
284
|
+
return (
|
|
285
|
+
<div className='flex gap-2 border border-error-border font-mono text-sm'>
|
|
286
|
+
<span className='bg-error-bg text-base-fg px-1 font-thin'>ERROR</span>
|
|
287
|
+
{String(this.props.path?.join('.'))}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return this.props.children;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private resetError() {
|
|
296
|
+
this.setState({ error: undefined });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -47,29 +47,41 @@ const Pipeline = Schema.Struct({
|
|
|
47
47
|
columns: columnsField,
|
|
48
48
|
}).pipe(Schema.mutable);
|
|
49
49
|
|
|
50
|
-
// `FormOrderedAnnotation` opts the array into a drag-to-reorder list (see `ArrayField`).
|
|
51
50
|
const OrderedPipeline = Schema.Struct({
|
|
52
51
|
...headerFields,
|
|
53
52
|
columns: columnsField.pipe(FormOrderedAnnotation.set(true)),
|
|
54
53
|
}).pipe(Schema.mutable);
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
const StringPipeline = Schema.Struct({
|
|
56
|
+
...headerFields,
|
|
57
|
+
columns: Schema.Array(Schema.String).pipe(FormOrderedAnnotation.set(true)),
|
|
58
|
+
}).pipe(Schema.mutable);
|
|
59
|
+
|
|
60
|
+
type PipelineValues = { name?: string; description?: string; columns: readonly unknown[] };
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
// Object-array columns (matches `Pipeline` / `OrderedPipeline`).
|
|
63
|
+
const objectColumns: PipelineValues = {
|
|
59
64
|
name: 'Sales',
|
|
60
65
|
description: 'Lead qualification pipeline.',
|
|
61
66
|
columns: [{ name: 'Contacts' }, { name: 'Organizations' }, { name: 'Tasks' }, { name: 'Messages', value: 100 }],
|
|
62
67
|
};
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
// String-array columns (matches `StringPipeline`); feeding object columns here renders `[object Object]`.
|
|
70
|
+
const stringColumns: PipelineValues = {
|
|
71
|
+
name: 'Sales',
|
|
72
|
+
description: 'Lead qualification pipeline.',
|
|
73
|
+
columns: ['Contacts', 'Organizations', 'Tasks', 'Messages'],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const DefaultStory = ({ schema, values: initial }: { schema: Schema.Schema<any>; values: PipelineValues }) => {
|
|
77
|
+
const [values, setValues] = useState<PipelineValues>(initial);
|
|
66
78
|
|
|
67
79
|
return (
|
|
68
80
|
<TestLayout json={values}>
|
|
69
81
|
<Form.Root
|
|
70
82
|
schema={schema}
|
|
71
83
|
values={values}
|
|
72
|
-
onValuesChanged={(
|
|
84
|
+
onValuesChanged={(next) => setValues((prev) => ({ ...prev, ...next }))}
|
|
73
85
|
>
|
|
74
86
|
<Form.Viewport scroll>
|
|
75
87
|
<Form.Content>
|
|
@@ -88,16 +100,20 @@ const meta = {
|
|
|
88
100
|
layout: 'fullscreen',
|
|
89
101
|
translations,
|
|
90
102
|
},
|
|
91
|
-
} satisfies Meta<typeof
|
|
103
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
92
104
|
|
|
93
105
|
export default meta;
|
|
94
106
|
|
|
95
107
|
type Story = StoryObj<typeof meta>;
|
|
96
108
|
|
|
97
109
|
export const Default: Story = {
|
|
98
|
-
render: () => <
|
|
110
|
+
render: () => <DefaultStory schema={Pipeline} values={objectColumns} />,
|
|
99
111
|
};
|
|
100
112
|
|
|
101
113
|
export const Ordered: Story = {
|
|
102
|
-
render: () => <
|
|
114
|
+
render: () => <DefaultStory schema={OrderedPipeline} values={objectColumns} />,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const Simple: Story = {
|
|
118
|
+
render: () => <DefaultStory schema={StringPipeline} values={stringColumns} />,
|
|
103
119
|
};
|
|
@@ -7,9 +7,13 @@ import * as SchemaAST from 'effect/SchemaAST';
|
|
|
7
7
|
import React, { type ReactNode, useCallback, useRef } from 'react';
|
|
8
8
|
|
|
9
9
|
import { Annotation, Ref } from '@dxos/echo';
|
|
10
|
+
import { useType as defaultUseType } from '@dxos/echo-react';
|
|
10
11
|
import { SchemaEx } from '@dxos/effect';
|
|
12
|
+
import { DXN } from '@dxos/keys';
|
|
13
|
+
import { log } from '@dxos/log';
|
|
11
14
|
import { useTranslation } from '@dxos/react-ui';
|
|
12
15
|
import { OrderedList } from '@dxos/react-ui-list';
|
|
16
|
+
import { mx } from '@dxos/ui-theme';
|
|
13
17
|
import { arrayMove } from '@dxos/util';
|
|
14
18
|
|
|
15
19
|
import { translationKey } from '#translations';
|
|
@@ -17,8 +21,8 @@ import { type FormFieldStateProps } from '#types';
|
|
|
17
21
|
|
|
18
22
|
import { useFormValues } from '../../../../../hooks';
|
|
19
23
|
import { getFormProperties } from '../../../../../util';
|
|
20
|
-
import { FieldHeader } from '../../FieldHeader';
|
|
21
24
|
import { CompactIconButton, FormField, type FormFieldProps } from '../../FormField';
|
|
25
|
+
import { FormFieldHeader } from '../../FormFieldHeader';
|
|
22
26
|
|
|
23
27
|
// Synthetic id assigned to each row when rendering an ordered list. Plain form
|
|
24
28
|
// values have no stable identity, so drag-and-drop (which requires a stable key
|
|
@@ -59,6 +63,14 @@ export const ArrayField = ({
|
|
|
59
63
|
// Order is meaningful and user-controllable -> render as a drag-to-reorder list.
|
|
60
64
|
const ordered = Annotation.FormOrderedAnnotation.getFromAst(type).pipe(Option.getOrElse(() => false));
|
|
61
65
|
|
|
66
|
+
// Owned-ref array (`FormCreateAnnotation`): "add" creates a new target object of the annotation's typename,
|
|
67
|
+
// and each row renders that target inline (no existing-object picker).
|
|
68
|
+
const { db, onCreate, getCreateDefaults, useType = defaultUseType } = props;
|
|
69
|
+
const createTypename = Annotation.FormCreateAnnotation.getFromAst(type).pipe(Option.getOrUndefined);
|
|
70
|
+
const createSchema = useType(db, createTypename ? DXN.make(createTypename) : undefined);
|
|
71
|
+
const createInline =
|
|
72
|
+
!!createTypename && !!elementType && Ref.isRefType(elementType) && !readonly && layout !== 'static';
|
|
73
|
+
|
|
62
74
|
// Synthetic identity map (see `DND_ID`). The counter only advances when the
|
|
63
75
|
// array grows; reconciliation here covers the initial mount and any external
|
|
64
76
|
// length change, while the handlers below keep ids aligned on mutation.
|
|
@@ -66,6 +78,21 @@ export const ArrayField = ({
|
|
|
66
78
|
const counterRef = useRef(0);
|
|
67
79
|
const nextId = useCallback(() => `${DND_ID}-${counterRef.current++}`, []);
|
|
68
80
|
|
|
81
|
+
// Create a new owned target object and append its ref (see `createInline`).
|
|
82
|
+
const handleAddCreate = useCallback(async () => {
|
|
83
|
+
if (!createSchema || !onCreate) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Let the container pre-populate the new object (e.g. a back-reference to the parent).
|
|
87
|
+
const defaults = getCreateDefaults?.({ jsonPath: SchemaEx.createJsonPath(path ?? []), schema: createSchema }) ?? {};
|
|
88
|
+
const created = await onCreate(createSchema, defaults);
|
|
89
|
+
if (!created) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
idsRef.current.push(nextId());
|
|
93
|
+
onValueChange(type, [...(values ?? []), Ref.make(created)]);
|
|
94
|
+
}, [createSchema, onCreate, getCreateDefaults, onValueChange, type, path, values, nextId]);
|
|
95
|
+
|
|
69
96
|
const getDefaultObjectValue = (typeNode: SchemaAST.AST): any => {
|
|
70
97
|
const baseNode = SchemaEx.findNode(typeNode, SchemaEx.isDiscriminatedUnion);
|
|
71
98
|
const typeLiteral = baseNode
|
|
@@ -86,10 +113,18 @@ export const ArrayField = ({
|
|
|
86
113
|
};
|
|
87
114
|
|
|
88
115
|
const handleAdd = useCallback(() => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
let defaultValue;
|
|
117
|
+
try {
|
|
118
|
+
defaultValue =
|
|
119
|
+
elementType && SchemaEx.isNestedType(elementType)
|
|
120
|
+
? getDefaultObjectValue(elementType)
|
|
121
|
+
: getDefaultValue(elementType);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// `getDefaultValue` throws on element types it can't seed (runs in this click handler, so an
|
|
124
|
+
// unguarded throw would be uncaught). Skip the add rather than crash the form.
|
|
125
|
+
log.catch(err);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
93
128
|
idsRef.current.push(nextId());
|
|
94
129
|
// `values` is `undefined` on first render for arrays whose parent path
|
|
95
130
|
// hasn't been materialised in the form values yet (e.g. `package.repos`
|
|
@@ -129,28 +164,46 @@ export const ArrayField = ({
|
|
|
129
164
|
|
|
130
165
|
// Object items: a recursively-rendered FormField (multiple sub-rows for the
|
|
131
166
|
// object's fields). Scalar items (refs, primitives): a single inline FormField.
|
|
132
|
-
const renderField = (index: number, isLast: boolean): ReactNode =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
const renderField = (index: number, isLast: boolean): ReactNode => {
|
|
168
|
+
const field = (
|
|
169
|
+
<FormField
|
|
170
|
+
{...props}
|
|
171
|
+
autoFocus={isLast}
|
|
172
|
+
type={elementType}
|
|
173
|
+
// Suppress the per-item header for object items and owned-ref items (both render their own
|
|
174
|
+
// sub-field labels). Scalar items (refs, primitives) take the array's resolved label (e.g. its
|
|
175
|
+
// `title` annotation, `Tags`) so inline-layout children (e.g. RefField) have a real label to use
|
|
176
|
+
// as a fallback placeholder, rather than re-deriving it from the raw array property name (`_tags`).
|
|
177
|
+
{...(renderItemAsObject || createInline ? { name: null } : { label })}
|
|
178
|
+
path={[...(path ?? []), index]}
|
|
179
|
+
readonly={readonly || layout === 'static'}
|
|
180
|
+
layout={renderItemAsObject ? (layout === 'static' ? 'static' : undefined) : 'inline'}
|
|
181
|
+
refInline={createInline || undefined}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// TODO(burdon): Hacky.
|
|
186
|
+
// An owned inline object form must establish its own column context: otherwise it inherits the parent
|
|
187
|
+
// form's `--dx-col` (center) placement and collapses into the array row's narrow second track.
|
|
188
|
+
return createInline ? (
|
|
189
|
+
<div role='none' className='w-full min-w-0 [--dx-col:auto]'>
|
|
190
|
+
{field}
|
|
191
|
+
</div>
|
|
192
|
+
) : (
|
|
193
|
+
field
|
|
194
|
+
);
|
|
195
|
+
};
|
|
147
196
|
|
|
148
197
|
const header = (layout !== 'static' || (values && values.length > 0)) && (
|
|
149
|
-
<
|
|
198
|
+
<FormFieldHeader
|
|
150
199
|
label={label}
|
|
151
200
|
path={SchemaEx.createJsonPath(path ?? [])}
|
|
152
201
|
readonly={readonly}
|
|
153
|
-
add={
|
|
202
|
+
add={
|
|
203
|
+
layout !== 'static'
|
|
204
|
+
? { label: t('add-item.button'), onClick: createInline ? () => void handleAddCreate() : handleAdd }
|
|
205
|
+
: undefined
|
|
206
|
+
}
|
|
154
207
|
/>
|
|
155
208
|
);
|
|
156
209
|
|
|
@@ -207,11 +260,17 @@ export const ArrayField = ({
|
|
|
207
260
|
return (
|
|
208
261
|
<>
|
|
209
262
|
{header}
|
|
210
|
-
<div className='flex flex-col'>
|
|
263
|
+
<div className='flex flex-col gap-2'>
|
|
211
264
|
{values?.map((_, index) => {
|
|
212
265
|
const isLast = index === values.length - 1;
|
|
213
266
|
return (
|
|
214
|
-
<div
|
|
267
|
+
<div
|
|
268
|
+
key={index}
|
|
269
|
+
className={mx(
|
|
270
|
+
'grid grid-cols-[minmax(0,1fr)_min-content] gap-1',
|
|
271
|
+
renderItemAsObject || createInline ? 'items-start' : 'items-center',
|
|
272
|
+
)}
|
|
273
|
+
>
|
|
215
274
|
{renderField(index, isLast)}
|
|
216
275
|
{!readonly && layout !== 'static' && (
|
|
217
276
|
<CompactIconButton
|