refactor: extract skill save context, stabilize mutation dependency, and deduplicate cache updates

Split SkillSaveContext and useSkillSaveManager into a separate file to
fix react-refresh/only-export-components lint error. Destructure
mutateAsync from useUpdateAppAssetFileContent for a stable callback
reference, preventing unnecessary useCallback cascade rebuilds. Extract
shared patchFileContentCache helper to unify setQueryData logic between
updateCachedContent and the collaboration event handler.
This commit is contained in:
yyh
2026-02-03 21:09:35 +08:00
parent b6b2af45a7
commit f1d099d50d
6 changed files with 42 additions and 40 deletions

View File

@ -17,10 +17,10 @@ import { useSkillMarkdownCollaboration } from '../collaboration/skills/use-skill
import { START_TAB_ID } from './constants'
import CodeFileEditor from './editor/code-file-editor'
import MarkdownFileEditor from './editor/markdown-file-editor'
import { useSkillSaveManager } from './hooks/skill-save-context'
import { useFileTypeInfo } from './hooks/use-file-type-info'
import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree'
import { useSkillFileData } from './hooks/use-skill-file-data'
import { useSkillSaveManager } from './hooks/use-skill-save-manager'
import StartTabContent from './start-tab'
import { getFileLanguage } from './utils/file-utils'
import MediaFilePreview from './viewer/media-file-preview'

View File

@ -0,0 +1,19 @@
import type { FallbackEntry, SaveFileOptions, SaveResult } from './use-skill-save-manager'
import * as React from 'react'
type SkillSaveContextValue = {
saveFile: (fileId: string, options?: SaveFileOptions) => Promise<SaveResult>
saveAllDirty: () => void
registerFallback: (fileId: string, entry: FallbackEntry) => void
unregisterFallback: (fileId: string) => void
}
export const SkillSaveContext = React.createContext<SkillSaveContextValue | null>(null)
export const useSkillSaveManager = () => {
const context = React.useContext(SkillSaveContext)
if (!context)
throw new Error('Missing SkillSaveProvider in the tree')
return context
}

View File

