mirror of
https://github.com/langgenius/dify.git
synced 2026-09-01 15:09:21 +08:00
fix(web): keep workflow ref updates out of render (#40428)
This commit is contained in:
+8
-15
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}, [])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user