fix: context menu

This commit is contained in:
yyh 2026-03-24 20:39:07 +08:00
parent c697c1244f
commit 60e46854c6
No known key found for this signature in database
6 changed files with 294 additions and 203 deletions

View File

@ -1,6 +1,4 @@
import type { RefObject } from 'react'
import type { NodeApi, TreeApi } from 'react-arborist'
import type { TreeNodeData } from '../../type'
import { fireEvent, render, screen } from '@testing-library/react'
import {
ContextMenu,
@ -43,8 +41,6 @@ type RenderNodeMenuProps = {
nodeId?: string
onClose?: () => void
onImportSkills?: () => void
treeRef?: RefObject<TreeApi<TreeNodeData> | null>
node?: NodeApi<TreeNodeData>
}
function createFileOperationsMock(): MockFileOperations {
@ -84,18 +80,12 @@ vi.mock('@/app/components/workflow/store', () => ({
}),
}))
vi.mock('../../hooks/file-tree/operations/use-file-operations', () => ({
useFileOperations: () => mocks.fileOperations,
}))
const renderNodeMenu = ({
type = NODE_MENU_TYPE.FOLDER,
menuType = 'dropdown',
nodeId = 'node-1',
onClose = vi.fn(),
onImportSkills,
treeRef,
node,
}: RenderNodeMenuProps = {}) => {
const ui = menuType === 'dropdown'
? (
@ -107,9 +97,17 @@ const renderNodeMenu = ({
menuType={menuType}
nodeId={nodeId}
onClose={onClose}
fileInputRef={mocks.fileOperations.fileInputRef}
folderInputRef={mocks.fileOperations.folderInputRef}
isLoading={mocks.fileOperations.isLoading}
onDownload={mocks.fileOperations.handleDownload}
onNewFile={mocks.fileOperations.handleNewFile}
onNewFolder={mocks.fileOperations.handleNewFolder}
onFileChange={mocks.fileOperations.handleFileChange}
onFolderChange={mocks.fileOperations.handleFolderChange}
onRename={mocks.fileOperations.handleRename}
onDeleteClick={mocks.fileOperations.handleDeleteClick}
onImportSkills={onImportSkills}
treeRef={treeRef}
node={node}
/>
</DropdownMenuContent>
</DropdownMenu>
@ -123,9 +121,17 @@ const renderNodeMenu = ({
menuType={menuType}
nodeId={nodeId}
onClose={onClose}
fileInputRef={mocks.fileOperations.fileInputRef}
folderInputRef={mocks.fileOperations.folderInputRef}
isLoading={mocks.fileOperations.isLoading}
onDownload={mocks.fileOperations.handleDownload}
onNewFile={mocks.fileOperations.handleNewFile}
onNewFolder={mocks.fileOperations.handleNewFolder}
onFileChange={mocks.fileOperations.handleFileChange}
onFolderChange={mocks.fileOperations.handleFolderChange}
onRename={mocks.fileOperations.handleRename}
onDeleteClick={mocks.fileOperations.handleDeleteClick}
onImportSkills={onImportSkills}
treeRef={treeRef}
node={node}
/>
</ContextMenuContent>
</ContextMenu>
@ -257,19 +263,5 @@ describe('NodeMenu', () => {
fireEvent.click(screen.getByRole('menuitem', { name: /workflow\.skillSidebar\.menu\.importSkills/i }))
expect(onImportSkills).toHaveBeenCalledTimes(1)
})
it('should render delete confirmation content for files and forward confirm callbacks', () => {
mocks.fileOperations.showDeleteConfirm = true
renderNodeMenu({ type: NODE_MENU_TYPE.FILE })
expect(screen.getByText('workflow.skillSidebar.menu.fileDeleteConfirmTitle')).toBeInTheDocument()
expect(screen.getByText('workflow.skillSidebar.menu.fileDeleteConfirmContent')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.confirm/i }))
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
expect(mocks.fileOperations.handleDeleteConfirm).toHaveBeenCalledTimes(1)
expect(mocks.fileOperations.handleDeleteCancel).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,23 +1,12 @@
'use client'
import type { NodeApi, TreeApi } from 'react-arborist'
import type { NodeMenuType } from '../../constants'
import type { TreeNodeData } from '../../type'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { FileAdd, FolderAdd } from '@/app/components/base/icons/src/vender/line/files'
import { UploadCloud02 } from '@/app/components/base/icons/src/vender/line/general'
import { Download02 } from '@/app/components/base/icons/src/vender/solid/general'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog'
import {
ContextMenuSeparator,
} from '@/app/components/base/ui/context-menu'
@ -26,7 +15,6 @@ import {
} from '@/app/components/base/ui/dropdown-menu'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { NODE_MENU_TYPE } from '../../constants'
import { useFileOperations } from '../../hooks/file-tree/operations/use-file-operations'
import MenuItem from './menu-item'
const KBD_CUT = ['ctrl', 'x'] as const
@ -37,8 +25,16 @@ type NodeMenuProps = {
menuType: 'dropdown' | 'context'
nodeId?: string
onClose: () => void
treeRef?: React.RefObject<TreeApi<TreeNodeData> | null>
node?: NodeApi<TreeNodeData>
fileInputRef: React.RefObject<HTMLInputElement | null>
folderInputRef: React.RefObject<HTMLInputElement | null>
isLoading: boolean
onDownload: () => void
onNewFile: () => void
onNewFolder: () => void
onFileChange: React.ChangeEventHandler<HTMLInputElement>
onFolderChange: React.ChangeEventHandler<HTMLInputElement>
onRename: () => void
onDeleteClick: () => void
onImportSkills?: () => void
}
@ -47,8 +43,16 @@ const NodeMenu = ({
menuType,
nodeId,
onClose,
treeRef,
node,
fileInputRef,
folderInputRef,
isLoading,
onDownload,
onNewFile,
onNewFolder,
onFileChange,
onFolderChange,
onRename,
onDeleteClick,
onImportSkills,
}: NodeMenuProps) => {
const { t } = useTranslation('workflow')
@ -58,24 +62,7 @@ const NodeMenu = ({
const isRoot = type === NODE_MENU_TYPE.ROOT
const isFolder = type === NODE_MENU_TYPE.FOLDER || isRoot
const {
fileInputRef,
folderInputRef,
showDeleteConfirm,
isLoading,
isDeleting,
handleDownload,
handleNewFile,
handleNewFolder,
handleFileChange,
handleFolderChange,
handleRename,
handleDeleteClick,
handleDeleteConfirm,
handleDeleteCancel,
} = useFileOperations({ nodeId, onClose, treeRef, node })
const currentNodeId = node?.data.id ?? nodeId
const currentNodeId = nodeId
const handleCut = useCallback(() => {
const ids = selectedNodeIds.size > 0 ? [...selectedNodeIds] : (currentNodeId ? [currentNodeId] : [])
@ -91,12 +78,6 @@ const NodeMenu = ({
}, [onClose])
const showRenameDelete = isFolder ? !isRoot : true
const deleteConfirmTitle = isFolder
? t('skillSidebar.menu.deleteConfirmTitle')
: t('skillSidebar.menu.fileDeleteConfirmTitle')
const deleteConfirmContent = isFolder
? t('skillSidebar.menu.deleteConfirmContent')
: t('skillSidebar.menu.fileDeleteConfirmContent')
const Separator = menuType === 'dropdown' ? DropdownMenuSeparator : ContextMenuSeparator
return (
@ -109,7 +90,7 @@ const NodeMenu = ({
multiple
className="hidden"
aria-label={t('skillSidebar.menu.uploadFile')}
onChange={handleFileChange}
onChange={onFileChange}
/>
<input
ref={folderInputRef}
@ -118,21 +99,21 @@ const NodeMenu = ({
webkitdirectory=""
className="hidden"
aria-label={t('skillSidebar.menu.uploadFolder')}
onChange={handleFolderChange}
onChange={onFolderChange}
/>
<MenuItem
menuType={menuType}
icon={FileAdd}
label={t('skillSidebar.menu.newFile')}
onClick={() => handleNewFile()}
onClick={onNewFile}
disabled={isLoading}
/>
<MenuItem
menuType={menuType}
icon={FolderAdd}
label={t('skillSidebar.menu.newFolder')}
onClick={() => handleNewFolder()}
onClick={onNewFolder}
disabled={isLoading}
/>
@ -177,7 +158,7 @@ const NodeMenu = ({
menuType={menuType}
icon={Download02}
label={t('skillSidebar.menu.download')}
onClick={handleDownload}
onClick={onDownload}
disabled={isLoading}
/>
<Separator />
@ -215,51 +196,19 @@ const NodeMenu = ({
menuType={menuType}
icon="i-ri-edit-2-line"
label={t('skillSidebar.menu.rename')}
onClick={() => handleRename()}
onClick={onRename}
disabled={isLoading}
/>
<MenuItem
menuType={menuType}
icon="i-ri-delete-bin-line"
label={t('skillSidebar.menu.delete')}
onClick={() => handleDeleteClick()}
onClick={onDeleteClick}
disabled={isLoading}
variant="destructive"
/>
</>
)}
<AlertDialog
open={showDeleteConfirm}
onOpenChange={(open) => {
if (!open)
handleDeleteCancel()
}}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 p-6 pb-4">
<AlertDialogTitle className="text-text-primary title-2xl-semi-bold">
{deleteConfirmTitle}
</AlertDialogTitle>
<AlertDialogDescription className="text-text-secondary system-sm-regular">
{deleteConfirmContent}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
disabled={isDeleting}
onClick={() => {
void handleDeleteConfirm()
}}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View File

@ -5,6 +5,22 @@ import TreeContextMenu from './tree-context-menu'
const mocks = vi.hoisted(() => ({
clearSelection: vi.fn(),
deselectAll: vi.fn(),
fileOperations: {
fileInputRef: { current: null },
folderInputRef: { current: null },
showDeleteConfirm: false,
isLoading: false,
isDeleting: false,
handleDownload: vi.fn(),
handleNewFile: vi.fn(),
handleNewFolder: vi.fn(),
handleFileChange: vi.fn(),
handleFolderChange: vi.fn(),
handleRename: vi.fn(),
handleDeleteClick: vi.fn(),
handleDeleteConfirm: vi.fn(),
handleDeleteCancel: vi.fn(),
},
}))
vi.mock('@/app/components/workflow/store', () => ({
@ -34,6 +50,10 @@ vi.mock('next/dynamic', () => ({
},
}))
vi.mock('../../hooks/file-tree/operations/use-file-operations', () => ({
useFileOperations: () => mocks.fileOperations,
}))
vi.mock('./node-menu', () => ({
default: ({ type, menuType, nodeId, onImportSkills }: { type: string, menuType: string, nodeId?: string, onImportSkills?: () => void }) => (
<div

View File

@ -12,6 +12,7 @@ import {
import { useWorkflowStore } from '@/app/components/workflow/store'
import dynamic from '@/next/dynamic'
import { NODE_MENU_TYPE, ROOT_ID } from '../../constants'
import { useFileOperations } from '../../hooks/file-tree/operations/use-file-operations'
import NodeMenu from './node-menu'
const ImportSkillModal = dynamic(() => import('../../start-tab/import-skill-modal'), {
@ -46,6 +47,11 @@ const TreeContextMenu = ({
}, [storeApi, treeRef])
const handleMenuClose = React.useCallback(() => {}, [])
const fileOperations = useFileOperations({
nodeId: ROOT_ID,
treeRef,
onClose: handleMenuClose,
})
const handleOpenImportSkills = React.useCallback(() => {
setIsImportModalOpen(true)
}, [])
@ -66,7 +72,16 @@ const TreeContextMenu = ({
type={NODE_MENU_TYPE.ROOT}
nodeId={ROOT_ID}
onClose={handleMenuClose}
treeRef={treeRef}
fileInputRef={fileOperations.fileInputRef}
folderInputRef={fileOperations.folderInputRef}
isLoading={fileOperations.isLoading}
onDownload={fileOperations.handleDownload}
onNewFile={fileOperations.handleNewFile}
onNewFolder={fileOperations.handleNewFolder}
onFileChange={fileOperations.handleFileChange}
onFolderChange={fileOperations.handleFolderChange}
onRename={fileOperations.handleRename}
onDeleteClick={fileOperations.handleDeleteClick}
onImportSkills={handleOpenImportSkills}
/>
</ContextMenuContent>

View File

@ -48,6 +48,23 @@ const dndMocks = vi.hoisted(() => ({
onDragLeave: vi.fn(),
}))
const fileOperationMocks = vi.hoisted(() => ({
fileInputRef: { current: null },
folderInputRef: { current: null },
showDeleteConfirm: false,
isLoading: false,
isDeleting: false,
handleDownload: vi.fn(),
handleNewFile: vi.fn(),
handleNewFolder: vi.fn(),
handleFileChange: vi.fn(),
handleFolderChange: vi.fn(),
handleRename: vi.fn(),
handleDeleteClick: vi.fn(),
handleDeleteConfirm: vi.fn(async () => undefined),
handleDeleteCancel: vi.fn(),
}))
vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: MockWorkflowSelectorState) => unknown) => selector({
dirtyContents: workflowState.dirtyContents,
@ -89,6 +106,10 @@ vi.mock('../../hooks/file-tree/dnd/use-folder-file-drop', () => ({
}),
}))
vi.mock('../../hooks/file-tree/operations/use-file-operations', () => ({
useFileOperations: () => fileOperationMocks,
}))
vi.mock('./node-menu', () => ({
default: ({ type, menuType, onClose }: { type: string, menuType: string, onClose: () => void }) => (
<div data-testid={`node-menu-${menuType}`} data-type={type}>
@ -151,6 +172,8 @@ describe('TreeNode', () => {
dndMocks.isDragOver = false
dndMocks.isBlinking = false
fileOperationMocks.showDeleteConfirm = false
fileOperationMocks.isDeleting = false
})
// Core rendering should reflect selection, folder expansion, and store-driven visual states.
@ -321,4 +344,22 @@ describe('TreeNode', () => {
expect(storeActions.setDragOverFolderId).toHaveBeenCalledWith(null)
})
})
describe('Dialogs', () => {
it('should keep delete confirmation dialog mounted when requested by menu actions', () => {
fileOperationMocks.showDeleteConfirm = true
const props = buildProps({ id: 'file-1', name: 'readme.md', nodeType: 'file' })
render(<TreeNode {...props} />)
expect(screen.getByText('workflow.skillSidebar.menu.fileDeleteConfirmTitle')).toBeInTheDocument()
expect(screen.getByText('workflow.skillSidebar.menu.fileDeleteConfirmContent')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.confirm/i }))
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
expect(fileOperationMocks.handleDeleteConfirm).toHaveBeenCalledTimes(1)
expect(fileOperationMocks.handleDeleteCancel).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -5,6 +5,15 @@ import type { TreeNodeData } from '../../type'
import * as React from 'react'
import { useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from '@/app/components/base/ui/alert-dialog'
import {
ContextMenu,
ContextMenuContent,
@ -19,6 +28,7 @@ import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { cn } from '@/utils/classnames'
import { useFolderFileDrop } from '../../hooks/file-tree/dnd/use-folder-file-drop'
import { useTreeNodeHandlers } from '../../hooks/file-tree/interaction/use-tree-node-handlers'
import { useFileOperations } from '../../hooks/file-tree/operations/use-file-operations'
import NodeMenu from './node-menu'
import TreeEditInput from './tree-edit-input'
import TreeGuideLines from './tree-guide-lines'
@ -91,107 +101,171 @@ const TreeNode = ({ node, style, dragHandle, treeChildren }: TreeNodeProps) => {
}, [node])
const handleMenuClose = useCallback(() => {}, [])
const fileOperations = useFileOperations({
nodeId: node.data.id,
node,
onClose: handleMenuClose,
})
const deleteConfirmTitle = isFolder
? t('skillSidebar.menu.deleteConfirmTitle')
: t('skillSidebar.menu.fileDeleteConfirmTitle')
const deleteConfirmContent = isFolder
? t('skillSidebar.menu.deleteConfirmContent')
: t('skillSidebar.menu.fileDeleteConfirmContent')
return (
<ContextMenu>
<ContextMenuTrigger
ref={dragHandle}
style={style}
role="treeitem"
tabIndex={0}
aria-selected={isSelected}
aria-expanded={isFolder ? node.isOpen : undefined}
className={cn(
'group relative flex h-6 cursor-pointer items-center rounded-md px-2',
'hover:bg-state-base-hover data-[popup-open]:bg-state-base-hover',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
isSelected && 'bg-state-base-active',
isDragOver && 'bg-state-accent-hover ring-1 ring-inset ring-state-accent-solid',
isBlinking && 'animate-drag-blink',
(isCut || node.isDragging) && 'opacity-50',
)}
onKeyDown={handleKeyDown}
onContextMenu={handleContextMenu}
{...(isFolder && {
onDragEnter: dragHandlers.onDragEnter,
onDragOver: dragHandlers.onDragOver,
onDrop: dragHandlers.onDrop,
onDragLeave: dragHandlers.onDragLeave,
})}
>
<TreeGuideLines level={node.level} />
<div
className="flex min-w-0 flex-1 items-center gap-2"
onClick={handleClick}
onDoubleClick={handleDoubleClick}
<>
<ContextMenu>
<ContextMenuTrigger
ref={dragHandle}
style={style}
role="treeitem"
tabIndex={0}
aria-selected={isSelected}
aria-expanded={isFolder ? node.isOpen : undefined}
className={cn(
'group relative flex h-6 cursor-pointer items-center rounded-md px-2',
'hover:bg-state-base-hover data-[popup-open]:bg-state-base-hover',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-components-input-border-active',
isSelected && 'bg-state-base-active',
isDragOver && 'bg-state-accent-hover ring-1 ring-inset ring-state-accent-solid',
isBlinking && 'animate-drag-blink',
(isCut || node.isDragging) && 'opacity-50',
)}
onKeyDown={handleKeyDown}
onContextMenu={handleContextMenu}
{...(isFolder && {
onDragEnter: dragHandlers.onDragEnter,
onDragOver: dragHandlers.onDragOver,
onDrop: dragHandlers.onDrop,
onDragLeave: dragHandlers.onDragLeave,
})}
>
<div className="flex size-5 shrink-0 items-center justify-center">
<TreeNodeIcon
isFolder={isFolder}
isOpen={node.isOpen}
fileName={node.data.name}
extension={node.data.extension}
isDirty={isDirty}
onToggle={handleToggle}
/>
<TreeGuideLines level={node.level} />
<div
className="flex min-w-0 flex-1 items-center gap-2"
onClick={handleClick}
onDoubleClick={handleDoubleClick}
>
<div className="flex size-5 shrink-0 items-center justify-center">
<TreeNodeIcon
isFolder={isFolder}
isOpen={node.isOpen}
fileName={node.data.name}
extension={node.data.extension}
isDirty={isDirty}
onToggle={handleToggle}
/>
</div>
{node.isEditing
? (
<TreeEditInput node={node} />
)
: (
<span
className={cn(
'min-w-0 flex-1 truncate text-[13px] font-normal leading-4',
isSelected
? 'text-text-primary'
: 'text-text-secondary',
)}
>
{node.data.name}
</span>
)}
</div>
{node.isEditing
? (
<TreeEditInput node={node} />
)
: (
<span
className={cn(
'min-w-0 flex-1 truncate text-[13px] font-normal leading-4',
isSelected
? 'text-text-primary'
: 'text-text-secondary',
)}
>
{node.data.name}
</span>
<DropdownMenu>
<DropdownMenuTrigger
type="button"
aria-label={t('skillSidebar.menu.moreActions')}
tabIndex={-1}
onClick={handleMoreClick}
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded',
'invisible focus-visible:visible group-hover:visible data-[popup-open]:visible',
'hover:bg-state-base-hover-alt data-[popup-open]:bg-state-base-hover-alt',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
)}
</div>
<DropdownMenu>
<DropdownMenuTrigger
type="button"
aria-label={t('skillSidebar.menu.moreActions')}
tabIndex={-1}
onClick={handleMoreClick}
className={cn(
'flex size-5 shrink-0 items-center justify-center rounded',
'invisible focus-visible:visible group-hover:visible data-[popup-open]:visible',
'hover:bg-state-base-hover-alt data-[popup-open]:bg-state-base-hover-alt',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
)}
>
<span className="i-ri-more-fill size-4 text-text-tertiary" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-start"
sideOffset={4}
popupClassName="min-w-[180px]"
>
<NodeMenu
menuType="dropdown"
type={isFolder ? 'folder' : 'file'}
onClose={handleMenuClose}
node={node}
/>
</DropdownMenuContent>
</DropdownMenu>
</ContextMenuTrigger>
<ContextMenuContent popupClassName="min-w-[180px]">
<NodeMenu
menuType="context"
type={isFolder ? 'folder' : 'file'}
onClose={handleMenuClose}
node={node}
/>
</ContextMenuContent>
</ContextMenu>
>
<span className="i-ri-more-fill size-4 text-text-tertiary" aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
placement="bottom-start"
sideOffset={4}
popupClassName="min-w-[180px]"
>
<NodeMenu
menuType="dropdown"
type={isFolder ? 'folder' : 'file'}
nodeId={node.data.id}
onClose={handleMenuClose}
fileInputRef={fileOperations.fileInputRef}
folderInputRef={fileOperations.folderInputRef}
isLoading={fileOperations.isLoading}
onDownload={fileOperations.handleDownload}
onNewFile={fileOperations.handleNewFile}
onNewFolder={fileOperations.handleNewFolder}
onFileChange={fileOperations.handleFileChange}
onFolderChange={fileOperations.handleFolderChange}
onRename={fileOperations.handleRename}
onDeleteClick={fileOperations.handleDeleteClick}
/>
</DropdownMenuContent>
</DropdownMenu>
</ContextMenuTrigger>
<ContextMenuContent popupClassName="min-w-[180px]">
<NodeMenu
menuType="context"
type={isFolder ? 'folder' : 'file'}
nodeId={node.data.id}
onClose={handleMenuClose}
fileInputRef={fileOperations.fileInputRef}
folderInputRef={fileOperations.folderInputRef}
isLoading={fileOperations.isLoading}
onDownload={fileOperations.handleDownload}
onNewFile={fileOperations.handleNewFile}
onNewFolder={fileOperations.handleNewFolder}
onFileChange={fileOperations.handleFileChange}
onFolderChange={fileOperations.handleFolderChange}
onRename={fileOperations.handleRename}
onDeleteClick={fileOperations.handleDeleteClick}
/>
</ContextMenuContent>
</ContextMenu>
<AlertDialog
open={fileOperations.showDeleteConfirm}
onOpenChange={(open) => {
if (!open)
fileOperations.handleDeleteCancel()
}}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 p-6 pb-4">
<AlertDialogTitle className="text-text-primary title-2xl-semi-bold">
{deleteConfirmTitle}
</AlertDialogTitle>
<AlertDialogDescription className="text-text-secondary system-sm-regular">
{deleteConfirmContent}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
disabled={fileOperations.isDeleting}
onClick={() => {
void fileOperations.handleDeleteConfirm()
}}
>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</>
)
}