fix(web): keep workflow ref updates out of render (#40428)

This commit is contained in:
yyh
2026-08-10 22:38:04 +08:00
committed by GitHub
parent 4eb9a24997
commit 6a9efa8ffd
17 changed files with 318 additions and 154 deletions
@@ -1,8 +1,7 @@
'use client'
import type { ReactNode } from 'react'
import type { PluginInstallPermissionStore } from '../hooks/use-plugin-install-permission'
import { use, useEffect, useRef } from 'react'
import { use, useEffect, useState } from 'react'
import {
createPluginInstallPermissionStore,
PluginInstallPermissionContext,
@@ -21,29 +20,23 @@ export const PluginInstallPermissionProvider = ({
currentDifyVersion,
children,
}: PluginInstallPermissionProviderProps) => {
const storeRef = useRef<PluginInstallPermissionStore | null>(null)
if (!storeRef.current) {
storeRef.current = createPluginInstallPermissionStore({
const [store] = useState(() =>
createPluginInstallPermissionStore({
canInstallPlugin,
canUpdatePlugin,
currentDifyVersion,
})
}
}),
)
useEffect(() => {
storeRef.current?.getState().setPluginInstallPermission({
store.getState().setPluginInstallPermission({
canInstallPlugin,
canUpdatePlugin: canUpdatePlugin ?? canInstallPlugin,
currentDifyVersion,
})
}, [canInstallPlugin, canUpdatePlugin, currentDifyVersion])
}, [canInstallPlugin, canUpdatePlugin, currentDifyVersion, store])
return (
<PluginInstallPermissionContext value={storeRef.current}>
{children}
</PluginInstallPermissionContext>
)
return <PluginInstallPermissionContext value={store}>{children}</PluginInstallPermissionContext>
}
export const PluginInstallPermissionProviderGuard = ({
@@ -50,7 +50,9 @@ vi.mock('../components/rag-pipeline-main', () => ({
}))
vi.mock('@/app/components/workflow', () => ({
default: ({ children }: { children: React.ReactNode }) => children,
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="workflow-default-context">{children}</div>
),
}))
vi.mock('@/app/components/workflow/context', () => ({
@@ -60,9 +60,7 @@ vi.mock('@/app/components/workflow', () => ({
}))
vi.mock('@/app/components/workflow/context', () => ({
WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="workflow-context-provider">{children}</div>
),
WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => children,
}))
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
@@ -164,7 +162,6 @@ describe('SnippetPage', () => {
it('should render the orchestrate route shell without owning the main landmark', () => {
render(<SnippetPage snippetId="snippet-1" />)
expect(screen.getByTestId('workflow-context-provider')).toBeInTheDocument()
expect(screen.getByTestId('workflow-default-context')).toBeInTheDocument()
expect(screen.getByTestId('snippet-main')).toHaveTextContent('snippet-1')
expect(screen.queryByRole('main')).not.toBeInTheDocument()
@@ -140,8 +140,24 @@ export function StepByStepTourCoachmark({
const measuredRectMatchesGuide =
measuredTargetElement === targetElement &&
targetElement.matches(getStepByStepTourTargetSelector(guide.target))
const currentOverlayReady = highlightPartsReady && rectSettled && measuredRectMatchesGuide
const currentOverlay = currentOverlayReady
? {
coachmarkPosition,
guide,
highlightRect,
onComplete,
onSkip,
placement: coachmarkPosition.placement,
skipLabel,
interactionPolicy,
stepLabel,
}
: undefined
useLayoutEffect(() => {
if (!currentOverlayReady) return
if (highlightPartsReady && rectSettled && measuredRectMatchesGuide) {
stableOverlayRef.current = {
coachmarkPosition,
guide,
@@ -153,9 +169,19 @@ export function StepByStepTourCoachmark({
interactionPolicy,
stepLabel,
}
}
}, [
coachmarkPosition,
currentOverlayReady,
guide,
highlightRect,
interactionPolicy,
onComplete,
onSkip,
skipLabel,
stepLabel,
])
const stableOverlay = stableOverlayRef.current
const stableOverlay = currentOverlay ?? stableOverlayRef.current
const isActionGuide = stableOverlay
? getStepByStepTourGuideKind(stableOverlay.guide) === 'action'
: false
@@ -142,7 +142,6 @@ export const useStepByStepTourTargetRect = (
getInitialTargetRects(targetElement, highlightPartSelectors),
)
const targetRectsRef = useRef(targetRects)
targetRectsRef.current = targetRects
useLayoutEffect(() => {
let animationFrame = 0
@@ -135,7 +135,9 @@ export function useConfigureButton(options: UseConfigureButtonOptions) {
const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools()
const invalidateDetailRef = useRef(invalidateDetail)
invalidateDetailRef.current = invalidateDetail
useEffect(() => {
invalidateDetailRef.current = invalidateDetail
}, [invalidateDetail])
// Refetch when detailNeedUpdate becomes true
useEffect(() => {
@@ -185,12 +185,7 @@ vi.mock('@/app/components/workflow', () => ({
}))
vi.mock('@/app/components/workflow/context', () => ({
WorkflowContextProvider: ({
children,
}: {
injectWorkflowStoreSliceFn: unknown
children: ReactNode
}) => <div data-testid="workflow-context-provider">{children}</div>,
WorkflowContextProvider: ({ children }: { children: ReactNode }) => children,
}))
vi.mock('@/app/components/workflow-app/components/workflow-main', () => ({
@@ -277,7 +272,6 @@ describe('WorkflowApp', () => {
render(<WorkflowApp />)
expect(screen.getByTestId('workflow-context-provider')).toBeInTheDocument()
expect(screen.getByTestId('workflow-default-context')).toHaveAttribute(
'data-nodes',
JSON.stringify([{ id: 'node-1' }]),
@@ -0,0 +1,139 @@
import { screen } from '@testing-library/react'
import { StrictMode } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { render } from '@/test/console/render'
import WorkflowApp from '../index'
vi.mock('@/context/account-state', async () => {
const { createAccountStateModuleMock } = await import('@/test/console/state-fixture')
return createAccountStateModuleMock(() => ({
userProfile: { id: 'user-1' },
}))
})
vi.mock('@/context/workspace-state', async () => {
const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture')
return createWorkspaceStateModuleMock(() => ({
currentWorkspace: { id: 'workspace-1' },
isLoadingCurrentWorkspace: false,
}))
})
vi.mock('@/context/permission-state', async () => {
const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture')
return createPermissionStateModuleMock(() => ({
workspacePermissionKeys: [],
}))
})
vi.mock('@/next/navigation', () => ({
useSearchParams: () => ({ get: () => null }),
}))
vi.mock('@/service/use-tools', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/service/use-tools')>()
return {
...actual,
useAppTriggers: () => ({}),
}
})
vi.mock('../hooks/use-get-run-and-trace-url', () => ({
useGetRunAndTraceUrl: () => ({
getWorkflowRunAndTraceUrl: () => ({ runUrl: '' }),
}),
}))
vi.mock('../hooks/use-workflow-init', async () => {
const React = await import('react')
const { useStore, useWorkflowStore } = await import('@/app/components/workflow/store')
return {
useWorkflowInit: () => {
useStore((state) => state.appId)
const workflowStore = useWorkflowStore()
React.useEffect(() => {
workflowStore.setState({ appId: 'initialized-app' })
}, [workflowStore])
return {
data: {
graph: {
nodes: [{ id: 'raw-node' }],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
},
features: {},
},
isLoading: false,
fileUploadConfigResponse: null,
}
},
}
})
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
return {
...actual,
initialNodes: () => [
{
id: 'node-1',
type: 'custom',
position: { x: 0, y: 0 },
data: { title: 'Start', type: 'start' },
},
],
initialEdges: () => [],
}
})
vi.mock('@/app/components/workflow/persistence/local-storage-bridge', () => ({
WorkflowLocalStorageBridge: () => null,
}))
vi.mock('../components/workflow-main', async () => {
const { useStore } = await import('@/app/components/workflow/store')
const { useWorkflowHistoryStore } =
await import('@/app/components/workflow/workflow-history-store')
const WorkflowMainProbe = () => {
const appId = useStore((state) => state.appId)
const hasWorkflowSlice = useStore((state) => typeof state.setNotInitialWorkflow === 'function')
const { store } = useWorkflowHistoryStore()
return (
<div>
{`app:${appId} history:${store.getState().nodes.length} slice:${String(hasWorkflowSlice)}`}
</div>
)
}
return {
default: WorkflowMainProbe,
}
})
describe('WorkflowApp provider contract', () => {
beforeEach(() => {
useAppStore.setState({
appDetail: {
id: 'app-1',
name: 'Workflow App',
mode: 'workflow',
permission_keys: [],
} as never,
})
})
it('shares one workflow store between initialization and the initialized canvas', async () => {
render(
<StrictMode>
<WorkflowApp />
</StrictMode>,
)
expect(await screen.findByText('app:initialized-app history:1 slice:true')).toBeInTheDocument()
})
})
+3 -5
View File
@@ -1,6 +1,6 @@
import type { StateCreator } from 'zustand'
import type { SliceFromInjection } from './store/workflow'
import { createContext, useRef } from 'react'
import { createContext, useState } from 'react'
import { createWorkflowStore } from './store/workflow'
type WorkflowStore = ReturnType<typeof createWorkflowStore>
@@ -14,9 +14,7 @@ export const WorkflowContextProvider = ({
children,
injectWorkflowStoreSliceFn,
}: WorkflowProviderProps) => {
const storeRef = useRef<WorkflowStore | undefined>(undefined)
const [store] = useState(() => createWorkflowStore({ injectWorkflowStoreSliceFn }))
if (!storeRef.current) storeRef.current = createWorkflowStore({ injectWorkflowStoreSliceFn })
return <WorkflowContext.Provider value={storeRef.current}>{children}</WorkflowContext.Provider>
return <WorkflowContext.Provider value={store}>{children}</WorkflowContext.Provider>
}
@@ -1,7 +1,7 @@
import type { FC } from 'react'
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
import type { CommonNodeType, Node } from '../types'
import { createContext, useCallback, useEffect, useRef } from 'react'
import { createContext, useCallback, useEffect, useState } from 'react'
import { fetchDatasets } from '@/service/datasets'
import { BlockEnum } from '../types'
import { createDatasetsDetailStore } from './store'
@@ -18,21 +18,21 @@ type DatasetsDetailProviderProps = {
}
const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({ nodes, children }) => {
const storeRef = useRef<DatasetsDetailStoreApi>(undefined)
const [store] = useState(createDatasetsDetailStore)
if (!storeRef.current) storeRef.current = createDatasetsDetailStore()
const updateDatasetsDetail = useCallback(async (datasetIds: string[]) => {
const { data: datasetsDetail } = await fetchDatasets({
url: '/datasets',
params: { page: 1, ids: datasetIds },
})
if (datasetsDetail && datasetsDetail.length > 0)
storeRef.current!.getState().updateDatasetsDetail(datasetsDetail)
}, [])
const updateDatasetsDetail = useCallback(
async (datasetIds: string[]) => {
const { data: datasetsDetail } = await fetchDatasets({
url: '/datasets',
params: { page: 1, ids: datasetIds },
})
if (datasetsDetail && datasetsDetail.length > 0)
store.getState().updateDatasetsDetail(datasetsDetail)
},
[store],
)
useEffect(() => {
if (!storeRef.current) return
const knowledgeRetrievalNodes = nodes.filter(
(node) => node.data.type === BlockEnum.KnowledgeRetrieval,
)
@@ -45,11 +45,7 @@ const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({ nodes, childr
updateDatasetsDetail(allDatasetIds)
}, [])
return (
<DatasetsDetailContext.Provider value={storeRef.current!}>
{children}
</DatasetsDetailContext.Provider>
)
return <DatasetsDetailContext.Provider value={store}>{children}</DatasetsDetailContext.Provider>
}
export default DatasetsDetailProvider
@@ -1,5 +1,5 @@
import type { Shape } from './store'
import { createContext, useEffect, useRef } from 'react'
import { createContext, useEffect, useState } from 'react'
import { useStore } from 'reactflow'
import { createHooksStore } from './store'
@@ -12,22 +12,18 @@ export const HooksStoreContextProvider = ({
children,
...restProps
}: HooksStoreContextProviderProps) => {
const storeRef = useRef<HooksStore | undefined>(undefined)
const [store] = useState(() => createHooksStore(restProps))
const d3Selection = useStore((s) => s.d3Selection)
const d3Zoom = useStore((s) => s.d3Zoom)
const { accessControl } = restProps
useEffect(() => {
if (storeRef.current && d3Selection && d3Zoom) storeRef.current.getState().refreshAll(restProps)
}, [d3Selection, d3Zoom])
if (d3Selection && d3Zoom) store.getState().refreshAll(restProps)
}, [d3Selection, d3Zoom, store])
useEffect(() => {
if (storeRef.current && accessControl) storeRef.current.getState().refreshAll({ accessControl })
}, [accessControl])
if (accessControl) store.getState().refreshAll({ accessControl })
}, [accessControl, store])
if (!storeRef.current) storeRef.current = createHooksStore(restProps)
return (
<HooksStoreContext.Provider value={storeRef.current}>{children}</HooksStoreContext.Provider>
)
return <HooksStoreContext.Provider value={store}>{children}</HooksStoreContext.Provider>
}
+18 -13
View File
@@ -4,6 +4,7 @@ import type { FC } from 'react'
import type { Viewport } from 'reactflow'
import type { CursorPosition, OnlineUser } from './collaboration/types/collaboration'
import type { Shape as HooksStoreShape } from './hooks-store'
import type { WorkflowHistoryState } from './store/workflow/history-slice'
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
import type { ConversationVariable, Edge, EnvironmentVariable, Node } from './types'
import type { EventEmitterValue } from '@/context/event-emitter'
@@ -29,6 +30,7 @@ import {
useCallback,
useEffect,
useEffectEvent,
useLayoutEffect,
useMemo,
useRef,
useState,
@@ -848,20 +850,23 @@ const WorkflowHistoryStoreInitializer = ({
children,
}: WorkflowWithDefaultContextProps) => {
const workflowStore = useWorkflowStore()
const initializedRef = useRef(false)
const workflowHistory = useStore((state) => state.workflowHistory)
const [initialWorkflowHistory] = useState<WorkflowHistoryState>(() => ({
nodes,
edges,
workflowHistoryEvent: undefined,
workflowHistoryEventMeta: undefined,
}))
if (!initializedRef.current) {
workflowStore.temporal.getState().pause()
workflowStore.getState().setWorkflowHistory({
nodes,
edges,
workflowHistoryEvent: undefined,
workflowHistoryEventMeta: undefined,
})
workflowStore.temporal.getState().clear()
workflowStore.temporal.getState().resume()
initializedRef.current = true
}
useLayoutEffect(() => {
const temporalStore = workflowStore.temporal.getState()
temporalStore.pause()
workflowStore.getState().setWorkflowHistory(initialWorkflowHistory)
temporalStore.clear()
temporalStore.resume()
}, [initialWorkflowHistory, workflowStore])
if (workflowHistory !== initialWorkflowHistory) return null
return children
}
@@ -89,10 +89,12 @@ export function useWorkflowInlineAgentConfigureSync({
consoleQuery.snippets.bySnippetId.workflows.draft.nodes.byNodeId.agentComposer.put.mutationOptions(),
)
baseConfigRef.current = baseConfig
currentModelRef.current = currentModel
enabledRef.current = enabled
onDraftSavedRef.current = onDraftSaved
useEffect(() => {
baseConfigRef.current = baseConfig
currentModelRef.current = currentModel
enabledRef.current = enabled
onDraftSavedRef.current = onDraftSaved
}, [baseConfig, currentModel, enabled, onDraftSaved])
const getAgentSoulDraft = useCallback(
() =>
@@ -170,9 +172,11 @@ export function useWorkflowInlineAgentConfigureSync({
)
const latestDraftSaveRef = useRef<() => void>(() => undefined)
latestDraftSaveRef.current = () => {
void saveComposer(getAgentSoulDraft())
}
useEffect(() => {
latestDraftSaveRef.current = () => {
void saveComposer(getAgentSoulDraft())
}
}, [getAgentSoulDraft, saveComposer])
const debouncedSaveDraft = useMemo(
() =>
@@ -32,6 +32,34 @@ type FormContentProps = {
readonly?: boolean
}
type AddInputFieldConfig = {
nodeId: string
unavailableVariableNames: string[]
handleInsertHITLNode: (onInsert: ShortcutPopupInsertHandler) => (payload: FormInputItem) => void
}
const AddInputFieldConfigContext = React.createContext<AddInputFieldConfig | null>(null)
const AddInputFieldShortcutPopup = ({
onClose,
onInsert,
}: {
onClose: () => void
onInsert: ShortcutPopupInsertHandler
}) => {
const config = React.use(AddInputFieldConfigContext)
if (!config) throw new Error('Missing AddInputFieldConfigContext provider')
return (
<AddInputField
nodeId={config.nodeId}
unavailableVariableNames={config.unavailableVariableNames}
onSave={config.handleInsertHITLNode(onInsert)}
onCancel={onClose}
/>
)
}
const FormContent: FC<FormContentProps> = ({
nodeId,
value,
@@ -108,16 +136,14 @@ const FormContent: FC<FormContentProps> = ({
const unavailableVariableNames = useMemo(() => {
return formInputs.map((input) => input.output_variable_name)
}, [formInputs])
const addInputFieldConfigRef = useRef({
nodeId,
unavailableVariableNames,
handleInsertHITLNode,
})
addInputFieldConfigRef.current = {
nodeId,
unavailableVariableNames,
handleInsertHITLNode,
}
const addInputFieldConfig = useMemo(
() => ({
nodeId,
unavailableVariableNames,
handleInsertHITLNode,
}),
[handleInsertHITLNode, nodeId, unavailableVariableNames],
)
const shortcutPopups = useMemo(() => {
if (readonly) return []
@@ -125,27 +151,7 @@ const FormContent: FC<FormContentProps> = ({
{
hotkey: ['mod', '/'],
displayMode: 'workflow-panel-adjacent-center' as const,
// Keep this component type stable while the popup is open; it reads fresh props from a ref.
// oxlint-disable-next-line eslint-react/no-nested-component-definitions
Popup: ({
onClose,
onInsert,
}: {
onClose: () => void
onInsert: ShortcutPopupInsertHandler
}) => {
const { nodeId, unavailableVariableNames, handleInsertHITLNode } =
addInputFieldConfigRef.current
return (
<AddInputField
nodeId={nodeId}
unavailableVariableNames={unavailableVariableNames}
onSave={handleInsertHITLNode(onInsert)}
onCancel={onClose}
/>
)
},
Popup: AddInputFieldShortcutPopup,
},
]
}, [readonly])
@@ -160,35 +166,39 @@ const FormContent: FC<FormContentProps> = ({
)}
>
<div className={cn('max-h-75 overflow-y-auto px-3', isExpand && 'h-0 max-h-full grow')}>
<PromptEditor
key={editorKey}
value={value}
onChange={onChange}
className={cn('min-h-20', isExpand && 'h-full')}
onFocus={setFocus}
onBlur={setBlur}
placeholder={t(($) => $['nodes.humanInput.formContent.placeholder'], { ns: 'workflow' })}
hitlInputBlock={{
show: true,
formInputs,
nodeId,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
variables: availableVars || [],
workflowNodesMap,
getVarType,
readonly,
}}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType,
workflowNodesMap,
}}
editable={!readonly}
shortcutPopups={shortcutPopups}
/>
<AddInputFieldConfigContext value={addInputFieldConfig}>
<PromptEditor
key={editorKey}
value={value}
onChange={onChange}
className={cn('min-h-20', isExpand && 'h-full')}
onFocus={setFocus}
onBlur={setBlur}
placeholder={t(($) => $['nodes.humanInput.formContent.placeholder'], {
ns: 'workflow',
})}
hitlInputBlock={{
show: true,
formInputs,
nodeId,
onFormInputsChange,
onFormInputItemRename,
onFormInputItemRemove,
variables: availableVars || [],
workflowNodesMap,
getVarType,
readonly,
}}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType,
workflowNodesMap,
}}
editable={!readonly}
shortcutPopups={shortcutPopups}
/>
</AddInputFieldConfigContext>
</div>
{isFocus && (
<div className="flex h-8 shrink-0 items-center px-3 system-xs-regular text-components-input-text-placeholder">
@@ -11,7 +11,7 @@ import {
} from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useRef } from 'react'
import { useCallback, useLayoutEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { useHooksStore } from '@/app/components/workflow/hooks-store/store'
@@ -111,7 +111,9 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ id, data }) => {
)
const modelSelectionKeyRef = useRef(modelSelectionKey)
const modelSelectionRequestGenerationRef = useRef(0)
modelSelectionKeyRef.current = modelSelectionKey
useLayoutEffect(() => {
modelSelectionKeyRef.current = modelSelectionKey
}, [modelSelectionKey])
const handleModelChange = useCallback(
(model: { provider: string; modelId: string; mode?: string }) => {
@@ -9,7 +9,7 @@ import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { cloneDeep } from 'es-toolkit/object'
import { isEqual } from 'es-toolkit/predicate'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
@@ -452,7 +452,9 @@ const EnvPanel = () => {
const committedEnvListRef = useRef(envList)
const latestEnvListRef = useRef(envList)
const pendingSaveEnvIdsRef = useRef(new Map<string, number>())
latestEnvListRef.current = envList
useLayoutEffect(() => {
latestEnvListRef.current = envList
}, [envList])
useEffect(() => {
if (pendingSaveEnvIdsRef.current.size === 0) committedEnvListRef.current = envList
@@ -1,7 +1,7 @@
import type { HotkeyCallback, UseHotkeyDefinition, UseHotkeyOptions } from '@tanstack/react-hotkeys'
import type { WorkflowCanvasHotkeyDefinition, WorkflowCanvasHotkeyMeta } from './definitions'
import { useHotkeys, useKeyHold } from '@tanstack/react-hotkeys'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useCallback, useEffect, useEffectEvent, useMemo, useRef } from 'react'
import { useReactFlow } from 'reactflow'
import { collaborationManager } from '../collaboration/core/collaboration-manager'
import { useEdgesInteractions } from '../hooks/use-edges-interactions'
@@ -70,8 +70,7 @@ export const useWorkflowHotkeys = (): void => {
const { zoomTo, getZoom, fitView, getNodes } = useReactFlow()
const isShiftHeld = useKeyHold(WORKFLOW_CANVAS_SHORTCUTS['workflow.dim-other-nodes'].holdKey)
const shiftDimmedRef = useRef(false)
const undimAllNodesRef = useRef(undimAllNodes)
undimAllNodesRef.current = undimAllNodes
const undimAllNodesOnUnmount = useEffectEvent(undimAllNodes)
const constrainedZoomOut = useCallback(() => {
const currentZoom = getZoom()
@@ -230,7 +229,7 @@ export const useWorkflowHotkeys = (): void => {
useEffect(() => {
return () => {
if (shiftDimmedRef.current) undimAllNodesRef.current()
if (shiftDimmedRef.current) undimAllNodesOnUnmount()
}
}, [])
}