Merge pull request #505 from AutoMaker-Org/feature/v0.12.0rc-1768509532254-tt6z

fix: "Remove Project" button not working on right click of the project
This commit is contained in:
Shirone
2026-01-15 22:05:24 +00:00
committed by GitHub

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, memo } from 'react'; import { useEffect, useRef, useState, memo, useCallback } from 'react';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react'; import { Edit2, Trash2, Palette, ChevronRight, Moon, Sun, Monitor } from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { type ThemeMode, useAppStore } from '@/store/app-store'; import { type ThemeMode, useAppStore } from '@/store/app-store';
import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { ConfirmDialog } from '@/components/ui/confirm-dialog';
@@ -8,6 +9,9 @@ import type { Project } from '@/lib/electron';
import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '@/components/layout/sidebar/constants'; import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '@/components/layout/sidebar/constants';
import { useThemePreview } from '@/components/layout/sidebar/hooks'; import { useThemePreview } from '@/components/layout/sidebar/hooks';
// Constant for "use global theme" option
const USE_GLOBAL_THEME = '' as const;
// Constants for z-index values // Constants for z-index values
const Z_INDEX = { const Z_INDEX = {
CONTEXT_MENU: 100, CONTEXT_MENU: 100,
@@ -124,19 +128,26 @@ export function ProjectContextMenu({
} = useAppStore(); } = useAppStore();
const [showRemoveDialog, setShowRemoveDialog] = useState(false); const [showRemoveDialog, setShowRemoveDialog] = useState(false);
const [showThemeSubmenu, setShowThemeSubmenu] = useState(false); const [showThemeSubmenu, setShowThemeSubmenu] = useState(false);
const [removeConfirmed, setRemoveConfirmed] = useState(false);
const themeSubmenuRef = useRef<HTMLDivElement>(null); const themeSubmenuRef = useRef<HTMLDivElement>(null);
const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme }); const { handlePreviewEnter, handlePreviewLeave } = useThemePreview({ setPreviewTheme });
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: globalThis.MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) { // Don't close if a confirmation dialog is open (dialog is in a portal)
if (showRemoveDialog) return;
if (menuRef.current && !menuRef.current.contains(event.target as globalThis.Node)) {
setPreviewTheme(null); setPreviewTheme(null);
onClose(); onClose();
} }
}; };
const handleEscape = (event: KeyboardEvent) => { const handleEscape = (event: globalThis.KeyboardEvent) => {
// Don't close if a confirmation dialog is open (let the dialog handle escape)
if (showRemoveDialog) return;
if (event.key === 'Escape') { if (event.key === 'Escape') {
setPreviewTheme(null); setPreviewTheme(null);
onClose(); onClose();
@@ -150,7 +161,7 @@ export function ProjectContextMenu({
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape); document.removeEventListener('keydown', handleEscape);
}; };
}, [onClose, setPreviewTheme]); }, [onClose, setPreviewTheme, showRemoveDialog]);
const handleEdit = () => { const handleEdit = () => {
onEdit(project); onEdit(project);
@@ -160,166 +171,187 @@ export function ProjectContextMenu({
setShowRemoveDialog(true); setShowRemoveDialog(true);
}; };
const handleThemeSelect = (value: ThemeMode | '') => { const handleThemeSelect = useCallback(
setPreviewTheme(null); (value: ThemeMode | typeof USE_GLOBAL_THEME) => {
if (value !== '') { setPreviewTheme(null);
setTheme(value); const isUsingGlobal = value === USE_GLOBAL_THEME;
} else { setTheme(isUsingGlobal ? globalTheme : value);
setTheme(globalTheme); setProjectTheme(project.id, isUsingGlobal ? null : value);
} setShowThemeSubmenu(false);
setProjectTheme(project.id, value === '' ? null : value); },
setShowThemeSubmenu(false); [globalTheme, project.id, setPreviewTheme, setProjectTheme, setTheme]
}; );
const handleConfirmRemove = () => { const handleConfirmRemove = useCallback(() => {
moveProjectToTrash(project.id); moveProjectToTrash(project.id);
onClose(); toast.success('Project removed', {
}; description: `${project.name} has been removed from your projects list`,
});
setRemoveConfirmed(true);
}, [moveProjectToTrash, project.id, project.name]);
const handleDialogClose = useCallback(
(isOpen: boolean) => {
setShowRemoveDialog(isOpen);
// Close the context menu when dialog closes (whether confirmed or cancelled)
// This prevents the context menu from reappearing after dialog interaction
if (!isOpen) {
// Reset confirmation state
setRemoveConfirmed(false);
// Always close the context menu when dialog closes
onClose();
}
},
[onClose]
);
return ( return (
<> <>
<div {/* Hide context menu when confirm dialog is open */}
ref={menuRef} {!showRemoveDialog && (
className={cn( <div
'fixed min-w-48 rounded-lg', ref={menuRef}
'bg-popover text-popover-foreground', className={cn(
'border border-border shadow-lg', 'fixed min-w-48 rounded-lg',
'animate-in fade-in zoom-in-95 duration-100' 'bg-popover text-popover-foreground',
)} 'border border-border shadow-lg',
style={{ 'animate-in fade-in zoom-in-95 duration-100'
top: position.y, )}
left: position.x, style={{
zIndex: Z_INDEX.CONTEXT_MENU, top: position.y,
}} left: position.x,
data-testid="project-context-menu" zIndex: Z_INDEX.CONTEXT_MENU,
> }}
<div className="p-1"> data-testid="project-context-menu"
<button >
onClick={handleEdit} <div className="p-1">
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent'
)}
data-testid="edit-project-button"
>
<Edit2 className="w-4 h-4" />
<span>Edit Name & Icon</span>
</button>
{/* Theme Submenu Trigger */}
<div
className="relative"
onMouseEnter={() => setShowThemeSubmenu(true)}
onMouseLeave={() => {
setShowThemeSubmenu(false);
setPreviewTheme(null);
}}
>
<button <button
onClick={() => setShowThemeSubmenu(!showThemeSubmenu)} onClick={handleEdit}
className={cn( className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md', 'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left', 'text-sm font-medium text-left',
'hover:bg-accent transition-colors', 'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent' 'focus:outline-none focus:bg-accent'
)} )}
data-testid="theme-project-button" data-testid="edit-project-button"
> >
<Palette className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
<span className="flex-1">Project Theme</span> <span>Edit Name & Icon</span>
{project.theme && (
<span className="text-[10px] text-muted-foreground capitalize">
{project.theme}
</span>
)}
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</button> </button>
{/* Theme Submenu */} {/* Theme Submenu Trigger */}
{showThemeSubmenu && ( <div
<div className="relative"
ref={themeSubmenuRef} onMouseEnter={() => setShowThemeSubmenu(true)}
onMouseLeave={() => {
setShowThemeSubmenu(false);
setPreviewTheme(null);
}}
>
<button
onClick={() => setShowThemeSubmenu(!showThemeSubmenu)}
className={cn( className={cn(
'absolute left-full top-0 ml-1 min-w-[420px] rounded-lg', 'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'bg-popover text-popover-foreground', 'text-sm font-medium text-left',
'border border-border shadow-lg', 'hover:bg-accent transition-colors',
'animate-in fade-in zoom-in-95 duration-100' 'focus:outline-none focus:bg-accent'
)} )}
style={{ zIndex: Z_INDEX.THEME_SUBMENU }} data-testid="theme-project-button"
data-testid="project-theme-submenu"
> >
<div className="p-2"> <Palette className="w-4 h-4" />
{/* Use Global Option */} <span className="flex-1">Project Theme</span>
<button {project.theme && (
onPointerEnter={() => handlePreviewEnter(globalTheme)} <span className="text-[10px] text-muted-foreground capitalize">
onPointerLeave={handlePreviewLeave} {project.theme}
onClick={() => handleThemeSelect('')} </span>
className={cn( )}
'w-full flex items-center gap-2 px-3 py-2 rounded-md', <ChevronRight className="w-4 h-4 text-muted-foreground" />
'text-sm font-medium text-left', </button>
'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent',
!project.theme && 'bg-accent'
)}
data-testid="project-theme-global"
>
<Monitor className="w-4 h-4" />
<span>Use Global</span>
<span className="text-[10px] text-muted-foreground ml-1 capitalize">
({globalTheme})
</span>
</button>
<div className="h-px bg-border my-2" /> {/* Theme Submenu */}
{showThemeSubmenu && (
<div
ref={themeSubmenuRef}
className={cn(
'absolute left-full top-0 ml-1 min-w-[420px] rounded-lg',
'bg-popover text-popover-foreground',
'border border-border shadow-lg',
'animate-in fade-in zoom-in-95 duration-100'
)}
style={{ zIndex: Z_INDEX.THEME_SUBMENU }}
data-testid="project-theme-submenu"
>
<div className="p-2">
{/* Use Global Option */}
<button
onPointerEnter={() => handlePreviewEnter(globalTheme)}
onPointerLeave={handlePreviewLeave}
onClick={() => handleThemeSelect(USE_GLOBAL_THEME)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left',
'hover:bg-accent transition-colors',
'focus:outline-none focus:bg-accent',
!project.theme && 'bg-accent'
)}
data-testid="project-theme-global"
>
<Monitor className="w-4 h-4" />
<span>Use Global</span>
<span className="text-[10px] text-muted-foreground ml-1 capitalize">
({globalTheme})
</span>
</button>
{/* Two Column Layout - Using reusable ThemeColumn component */} <div className="h-px bg-border my-2" />
<div className="flex gap-2">
<ThemeColumn {/* Two Column Layout - Using reusable ThemeColumn component */}
title="Dark" <div className="flex gap-2">
icon={Moon} <ThemeColumn
themes={PROJECT_DARK_THEMES as ThemeOption[]} title="Dark"
selectedTheme={project.theme as ThemeMode | null} icon={Moon}
onPreviewEnter={handlePreviewEnter} themes={PROJECT_DARK_THEMES as ThemeOption[]}
onPreviewLeave={handlePreviewLeave} selectedTheme={project.theme as ThemeMode | null}
onSelect={handleThemeSelect} onPreviewEnter={handlePreviewEnter}
/> onPreviewLeave={handlePreviewLeave}
<ThemeColumn onSelect={handleThemeSelect}
title="Light" />
icon={Sun} <ThemeColumn
themes={PROJECT_LIGHT_THEMES as ThemeOption[]} title="Light"
selectedTheme={project.theme as ThemeMode | null} icon={Sun}
onPreviewEnter={handlePreviewEnter} themes={PROJECT_LIGHT_THEMES as ThemeOption[]}
onPreviewLeave={handlePreviewLeave} selectedTheme={project.theme as ThemeMode | null}
onSelect={handleThemeSelect} onPreviewEnter={handlePreviewEnter}
/> onPreviewLeave={handlePreviewLeave}
onSelect={handleThemeSelect}
/>
</div>
</div> </div>
</div> </div>
</div> )}
)} </div>
</div>
<button <button
onClick={handleRemove} onClick={handleRemove}
className={cn( className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-md', 'w-full flex items-center gap-2 px-3 py-2 rounded-md',
'text-sm font-medium text-left', 'text-sm font-medium text-left',
'text-destructive hover:bg-destructive/10', 'text-destructive hover:bg-destructive/10',
'transition-colors', 'transition-colors',
'focus:outline-none focus:bg-destructive/10' 'focus:outline-none focus:bg-destructive/10'
)} )}
data-testid="remove-project-button" data-testid="remove-project-button"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
<span>Remove Project</span> <span>Remove Project</span>
</button> </button>
</div>
</div> </div>
</div> )}
<ConfirmDialog <ConfirmDialog
open={showRemoveDialog} open={showRemoveDialog}
onOpenChange={setShowRemoveDialog} onOpenChange={handleDialogClose}
onConfirm={handleConfirmRemove} onConfirm={handleConfirmRemove}
title="Remove Project" title="Remove Project"
description={`Are you sure you want to remove "${project.name}" from the project list? This won't delete any files on disk.`} description={`Are you sure you want to remove "${project.name}" from the project list? This won't delete any files on disk.`}