### What problem does this PR solve? Feat: Refactor BeginForm with shadcn #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.x
| const FormConfigMap = { | const FormConfigMap = { | ||||
| [Operator.Begin]: { | [Operator.Begin]: { | ||||
| component: BeginForm, | component: BeginForm, | ||||
| defaultValues: {}, | |||||
| defaultValues: { | |||||
| prologue: t('chat.setAnOpenerInitial'), | |||||
| }, | |||||
| schema: z.object({ | schema: z.object({ | ||||
| name: z | |||||
| .string() | |||||
| .min(1, { | |||||
| message: t('common.namePlaceholder'), | |||||
| }) | |||||
| .trim(), | |||||
| age: z | |||||
| prologue: z | |||||
| .string() | .string() | ||||
| .min(1, { | .min(1, { | ||||
| message: t('common.namePlaceholder'), | message: t('common.namePlaceholder'), | ||||
| }) | }) | ||||
| .trim(), | .trim(), | ||||
| query: z.array( | |||||
| z.object({ | |||||
| key: z.string(), | |||||
| type: z.string(), | |||||
| value: z.string(), | |||||
| optional: z.boolean(), | |||||
| name: z.string(), | |||||
| options: z.array(z.union([z.number(), z.string(), z.boolean()])), | |||||
| }), | |||||
| ), | |||||
| }), | }), | ||||
| }, | }, | ||||
| [Operator.Retrieval]: { | [Operator.Retrieval]: { |
| import { useSetModalState } from '@/hooks/common-hooks'; | import { useSetModalState } from '@/hooks/common-hooks'; | ||||
| import { useSetSelectedRecord } from '@/hooks/logic-hooks'; | import { useSetSelectedRecord } from '@/hooks/logic-hooks'; | ||||
| import { useCallback, useMemo, useState } from 'react'; | import { useCallback, useMemo, useState } from 'react'; | ||||
| import { BeginQuery, IOperatorForm } from '../../interface'; | |||||
| import { BeginQuery, INextOperatorForm } from '../../interface'; | |||||
| export const useEditQueryRecord = ({ form, onValuesChange }: IOperatorForm) => { | |||||
| export const useEditQueryRecord = ({ form }: INextOperatorForm) => { | |||||
| const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>(); | const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>(); | ||||
| const { visible, hideModal, showModal } = useSetModalState(); | const { visible, hideModal, showModal } = useSetModalState(); | ||||
| const [index, setIndex] = useState(-1); | const [index, setIndex] = useState(-1); | ||||
| const otherThanCurrentQuery = useMemo(() => { | const otherThanCurrentQuery = useMemo(() => { | ||||
| const query: BeginQuery[] = form?.getFieldValue('query') || []; | |||||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||||
| return query.filter((item, idx) => idx !== index); | return query.filter((item, idx) => idx !== index); | ||||
| }, [form, index]); | }, [form, index]); | ||||
| const handleEditRecord = useCallback( | const handleEditRecord = useCallback( | ||||
| (record: BeginQuery) => { | (record: BeginQuery) => { | ||||
| const query: BeginQuery[] = form?.getFieldValue('query') || []; | |||||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||||
| const nextQuery: BeginQuery[] = | const nextQuery: BeginQuery[] = | ||||
| index > -1 ? query.toSpliced(index, 1, record) : [...query, record]; | index > -1 ? query.toSpliced(index, 1, record) : [...query, record]; | ||||
| onValuesChange?.( | |||||
| { query: nextQuery }, | |||||
| { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||||
| ); | |||||
| // onValuesChange?.( | |||||
| // { query: nextQuery }, | |||||
| // { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||||
| // ); | |||||
| hideModal(); | hideModal(); | ||||
| }, | }, | ||||
| [form, hideModal, index, onValuesChange], | |||||
| [form, hideModal, index], | |||||
| ); | ); | ||||
| const handleShowModal = useCallback( | const handleShowModal = useCallback( |
| .dynamicInputVariable { | |||||
| background-color: #ebe9e950; | |||||
| :global(.ant-collapse-content) { | |||||
| background-color: #f6f6f657; | |||||
| } | |||||
| :global(.ant-collapse-content-box) { | |||||
| padding: 0 !important; | |||||
| } | |||||
| margin-bottom: 20px; | |||||
| .title { | |||||
| font-weight: 600; | |||||
| font-size: 16px; | |||||
| } | |||||
| .addButton { | |||||
| color: rgb(22, 119, 255); | |||||
| font-weight: 600; | |||||
| } | |||||
| } | |||||
| .addButton { | |||||
| color: rgb(22, 119, 255); | |||||
| font-weight: 600; | |||||
| } |
| import { PlusOutlined } from '@ant-design/icons'; | |||||
| import { Button, Form, Input } from 'antd'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Textarea } from '@/components/ui/textarea'; | |||||
| import { useCallback } from 'react'; | import { useCallback } from 'react'; | ||||
| import { useWatch } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { BeginQuery, IOperatorForm } from '../../interface'; | |||||
| import { BeginQuery, INextOperatorForm } from '../../interface'; | |||||
| import { useEditQueryRecord } from './hooks'; | import { useEditQueryRecord } from './hooks'; | ||||
| import { ModalForm } from './paramater-modal'; | |||||
| import { ParameterDialog } from './next-paramater-modal'; | |||||
| import QueryTable from './query-table'; | import QueryTable from './query-table'; | ||||
| import styles from './index.less'; | |||||
| const BeginForm = ({ form }: INextOperatorForm) => { | |||||
| const { t } = useTranslation(); | |||||
| type FieldType = { | |||||
| prologue?: string; | |||||
| }; | |||||
| const query = useWatch({ control: form.control, name: 'query' }); | |||||
| const BeginForm = ({ onValuesChange, form }: IOperatorForm) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | const { | ||||
| ok, | ok, | ||||
| currentRecord, | currentRecord, | ||||
| otherThanCurrentQuery, | otherThanCurrentQuery, | ||||
| } = useEditQueryRecord({ | } = useEditQueryRecord({ | ||||
| form, | form, | ||||
| onValuesChange, | |||||
| }); | }); | ||||
| const handleDeleteRecord = useCallback( | const handleDeleteRecord = useCallback( | ||||
| (idx: number) => { | (idx: number) => { | ||||
| const query = form?.getFieldValue('query') || []; | |||||
| const query = form?.getValues('query') || []; | |||||
| const nextQuery = query.filter( | const nextQuery = query.filter( | ||||
| (item: BeginQuery, index: number) => index !== idx, | (item: BeginQuery, index: number) => index !== idx, | ||||
| ); | ); | ||||
| onValuesChange?.( | |||||
| { query: nextQuery }, | |||||
| { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||||
| ); | |||||
| // onValuesChange?.( | |||||
| // { query: nextQuery }, | |||||
| // { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||||
| // ); | |||||
| }, | }, | ||||
| [form, onValuesChange], | |||||
| [form], | |||||
| ); | ); | ||||
| return ( | return ( | ||||
| <Form.Provider | |||||
| onFormFinish={(name, { values }) => { | |||||
| if (name === 'queryForm') { | |||||
| ok(values as BeginQuery); | |||||
| } | |||||
| }} | |||||
| > | |||||
| <Form | |||||
| name="basicForm" | |||||
| onValuesChange={onValuesChange} | |||||
| autoComplete="off" | |||||
| form={form} | |||||
| layout="vertical" | |||||
| > | |||||
| <Form.Item<FieldType> | |||||
| name={'prologue'} | |||||
| label={t('chat.setAnOpener')} | |||||
| tooltip={t('chat.setAnOpenerTip')} | |||||
| initialValue={t('chat.setAnOpenerInitial')} | |||||
| > | |||||
| <Input.TextArea autoSize={{ minRows: 5 }} /> | |||||
| </Form.Item> | |||||
| {/* Create a hidden field to make Form instance record this */} | |||||
| <Form.Item name="query" noStyle /> | |||||
| <Form {...form}> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={'prologue'} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel tooltip={t('chat.setAnOpenerTip')}> | |||||
| {t('chat.setAnOpener')} | |||||
| </FormLabel> | |||||
| <FormControl> | |||||
| <Textarea | |||||
| rows={5} | |||||
| {...field} | |||||
| placeholder={t('common.pleaseInput')} | |||||
| ></Textarea> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| {/* Create a hidden field to make Form instance record this */} | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={'query'} | |||||
| render={() => <div></div>} | |||||
| /> | |||||
| <Form.Item | |||||
| shouldUpdate={(prevValues, curValues) => | |||||
| prevValues.query !== curValues.query | |||||
| } | |||||
| > | |||||
| {({ getFieldValue }) => { | |||||
| const query: BeginQuery[] = getFieldValue('query') || []; | |||||
| return ( | |||||
| <QueryTable | |||||
| data={query} | |||||
| showModal={showModal} | |||||
| deleteRecord={handleDeleteRecord} | |||||
| ></QueryTable> | |||||
| ); | |||||
| }} | |||||
| </Form.Item> | |||||
| <QueryTable | |||||
| data={query} | |||||
| showModal={showModal} | |||||
| deleteRecord={handleDeleteRecord} | |||||
| ></QueryTable> | |||||
| <Button | |||||
| htmlType="button" | |||||
| style={{ margin: '0 8px' }} | |||||
| onClick={() => showModal()} | |||||
| icon={<PlusOutlined />} | |||||
| block | |||||
| className={styles.addButton} | |||||
| > | |||||
| {t('flow.addItem')} | |||||
| </Button> | |||||
| {visible && ( | |||||
| <ModalForm | |||||
| visible={visible} | |||||
| hideModal={hideModal} | |||||
| initialValue={currentRecord} | |||||
| onOk={ok} | |||||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||||
| /> | |||||
| )} | |||||
| </Form> | |||||
| </Form.Provider> | |||||
| <Button onClick={() => showModal()}>{t('flow.addItem')}</Button> | |||||
| {visible && ( | |||||
| <ParameterDialog | |||||
| visible={visible} | |||||
| hideModal={hideModal} | |||||
| initialValue={currentRecord} | |||||
| onOk={ok} | |||||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||||
| ></ParameterDialog> | |||||
| )} | |||||
| </Form> | |||||
| ); | ); | ||||
| }; | }; | ||||
| 'use client'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Input } from '@/components/ui/input'; | |||||
| import { Plus, X } from 'lucide-react'; | |||||
| import { useFieldArray, useFormContext } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| export function BeginDynamicOptions() { | |||||
| const { t } = useTranslation(); | |||||
| const form = useFormContext(); | |||||
| const name = 'options'; | |||||
| const { fields, remove, append } = useFieldArray({ | |||||
| name: name, | |||||
| control: form.control, | |||||
| }); | |||||
| return ( | |||||
| <div className="space-y-5"> | |||||
| {fields.map((field, index) => { | |||||
| const typeField = `${name}.${index}`; | |||||
| return ( | |||||
| <div key={field.id} className="flex items-center gap-2"> | |||||
| <FormField | |||||
| control={form.control} | |||||
| name={typeField} | |||||
| render={({ field }) => ( | |||||
| <FormItem className="w-2/5"> | |||||
| <FormControl> | |||||
| <Input | |||||
| {...field} | |||||
| placeholder={t('common.pleaseInput')} | |||||
| ></Input> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <Button variant={'ghost'} onClick={() => remove(index)}> | |||||
| <X className="text-text-sub-title-invert " /> | |||||
| </Button> | |||||
| </div> | |||||
| ); | |||||
| })} | |||||
| <Button | |||||
| onClick={append} | |||||
| className="mt-4 border-dashed w-full" | |||||
| variant={'outline'} | |||||
| > | |||||
| <Plus /> | |||||
| {t('flow.addVariable')} | |||||
| </Button> | |||||
| </div> | |||||
| ); | |||||
| } |
| import { toast } from '@/components/hooks/use-toast'; | |||||
| import { Button } from '@/components/ui/button'; | |||||
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogFooter, | |||||
| DialogHeader, | |||||
| DialogTitle, | |||||
| } from '@/components/ui/dialog'; | |||||
| import { | |||||
| Form, | |||||
| FormControl, | |||||
| FormField, | |||||
| FormItem, | |||||
| FormLabel, | |||||
| FormMessage, | |||||
| } from '@/components/ui/form'; | |||||
| import { Input } from '@/components/ui/input'; | |||||
| import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select'; | |||||
| import { Switch } from '@/components/ui/switch'; | |||||
| import { IModalProps } from '@/interfaces/common'; | |||||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||||
| import { useEffect, useMemo } from 'react'; | |||||
| import { useForm, useWatch } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { z } from 'zod'; | |||||
| import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; | |||||
| import { BeginQuery } from '../../interface'; | |||||
| import { BeginDynamicOptions } from './next-begin-dynamic-options'; | |||||
| type ModalFormProps = { | |||||
| initialValue: BeginQuery; | |||||
| otherThanCurrentQuery: BeginQuery[]; | |||||
| }; | |||||
| const FormId = 'BeginParameterForm'; | |||||
| function ParameterForm({ | |||||
| initialValue, | |||||
| otherThanCurrentQuery, | |||||
| }: ModalFormProps) { | |||||
| const FormSchema = z.object({ | |||||
| type: z.string(), | |||||
| key: z | |||||
| .string() | |||||
| .trim() | |||||
| .refine( | |||||
| (value) => | |||||
| !value || !otherThanCurrentQuery.some((x) => x.key === value), | |||||
| { message: 'The key cannot be repeated!' }, | |||||
| ), | |||||
| optional: z.boolean(), | |||||
| options: z.array(z.string().or(z.boolean()).or(z.number())), | |||||
| }); | |||||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||||
| resolver: zodResolver(FormSchema), | |||||
| defaultValues: { | |||||
| type: BeginQueryType.Line, | |||||
| optional: false, | |||||
| }, | |||||
| }); | |||||
| const options = useMemo(() => { | |||||
| return Object.values(BeginQueryType).reduce<RAGFlowSelectOptionType[]>( | |||||
| (pre, cur) => { | |||||
| const Icon = BeginQueryTypeIconMap[cur]; | |||||
| return [ | |||||
| ...pre, | |||||
| { | |||||
| label: ( | |||||
| <div className="flex items-center gap-2"> | |||||
| <Icon | |||||
| className={`size-${cur === BeginQueryType.Options ? 4 : 5}`} | |||||
| ></Icon> | |||||
| {cur} | |||||
| </div> | |||||
| ), | |||||
| value: cur, | |||||
| }, | |||||
| ]; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| }, []); | |||||
| const type = useWatch({ | |||||
| control: form.control, | |||||
| name: 'type', | |||||
| }); | |||||
| useEffect(() => { | |||||
| form.reset(initialValue); | |||||
| }, [form, initialValue]); | |||||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||||
| toast({ | |||||
| title: 'You submitted the following values:', | |||||
| description: ( | |||||
| <pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> | |||||
| <code className="text-white">{JSON.stringify(data, null, 2)}</code> | |||||
| </pre> | |||||
| ), | |||||
| }); | |||||
| } | |||||
| return ( | |||||
| <Form {...form}> | |||||
| <form onSubmit={form.handleSubmit(onSubmit)} id={FormId}> | |||||
| <FormField | |||||
| name="type" | |||||
| control={form.control} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>Type</FormLabel> | |||||
| <FormControl> | |||||
| <RAGFlowSelect {...field} options={options} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| name="key" | |||||
| control={form.control} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>Key</FormLabel> | |||||
| <FormControl> | |||||
| <Input {...field} /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| <FormField | |||||
| name="optional" | |||||
| control={form.control} | |||||
| render={({ field }) => ( | |||||
| <FormItem> | |||||
| <FormLabel>Optional</FormLabel> | |||||
| <FormControl> | |||||
| <Switch | |||||
| checked={field.value} | |||||
| onCheckedChange={field.onChange} | |||||
| /> | |||||
| </FormControl> | |||||
| <FormMessage /> | |||||
| </FormItem> | |||||
| )} | |||||
| /> | |||||
| {type === BeginQueryType.Options && ( | |||||
| <BeginDynamicOptions></BeginDynamicOptions> | |||||
| )} | |||||
| </form> | |||||
| </Form> | |||||
| ); | |||||
| } | |||||
| export function ParameterDialog({ | |||||
| initialValue, | |||||
| hideModal, | |||||
| otherThanCurrentQuery, | |||||
| }: ModalFormProps & IModalProps<BeginQuery>) { | |||||
| const { t } = useTranslation(); | |||||
| return ( | |||||
| <Dialog open onOpenChange={hideModal}> | |||||
| <DialogContent> | |||||
| <DialogHeader> | |||||
| <DialogTitle>{t('flow.variableSettings')}</DialogTitle> | |||||
| </DialogHeader> | |||||
| <ParameterForm | |||||
| initialValue={initialValue} | |||||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||||
| ></ParameterForm> | |||||
| </DialogContent> | |||||
| <DialogFooter> | |||||
| <Button type="submit" id={FormId}> | |||||
| Confirm | |||||
| </Button> | |||||
| </DialogFooter> | |||||
| </Dialog> | |||||
| ); | |||||
| } |
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import styles from './index.less'; | |||||
| interface IProps { | interface IProps { | ||||
| data: BeginQuery[]; | data: BeginQuery[]; | ||||
| return ( | return ( | ||||
| <Collapse | <Collapse | ||||
| defaultActiveKey={['1']} | defaultActiveKey={['1']} | ||||
| className={styles.dynamicInputVariable} | |||||
| items={[ | items={[ | ||||
| { | { | ||||
| key: '1', | key: '1', | ||||
| label: <span className={styles.title}>{t('flow.input')}</span>, | |||||
| label: <span>{t('flow.input')}</span>, | |||||
| children: ( | children: ( | ||||
| <Table<BeginQuery> | <Table<BeginQuery> | ||||
| columns={columns} | columns={columns} |