refactor(skill-editor): simplify state management and remove dead code

- Replace useRef pattern with useMemo for store creation in context.tsx
- Remove unused extension prop from EditorTabItem
- Fix useMemo dependency warnings in editor-tabs.tsx and skill-doc-editor.tsx
- Add proper OnMount type for Monaco editor instead of any
- Delete unused file-item.tsx and fold-item.tsx components
- Remove unused getExtension and fromOpensObject utilities from type.ts
- Refactor auto-reveal effect in files.tsx for better readability
This commit is contained in:
yyh
2026-01-15 14:02:15 +08:00
parent fe17cbc1a8
commit fcd814a2c3
8 changed files with 45 additions and 149 deletions

View File

@ -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<SkillEditorStore | undefined>(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<string | undefined>(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 (
<SkillEditorContext.Provider value={storeRef.current}>
<SkillEditorContext.Provider value={store}>
{children}
</SkillEditorContext.Provider>
)
}
// Re-export for convenience
export { SkillEditorContext } from './store'

View File

@ -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<EditorTabItemProps> = ({
fileId,
name,
extension: _extension,
isActive,
isDirty,
onClick,

View File

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

View File

@ -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<FileItemProps> = ({ name, prefix, active = false }) => {
const appearanceType = getFileIconType(name)
return (
<div
className={cn(
'flex h-6 items-center rounded-md pl-2 pr-1.5 text-text-secondary',
active && 'bg-state-base-active text-text-primary',
)}
>
{prefix}
<div className="flex items-center gap-2 py-0.5">
<FileTypeIcon type={appearanceType} size="sm" className={cn(active && 'text-text-primary')} />
<span className={cn('system-sm-regular', active && 'font-medium text-text-primary')}>
{name}
</span>
</div>
</div>
)
}
export default React.memo(FileItem)

View File

@ -84,32 +84,35 @@ const Files: React.FC<FilesProps> = ({ 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

View File

@ -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<FoldItemProps> = ({ name, prefix, active = false, open = false }) => {
const Icon = open ? RiFolderOpenLine : RiFolder6Line
return (
<div
className={cn(
'flex h-6 items-center rounded-md pl-2 pr-1.5 text-text-secondary',
active && 'bg-state-base-active text-text-primary',
)}
>
{prefix}
<div className="flex items-center gap-2 py-0.5">
<Icon
className={cn(
'size-4',
open ? 'text-primary-600' : 'text-text-secondary',
active && 'text-text-primary',
)}
/>
<span className={cn('system-sm-regular', active && 'font-medium text-text-primary')}>
{name}
</span>
</div>
</div>
)
}
export default React.memo(FoldItem)

View File

@ -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<any>(null)
const editorRef = useRef<Parameters<OnMount>[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<string, AppAssetTreeView>()
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)

View File

@ -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<string>): Record<string, boolean>
})
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<string, boolean>): Set<string> {
const set = new Set<string>()
Object.entries(opens).forEach(([id, isOpen]) => {
if (isOpen)
set.add(id)
})
return set
}