dify/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts
Coding On Star 210710e76d
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Main CI Pipeline / Check Changed Files (push) Waiting to run
Main CI Pipeline / API Tests (push) Blocked by required conditions
Main CI Pipeline / Web Tests (push) Blocked by required conditions
Main CI Pipeline / Style Check (push) Waiting to run
Main CI Pipeline / VDB Tests (push) Blocked by required conditions
Main CI Pipeline / DB Migration Test (push) Blocked by required conditions
refactor(web): extract custom hooks from complex components and add comprehensive tests (#32301)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
2026-02-13 17:21:34 +08:00

236 lines
6.9 KiB
TypeScript

import type { Emoji, WorkflowToolProviderOutputParameter, WorkflowToolProviderParameter, WorkflowToolProviderRequest, WorkflowToolProviderResponse } from '@/app/components/tools/types'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import { createWorkflowToolProvider, saveWorkflowToolProvider } from '@/service/tools'
import { useInvalidateAllWorkflowTools, useInvalidateWorkflowToolDetailByAppID, useWorkflowToolDetailByAppID } from '@/service/use-tools'
// region Pure helpers
/**
* Check if workflow tool parameters are outdated compared to current inputs.
* Uses flat early-return style to reduce cyclomatic complexity.
*/
export function isParametersOutdated(
detail: WorkflowToolProviderResponse | undefined,
inputs: InputVar[] | undefined,
): boolean {
if (!detail)
return false
if (detail.tool.parameters.length !== (inputs?.length ?? 0))
return true
for (const item of inputs || []) {
const param = detail.tool.parameters.find(p => p.name === item.variable)
if (!param)
return true
if (param.required !== item.required)
return true
const needsStringType = item.type === 'paragraph' || item.type === 'text-input'
if (needsStringType && param.type !== 'string')
return true
}
return false
}
function buildNewParameters(inputs?: InputVar[]): WorkflowToolProviderParameter[] {
return (inputs || []).map(item => ({
name: item.variable,
description: '',
form: 'llm',
required: item.required,
type: item.type,
}))
}
function buildExistingParameters(
inputs: InputVar[] | undefined,
detail: WorkflowToolProviderResponse,
): WorkflowToolProviderParameter[] {
return (inputs || []).map((item) => {
const matched = detail.tool.parameters.find(p => p.name === item.variable)
return {
name: item.variable,
required: item.required,
type: item.type === 'paragraph' ? 'string' : item.type,
description: matched?.llm_description || '',
form: matched?.form || 'llm',
}
})
}
function buildNewOutputParameters(outputs?: Variable[]): WorkflowToolProviderOutputParameter[] {
return (outputs || []).map(item => ({
name: item.variable,
description: '',
type: item.value_type,
}))
}
function buildExistingOutputParameters(
outputs: Variable[] | undefined,
detail: WorkflowToolProviderResponse,
): WorkflowToolProviderOutputParameter[] {
return (outputs || []).map((item) => {
const found = detail.tool.output_schema?.properties?.[item.variable]
return {
name: item.variable,
description: found ? found.description : '',
type: item.value_type,
}
})
}
// endregion
type UseConfigureButtonOptions = {
published: boolean
detailNeedUpdate: boolean
workflowAppId: string
icon: Emoji
name: string
description: string
inputs?: InputVar[]
outputs?: Variable[]
handlePublish: (params?: PublishWorkflowParams) => Promise<void>
onRefreshData?: () => void
}
export function useConfigureButton(options: UseConfigureButtonOptions) {
const {
published,
detailNeedUpdate,
workflowAppId,
icon,
name,
description,
inputs,
outputs,
handlePublish,
onRefreshData,
} = options
const { t } = useTranslation()
const router = useRouter()
const { isCurrentWorkspaceManager } = useAppContext()
const [showModal, setShowModal] = useState(false)
// Data fetching via React Query
const { data: detail, isLoading } = useWorkflowToolDetailByAppID(workflowAppId, published)
// Invalidation functions (store in ref for stable effect dependency)
const invalidateDetail = useInvalidateWorkflowToolDetailByAppID()
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const invalidateDetailRef = useRef(invalidateDetail)
invalidateDetailRef.current = invalidateDetail
// Refetch when detailNeedUpdate becomes true
useEffect(() => {
if (detailNeedUpdate)
invalidateDetailRef.current(workflowAppId)
}, [detailNeedUpdate, workflowAppId])
// Computed values
const outdated = useMemo(
() => isParametersOutdated(detail, inputs),
[detail, inputs],
)
const payload = useMemo(() => {
const hasPublishedDetail = published && detail?.tool
const parameters = !published
? buildNewParameters(inputs)
: hasPublishedDetail
? buildExistingParameters(inputs, detail)
: []
const outputParameters = !published
? buildNewOutputParameters(outputs)
: hasPublishedDetail
? buildExistingOutputParameters(outputs, detail)
: []
return {
icon: detail?.icon || icon,
label: detail?.label || name,
name: detail?.name || '',
description: detail?.description || description,
parameters,
outputParameters,
labels: detail?.tool?.labels || [],
privacy_policy: detail?.privacy_policy || '',
...(published
? { workflow_tool_id: detail?.workflow_tool_id }
: { workflow_app_id: workflowAppId }),
}
}, [detail, published, workflowAppId, icon, name, description, inputs, outputs])
// Modal controls (stable callbacks)
const openModal = useCallback(() => setShowModal(true), [])
const closeModal = useCallback(() => setShowModal(false), [])
const navigateToTools = useCallback(
() => router.push('/tools?category=workflow'),
[router],
)
// Mutation handlers (not memoized — only used in conditionally-rendered modal)
const handleCreate = async (data: WorkflowToolProviderRequest & { workflow_app_id: string }) => {
try {
await createWorkflowToolProvider(data)
invalidateAllWorkflowTools()
onRefreshData?.()
invalidateDetail(workflowAppId)
Toast.notify({
type: 'success',
message: t('api.actionSuccess', { ns: 'common' }),
})
setShowModal(false)
}
catch (e) {
Toast.notify({ type: 'error', message: (e as Error).message })
}
}
const handleUpdate = async (data: WorkflowToolProviderRequest & Partial<{
workflow_app_id: string
workflow_tool_id: string
}>) => {
try {
await handlePublish()
await saveWorkflowToolProvider(data)
onRefreshData?.()
invalidateAllWorkflowTools()
invalidateDetail(workflowAppId)
Toast.notify({
type: 'success',
message: t('api.actionSuccess', { ns: 'common' }),
})
setShowModal(false)
}
catch (e) {
Toast.notify({ type: 'error', message: (e as Error).message })
}
}
return {
showModal,
isLoading,
outdated,
payload,
isCurrentWorkspaceManager,
openModal,
closeModal,
handleCreate,
handleUpdate,
navigateToTools,
}
}