mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
@@ -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.`}
|
||||||
|
|||||||
Reference in New Issue
Block a user