Feat: Submit clean data operations form data to the backend. #10427 (#11030)

### What problem does this PR solve?

Feat: Submit clean data operations form data to the backend. #10427

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-11-05 17:32:35 +08:00
committed by GitHub
parent 24335485bf
commit 4e76220e25
18 changed files with 268 additions and 155 deletions

View File

@ -0,0 +1,25 @@
import { Button } from '@/components/ui/button';
import { FormLabel } from '@/components/ui/form';
import { Plus } from 'lucide-react';
import { ReactNode } from 'react';
export type FormListHeaderProps = {
label: ReactNode;
tooltip?: string;
onClick?: () => void;
};
export function DynamicFormHeader({
label,
tooltip,
onClick,
}: FormListHeaderProps) {
return (
<div className="flex items-center justify-between">
<FormLabel tooltip={tooltip}>{label}</FormLabel>
<Button variant={'ghost'} type="button" onClick={onClick}>
<Plus />
</Button>
</div>
);
}

View File

@ -1,4 +1,7 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Input } from '@/components/ui/input';
import { t } from 'i18next';
import { z } from 'zod';
export type OutputType = {
title: string;
@ -7,6 +10,7 @@ export type OutputType = {
type OutputProps = {
list: Array<OutputType>;
isFormRequired?: boolean;
};
export function transferOutputs(outputs: Record<string, any>) {
@ -16,7 +20,11 @@ export function transferOutputs(outputs: Record<string, any>) {
}));
}
export function Output({ list }: OutputProps) {
export const OutputSchema = {
outputs: z.record(z.any()),
};
export function Output({ list, isFormRequired = false }: OutputProps) {
return (
<section className="space-y-2">
<div className="text-sm">{t('flow.output')}</div>
@ -30,6 +38,11 @@ export function Output({ list }: OutputProps) {
</li>
))}
</ul>
{isFormRequired && (
<RAGFlowFormItem name="outputs" className="hidden">
<Input></Input>
</RAGFlowFormItem>
)}
</section>
);
}

View File

@ -1,17 +1,20 @@
import { BlockButton, Button } from '@/components/ui/button';
import { 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 { DynamicFormHeader, FormListHeaderProps } from './dynamic-fom-header';
import { QueryVariable } from './query-variable';
type QueryVariableListProps = {
types?: JsonSchemaDataType[];
};
export function QueryVariableList({ types }: QueryVariableListProps) {
const { t } = useTranslation();
} & FormListHeaderProps;
export function QueryVariableList({
types,
label,
tooltip,
}: QueryVariableListProps) {
const form = useFormContext();
const name = 'inputs';
const name = 'query';
const { fields, remove, append } = useFieldArray({
name: name,
@ -19,28 +22,31 @@ export function QueryVariableList({ types }: QueryVariableListProps) {
});
return (
<div className="space-y-5">
{fields.map((field, index) => {
const nameField = `${name}.${index}.input`;
<section className="space-y-2">
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() => append({ input: '' })}
></DynamicFormHeader>
<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>
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>
);
})}
</div>
</section>
);
}

View File

@ -1,13 +1,14 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { FormLabel } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DataOperationsOperatorOptions } from '../../constant';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
type SelectKeysProps = {
name: string;
@ -25,7 +26,6 @@ export function FilterValues({
valueField = 'value',
operatorField = 'operator',
}: SelectKeysProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
@ -33,9 +33,18 @@ export function FilterValues({
control: form.control,
});
const operatorOptions = useBuildSwitchOperatorOptions(
DataOperationsOperatorOptions,
);
return (
<section className="space-y-2">
<FormLabel tooltip={tooltip}>{label}</FormLabel>
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() => append({ [keyField]: '', [valueField]: '' })}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const keyFieldAlias = `${name}.${index}.${keyField}`;
@ -47,12 +56,15 @@ export function FilterValues({
<RAGFlowFormItem name={keyFieldAlias} className="flex-1">
<Input></Input>
</RAGFlowFormItem>
<Separator orientation="vertical" className="h-2.5" />
<Separator className="w-2" />
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
<SelectWithSearch {...field} options={[]}></SelectWithSearch>
<SelectWithSearch
{...field}
options={operatorOptions}
></SelectWithSearch>
</RAGFlowFormItem>
<Separator orientation="vertical" className="h-2.5" />
<Separator className="w-2" />
<RAGFlowFormItem name={valueFieldAlias} className="flex-1">
<Input></Input>
@ -64,10 +76,6 @@ export function FilterValues({
);
})}
</div>
<BlockButton onClick={() => append({ [keyField]: '', [valueField]: '' })}>
{t('common.add')}
</BlockButton>
</section>
);
}

View File

