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.
This commit is contained in:
yyh
2026-01-15 19:45:18 +08:00
parent b8adc8f498
commit 3b6946d3da
8 changed files with 63 additions and 45 deletions

View File

@ -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<string, AppAssetTreeView>()
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)

View File

@ -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<FileTreeProps> = ({ 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)

View File

@ -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

View File

@ -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<string, AppAssetTreeView> => {
if (!data?.children)
return new Map()
return buildNodeMap(data.children)
},
})
}

View File

@ -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(() => {

View File

@ -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<string, AppAssetTreeView>()
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)

View File

@ -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<TreeContextMenuProps> = ({ treeRef }) => {
const ref = useRef<HTMLDivElement>(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)

View File

@ -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<TData = AppAssetTreeResponse> = {
select?: (data: AppAssetTreeResponse) => TData
}
export function useGetAppAssetTree<TData = AppAssetTreeResponse>(
appId: string,
options?: UseGetAppAssetTreeOptions<TData>,
) {
return useQuery({
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }),
queryFn: () => consoleClient.appAsset.tree({ params: { appId } }),
enabled: !!appId,
select: options?.select,
})
}