@ -1,5 +1,5 @@
import { useEventListener, useUnmount } from 'ahooks'
import { useSkillSaveManager } from './use-skill-save-manager'
import { useSkillSaveManager } from './skill-save-context'
export function useSkillAutoSave(): void {
const { saveAllDirty } = useSkillSaveManager()

View File

@ -5,7 +5,8 @@ import { WorkflowContext } from '@/app/components/workflow/context'
import { createWorkflowStore } from '@/app/components/workflow/store'
import { consoleQuery } from '@/service/client'
import { START_TAB_ID } from '../constants'
import { SkillSaveProvider, useSkillSaveManager } from './use-skill-save-manager'
import { useSkillSaveManager } from './skill-save-context'
import { SkillSaveProvider } from './use-skill-save-manager'
const { mockMutateAsync, mockToastNotify } = vi.hoisted(() => ({
mockMutateAsync: vi.fn(),

View File

@ -1,3 +1,4 @@
import type { QueryClient } from '@tanstack/react-query'
import { useQueryClient } from '@tanstack/react-query'
import { useEventListener } from 'ahooks'
import isDeepEqual from 'fast-deep-equal'
@ -12,6 +13,7 @@ import { consoleQuery } from '@/service/client'
import { useUpdateAppAssetFileContent } from '@/service/use-app-asset'
import { skillCollaborationManager } from '../../collaboration/skills/skill-collaboration-manager'
import { START_TAB_ID } from '../constants'
import { SkillSaveContext } from './skill-save-context'
type SaveSnapshot = {
content: string
@ -41,13 +43,6 @@ export type FallbackEntry = {
metadata?: Record<string, unknown>
}
type SkillSaveContextValue = {
saveFile: (fileId: string, options?: SaveFileOptions) => Promise<SaveResult>
saveAllDirty: () => void
registerFallback: (fileId: string, entry: FallbackEntry) => void
unregisterFallback: (fileId: string) => void
}
type SkillSaveProviderProps = {
appId: string
children: React.ReactNode
@ -79,7 +74,17 @@ const normalizeMetadata = (
return nextMetadata
}
const SkillSaveContext = React.createContext<SkillSaveContextValue | null>(null)
const patchFileContentCache = (
qc: QueryClient,
queryKey: readonly unknown[],
serialized: string,
) => {
qc.setQueryData<CachedFileContent>(queryKey, (existing) => {
if (!existing || typeof existing !== 'object')
return { content: serialized }
return { ...existing, content: serialized }
})
}
export const SkillSaveProvider = ({
appId,
@ -88,7 +93,7 @@ export const SkillSaveProvider = ({
const { t } = useTranslation()
const storeApi = useWorkflowStore()
const queryClient = useQueryClient()
const updateContent = useUpdateAppAssetFileContent()
const { mutateAsync: updateFileContent } = useUpdateAppAssetFileContent()
const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
const queueRef = useRef<Map<string, Promise<SaveResult>>>(new Map())
const fallbackRegistryRef = useRef<Map<string, FallbackEntry>>(new Map())
@ -155,17 +160,11 @@ export const SkillSaveProvider = ({
const queryKey = consoleQuery.appAsset.getFileContent.queryKey({
input: { params: { appId, nodeId: fileId } },
})
const existing = queryClient.getQueryData<CachedFileContent>(queryKey)
const serialized = JSON.stringify({
content: snapshot.content,
...(snapshot.metadata ? { metadata: snapshot.metadata } : {}),
})
const nextData: CachedFileContent & { content: string } = {
...(existing && typeof existing === 'object' ? existing : {}),
content: serialized,
}
queryClient.setQueryData(queryKey, nextData)
patchFileContentCache(queryClient, queryKey, serialized)
}, [appId, queryClient])
const performSave = useCallback(async (
@ -186,7 +185,7 @@ export const SkillSaveProvider = ({
}
try {
await updateContent.mutateAsync({
await updateFileContent({
appId,
nodeId: fileId,
payload: {
@ -220,7 +219,7 @@ export const SkillSaveProvider = ({
catch (error) {
return { saved: false, error }
}
}, [appId, buildSnapshot, isCollaborationEnabled, storeApi, updateCachedContent, updateContent])
}, [appId, buildSnapshot, isCollaborationEnabled, storeApi, updateCachedContent, updateFileContent])
const saveFile = useCallback(async (
fileId: string,
@ -297,7 +296,7 @@ export const SkillSaveProvider = ({
}
}, { target: typeof window !== 'undefined' ? window : undefined })
const value = useMemo<SkillSaveContextValue>(() => ({
const value = useMemo(() => ({
saveFile,
saveAllDirty,
registerFallback,
@ -320,11 +319,7 @@ export const SkillSaveProvider = ({
content: payload.content,
...(payload.metadata ? { metadata: payload.metadata } : {}),
})
const existing = queryClient.getQueryData<CachedFileContent>(queryKey)
queryClient.setQueryData(queryKey, {
...(existing && typeof existing === 'object' ? existing : {}),
content: serialized,
})
patchFileContentCache(queryClient, queryKey, serialized)
const state = storeApi.getState()
state.clearDraftContent(fileId)
@ -342,11 +337,3 @@ export const SkillSaveProvider = ({
</SkillSaveContext.Provider>
)
}
export const useSkillSaveManager = () => {
const context = React.useContext(SkillSaveContext)
if (!context)
throw new Error('Missing SkillSaveProvider in the tree')
return context
}

View File

@ -4123,11 +4123,6 @@
"count": 1
}
},
"app/components/workflow/skill/hooks/use-skill-save-manager.tsx": {
"react-refresh/only-export-components": {
"count": 1
}
},
"app/components/workflow/store/workflow/debug/inspect-vars-slice.ts": {
"ts/no-explicit-any": {
"count": 2