Workflow-Engine / web /app /components /workflow /hooks /use-workflow-interactions.ts
Severian's picture
initial commit
a8b3f00
raw
history blame
9.14 kB
import {
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useReactFlow, useStoreApi } from 'reactflow'
import produce from 'immer'
import { useStore, useWorkflowStore } from '../store'
import {
CUSTOM_NODE, DSL_EXPORT_CHECK,
WORKFLOW_DATA_UPDATE,
} from '../constants'
import type { Node, WorkflowDataUpdater } from '../types'
import { ControlMode } from '../types'
import {
getLayoutByDagre,
initialEdges,
initialNodes,
} from '../utils'
import {
useNodesReadOnly,
useSelectionInteractions,
useWorkflowReadOnly,
} from '../hooks'
import { useEdgesInteractions } from './use-edges-interactions'
import { useNodesInteractions } from './use-nodes-interactions'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { fetchWorkflowDraft } from '@/service/workflow'
import { exportAppConfig } from '@/service/apps'
import { useToastContext } from '@/app/components/base/toast'
import { useStore as useAppStore } from '@/app/components/app/store'
export const useWorkflowInteractions = () => {
const workflowStore = useWorkflowStore()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
const handleCancelDebugAndPreviewPanel = useCallback(() => {
workflowStore.setState({
showDebugAndPreviewPanel: false,
workflowRunningData: undefined,
})
handleNodeCancelRunningStatus()
handleEdgeCancelRunningStatus()
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
return {
handleCancelDebugAndPreviewPanel,
}
}
export const useWorkflowMoveMode = () => {
const setControlMode = useStore(s => s.setControlMode)
const {
getNodesReadOnly,
} = useNodesReadOnly()
const { handleSelectionCancel } = useSelectionInteractions()
const handleModePointer = useCallback(() => {
if (getNodesReadOnly())
return
setControlMode(ControlMode.Pointer)
}, [getNodesReadOnly, setControlMode])
const handleModeHand = useCallback(() => {
if (getNodesReadOnly())
return
setControlMode(ControlMode.Hand)
handleSelectionCancel()
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
return {
handleModePointer,
handleModeHand,
}
}
export const useWorkflowOrganize = () => {
const workflowStore = useWorkflowStore()
const store = useStoreApi()
const reactflow = useReactFlow()
const { getNodesReadOnly } = useNodesReadOnly()
const { saveStateToHistory } = useWorkflowHistory()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const handleLayout = useCallback(async () => {
if (getNodesReadOnly())
return
workflowStore.setState({ nodeAnimation: true })
const {
getNodes,
edges,
setNodes,
} = store.getState()
const { setViewport } = reactflow
const nodes = getNodes()
const layout = getLayoutByDagre(nodes, edges)
const rankMap = {} as Record<string, Node>
nodes.forEach((node) => {
if (!node.parentId && node.type === CUSTOM_NODE) {
const rank = layout.node(node.id).rank!
if (!rankMap[rank]) {
rankMap[rank] = node
}
else {
if (rankMap[rank].position.y > node.position.y)
rankMap[rank] = node
}
}
})
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
if (!node.parentId && node.type === CUSTOM_NODE) {
const nodeWithPosition = layout.node(node.id)
node.position = {
x: nodeWithPosition.x - node.width! / 2,
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
}
}
})
})
setNodes(newNodes)
const zoom = 0.7
setViewport({
x: 0,
y: 0,
zoom,
})
saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
setTimeout(() => {
handleSyncWorkflowDraft()
})
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
return {
handleLayout,
}
}
export const useWorkflowZoom = () => {
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { getWorkflowReadOnly } = useWorkflowReadOnly()
const {
zoomIn,
zoomOut,
zoomTo,
fitView,
} = useReactFlow()
const handleFitView = useCallback(() => {
if (getWorkflowReadOnly())
return
fitView()
handleSyncWorkflowDraft()
}, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
const handleBackToOriginalSize = useCallback(() => {
if (getWorkflowReadOnly())
return
zoomTo(1)
handleSyncWorkflowDraft()
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
const handleSizeToHalf = useCallback(() => {
if (getWorkflowReadOnly())
return
zoomTo(0.5)
handleSyncWorkflowDraft()
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
const handleZoomOut = useCallback(() => {
if (getWorkflowReadOnly())
return
zoomOut()
handleSyncWorkflowDraft()
}, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
const handleZoomIn = useCallback(() => {
if (getWorkflowReadOnly())
return
zoomIn()
handleSyncWorkflowDraft()
}, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
return {
handleFitView,
handleBackToOriginalSize,
handleSizeToHalf,
handleZoomOut,
handleZoomIn,
}
}
export const useWorkflowUpdate = () => {
const reactflow = useReactFlow()
const workflowStore = useWorkflowStore()
const { eventEmitter } = useEventEmitterContextContext()
const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdater) => {
const {
nodes,
edges,
viewport,
} = payload
const { setViewport } = reactflow
eventEmitter?.emit({
type: WORKFLOW_DATA_UPDATE,
payload: {
nodes: initialNodes(nodes, edges),
edges: initialEdges(edges, nodes),
},
} as any)
setViewport(viewport)
}, [eventEmitter, reactflow])
const handleRefreshWorkflowDraft = useCallback(() => {
const {
appId,
setSyncWorkflowDraftHash,
setIsSyncingWorkflowDraft,
setEnvironmentVariables,
setEnvSecrets,
setConversationVariables,
} = workflowStore.getState()
setIsSyncingWorkflowDraft(true)
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdater)
setSyncWorkflowDraftHash(response.hash)
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
acc[env.id] = env.value
return acc
}, {} as Record<string, string>))
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
// #TODO chatVar sync#
setConversationVariables(response.conversation_variables || [])
}).finally(() => setIsSyncingWorkflowDraft(false))
}, [handleUpdateWorkflowCanvas, workflowStore])
return {
handleUpdateWorkflowCanvas,
handleRefreshWorkflowDraft,
}
}
export const useDSL = () => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { eventEmitter } = useEventEmitterContextContext()
const [exporting, setExporting] = useState(false)
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const appDetail = useAppStore(s => s.appDetail)
const handleExportDSL = useCallback(async (include = false) => {
if (!appDetail)
return
if (exporting)
return
try {
setExporting(true)
await doSyncWorkflowDraft()
const { data } = await exportAppConfig({
appID: appDetail.id,
include,
})
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${appDetail.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
finally {
setExporting(false)
}
}, [appDetail, notify, t, doSyncWorkflowDraft, exporting])
const exportCheck = useCallback(async () => {
if (!appDetail)
return
try {
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
if (list.length === 0) {
handleExportDSL()
return
}
eventEmitter?.emit({
type: DSL_EXPORT_CHECK,
payload: {
data: list,
},
} as any)
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}, [appDetail, eventEmitter, handleExportDSL, notify, t])
return {
exportCheck,
handleExportDSL,
}
}