Merge branch 'main' of https://github.com/refactoringhq/tolaria
This commit is contained in:
@@ -91,6 +91,31 @@ describe('FolderTree', () => {
|
||||
expect(onStartRenameFolder).toHaveBeenCalledWith('areas')
|
||||
})
|
||||
|
||||
it('shows inline rename and delete actions for folders', () => {
|
||||
const onDeleteFolder = vi.fn()
|
||||
const onStartRenameFolder = vi.fn()
|
||||
const onSelect = vi.fn()
|
||||
render(
|
||||
<FolderTree
|
||||
folders={mockFolders}
|
||||
selection={defaultSelection}
|
||||
onSelect={onSelect}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
onRenameFolder={vi.fn().mockResolvedValue(true)}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
onCancelRenameFolder={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('rename-folder-btn:projects'))
|
||||
fireEvent.click(screen.getByTestId('delete-folder-btn:projects'))
|
||||
|
||||
expect(onSelect).toHaveBeenNthCalledWith(1, { kind: 'folder', path: 'projects' })
|
||||
expect(onStartRenameFolder).toHaveBeenCalledWith('projects')
|
||||
expect(onSelect).toHaveBeenNthCalledWith(2, { kind: 'folder', path: 'projects' })
|
||||
expect(onDeleteFolder).toHaveBeenCalledWith('projects')
|
||||
})
|
||||
|
||||
it('shows the rename input when a folder is being renamed', () => {
|
||||
render(
|
||||
<FolderTree
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { memo, useCallback, type MouseEvent as ReactMouseEvent } from 'react'
|
||||
import { memo, useCallback, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react'
|
||||
import {
|
||||
CaretDown,
|
||||
CaretRight,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
} from '@phosphor-icons/react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -56,6 +58,7 @@ function FolderItemRow({
|
||||
isExpanded,
|
||||
isSelected,
|
||||
node,
|
||||
onDeleteFolder,
|
||||
onOpenMenu,
|
||||
onSelect,
|
||||
onStartRenameFolder,
|
||||
@@ -65,6 +68,7 @@ function FolderItemRow({
|
||||
isExpanded: boolean
|
||||
isSelected: boolean
|
||||
node: FolderNode
|
||||
onDeleteFolder?: (folderPath: string) => void
|
||||
onOpenMenu: FolderTreeRowProps['onOpenMenu']
|
||||
onSelect: () => void
|
||||
onStartRenameFolder?: (folderPath: string) => void
|
||||
@@ -72,11 +76,12 @@ function FolderItemRow({
|
||||
}) {
|
||||
const hasChildren = node.children.length > 0
|
||||
const expandLabel = isExpanded ? `Collapse ${node.name}` : `Expand ${node.name}`
|
||||
const hasActions = !!onStartRenameFolder || !!onDeleteFolder
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-center gap-1 rounded-[5px] transition-colors',
|
||||
'group relative flex items-center gap-1 rounded-[5px] transition-colors',
|
||||
isSelected
|
||||
? 'bg-[var(--accent-blue-light,rgba(0,100,255,0.08))] text-primary'
|
||||
: 'text-foreground hover:bg-accent',
|
||||
@@ -94,12 +99,44 @@ function FolderItemRow({
|
||||
onToggle={() => onToggle(node.path)}
|
||||
/>
|
||||
<FolderSelectButton
|
||||
hasActions={hasActions}
|
||||
isExpanded={isExpanded}
|
||||
isSelected={isSelected}
|
||||
node={node}
|
||||
onSelect={onSelect}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
/>
|
||||
{hasActions && (
|
||||
<div className="pointer-events-none absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
{onStartRenameFolder && (
|
||||
<FolderActionButton
|
||||
ariaLabel={`Rename ${node.name}`}
|
||||
testId={`rename-folder-btn:${node.path}`}
|
||||
title="Rename folder"
|
||||
onClick={() => {
|
||||
onSelect()
|
||||
onStartRenameFolder(node.path)
|
||||
}}
|
||||
>
|
||||
<PencilSimple size={12} />
|
||||
</FolderActionButton>
|
||||
)}
|
||||
{onDeleteFolder && (
|
||||
<FolderActionButton
|
||||
ariaLabel={`Delete ${node.name}`}
|
||||
testId={`delete-folder-btn:${node.path}`}
|
||||
title="Delete folder"
|
||||
destructive
|
||||
onClick={() => {
|
||||
onSelect()
|
||||
onDeleteFolder(node.path)
|
||||
}}
|
||||
>
|
||||
<Trash size={12} />
|
||||
</FolderActionButton>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -137,13 +174,52 @@ function FolderToggleButton({
|
||||
)
|
||||
}
|
||||
|
||||
function FolderActionButton({
|
||||
ariaLabel,
|
||||
children,
|
||||
destructive = false,
|
||||
onClick,
|
||||
testId,
|
||||
title,
|
||||
}: {
|
||||
ariaLabel: string
|
||||
children: ReactNode
|
||||
destructive?: boolean
|
||||
onClick: () => void
|
||||
testId: string
|
||||
title: string
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
className={cn(
|
||||
'h-5 w-5 rounded p-0 text-muted-foreground',
|
||||
destructive ? 'hover:text-destructive' : 'hover:text-foreground',
|
||||
)}
|
||||
data-testid={testId}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function FolderSelectButton({
|
||||
hasActions,
|
||||
isExpanded,
|
||||
isSelected,
|
||||
node,
|
||||
onSelect,
|
||||
onStartRenameFolder,
|
||||
}: {
|
||||
hasActions: boolean
|
||||
isExpanded: boolean
|
||||
isSelected: boolean
|
||||
node: FolderNode
|
||||
@@ -156,6 +232,7 @@ function FolderSelectButton({
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'h-7 flex-1 justify-start gap-2 rounded-[5px] px-2 py-0 text-left text-[13px]',
|
||||
hasActions && 'pr-12',
|
||||
isSelected ? 'font-medium text-primary hover:text-primary' : 'hover:text-foreground',
|
||||
)}
|
||||
title={node.path}
|
||||
@@ -258,6 +335,7 @@ export const FolderTreeRow = memo(function FolderTreeRow({
|
||||
isExpanded={isExpanded}
|
||||
isSelected={isSelected}
|
||||
node={node}
|
||||
onDeleteFolder={onDeleteFolder}
|
||||
onOpenMenu={onOpenMenu}
|
||||
onSelect={selectFolder}
|
||||
onStartRenameFolder={onStartRenameFolder}
|
||||
|
||||
Reference in New Issue
Block a user