mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 05:12:39 +00:00
Test version (#4792)
* plugin node version select (#4760) * plugin node version select * type * fix * fix * perf: version list * fix node version (#4787) * change my select * fix-ui * fix test * add test * fix * remove invalid version field * filter deprecated field * fix: claude tool call * fix: test --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
getAppVersionDetail,
|
||||
getWorkflowVersionList,
|
||||
getAppVersionList,
|
||||
updateAppVersion
|
||||
} from '@/web/core/app/api/version';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
@@ -186,7 +186,7 @@ const TeamCloud = ({
|
||||
ScrollData,
|
||||
data: scrollDataList,
|
||||
setData
|
||||
} = useScrollPagination(getWorkflowVersionList, {
|
||||
} = useScrollPagination(getAppVersionList, {
|
||||
pageSize: 30,
|
||||
params: {
|
||||
appId: appDetail._id
|
||||
|
@@ -24,7 +24,10 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
AppNodeFlowNodeTypeMap,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import {
|
||||
getPreviewPluginNode,
|
||||
getSystemPlugTemplates,
|
||||
@@ -475,11 +478,7 @@ const RenderList = React.memo(function RenderList({
|
||||
const templateNode = await (async () => {
|
||||
try {
|
||||
// get plugin preview module
|
||||
if (
|
||||
template.flowNodeType === FlowNodeTypeEnum.pluginModule ||
|
||||
template.flowNodeType === FlowNodeTypeEnum.appModule ||
|
||||
template.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
if (AppNodeFlowNodeTypeMap[template.flowNodeType]) {
|
||||
setLoading(true);
|
||||
const res = await getPreviewPluginNode({ appId: template.id });
|
||||
|
||||
@@ -533,21 +532,25 @@ const RenderList = React.memo(function RenderList({
|
||||
pluginId: templateNode.pluginId
|
||||
}),
|
||||
intro: t(templateNode.intro as any),
|
||||
inputs: templateNode.inputs.map((input) => ({
|
||||
...input,
|
||||
value: defaultValueMap[input.key] ?? input.value,
|
||||
valueDesc: t(input.valueDesc as any),
|
||||
label: t(input.label as any),
|
||||
description: t(input.description as any),
|
||||
debugLabel: t(input.debugLabel as any),
|
||||
toolDescription: t(input.toolDescription as any)
|
||||
})),
|
||||
outputs: templateNode.outputs.map((output) => ({
|
||||
...output,
|
||||
valueDesc: t(output.valueDesc as any),
|
||||
label: t(output.label as any),
|
||||
description: t(output.description as any)
|
||||
}))
|
||||
inputs: templateNode.inputs
|
||||
.filter((input) => input.deprecated !== true)
|
||||
.map((input) => ({
|
||||
...input,
|
||||
value: defaultValueMap[input.key] ?? input.value,
|
||||
valueDesc: t(input.valueDesc as any),
|
||||
label: t(input.label as any),
|
||||
description: t(input.description as any),
|
||||
debugLabel: t(input.debugLabel as any),
|
||||
toolDescription: t(input.toolDescription as any)
|
||||
})),
|
||||
outputs: templateNode.outputs
|
||||
.filter((output) => output.deprecated !== true)
|
||||
.map((output) => ({
|
||||
...output,
|
||||
valueDesc: t(output.valueDesc as any),
|
||||
label: t(output.label as any),
|
||||
description: t(output.description as any)
|
||||
}))
|
||||
},
|
||||
position: { x: mouseX, y: mouseY },
|
||||
selected: true,
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Box, Button, Flex, type FlexProps } from '@chakra-ui/react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { Box, Button, Flex, HStack, useDisclosure, type FlexProps } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import type { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useEditTitle } from '@/web/common/hooks/useEditTitle';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import {
|
||||
AppNodeFlowNodeTypeMap,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { LOGO_ICON } from '@fastgpt/global/common/system/constants';
|
||||
import { ToolSourceHandle, ToolTargetHandle } from './Handle/ToolHandle';
|
||||
import { useEditTextarea } from '@fastgpt/web/hooks/useEditTextarea';
|
||||
@@ -28,6 +30,11 @@ import MyImage from '@fastgpt/web/components/common/Image/MyImage';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
import NodeDebugResponse from './RenderDebug/NodeDebugResponse';
|
||||
import { getAppVersionList } from '@/web/core/app/api/version';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import MyTag from '@fastgpt/web/components/common/Tag/index';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import { useCreation } from 'ahooks';
|
||||
|
||||
type Props = FlowNodeItemType & {
|
||||
children?: React.ReactNode | React.ReactNode[] | string;
|
||||
@@ -61,7 +68,6 @@ const NodeCard = (props: Props) => {
|
||||
w = 'full',
|
||||
h = 'full',
|
||||
nodeId,
|
||||
flowNodeType,
|
||||
selected,
|
||||
menuForbid,
|
||||
isTool = false,
|
||||
@@ -73,7 +79,6 @@ const NodeCard = (props: Props) => {
|
||||
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
|
||||
const onUpdateNodeError = useContextSelector(WorkflowContext, (v) => v.onUpdateNodeError);
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
|
||||
const setHoverNodeId = useContextSelector(WorkflowEventContext, (v) => v.setHoverNodeId);
|
||||
|
||||
// custom title edit
|
||||
@@ -96,6 +101,7 @@ const NodeCard = (props: Props) => {
|
||||
|
||||
return { node, parentNode };
|
||||
}, [nodeList, nodeId]);
|
||||
const isAppNode = node && AppNodeFlowNodeTypeMap[node?.flowNodeType];
|
||||
|
||||
const { data: nodeTemplate } = useRequest2(
|
||||
async () => {
|
||||
@@ -103,12 +109,7 @@ const NodeCard = (props: Props) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
node?.flowNodeType === FlowNodeTypeEnum.pluginModule ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.appModule ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.tool ||
|
||||
node?.flowNodeType === FlowNodeTypeEnum.toolSet
|
||||
) {
|
||||
if (isAppNode) {
|
||||
return { ...node, ...node.pluginData };
|
||||
} else {
|
||||
const template = moduleTemplatesFlat.find(
|
||||
@@ -132,51 +133,6 @@ const NodeCard = (props: Props) => {
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
openConfirm: onOpenConfirmSync,
|
||||
onClose: onCloseConfirmSync,
|
||||
ConfirmModal: ConfirmSyncModal
|
||||
} = useConfirm({
|
||||
content: t('workflow:Confirm_sync_node')
|
||||
});
|
||||
|
||||
const hasNewVersion = nodeTemplate && nodeTemplate.version !== node?.version;
|
||||
|
||||
const { runAsync: onClickSyncVersion } = useRequest2(
|
||||
async () => {
|
||||
if (!node) return;
|
||||
|
||||
if (node.pluginId) {
|
||||
const template = await getPreviewPluginNode({ appId: node.pluginId });
|
||||
|
||||
if (!!template) {
|
||||
onResetNode({
|
||||
id: nodeId,
|
||||
node: template
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const template = moduleTemplatesFlat.find(
|
||||
(item) => item.flowNodeType === node.flowNodeType
|
||||
);
|
||||
if (!template) {
|
||||
return toast({
|
||||
title: t('app:app.modules.not_found_tips'),
|
||||
status: 'warning'
|
||||
});
|
||||
}
|
||||
onResetNode({
|
||||
id: nodeId,
|
||||
node: template
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
refreshDeps: [node, nodeId, onResetNode],
|
||||
onFinally() {}
|
||||
}
|
||||
);
|
||||
|
||||
/* Node header */
|
||||
const Header = useMemo(() => {
|
||||
const showHeader = node?.flowNodeType !== FlowNodeTypeEnum.comment;
|
||||
@@ -255,28 +211,9 @@ const NodeCard = (props: Props) => {
|
||||
>
|
||||
<MyIcon name={'edit'} w={'14px'} />
|
||||
</Button>
|
||||
<Box flex={1} />
|
||||
{hasNewVersion && (
|
||||
<MyTooltip label={t('app:app.modules.click to update')}>
|
||||
<Button
|
||||
bg={'yellow.50'}
|
||||
color={'yellow.600'}
|
||||
variant={'ghost'}
|
||||
h={8}
|
||||
px={2}
|
||||
rounded={'6px'}
|
||||
fontSize={'xs'}
|
||||
fontWeight={'medium'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'yellow.100' }}
|
||||
onClick={onOpenConfirmSync(onClickSyncVersion)}
|
||||
>
|
||||
<Box>{t('app:app.modules.has new version')}</Box>
|
||||
<MyIcon name={'help'} w={'14px'} ml={1} />
|
||||
</Button>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{!!nodeTemplate?.diagram && !hasNewVersion && (
|
||||
<Box flex={1} mr={1} />
|
||||
{isAppNode && <NodeVersion node={node} />}
|
||||
{!!nodeTemplate?.diagram && (
|
||||
<MyTooltip
|
||||
label={
|
||||
<MyImage
|
||||
@@ -295,7 +232,7 @@ const NodeCard = (props: Props) => {
|
||||
{!!nodeTemplate?.diagram && node?.courseUrl && (
|
||||
<Box bg={'myGray.300'} w={'1px'} h={'12px'} ml={1} mr={0.5} />
|
||||
)}
|
||||
{!!(node?.courseUrl || nodeTemplate?.userGuide) && !hasNewVersion && (
|
||||
{!!(node?.courseUrl || nodeTemplate?.userGuide) && (
|
||||
<UseGuideModal
|
||||
title={nodeTemplate?.name}
|
||||
iconSrc={nodeTemplate?.avatar}
|
||||
@@ -310,7 +247,7 @@ const NodeCard = (props: Props) => {
|
||||
</UseGuideModal>
|
||||
)}
|
||||
{!!node?.pluginData?.error && (
|
||||
<MyTooltip label={t('app:app.modules.not_found_tips')}>
|
||||
<MyTooltip label={node?.pluginData?.error || t('app:app.modules.not_found_tips')}>
|
||||
<Flex
|
||||
bg={'red.50'}
|
||||
alignItems={'center'}
|
||||
@@ -321,7 +258,9 @@ const NodeCard = (props: Props) => {
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
<MyIcon name={'common/errorFill'} w={'14px'} mr={1} />
|
||||
<Box color={'red.600'}>{t('app:app.modules.not_found')}</Box>
|
||||
<Box color={'red.600'}>
|
||||
{node?.pluginData?.error || t('app:app.modules.not_found')}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
@@ -333,18 +272,14 @@ const NodeCard = (props: Props) => {
|
||||
</Box>
|
||||
);
|
||||
}, [
|
||||
node?.flowNodeType,
|
||||
node?.courseUrl,
|
||||
node?.pluginData?.error,
|
||||
node,
|
||||
showToolHandle,
|
||||
nodeId,
|
||||
isFolded,
|
||||
avatar,
|
||||
t,
|
||||
name,
|
||||
hasNewVersion,
|
||||
onOpenConfirmSync,
|
||||
onClickSyncVersion,
|
||||
isAppNode,
|
||||
nodeTemplate?.diagram,
|
||||
nodeTemplate?.userGuide,
|
||||
nodeTemplate?.name,
|
||||
@@ -419,7 +354,6 @@ const NodeCard = (props: Props) => {
|
||||
{RenderHandle}
|
||||
{RenderToolHandle}
|
||||
|
||||
<ConfirmSyncModal />
|
||||
<EditTitleModal maxLength={100} />
|
||||
</Flex>
|
||||
);
|
||||
@@ -663,3 +597,90 @@ const NodeIntro = React.memo(function NodeIntro({
|
||||
|
||||
return Render;
|
||||
});
|
||||
|
||||
const NodeVersion = React.memo(function NodeVersion({ node }: { node: FlowNodeItemType }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onResetNode = useContextSelector(WorkflowContext, (v) => v.onResetNode);
|
||||
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
// Load version list
|
||||
const { ScrollData, data: versionList } = useScrollPagination(getAppVersionList, {
|
||||
pageSize: 20,
|
||||
params: {
|
||||
appId: node.pluginId,
|
||||
isPublish: true
|
||||
},
|
||||
refreshDeps: [node.pluginId, isOpen],
|
||||
disalbed: !isOpen,
|
||||
manual: false
|
||||
});
|
||||
|
||||
const { runAsync: onUpdateVersion, loading: isUpdating } = useRequest2(
|
||||
async (versionId: string) => {
|
||||
if (!node) return;
|
||||
|
||||
if (node.pluginId) {
|
||||
const template = await getPreviewPluginNode({ appId: node.pluginId, versionId });
|
||||
|
||||
if (!!template) {
|
||||
onResetNode({
|
||||
id: node.nodeId,
|
||||
node: {
|
||||
...template,
|
||||
name: node.name,
|
||||
intro: node.intro,
|
||||
avatar: node.avatar
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
refreshDeps: [node, onResetNode]
|
||||
}
|
||||
);
|
||||
|
||||
const renderList = useCreation(
|
||||
() =>
|
||||
versionList.map((item) => ({
|
||||
label: item.versionName,
|
||||
value: item._id
|
||||
})),
|
||||
[node.isLatestVersion, node.version, t, versionList]
|
||||
);
|
||||
const valueLabel = useMemo(() => {
|
||||
return (
|
||||
<Flex alignItems={'center'} gap={0.5}>
|
||||
{node?.versionLabel}
|
||||
{!node.isLatestVersion && (
|
||||
<MyTag type="fill" colorSchema={'adora'} fontSize={'mini'} borderRadius={'lg'}>
|
||||
{t('app:not_the_newest')}
|
||||
</MyTag>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}, [node.isLatestVersion, node?.versionLabel, t]);
|
||||
|
||||
return (
|
||||
<MySelect
|
||||
className="nowheel"
|
||||
value={node.version}
|
||||
onChange={onUpdateVersion}
|
||||
isLoading={isUpdating}
|
||||
customOnOpen={onOpen}
|
||||
customOnClose={onClose}
|
||||
placeholder={node?.versionLabel}
|
||||
variant={'whitePrimaryOutline'}
|
||||
size={'sm'}
|
||||
list={renderList}
|
||||
ScrollData={(props) => (
|
||||
<ScrollData minH={'100px'} maxH={'40vh'}>
|
||||
{props.children}
|
||||
</ScrollData>
|
||||
)}
|
||||
valueLabel={valueLabel}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@@ -10,6 +10,8 @@ import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '@/pageComponents/app/detail/WorkflowComponents/context';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
|
||||
type Props = {
|
||||
nodeId: string;
|
||||
@@ -68,8 +70,38 @@ const InputLabel = ({ nodeId, input, RightComponent }: Props) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{input.deprecated && (
|
||||
<>
|
||||
<Box flex={'1'} />
|
||||
<MyTooltip label={t('app:Click_to_delete_this_field')}>
|
||||
<Flex
|
||||
px={1.5}
|
||||
py={1}
|
||||
bg={'adora.50'}
|
||||
rounded={'6px'}
|
||||
fontSize={'14px'}
|
||||
cursor="pointer"
|
||||
alignItems={'center'}
|
||||
_hover={{
|
||||
bg: 'adora.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delInput',
|
||||
key: input.key
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'adora.600'} w={4} mr={1} />
|
||||
<Box color={'adora.600'}>{t('app:Filed_is_deprecated')}</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Right Component */}
|
||||
{RightComponent && (
|
||||
{!input.deprecated && RightComponent && (
|
||||
<>
|
||||
<Box flex={'1'} />
|
||||
{RightComponent}
|
||||
|
@@ -8,11 +8,17 @@ import { getHandleId } from '@fastgpt/global/core/workflow/utils';
|
||||
import { Position } from 'reactflow';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import ValueTypeLabel from '../ValueTypeLabel';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { WorkflowContext } from '../../../../context';
|
||||
|
||||
const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutputItemType }) => {
|
||||
const { t } = useTranslation();
|
||||
const { label = '', description, valueType, valueDesc } = output;
|
||||
|
||||
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
|
||||
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
<Flex
|
||||
@@ -36,6 +42,36 @@ const OutputLabel = ({ nodeId, output }: { nodeId: string; output: FlowNodeOutpu
|
||||
</Box>
|
||||
{description && <QuestionTip ml={1} label={t(description as any)} />}
|
||||
<ValueTypeLabel valueType={valueType} valueDesc={valueDesc} />
|
||||
|
||||
{output.deprecated && (
|
||||
<>
|
||||
<Box flex={'1'} />
|
||||
<MyTooltip label={t('app:Click_to_delete_this_field')}>
|
||||
<Flex
|
||||
px={1.5}
|
||||
py={1}
|
||||
bg={'adora.50'}
|
||||
rounded={'6px'}
|
||||
fontSize={'14px'}
|
||||
cursor="pointer"
|
||||
alignItems={'center'}
|
||||
_hover={{
|
||||
bg: 'adora.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
type: 'delOutput',
|
||||
key: output.key
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyIcon name={'common/info'} color={'adora.600'} w={4} mr={1} />
|
||||
<Box color={'adora.600'}>{t('app:Filed_is_deprecated')}</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
{output.type === FlowNodeOutputTypeEnum.source && (
|
||||
<SourceHandle
|
||||
|
@@ -5,7 +5,6 @@ import { parseParentIdInMongo } from '@fastgpt/global/common/parentFolder/utils'
|
||||
import type { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppFolderTypeList } from '@fastgpt/global/core/app/constants';
|
||||
import type { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import { defaultNodeVersion } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { type ShortUrlParams } from '@fastgpt/global/support/marketing/type';
|
||||
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { TeamAppCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
|
||||
@@ -125,8 +124,7 @@ export const onCreateApp = async ({
|
||||
chatConfig,
|
||||
type,
|
||||
version: 'v2',
|
||||
pluginData,
|
||||
'pluginData.nodeVersion': defaultNodeVersion
|
||||
pluginData
|
||||
}
|
||||
],
|
||||
{ session, ordered: true }
|
||||
|
@@ -13,13 +13,13 @@ import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { PluginSourceEnum } from '@fastgpt/global/core/plugin/constants';
|
||||
|
||||
export type GetPreviewNodeQuery = { appId: string };
|
||||
export type GetPreviewNodeQuery = { appId: string; versionId?: string };
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<{}, GetPreviewNodeQuery>,
|
||||
_res: NextApiResponse<any>
|
||||
): Promise<FlowNodeTemplateType> {
|
||||
const { appId } = req.query;
|
||||
const { appId, versionId } = req.query;
|
||||
|
||||
const { source } = await splitCombinePluginId(appId);
|
||||
|
||||
@@ -27,7 +27,7 @@ async function handler(
|
||||
await authApp({ req, authToken: true, appId, per: ReadPermissionVal });
|
||||
}
|
||||
|
||||
return getChildAppPreviewNode({ id: appId });
|
||||
return getChildAppPreviewNode({ appId, versionId });
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
@@ -11,6 +11,7 @@ import { addSourceMember } from '@fastgpt/service/support/user/utils';
|
||||
|
||||
export type versionListBody = PaginationProps<{
|
||||
appId: string;
|
||||
isPublish?: boolean;
|
||||
}>;
|
||||
|
||||
export type versionListResponse = PaginationResponse<VersionListItemType>;
|
||||
@@ -19,16 +20,19 @@ async function handler(
|
||||
req: ApiRequestProps<versionListBody>,
|
||||
_res: NextApiResponse<any>
|
||||
): Promise<versionListResponse> {
|
||||
const { appId } = req.body;
|
||||
const { appId, isPublish } = req.body;
|
||||
const { offset, pageSize } = parsePaginationRequest(req);
|
||||
|
||||
await authApp({ appId, req, per: WritePermissionVal, authToken: true });
|
||||
|
||||
const match = {
|
||||
appId,
|
||||
isPublish
|
||||
};
|
||||
|
||||
const [result, total] = await Promise.all([
|
||||
(async () => {
|
||||
const versions = await MongoAppVersion.find({
|
||||
appId
|
||||
})
|
||||
const versions = await MongoAppVersion.find(match)
|
||||
.sort({
|
||||
time: -1
|
||||
})
|
||||
@@ -45,7 +49,7 @@ async function handler(
|
||||
}))
|
||||
);
|
||||
})(),
|
||||
MongoAppVersion.countDocuments({ appId })
|
||||
MongoAppVersion.countDocuments(match)
|
||||
]);
|
||||
|
||||
return {
|
||||
|
@@ -146,7 +146,7 @@ export const checkNode = async ({
|
||||
}: {
|
||||
node: StoreNodeItemType;
|
||||
ownerTmbId: string;
|
||||
}) => {
|
||||
}): Promise<StoreNodeItemType> => {
|
||||
const pluginId = node.pluginId;
|
||||
if (!pluginId) return node;
|
||||
|
||||
@@ -160,7 +160,7 @@ export const checkNode = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const preview = await getChildAppPreviewNode({ id: pluginId });
|
||||
const preview = await getChildAppPreviewNode({ appId: pluginId });
|
||||
return {
|
||||
...node,
|
||||
pluginData: {
|
||||
@@ -175,7 +175,6 @@ export const checkNode = async ({
|
||||
} catch (error: any) {
|
||||
return {
|
||||
...node,
|
||||
isError: true,
|
||||
pluginData: {
|
||||
error
|
||||
} as PluginDataType
|
||||
|
@@ -7,7 +7,7 @@ import type {
|
||||
} from '@fastgpt/global/core/workflow/type/node';
|
||||
import { getMyApps } from '../api';
|
||||
import type { ListAppBody } from '@/pages/api/core/app/list';
|
||||
import { defaultNodeVersion, FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { FlowNodeTemplateTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import type { GetPreviewNodeQuery } from '@/pages/api/core/app/plugin/getPreviewNode';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
@@ -47,7 +47,7 @@ export const getTeamPlugTemplates = (data?: ListAppBody) =>
|
||||
name: app.name,
|
||||
intro: app.intro,
|
||||
showStatus: false,
|
||||
version: app.pluginData?.nodeVersion || defaultNodeVersion,
|
||||
version: app.pluginData?.nodeVersion,
|
||||
isTool: true,
|
||||
sourceMember: app.sourceMember
|
||||
}))
|
||||
|
@@ -15,7 +15,7 @@ export const getAppLatestVersion = (data: getLatestVersionQuery) =>
|
||||
export const postPublishApp = (appId: string, data: PostPublishAppProps) =>
|
||||
POST(`/core/app/version/publish?appId=${appId}`, data);
|
||||
|
||||
export const getWorkflowVersionList = (data: PaginationProps<{ appId: string }>) =>
|
||||
export const getAppVersionList = (data: PaginationProps<{ appId: string }>) =>
|
||||
POST<versionListResponse>('/core/app/version/list', data);
|
||||
|
||||
export const getAppVersionDetail = (versionId: string, appId: string) =>
|
||||
|
@@ -7,7 +7,6 @@ import {
|
||||
import { type StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
|
||||
import {
|
||||
chatHistoryValueDesc,
|
||||
defaultNodeVersion,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
@@ -402,7 +401,6 @@ export function form2AppWorkflow(
|
||||
y: 545
|
||||
},
|
||||
// 这里不需要固定版本,给一个不存在的版本,每次都会用最新版
|
||||
version: defaultNodeVersion,
|
||||
pluginData: tool.pluginData,
|
||||
inputs: tool.inputs.map((input) => {
|
||||
// Special key value
|
||||
|
@@ -9,8 +9,7 @@ import {
|
||||
EDGE_TYPE,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
FlowNodeTypeEnum,
|
||||
defaultNodeVersion
|
||||
FlowNodeTypeEnum
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { EmptyNode } from '@fastgpt/global/core/workflow/template/system/emptyNode';
|
||||
import { type StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
|
||||
@@ -101,10 +100,8 @@ export const storeNode2FlowNode = ({
|
||||
...template,
|
||||
...storeNode,
|
||||
avatar: template.avatar ?? storeNode.avatar,
|
||||
version: storeNode.version ?? template.version ?? defaultNodeVersion,
|
||||
/*
|
||||
Inputs and outputs, New fields are added, not reduced
|
||||
*/
|
||||
version: template.version || storeNode.version,
|
||||
// template 中的输入必须都有
|
||||
inputs: templateInputs
|
||||
.map<FlowNodeInputItemType>((templateInput) => {
|
||||
const storeInput =
|
||||
@@ -113,10 +110,8 @@ export const storeNode2FlowNode = ({
|
||||
return {
|
||||
...storeInput,
|
||||
...templateInput,
|
||||
|
||||
debugLabel: t(templateInput.debugLabel ?? (storeInput.debugLabel as any)),
|
||||
toolDescription: t(templateInput.toolDescription ?? (storeInput.toolDescription as any)),
|
||||
|
||||
selectedTypeIndex: storeInput.selectedTypeIndex ?? templateInput.selectedTypeIndex,
|
||||
value: storeInput.value,
|
||||
valueType: storeInput.valueType ?? templateInput.valueType,
|
||||
@@ -124,29 +119,29 @@ export const storeNode2FlowNode = ({
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
/* Concat dynamic inputs */
|
||||
// 合并 store 中有,template 中没有的输入
|
||||
storeNode.inputs
|
||||
.filter((item) => !templateInputs.find((input) => input.key === item.key))
|
||||
.map((item) => {
|
||||
if (!dynamicInput) return item;
|
||||
const templateInput = template.inputs.find((input) => input.key === item.key);
|
||||
|
||||
return {
|
||||
...item,
|
||||
...getInputComponentProps(dynamicInput)
|
||||
...getInputComponentProps(dynamicInput),
|
||||
deprecated: templateInput?.deprecated
|
||||
};
|
||||
})
|
||||
),
|
||||
outputs: templateOutputs
|
||||
.map<FlowNodeOutputItemType>((templateOutput) => {
|
||||
const storeOutput =
|
||||
template.outputs.find((item) => item.key === templateOutput.key) || templateOutput;
|
||||
storeNode.outputs.find((item) => item.key === templateOutput.key) || templateOutput;
|
||||
|
||||
return {
|
||||
...storeOutput,
|
||||
...templateOutput,
|
||||
|
||||
description: t(templateOutput.description ?? (storeOutput.description as any)),
|
||||
|
||||
id: storeOutput.id ?? templateOutput.id,
|
||||
label: storeOutput.label ?? templateOutput.label,
|
||||
value: storeOutput.value ?? templateOutput.value,
|
||||
@@ -154,9 +149,15 @@ export const storeNode2FlowNode = ({
|
||||
};
|
||||
})
|
||||
.concat(
|
||||
storeNode.outputs.filter(
|
||||
(item) => !templateOutputs.find((output) => output.key === item.key)
|
||||
)
|
||||
storeNode.outputs
|
||||
.filter((item) => !templateOutputs.find((output) => output.key === item.key))
|
||||
.map((item) => {
|
||||
const templateOutput = template.outputs.find((output) => output.key === item.key);
|
||||
return {
|
||||
...item,
|
||||
deprecated: templateOutput?.deprecated
|
||||
};
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,86 @@
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
describe('storeNode2FlowNode with deprecated inputs/outputs', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock('@fastgpt/global/core/workflow/template/constants', () => {
|
||||
return {
|
||||
moduleTemplatesFlat: [
|
||||
{
|
||||
flowNodeType: 'userInput',
|
||||
name: 'User Input',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
version: '1.0',
|
||||
inputs: [
|
||||
{
|
||||
key: 'deprecatedInput',
|
||||
deprecated: true,
|
||||
label: 'Deprecated Input',
|
||||
renderTypeList: ['input'],
|
||||
selectedTypeIndex: 0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'deprecatedOutput',
|
||||
id: 'deprecatedId',
|
||||
type: 'input',
|
||||
deprecated: true,
|
||||
label: 'Deprecated Output'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('should handle deprecated inputs and outputs', () => {
|
||||
const storeNode = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [
|
||||
{
|
||||
key: 'deprecatedInput',
|
||||
value: 'old value',
|
||||
renderTypeList: ['input'],
|
||||
label: 'Deprecated Input'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'deprecatedOutput',
|
||||
id: 'deprecatedId',
|
||||
type: 'input',
|
||||
label: 'Deprecated Output'
|
||||
}
|
||||
],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const deprecatedInput = result.data.inputs.find((input) => input.key === 'deprecatedInput');
|
||||
expect(deprecatedInput).toBeDefined();
|
||||
expect(deprecatedInput?.deprecated).toBe(true);
|
||||
|
||||
const deprecatedOutput = result.data.outputs.find(
|
||||
(output) => output.key === 'deprecatedOutput'
|
||||
);
|
||||
expect(deprecatedOutput).toBeDefined();
|
||||
expect(deprecatedOutput?.deprecated).toBe(true);
|
||||
});
|
||||
});
|
113
projects/app/test/cases/web/workflow/store2flow.version.test.ts
Normal file
113
projects/app/test/cases/web/workflow/store2flow.version.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
describe('storeNode2FlowNode with version and avatar inheritance', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock('@fastgpt/global/core/workflow/template/constants', () => {
|
||||
return {
|
||||
moduleTemplatesFlat: [
|
||||
{
|
||||
flowNodeType: 'userInput',
|
||||
name: 'User Input',
|
||||
avatar: 'template-avatar.png',
|
||||
intro: '',
|
||||
version: '2.0',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
vi.mock('@fastgpt/global/core/workflow/node/constant', () => {
|
||||
return {
|
||||
FlowNodeTypeEnum: { userInput: 'userInput' },
|
||||
FlowNodeInputTypeEnum: {
|
||||
addInputParam: 'addInputParam',
|
||||
input: 'input',
|
||||
reference: 'reference',
|
||||
textarea: 'textarea',
|
||||
numberInput: 'numberInput',
|
||||
switch: 'switch',
|
||||
select: 'select'
|
||||
},
|
||||
FlowNodeOutputTypeEnum: {
|
||||
dynamic: 'dynamic',
|
||||
static: 'static',
|
||||
source: 'source',
|
||||
hidden: 'hidden'
|
||||
},
|
||||
EDGE_TYPE: 'custom-edge',
|
||||
chatHistoryValueDesc: 'chat history description',
|
||||
datasetSelectValueDesc: 'dataset value description',
|
||||
datasetQuoteValueDesc: 'dataset quote value description'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('should handle version and avatar inheritance', () => {
|
||||
// 测试场景1:storeNode没有version,使用template的version
|
||||
const storeNode1 = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 1'
|
||||
};
|
||||
|
||||
// 测试场景2:storeNode没有avatar,使用template的avatar
|
||||
const storeNode2 = {
|
||||
nodeId: 'node2',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 2',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
// 测试场景3:storeNode和template都有avatar,使用template的avatar
|
||||
const storeNode3 = {
|
||||
nodeId: 'node3',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 3',
|
||||
version: '3.0',
|
||||
avatar: 'store-avatar.png'
|
||||
};
|
||||
|
||||
const result1 = storeNode2FlowNode({
|
||||
item: storeNode1 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const result2 = storeNode2FlowNode({
|
||||
item: storeNode2 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const result3 = storeNode2FlowNode({
|
||||
item: storeNode3 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
// 验证版本继承关系
|
||||
expect(result1.data.version).toBe('2.0'); // 使用template的version
|
||||
expect(result2.data.version).toBe('2.0'); // 使用storeNode的version
|
||||
expect(result3.data.version).toBe('2.0'); // 使用storeNode的version
|
||||
|
||||
// 验证avatar继承关系
|
||||
expect(result1.data.avatar).toBe('template-avatar.png'); // 使用template的avatar
|
||||
expect(result2.data.avatar).toBe('template-avatar.png'); // 使用template的avatar
|
||||
expect(result3.data.avatar).toBe('template-avatar.png'); // 根据源码,应该使用template的avatar
|
||||
});
|
||||
});
|
339
projects/app/test/cases/web/workflow/utils.test.ts
Normal file
339
projects/app/test/cases/web/workflow/utils.test.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import type {
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType,
|
||||
StoreNodeItemType
|
||||
} from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { Node, Edge } from 'reactflow';
|
||||
import {
|
||||
FlowNodeTypeEnum,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
EDGE_TYPE
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
nodeTemplate2FlowNode,
|
||||
storeNode2FlowNode,
|
||||
filterWorkflowNodeOutputsByType,
|
||||
checkWorkflowNodeAndConnection,
|
||||
getLatestNodeTemplate
|
||||
} from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
|
||||
describe('nodeTemplate2FlowNode', () => {
|
||||
it('should convert template to flow node', () => {
|
||||
const template: FlowNodeTemplateType = {
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
name: 'Test Node',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
};
|
||||
|
||||
const result = nodeTemplate2FlowNode({
|
||||
template,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true,
|
||||
parentNodeId: 'parent1',
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true,
|
||||
data: {
|
||||
name: 'Test Node',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
parentNodeId: 'parent1'
|
||||
}
|
||||
});
|
||||
expect(result.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeNode2FlowNode', () => {
|
||||
it('should convert store node to flow node', () => {
|
||||
const storeNode: StoreNodeItemType = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode,
|
||||
selected: true,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 'node1',
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle dynamic inputs and outputs', () => {
|
||||
const storeNode: StoreNodeItemType = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [
|
||||
{
|
||||
key: 'dynamicInput',
|
||||
label: 'Dynamic Input',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.addInputParam]
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'dynamicOutput',
|
||||
key: 'dynamicOutput',
|
||||
label: 'Dynamic Output',
|
||||
type: FlowNodeOutputTypeEnum.dynamic
|
||||
}
|
||||
],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result.data.inputs).toHaveLength(3);
|
||||
expect(result.data.outputs).toHaveLength(2);
|
||||
});
|
||||
|
||||
// 这两个测试涉及到模拟冲突,请运行单独的测试文件:
|
||||
// - utils.deprecated.test.ts: 测试 deprecated inputs/outputs
|
||||
// - utils.version.test.ts: 测试 version 和 avatar inheritance
|
||||
});
|
||||
|
||||
describe('filterWorkflowNodeOutputsByType', () => {
|
||||
it('should filter outputs by type', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
valueType: WorkflowIOValueTypeEnum.boolean,
|
||||
key: '3',
|
||||
label: '3',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.string);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('should return all outputs for any type', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.any);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle array types correctly', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.arrayString,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.arrayString);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkWorkflowNodeAndConnection', () => {
|
||||
it('should validate nodes and connections', () => {
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: 'node1',
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
data: {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: NodeInputKeyEnum.aiChatDatasetQuote,
|
||||
required: true,
|
||||
value: undefined,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input]
|
||||
}
|
||||
],
|
||||
outputs: []
|
||||
},
|
||||
position: { x: 0, y: 0 }
|
||||
}
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
type: EDGE_TYPE
|
||||
}
|
||||
];
|
||||
|
||||
const result = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
expect(result).toEqual(['node1']);
|
||||
});
|
||||
|
||||
it('should handle empty nodes and edges', () => {
|
||||
const result = checkWorkflowNodeAndConnection({ nodes: [], edges: [] });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatestNodeTemplate', () => {
|
||||
it('should update node to latest template version', () => {
|
||||
const node: FlowNodeItemType = {
|
||||
id: 'node1',
|
||||
nodeId: 'node1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'input1',
|
||||
value: 'test',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input],
|
||||
label: 'Input 1'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'output1',
|
||||
value: 'test',
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
label: 'Output 1',
|
||||
id: 'output1'
|
||||
}
|
||||
],
|
||||
name: 'Old Name',
|
||||
intro: 'Old Intro'
|
||||
};
|
||||
|
||||
const template: FlowNodeTemplateType = {
|
||||
name: 'Template 1',
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{ key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' },
|
||||
{ key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' }
|
||||
],
|
||||
outputs: [
|
||||
{ id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' },
|
||||
{ id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = getLatestNodeTemplate(node, template);
|
||||
|
||||
expect(result.inputs).toHaveLength(2);
|
||||
expect(result.outputs).toHaveLength(2);
|
||||
expect(result.name).toBe('Old Name');
|
||||
});
|
||||
|
||||
it('should preserve existing values when updating template', () => {
|
||||
const node: FlowNodeItemType = {
|
||||
id: 'node1',
|
||||
nodeId: 'node1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'input1',
|
||||
value: 'existingValue',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input],
|
||||
label: 'Input 1'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'output1',
|
||||
value: 'existingOutput',
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
label: 'Output 1',
|
||||
id: 'output1'
|
||||
}
|
||||
],
|
||||
name: 'Node Name',
|
||||
intro: 'Node Intro'
|
||||
};
|
||||
|
||||
const template: FlowNodeTemplateType = {
|
||||
name: 'Template 1',
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{ key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' },
|
||||
{ key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' }
|
||||
],
|
||||
outputs: [
|
||||
{ id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' },
|
||||
{ id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = getLatestNodeTemplate(node, template);
|
||||
|
||||
expect(result.inputs[0].value).toBe('existingValue');
|
||||
expect(result.outputs[0].value).toBe('existingOutput');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user