mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
fix(workflow): gate “publish as tool” on published user input node validity
This commit is contained in:
@ -64,6 +64,7 @@ export type AppPublisherProps = {
|
|||||||
toolPublished?: boolean
|
toolPublished?: boolean
|
||||||
inputs?: InputVar[]
|
inputs?: InputVar[]
|
||||||
onRefreshData?: () => void
|
onRefreshData?: () => void
|
||||||
|
workflowToolAvailable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||||
@ -82,6 +83,7 @@ const AppPublisher = ({
|
|||||||
toolPublished,
|
toolPublished,
|
||||||
inputs,
|
inputs,
|
||||||
onRefreshData,
|
onRefreshData,
|
||||||
|
workflowToolAvailable = true,
|
||||||
}: AppPublisherProps) => {
|
}: AppPublisherProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [published, setPublished] = useState(false)
|
const [published, setPublished] = useState(false)
|
||||||
@ -178,6 +180,10 @@ const AppPublisher = ({
|
|||||||
handlePublish()
|
handlePublish()
|
||||||
}, { exactMatch: true, useCapture: true })
|
}, { exactMatch: true, useCapture: true })
|
||||||
|
|
||||||
|
const hasPublishedVersion = !!publishedAt
|
||||||
|
const workflowToolDisabled = !hasPublishedVersion || !workflowToolAvailable
|
||||||
|
const workflowToolMessage = workflowToolDisabled ? t('workflow.common.workflowAsToolDisabledHint') : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
@ -366,7 +372,7 @@ const AppPublisher = ({
|
|||||||
</SuggestedAction>
|
</SuggestedAction>
|
||||||
{appDetail?.mode === 'workflow' && (
|
{appDetail?.mode === 'workflow' && (
|
||||||
<WorkflowToolConfigureButton
|
<WorkflowToolConfigureButton
|
||||||
disabled={!publishedAt}
|
disabled={workflowToolDisabled}
|
||||||
published={!!toolPublished}
|
published={!!toolPublished}
|
||||||
detailNeedUpdate={!!toolPublished && published}
|
detailNeedUpdate={!!toolPublished && published}
|
||||||
workflowAppId={appDetail?.id}
|
workflowAppId={appDetail?.id}
|
||||||
@ -379,6 +385,7 @@ const AppPublisher = ({
|
|||||||
inputs={inputs}
|
inputs={inputs}
|
||||||
handlePublish={handlePublish}
|
handlePublish={handlePublish}
|
||||||
onRefreshData={onRefreshData}
|
onRefreshData={onRefreshData}
|
||||||
|
disabledReason={workflowToolMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -28,6 +28,7 @@ type Props = {
|
|||||||
inputs?: InputVar[]
|
inputs?: InputVar[]
|
||||||
handlePublish: (params?: PublishWorkflowParams) => Promise<void>
|
handlePublish: (params?: PublishWorkflowParams) => Promise<void>
|
||||||
onRefreshData?: () => void
|
onRefreshData?: () => void
|
||||||
|
disabledReason?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkflowToolConfigureButton = ({
|
const WorkflowToolConfigureButton = ({
|
||||||
@ -41,6 +42,7 @@ const WorkflowToolConfigureButton = ({
|
|||||||
inputs,
|
inputs,
|
||||||
handlePublish,
|
handlePublish,
|
||||||
onRefreshData,
|
onRefreshData,
|
||||||
|
disabledReason,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -200,7 +202,8 @@ const WorkflowToolConfigureButton = ({
|
|||||||
{t('workflow.common.configureRequired')}
|
{t('workflow.common.configureRequired')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>)
|
</div>
|
||||||
|
)
|
||||||
: (
|
: (
|
||||||
<div
|
<div
|
||||||
className='flex items-center justify-start gap-2 p-2 pl-2.5'
|
className='flex items-center justify-start gap-2 p-2 pl-2.5'
|
||||||
@ -214,6 +217,11 @@ const WorkflowToolConfigureButton = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{disabledReason && (
|
||||||
|
<div className='mt-1 px-2.5 pb-2 text-xs leading-[18px] text-text-tertiary'>
|
||||||
|
{disabledReason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{published && (
|
{published && (
|
||||||
<div className='border-t-[0.5px] border-divider-regular px-2.5 py-2'>
|
<div className='border-t-[0.5px] border-divider-regular px-2.5 py-2'>
|
||||||
<div className='flex justify-between gap-x-2'>
|
<div className='flex justify-between gap-x-2'>
|
||||||
@ -221,7 +229,7 @@ const WorkflowToolConfigureButton = ({
|
|||||||
size='small'
|
size='small'
|
||||||
className='w-[140px]'
|
className='w-[140px]'
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
disabled={!isCurrentWorkspaceManager}
|
disabled={!isCurrentWorkspaceManager || disabled}
|
||||||
>
|
>
|
||||||
{t('workflow.common.configure')}
|
{t('workflow.common.configure')}
|
||||||
{outdated && <Indicator className='ml-1' color={'yellow'} />}
|
{outdated && <Indicator className='ml-1' color={'yellow'} />}
|
||||||
@ -230,14 +238,17 @@ const WorkflowToolConfigureButton = ({
|
|||||||
size='small'
|
size='small'
|
||||||
className='w-[140px]'
|
className='w-[140px]'
|
||||||
onClick={() => router.push('/tools?category=workflow')}
|
onClick={() => router.push('/tools?category=workflow')}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{t('workflow.common.manageInTools')}
|
{t('workflow.common.manageInTools')}
|
||||||
<RiArrowRightUpLine className='ml-1 h-4 w-4' />
|
<RiArrowRightUpLine className='ml-1 h-4 w-4' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{outdated && <div className='mt-1 text-xs leading-[18px] text-text-warning'>
|
{outdated && (
|
||||||
{t('workflow.common.workflowAsToolTip')}
|
<div className='mt-1 text-xs leading-[18px] text-text-warning'>
|
||||||
</div>}
|
{t('workflow.common.workflowAsToolTip')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -50,9 +50,12 @@ const FeaturesTrigger = () => {
|
|||||||
const publishedAt = useStore(s => s.publishedAt)
|
const publishedAt = useStore(s => s.publishedAt)
|
||||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||||
const toolPublished = useStore(s => s.toolPublished)
|
const toolPublished = useStore(s => s.toolPublished)
|
||||||
|
const lastPublishedHasUserInput = useStore(s => s.lastPublishedHasUserInput)
|
||||||
const startVariables = useReactflowStore(
|
const startVariables = useReactflowStore(
|
||||||
s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables,
|
s => s.getNodes().find(node => node.data.type === BlockEnum.Start)?.data.variables,
|
||||||
)
|
)
|
||||||
|
const nodes = useNodes<CommonNodeType>()
|
||||||
|
const edges = useEdges<CommonEdgeType>()
|
||||||
const fileSettings = useFeatures(s => s.features.file)
|
const fileSettings = useFeatures(s => s.features.file)
|
||||||
const variables = useMemo(() => {
|
const variables = useMemo(() => {
|
||||||
const data = startVariables || []
|
const data = startVariables || []
|
||||||
@ -74,6 +77,15 @@ const FeaturesTrigger = () => {
|
|||||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||||
const { notify } = useToastContext()
|
const { notify } = useToastContext()
|
||||||
|
const startNodeIds = useMemo(
|
||||||
|
() => nodes.filter(node => node.data.type === BlockEnum.Start).map(node => node.id),
|
||||||
|
[nodes],
|
||||||
|
)
|
||||||
|
const hasUserInputNode = useMemo(() => {
|
||||||
|
if (!startNodeIds.length)
|
||||||
|
return false
|
||||||
|
return edges.some(edge => startNodeIds.includes(edge.source))
|
||||||
|
}, [edges, startNodeIds])
|
||||||
|
|
||||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory()
|
||||||
const invalidateAppTriggers = useInvalidateAppTriggers()
|
const invalidateAppTriggers = useInvalidateAppTriggers()
|
||||||
@ -101,8 +113,6 @@ const FeaturesTrigger = () => {
|
|||||||
|
|
||||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
|
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
|
||||||
// const { validateBeforeRun } = useWorkflowRunValidation()
|
// const { validateBeforeRun } = useWorkflowRunValidation()
|
||||||
const nodes = useNodes<CommonNodeType>()
|
|
||||||
const edges = useEdges<CommonEdgeType>()
|
|
||||||
const needWarningNodes = useChecklist(nodes, edges)
|
const needWarningNodes = useChecklist(nodes, edges)
|
||||||
|
|
||||||
const updatePublishedWorkflow = useInvalidateAppWorkflow()
|
const updatePublishedWorkflow = useInvalidateAppWorkflow()
|
||||||
@ -130,6 +140,7 @@ const FeaturesTrigger = () => {
|
|||||||
updateAppDetail()
|
updateAppDetail()
|
||||||
invalidateAppTriggers(appID!)
|
invalidateAppTriggers(appID!)
|
||||||
workflowStore.getState().setPublishedAt(res.created_at)
|
workflowStore.getState().setPublishedAt(res.created_at)
|
||||||
|
workflowStore.getState().setLastPublishedHasUserInput(hasUserInputNode)
|
||||||
resetWorkflowVersionHistory()
|
resetWorkflowVersionHistory()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,6 +183,7 @@ const FeaturesTrigger = () => {
|
|||||||
onRefreshData: handleToolConfigureUpdate,
|
onRefreshData: handleToolConfigureUpdate,
|
||||||
onPublish,
|
onPublish,
|
||||||
onToggle: onPublisherToggle,
|
onToggle: onPublisherToggle,
|
||||||
|
workflowToolAvailable: lastPublishedHasUserInput,
|
||||||
crossAxisOffset: 4,
|
crossAxisOffset: 4,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -18,7 +18,18 @@ import {
|
|||||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||||
import type { FileUploadConfigResponse } from '@/models/common'
|
import type { FileUploadConfigResponse } from '@/models/common'
|
||||||
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
const hasConnectedUserInput = (nodes: any[] = [], edges: any[] = []) => {
|
||||||
|
const startNodeIds = nodes
|
||||||
|
.filter(node => node?.data?.type === BlockEnum.Start)
|
||||||
|
.map((node: any) => node.id)
|
||||||
|
|
||||||
|
if (!startNodeIds.length)
|
||||||
|
return false
|
||||||
|
|
||||||
|
return edges?.some((edge: any) => startNodeIds.includes(edge.source))
|
||||||
|
}
|
||||||
export const useWorkflowInit = () => {
|
export const useWorkflowInit = () => {
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const {
|
const {
|
||||||
@ -110,9 +121,14 @@ export const useWorkflowInit = () => {
|
|||||||
}, {} as Record<string, any>),
|
}, {} as Record<string, any>),
|
||||||
})
|
})
|
||||||
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
|
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
|
||||||
|
const graph = publishedWorkflow?.graph
|
||||||
|
workflowStore.getState().setLastPublishedHasUserInput(
|
||||||
|
hasConnectedUserInput(graph?.nodes, graph?.edges),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
workflowStore.getState().setLastPublishedHasUserInput(false)
|
||||||
}
|
}
|
||||||
}, [workflowStore, appDetail])
|
}, [workflowStore, appDetail])
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,8 @@ export type ToolSliceShape = {
|
|||||||
setMcpTools: (tools: ToolWithProvider[]) => void
|
setMcpTools: (tools: ToolWithProvider[]) => void
|
||||||
toolPublished: boolean
|
toolPublished: boolean
|
||||||
setToolPublished: (toolPublished: boolean) => void
|
setToolPublished: (toolPublished: boolean) => void
|
||||||
|
lastPublishedHasUserInput: boolean
|
||||||
|
setLastPublishedHasUserInput: (hasUserInput: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createToolSlice: StateCreator<ToolSliceShape> = set => ({
|
export const createToolSlice: StateCreator<ToolSliceShape> = set => ({
|
||||||
@ -27,4 +29,6 @@ export const createToolSlice: StateCreator<ToolSliceShape> = set => ({
|
|||||||
setMcpTools: mcpTools => set(() => ({ mcpTools })),
|
setMcpTools: mcpTools => set(() => ({ mcpTools })),
|
||||||
toolPublished: false,
|
toolPublished: false,
|
||||||
setToolPublished: toolPublished => set(() => ({ toolPublished })),
|
setToolPublished: toolPublished => set(() => ({ toolPublished })),
|
||||||
|
lastPublishedHasUserInput: false,
|
||||||
|
setLastPublishedHasUserInput: hasUserInput => set(() => ({ lastPublishedHasUserInput: hasUserInput })),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -88,6 +88,7 @@ const translation = {
|
|||||||
configure: 'Configure',
|
configure: 'Configure',
|
||||||
manageInTools: 'Manage in Tools',
|
manageInTools: 'Manage in Tools',
|
||||||
workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.',
|
workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.',
|
||||||
|
workflowAsToolDisabledHint: 'Publish the latest workflow and ensure a connected User Input node before configuring it as a tool.',
|
||||||
viewDetailInTracingPanel: 'View details',
|
viewDetailInTracingPanel: 'View details',
|
||||||
syncingData: 'Syncing data, just a few seconds.',
|
syncingData: 'Syncing data, just a few seconds.',
|
||||||
importDSL: 'Import DSL',
|
importDSL: 'Import DSL',
|
||||||
|
|||||||
@ -83,6 +83,7 @@ const translation = {
|
|||||||
configure: '設定',
|
configure: '設定',
|
||||||
manageInTools: 'ツールページで管理',
|
manageInTools: 'ツールページで管理',
|
||||||
workflowAsToolTip: 'ワークフロー更新後はツールの再設定が必要です',
|
workflowAsToolTip: 'ワークフロー更新後はツールの再設定が必要です',
|
||||||
|
workflowAsToolDisabledHint: '最新のワークフローを公開し、接続済みの User Input ノードを用意してからツールとして設定してください。',
|
||||||
viewDetailInTracingPanel: '詳細を表示',
|
viewDetailInTracingPanel: '詳細を表示',
|
||||||
syncingData: 'データ同期中。。。',
|
syncingData: 'データ同期中。。。',
|
||||||
importDSL: 'DSL をインポート',
|
importDSL: 'DSL をインポート',
|
||||||
|
|||||||
@ -86,6 +86,7 @@ const translation = {
|
|||||||
configure: '配置',
|
configure: '配置',
|
||||||
manageInTools: '访问工具页',
|
manageInTools: '访问工具页',
|
||||||
workflowAsToolTip: '工作流更新后需要重新配置工具参数',
|
workflowAsToolTip: '工作流更新后需要重新配置工具参数',
|
||||||
|
workflowAsToolDisabledHint: '请先发布最新的工作流,并确保已连接的 User Input 节点后再配置为工具。',
|
||||||
viewDetailInTracingPanel: '查看详细信息',
|
viewDetailInTracingPanel: '查看详细信息',
|
||||||
syncingData: '同步数据中,只需几秒钟。',
|
syncingData: '同步数据中,只需几秒钟。',
|
||||||
importDSL: '导入 DSL',
|
importDSL: '导入 DSL',
|
||||||
|
|||||||
Reference in New Issue
Block a user