Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,19 @@ export const checkboxFieldSchema = baseFieldSchema.extend({
default_value: z.boolean().nullish(),
});

export const checkboxGroupFieldSchema = baseFieldSchema.extend({
type: z.literal('checkbox_group'),
fields: z.array(checkboxFieldSchema),
});

export const formFieldSchema = z.discriminatedUnion('type', [
textFieldSchema,
dateFieldSchema,
fileFieldSchema,
singleSelectFieldSchema,
multiSelectFieldSchema,
checkboxFieldSchema,
checkboxGroupFieldSchema,
]);

export const textFieldValueSchema = z.object({
Expand Down Expand Up @@ -100,13 +106,19 @@ export const checkboxFieldValueSchema = z.object({
value: z.boolean().nullish(),
});

export const checkboxGroupFieldValueSchema = z.object({
type: checkboxGroupFieldSchema.shape.type,
value: z.record(z.string(), z.boolean().nullish()).nullish(),
});

export const formFieldValueSchema = z.discriminatedUnion('type', [
textFieldValueSchema,
dateFieldValueSchema,
fileFieldValueSchema,
singleSelectFieldValueSchema,
multiSelectFieldValueSchema,
checkboxFieldValueSchema,
checkboxGroupFieldValueSchema,
]);

export const formRenderSchema = z.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type z from 'zod';
import type {
checkboxFieldSchema,
checkboxFieldValueSchema,
checkboxGroupFieldSchema,
checkboxGroupFieldValueSchema,
dateFieldSchema,
dateFieldValueSchema,
fileFieldSchema,
Expand All @@ -33,6 +35,7 @@ export type SelectFieldOption = z.infer<typeof selectFieldOptionSchema>;
export type SingleSelectField = z.infer<typeof singleSelectFieldSchema>;
export type MultiSelectField = z.infer<typeof multiSelectFieldSchema>;
export type CheckboxField = z.infer<typeof checkboxFieldSchema>;
export type CheckboxGroupField = z.infer<typeof checkboxGroupFieldSchema>;

export type FormField = z.infer<typeof formFieldSchema>;

Expand All @@ -42,6 +45,7 @@ export type FileFieldValue = z.infer<typeof fileFieldValueSchema>;
export type SingleSelectFieldValue = z.infer<typeof singleSelectFieldValueSchema>;
export type MultiSelectFieldValue = z.infer<typeof multiSelectFieldValueSchema>;
export type CheckboxFieldValue = z.infer<typeof checkboxFieldValueSchema>;
export type CheckboxGroupFieldValue = z.infer<typeof checkboxGroupFieldValueSchema>;

export type FormFieldValue = z.infer<typeof formFieldValueSchema>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,51 @@

import z from 'zod';

import { formRenderSchema, formResponseSchema } from '../../common/form/schemas';
import {
checkboxGroupFieldSchema,
checkboxGroupFieldValueSchema,
formRenderSchema,
formResponseSchema,
singleSelectFieldSchema,
singleSelectFieldValueSchema,
} from '../../common/form/schemas';

export const settingsFormFieldSchema = z.discriminatedUnion('type', [
checkboxGroupFieldSchema,
singleSelectFieldSchema,
]);

export const settingsFormFieldValueSchema = z.discriminatedUnion('type', [
checkboxGroupFieldValueSchema,
singleSelectFieldValueSchema,
]);

export const settingsFormRenderSchema = formRenderSchema.extend({
fields: z.array(settingsFormFieldSchema).nonempty(),
});

export const settingsFormValuesSchema = z.record(z.string(), settingsFormFieldValueSchema);

export const settingsFormResponseSchema = formResponseSchema.extend({
values: settingsFormValuesSchema,
});

export const formDemandsSchema = z.object({
form_demands: z
.object({
initial_form: formRenderSchema,
settings_form: settingsFormRenderSchema,
})
.partial(),
.partial()
.catchall(formRenderSchema),
});

export const formFulfillmentsSchema = z.object({
form_fulfillments: z.record(z.string(), formResponseSchema),
form_fulfillments: z
.object({
initial_form: formResponseSchema,
settings_form: settingsFormResponseSchema,
})
.partial()
.catchall(formResponseSchema),
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@

import type z from 'zod';

import type { formDemandsSchema, formFulfillmentsSchema } from './schemas';
import type {
formDemandsSchema,
formFulfillmentsSchema,
settingsFormFieldSchema,
settingsFormFieldValueSchema,
settingsFormRenderSchema,
settingsFormResponseSchema,
settingsFormValuesSchema,
} from './schemas';

export type FormDemands = z.infer<typeof formDemandsSchema>;
export type SettingsFormField = z.infer<typeof settingsFormFieldSchema>;
export type SettingsFormFieldValue = z.infer<typeof settingsFormFieldValueSchema>;

export type SettingsFormRender = z.infer<typeof settingsFormRenderSchema>;
export type SettingsFormValues = z.infer<typeof settingsFormValuesSchema>;
export type SettingsFormResponse = z.infer<typeof settingsFormResponseSchema>;

export type FormDemands = z.infer<typeof formDemandsSchema>;
export type FormFulfillments = z.infer<typeof formFulfillmentsSchema>;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import type { SettingsDemands, SettingsFulfillments } from './types';

const URI = 'https://a2a-extensions.agentstack.beeai.dev/ui/settings/v1';

/**
* @deprecated Use the form extension with `form_demands.settings_form`.
*/
export const settingsExtension: A2AServiceExtension<typeof URI, SettingsDemands, SettingsFulfillments> = {
getUri: () => URI,
getDemandsSchema: () => settingsDemandsSchema,
Expand Down
3 changes: 3 additions & 0 deletions apps/agentstack-sdk-ts/src/client/core/extensions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export type Fulfillments = Partial<{
embedding: (demand: EmbeddingDemands) => Promise<EmbeddingFulfillments>;
mcp: (demand: MCPDemands) => Promise<MCPFulfillments>;
oauth: (demand: OAuthDemands) => Promise<OAuthFulfillments>;
/**
* @deprecated - keeping this for backwards compatibility, use form extension with "settings_form" demand instead
*/
settings: (demand: SettingsDemands) => Promise<SettingsFulfillments>;
secrets: (demand: SecretDemands) => Promise<SecretFulfillments>;
form: (demand: FormDemands) => Promise<FormFulfillments>;
Expand Down
2 changes: 2 additions & 0 deletions apps/agentstack-ui/src/modules/form/components/FormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { CSSProperties } from 'react';
import { match, P } from 'ts-pattern';

import { CheckboxField } from './fields/CheckboxField';
import { CheckboxGroupField } from './fields/CheckboxGroupField';
import { DateField } from './fields/DateField';
import { FileField } from './fields/FileField';
import { FileFieldValue } from './fields/FileFieldValue';
Expand Down Expand Up @@ -35,6 +36,7 @@ export function FormField({ field, value }: Props) {
.with({ type: 'singleselect' }, (field) => <SingleSelectField field={field} />)
.with({ type: 'multiselect' }, (field) => <MultiSelectField field={field} />)
.with({ type: 'checkbox' }, (field) => <CheckboxField field={field} />)
.with({ type: 'checkbox_group' }, (field) => <CheckboxGroupField field={field} />)
.exhaustive();

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

.root {
> :global(.cds--checkbox-group__validation-msg) {
margin-block-start: 0;
}

:global(.cds--checkbox-wrapper--invalid > .cds--checkbox__validation-msg) {
display: flex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { CheckboxGroup } from '@carbon/react';
import type { CheckboxGroupField, CheckboxGroupFieldValue } from 'agentstack-sdk';
import { useController, useFormContext } from 'react-hook-form';

import { REQUIRED_GROUP_ERROR_MESSAGE } from '#modules/form/constants.ts';
import { useFormFieldValidation } from '#modules/form/hooks/useFormFieldValidation.ts';
import type { ValuesOfField } from '#modules/form/types.ts';
import { getFieldName } from '#modules/form/utils.ts';

import classes from './CheckboxGroupField.module.scss';
import { CheckboxGroupFieldItem } from './CheckboxGroupFieldItem';

interface Props {
field: CheckboxGroupField;
}

export function CheckboxGroupField({ field }: Props) {
const { label, fields, required } = field;
const name = getFieldName(field);

const { control, formState } = useFormContext<ValuesOfField<CheckboxGroupField>>();
const {
rules,
invalid: invalidState,
invalidText,
} = useFormFieldValidation({
field,
formState,
rules: {
validate: (value: CheckboxGroupFieldValue['value']) => {
if (!required) {
return true;
}

return Object.values(value ?? {}).some(Boolean) || REQUIRED_GROUP_ERROR_MESSAGE;
},
},
});
const invalid = invalidState && Boolean(invalidText);

useController({ control, name, rules });

return (
<CheckboxGroup className={classes.root} legendText={label} invalid={invalid} invalidText={invalidText}>
{fields.map((itemField) => (
<CheckboxGroupFieldItem key={itemField.id} field={itemField} groupField={field} />
))}
</CheckboxGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright 2025 © BeeAI a Series of LF Projects, LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { Checkbox } from '@carbon/react';
import type { CheckboxField, CheckboxGroupField } from 'agentstack-sdk';
import { useFormContext } from 'react-hook-form';

import { useFormFieldValidation } from '#modules/form/hooks/useFormFieldValidation.ts';
import type { ValuesOfField } from '#modules/form/types.ts';
import { getFieldName } from '#modules/form/utils.ts';

interface Props {
field: CheckboxField;
groupField: CheckboxGroupField;
}

export function CheckboxGroupFieldItem({ field, groupField }: Props) {
const { id, content } = field;

const groupName = getFieldName(groupField);
const name = `${groupName}.${id}`;

const { register, formState } = useFormContext<ValuesOfField<CheckboxField>>();
const { rules, invalid, invalidText } = useFormFieldValidation({ field, formState, name });

const inputProps = register(name, {
...rules,
deps: groupName,
});

return <Checkbox id={name} labelText={content} invalid={invalid} invalidText={invalidText} {...inputProps} />;
}
2 changes: 2 additions & 0 deletions apps/agentstack-ui/src/modules/form/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
*/

export const REQUIRED_ERROR_MESSAGE = 'This field is required.';

export const REQUIRED_GROUP_ERROR_MESSAGE = 'Please select at least one option.';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import type { FormField } from 'agentstack-sdk';
import get from 'lodash/get';
import type { FormState } from 'react-hook-form';
import type { FormState, RegisterOptions } from 'react-hook-form';

import { REQUIRED_ERROR_MESSAGE } from '../constants';
import type { ValuesOfField } from '../types';
Expand All @@ -14,18 +14,24 @@ import { getFieldName } from '../utils';
export function useFormFieldValidation<F extends FormField>({
field,
formState,
name,
rules: customRules,
}: {
field: F;
formState: FormState<ValuesOfField<F>>;
name?: string;
rules?: RegisterOptions;
}) {
const { required } = field;

const error = get(formState.errors, getFieldName(field));
const fieldName = name ?? getFieldName(field);
const error = get(formState.errors, fieldName);
const invalid = Boolean(error);
const invalidText = error?.message;

const rules = {
required: Boolean(required) && REQUIRED_ERROR_MESSAGE,
...customRules,
};

return {
Expand Down
20 changes: 19 additions & 1 deletion apps/agentstack-ui/src/modules/form/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*/

import type { FormField } from 'agentstack-sdk';
import keyBy from 'lodash/keyBy';
import mapValues from 'lodash/mapValues';
import { match } from 'ts-pattern';

import { getFilePlatformUrl } from '#api/a2a/utils.ts';
Expand All @@ -21,8 +23,24 @@ export function getDefaultValues(fields: FormField[]) {
{ type: 'singleselect' },
{ type: 'multiselect' },
{ type: 'checkbox' },
({ id, type, default_value }) => [id, { type, value: default_value }],
({ id, type, default_value }) => [
id,
{
type,
value: default_value,
},
],
)
.with({ type: 'checkbox_group' }, ({ id, type, fields }) => [
id,
{
type,
value: mapValues(
keyBy(fields, ({ id }) => id),
({ default_value }) => default_value,
),
},
])
.otherwise(({ id, type }) => [id, { type }]),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Button } from '@carbon/react';
import type {
CheckboxField,
CheckboxGroupField,
DateField,
FileField,
FormField,
Expand Down Expand Up @@ -87,6 +88,13 @@ function FormValueRenderer({ field }: { field: FieldWithValue }) {
{match(field)
.with({ type: 'text' }, { type: 'date' }, ({ value }) => value)
.with({ type: 'checkbox' }, ({ value }) => (value ? 'yes' : 'no'))
.with({ type: 'checkbox_group' }, ({ value }) =>
value
? Object.entries(value)
.map(([id, value]) => `${id} - ${value ? 'yes' : 'no'}`)
.join(', ')
: null,
)
.with({ type: 'singleselect' }, ({ value }) => value)
.with({ type: 'multiselect' }, ({ value }) => value?.join(', '))
.with({ type: 'file' }, ({ value }) => value?.map(({ name }) => name).join(', '))
Expand All @@ -100,6 +108,7 @@ type FieldWithValue =
| FieldWithValueMapper<TextField>
| FieldWithValueMapper<DateField>
| FieldWithValueMapper<CheckboxField>
| FieldWithValueMapper<CheckboxGroupField>
| FieldWithValueMapper<SingleSelectField>
| FieldWithValueMapper<MultiSelectField>
| FieldWithValueMapper<FileField>;
Loading
Loading