### 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
| 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 ( | 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> | </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; | |||||
| } | 
| import { useWatch } from 'react-hook-form'; | import { useWatch } from 'react-hook-form'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { AgentDialogueMode } from '../../constant'; | 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 QueryTable from './query-table'; | ||||
| import { useEditQueryRecord } from './use-edit-query'; | |||||
| const ModeOptions = buildSelectOptions([ | const ModeOptions = buildSelectOptions([ | ||||
| (AgentDialogueMode.Conversational, AgentDialogueMode.Task), | (AgentDialogueMode.Conversational, AgentDialogueMode.Task), | ||||
| ]); | ]); | ||||
| const BeginForm = ({ form }: INextOperatorForm) => { | |||||
| const BeginForm = ({ form, node }: INextOperatorForm) => { | |||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const query = useWatch({ control: form.control, name: 'query' }); | const query = useWatch({ control: form.control, name: 'query' }); | ||||
| hideModal, | hideModal, | ||||
| showModal, | showModal, | ||||
| otherThanCurrentQuery, | otherThanCurrentQuery, | ||||
| handleDeleteRecord, | |||||
| } = useEditQueryRecord({ | } = useEditQueryRecord({ | ||||
| form, | 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 ( | return ( | ||||
| /> | /> | ||||
| )} | )} | ||||
| {/* Create a hidden field to make Form instance record this */} | {/* Create a hidden field to make Form instance record this */} | ||||
| {/* <FormField | |||||
| <FormField | |||||
| control={form.control} | control={form.control} | ||||
| name={'query'} | name={'query'} | ||||
| render={() => <div></div>} | render={() => <div></div>} | ||||
| /> */} | |||||
| /> | |||||
| <QueryTable | <QueryTable | ||||
| data={query} | data={query} | ||||
| showModal={showModal} | showModal={showModal} | ||||
| initialValue={currentRecord} | initialValue={currentRecord} | ||||
| onOk={ok} | onOk={ok} | ||||
| otherThanCurrentQuery={otherThanCurrentQuery} | otherThanCurrentQuery={otherThanCurrentQuery} | ||||
| submit={handleParameterDialogSubmit} | |||||
| ></ParameterDialog> | ></ParameterDialog> | ||||
| )} | )} | ||||
| </Form> | </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 { 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> | |||||
| ); | |||||
| }; | 
| import { z } from 'zod'; | import { z } from 'zod'; | ||||
| import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; | import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant'; | ||||
| import { BeginQuery } from '../../interface'; | import { BeginQuery } from '../../interface'; | ||||
| import { BeginDynamicOptions } from './next-begin-dynamic-options'; | |||||
| import { BeginDynamicOptions } from './begin-dynamic-options'; | |||||
| type ModalFormProps = { | type ModalFormProps = { | ||||
| initialValue: BeginQuery; | initialValue: BeginQuery; | ||||
| otherThanCurrentQuery: BeginQuery[]; | otherThanCurrentQuery: BeginQuery[]; | ||||
| submit(values: any): void; | |||||
| }; | }; | ||||
| const FormId = 'BeginParameterForm'; | const FormId = 'BeginParameterForm'; | ||||
| function ParameterForm({ | function ParameterForm({ | ||||
| initialValue, | initialValue, | ||||
| otherThanCurrentQuery, | otherThanCurrentQuery, | ||||
| submit, | |||||
| }: ModalFormProps) { | }: ModalFormProps) { | ||||
| const FormSchema = z.object({ | const FormSchema = z.object({ | ||||
| type: z.string(), | type: z.string(), | ||||
| ), | ), | ||||
| optional: z.boolean(), | optional: z.boolean(), | ||||
| name: z.string().trim().min(1), | 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>>({ | const form = useForm<z.infer<typeof FormSchema>>({ | ||||
| }); | }); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| form.reset(initialValue); | |||||
| form.reset({ | |||||
| ...initialValue, | |||||
| options: initialValue.options?.map((x) => ({ value: x })), | |||||
| }); | |||||
| }, [form, initialValue]); | }, [form, initialValue]); | ||||
| function onSubmit(data: z.infer<typeof FormSchema>) { | 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 ( | return ( | ||||
| initialValue, | initialValue, | ||||
| hideModal, | hideModal, | ||||
| otherThanCurrentQuery, | otherThanCurrentQuery, | ||||
| submit, | |||||
| }: ModalFormProps & IModalProps<BeginQuery>) { | }: ModalFormProps & IModalProps<BeginQuery>) { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| <ParameterForm | <ParameterForm | ||||
| initialValue={initialValue} | initialValue={initialValue} | ||||
| otherThanCurrentQuery={otherThanCurrentQuery} | otherThanCurrentQuery={otherThanCurrentQuery} | ||||
| submit={submit} | |||||
| ></ParameterForm> | ></ParameterForm> | ||||
| <DialogFooter> | <DialogFooter> | ||||
| <Button type="submit" form={FormId}> | <Button type="submit" form={FormId}> | 
| 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, INextOperatorForm } from '../../interface'; | 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 { 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 { update } = useUpdateQueryToNodeForm({ form, node }); | |||||
| const otherThanCurrentQuery = useMemo(() => { | const otherThanCurrentQuery = useMemo(() => { | ||||
| const query: BeginQuery[] = form?.getValues('query') || []; | const query: BeginQuery[] = form?.getValues('query') || []; | ||||
| const handleEditRecord = useCallback( | const handleEditRecord = useCallback( | ||||
| (record: BeginQuery) => { | (record: BeginQuery) => { | ||||
| const query: BeginQuery[] = form?.getValues('query') || []; | const query: BeginQuery[] = form?.getValues('query') || []; | ||||
| console.log('🚀 ~ useEditQueryRecord ~ query:', 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') }, | |||||
| // ); | |||||
| form.setValue('query', nextQuery, { | |||||
| shouldDirty: true, | |||||
| shouldTouch: true, | |||||
| }); | |||||
| update(nextQuery); | |||||
| hideModal(); | hideModal(); | ||||
| }, | }, | ||||
| [form, hideModal, index], | |||||
| [form, hideModal, index, update], | |||||
| ); | ); | ||||
| const handleShowModal = useCallback( | const handleShowModal = useCallback( | ||||
| [setRecord, showModal], | [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 { | return { | ||||
| ok: handleEditRecord, | ok: handleEditRecord, | ||||
| currentRecord, | currentRecord, | ||||
| hideModal, | hideModal, | ||||
| showModal: handleShowModal, | showModal: handleShowModal, | ||||
| otherThanCurrentQuery, | otherThanCurrentQuery, | ||||
| handleDeleteRecord, | |||||
| }; | }; | ||||
| }; | }; |