Feat: Add a form with data operations operators #10427 (#11001)

### What problem does this PR solve?

Feat: Add a form with data operations operators #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-04 19:42:59 +08:00
committed by GitHub
parent 880a6a0428
commit db9fa3042b
16 changed files with 433 additions and 49 deletions

View File

@ -0,0 +1,46 @@
import { BlockButton, Button } from '@/components/ui/button';
import { X } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { JsonSchemaDataType } from '../../constant';
import { QueryVariable } from './query-variable';
type QueryVariableListProps = {
types?: JsonSchemaDataType[];
};
export function QueryVariableList({ types }: QueryVariableListProps) {
const { t } = useTranslation();
const form = useFormContext();
const name = 'inputs';
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
return (
<div className="space-y-5">
{fields.map((field, index) => {
const nameField = `${name}.${index}.input`;
return (
<div key={field.id} className="flex items-center gap-2">
<QueryVariable
name={nameField}
hideLabel
className="flex-1"
types={types}
></QueryVariable>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
<BlockButton onClick={() => append({ input: '' })}>
{t('common.add')}
</BlockButton>
</div>
);
}

View File

@ -5,24 +5,28 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { toLower } from 'lodash';
import { isEmpty, toLower } from 'lodash';
import { ReactNode, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { VariableType } from '../../constant';
import { JsonSchemaDataType } from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu';
type QueryVariableProps = {
name?: string;
type?: VariableType;
types?: JsonSchemaDataType[];
label?: ReactNode;
hideLabel?: boolean;
className?: string;
};
export function QueryVariable({
name = 'query',
type,
types = [],
label,
hideLabel = false,
className,
}: QueryVariableProps) {
const { t } = useTranslation();
const form = useFormContext();
@ -30,23 +34,25 @@ export function QueryVariable({
const nextOptions = useBuildQueryVariableOptions();
const finalOptions = useMemo(() => {
return type
return !isEmpty(types)
? nextOptions.map((x) => {
return {
...x,
options: x.options.filter((y) => toLower(y.type).includes(type)),
options: x.options.filter((y) =>
types?.some((x) => toLower(y.type).includes(x)),
),
};
})
: nextOptions;
}, [nextOptions, type]);
}, [nextOptions, types]);
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
{label || (
<FormItem className={className}>
{hideLabel || label || (
<FormLabel tooltip={t('flow.queryTip')}>
{t('flow.query')}
</FormLabel>
@ -56,7 +62,7 @@ export function QueryVariable({
options={finalOptions}
{...field}
// allowClear
type={type}
types={types}
></GroupedSelectWithSecondaryMenu>
</FormControl>
<FormMessage />

View File

@ -23,7 +23,7 @@ import { ChevronDownIcon, XIcon } from 'lucide-react';
import * as React from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { VariableType } from '../../constant';
import { JsonSchemaDataType } from '../../constant';
import {
useFindAgentStructuredOutputLabel,
useShowSecondaryMenu,
@ -52,7 +52,7 @@ interface GroupedSelectWithSecondaryMenuProps {
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
type?: VariableType;
types?: JsonSchemaDataType[];
}
export function GroupedSelectWithSecondaryMenu({
@ -60,7 +60,7 @@ export function GroupedSelectWithSecondaryMenu({
value,
onChange,
placeholder,
type,
types,
}: GroupedSelectWithSecondaryMenuProps) {
const { t } = useTranslation();
const [open, setOpen] = React.useState(false);
@ -157,7 +157,7 @@ export function GroupedSelectWithSecondaryMenu({
key={option.value}
data={option}
click={handleSecondaryMenuClick}
type={type}
types={types}
></StructuredOutputSecondaryMenu>
);
}

View File

@ -8,7 +8,7 @@ import { get, isEmpty, isPlainObject } from 'lodash';
import { ChevronRight } from 'lucide-react';
import { PropsWithChildren, ReactNode, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { JsonSchemaDataType, VariableType } from '../../constant';
import { JsonSchemaDataType } from '../../constant';
import { useGetStructuredOutputByValue } from '../../hooks/use-build-structured-output';
import {
hasJsonSchemaChild,
@ -20,13 +20,13 @@ type DataItem = { label: ReactNode; value: string; parentLabel?: ReactNode };
type StructuredOutputSecondaryMenuProps = {
data: DataItem;
click(option: { label: ReactNode; value: string }): void;
type?: VariableType | JsonSchemaDataType;
types?: JsonSchemaDataType[];
} & PropsWithChildren;
export function StructuredOutputSecondaryMenu({
data,
click,
type,
types = [],
}: StructuredOutputSecondaryMenuProps) {
const { t } = useTranslation();
const filterStructuredOutput = useGetStructuredOutputByValue();
@ -35,18 +35,21 @@ export function StructuredOutputSecondaryMenu({
const handleSubMenuClick = useCallback(
(option: { label: ReactNode; value: string }, dataType?: string) => () => {
// The query variable of the iteration operator can only select array type data.
if ((type && type === dataType) || !type) {
if (
(!isEmpty(types) && types?.some((x) => x === dataType)) ||
isEmpty(types)
) {
click(option);
}
},
[click, type],
[click, types],
);
const handleMenuClick = useCallback(() => {
if (isEmpty(type) || type === JsonSchemaDataType.Object) {
if (isEmpty(types) || types?.some((x) => x === JsonSchemaDataType.Object)) {
click(data);
}
}, [click, data, type]);
}, [click, data, types]);
const renderAgentStructuredOutput = useCallback(
(values: any, option: { label: ReactNode; value: string }) => {
@ -62,10 +65,10 @@ export function StructuredOutputSecondaryMenu({
const dataType = get(value, 'type');
if (
!type ||
(type &&
(dataType === type ||
hasSpecificTypeChild(value ?? {}, type)))
isEmpty(types) ||
(!isEmpty(types) &&
(types?.some((x) => x === dataType) ||
hasSpecificTypeChild(value ?? {}, types)))
) {
return (
<li key={key} className="pl-1">
@ -90,10 +93,13 @@ export function StructuredOutputSecondaryMenu({
return <div></div>;
},
[handleSubMenuClick, type],
[handleSubMenuClick, types],
);
if (!hasJsonSchemaChild(structuredOutput)) {
if (
!hasJsonSchemaChild(structuredOutput) ||
(!isEmpty(types) && !hasSpecificTypeChild(structuredOutput, types))
) {
return null;
}