From 0b7b88592f12724cd71dec90557dc388f07a1b5b Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Thu, 6 Nov 2025 20:07:38 +0800 Subject: [PATCH] Fix: Improve some functional issues with the data source. #10703 (#11081) ### What problem does this PR solve? Fix: Improve some functional issues with the data source. #10703 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- web/src/assets/svg/data-source/confluence.svg | 28 + web/src/components/dynamic-form.tsx | 625 ++++++++++++++++++ web/src/components/ui/input.tsx | 39 +- web/src/locales/en.ts | 5 + web/src/locales/zh.ts | 5 + .../dataset/dataset-overview/interface.ts | 2 +- .../dataset-overview/overview-table.tsx | 38 +- .../components/added-source-card.tsx | 2 +- .../components/link-data-source.tsx | 12 +- .../pages/dataset/dataset-setting/hooks.ts | 1 + .../pages/dataset/dataset-setting/index.tsx | 2 +- .../dataset/use-dataset-table-columns.tsx | 14 + .../data-source/add-datasource-modal.tsx | 9 +- .../component/added-source-card.tsx | 23 +- .../component/delete-source-modal.tsx | 80 +++ .../user-setting/data-source/contant.tsx | 65 +- .../data-source-detail-page/index.tsx | 14 +- .../data-source-detail-page/log-table.tsx | 2 +- .../pages/user-setting/data-source/index.tsx | 50 +- 19 files changed, 937 insertions(+), 79 deletions(-) create mode 100644 web/src/assets/svg/data-source/confluence.svg create mode 100644 web/src/components/dynamic-form.tsx create mode 100644 web/src/pages/user-setting/data-source/component/delete-source-modal.tsx diff --git a/web/src/assets/svg/data-source/confluence.svg b/web/src/assets/svg/data-source/confluence.svg new file mode 100644 index 000000000..91c9d12be --- /dev/null +++ b/web/src/assets/svg/data-source/confluence.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx new file mode 100644 index 000000000..e770e0d44 --- /dev/null +++ b/web/src/components/dynamic-form.tsx @@ -0,0 +1,625 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'; +import { + DefaultValues, + FieldValues, + SubmitHandler, + useForm, + useFormContext, +} from 'react-hook-form'; +import { ZodSchema, z } from 'zod'; + +import EditTag from '@/components/edit-tag'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { cn } from '@/lib/utils'; +import { t } from 'i18next'; +import { Loader } from 'lucide-react'; + +// Field type enumeration +export enum FormFieldType { + Text = 'text', + Email = 'email', + Password = 'password', + Number = 'number', + Textarea = 'textarea', + Select = 'select', + Checkbox = 'checkbox', + Tag = 'tag', +} + +// Field configuration interface +export interface FormFieldConfig { + name: string; + label: string; + type: FormFieldType; + hidden?: boolean; + required?: boolean; + placeholder?: string; + options?: { label: string; value: string }[]; + defaultValue?: any; + validation?: { + pattern?: RegExp; + minLength?: number; + maxLength?: number; + min?: number; + max?: number; + message?: string; + }; + render?: (fieldProps: any) => React.ReactNode; + horizontal?: boolean; + onChange?: (value: any) => void; + tooltip?: React.ReactNode; +} + +// Component props interface +interface DynamicFormProps { + fields: FormFieldConfig[]; + onSubmit: SubmitHandler; + className?: string; + children?: React.ReactNode; + defaultValues?: DefaultValues; +} + +// Form ref interface +export interface DynamicFormRef { + submit: () => void; + getValues: () => any; + reset: (values?: any) => void; +} + +// Generate Zod validation schema based on field configurations +const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { + const schema: Record = {}; + const nestedSchemas: Record> = {}; + + fields.forEach((field) => { + let fieldSchema: ZodSchema; + + // Create base validation schema based on field type + switch (field.type) { + case FormFieldType.Email: + fieldSchema = z.string().email('Please enter a valid email address'); + break; + case FormFieldType.Number: + fieldSchema = z.coerce.number(); + if (field.validation?.min !== undefined) { + fieldSchema = (fieldSchema as z.ZodNumber).min( + field.validation.min, + field.validation.message || + `Value cannot be less than ${field.validation.min}`, + ); + } + if (field.validation?.max !== undefined) { + fieldSchema = (fieldSchema as z.ZodNumber).max( + field.validation.max, + field.validation.message || + `Value cannot be greater than ${field.validation.max}`, + ); + } + break; + case FormFieldType.Checkbox: + fieldSchema = z.boolean(); + break; + case FormFieldType.Tag: + fieldSchema = z.array(z.string()); + break; + default: + fieldSchema = z.string(); + break; + } + + // Handle required fields + if (field.required) { + if (field.type === FormFieldType.Checkbox) { + fieldSchema = (fieldSchema as z.ZodBoolean).refine( + (val) => val === true, + { + message: `${field.label} is required`, + }, + ); + } else if (field.type === FormFieldType.Tag) { + fieldSchema = (fieldSchema as z.ZodArray).min(1, { + message: `${field.label} is required`, + }); + } else { + fieldSchema = (fieldSchema as z.ZodString).min(1, { + message: `${field.label} is required`, + }); + } + } + + if (!field.required) { + fieldSchema = fieldSchema.optional(); + } + + // Handle other validation rules + if ( + field.type !== FormFieldType.Number && + field.type !== FormFieldType.Checkbox && + field.type !== FormFieldType.Tag && + field.required + ) { + fieldSchema = fieldSchema as z.ZodString; + + if (field.validation?.minLength !== undefined) { + fieldSchema = (fieldSchema as z.ZodString).min( + field.validation.minLength, + field.validation.message || + `Enter at least ${field.validation.minLength} characters`, + ); + } + + if (field.validation?.maxLength !== undefined) { + fieldSchema = (fieldSchema as z.ZodString).max( + field.validation.maxLength, + field.validation.message || + `Enter up to ${field.validation.maxLength} characters`, + ); + } + + if (field.validation?.pattern) { + fieldSchema = (fieldSchema as z.ZodString).regex( + field.validation.pattern, + field.validation.message || 'Invalid input format', + ); + } + } + + if (field.name.includes('.')) { + const keys = field.name.split('.'); + const firstKey = keys[0]; + + if (!nestedSchemas[firstKey]) { + nestedSchemas[firstKey] = {}; + } + + let currentSchema = nestedSchemas[firstKey]; + for (let i = 1; i < keys.length - 1; i++) { + const key = keys[i]; + if (!currentSchema[key]) { + currentSchema[key] = {}; + } + currentSchema = currentSchema[key]; + } + + const lastKey = keys[keys.length - 1]; + currentSchema[lastKey] = fieldSchema; + } else { + schema[field.name] = fieldSchema; + } + }); + + Object.keys(nestedSchemas).forEach((key) => { + const buildNestedSchema = (obj: Record): ZodSchema => { + const nestedSchema: Record = {}; + Object.keys(obj).forEach((subKey) => { + if ( + typeof obj[subKey] === 'object' && + !(obj[subKey] instanceof z.ZodType) + ) { + nestedSchema[subKey] = buildNestedSchema(obj[subKey]); + } else { + nestedSchema[subKey] = obj[subKey]; + } + }); + return z.object(nestedSchema); + }; + + schema[key] = buildNestedSchema(nestedSchemas[key]); + }); + return z.object(schema); +}; + +// Generate default values based on field configurations +const generateDefaultValues = ( + fields: FormFieldConfig[], +): DefaultValues => { + const defaultValues: Record = {}; + + fields.forEach((field) => { + if (field.name.includes('.')) { + const keys = field.name.split('.'); + let current = defaultValues; + + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (!current[key]) { + current[key] = {}; + } + current = current[key]; + } + + const lastKey = keys[keys.length - 1]; + if (field.defaultValue !== undefined) { + current[lastKey] = field.defaultValue; + } else if (field.type === FormFieldType.Checkbox) { + current[lastKey] = false; + } else if (field.type === FormFieldType.Tag) { + current[lastKey] = []; + } else { + current[lastKey] = ''; + } + } else { + if (field.defaultValue !== undefined) { + defaultValues[field.name] = field.defaultValue; + } else if (field.type === FormFieldType.Checkbox) { + defaultValues[field.name] = false; + } else if (field.type === FormFieldType.Tag) { + defaultValues[field.name] = []; + } else { + defaultValues[field.name] = ''; + } + } + }); + + return defaultValues as DefaultValues; +}; + +// Dynamic form component +const DynamicForm = { + Root: forwardRef( + ( + { + fields, + onSubmit, + className = '', + children, + defaultValues: formDefaultValues = {} as DefaultValues, + }: DynamicFormProps, + ref: React.Ref, + ) => { + // Generate validation schema and default values + const schema = useMemo(() => generateSchema(fields), [fields]); + + const defaultValues = useMemo(() => { + const value = { + ...generateDefaultValues(fields), + ...formDefaultValues, + }; + return value; + }, [fields, formDefaultValues]); + + // Initialize form + const form = useForm({ + resolver: zodResolver(schema), + defaultValues, + }); + + // Expose form methods via ref + useImperativeHandle(ref, () => ({ + submit: () => form.handleSubmit(onSubmit)(), + getValues: () => form.getValues(), + reset: (values?: T) => { + if (values) { + form.reset(values); + } else { + form.reset(); + } + }, + setError: form.setError, + clearErrors: form.clearErrors, + trigger: form.trigger, + })); + + useEffect(() => { + if (formDefaultValues && Object.keys(formDefaultValues).length > 0) { + form.reset({ + ...generateDefaultValues(fields), + ...formDefaultValues, + }); + } + }, [form, formDefaultValues, fields]); + + // Submit handler + // const handleSubmit = form.handleSubmit(onSubmit); + + // Render form fields + const renderField = (field: FormFieldConfig) => { + if (field.render) { + return ( + + {(fieldProps) => { + const finalFieldProps = field.onChange + ? { + ...fieldProps, + onChange: (e: any) => { + fieldProps.onChange(e); + field.onChange?.(e.target?.value ?? e); + }, + } + : fieldProps; + return field.render?.(finalFieldProps); + }} + + ); + } + switch (field.type) { + case FormFieldType.Textarea: + return ( + + {(fieldProps) => { + const finalFieldProps = field.onChange + ? { + ...fieldProps, + onChange: (e: any) => { + fieldProps.onChange(e); + field.onChange?.(e.target.value); + }, + } + : fieldProps; + return ( +