mirror of
https://github.com/langgenius/dify.git
synced 2026-04-23 12:16:11 +08:00
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:
@ -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'
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user