mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 18:06:14 +08:00
fix workflow view switch refresh
This commit is contained in:
@ -11,6 +11,7 @@ import { useResetWorkflowVersionHistory } from '@/service/use-workflow'
|
||||
import { useIsChatMode } from '../../hooks'
|
||||
import ChatVariableTrigger from './chat-variable-trigger'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
import ViewPickerTrigger from './view-picker-trigger'
|
||||
|
||||
const WorkflowHeader = () => {
|
||||
const { appDetail, setCurrentLogItem, setShowMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
@ -37,6 +38,7 @@ const WorkflowHeader = () => {
|
||||
return {
|
||||
normal: {
|
||||
components: {
|
||||
left: <ViewPickerTrigger />,
|
||||
middle: <FeaturesTrigger />,
|
||||
chatVariableTrigger: <ChatVariableTrigger />,
|
||||
},
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback } from 'react'
|
||||
import { useFeatures } from '@/app/components/base/features/hooks'
|
||||
import { ViewType } from '@/app/components/workflow/types'
|
||||
import ViewPicker from '@/app/components/workflow/view-picker'
|
||||
import { parseAsViewType, WORKFLOW_VIEW_PARAM_KEY } from '../../search-params'
|
||||
|
||||
const ViewPickerTrigger = () => {
|
||||
const isSupportSandbox = useFeatures(s => !!s.features.sandbox?.enabled)
|
||||
const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType)
|
||||
const handleViewTypeChange = useCallback((type: ViewType) => {
|
||||
doSetViewType(type)
|
||||
}, [doSetViewType])
|
||||
|
||||
if (!isSupportSandbox)
|
||||
return null
|
||||
|
||||
return (
|
||||
<ViewPicker
|
||||
value={viewType}
|
||||
onChange={handleViewTypeChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ViewPickerTrigger
|
||||
@ -21,8 +21,9 @@ export const useWorkflowRefreshDraft = () => {
|
||||
debouncedSyncWorkflowDraft,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (debouncedSyncWorkflowDraft && typeof (debouncedSyncWorkflowDraft as any).cancel === 'function')
|
||||
(debouncedSyncWorkflowDraft as any).cancel()
|
||||
const { cancel } = debouncedSyncWorkflowDraft
|
||||
if (typeof cancel === 'function')
|
||||
cancel()
|
||||
|
||||
const wasLoaded = isWorkflowDataLoaded
|
||||
if (wasLoaded)
|
||||
|
||||
@ -10,8 +10,8 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { FeaturesProvider } from '@/app/components/base/features'
|
||||
@ -21,6 +21,7 @@ import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import {
|
||||
WorkflowContextProvider,
|
||||
} from '@/app/components/workflow/context'
|
||||
import { HeaderShell } from '@/app/components/workflow/header'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
|
||||
import {
|
||||
@ -36,7 +37,7 @@ import { fetchRunDetail } from '@/service/log'
|
||||
import { useAppTriggers } from '@/service/use-tools'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { useFeatures } from '../base/features/hooks'
|
||||
import ViewPicker from '../workflow/view-picker'
|
||||
import ViewPickerTrigger from './components/workflow-header/view-picker-trigger'
|
||||
import WorkflowAppMain from './components/workflow-main'
|
||||
import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url'
|
||||
import { useNodesSyncDraft } from './hooks/use-nodes-sync-draft'
|
||||
@ -55,52 +56,85 @@ type WorkflowViewContentProps = {
|
||||
reload: () => Promise<void>
|
||||
}
|
||||
|
||||
const WorkflowViewPickerDock = () => {
|
||||
return (
|
||||
<HeaderShell>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<ViewPickerTrigger />
|
||||
</div>
|
||||
</div>
|
||||
</HeaderShell>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowViewContent = ({
|
||||
graphContent,
|
||||
reload,
|
||||
}: WorkflowViewContentProps) => {
|
||||
const features = useFeatures(s => s.features)
|
||||
const isSupportSandbox = !!features.sandbox?.enabled
|
||||
const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType)
|
||||
const [viewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType)
|
||||
const { syncWorkflowDraftImmediately } = useNodesSyncDraft()
|
||||
const [isGraphRefreshing, setGraphRefreshing] = useReducer((_: boolean, next: boolean) => next, false)
|
||||
const pendingSyncRef = useRef<Promise<void> | null>(null)
|
||||
const [isGraphRefreshing, setIsGraphRefreshing] = useState(false)
|
||||
const refreshInFlightRef = useRef<Promise<void> | null>(null)
|
||||
const previousViewTypeRef = useRef<ViewType | null>(viewType)
|
||||
const viewTypeRef = useRef(viewType)
|
||||
|
||||
const refreshGraph = useCallback(() => {
|
||||
setIsGraphRefreshing(true)
|
||||
return reload().finally(() => {
|
||||
setIsGraphRefreshing(false)
|
||||
})
|
||||
const refreshGraph = useCallback((waitFor?: Promise<void>) => {
|
||||
if (refreshInFlightRef.current)
|
||||
return refreshInFlightRef.current
|
||||
|
||||
setGraphRefreshing(true)
|
||||
const runRefresh = () => {
|
||||
if (viewTypeRef.current !== ViewType.graph) {
|
||||
refreshInFlightRef.current = null
|
||||
setGraphRefreshing(false)
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return reload().finally(() => {
|
||||
refreshInFlightRef.current = null
|
||||
setGraphRefreshing(false)
|
||||
})
|
||||
}
|
||||
refreshInFlightRef.current = waitFor ? waitFor.then(runRefresh, runRefresh) : runRefresh()
|
||||
return refreshInFlightRef.current
|
||||
}, [reload])
|
||||
|
||||
const handleViewTypeChange = useCallback((type: ViewType) => {
|
||||
if (viewType === ViewType.graph && type !== viewType)
|
||||
pendingSyncRef.current = syncWorkflowDraftImmediately(true).catch(() => { })
|
||||
useEffect(() => {
|
||||
viewTypeRef.current = viewType
|
||||
if (!isSupportSandbox) {
|
||||
previousViewTypeRef.current = viewType
|
||||
return
|
||||
}
|
||||
|
||||
doSetViewType(type)
|
||||
if (type === ViewType.graph) {
|
||||
const pending = pendingSyncRef.current
|
||||
if (pending) {
|
||||
const previousView = previousViewTypeRef.current
|
||||
if (previousView === ViewType.graph && viewType !== ViewType.graph) {
|
||||
if (!pendingSyncRef.current) {
|
||||
const pending = syncWorkflowDraftImmediately(true).catch(() => { })
|
||||
pendingSyncRef.current = pending
|
||||
pending.finally(() => {
|
||||
refreshGraph()
|
||||
if (pendingSyncRef.current === pending)
|
||||
pendingSyncRef.current = null
|
||||
})
|
||||
pendingSyncRef.current = null
|
||||
}
|
||||
else {
|
||||
refreshGraph()
|
||||
}
|
||||
}
|
||||
}, [doSetViewType, refreshGraph, syncWorkflowDraftImmediately, viewType])
|
||||
|
||||
if (!isSupportSandbox) {
|
||||
if (previousView !== ViewType.graph && viewType === ViewType.graph) {
|
||||
refreshGraph(pendingSyncRef.current ?? undefined)
|
||||
}
|
||||
|
||||
previousViewTypeRef.current = viewType
|
||||
}, [isSupportSandbox, refreshGraph, syncWorkflowDraftImmediately, viewType])
|
||||
|
||||
if (!isSupportSandbox)
|
||||
return graphContent
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<ViewPicker
|
||||
value={viewType}
|
||||
onChange={handleViewTypeChange}
|
||||
/>
|
||||
<div className="relative h-full w-full min-w-[960px]">
|
||||
{viewType !== ViewType.graph ? <WorkflowViewPickerDock /> : null}
|
||||
{viewType === ViewType.graph
|
||||
? (
|
||||
isGraphRefreshing
|
||||
|
||||
@ -61,18 +61,18 @@ const HeaderInNormal = ({
|
||||
setShowChatVariablePanel(false)
|
||||
setShowGlobalVariablePanel(false)
|
||||
closeAllInputFieldPanels()
|
||||
}, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel, setShowGlobalVariablePanel])
|
||||
}, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel, setShowGlobalVariablePanel, closeAllInputFieldPanels])
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="relative top-[30px]">
|
||||
<div className="flex items-center gap-2">
|
||||
{components?.left}
|
||||
<EditingTitle />
|
||||
</div>
|
||||
<div>
|
||||
<ScrollToSelectedNodeButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{components?.left}
|
||||
<Divider type="vertical" className="mx-auto h-3.5" />
|
||||
<RunAndHistory {...runAndHistoryProps} />
|
||||
<div className="shrink-0 cursor-pointer rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs backdrop-blur-[10px]">
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { HeaderInNormalProps } from './header-in-normal'
|
||||
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||
import type { HeaderInHistoryProps } from './header-in-view-history'
|
||||
@ -16,6 +17,26 @@ const HeaderInRestoring = dynamic(() => import('./header-in-restoring'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
type HeaderShellProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const HeaderShell = ({ children }: HeaderShellProps) => {
|
||||
const pathname = usePathname()
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const maximizeCanvas = useStore(s => s.maximizeCanvas)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3"
|
||||
>
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className="h-14 w-[52px]" />}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export type HeaderProps = {
|
||||
normal?: HeaderInNormalProps
|
||||
viewHistory?: HeaderInHistoryProps
|
||||
@ -26,21 +47,14 @@ const Header = ({
|
||||
viewHistory: viewHistoryProps,
|
||||
restoring: restoringProps,
|
||||
}: HeaderProps) => {
|
||||
const pathname = usePathname()
|
||||
const inWorkflowCanvas = pathname.endsWith('/workflow')
|
||||
const isPipelineCanvas = pathname.endsWith('/pipeline')
|
||||
const {
|
||||
normal,
|
||||
restoring,
|
||||
viewHistory,
|
||||
} = useWorkflowMode()
|
||||
const maximizeCanvas = useStore(s => s.maximizeCanvas)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3"
|
||||
>
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className="h-14 w-[52px]" />}
|
||||
<HeaderShell>
|
||||
{
|
||||
normal && (
|
||||
<HeaderInNormal
|
||||
@ -62,7 +76,7 @@ const Header = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</HeaderShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ const ViewPicker: FC<ViewPickerProps> = ({
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
className={cn('absolute left-3 top-3 z-[12] text-text-accent-light-mode-only', className)}
|
||||
className={cn('text-text-accent-light-mode-only', className)}
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
||||
Reference in New Issue
Block a user