feat: http body type & http input support editor variable (#2603)

* feat: http body type & http input support editor variable

* fix type

* chore: code

* code
This commit is contained in:
heheer
2024-09-03 23:43:21 +08:00
committed by GitHub
parent a7569037fe
commit 85a11d08b2
11 changed files with 407 additions and 258 deletions

View File

@@ -24,15 +24,18 @@ import {
NumberDecrementStepper,
NumberInput
} from '@chakra-ui/react';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import {
ContentTypes,
NodeInputKeyEnum,
WorkflowIOValueTypeEnum
} from '@fastgpt/global/core/workflow/constants';
import { useTranslation } from 'next-i18next';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import JSONEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import { formatEditorVariablePickerIcon } from '@fastgpt/global/core/workflow/utils';
import { EditorVariablePickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import { EditorVariableLabelPickerType } from '@fastgpt/web/components/common/Textarea/PromptEditor/type';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import dynamic from 'next/dynamic';
import MySelect from '@fastgpt/web/components/common/MySelect';
@@ -40,52 +43,22 @@ import RenderToolInput from '../render/RenderToolInput';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils';
import { useMemoizedFn } from 'ahooks';
import { useCreation, useMemoizedFn } from 'ahooks';
import { AppContext } from '@/pages/app/detail/components/context';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { getEditorVariables } from '../../../utils';
import PromptEditor from '@fastgpt/web/components/common/Textarea/PromptEditor';
const CurlImportModal = dynamic(() => import('./CurlImportModal'));
export const HttpHeaders = [
{ key: 'A-IM', label: 'A-IM' },
{ key: 'Accept', label: 'Accept' },
{ key: 'Accept-Charset', label: 'Accept-Charset' },
{ key: 'Accept-Encoding', label: 'Accept-Encoding' },
{ key: 'Accept-Language', label: 'Accept-Language' },
{ key: 'Accept-Datetime', label: 'Accept-Datetime' },
{ key: 'Access-Control-Request-Method', label: 'Access-Control-Request-Method' },
{ key: 'Access-Control-Request-Headers', label: 'Access-Control-Request-Headers' },
{ key: 'Authorization', label: 'Authorization' },
{ key: 'Cache-Control', label: 'Cache-Control' },
{ key: 'Connection', label: 'Connection' },
{ key: 'Content-Length', label: 'Content-Length' },
{ key: 'Content-Type', label: 'Content-Type' },
{ key: 'Cookie', label: 'Cookie' },
{ key: 'Date', label: 'Date' },
{ key: 'Expect', label: 'Expect' },
{ key: 'Forwarded', label: 'Forwarded' },
{ key: 'From', label: 'From' },
{ key: 'Host', label: 'Host' },
{ key: 'If-Match', label: 'If-Match' },
{ key: 'If-Modified-Since', label: 'If-Modified-Since' },
{ key: 'If-None-Match', label: 'If-None-Match' },
{ key: 'If-Range', label: 'If-Range' },
{ key: 'If-Unmodified-Since', label: 'If-Unmodified-Since' },
{ key: 'Max-Forwards', label: 'Max-Forwards' },
{ key: 'Origin', label: 'Origin' },
{ key: 'Pragma', label: 'Pragma' },
{ key: 'Proxy-Authorization', label: 'Proxy-Authorization' },
{ key: 'Range', label: 'Range' },
{ key: 'Referer', label: 'Referer' },
{ key: 'TE', label: 'TE' },
{ key: 'User-Agent', label: 'User-Agent' },
{ key: 'Upgrade', label: 'Upgrade' },
{ key: 'Via', label: 'Via' },
{ key: 'Warning', label: 'Warning' },
{ key: 'Dnt', label: 'Dnt' },
{ key: 'X-Requested-With', label: 'X-Requested-With' },
{ key: 'X-CSRF-Token', label: 'X-CSRF-Token' }
];
const defaultFormBody = {
key: NodeInputKeyEnum.httpFormBody,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.any,
value: [],
label: '',
required: false
};
enum TabEnum {
params = 'params',
@@ -260,7 +233,7 @@ export function RenderHttpProps({
const { t } = useTranslation();
const [selectedTab, setSelectedTab] = useState(TabEnum.params);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const getNodeDynamicInputs = useContextSelector(WorkflowContext, (v) => v.getNodeDynamicInputs);
const edges = useContextSelector(WorkflowContext, (v) => v.edges);
const { appDetail } = useContextSelector(AppContext, (v) => v);
@@ -268,21 +241,23 @@ export function RenderHttpProps({
const params = inputs.find((item) => item.key === NodeInputKeyEnum.httpParams);
const headers = inputs.find((item) => item.key === NodeInputKeyEnum.httpHeaders);
const jsonBody = inputs.find((item) => item.key === NodeInputKeyEnum.httpJsonBody);
const formBody =
inputs.find((item) => item.key === NodeInputKeyEnum.httpFormBody) || defaultFormBody;
const contentType = inputs.find((item) => item.key === NodeInputKeyEnum.httpContentType);
const paramsLength = params?.value?.length || 0;
const headersLength = headers?.value?.length || 0;
// get variable
const variables = useMemo(() => {
const globalVariables = getWorkflowGlobalVariables({
nodes: nodeList,
chatConfig: appDetail.chatConfig
const variables = useCreation(() => {
return getEditorVariables({
nodeId,
nodeList,
edges,
appDetail,
t
});
const nodeVariables = formatEditorVariablePickerIcon(getNodeDynamicInputs(nodeId));
return [...nodeVariables, ...globalVariables];
}, [appDetail.chatConfig, getNodeDynamicInputs, nodeId, nodeList]);
}, [nodeList, edges, inputs, t]);
const variableText = useMemo(() => {
return variables
@@ -310,10 +285,11 @@ export function RenderHttpProps({
<QuestionTip
ml={1}
label={t('common:core.module.http.Props tip', { variable: variableText })}
></QuestionTip>
/>
</Flex>
<LightRowTabs<TabEnum>
width={'100%'}
mb={selectedTab === TabEnum.body ? 1 : 2}
list={[
{ label: <RenderPropsItem text="Params" num={paramsLength} />, value: TabEnum.params },
...(!['GET', 'DELETE'].includes(requestMethods)
@@ -337,33 +313,31 @@ export function RenderHttpProps({
value={selectedTab}
onChange={setSelectedTab}
/>
<Box bg={'white'} borderRadius={'md'}>
<Box bg={'white'} borderRadius={'md'} minW={'560px'}>
{params &&
headers &&
jsonBody &&
{
[TabEnum.params]: (
<RenderForm
[TabEnum.params]: <RenderForm nodeId={nodeId} input={params} variables={variables} />,
[TabEnum.body]: (
<RenderBody
nodeId={nodeId}
input={params}
variables={variables}
tabType={TabEnum.params}
jsonBody={jsonBody}
formBody={formBody}
typeInput={contentType}
/>
),
[TabEnum.body]: <RenderJson nodeId={nodeId} variables={variables} input={jsonBody} />,
[TabEnum.headers]: (
<RenderForm
nodeId={nodeId}
input={headers}
variables={variables}
tabType={TabEnum.headers}
/>
<RenderForm nodeId={nodeId} input={headers} variables={variables} />
)
}[selectedTab]}
</Box>
</Box>
);
}, [
contentType,
formBody,
headersLength,
nodeId,
paramsLength,
@@ -433,13 +407,11 @@ const RenderHttpTimeout = ({
const RenderForm = ({
nodeId,
input,
variables,
tabType
variables
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
tabType?: TabEnum;
variables: EditorVariableLabelPickerType[];
}) => {
const { t } = useTranslation();
const { toast } = useToast();
@@ -449,13 +421,6 @@ const RenderForm = ({
const [updateTrigger, setUpdateTrigger] = useState(false);
const [shouldUpdateNode, setShouldUpdateNode] = useState(false);
const leftVariables = useMemo(() => {
return (tabType === TabEnum.headers ? HttpHeaders : variables).filter((variable) => {
const existVariables = list.map((item) => item.key);
return !existVariables.includes(variable.key);
});
}, [list, tabType, variables]);
useEffect(() => {
setList(input.value || []);
}, [input.value]);
@@ -529,7 +494,7 @@ const RenderForm = ({
const Render = useMemo(() => {
return (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
<Box borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom={'none'}>
<TableContainer overflowY={'visible'} overflowX={'unset'}>
<Table>
<Thead>
@@ -545,29 +510,25 @@ const RenderForm = ({
<Tbody>
{list.map((item, index) => (
<Tr key={`${input.key}${index}`}>
<Td p={0} w={'150px'}>
<Td p={0} w={'50%'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(value) => {
handleKeyChange(index, value);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('common:core.module.http.Props name')}
value={item.key}
variables={leftVariables}
variableLabels={variables}
variables={variables}
onBlur={(val) => {
handleKeyChange(index, val);
}}
updateTrigger={updateTrigger}
/>
</Td>
<Td p={0}>
<Td p={0} w={'50%'}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('common:core.module.http.Props value')}
value={item.value}
variables={variables}
variableLabels={variables}
onBlur={(val) => {
setList((prevList) =>
prevList.map((item, i) =>
@@ -592,17 +553,12 @@ const RenderForm = ({
</Tr>
))}
<Tr>
<Td p={0} w={'150px'}>
<Td p={0} w={'50%'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={tabType === TabEnum.headers}
setDropdownValue={(val) => {
handleAddNewProps(val);
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('common:core.module.http.Add props')}
value={''}
variables={leftVariables}
variableLabels={variables}
variables={variables}
updateTrigger={updateTrigger}
onBlur={(val) => {
handleAddNewProps(val);
@@ -610,7 +566,7 @@ const RenderForm = ({
}}
/>
</Td>
<Td p={0}>
<Td p={0} w={'50%'}>
<Box display={'flex'} alignItems={'center'}>
<HttpInput />
</Box>
@@ -621,63 +577,158 @@ const RenderForm = ({
</TableContainer>
</Box>
);
}, [
handleAddNewProps,
handleKeyChange,
input.key,
leftVariables,
list,
t,
tabType,
updateTrigger,
variables
]);
}, [handleAddNewProps, handleKeyChange, input.key, list, t, updateTrigger, variables]);
return Render;
};
const RenderJson = ({
const RenderBody = ({
nodeId,
input,
jsonBody,
formBody,
typeInput,
variables
}: {
nodeId: string;
input: FlowNodeInputItemType;
variables: EditorVariablePickerType[];
jsonBody: FlowNodeInputItemType;
formBody: FlowNodeInputItemType;
typeInput: FlowNodeInputItemType | undefined;
variables: EditorVariableLabelPickerType[];
}) => {
const { t } = useTranslation();
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const [_, startSts] = useTransition();
useEffect(() => {
if (typeInput === undefined) {
onChangeNode({
nodeId,
type: 'addInput',
value: {
key: NodeInputKeyEnum.httpContentType,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.string,
value: ContentTypes.json,
label: '',
required: false
}
});
}
}, [nodeId, onChangeNode, typeInput]);
const Render = useMemo(() => {
return (
<Box mt={1}>
<JSONEditor
bg={'white'}
defaultHeight={200}
resize
value={input.value}
placeholder={t('common:core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: input.key,
value: {
...input,
value: e
}
<Box>
<Flex bg={'myGray.50'}>
{Object.values(ContentTypes).map((item) => (
<Box
key={item}
as={'span'}
px={3}
py={1.5}
mb={2}
borderRadius={'6px'}
border={'1px solid'}
{...(typeInput?.value === item
? {
bg: 'white',
borderColor: 'myGray.200',
color: 'primary.700'
}
: {
bg: 'myGray.50',
borderColor: 'transparent',
color: 'myGray.500'
})}
_hover={{ bg: 'white', borderColor: 'myGray.200', color: 'primary.700' }}
onClick={() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: NodeInputKeyEnum.httpContentType,
value: {
key: NodeInputKeyEnum.httpContentType,
renderTypeList: [FlowNodeInputTypeEnum.hidden],
valueType: WorkflowIOValueTypeEnum.string,
value: item,
label: '',
required: false
}
});
}}
cursor={'pointer'}
whiteSpace={'nowrap'}
>
{item}
</Box>
))}
</Flex>
{typeInput?.value === ContentTypes.none && (
<Box
px={4}
py={12}
bg={'white'}
color={'myGray.400'}
borderRadius={'6px'}
border={'1px solid'}
borderColor={'myGray.200'}
>
{t('workflow:http.body_none')}
</Box>
)}
{(typeInput?.value === ContentTypes.formData ||
typeInput?.value === ContentTypes.xWwwFormUrlencoded) && (
<RenderForm nodeId={nodeId} input={formBody} variables={variables} />
)}
{typeInput?.value === ContentTypes.json && (
<JSONEditor
bg={'white'}
defaultHeight={200}
resize
value={jsonBody.value}
placeholder={t('common:core.module.template.http body placeholder')}
onChange={(e) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: jsonBody.key,
value: {
...jsonBody,
value: e
}
});
});
});
}}
variables={variables}
/>
}}
variables={variables}
/>
)}
{(typeInput?.value === ContentTypes.xml || typeInput?.value === ContentTypes.raw) && (
<PromptEditor
value={jsonBody.value}
onChange={(e) => {
startSts(() => {
onChangeNode({
nodeId,
type: 'updateInput',
key: jsonBody.key,
value: {
...jsonBody,
value: e
}
});
});
}}
showOpenModal={false}
variableLabels={variables}
h={200}
/>
)}
</Box>
);
}, [input, nodeId, onChangeNode, t, variables]);
}, [typeInput?.value, t, nodeId, formBody, variables, jsonBody, onChangeNode]);
return Render;
};
const RenderPropsItem = ({ text, num }: { text: string; num: number }) => {
return (
<Flex alignItems={'center'}>
@@ -709,7 +760,7 @@ const NodeHttp = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
[NodeInputKeyEnum.httpHeaders]: Headers,
[NodeInputKeyEnum.httpTimeout]: HttpTimeout
};
}, [Headers, HttpMethodAndUrl]);
}, [Headers, HttpMethodAndUrl, HttpTimeout]);
return (
<NodeCard minW={'350px'} selected={selected} {...data}>

View File

@@ -34,7 +34,6 @@ import { str2OpenApiSchema } from '@fastgpt/global/core/app/httpPlugin/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyModal from '@fastgpt/web/components/common/MyModal';
import HttpInput from '@fastgpt/web/components/common/Input/HttpInput';
import { HttpHeaders } from '../../detail/components/WorkflowComponents/Flow/nodes/NodeHttp';
import { OpenApiJsonSchema } from '@fastgpt/global/core/app/httpPlugin/type';
import { AppSchema } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector';
@@ -168,15 +167,6 @@ const HttpPluginEditModal = ({
errorToast: t('common:plugin.Invalid Schema')
});
const leftVariables = useMemo(
() =>
HttpHeaders.filter((variable) => {
const existVariables = customHeaders.map((item) => item.key);
return !existVariables.includes(variable.key);
}),
[customHeaders]
);
useEffect(() => {
(async () => {
if (!apiSchemaStr) {
@@ -315,28 +305,8 @@ const HttpPluginEditModal = ({
<Tr key={`${index}`}>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={true}
setDropdownValue={(val) => {
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
i === index ? { ...item, key: val } : item
);
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('common:core.module.http.Props name')}
value={item.key}
variables={leftVariables}
onBlur={(val) => {
setCustomHeaders((prev) => {
const newHeaders = prev.map((item, i) =>
@@ -360,7 +330,6 @@ const HttpPluginEditModal = ({
<Box display={'flex'} alignItems={'center'}>
<HttpInput
placeholder={t('common:core.module.http.Props value')}
hasVariablePlugin={false}
value={item.value}
onBlur={(val) =>
setCustomHeaders((prev) => {
@@ -406,26 +375,8 @@ const HttpPluginEditModal = ({
<Tr>
<Td p={0} w={'150px'}>
<HttpInput
hasVariablePlugin={false}
hasDropDownPlugin={true}
setDropdownValue={(val) => {
setCustomHeaders((prev) => {
const newHeaders = [...prev, { key: val, value: '' }];
setValue(
'pluginData.customHeaders',
'{\n' +
newHeaders
.map((item) => `"${item.key}":"${item.value}"`)
.join(',\n') +
'\n}'
);
return newHeaders;
});
setUpdateTrigger((prev) => !prev);
}}
placeholder={t('common:core.module.http.Add props')}
value={''}
variables={leftVariables}
updateTrigger={updateTrigger}
onBlur={(val) => {
if (!val) return;