From 3b6946d3da779c23efb5ecacff1c611dcd5f4a52 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 15 Jan 2026 19:45:18 +0800 Subject: [PATCH] refactor(skill): centralize asset tree data fetching with custom hooks Extract repeated appId retrieval and tree data fetching patterns into dedicated hooks (useSkillAssetTreeData, useSkillAssetNodeMap) to reduce code duplication across 6 components and leverage TanStack Query's select option for efficient nodeMap computation. --- .../components/workflow/skill/editor-tabs.tsx | 20 ++-------- .../components/workflow/skill/file-tree.tsx | 5 ++- .../skill/hooks/use-file-operations.ts | 4 +- .../skill/hooks/use-skill-asset-tree.ts | 37 +++++++++++++++++++ .../workflow/skill/sidebar-search-add.tsx | 7 +--- .../workflow/skill/skill-doc-editor.tsx | 16 ++------ .../workflow/skill/tree-context-menu.tsx | 8 +--- web/service/use-app-asset.ts | 11 +++++- 8 files changed, 63 insertions(+), 45 deletions(-) create mode 100644 web/app/components/workflow/skill/hooks/use-skill-asset-tree.ts diff --git a/web/app/components/workflow/skill/editor-tabs.tsx b/web/app/components/workflow/skill/editor-tabs.tsx index daf7588787..7b7c633b8f 100644 --- a/web/app/components/workflow/skill/editor-tabs.tsx +++ b/web/app/components/workflow/skill/editor-tabs.tsx @@ -1,32 +1,18 @@ 'use client' import type { FC } from 'react' -import type { AppAssetTreeView } from '@/types/app-asset' import * as React from 'react' -import { useMemo } from 'react' -import { useStore as useAppStore } from '@/app/components/app/store' -import { useGetAppAssetTree } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' import EditorTabItem from './editor-tab-item' +import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' -import { buildNodeMap } from './utils/tree-utils' const EditorTabs: FC = () => { - const appDetail = useAppStore(s => s.appDetail) - const appId = appDetail?.id || '' - - const { data: treeData } = useGetAppAssetTree(appId) - const openTabIds = useSkillEditorStore(s => s.openTabIds) const activeTabId = useSkillEditorStore(s => s.activeTabId) const dirtyContents = useSkillEditorStore(s => s.dirtyContents) const storeApi = useSkillEditorStoreApi() - - const nodeMap = useMemo(() => { - if (!treeData?.children) - return new Map() - return buildNodeMap(treeData.children) - }, [treeData?.children]) + const { data: nodeMap } = useSkillAssetNodeMap() const handleTabClick = (fileId: string) => { storeApi.getState().activateTab(fileId) @@ -47,7 +33,7 @@ const EditorTabs: FC = () => { )} > {openTabIds.map((fileId) => { - const node = nodeMap.get(fileId) + const node = nodeMap?.get(fileId) const name = node?.name ?? fileId const isActive = activeTabId === fileId const isDirty = dirtyContents.has(fileId) diff --git a/web/app/components/workflow/skill/file-tree.tsx b/web/app/components/workflow/skill/file-tree.tsx index 1188050922..dd3c48d76d 100644 --- a/web/app/components/workflow/skill/file-tree.tsx +++ b/web/app/components/workflow/skill/file-tree.tsx @@ -13,8 +13,9 @@ import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' -import { useGetAppAssetTree, useRenameAppAssetNode } from '@/service/use-app-asset' +import { useRenameAppAssetNode } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' +import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' import TreeContextMenu from './tree-context-menu' import TreeNode from './tree-node' @@ -45,7 +46,7 @@ const FileTree: React.FC = ({ className }) => { const appDetail = useAppStore(s => s.appDetail) const appId = appDetail?.id || '' - const { data: treeData, isLoading, error } = useGetAppAssetTree(appId) + const { data: treeData, isLoading, error } = useSkillAssetTreeData() const isMutating = useIsMutating() > 0 const expandedFolderIds = useSkillEditorStore(s => s.expandedFolderIds) diff --git a/web/app/components/workflow/skill/hooks/use-file-operations.ts b/web/app/components/workflow/skill/hooks/use-file-operations.ts index 452aad46f3..4368ceb1fe 100644 --- a/web/app/components/workflow/skill/hooks/use-file-operations.ts +++ b/web/app/components/workflow/skill/hooks/use-file-operations.ts @@ -10,10 +10,10 @@ import { useCreateAppAssetFile, useCreateAppAssetFolder, useDeleteAppAssetNode, - useGetAppAssetTree, } from '@/service/use-app-asset' import { useSkillEditorStoreApi } from '../store' import { getAllDescendantFileIds } from '../utils/tree-utils' +import { useSkillAssetTreeData } from './use-skill-asset-tree' type UseFileOperationsOptions = { nodeId: string @@ -40,7 +40,7 @@ export function useFileOperations({ const createFolder = useCreateAppAssetFolder() const createFile = useCreateAppAssetFile() const deleteNode = useDeleteAppAssetNode() - const { data: treeData } = useGetAppAssetTree(appId) + const { data: treeData } = useSkillAssetTreeData() const parentId = nodeId === 'root' ? null : nodeId diff --git a/web/app/components/workflow/skill/hooks/use-skill-asset-tree.ts b/web/app/components/workflow/skill/hooks/use-skill-asset-tree.ts new file mode 100644 index 0000000000..65da8d5e3b --- /dev/null +++ b/web/app/components/workflow/skill/hooks/use-skill-asset-tree.ts @@ -0,0 +1,37 @@ +import type { AppAssetTreeResponse, AppAssetTreeView } from '@/types/app-asset' +import { useStore as useAppStore } from '@/app/components/app/store' +import { useGetAppAssetTree } from '@/service/use-app-asset' +import { buildNodeMap } from '../utils/tree-utils' + +/** + * Get the current app ID from the app store. + * Used internally by skill asset tree hooks. + */ +function useSkillAppId(): string { + const appDetail = useAppStore(s => s.appDetail) + return appDetail?.id || '' +} + +/** + * Hook to get the asset tree data for the current skill app. + * Returns the raw tree data along with loading and error states. + */ +export function useSkillAssetTreeData() { + const appId = useSkillAppId() + return useGetAppAssetTree(appId) +} + +/** + * Hook to get the node map (id -> node) for the current skill app. + * Uses TanStack Query's select option to compute and cache the map. + */ +export function useSkillAssetNodeMap() { + const appId = useSkillAppId() + return useGetAppAssetTree(appId, { + select: (data: AppAssetTreeResponse): Map => { + if (!data?.children) + return new Map() + return buildNodeMap(data.children) + }, + }) +} diff --git a/web/app/components/workflow/skill/sidebar-search-add.tsx b/web/app/components/workflow/skill/sidebar-search-add.tsx index 7611c1af1a..ae02b23c75 100644 --- a/web/app/components/workflow/skill/sidebar-search-add.tsx +++ b/web/app/components/workflow/skill/sidebar-search-add.tsx @@ -11,7 +11,6 @@ import { import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useStore as useAppStore } from '@/app/components/app/store' import Button from '@/app/components/base/button' import { PortalToFollowElem, @@ -19,9 +18,9 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import SearchInput from '@/app/components/base/search-input' -import { useGetAppAssetTree } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' import { useFileOperations } from './hooks/use-file-operations' +import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' import { useSkillEditorStore } from './store' import { getTargetFolderIdFromSelection } from './utils/tree-utils' @@ -55,9 +54,7 @@ const SidebarSearchAdd: FC = () => { const [searchValue, setSearchValue] = useState('') const [showMenu, setShowMenu] = useState(false) - const appDetail = useAppStore(s => s.appDetail) - const appId = appDetail?.id || '' - const { data: treeData } = useGetAppAssetTree(appId) + const { data: treeData } = useSkillAssetTreeData() const activeTabId = useSkillEditorStore(s => s.activeTabId) const targetFolderId = useMemo(() => { diff --git a/web/app/components/workflow/skill/skill-doc-editor.tsx b/web/app/components/workflow/skill/skill-doc-editor.tsx index 5be217aeb8..73f6eec3f3 100644 --- a/web/app/components/workflow/skill/skill-doc-editor.tsx +++ b/web/app/components/workflow/skill/skill-doc-editor.tsx @@ -2,7 +2,6 @@ import type { OnMount } from '@monaco-editor/react' import type { FC } from 'react' -import type { AppAssetTreeView } from '@/types/app-asset' import { loader } from '@monaco-editor/react' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -11,7 +10,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import Loading from '@/app/components/base/loading' import Toast from '@/app/components/base/toast' import useTheme from '@/hooks/use-theme' -import { useGetAppAssetFileContent, useGetAppAssetTree, useUpdateAppAssetFileContent } from '@/service/use-app-asset' +import { useGetAppAssetFileContent, useUpdateAppAssetFileContent } from '@/service/use-app-asset' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' import CodeFileEditor from './editor/code-file-editor' @@ -19,9 +18,9 @@ import MarkdownFileEditor from './editor/markdown-file-editor' import MediaFilePreview from './editor/media-file-preview' import OfficeFilePlaceholder from './editor/office-file-placeholder' import UnsupportedFileDownload from './editor/unsupported-file-download' +import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' import { getFileExtension, getFileLanguage, isCodeOrTextFile, isImageFile, isMarkdownFile, isOfficeFile, isVideoFile } from './utils/file-utils' -import { buildNodeMap } from './utils/tree-utils' if (typeof window !== 'undefined') loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } }) @@ -38,16 +37,9 @@ const SkillDocEditor: FC = () => { const activeTabId = useSkillEditorStore(s => s.activeTabId) const dirtyContents = useSkillEditorStore(s => s.dirtyContents) const storeApi = useSkillEditorStoreApi() + const { data: nodeMap } = useSkillAssetNodeMap() - const { data: treeData } = useGetAppAssetTree(appId) - - const nodeMap = useMemo(() => { - if (!treeData?.children) - return new Map() - return buildNodeMap(treeData.children) - }, [treeData?.children]) - - const currentFileNode = activeTabId ? nodeMap.get(activeTabId) : undefined + const currentFileNode = activeTabId ? nodeMap?.get(activeTabId) : undefined const fileExtension = getFileExtension(currentFileNode?.name, currentFileNode?.extension) const isMarkdown = isMarkdownFile(fileExtension) const isCodeOrText = isCodeOrTextFile(fileExtension) diff --git a/web/app/components/workflow/skill/tree-context-menu.tsx b/web/app/components/workflow/skill/tree-context-menu.tsx index fda25ca348..e1b4913895 100644 --- a/web/app/components/workflow/skill/tree-context-menu.tsx +++ b/web/app/components/workflow/skill/tree-context-menu.tsx @@ -6,10 +6,9 @@ import type { TreeNodeData } from './type' import { useClickAway } from 'ahooks' import * as React from 'react' import { useCallback, useMemo, useRef } from 'react' -import { useStore as useAppStore } from '@/app/components/app/store' -import { useGetAppAssetTree } from '@/service/use-app-asset' import FileNodeMenu from './file-node-menu' import FolderNodeMenu from './folder-node-menu' +import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree' import { useSkillEditorStore, useSkillEditorStoreApi } from './store' import { findNodeById } from './utils/tree-utils' @@ -21,10 +20,7 @@ const TreeContextMenu: FC = ({ treeRef }) => { const ref = useRef(null) const contextMenu = useSkillEditorStore(s => s.contextMenu) const storeApi = useSkillEditorStoreApi() - - const appDetail = useAppStore(s => s.appDetail) - const appId = appDetail?.id || '' - const { data: treeData } = useGetAppAssetTree(appId) + const { data: treeData } = useSkillAssetTreeData() const handleClose = useCallback(() => { storeApi.getState().setContextMenu(null) diff --git a/web/service/use-app-asset.ts b/web/service/use-app-asset.ts index 356c90da59..ebf4c01773 100644 --- a/web/service/use-app-asset.ts +++ b/web/service/use-app-asset.ts @@ -1,5 +1,6 @@ import type { AppAssetNode, + AppAssetTreeResponse, CreateFolderPayload, MoveNodePayload, RenameNodePayload, @@ -14,11 +15,19 @@ import { import { consoleClient, consoleQuery } from '@/service/client' import { upload } from './base' -export const useGetAppAssetTree = (appId: string) => { +type UseGetAppAssetTreeOptions = { + select?: (data: AppAssetTreeResponse) => TData +} + +export function useGetAppAssetTree( + appId: string, + options?: UseGetAppAssetTreeOptions, +) { return useQuery({ queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }), queryFn: () => consoleClient.appAsset.tree({ params: { appId } }), enabled: !!appId, + select: options?.select, }) }