### 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
| @@ -43,20 +43,26 @@ export function useFormConfigMap() { | |||
| const FormConfigMap = { | |||
| [Operator.Begin]: { | |||
| component: BeginForm, | |||
| defaultValues: {}, | |||
| defaultValues: { | |||
| prologue: t('chat.setAnOpenerInitial'), | |||
| }, | |||
| schema: z.object({ | |||
| name: z | |||
| .string() | |||
| .min(1, { | |||
| message: t('common.namePlaceholder'), | |||
| }) | |||
| .trim(), | |||
| age: z | |||
| prologue: z | |||
| .string() | |||
| .min(1, { | |||
| message: t('common.namePlaceholder'), | |||
| }) | |||
| .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]: { | |||
| @@ -1,32 +1,32 @@ | |||
| import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useSetSelectedRecord } from '@/hooks/logic-hooks'; | |||
| 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 { visible, hideModal, showModal } = useSetModalState(); | |||
| const [index, setIndex] = useState(-1); | |||
| const otherThanCurrentQuery = useMemo(() => { | |||
| const query: BeginQuery[] = form?.getFieldValue('query') || []; | |||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||
| return query.filter((item, idx) => idx !== index); | |||
| }, [form, index]); | |||
| const handleEditRecord = useCallback( | |||
| (record: BeginQuery) => { | |||
| const query: BeginQuery[] = form?.getFieldValue('query') || []; | |||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||
| const nextQuery: BeginQuery[] = | |||
| 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(); | |||
| }, | |||
| [form, hideModal, index, onValuesChange], | |||
| [form, hideModal, index], | |||
| ); | |||
| const handleShowModal = useCallback( | |||
| @@ -1,24 +0,0 @@ | |||
| .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; | |||
| } | |||
| @@ -1,20 +1,26 @@ | |||
| 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 { useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { BeginQuery, IOperatorForm } from '../../interface'; | |||
| import { BeginQuery, INextOperatorForm } from '../../interface'; | |||
| import { useEditQueryRecord } from './hooks'; | |||
| import { ModalForm } from './paramater-modal'; | |||
| import { ParameterDialog } from './next-paramater-modal'; | |||
| 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 { | |||
| ok, | |||
| currentRecord, | |||
| @@ -24,87 +30,68 @@ const BeginForm = ({ onValuesChange, form }: IOperatorForm) => { | |||
| otherThanCurrentQuery, | |||
| } = useEditQueryRecord({ | |||
| form, | |||
| onValuesChange, | |||
| }); | |||
| const handleDeleteRecord = useCallback( | |||
| (idx: number) => { | |||
| const query = form?.getFieldValue('query') || []; | |||
| const query = form?.getValues('query') || []; | |||
| const nextQuery = query.filter( | |||
| (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 ( | |||
| <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> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,62 @@ | |||
| '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> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,186 @@ | |||
| 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> | |||
| ); | |||
| } | |||
| @@ -4,7 +4,6 @@ import { Collapse, Space, Table, Tooltip } from 'antd'; | |||
| import { BeginQuery } from '../../interface'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import styles from './index.less'; | |||
| interface IProps { | |||
| data: BeginQuery[]; | |||
| @@ -71,11 +70,10 @@ const QueryTable = ({ data, deleteRecord, showModal }: IProps) => { | |||
| return ( | |||
| <Collapse | |||
| defaultActiveKey={['1']} | |||
| className={styles.dynamicInputVariable} | |||
| items={[ | |||
| { | |||
| key: '1', | |||
| label: <span className={styles.title}>{t('flow.input')}</span>, | |||
| label: <span>{t('flow.input')}</span>, | |||
| children: ( | |||
| <Table<BeginQuery> | |||
| columns={columns} | |||