| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155 | 'use client'import type { FC } from 'react'import React, { useCallback, useState } from 'react'import { useTranslation } from 'react-i18next'import { BodyType, type HttpNodeType, Method } from '../types'import Modal from '@/app/components/base/modal'import Button from '@/app/components/base/button'import Toast from '@/app/components/base/toast'import { useNodesInteractions } from '@/app/components/workflow/hooks'type Props = {  nodeId: string  isShow: boolean  onHide: () => void  handleCurlImport: (node: HttpNodeType) => void}const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => {  if (!curlCommand.trim().toLowerCase().startsWith('curl'))    return { node: null, error: 'Invalid cURL command. Command must start with "curl".' }  const node: Partial<HttpNodeType> = {    title: 'HTTP Request',    desc: 'Imported from cURL',    method: Method.get,    url: '',    headers: '',    params: '',    body: { type: BodyType.none, data: '' },  }  const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []  for (let i = 1; i < args.length; i++) {    const arg = args[i].replace(/^['"]|['"]$/g, '')    switch (arg) {      case '-X':      case '--request':        if (i + 1 >= args.length)          return { node: null, error: 'Missing HTTP method after -X or --request.' }        node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get        break      case '-H':      case '--header':        if (i + 1 >= args.length)          return { node: null, error: 'Missing header value after -H or --header.' }        node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')        break      case '-d':      case '--data':      case '--data-raw':      case '--data-binary':        if (i + 1 >= args.length)          return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }        node.body = { type: BodyType.rawText, data: args[++i].replace(/^['"]|['"]$/g, '') }        break      case '-F':      case '--form': {        if (i + 1 >= args.length)          return { node: null, error: 'Missing form data after -F or --form.' }        if (node.body?.type !== BodyType.formData)          node.body = { type: BodyType.formData, data: '' }        const formData = args[++i].replace(/^['"]|['"]$/g, '')        const [key, ...valueParts] = formData.split('=')        if (!key)          return { node: null, error: 'Invalid form data format.' }        let value = valueParts.join('=')        // To support command like `curl -F "file=@/path/to/file;type=application/zip"`        // the `;type=application/zip` should translate to `Content-Type: application/zip`        const typeMatch = value.match(/^(.+?);type=(.+)$/)        if (typeMatch) {          const [, actualValue, mimeType] = typeMatch          value = actualValue          node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}`        }        node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}`        break      }      case '--json':        if (i + 1 >= args.length)          return { node: null, error: 'Missing JSON data after --json.' }        node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }        break      default:        if (arg.startsWith('http') && !node.url)          node.url = arg        break    }  }  if (!node.url)    return { node: null, error: 'Missing URL or url not start with http.' }  // Extract query params from URL  const urlParts = node.url?.split('?') || []  if (urlParts.length > 1) {    node.url = urlParts[0]    node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ')  }  return { node: node as HttpNodeType, error: null }}const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {  const [inputString, setInputString] = useState('')  const { handleNodeSelect } = useNodesInteractions()  const { t } = useTranslation()  const handleSave = useCallback(() => {    const { node, error } = parseCurl(inputString)    if (error) {      Toast.notify({        type: 'error',        message: error,      })      return    }    if (!node)      return    onHide()    handleCurlImport(node)    // Close the panel then open it again to make the panel re-render    handleNodeSelect(nodeId, true)    setTimeout(() => {      handleNodeSelect(nodeId)    }, 0)  }, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])  return (    <Modal      title={t('workflow.nodes.http.curl.title')}      isShow={isShow}      onClose={onHide}      className='!w-[400px] !max-w-[400px] !p-4'    >      <div>        <textarea          value={inputString}          className='w-full my-3 p-3 text-sm text-gray-900 border-0 rounded-lg grow bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200 h-40'          onChange={e => setInputString(e.target.value)}          placeholder={t('workflow.nodes.http.curl.placeholder')!}        />      </div>      <div className='mt-4 flex justify-end space-x-2'>        <Button className='!w-[95px]' onClick={onHide} >{t('common.operation.cancel')}</Button>        <Button className='!w-[95px]' variant='primary' onClick={handleSave} > {t('common.operation.save')}</Button>      </div>    </Modal>  )}export default React.memo(CurlPanel)
 |