@ -1,6 +1,7 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Form, FormLabel } from '@/components/ui/form';
import { Form } from '@/components/ui/form';
import { Separator } from '@/components/ui/separator';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
@ -17,14 +18,14 @@ import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { Output, OutputSchema } from '../components/output';
import { QueryVariableList } from '../components/query-variable-list';
import { FilterValues } from './filter-values';
import { SelectKeys } from './select-keys';
import { Updates } from './updates';
export const RetrievalPartialSchema = {
inputs: z.array(z.object({ input: z.string().optional() })),
query: z.array(z.object({ input: z.string().optional() })),
operations: z.string(),
select_keys: z.array(z.object({ name: z.string().optional() })).optional(),
remove_keys: z.array(z.object({ name: z.string().optional() })).optional(),
@ -50,6 +51,7 @@ export const RetrievalPartialSchema = {
}),
)
.optional(),
...OutputSchema,
};
export const FormSchema = z.object(RetrievalPartialSchema);
@ -65,6 +67,7 @@ function DataOperationsForm({ node }: INextOperatorForm) {
const form = useForm<DataOperationsFormSchemaType>({
defaultValues: defaultValues,
mode: 'onChange',
resolver: zodResolver(FormSchema),
shouldUnregister: true,
});
@ -78,17 +81,17 @@ function DataOperationsForm({ node }: INextOperatorForm) {
true,
);
useWatchFormChange(node?.id, form);
useWatchFormChange(node?.id, form, true);
return (
<Form {...form}>
<FormWrapper>
<div className="space-y-2">
<FormLabel tooltip={t('flow.queryTip')}>{t('flow.query')}</FormLabel>
<QueryVariableList
types={[JsonSchemaDataType.Array, JsonSchemaDataType.Object]}
></QueryVariableList>
</div>
<QueryVariableList
tooltip={t('flow.queryTip')}
label={t('flow.query')}
types={[JsonSchemaDataType.Array, JsonSchemaDataType.Object]}
></QueryVariableList>
<Separator />
<RAGFlowFormItem name="operations" label={t('flow.operations')}>
<SelectWithSearch options={OperationsOptions} allowClear />
</RAGFlowFormItem>
@ -107,7 +110,7 @@ function DataOperationsForm({ node }: INextOperatorForm) {
{operations === Operations.AppendOrUpdate && (
<Updates
name="updates"
label={t('flow.operationsOptions.updates')}
label={t('flow.operationsOptions.appendOrUpdate')}
keyField="key"
valueField="value"
></Updates>
@ -126,7 +129,7 @@ function DataOperationsForm({ node }: INextOperatorForm) {
label={t('flow.operationsOptions.filterValues')}
></FilterValues>
)}
<Output list={outputList}></Output>
<Output list={outputList} isFormRequired></Output>
</FormWrapper>
</Form>
);

View File

@ -1,11 +1,10 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { FormLabel } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
type SelectKeysProps = {
name: string;
@ -13,7 +12,6 @@ type SelectKeysProps = {
tooltip?: string;
};
export function SelectKeys({ name, label, tooltip }: SelectKeysProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
@ -23,7 +21,11 @@ export function SelectKeys({ name, label, tooltip }: SelectKeysProps) {
return (
<section className="space-y-2">
<FormLabel tooltip={tooltip}>{label}</FormLabel>
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() => append({ name: '' })}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const nameField = `${name}.${index}.name`;
@ -40,10 +42,6 @@ export function SelectKeys({ name, label, tooltip }: SelectKeysProps) {
);
})}
</div>
<BlockButton onClick={() => append({ name: '' })}>
{t('common.add')}
</BlockButton>
</section>
);
}

View File

@ -1,11 +1,11 @@
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { BlockButton, Button } from '@/components/ui/button';
import { FormLabel } from '@/components/ui/form';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DynamicFormHeader } from '../components/dynamic-fom-header';
type SelectKeysProps = {
name: string;
@ -21,7 +21,6 @@ export function Updates({
keyField,
valueField,
}: SelectKeysProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
@ -31,7 +30,11 @@ export function Updates({
return (
<section className="space-y-2">
<FormLabel tooltip={tooltip}>{label}</FormLabel>
<DynamicFormHeader
label={label}
tooltip={tooltip}
onClick={() => append({ [keyField]: '', [valueField]: '' })}
></DynamicFormHeader>
<div className="space-y-5">
{fields.map((field, index) => {
const keyFieldAlias = `${name}.${index}.${keyField}`;
@ -42,6 +45,7 @@ export function Updates({
<RAGFlowFormItem name={keyFieldAlias} className="flex-1">
<Input></Input>
</RAGFlowFormItem>
<Separator className="w-2" />
<RAGFlowFormItem name={valueFieldAlias} className="flex-1">
<Input></Input>
</RAGFlowFormItem>
@ -52,10 +56,6 @@ export function Updates({
);
})}
</div>
<BlockButton onClick={() => append({ [keyField]: '', [valueField]: '' })}>
{t('common.add')}
</BlockButton>
</section>
);
}

View File

@ -1,5 +1,4 @@
import { FormContainer } from '@/components/form-container';
import { IconFont } from '@/components/icon-font';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
@ -13,6 +12,7 @@ import {
import { RAGFlowSelect } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
@ -22,11 +22,7 @@ import { memo, useCallback, useMemo } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
SwitchLogicOperatorOptions,
SwitchOperatorOptions,
VariableType,
} from '../../constant';
import { SwitchLogicOperatorOptions, VariableType } from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
import { IOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
@ -43,42 +39,6 @@ type ConditionCardsProps = {
parentLength: number;
} & IOperatorForm;
export const LogicalOperatorIcon = function OperatorIcon({
icon,
value,
}: Omit<(typeof SwitchOperatorOptions)[0], 'label'>) {
if (typeof icon === 'string') {
return (
<IconFont
name={icon}
className={cn('size-4', {
'rotate-180': value === '>',
})}
></IconFont>
);
}
return icon;
};
export function useBuildSwitchOperatorOptions() {
const { t } = useTranslation();
const switchOperatorOptions = useMemo(() => {
return SwitchOperatorOptions.map((x) => ({
value: x.value,
icon: (
<LogicalOperatorIcon
icon={x.icon}
value={x.value}
></LogicalOperatorIcon>
),
label: t(`flow.switchOperatorOptions.${x.label}`),
}));
}, [t]);
return switchOperatorOptions;
}
function ConditionCards({
name: parentName,
parentIndex,