diff --git a/web/app/components/workflow/skill/context.tsx b/web/app/components/workflow/skill/context.tsx index 6c103d29d8..e286b411d8 100644 --- a/web/app/components/workflow/skill/context.tsx +++ b/web/app/components/workflow/skill/context.tsx @@ -1,8 +1,8 @@ 'use client' -import type { SkillEditorStore } from './store' import { useEffect, + useMemo, useRef, } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' @@ -23,30 +23,24 @@ export type SkillEditorProviderProps = { } export const SkillEditorProvider = ({ children }: SkillEditorProviderProps) => { - const storeRef = useRef(undefined) + // Create store once using useMemo (stable across re-renders) + const store = useMemo(() => createSkillEditorStore(), []) + const appDetail = useAppStore(s => s.appDetail) const appId = appDetail?.id const prevAppIdRef = useRef(undefined) - // Create store on first render (pattern recommended by React) - if (storeRef.current === null || storeRef.current === undefined) - storeRef.current = createSkillEditorStore() - // Reset store when appId changes useEffect(() => { - if (prevAppIdRef.current !== undefined && prevAppIdRef.current !== appId) { - // appId changed, reset the store - storeRef.current?.getState().reset() - } + if (prevAppIdRef.current !== undefined && prevAppIdRef.current !== appId) + store.getState().reset() + prevAppIdRef.current = appId - }, [appId]) + }, [appId, store]) return ( - + {children} ) } - -// Re-export for convenience -export { SkillEditorContext } from './store' diff --git a/web/app/components/workflow/skill/editor-tab-item.tsx b/web/app/components/workflow/skill/editor-tab-item.tsx index aca03d58c4..d39b621cf6 100644 --- a/web/app/components/workflow/skill/editor-tab-item.tsx +++ b/web/app/components/workflow/skill/editor-tab-item.tsx @@ -28,7 +28,6 @@ import { getFileIconType } from './utils' type EditorTabItemProps = { fileId: string name: string - extension?: string isActive: boolean isDirty: boolean onClick: (fileId: string) => void @@ -38,7 +37,6 @@ type EditorTabItemProps = { const EditorTabItem: FC = ({ fileId, name, - extension: _extension, isActive, isDirty, onClick, diff --git a/web/app/components/workflow/skill/editor-tabs.tsx b/web/app/components/workflow/skill/editor-tabs.tsx index 3fd568af33..093a5c7de8 100644 --- a/web/app/components/workflow/skill/editor-tabs.tsx +++ b/web/app/components/workflow/skill/editor-tabs.tsx @@ -36,11 +36,12 @@ const EditorTabs: FC = () => { const storeApi = useSkillEditorStoreApi() // Build node map for quick lookup + const treeChildren = treeData?.children const nodeMap = useMemo(() => { - if (!treeData?.children) + if (!treeChildren) return new Map() - return buildNodeMap(treeData.children) - }, [treeData?.children]) + return buildNodeMap(treeChildren) + }, [treeChildren]) // Handle tab click const handleTabClick = (fileId: string) => { @@ -69,7 +70,6 @@ const EditorTabs: FC = () => { {openTabIds.map((fileId) => { const node = nodeMap.get(fileId) const name = node?.name ?? fileId - const extension = node?.extension ?? '' const isActive = activeTabId === fileId const isDirty = dirtyContents.has(fileId) @@ -78,7 +78,6 @@ const EditorTabs: FC = () => { key={fileId} fileId={fileId} name={name} - extension={extension} isActive={isActive} isDirty={isDirty} onClick={handleTabClick} diff --git a/web/app/components/workflow/skill/file-item.tsx b/web/app/components/workflow/skill/file-item.tsx deleted file mode 100644 index dedfc02c42..0000000000 --- a/web/app/components/workflow/skill/file-item.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { FC, ReactNode } from 'react' -import * as React from 'react' -import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon' -import { cn } from '@/utils/classnames' -import { getFileIconType } from './utils' - -type FileItemProps = { - name: string - prefix?: ReactNode - active?: boolean -} - -const FileItem: FC = ({ name, prefix, active = false }) => { - const appearanceType = getFileIconType(name) - - return ( -
- {prefix} -
- - - {name} - -
-
- ) -} - -export default React.memo(FileItem) diff --git a/web/app/components/workflow/skill/files.tsx b/web/app/components/workflow/skill/files.tsx index 714df68ec8..e1a18c1264 100644 --- a/web/app/components/workflow/skill/files.tsx +++ b/web/app/components/workflow/skill/files.tsx @@ -84,32 +84,35 @@ const Files: React.FC = ({ className }) => { // Auto-reveal when activeTabId changes (sync from tab click to tree) useEffect(() => { - if (!activeTabId || !treeData?.children) + if (!activeTabId || !treeData?.children || !treeRef.current) return - // Get ancestors and expand them const ancestors = getAncestorIds(activeTabId, treeData.children) - if (ancestors.length > 0) { - storeApi.getState().revealFile(activeTabId, ancestors) - } - // Scroll to and select the node - if (treeRef.current) { - // Small delay to allow tree to update - const timeoutId = setTimeout(() => { - const node = treeRef.current?.get(activeTabId) - if (node) { - node.select() - // Open all parents programmatically - ancestors.forEach((ancestorId) => { - const ancestorNode = treeRef.current?.get(ancestorId) - if (ancestorNode && !ancestorNode.isOpen) - ancestorNode.open() - }) - } - }, 0) - return () => clearTimeout(timeoutId) - } + // Update store for state persistence + if (ancestors.length > 0) + storeApi.getState().revealFile(activeTabId, ancestors) + + // Use Tree API for immediate UI update (initialOpenState only applies on first render) + const timeoutId = setTimeout(() => { + const tree = treeRef.current + if (!tree) + return + + // Open all ancestor folders + for (const ancestorId of ancestors) { + const ancestorNode = tree.get(ancestorId) + if (ancestorNode && !ancestorNode.isOpen) + ancestorNode.open() + } + + // Select the target node + const node = tree.get(activeTabId) + if (node) + node.select() + }, 0) + + return () => clearTimeout(timeoutId) }, [activeTabId, treeData?.children, storeApi]) // Loading state diff --git a/web/app/components/workflow/skill/fold-item.tsx b/web/app/components/workflow/skill/fold-item.tsx deleted file mode 100644 index 62517e95a8..0000000000 --- a/web/app/components/workflow/skill/fold-item.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import type { FC, ReactNode } from 'react' -import { RiFolder6Line, RiFolderOpenLine } from '@remixicon/react' -import * as React from 'react' -import { cn } from '@/utils/classnames' - -type FoldItemProps = { - name: string - prefix?: ReactNode - active?: boolean - open?: boolean -} - -const FoldItem: FC = ({ name, prefix, active = false, open = false }) => { - const Icon = open ? RiFolderOpenLine : RiFolder6Line - - return ( -
- {prefix} -
- - - {name} - -
-
- ) -} - -export default React.memo(FoldItem) diff --git a/web/app/components/workflow/skill/skill-doc-editor.tsx b/web/app/components/workflow/skill/skill-doc-editor.tsx index 245a086b61..f2dab8a5d2 100644 --- a/web/app/components/workflow/skill/skill-doc-editor.tsx +++ b/web/app/components/workflow/skill/skill-doc-editor.tsx @@ -1,5 +1,6 @@ 'use client' +import type { OnMount } from '@monaco-editor/react' import type { FC } from 'react' import type { AppAssetTreeView } from './type' import Editor, { loader } from '@monaco-editor/react' @@ -39,7 +40,7 @@ const SkillDocEditor: FC = () => { const { t } = useTranslation('workflow') const { theme: appTheme } = useTheme() const [isMounted, setIsMounted] = useState(false) - const editorRef = useRef(null) + const editorRef = useRef[0] | null>(null) // Get appId from app store const appDetail = useAppStore(s => s.appDetail) @@ -54,11 +55,12 @@ const SkillDocEditor: FC = () => { const { data: treeData } = useGetAppAssetTree(appId) // Build node map for quick lookup + const treeChildren = treeData?.children const nodeMap = useMemo(() => { - if (!treeData?.children) + if (!treeChildren) return new Map() - return buildNodeMap(treeData.children) - }, [treeData?.children]) + return buildNodeMap(treeChildren) + }, [treeChildren]) // Get current file node const currentFileNode = activeTabId ? nodeMap.get(activeTabId) : undefined @@ -138,7 +140,7 @@ const SkillDocEditor: FC = () => { }, [handleSave]) // Handle editor mount - const handleEditorDidMount = useCallback((editor: any, monaco: any) => { + const handleEditorDidMount: OnMount = useCallback((editor, monaco) => { editorRef.current = editor monaco.editor.setTheme(appTheme === Theme.light ? 'light' : 'vs-dark') setIsMounted(true) diff --git a/web/app/components/workflow/skill/type.ts b/web/app/components/workflow/skill/type.ts index cd52d44818..25e0ff5eab 100644 --- a/web/app/components/workflow/skill/type.ts +++ b/web/app/components/workflow/skill/type.ts @@ -96,18 +96,6 @@ export function getAncestorIds(nodeId: string, nodes: AppAssetTreeView[]): strin return ancestors } -/** - * Get file extension from file name - * @param name - File name (e.g., 'file.txt') - * @returns Extension without dot (e.g., 'txt') or empty string - */ -export function getExtension(name: string): string { - const lastDot = name.lastIndexOf('.') - if (lastDot === -1 || lastDot === 0) - return '' - return name.slice(lastDot + 1).toLowerCase() -} - /** * Convert expanded folder IDs set to react-arborist opens object * @param expandedIds - Set of expanded folder IDs @@ -120,17 +108,3 @@ export function toOpensObject(expandedIds: Set): Record }) return opens } - -/** - * Convert react-arborist opens object to Set - * @param opens - Opens object from react-arborist - * @returns Set of expanded folder IDs - */ -export function fromOpensObject(opens: Record): Set { - const set = new Set() - Object.entries(opens).forEach(([id, isOpen]) => { - if (isOpen) - set.add(id) - }) - return set -}