Skip to content

Commit e928dad

Browse files
committed
feat(ui): add single select component to form extension
Signed-off-by: Petr Bulánek <[email protected]>
1 parent 37afff1 commit e928dad

File tree

15 files changed

+161
-13
lines changed

15 files changed

+161
-13
lines changed

apps/beeai-sdk-py/examples/request_form_agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
FormRender,
1414
MultiSelectField,
1515
OptionItem,
16+
SingleSelectField,
1617
TextField,
1718
)
1819
from beeai_sdk.server import Server
@@ -45,6 +46,15 @@ async def request_form_agent(
4546
TextField(id="text_field", label="Text Field", col_span=1),
4647
DateField(id="date_field", label="Date Field", col_span=1),
4748
FileField(id="file_field", label="File Field", accept=["*/*"], col_span=2),
49+
SingleSelectField(
50+
id="singleselect_field",
51+
label="Single-Select Field",
52+
options=[
53+
OptionItem(id="option1", label="Option 1"),
54+
OptionItem(id="option2", label="Option 2"),
55+
],
56+
col_span=2,
57+
),
4858
MultiSelectField(
4959
id="multiselect_field",
5060
label="Multi-Select Field",

apps/beeai-sdk-py/src/beeai_sdk/a2a/extensions/ui/form.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ class OptionItem(BaseModel):
5252
label: str
5353

5454

55+
class SingleSelectField(BaseField):
56+
type: Literal["singleselect"] = "singleselect"
57+
options: list[OptionItem]
58+
default_value: str | None = None
59+
60+
@model_validator(mode="after")
61+
def default_value_validator(self):
62+
if self.default_value:
63+
valid_values = {opt.id for opt in self.options}
64+
if self.default_value not in valid_values:
65+
raise ValueError(f"Invalid default_value: {self.default_value}. Must be one of {valid_values}")
66+
return self
67+
68+
5569
class MultiSelectField(BaseField):
5670
type: Literal["multiselect"] = "multiselect"
5771
options: list[OptionItem]
@@ -73,7 +87,7 @@ class CheckboxField(BaseField):
7387
default_value: bool = False
7488

7589

76-
FormField = TextField | DateField | FileField | MultiSelectField | CheckboxField
90+
FormField = TextField | DateField | FileField | SingleSelectField | MultiSelectField | CheckboxField
7791

7892

7993
class FormRender(BaseModel):
@@ -106,6 +120,11 @@ class FileFieldValue(BaseModel):
106120
value: list[FileInfo] | None = None
107121

108122

123+
class SingleSelectFieldValue(BaseModel):
124+
type: Literal["singleselect"] = "singleselect"
125+
value: str | None = None
126+
127+
109128
class MultiSelectFieldValue(BaseModel):
110129
type: Literal["multiselect"] = "multiselect"
111130
value: list[str] | None = None
@@ -116,7 +135,14 @@ class CheckboxFieldValue(BaseModel):
116135
value: bool | None = None
117136

118137

119-
FormFieldValue = TextFieldValue | DateFieldValue | FileFieldValue | MultiSelectFieldValue | CheckboxFieldValue
138+
FormFieldValue = (
139+
TextFieldValue
140+
| DateFieldValue
141+
| FileFieldValue
142+
| SingleSelectFieldValue
143+
| MultiSelectFieldValue
144+
| CheckboxFieldValue
145+
)
120146

121147

122148
class FormResponse(BaseModel):

apps/beeai-sdk-ts/src/client/a2a/extensions/ui/form.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,24 @@ const fileFieldValue = z.object({
5757
.nullish(),
5858
});
5959

60+
const singleSelectField = baseField.extend({
61+
type: z.literal('singleselect'),
62+
options: z
63+
.array(
64+
z.object({
65+
id: z.string().nonempty(),
66+
label: z.string().nonempty(),
67+
}),
68+
)
69+
.nonempty(),
70+
default_value: z.string().nullish(),
71+
});
72+
73+
const singleSelectFieldValue = z.object({
74+
type: singleSelectField.shape.type,
75+
value: z.string().nullish(),
76+
});
77+
6078
const multiSelectField = baseField.extend({
6179
type: z.literal('multiselect'),
6280
options: z
@@ -86,7 +104,14 @@ const checkboxFieldValue = z.object({
86104
value: z.boolean().nullish(),
87105
});
88106

89-
const fieldSchema = z.discriminatedUnion('type', [textField, dateField, fileField, multiSelectField, checkboxField]);
107+
const fieldSchema = z.discriminatedUnion('type', [
108+
textField,
109+
dateField,
110+
fileField,
111+
singleSelectField,
112+
multiSelectField,
113+
checkboxField,
114+
]);
90115

91116
const renderSchema = z.object({
92117
id: z.string().nonempty(),
@@ -105,6 +130,7 @@ const responseSchema = z.object({
105130
textFieldValue,
106131
dateFieldValue,
107132
fileFieldValue,
133+
singleSelectFieldValue,
108134
multiSelectFieldValue,
109135
checkboxFieldValue,
110136
]),
@@ -114,6 +140,7 @@ const responseSchema = z.object({
114140
export type TextField = z.infer<typeof textField>;
115141
export type DateField = z.infer<typeof dateField>;
116142
export type FileField = z.infer<typeof fileField>;
143+
export type SingleSelectField = z.infer<typeof singleSelectField>;
117144
export type MultiSelectField = z.infer<typeof multiSelectField>;
118145
export type CheckboxField = z.infer<typeof checkboxField>;
119146

apps/beeai-ui/src/modules/agents/components/ImportAgentsModal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export function ImportAgentsModal({ onRequestClose, ...modalProps }: ModalProps)
107107
{actionField.value === 'update_provider' && providersToUpdate && (
108108
<Select
109109
id={`${id}:provider`}
110+
size="lg"
110111
labelText="Select agent to update"
111112
{...register('providerId', { required: true, disabled: isPending })}
112113
>

apps/beeai-ui/src/modules/form/components/FormField.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DateField } from './fields/DateField';
1212
import { FileField } from './fields/FileField';
1313
import { FileFieldValue } from './fields/FileFieldValue';
1414
import { MultiSelectField } from './fields/MultiSelectField';
15+
import { SingleSelectField } from './fields/SingleSelectField';
1516
import { TextField } from './fields/TextField';
1617
import classes from './FormField.module.scss';
1718

@@ -31,6 +32,7 @@ export function FormField({ field, value }: Props) {
3132
.with({ type: 'file', value: P.nonNullable }, ({ value }) => <FileFieldValue field={field} value={value} />)
3233
.otherwise(() => <FileField field={field} />),
3334
)
35+
.with({ type: 'singleselect' }, (field) => <SingleSelectField field={field} />)
3436
.with({ type: 'multiselect' }, (field) => <MultiSelectField field={field} />)
3537
.with({ type: 'checkbox' }, (field) => <CheckboxField field={field} />)
3638
.exhaustive();
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Select, SelectItem } from '@carbon/react';
7+
import type { SingleSelectField } from 'beeai-sdk';
8+
import { useFormContext } from 'react-hook-form';
9+
10+
import type { ValuesOfField } from '#modules/form/types.ts';
11+
12+
interface Props {
13+
field: SingleSelectField;
14+
}
15+
16+
export function SingleSelectField({ field }: Props) {
17+
const { id, label, required, options } = field;
18+
19+
const { register } = useFormContext<ValuesOfField<SingleSelectField>>();
20+
21+
const inputProps = register(`${id}.value`, { required: Boolean(required) });
22+
23+
return (
24+
<Select id={id} size="lg" labelText={label} {...inputProps}>
25+
{options.map(({ id, label }) => (
26+
<SelectItem key={id} text={label} value={id} />
27+
))}
28+
</Select>
29+
);
30+
}

apps/beeai-ui/src/modules/form/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function getDefaultValues(fields: FormField[]) {
1818
.with(
1919
{ type: 'text' },
2020
{ type: 'date' },
21+
{ type: 'singleselect' },
2122
{ type: 'multiselect' },
2223
{ type: 'checkbox' },
2324
({ id, type, default_value }) => [id, { type, value: default_value }],

apps/beeai-ui/src/modules/messages/components/MessageFormResponse.module.scss

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
color: $text-secondary;
2222
}
2323
textarea,
24-
:global(.cds--date-picker__input) {
24+
:global(.cds--date-picker__input),
25+
:global(.cds--select-input) {
2526
color: $text-primary;
2627
background-color: transparent;
2728
}

apps/beeai-ui/src/modules/messages/components/MessageFormResponse.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
*/
55

66
import { Button } from '@carbon/react';
7-
import type { CheckboxField, DateField, FileField, FormField, MultiSelectField, TextField } from 'beeai-sdk';
7+
import type {
8+
CheckboxField,
9+
DateField,
10+
FileField,
11+
FormField,
12+
MultiSelectField,
13+
SingleSelectField,
14+
TextField,
15+
} from 'beeai-sdk';
816
import { useMemo } from 'react';
917
import { FormProvider, useForm } from 'react-hook-form';
1018
import { match } from 'ts-pattern';
@@ -79,6 +87,7 @@ function FormValueRenderer({ field }: { field: FieldWithValue }) {
7987
{match(field)
8088
.with({ type: 'text' }, { type: 'date' }, ({ value }) => value)
8189
.with({ type: 'checkbox' }, ({ value }) => (value ? 'yes' : 'no'))
90+
.with({ type: 'singleselect' }, ({ value }) => value)
8291
.with({ type: 'multiselect' }, ({ value }) => value?.join(', '))
8392
.with({ type: 'file' }, ({ value }) => value?.map(({ name }) => name).join(', '))
8493
.otherwise(() => null)}
@@ -91,5 +100,6 @@ type FieldWithValue =
91100
| FieldWithValueMapper<TextField>
92101
| FieldWithValueMapper<DateField>
93102
| FieldWithValueMapper<CheckboxField>
103+
| FieldWithValueMapper<SingleSelectField>
94104
| FieldWithValueMapper<MultiSelectField>
95105
| FieldWithValueMapper<FileField>;

apps/beeai-ui/src/styles/components/_date-picker.scss

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
.cds--date-picker {
1717
.cds--date-picker__input {
18-
background-color: $background;
1918
&:disabled {
2019
background-color: $field;
2120
&,

0 commit comments

Comments
 (0)