mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
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:
@ -1,32 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
|
||||||
import * as React from 'react'
|
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 { cn } from '@/utils/classnames'
|
||||||
import EditorTabItem from './editor-tab-item'
|
import EditorTabItem from './editor-tab-item'
|
||||||
|
import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree'
|
||||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||||
import { buildNodeMap } from './utils/tree-utils'
|
|
||||||
|
|
||||||
const EditorTabs: FC = () => {
|
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 openTabIds = useSkillEditorStore(s => s.openTabIds)
|
||||||
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
||||||
const dirtyContents = useSkillEditorStore(s => s.dirtyContents)
|
const dirtyContents = useSkillEditorStore(s => s.dirtyContents)
|
||||||
const storeApi = useSkillEditorStoreApi()
|
const storeApi = useSkillEditorStoreApi()
|
||||||
|
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||||
const nodeMap = useMemo(() => {
|
|
||||||
if (!treeData?.children)
|
|
||||||
return new Map<string, AppAssetTreeView>()
|
|
||||||
return buildNodeMap(treeData.children)
|
|
||||||
}, [treeData?.children])
|
|
||||||
|
|
||||||
const handleTabClick = (fileId: string) => {
|
const handleTabClick = (fileId: string) => {
|
||||||
storeApi.getState().activateTab(fileId)
|
storeApi.getState().activateTab(fileId)
|
||||||
@ -47,7 +33,7 @@ const EditorTabs: FC = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{openTabIds.map((fileId) => {
|
{openTabIds.map((fileId) => {
|
||||||
const node = nodeMap.get(fileId)
|
const node = nodeMap?.get(fileId)
|
||||||
const name = node?.name ?? fileId
|
const name = node?.name ?? fileId
|
||||||
const isActive = activeTabId === fileId
|
const isActive = activeTabId === fileId
|
||||||
const isDirty = dirtyContents.has(fileId)
|
const isDirty = dirtyContents.has(fileId)
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import Toast from '@/app/components/base/toast'
|
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 { cn } from '@/utils/classnames'
|
||||||
|
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||||
import TreeContextMenu from './tree-context-menu'
|
import TreeContextMenu from './tree-context-menu'
|
||||||
import TreeNode from './tree-node'
|
import TreeNode from './tree-node'
|
||||||
@ -45,7 +46,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
|||||||
const appDetail = useAppStore(s => s.appDetail)
|
const appDetail = useAppStore(s => s.appDetail)
|
||||||
const appId = appDetail?.id || ''
|
const appId = appDetail?.id || ''
|
||||||
|
|
||||||
const { data: treeData, isLoading, error } = useGetAppAssetTree(appId)
|
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
||||||
const isMutating = useIsMutating() > 0
|
const isMutating = useIsMutating() > 0
|
||||||
|
|
||||||
const expandedFolderIds = useSkillEditorStore(s => s.expandedFolderIds)
|
const expandedFolderIds = useSkillEditorStore(s => s.expandedFolderIds)
|
||||||
|
|||||||
@ -10,10 +10,10 @@ import {
|
|||||||
useCreateAppAssetFile,
|
useCreateAppAssetFile,
|
||||||
useCreateAppAssetFolder,
|
useCreateAppAssetFolder,
|
||||||
useDeleteAppAssetNode,
|
useDeleteAppAssetNode,
|
||||||
useGetAppAssetTree,
|
|
||||||
} from '@/service/use-app-asset'
|
} from '@/service/use-app-asset'
|
||||||
import { useSkillEditorStoreApi } from '../store'
|
import { useSkillEditorStoreApi } from '../store'
|
||||||
import { getAllDescendantFileIds } from '../utils/tree-utils'
|
import { getAllDescendantFileIds } from '../utils/tree-utils'
|
||||||
|
import { useSkillAssetTreeData } from './use-skill-asset-tree'
|
||||||
|
|
||||||
type UseFileOperationsOptions = {
|
type UseFileOperationsOptions = {
|
||||||
nodeId: string
|
nodeId: string
|
||||||
@ -40,7 +40,7 @@ export function useFileOperations({
|
|||||||
const createFolder = useCreateAppAssetFolder()
|
const createFolder = useCreateAppAssetFolder()
|
||||||
const createFile = useCreateAppAssetFile()
|
const createFile = useCreateAppAssetFile()
|
||||||
const deleteNode = useDeleteAppAssetNode()
|
const deleteNode = useDeleteAppAssetNode()
|
||||||
const { data: treeData } = useGetAppAssetTree(appId)
|
const { data: treeData } = useSkillAssetTreeData()
|
||||||
|
|
||||||
const parentId = nodeId === 'root' ? null : nodeId
|
const parentId = nodeId === 'root' ? null : nodeId
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
@ -19,9 +18,9 @@ import {
|
|||||||
PortalToFollowElemTrigger,
|
PortalToFollowElemTrigger,
|
||||||
} from '@/app/components/base/portal-to-follow-elem'
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
import SearchInput from '@/app/components/base/search-input'
|
import SearchInput from '@/app/components/base/search-input'
|
||||||
import { useGetAppAssetTree } from '@/service/use-app-asset'
|
|
||||||
import { cn } from '@/utils/classnames'
|
import { cn } from '@/utils/classnames'
|
||||||
import { useFileOperations } from './hooks/use-file-operations'
|
import { useFileOperations } from './hooks/use-file-operations'
|
||||||
|
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||||
import { useSkillEditorStore } from './store'
|
import { useSkillEditorStore } from './store'
|
||||||
import { getTargetFolderIdFromSelection } from './utils/tree-utils'
|
import { getTargetFolderIdFromSelection } from './utils/tree-utils'
|
||||||
|
|
||||||
@ -55,9 +54,7 @@ const SidebarSearchAdd: FC = () => {
|
|||||||
const [searchValue, setSearchValue] = useState('')
|
const [searchValue, setSearchValue] = useState('')
|
||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
|
||||||
const appDetail = useAppStore(s => s.appDetail)
|
const { data: treeData } = useSkillAssetTreeData()
|
||||||
const appId = appDetail?.id || ''
|
|
||||||
const { data: treeData } = useGetAppAssetTree(appId)
|
|
||||||
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
||||||
|
|
||||||
const targetFolderId = useMemo(() => {
|
const targetFolderId = useMemo(() => {
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import type { OnMount } from '@monaco-editor/react'
|
import type { OnMount } from '@monaco-editor/react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import type { AppAssetTreeView } from '@/types/app-asset'
|
|
||||||
import { loader } from '@monaco-editor/react'
|
import { loader } from '@monaco-editor/react'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } 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 Loading from '@/app/components/base/loading'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import useTheme from '@/hooks/use-theme'
|
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 { Theme } from '@/types/app'
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import CodeFileEditor from './editor/code-file-editor'
|
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 MediaFilePreview from './editor/media-file-preview'
|
||||||
import OfficeFilePlaceholder from './editor/office-file-placeholder'
|
import OfficeFilePlaceholder from './editor/office-file-placeholder'
|
||||||
import UnsupportedFileDownload from './editor/unsupported-file-download'
|
import UnsupportedFileDownload from './editor/unsupported-file-download'
|
||||||
|
import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree'
|
||||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||||
import { getFileExtension, getFileLanguage, isCodeOrTextFile, isImageFile, isMarkdownFile, isOfficeFile, isVideoFile } from './utils/file-utils'
|
import { getFileExtension, getFileLanguage, isCodeOrTextFile, isImageFile, isMarkdownFile, isOfficeFile, isVideoFile } from './utils/file-utils'
|
||||||
import { buildNodeMap } from './utils/tree-utils'
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined')
|
if (typeof window !== 'undefined')
|
||||||
loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } })
|
loader.config({ paths: { vs: `${window.location.origin}${basePath}/vs` } })
|
||||||
@ -38,16 +37,9 @@ const SkillDocEditor: FC = () => {
|
|||||||
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
const activeTabId = useSkillEditorStore(s => s.activeTabId)
|
||||||
const dirtyContents = useSkillEditorStore(s => s.dirtyContents)
|
const dirtyContents = useSkillEditorStore(s => s.dirtyContents)
|
||||||
const storeApi = useSkillEditorStoreApi()
|
const storeApi = useSkillEditorStoreApi()
|
||||||
|
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||||
|
|
||||||
const { data: treeData } = useGetAppAssetTree(appId)
|
const currentFileNode = activeTabId ? nodeMap?.get(activeTabId) : undefined
|
||||||
|
|
||||||
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 fileExtension = getFileExtension(currentFileNode?.name, currentFileNode?.extension)
|
const fileExtension = getFileExtension(currentFileNode?.name, currentFileNode?.extension)
|
||||||
const isMarkdown = isMarkdownFile(fileExtension)
|
const isMarkdown = isMarkdownFile(fileExtension)
|
||||||
const isCodeOrText = isCodeOrTextFile(fileExtension)
|
const isCodeOrText = isCodeOrTextFile(fileExtension)
|
||||||
|
|||||||
@ -6,10 +6,9 @@ import type { TreeNodeData } from './type'
|
|||||||
import { useClickAway } from 'ahooks'
|
import { useClickAway } from 'ahooks'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useCallback, useMemo, useRef } 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 FileNodeMenu from './file-node-menu'
|
||||||
import FolderNodeMenu from './folder-node-menu'
|
import FolderNodeMenu from './folder-node-menu'
|
||||||
|
import { useSkillAssetTreeData } from './hooks/use-skill-asset-tree'
|
||||||
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
import { useSkillEditorStore, useSkillEditorStoreApi } from './store'
|
||||||
import { findNodeById } from './utils/tree-utils'
|
import { findNodeById } from './utils/tree-utils'
|
||||||
|
|
||||||
@ -21,10 +20,7 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
|
|||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
const contextMenu = useSkillEditorStore(s => s.contextMenu)
|
const contextMenu = useSkillEditorStore(s => s.contextMenu)
|
||||||
const storeApi = useSkillEditorStoreApi()
|
const storeApi = useSkillEditorStoreApi()
|
||||||
|
const { data: treeData } = useSkillAssetTreeData()
|
||||||
const appDetail = useAppStore(s => s.appDetail)
|
|
||||||
const appId = appDetail?.id || ''
|
|
||||||
const { data: treeData } = useGetAppAssetTree(appId)
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
storeApi.getState().setContextMenu(null)
|
storeApi.getState().setContextMenu(null)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AppAssetNode,
|
AppAssetNode,
|
||||||
|
AppAssetTreeResponse,
|
||||||
CreateFolderPayload,
|
CreateFolderPayload,
|
||||||
MoveNodePayload,
|
MoveNodePayload,
|
||||||
RenameNodePayload,
|
RenameNodePayload,
|
||||||
@ -14,11 +15,19 @@ import {
|
|||||||
import { consoleClient, consoleQuery } from '@/service/client'
|
import { consoleClient, consoleQuery } from '@/service/client'
|
||||||
import { upload } from './base'
|
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({
|
return useQuery({
|
||||||
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }),
|
queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }),
|
||||||
queryFn: () => consoleClient.appAsset.tree({ params: { appId } }),
|
queryFn: () => consoleClient.appAsset.tree({ params: { appId } }),
|
||||||
enabled: !!appId,
|
enabled: !!appId,
|
||||||
|
select: options?.select,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user