mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 15:35:32 +08:00
refactor(web): standardize lazy ref initialization (#42562)
This commit is contained in:
+15
-4
@@ -3,8 +3,9 @@
|
||||
import { DeploymentOperationStatus } from '@dify/contracts/enterprise-app-deploy/types.gen'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { shouldPollEnvironmentDeployment } from '@/app/components/app/deploy/utils/environment-deployment'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import {
|
||||
appPublisherOpenAtom,
|
||||
@@ -17,8 +18,8 @@ import {
|
||||
|
||||
export function useRefreshAppEnvironmentsAfterPublisherDeploymentPolling(appId?: string) {
|
||||
const queryClient = useQueryClient()
|
||||
const operationsNeedingEnvironmentRefreshRef = useRef(new Set<string>())
|
||||
const refreshedOperationKeysRef = useRef(new Set<string>())
|
||||
const operationsNeedingEnvironmentRefreshRef = useRefWithInit(() => new Set<string>())
|
||||
const refreshedOperationKeysRef = useRefWithInit(() => new Set<string>())
|
||||
const open = useAtomValue(appPublisherOpenAtom)
|
||||
const polling = useAtomValue(publisherEnvironmentDeploymentPollingAtom)
|
||||
const environment = useAtomValue(selectedPublisherEnvironmentAtom)
|
||||
@@ -117,5 +118,15 @@ export function useRefreshAppEnvironmentsAfterPublisherDeploymentPolling(appId?:
|
||||
})
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: appEnvironmentsQuery.queryKey })
|
||||
}, [appId, deployment, environment, finishPolling, open, polling, queryClient])
|
||||
}, [
|
||||
appId,
|
||||
deployment,
|
||||
environment,
|
||||
finishPolling,
|
||||
open,
|
||||
polling,
|
||||
queryClient,
|
||||
operationsNeedingEnvironmentRefreshRef,
|
||||
refreshedOperationKeysRef,
|
||||
])
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
|
||||
type Options<T> = {
|
||||
items: T[]
|
||||
@@ -30,7 +31,7 @@ export function useKeyboardSortable<T>({
|
||||
const descriptionId = useId()
|
||||
const [session, setSession] = useState<SortSession<T> | null>(null)
|
||||
const [message, setMessage] = useState('')
|
||||
const handlesRef = useRef(new Map<number, HTMLButtonElement>())
|
||||
const handlesRef = useRefWithInit(() => new Map<number, HTMLButtonElement>())
|
||||
const pendingFocusRef = useRef<number | null>(null)
|
||||
const current =
|
||||
session?.source === items && !disabled && session.minIndex === minIndex ? session : null
|
||||
|
||||
+3
-4
@@ -1,4 +1,5 @@
|
||||
import { createContext, useRef } from 'react'
|
||||
import { createContext } from 'react'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { createDataSourceStore } from './'
|
||||
|
||||
type DataSourceStoreApi = ReturnType<typeof createDataSourceStore>
|
||||
@@ -12,9 +13,7 @@ type DataSourceProviderProps = {
|
||||
}
|
||||
|
||||
const DataSourceProvider = ({ children }: DataSourceProviderProps) => {
|
||||
const storeRef = useRef<DataSourceStoreApi | null>(null)
|
||||
|
||||
if (storeRef.current === null) storeRef.current = createDataSourceStore()
|
||||
const storeRef = useRefWithInit(createDataSourceStore)
|
||||
|
||||
return (
|
||||
<DataSourceContext.Provider value={storeRef.current}>{children}</DataSourceContext.Provider>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTheme } from 'next-themes'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useLocale, useTranslation } from '#i18n'
|
||||
import { useOptionalPluginInstallPermission } from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { getPluginLinkInMarketplace } from '../utils'
|
||||
import MarketplaceDetailDialogFrame from './frame'
|
||||
import { useSilentMarketplaceInstall } from './use-silent-install'
|
||||
@@ -45,14 +46,14 @@ function OpenMarketplaceDetailDialog({
|
||||
title: string
|
||||
}) {
|
||||
const { install } = useSilentMarketplaceInstall()
|
||||
const timeoutIdsRef = useRef(new Set<number>())
|
||||
const timeoutIdsRef = useRefWithInit(() => new Set<number>())
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
timeoutIdsRef.current.forEach((id) => window.clearTimeout(id))
|
||||
timeoutIdsRef.current.clear()
|
||||
},
|
||||
[],
|
||||
[timeoutIdsRef],
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
@@ -96,7 +97,7 @@ function OpenMarketplaceDetailDialog({
|
||||
})
|
||||
})
|
||||
},
|
||||
[canInstallPlugin, install, plugin],
|
||||
[canInstallPlugin, install, plugin, timeoutIdsRef],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { RefObject } from 'react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from '#i18n'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { MARKETPLACE_CONTAINER_ID } from '../constants'
|
||||
import styles from './home-trending.module.css'
|
||||
|
||||
@@ -49,8 +50,8 @@ function TrendingNavigation({
|
||||
const { t } = useTranslation('plugin')
|
||||
const progressRef = useRef<HTMLSpanElement>(null)
|
||||
const progressAnimationRef = useRef<Animation | null>(null)
|
||||
const pauseReasonsRef = useRef(
|
||||
new Set<AutoplayPauseReason>(pauseWhenOffscreen ? ['viewport'] : []),
|
||||
const pauseReasonsRef = useRefWithInit(
|
||||
() => new Set<AutoplayPauseReason>(pauseWhenOffscreen ? ['viewport'] : []),
|
||||
)
|
||||
const [isUserPaused, setIsUserPaused] = useState(false)
|
||||
const [isReducedMotionPaused, setIsReducedMotionPaused] = useState(false)
|
||||
@@ -72,7 +73,7 @@ function TrendingNavigation({
|
||||
if (isPaused) progressAnimation.pause()
|
||||
else progressAnimation.play()
|
||||
},
|
||||
[onPausedChange],
|
||||
[onPausedChange, pauseReasonsRef],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -103,7 +104,7 @@ function TrendingNavigation({
|
||||
progressAnimation.cancel()
|
||||
if (progressAnimationRef.current === progressAnimation) progressAnimationRef.current = null
|
||||
}
|
||||
}, [onNext, selectedIndex])
|
||||
}, [onNext, selectedIndex, pauseReasonsRef])
|
||||
|
||||
useEffect(() => {
|
||||
const carouselRoot = carouselRootRef.current
|
||||
|
||||
@@ -10,6 +10,7 @@ import Divider from '@/app/components/base/divider'
|
||||
import { Infotip } from '@/app/components/base/infotip'
|
||||
import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { useAllMCPTools } from '@/service/use-tools'
|
||||
|
||||
type Props = Readonly<{
|
||||
@@ -45,7 +46,7 @@ const MultipleToolSelector = ({
|
||||
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const addToolButtonRef = React.useRef<HTMLButtonElement>(null)
|
||||
const toolItemTriggerByKeyRef = React.useRef(new Map<string, HTMLButtonElement>())
|
||||
const toolItemTriggerByKeyRef = useRefWithInit(() => new Map<string, HTMLButtonElement>())
|
||||
const pendingFocusTargetRef = React.useRef<{ toolKey?: string } | null>(null)
|
||||
const enabledCount = value.filter((item) => {
|
||||
const isMCPTool = mcpTools?.find((tool) => tool.id === item.provider_name)
|
||||
@@ -65,7 +66,7 @@ const MultipleToolSelector = ({
|
||||
|
||||
resolvedFocusTarget?.focus()
|
||||
pendingFocusTargetRef.current = null
|
||||
}, [value])
|
||||
}, [value, toolItemTriggerByKeyRef])
|
||||
|
||||
// add tool
|
||||
const [selectorOpen, setSelectorOpen] = React.useState(false)
|
||||
|
||||
@@ -9,10 +9,6 @@ const reactFlowState = vi.hoisted(() => ({
|
||||
nodes: [] as Node[],
|
||||
}))
|
||||
|
||||
vi.mock('es-toolkit/compat', () => ({
|
||||
debounce: <T extends (...args: unknown[]) => unknown>(fn: T) => fn,
|
||||
}))
|
||||
|
||||
vi.mock('reactflow', async () => {
|
||||
const actual = await vi.importActual<typeof import('reactflow')>('reactflow')
|
||||
return {
|
||||
@@ -57,8 +53,13 @@ describe('useWorkflowHistory', () => {
|
||||
reactFlowState.edges = edges
|
||||
})
|
||||
|
||||
it('stores the latest workflow graph snapshot for supported events', () => {
|
||||
const { result } = renderWorkflowHook(() => useWorkflowHistory(), {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('coalesces saves across rerenders and stores the latest graph snapshot', () => {
|
||||
vi.useFakeTimers()
|
||||
const { result, rerender } = renderWorkflowHook(() => useWorkflowHistory(), {
|
||||
historyStore: {
|
||||
nodes,
|
||||
edges,
|
||||
@@ -66,8 +67,20 @@ describe('useWorkflowHistory', () => {
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: 'node-1' })
|
||||
result.current.saveStateToHistory(WorkflowHistoryEvent.NodeDelete, { nodeId: 'node-2' })
|
||||
vi.advanceTimersByTime(200)
|
||||
})
|
||||
rerender()
|
||||
reactFlowState.nodes = nodes.map((node) => ({
|
||||
...node,
|
||||
data: { ...node.data, title: 'Updated' },
|
||||
}))
|
||||
act(() => {
|
||||
result.current.saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: 'node-1' })
|
||||
vi.advanceTimersByTime(499)
|
||||
})
|
||||
expect(result.current.store.getState().workflowHistoryEvent).toBeUndefined()
|
||||
act(() => vi.advanceTimersByTime(1))
|
||||
|
||||
expect(result.current.store.getState().workflowHistoryEvent).toBe(WorkflowHistoryEvent.NodeAdd)
|
||||
expect(result.current.store.getState().workflowHistoryEventMeta).toEqual({ nodeId: 'node-1' })
|
||||
@@ -76,7 +89,7 @@ describe('useWorkflowHistory', () => {
|
||||
id: 'node-1',
|
||||
data: expect.objectContaining({
|
||||
selected: false,
|
||||
title: 'Start',
|
||||
title: 'Updated',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
|
||||
export const useSerialAsyncCallback = <Args extends any[], Result = void>(
|
||||
fn: (...args: Args) => Promise<Result> | Result,
|
||||
shouldSkip?: () => boolean,
|
||||
) => {
|
||||
const queueRef = useRef<Promise<unknown>>(Promise.resolve())
|
||||
const queueRef = useRefWithInit<Promise<unknown>>(() => Promise.resolve())
|
||||
|
||||
return useCallback(
|
||||
(...args: Args) => {
|
||||
@@ -16,6 +17,6 @@ export const useSerialAsyncCallback = <Args extends any[], Result = void>(
|
||||
|
||||
return nextPromise
|
||||
},
|
||||
[fn, shouldSkip],
|
||||
[fn, shouldSkip, queueRef],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { WorkflowHistoryEventMeta } from '../store/workflow/history-slice'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
|
||||
/**
|
||||
@@ -63,7 +64,7 @@ export const useWorkflowHistory = () => {
|
||||
// Some events may be triggered multiple times in a short period of time.
|
||||
// We debounce the history state update to avoid creating multiple history states
|
||||
// with minimal changes.
|
||||
const saveStateToHistoryRef = useRef(
|
||||
const saveStateToHistoryRef = useRefWithInit(() =>
|
||||
debounce((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
||||
workflowHistoryStore.setState({
|
||||
workflowHistoryEvent: event,
|
||||
@@ -105,7 +106,7 @@ export const useWorkflowHistory = () => {
|
||||
break
|
||||
}
|
||||
},
|
||||
[],
|
||||
[saveStateToHistoryRef],
|
||||
)
|
||||
|
||||
const getHistoryLabel = useCallback(
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@/app/components/base/prompt-editor/plugins/agent-output-block/utils'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { useNodeDataUpdate } from '../../hooks/use-node-data-update'
|
||||
@@ -119,7 +120,7 @@ export function AgentV2Panel({ id, data }: NodePanelProps<AgentV2NodeType>) {
|
||||
const { t } = useTranslation()
|
||||
const { inputs, setInputs } = useNodeCrud<AgentV2NodeType>(id, data)
|
||||
const inputsRef = useRef(inputs)
|
||||
const promptOutputNamesRef = useRef(extractAgentOutputNames(inputs.agent_task || ''))
|
||||
const promptOutputNamesRef = useRefWithInit(extractAgentOutputNames, inputs.agent_task || '')
|
||||
const [isRosterAgentPanelOpen, setIsRosterAgentPanelOpen] = useState(false)
|
||||
const [isInlineAgentPanelOpenedFromTrigger, setIsInlineAgentPanelOpenedFromTrigger] =
|
||||
useState(false)
|
||||
@@ -208,7 +209,7 @@ export function AgentV2Panel({ id, data }: NodePanelProps<AgentV2NodeType>) {
|
||||
useEffect(() => {
|
||||
inputsRef.current = inputs
|
||||
promptOutputNamesRef.current = extractAgentOutputNames(inputs.agent_task || '')
|
||||
}, [inputs])
|
||||
}, [inputs, promptOutputNamesRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputs._openInlineAgentPanel || !isInlineAgentReady) return
|
||||
@@ -268,7 +269,7 @@ export function AgentV2Panel({ id, data }: NodePanelProps<AgentV2NodeType>) {
|
||||
setLocalDeclaredOutputs(getAgentV2DeclaredOutputs(newInputs))
|
||||
setInputs(newInputs)
|
||||
},
|
||||
[setInputs],
|
||||
[setInputs, promptOutputNamesRef],
|
||||
)
|
||||
|
||||
const handleRosterChange = useCallback(
|
||||
@@ -606,7 +607,7 @@ export function AgentV2Panel({ id, data }: NodePanelProps<AgentV2NodeType>) {
|
||||
},
|
||||
)
|
||||
},
|
||||
[handleNodeDataUpdateWithSyncDraft, id],
|
||||
[handleNodeDataUpdateWithSyncDraft, id, promptOutputNamesRef],
|
||||
)
|
||||
|
||||
const handleEditTaskOutput = useCallback(
|
||||
|
||||
+3
-4
@@ -1,6 +1,7 @@
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { createContext, use, useRef } from 'react'
|
||||
import { createContext, use } from 'react'
|
||||
import { useMitt } from '@/hooks/use-mitt'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { createVisualEditorStore } from './store'
|
||||
|
||||
type VisualEditorStore = ReturnType<typeof createVisualEditorStore>
|
||||
@@ -14,9 +15,7 @@ type VisualEditorProviderProps = {
|
||||
export const VisualEditorContext = createContext<VisualEditorContextType>(null)
|
||||
|
||||
export const VisualEditorContextProvider = ({ children }: VisualEditorProviderProps) => {
|
||||
const storeRef = useRef<VisualEditorStore | null>(null)
|
||||
|
||||
if (!storeRef.current) storeRef.current = createVisualEditorStore()
|
||||
const storeRef = useRefWithInit(createVisualEditorStore)
|
||||
|
||||
return (
|
||||
<VisualEditorContext.Provider value={storeRef.current}>{children}</VisualEditorContext.Provider>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { $getRoot } from 'lexical'
|
||||
import { createContext, memo, useEffect, useRef } from 'react'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { createNoteEditorStore } from './store'
|
||||
import theme from './theme'
|
||||
|
||||
@@ -65,9 +66,7 @@ type NoteEditorContextProviderProps = {
|
||||
}
|
||||
export const NoteEditorContextProvider = memo(
|
||||
({ value, children, editable = true }: NoteEditorContextProviderProps) => {
|
||||
const storeRef = useRef<NoteEditorStore | undefined>(undefined)
|
||||
|
||||
if (!storeRef.current) storeRef.current = createNoteEditorStore()
|
||||
const storeRef = useRefWithInit(createNoteEditorStore)
|
||||
|
||||
let initialValue = null
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ import VariableTrigger from '@/app/components/workflow/panel/env-panel/variable-
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { Resolution } from '@/types/app'
|
||||
import {
|
||||
fetchModelParameterRulesForModel,
|
||||
@@ -447,25 +448,28 @@ const EnvPanel = () => {
|
||||
|
||||
const [showRemoveVarConfirm, setShowRemoveVarConfirm] = useState(false)
|
||||
const [cacheForDelete, setCacheForDelete] = useState<EnvironmentVariable>()
|
||||
const saveOperationQueueRef = useRef(Promise.resolve())
|
||||
const saveOperationQueueRef = useRefWithInit(() => Promise.resolve())
|
||||
const committedEnvListRef = useRef(envList)
|
||||
const latestEnvListRef = useRef(envList)
|
||||
const pendingSaveEnvIdsRef = useRef(new Map<string, number>())
|
||||
const pendingSaveEnvIdsRef = useRefWithInit(() => new Map<string, number>())
|
||||
useLayoutEffect(() => {
|
||||
latestEnvListRef.current = envList
|
||||
}, [envList])
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingSaveEnvIdsRef.current.size === 0) committedEnvListRef.current = envList
|
||||
}, [envList])
|
||||
}, [envList, pendingSaveEnvIdsRef])
|
||||
|
||||
const mergeLatestUntouchedEnvList = useCallback((envListToMerge: EnvironmentVariable[]) => {
|
||||
return mergeUntouchedEnvironmentVariables(
|
||||
envListToMerge,
|
||||
latestEnvListRef.current,
|
||||
new Set(pendingSaveEnvIdsRef.current.keys()),
|
||||
)
|
||||
}, [])
|
||||
const mergeLatestUntouchedEnvList = useCallback(
|
||||
(envListToMerge: EnvironmentVariable[]) => {
|
||||
return mergeUntouchedEnvironmentVariables(
|
||||
envListToMerge,
|
||||
latestEnvListRef.current,
|
||||
new Set(pendingSaveEnvIdsRef.current.keys()),
|
||||
)
|
||||
},
|
||||
[pendingSaveEnvIdsRef],
|
||||
)
|
||||
|
||||
const restoreCommittedEnvList = useCallback(() => {
|
||||
committedEnvListRef.current = mergeLatestUntouchedEnvList(committedEnvListRef.current)
|
||||
@@ -523,7 +527,7 @@ const EnvPanel = () => {
|
||||
saveOperationQueueRef.current = queuedOperation.catch(() => undefined)
|
||||
return queuedOperation
|
||||
},
|
||||
[mergeLatestUntouchedEnvList],
|
||||
[mergeLatestUntouchedEnvList, pendingSaveEnvIdsRef, saveOperationQueueRef],
|
||||
)
|
||||
|
||||
const syncDraftWithResult = useCallback(
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
isAgentComposerDirtyAtom,
|
||||
} from '@/features/agent-v2/agent-composer/store'
|
||||
import { agentComposerToolPresentationIdentitiesAtom } from '@/features/agent-v2/agent-composer/store-modules/tools'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import {
|
||||
getAgentToolPublishIssue,
|
||||
@@ -61,7 +62,7 @@ export function useAgentConfigureSync({
|
||||
const latestAppliedSaveSequenceRef = useRef(0)
|
||||
const nextSaveSequenceRef = useRef(0)
|
||||
const pageCloseSavingDraftKeyRef = useRef<string | undefined>(undefined)
|
||||
const explicitlySavingDraftKeysRef = useRef(new Set<string>())
|
||||
const explicitlySavingDraftKeysRef = useRefWithInit(() => new Set<string>())
|
||||
const publishInFlightRef = useRef(false)
|
||||
|
||||
// The layout can unmount this hook before a revoked permission reaches its props.
|
||||
@@ -314,7 +315,14 @@ export function useAgentConfigureSync({
|
||||
} finally {
|
||||
explicitlySavingDraftKeysRef.current.delete(draftKey)
|
||||
}
|
||||
}, [debouncedSaveDraft, getAgentSoulDraft, saveComposer, store, tCommon])
|
||||
}, [
|
||||
debouncedSaveDraft,
|
||||
getAgentSoulDraft,
|
||||
saveComposer,
|
||||
store,
|
||||
tCommon,
|
||||
explicitlySavingDraftKeysRef,
|
||||
])
|
||||
|
||||
const saveDirtyDraftOnPageClose = useCallback(
|
||||
(allowInFlightDuplicate = false) => {
|
||||
@@ -346,7 +354,13 @@ export function useAgentConfigureSync({
|
||||
pageCloseSavingDraftKeyRef.current = undefined
|
||||
})
|
||||
},
|
||||
[debouncedSaveDraft, getAgentSoulDraft, saveComposerOnPageClose, store],
|
||||
[
|
||||
debouncedSaveDraft,
|
||||
getAgentSoulDraft,
|
||||
saveComposerOnPageClose,
|
||||
store,
|
||||
explicitlySavingDraftKeysRef,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useLocale } from '#i18n'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { Carousel, useCarousel } from '@/app/components/base/carousel'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { BannerItem } from './banner-item'
|
||||
import { IndicatorButton } from './indicator-button'
|
||||
|
||||
@@ -78,7 +79,7 @@ function BannerCarouselContent({
|
||||
}: BannerCarouselContentProps) {
|
||||
const { t } = useTranslation()
|
||||
const { api, selectedIndex } = useCarousel()
|
||||
const trackedBannerKeysRef = useRef(new Set<string>())
|
||||
const trackedBannerKeysRef = useRefWithInit(() => new Set<string>())
|
||||
const nextIndex = (selectedIndex + 1) % banners.length
|
||||
const activeBanner = banners[selectedIndex]
|
||||
const trackingKey = accountId && activeBanner ? `${accountId}:${activeBanner.id}` : null
|
||||
@@ -107,7 +108,7 @@ function BannerCarouselContent({
|
||||
event_time: Date.now(),
|
||||
})
|
||||
trackedBannerKeysRef.current.add(trackingKey)
|
||||
}, [accountId, activeBanner, language, selectedIndex, trackingKey])
|
||||
}, [accountId, activeBanner, language, selectedIndex, trackingKey, trackedBannerKeysRef])
|
||||
|
||||
const controls =
|
||||
banners.length > 1 ? (
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-quer
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { PendingWebsiteSetup, UnavailableConnectedSourceSetup } from './add-source-placeholder'
|
||||
@@ -608,8 +609,9 @@ export function AddSourcePage({
|
||||
const { t } = useTranslation('dataset')
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const initialDraftRef = useRef<NewKnowledgeSourceDraft>(
|
||||
initialSourceDraft ??
|
||||
const initialDraftRef = useRefWithInit<NewKnowledgeSourceDraft>(
|
||||
() =>
|
||||
initialSourceDraft ??
|
||||
createNewKnowledgeSourceDraft(normalizeSourceType(initialSourceType ?? null)),
|
||||
)
|
||||
const [sourceDraft, setSourceDraft] = useState<NewKnowledgeSourceDraft>(initialDraftRef.current)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { taskVersionIsAfter } from './document-model'
|
||||
|
||||
export type AuxiliaryTaskReadGuard = ReturnType<typeof createAuxiliaryTaskReadGuard>
|
||||
|
||||
type RefetchDocuments = (options: { cancelRefetch: true }) => Promise<{ error: unknown }>
|
||||
|
||||
export function createAuxiliaryTaskReadGuard() {
|
||||
@@ -45,8 +44,7 @@ export function useAuxiliaryTaskReadGuard({
|
||||
documentPermissionDenied: boolean
|
||||
refetchDocuments: RefetchDocuments
|
||||
}) {
|
||||
const guardRef = useRef<AuxiliaryTaskReadGuard | null>(null)
|
||||
if (!guardRef.current) guardRef.current = createAuxiliaryTaskReadGuard()
|
||||
const guardRef = useRefWithInit(createAuxiliaryTaskReadGuard)
|
||||
const guard = guardRef.current
|
||||
const previousDocumentPermissionDeniedRef = useRef(documentPermissionDenied)
|
||||
const denialGenerationRef = useRef(0)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SpinnerIcon } from '@langgenius/dify-ui/spinner'
|
||||
import { defaultRangeExtractor, useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { chunkTreeLabel, visibleDocumentChunkNodes } from './document-detail-model'
|
||||
|
||||
const VIRTUALIZATION_THRESHOLD = 80
|
||||
@@ -46,7 +47,7 @@ export function DocumentChunkTreePanel({
|
||||
const [treeHasFocus, setTreeHasFocus] = useState(false)
|
||||
const treeScrollRef = useRef<HTMLDivElement>(null)
|
||||
const loadMoreRequestedRef = useRef(false)
|
||||
const chunkIdsBeforeLoadRef = useRef<Set<string>>(new Set())
|
||||
const chunkIdsBeforeLoadRef = useRefWithInit<Set<string>>(() => new Set())
|
||||
const wasFetchingNextPageRef = useRef(false)
|
||||
const expandedChunkIds = useMemo(
|
||||
() => new Set([...tree.byId.keys()].filter((id) => !collapsedChunkIds.has(id))),
|
||||
@@ -148,7 +149,13 @@ export function DocumentChunkTreePanel({
|
||||
if (shouldVirtualize) rowVirtualizerRef.current.scrollToIndex(index, { align: 'auto' })
|
||||
treeScrollRef.current?.focus()
|
||||
})
|
||||
}, [isFetchNextPageError, isFetchingNextPage, shouldVirtualize, visibleNodes])
|
||||
}, [
|
||||
isFetchNextPageError,
|
||||
isFetchingNextPage,
|
||||
shouldVirtualize,
|
||||
visibleNodes,
|
||||
chunkIdsBeforeLoadRef,
|
||||
])
|
||||
|
||||
const renderTreeItem = (item: (typeof visibleNodes)[number], style?: React.CSSProperties) => {
|
||||
const { depth, node, positionInSet, setSize } = item
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
workspacePermissionKeysFetchingAtom,
|
||||
workspacePermissionKeysLoadingAtom,
|
||||
} from '@/context/permission-state'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleClient, consoleQuery } from '@/service/console'
|
||||
import { DatasetACLPermission, hasPermission } from '@/utils/permission'
|
||||
import { useAuxiliaryTaskReadGuard } from './auxiliary-task-read-guard'
|
||||
@@ -186,21 +187,30 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
>({})
|
||||
const [terminalTaskPins, setTerminalTaskPins] = useState<Record<string, TerminalTaskPin>>({})
|
||||
const [taskObserverGenerations, setTaskObserverGenerations] = useState<Record<string, number>>({})
|
||||
const terminalReconciliationGenerationsRef = useRef(new Map<string, number>())
|
||||
const failedTaskPollGenerationsRef = useRef(new Map<string, number>())
|
||||
const blockedFailedTaskPollVersionsRef = useRef(new Map<string, string>())
|
||||
const failedPollAuxiliaryDenialsRef = useRef(new Map<string, AuxiliaryTaskReadDenial>())
|
||||
const terminalConfirmableAuxiliaryDenialsRef = useRef(new Map<string, AuxiliaryTaskReadDenial>())
|
||||
const equalRetryListGenerationsRef = useRef(new Map<string, number>())
|
||||
const terminalReconciliationTimeoutsRef = useRef(new Map<string, number>())
|
||||
const terminalReconciliationControllersRef = useRef(new Map<string, AbortController>())
|
||||
const pendingTerminalProgressRef = useRef(new Map<string, ProcessingTaskProgressEvent>())
|
||||
const terminalReconciliationGenerationsRef = useRefWithInit(() => new Map<string, number>())
|
||||
const failedTaskPollGenerationsRef = useRefWithInit(() => new Map<string, number>())
|
||||
const blockedFailedTaskPollVersionsRef = useRefWithInit(() => new Map<string, string>())
|
||||
const failedPollAuxiliaryDenialsRef = useRefWithInit(
|
||||
() => new Map<string, AuxiliaryTaskReadDenial>(),
|
||||
)
|
||||
const terminalConfirmableAuxiliaryDenialsRef = useRefWithInit(
|
||||
() => new Map<string, AuxiliaryTaskReadDenial>(),
|
||||
)
|
||||
const equalRetryListGenerationsRef = useRefWithInit(() => new Map<string, number>())
|
||||
const terminalReconciliationTimeoutsRef = useRefWithInit(() => new Map<string, number>())
|
||||
const terminalReconciliationControllersRef = useRefWithInit(
|
||||
() => new Map<string, AbortController>(),
|
||||
)
|
||||
const pendingTerminalProgressRef = useRefWithInit(
|
||||
() => new Map<string, ProcessingTaskProgressEvent>(),
|
||||
)
|
||||
const taskEventCursorsRef = useRef(new Map<string, string>())
|
||||
const streamActiveOverrideVersionsRef = useRef(new Map<string, string>())
|
||||
const trustedActiveOverrideVersionsRef = useRef(new Map<string, TrustedActiveOverride>())
|
||||
const trustedOverrideListGenerationsRef = useRef(new Map<string, number>())
|
||||
const taskProgressStoreRef = useRef<ReturnType<typeof createTaskProgressStore> | null>(null)
|
||||
if (!taskProgressStoreRef.current) taskProgressStoreRef.current = createTaskProgressStore()
|
||||
const trustedActiveOverrideVersionsRef = useRefWithInit(
|
||||
() => new Map<string, TrustedActiveOverride>(),
|
||||
)
|
||||
const trustedOverrideListGenerationsRef = useRefWithInit(() => new Map<string, number>())
|
||||
const taskProgressStoreRef = useRefWithInit(createTaskProgressStore)
|
||||
const taskProgressStore = taskProgressStoreRef.current
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [reindexing, setReindexing] = useState(false)
|
||||
@@ -242,7 +252,11 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
if (!documentPermissionDenied) return
|
||||
failedPollAuxiliaryDenialsRef.current.clear()
|
||||
terminalConfirmableAuxiliaryDenialsRef.current.clear()
|
||||
}, [documentPermissionDenied])
|
||||
}, [
|
||||
documentPermissionDenied,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
])
|
||||
const tasksQueryOptions = useMemo(
|
||||
() =>
|
||||
consoleQuery.knowledgeFs.getKnowledgeSpacesByIdProcessingTasks.infiniteOptions({
|
||||
@@ -445,7 +459,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
trustedOverrideListGenerationsRef.current.delete(task.id)
|
||||
}
|
||||
}
|
||||
}, [tasks])
|
||||
}, [tasks, trustedActiveOverrideVersionsRef, trustedOverrideListGenerationsRef])
|
||||
|
||||
useEffect(() => {
|
||||
const clearedActiveTaskIds: string[] = []
|
||||
@@ -470,9 +484,19 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
for (const taskId of clearedActiveTaskIds) next[taskId] = (next[taskId] ?? 0) + 1
|
||||
return next
|
||||
})
|
||||
}, [auxiliaryTaskReadGuard, baseTasks, effectiveTaskById, taskListGeneration])
|
||||
const currentTaskStateRef = useRef(new Map(tasks.map((task) => [task.id, task.state])))
|
||||
const currentTaskVersionRef = useRef(new Map(tasks.map((task) => [task.id, task.updatedAt])))
|
||||
}, [
|
||||
auxiliaryTaskReadGuard,
|
||||
baseTasks,
|
||||
effectiveTaskById,
|
||||
taskListGeneration,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
])
|
||||
const currentTaskStateRef = useRefWithInit(
|
||||
() => new Map(tasks.map((task) => [task.id, task.state])),
|
||||
)
|
||||
const currentTaskVersionRef = useRefWithInit(
|
||||
() => new Map(tasks.map((task) => [task.id, task.updatedAt])),
|
||||
)
|
||||
useLayoutEffect(() => {
|
||||
const currentTaskIds = new Set(tasks.map((task) => task.id))
|
||||
for (const task of tasks) {
|
||||
@@ -487,7 +511,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
currentTaskStateRef.current.delete(taskId)
|
||||
currentTaskVersionRef.current.delete(taskId)
|
||||
}
|
||||
}, [tasks])
|
||||
}, [tasks, currentTaskStateRef, currentTaskVersionRef])
|
||||
|
||||
const taskByDocument = useMemo(() => newestTaskByDocument(tasks), [tasks])
|
||||
const documentStatuses = useMemo(
|
||||
@@ -813,7 +837,14 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
}
|
||||
if (shouldRestoreFocus) documentPermissionAlertRef.current?.focus()
|
||||
bulkActionsHadFocusRef.current = false
|
||||
}, [permissionDenialMask, permissionDenied, refetchSourcesQuery, refetchTasksQuery, tasksOpen])
|
||||
}, [
|
||||
permissionDenialMask,
|
||||
permissionDenied,
|
||||
refetchSourcesQuery,
|
||||
refetchTasksQuery,
|
||||
tasksOpen,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -1123,7 +1154,22 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
window.clearTimeout(requestTimeout)
|
||||
}
|
||||
},
|
||||
[auxiliaryTaskReadGuard, denyAuxiliaryTaskRead, knowledgeSpaceId, taskProgressStore],
|
||||
[
|
||||
auxiliaryTaskReadGuard,
|
||||
denyAuxiliaryTaskRead,
|
||||
knowledgeSpaceId,
|
||||
taskProgressStore,
|
||||
currentTaskStateRef,
|
||||
currentTaskVersionRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
terminalReconciliationControllersRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
trustedActiveOverrideVersionsRef,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1147,7 +1193,16 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
terminalReconciliationGenerationsRef.current.set(task.id, reconciliationGeneration)
|
||||
void reconcileTerminalTask(task.id, task.updatedAt, reconciliationGeneration)
|
||||
}
|
||||
}, [baseTasks, permissionDenied, reconcileTerminalTask, taskListGeneration, taskOverrides])
|
||||
}, [
|
||||
baseTasks,
|
||||
permissionDenied,
|
||||
reconcileTerminalTask,
|
||||
taskListGeneration,
|
||||
taskOverrides,
|
||||
trustedActiveOverrideVersionsRef,
|
||||
trustedOverrideListGenerationsRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -1158,7 +1213,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
window.clearTimeout(timeout)
|
||||
terminalReconciliationTimeoutsRef.current.clear()
|
||||
},
|
||||
[knowledgeSpaceId],
|
||||
[knowledgeSpaceId, terminalReconciliationControllersRef, terminalReconciliationTimeoutsRef],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1181,7 +1236,15 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
window.clearTimeout(timeout)
|
||||
terminalReconciliationTimeoutsRef.current.clear()
|
||||
equalRetryListGenerationsRef.current.clear()
|
||||
}, [knowledgeSpaceId, permissionDenied, queryClient])
|
||||
}, [
|
||||
knowledgeSpaceId,
|
||||
permissionDenied,
|
||||
queryClient,
|
||||
terminalReconciliationControllersRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
equalRetryListGenerationsRef,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
const taskIds = new Set(baseTasks.map((task) => task.id))
|
||||
@@ -1234,7 +1297,22 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
setTerminalTaskPins(retainTaskState)
|
||||
// oxlint-disable-next-line eslint-react/set-state-in-effect -- Prune state for tasks removed by a refreshed cursor result.
|
||||
setTaskObserverGenerations(retainTaskState)
|
||||
}, [auxiliaryTaskReadGuard, baseTasks, taskProgressStore])
|
||||
}, [
|
||||
auxiliaryTaskReadGuard,
|
||||
baseTasks,
|
||||
taskProgressStore,
|
||||
terminalReconciliationControllersRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
equalRetryListGenerationsRef,
|
||||
pendingTerminalProgressRef,
|
||||
trustedOverrideListGenerationsRef,
|
||||
trustedActiveOverrideVersionsRef,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionDenied) return
|
||||
@@ -1340,6 +1418,14 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
taskListGeneration,
|
||||
taskProgressStore,
|
||||
terminalTaskPins,
|
||||
terminalReconciliationGenerationsRef,
|
||||
terminalReconciliationControllersRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
equalRetryListGenerationsRef,
|
||||
])
|
||||
|
||||
const handleUploadFiles = useCallback(
|
||||
@@ -1599,7 +1685,19 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
}
|
||||
return true
|
||||
},
|
||||
[reconcileTerminalTask, refreshDocuments, t, taskProgressStore],
|
||||
[
|
||||
reconcileTerminalTask,
|
||||
refreshDocuments,
|
||||
t,
|
||||
taskProgressStore,
|
||||
currentTaskStateRef,
|
||||
currentTaskVersionRef,
|
||||
pendingTerminalProgressRef,
|
||||
trustedActiveOverrideVersionsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
],
|
||||
)
|
||||
|
||||
const handleTaskUpdated = useCallback(
|
||||
@@ -1643,7 +1741,22 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
})
|
||||
}
|
||||
},
|
||||
[auxiliaryTaskReadGuard, taskProgressStore],
|
||||
[
|
||||
auxiliaryTaskReadGuard,
|
||||
taskProgressStore,
|
||||
currentTaskStateRef,
|
||||
currentTaskVersionRef,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
terminalConfirmableAuxiliaryDenialsRef,
|
||||
trustedActiveOverrideVersionsRef,
|
||||
trustedOverrideListGenerationsRef,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
terminalReconciliationControllersRef,
|
||||
terminalReconciliationTimeoutsRef,
|
||||
terminalReconciliationGenerationsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
pendingTerminalProgressRef,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1656,7 +1769,13 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
failedPollAuxiliaryDenialsRef.current.delete(task.id)
|
||||
handleTaskUpdated(task)
|
||||
}
|
||||
}, [baseTasks, handleTaskUpdated, permissionDenied, taskListGeneration])
|
||||
}, [
|
||||
baseTasks,
|
||||
handleTaskUpdated,
|
||||
permissionDenied,
|
||||
taskListGeneration,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
for (const task of activeTasks) {
|
||||
@@ -1665,7 +1784,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
const generation = failedTaskPollGenerationsRef.current.get(task.id) ?? 0
|
||||
failedTaskPollGenerationsRef.current.set(task.id, generation + 1)
|
||||
}
|
||||
}, [activeTasks])
|
||||
}, [activeTasks, blockedFailedTaskPollVersionsRef, failedTaskPollGenerationsRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionDenied || !orderedFailedTasksRef.current.length) return
|
||||
@@ -1767,10 +1886,14 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
}, [
|
||||
auxiliaryTaskReadGuard,
|
||||
denyAuxiliaryTaskRead,
|
||||
currentTaskVersionRef,
|
||||
failedTaskPollSignature,
|
||||
handleTaskUpdated,
|
||||
knowledgeSpaceId,
|
||||
permissionDenied,
|
||||
blockedFailedTaskPollVersionsRef,
|
||||
failedTaskPollGenerationsRef,
|
||||
failedPollAuxiliaryDenialsRef,
|
||||
])
|
||||
|
||||
const toggleDocument = useCallback(
|
||||
@@ -1830,7 +1953,7 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
})
|
||||
denyAuxiliaryTaskRead(taskId, taskVersion)
|
||||
},
|
||||
[denyAuxiliaryTaskRead],
|
||||
[denyAuxiliaryTaskRead, terminalConfirmableAuxiliaryDenialsRef],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalS
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { taskCanRetry, taskIsActive, taskVersionIsAfter } from './document-model'
|
||||
|
||||
@@ -150,7 +151,7 @@ export function ProcessingTasksDrawer({
|
||||
const retryTask = useMutation(
|
||||
consoleQuery.knowledgeFs.postKnowledgeSpacesByIdDocumentsByDocumentIdProcessingTasksByTaskIdRetry.mutationOptions(),
|
||||
)
|
||||
const pendingActionsRef = useRef(new Set<string>())
|
||||
const pendingActionsRef = useRefWithInit(() => new Set<string>())
|
||||
const drawerCloseButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const taskQueryRetryButtonRef = useRef<HTMLButtonElement>(null)
|
||||
const documentQueryRetryButtonRef = useRef<HTMLButtonElement>(null)
|
||||
@@ -222,8 +223,8 @@ export function ProcessingTasksDrawer({
|
||||
() => new Map(tasks.map((task) => [task.id, taskLifecycle(task)])),
|
||||
[tasks],
|
||||
)
|
||||
const taskLifecycleGenerationsRef = useRef(
|
||||
new Map<string, { generation: number; lifecycle: string }>(),
|
||||
const taskLifecycleGenerationsRef = useRefWithInit(
|
||||
() => new Map<string, { generation: number; lifecycle: string }>(),
|
||||
)
|
||||
useLayoutEffect(() => {
|
||||
actionResultsValidRef.current = actionResultsValid
|
||||
@@ -240,7 +241,7 @@ export function ProcessingTasksDrawer({
|
||||
for (const taskId of taskLifecycleGenerationsRef.current.keys()) {
|
||||
if (!currentTaskIds.has(taskId)) taskLifecycleGenerationsRef.current.delete(taskId)
|
||||
}
|
||||
}, [actionResultsValid, tasks])
|
||||
}, [actionResultsValid, tasks, taskLifecycleGenerationsRef])
|
||||
|
||||
useEffect(() => {
|
||||
openRef.current = open
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { consoleClient } from '@/service/console'
|
||||
import { CrawlSelectionForm } from './crawl-selection-form'
|
||||
@@ -328,7 +329,7 @@ export function WebsiteCrawlPreview({
|
||||
const cancelFingerprintRef = useRef<string | undefined>(undefined)
|
||||
const rootUrlInputRef = useRef<HTMLInputElement>(null)
|
||||
const sourceNameInputRef = useRef<HTMLInputElement>(null)
|
||||
const pageMapRef = useRef(new Map<string, PreviewPage>())
|
||||
const pageMapRef = useRefWithInit(() => new Map<string, PreviewPage>())
|
||||
const pageCursorRef = useRef<string | undefined>(undefined)
|
||||
const submittedRef = useRef(false)
|
||||
const discardRequestedRef = useRef(false)
|
||||
@@ -350,7 +351,7 @@ export function WebsiteCrawlPreview({
|
||||
pageCursorRef.current = undefined
|
||||
setPages([])
|
||||
setPagesLoaded(false)
|
||||
}, [])
|
||||
}, [pageMapRef])
|
||||
|
||||
const updateRun = useCallback((nextRun: SourceWorkflowRun | undefined) => {
|
||||
if (nextRun && pendingCancelRunRef.current?.id === nextRun.id)
|
||||
@@ -842,7 +843,7 @@ export function WebsiteCrawlPreview({
|
||||
disposed = true
|
||||
if (timer) clearTimeout(timer)
|
||||
}
|
||||
}, [knowledgeSpaceId, runId, shouldPoll, updateRun])
|
||||
}, [knowledgeSpaceId, runId, shouldPoll, updateRun, pageMapRef])
|
||||
|
||||
const stop = async (targetRun = run) => {
|
||||
if (!targetRun || isTerminal(targetRun.state)) return true
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useDefaultModel } from '@/app/components/header/account-setting/model-p
|
||||
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
|
||||
import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { toast } from '@/app/notifications'
|
||||
import { useRefWithInit } from '@/hooks/use-ref-with-init'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { sendSkillAssistMessage, uploadSkillFile } from '../client'
|
||||
import { SkillBuilderGridTexture } from './builder-grid-texture'
|
||||
@@ -325,7 +326,7 @@ export function SkillBuilderPanel({
|
||||
)
|
||||
const [messages, setMessages] = useState<BuilderChatMessage[]>(initialMessages)
|
||||
const messagesRef = useRef<BuilderChatMessage[]>(initialMessages)
|
||||
const rawAssistantMessagesRef = useRef(new Map<string, string>())
|
||||
const rawAssistantMessagesRef = useRefWithInit(() => new Map<string, string>())
|
||||
const [attachments, setAttachments] = useState<SkillBuilderAttachment[]>([])
|
||||
const [isUploadingAttachment, setIsUploadingAttachment] = useState(false)
|
||||
const attachmentInputRef = useRef<HTMLInputElement>(null)
|
||||
@@ -419,7 +420,7 @@ export function SkillBuilderPanel({
|
||||
messagesRef.current = initialMessages
|
||||
rawAssistantMessagesRef.current.clear()
|
||||
setMessages(initialMessages)
|
||||
}, [initialMessages])
|
||||
}, [initialMessages, rawAssistantMessagesRef])
|
||||
|
||||
useEffect(() => {
|
||||
messagesRef.current = messages
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Material-UI SAS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,66 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { StrictMode } from 'react'
|
||||
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
|
||||
import { useRefWithInit } from './index'
|
||||
|
||||
describe('useRefWithInit', () => {
|
||||
it('preserves the ref and initial value when the initializer and argument change', () => {
|
||||
const init = vi.fn((value: number) => ({ value }))
|
||||
const replacementInit = vi.fn((value: number) => ({ value }))
|
||||
const { result, rerender } = renderHook(
|
||||
({ initializer, value }) => useRefWithInit(initializer, value),
|
||||
{ initialProps: { initializer: init, value: 1 } },
|
||||
)
|
||||
const ref = result.current
|
||||
const initialValue = ref.current
|
||||
|
||||
rerender({ initializer: replacementInit, value: 2 })
|
||||
|
||||
expect(result.current).toBe(ref)
|
||||
expect(result.current.current).toBe(initialValue)
|
||||
expect(result.current.current).toEqual({ value: 1 })
|
||||
expect(init).toHaveBeenCalledExactlyOnceWith(1)
|
||||
expect(replacementInit).not.toHaveBeenCalled()
|
||||
expectTypeOf(result.current.current).toEqualTypeOf<{ value: number }>()
|
||||
})
|
||||
|
||||
it.each([null, undefined])('accepts %s as an initialized value', (value) => {
|
||||
const init = vi.fn(() => value)
|
||||
const { result, rerender } = renderHook(() => useRefWithInit(init))
|
||||
|
||||
rerender()
|
||||
|
||||
expect(result.current.current).toBe(value)
|
||||
expect(init).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('preserves explicit ref updates, including clearing the value', () => {
|
||||
const init = vi.fn((): number | null => 1)
|
||||
const { result, rerender } = renderHook(() => useRefWithInit(init))
|
||||
expectTypeOf(result.current.current).toEqualTypeOf<number | null>()
|
||||
|
||||
result.current.current = 2
|
||||
rerender()
|
||||
expect(result.current.current).toBe(2)
|
||||
|
||||
result.current.current = null
|
||||
rerender()
|
||||
expect(result.current.current).toBeNull()
|
||||
expect(init).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps committed ref identity across rerenders in Strict Mode', () => {
|
||||
const { result, rerender } = renderHook(() => useRefWithInit(() => new Map<string, number>()), {
|
||||
wrapper: StrictMode,
|
||||
})
|
||||
const ref = result.current
|
||||
const map = ref.current
|
||||
map.set('count', 1)
|
||||
|
||||
rerender()
|
||||
|
||||
expect(result.current).toBe(ref)
|
||||
expect(result.current.current).toBe(map)
|
||||
expect(result.current.current.get('count')).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
* Adapted from Base UI (MIT). Copyright (c) 2019 Material-UI SAS.
|
||||
* https://github.com/mui/base-ui/blob/50f90e8f8ff4cc77b624b6ad8550346e00b15445/packages/utils/src/useRefWithInit.ts
|
||||
* See ./LICENSE for the full license.
|
||||
*/
|
||||
|
||||
'use client'
|
||||
import * as React from 'react'
|
||||
|
||||
const UNINITIALIZED = {}
|
||||
|
||||
/**
|
||||
* Lazily initializes a ref. Later changes to initArg do not reinitialize it.
|
||||
* The initializer runs during render and must be safe to retry or discard.
|
||||
*/
|
||||
export function useRefWithInit<T>(init: () => T): React.RefObject<T>
|
||||
export function useRefWithInit<T, U>(init: (arg: U) => T, initArg: U): React.RefObject<T>
|
||||
export function useRefWithInit(init: (arg?: unknown) => unknown, initArg?: unknown) {
|
||||
const ref = React.useRef<unknown>(UNINITIALIZED)
|
||||
|
||||
if (ref.current === UNINITIALIZED) {
|
||||
ref.current = init(initArg)
|
||||
}
|
||||
|
||||
return ref
|
||||
}
|
||||
Reference in New Issue
Block a user