### What problem does this PR solve? Feat: Synchronize BeginForm's query data to the canvas #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.19.x
| @@ -1,68 +1,61 @@ | |||
| import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { Button, Form, Input } from 'antd'; | |||
| 'use client'; | |||
| import { BlockButton, Button } from '@/components/ui/button'; | |||
| import { | |||
| FormControl, | |||
| FormField, | |||
| FormItem, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { Input } from '@/components/ui/input'; | |||
| import { 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, | |||
| }); | |||
| const BeginDynamicOptions = () => { | |||
| return ( | |||
| <Form.List | |||
| name="options" | |||
| rules={[ | |||
| { | |||
| validator: async (_, names) => { | |||
| if (!names || names.length < 1) { | |||
| return Promise.reject(new Error('At least 1 option')); | |||
| } | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| {(fields, { add, remove }, { errors }) => ( | |||
| <> | |||
| {fields.map((field, index) => ( | |||
| <Form.Item | |||
| label={index === 0 ? 'Options' : ''} | |||
| required={false} | |||
| key={field.key} | |||
| > | |||
| <Form.Item | |||
| {...field} | |||
| validateTrigger={['onChange', 'onBlur']} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| whitespace: true, | |||
| message: 'Please input option or delete this field.', | |||
| }, | |||
| ]} | |||
| noStyle | |||
| > | |||
| <Input | |||
| placeholder="option" | |||
| style={{ width: '90%', marginRight: 16 }} | |||
| /> | |||
| </Form.Item> | |||
| {fields.length > 1 ? ( | |||
| <MinusCircleOutlined | |||
| className="dynamic-delete-button" | |||
| onClick={() => remove(field.name)} | |||
| /> | |||
| ) : null} | |||
| </Form.Item> | |||
| ))} | |||
| <Form.Item> | |||
| <Button | |||
| type="dashed" | |||
| onClick={() => add()} | |||
| icon={<PlusOutlined />} | |||
| block | |||
| > | |||
| Add option | |||
| <div className="space-y-5"> | |||
| {fields.map((field, index) => { | |||
| const typeField = `${name}.${index}.value`; | |||
| return ( | |||
| <div key={field.id} className="flex items-center gap-2"> | |||
| <FormField | |||
| control={form.control} | |||
| name={typeField} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <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> | |||
| <Form.ErrorList errors={errors} /> | |||
| </Form.Item> | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| </div> | |||
| ); | |||
| })} | |||
| <BlockButton | |||
| onClick={() => append({ value: '' })} | |||
| variant={'outline'} | |||
| type="button" | |||
| > | |||
| {t('flow.addVariable')} | |||
| </BlockButton> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default BeginDynamicOptions; | |||
| } | |||
| @@ -15,16 +15,16 @@ import { useCallback } from 'react'; | |||
| import { useWatch } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { AgentDialogueMode } from '../../constant'; | |||
| import { BeginQuery, INextOperatorForm } from '../../interface'; | |||
| import { useEditQueryRecord } from './hooks'; | |||
| import { ParameterDialog } from './paramater-dialog'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { ParameterDialog } from './parameter-dialog'; | |||
| import QueryTable from './query-table'; | |||
| import { useEditQueryRecord } from './use-edit-query'; | |||
| const ModeOptions = buildSelectOptions([ | |||
| (AgentDialogueMode.Conversational, AgentDialogueMode.Task), | |||
| ]); | |||
| const BeginForm = ({ form }: INextOperatorForm) => { | |||
| const BeginForm = ({ form, node }: INextOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const query = useWatch({ control: form.control, name: 'query' }); | |||
| @@ -40,22 +40,17 @@ const BeginForm = ({ form }: INextOperatorForm) => { | |||
| hideModal, | |||
| showModal, | |||
| otherThanCurrentQuery, | |||
| handleDeleteRecord, | |||
| } = useEditQueryRecord({ | |||
| form, | |||
| node, | |||
| }); | |||
| const handleDeleteRecord = useCallback( | |||
| (idx: number) => { | |||
| const query = form?.getValues('query') || []; | |||
| const nextQuery = query.filter( | |||
| (item: BeginQuery, index: number) => index !== idx, | |||
| ); | |||
| // onValuesChange?.( | |||
| // { query: nextQuery }, | |||
| // { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||
| // ); | |||
| const handleParameterDialogSubmit = useCallback( | |||
| (values: any) => { | |||
| ok(values); | |||
| }, | |||
| [form], | |||
| [ok], | |||
| ); | |||
| return ( | |||
| @@ -118,11 +113,11 @@ const BeginForm = ({ form }: INextOperatorForm) => { | |||
| /> | |||
| )} | |||
| {/* Create a hidden field to make Form instance record this */} | |||
| {/* <FormField | |||
| <FormField | |||
| control={form.control} | |||
| name={'query'} | |||
| render={() => <div></div>} | |||
| /> */} | |||
| /> | |||
| <QueryTable | |||
| data={query} | |||
| showModal={showModal} | |||
| @@ -138,6 +133,7 @@ const BeginForm = ({ form }: INextOperatorForm) => { | |||
| initialValue={currentRecord} | |||
| onOk={ok} | |||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||
| submit={handleParameterDialogSubmit} | |||
| ></ParameterDialog> | |||
| )} | |||
| </Form> | |||
| @@ -1,62 +0,0 @@ | |||
| '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> | |||
| ); | |||
| } | |||
| @@ -1,124 +0,0 @@ | |||
| import { useResetFormOnCloseModal } from '@/hooks/logic-hooks'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { Form, Input, Modal, Select, Switch } from 'antd'; | |||
| import { DefaultOptionType } from 'antd/es/select'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; | |||
| import { BeginQuery } from '../../interface'; | |||
| import BeginDynamicOptions from './begin-dynamic-options'; | |||
| export const ModalForm = ({ | |||
| visible, | |||
| initialValue, | |||
| hideModal, | |||
| otherThanCurrentQuery, | |||
| }: IModalProps<BeginQuery> & { | |||
| initialValue: BeginQuery; | |||
| otherThanCurrentQuery: BeginQuery[]; | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const [form] = Form.useForm(); | |||
| const options = useMemo(() => { | |||
| return Object.values(BeginQueryType).reduce<DefaultOptionType[]>( | |||
| (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, | |||
| }, | |||
| ]; | |||
| }, | |||
| [], | |||
| ); | |||
| }, []); | |||
| useResetFormOnCloseModal({ | |||
| form, | |||
| visible: visible, | |||
| }); | |||
| useEffect(() => { | |||
| form.setFieldsValue(initialValue); | |||
| }, [form, initialValue]); | |||
| const onOk = () => { | |||
| form.submit(); | |||
| }; | |||
| return ( | |||
| <Modal | |||
| title={t('flow.variableSettings')} | |||
| open={visible} | |||
| onOk={onOk} | |||
| onCancel={hideModal} | |||
| centered | |||
| > | |||
| <Form form={form} layout="vertical" name="queryForm" autoComplete="false"> | |||
| <Form.Item | |||
| name="type" | |||
| label="Type" | |||
| rules={[{ required: true }]} | |||
| initialValue={BeginQueryType.Line} | |||
| > | |||
| <Select options={options} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="key" | |||
| label="Key" | |||
| rules={[ | |||
| { required: true }, | |||
| () => ({ | |||
| validator(_, value) { | |||
| if ( | |||
| !value || | |||
| !otherThanCurrentQuery.some((x) => x.key === value) | |||
| ) { | |||
| return Promise.resolve(); | |||
| } | |||
| return Promise.reject(new Error('The key cannot be repeated!')); | |||
| }, | |||
| }), | |||
| ]} | |||
| > | |||
| <Input /> | |||
| </Form.Item> | |||
| <Form.Item name="name" label="Name" rules={[{ required: true }]}> | |||
| <Input /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| name="optional" | |||
| label={'Optional'} | |||
| valuePropName="checked" | |||
| initialValue={false} | |||
| > | |||
| <Switch /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| shouldUpdate={(prevValues, curValues) => | |||
| prevValues.type !== curValues.type | |||
| } | |||
| > | |||
| {({ getFieldValue }) => { | |||
| const type: BeginQueryType = getFieldValue('type'); | |||
| return ( | |||
| type === BeginQueryType.Options && ( | |||
| <BeginDynamicOptions></BeginDynamicOptions> | |||
| ) | |||
| ); | |||
| }} | |||
| </Form.Item> | |||
| </Form> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| @@ -25,11 +25,12 @@ 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'; | |||
| import { BeginDynamicOptions } from './begin-dynamic-options'; | |||
| type ModalFormProps = { | |||
| initialValue: BeginQuery; | |||
| otherThanCurrentQuery: BeginQuery[]; | |||
| submit(values: any): void; | |||
| }; | |||
| const FormId = 'BeginParameterForm'; | |||
| @@ -37,6 +38,7 @@ const FormId = 'BeginParameterForm'; | |||
| function ParameterForm({ | |||
| initialValue, | |||
| otherThanCurrentQuery, | |||
| submit, | |||
| }: ModalFormProps) { | |||
| const FormSchema = z.object({ | |||
| type: z.string(), | |||
| @@ -51,7 +53,9 @@ function ParameterForm({ | |||
| ), | |||
| optional: z.boolean(), | |||
| name: z.string().trim().min(1), | |||
| options: z.array(z.string().or(z.boolean()).or(z.number())).optional(), | |||
| options: z | |||
| .array(z.object({ value: z.string().or(z.boolean()).or(z.number()) })) | |||
| .optional(), | |||
| }); | |||
| const form = useForm<z.infer<typeof FormSchema>>({ | |||
| @@ -94,11 +98,17 @@ function ParameterForm({ | |||
| }); | |||
| useEffect(() => { | |||
| form.reset(initialValue); | |||
| form.reset({ | |||
| ...initialValue, | |||
| options: initialValue.options?.map((x) => ({ value: x })), | |||
| }); | |||
| }, [form, initialValue]); | |||
| function onSubmit(data: z.infer<typeof FormSchema>) { | |||
| console.log('🚀 ~ onSubmit ~ data:', data); | |||
| const values = { ...data, options: data.options?.map((x) => x.value) }; | |||
| console.log('🚀 ~ onSubmit ~ values:', values); | |||
| submit(values); | |||
| } | |||
| return ( | |||
| @@ -176,6 +186,7 @@ export function ParameterDialog({ | |||
| initialValue, | |||
| hideModal, | |||
| otherThanCurrentQuery, | |||
| submit, | |||
| }: ModalFormProps & IModalProps<BeginQuery>) { | |||
| const { t } = useTranslation(); | |||
| @@ -188,6 +199,7 @@ export function ParameterDialog({ | |||
| <ParameterForm | |||
| initialValue={initialValue} | |||
| otherThanCurrentQuery={otherThanCurrentQuery} | |||
| submit={submit} | |||
| ></ParameterForm> | |||
| <DialogFooter> | |||
| <Button type="submit" form={FormId}> | |||
| @@ -2,11 +2,30 @@ import { useSetModalState } from '@/hooks/common-hooks'; | |||
| import { useSetSelectedRecord } from '@/hooks/logic-hooks'; | |||
| import { useCallback, useMemo, useState } from 'react'; | |||
| import { BeginQuery, INextOperatorForm } from '../../interface'; | |||
| import useGraphStore from '../../store'; | |||
| export const useEditQueryRecord = ({ form }: INextOperatorForm) => { | |||
| export function useUpdateQueryToNodeForm({ form, node }: INextOperatorForm) { | |||
| const updateNodeForm = useGraphStore((state) => state.updateNodeForm); | |||
| const update = useCallback( | |||
| (query: BeginQuery[]) => { | |||
| const values = form.getValues(); | |||
| const nextValues = { ...values, query }; | |||
| if (node?.id) { | |||
| updateNodeForm(node.id, nextValues); | |||
| } | |||
| }, | |||
| [form, node?.id, updateNodeForm], | |||
| ); | |||
| return { update }; | |||
| } | |||
| export const useEditQueryRecord = ({ form, node }: INextOperatorForm) => { | |||
| const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>(); | |||
| const { visible, hideModal, showModal } = useSetModalState(); | |||
| const [index, setIndex] = useState(-1); | |||
| const { update } = useUpdateQueryToNodeForm({ form, node }); | |||
| const otherThanCurrentQuery = useMemo(() => { | |||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||
| @@ -16,17 +35,21 @@ export const useEditQueryRecord = ({ form }: INextOperatorForm) => { | |||
| const handleEditRecord = useCallback( | |||
| (record: BeginQuery) => { | |||
| const query: BeginQuery[] = form?.getValues('query') || []; | |||
| console.log('🚀 ~ useEditQueryRecord ~ query:', query); | |||
| const nextQuery: BeginQuery[] = | |||
| index > -1 ? query.toSpliced(index, 1, record) : [...query, record]; | |||
| // onValuesChange?.( | |||
| // { query: nextQuery }, | |||
| // { query: nextQuery, prologue: form?.getFieldValue('prologue') }, | |||
| // ); | |||
| form.setValue('query', nextQuery, { | |||
| shouldDirty: true, | |||
| shouldTouch: true, | |||
| }); | |||
| update(nextQuery); | |||
| hideModal(); | |||
| }, | |||
| [form, hideModal, index], | |||
| [form, hideModal, index, update], | |||
| ); | |||
| const handleShowModal = useCallback( | |||
| @@ -38,6 +61,20 @@ export const useEditQueryRecord = ({ form }: INextOperatorForm) => { | |||
| [setRecord, showModal], | |||
| ); | |||
| const handleDeleteRecord = useCallback( | |||
| (idx: number) => { | |||
| const query = form?.getValues('query') || []; | |||
| const nextQuery = query.filter( | |||
| (item: BeginQuery, index: number) => index !== idx, | |||
| ); | |||
| form.setValue('query', nextQuery, { shouldDirty: true }); | |||
| update(nextQuery); | |||
| }, | |||
| [form, update], | |||
| ); | |||
| return { | |||
| ok: handleEditRecord, | |||
| currentRecord, | |||
| @@ -46,5 +83,6 @@ export const useEditQueryRecord = ({ form }: INextOperatorForm) => { | |||
| hideModal, | |||
| showModal: handleShowModal, | |||
| otherThanCurrentQuery, | |||
| handleDeleteRecord, | |||
| }; | |||
| }; | |||