diff --git a/web/app/components/workflow/skill/file-content-panel.tsx b/web/app/components/workflow/skill/file-content-panel.tsx index e463143648..eceb6a97e3 100644 --- a/web/app/components/workflow/skill/file-content-panel.tsx +++ b/web/app/components/workflow/skill/file-content-panel.tsx @@ -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' diff --git a/web/app/components/workflow/skill/hooks/skill-save-context.ts b/web/app/components/workflow/skill/hooks/skill-save-context.ts new file mode 100644 index 0000000000..142024b929 --- /dev/null +++ b/web/app/components/workflow/skill/hooks/skill-save-context.ts @@ -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 + saveAllDirty: () => void + registerFallback: (fileId: string, entry: FallbackEntry) => void + unregisterFallback: (fileId: string) => void +} + +export const SkillSaveContext = React.createContext(null) + +export const useSkillSaveManager = () => { + const context = React.useContext(SkillSaveContext) + if (!context) + throw new Error('Missing SkillSaveProvider in the tree') + + return context +} diff --git a/web/app/components/workflow/skill/hooks/use-skill-auto-save.ts b/web/app/components/workflow/skill/hooks/use-skill-auto-save.ts index a0407a0b0f..b8402f5e49 100644 --- a/web/app/components/workflow/skill/hooks/use-skill-auto-save.ts +++ b/web/app/components/workflow/skill/hooks/use-skill-auto-save.ts @@ -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() diff --git a/web/app/components/workflow/skill/hooks/use-skill-save-manager.spec.tsx b/web/app/components/workflow/skill/hooks/use-skill-save-manager.spec.tsx index 14ab8dfc09..f84c64250b 100644 --- a/web/app/components/workflow/skill/hooks/use-skill-save-manager.spec.tsx +++ b/web/app/components/workflow/skill/hooks/use-skill-save-manager.spec.tsx @@ -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(), diff --git a/web/app/components/workflow/skill/hooks/use-skill-save-manager.tsx b/web/app/components/workflow/skill/hooks/use-skill-save-manager.tsx index 9633c5bfec..60b67bffd4 100644 --- a/web/app/components/workflow/skill/hooks/use-skill-save-manager.tsx +++ b/web/app/components/workflow/skill/hooks/use-skill-save-manager.tsx @@ -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 } -type SkillSaveContextValue = { - saveFile: (fileId: string, options?: SaveFileOptions) => Promise - 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(null) +const patchFileContentCache = ( + qc: QueryClient, + queryKey: readonly unknown[], + serialized: string, +) => { + qc.setQueryData(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>>(new Map()) const fallbackRegistryRef = useRef>(new Map()) @@ -155,17 +160,11 @@ export const SkillSaveProvider = ({ const queryKey = consoleQuery.appAsset.getFileContent.queryKey({ input: { params: { appId, nodeId: fileId } }, }) - const existing = queryClient.getQueryData(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(() => ({ + 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(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 = ({ ) } - -export const useSkillSaveManager = () => { - const context = React.useContext(SkillSaveContext) - if (!context) - throw new Error('Missing SkillSaveProvider in the tree') - - return context -} diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index a0d2e2afab..3f52f0fc1b 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -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