mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
refactor: Enhance project management features and UI components
- Updated create-pr.ts to improve commit error handling and logging. - Enhanced project-switcher.tsx with new folder opening functionality and state management for project setup. - Expanded icon-picker.tsx to include a comprehensive list of icons organized by category. - Replaced dialog components with popover components for auto mode and plan settings, improving UI responsiveness. - Refactored board-view components to streamline feature management and enhance user experience. - Removed outdated dialog components and replaced them with popover alternatives for better accessibility. These changes aim to improve the overall usability and functionality of the project management interface.
This commit is contained in:
@@ -70,9 +70,8 @@ export function createCreatePRHandler() {
|
||||
logger.debug(`Changed files:\n${status}`);
|
||||
}
|
||||
|
||||
// If there are changes, commit them
|
||||
// If there are changes, commit them before creating the PR
|
||||
let commitHash: string | null = null;
|
||||
let commitError: string | null = null;
|
||||
if (hasChanges) {
|
||||
const message = commitMessage || `Changes from ${branchName}`;
|
||||
logger.debug(`Committing changes with message: ${message}`);
|
||||
@@ -98,14 +97,13 @@ export function createCreatePRHandler() {
|
||||
logger.info(`Commit successful: ${commitHash}`);
|
||||
} catch (commitErr: unknown) {
|
||||
const err = commitErr as { stderr?: string; message?: string };
|
||||
commitError = err.stderr || err.message || 'Commit failed';
|
||||
const commitError = err.stderr || err.message || 'Commit failed';
|
||||
logger.error(`Commit failed: ${commitError}`);
|
||||
|
||||
// Return error immediately - don't proceed with push/PR if commit fails
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Failed to commit changes: ${commitError}`,
|
||||
commitError,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -381,9 +379,8 @@ export function createCreatePRHandler() {
|
||||
success: true,
|
||||
result: {
|
||||
branch: branchName,
|
||||
committed: hasChanges && !commitError,
|
||||
committed: hasChanges,
|
||||
commitHash,
|
||||
commitError: commitError || undefined,
|
||||
pushed: true,
|
||||
prUrl,
|
||||
prNumber,
|
||||
|
||||
@@ -10,43 +10,434 @@ interface IconPickerProps {
|
||||
onSelectIcon: (icon: string | null) => void;
|
||||
}
|
||||
|
||||
// Popular project-related icons
|
||||
// Comprehensive list of project-related icons from Lucide
|
||||
// Organized by category for easier browsing
|
||||
const POPULAR_ICONS = [
|
||||
// Folders & Files
|
||||
'Folder',
|
||||
'FolderOpen',
|
||||
'FolderCode',
|
||||
'FolderGit',
|
||||
'FolderKanban',
|
||||
'Package',
|
||||
'Box',
|
||||
'Boxes',
|
||||
'FolderTree',
|
||||
'FolderInput',
|
||||
'FolderOutput',
|
||||
'FolderPlus',
|
||||
'File',
|
||||
'FileCode',
|
||||
'FileText',
|
||||
'FileJson',
|
||||
'FileImage',
|
||||
'FileVideo',
|
||||
'FileAudio',
|
||||
'FileSpreadsheet',
|
||||
'Files',
|
||||
'Archive',
|
||||
|
||||
// Code & Development
|
||||
'Code',
|
||||
'Code2',
|
||||
'Braces',
|
||||
'FileCode',
|
||||
'Brackets',
|
||||
'Terminal',
|
||||
'Globe',
|
||||
'Server',
|
||||
'Database',
|
||||
'TerminalSquare',
|
||||
'Command',
|
||||
'GitBranch',
|
||||
'GitCommit',
|
||||
'GitMerge',
|
||||
'GitPullRequest',
|
||||
'GitCompare',
|
||||
'GitFork',
|
||||
'GitHub',
|
||||
'Gitlab',
|
||||
'Bitbucket',
|
||||
'Vscode',
|
||||
|
||||
// Packages & Containers
|
||||
'Package',
|
||||
'PackageSearch',
|
||||
'PackageCheck',
|
||||
'PackageX',
|
||||
'Box',
|
||||
'Boxes',
|
||||
'Container',
|
||||
|
||||
// UI & Design
|
||||
'Layout',
|
||||
'LayoutGrid',
|
||||
'LayoutList',
|
||||
'LayoutDashboard',
|
||||
'LayoutTemplate',
|
||||
'Layers',
|
||||
'Layers2',
|
||||
'Layers3',
|
||||
'Blocks',
|
||||
'Component',
|
||||
'Puzzle',
|
||||
'Palette',
|
||||
'Paintbrush',
|
||||
'Brush',
|
||||
'PenTool',
|
||||
'Ruler',
|
||||
'Grid',
|
||||
'Grid3x3',
|
||||
'Square',
|
||||
'RectangleHorizontal',
|
||||
'RectangleVertical',
|
||||
'Circle',
|
||||
|
||||
// Tools & Settings
|
||||
'Cog',
|
||||
'Settings',
|
||||
'Settings2',
|
||||
'Wrench',
|
||||
'Hammer',
|
||||
'Screwdriver',
|
||||
'WrenchIcon',
|
||||
'Tool',
|
||||
'ScrewdriverWrench',
|
||||
'Sliders',
|
||||
'SlidersHorizontal',
|
||||
'Filter',
|
||||
'FilterX',
|
||||
|
||||
// Technology & Infrastructure
|
||||
'Server',
|
||||
'ServerCrash',
|
||||
'ServerCog',
|
||||
'Database',
|
||||
'DatabaseBackup',
|
||||
'CloudUpload',
|
||||
'CloudDownload',
|
||||
'CloudOff',
|
||||
'Globe',
|
||||
'Globe2',
|
||||
'Network',
|
||||
'Wifi',
|
||||
'WifiOff',
|
||||
'Router',
|
||||
'Cpu',
|
||||
'MemoryStick',
|
||||
'HardDrive',
|
||||
'HardDriveIcon',
|
||||
'CircuitBoard',
|
||||
'Microchip',
|
||||
'Monitor',
|
||||
'MonitorSpeaker',
|
||||
'Laptop',
|
||||
'Smartphone',
|
||||
'Tablet',
|
||||
'Mouse',
|
||||
'Keyboard',
|
||||
'Headphones',
|
||||
'Printer',
|
||||
'Scanner',
|
||||
|
||||
// Workflow & Process
|
||||
'Workflow',
|
||||
'Zap',
|
||||
'Rocket',
|
||||
'Sparkles',
|
||||
'Star',
|
||||
'Heart',
|
||||
'Flame',
|
||||
'Lightning',
|
||||
'Bolt',
|
||||
'Target',
|
||||
'Flag',
|
||||
'FlagTriangleRight',
|
||||
'CheckCircle',
|
||||
'CheckCircle2',
|
||||
'XCircle',
|
||||
'AlertCircle',
|
||||
'Info',
|
||||
'HelpCircle',
|
||||
'Clock',
|
||||
'Timer',
|
||||
'Stopwatch',
|
||||
'Calendar',
|
||||
'CalendarDays',
|
||||
'CalendarCheck',
|
||||
'CalendarClock',
|
||||
|
||||
// Security & Access
|
||||
'Shield',
|
||||
'ShieldCheck',
|
||||
'ShieldAlert',
|
||||
'ShieldOff',
|
||||
'Lock',
|
||||
'Unlock',
|
||||
'Key',
|
||||
'Cpu',
|
||||
'CircuitBoard',
|
||||
'Workflow',
|
||||
'KeyRound',
|
||||
'Eye',
|
||||
'EyeOff',
|
||||
'User',
|
||||
'Users',
|
||||
'UserCheck',
|
||||
'UserX',
|
||||
'UserPlus',
|
||||
'UserCog',
|
||||
|
||||
// Business & Finance
|
||||
'Briefcase',
|
||||
'Building',
|
||||
'Building2',
|
||||
'Store',
|
||||
'ShoppingCart',
|
||||
'ShoppingBag',
|
||||
'CreditCard',
|
||||
'Wallet',
|
||||
'DollarSign',
|
||||
'Euro',
|
||||
'PoundSterling',
|
||||
'Yen',
|
||||
'Coins',
|
||||
'Receipt',
|
||||
'ChartBar',
|
||||
'ChartLine',
|
||||
'ChartPie',
|
||||
'TrendingUp',
|
||||
'TrendingDown',
|
||||
'Activity',
|
||||
'BarChart',
|
||||
'LineChart',
|
||||
'PieChart',
|
||||
|
||||
// Communication & Media
|
||||
'MessageSquare',
|
||||
'MessageCircle',
|
||||
'Mail',
|
||||
'MailOpen',
|
||||
'Send',
|
||||
'Inbox',
|
||||
'Phone',
|
||||
'PhoneCall',
|
||||
'Video',
|
||||
'VideoOff',
|
||||
'Camera',
|
||||
'CameraOff',
|
||||
'Image',
|
||||
'ImageIcon',
|
||||
'Film',
|
||||
'Music',
|
||||
'Mic',
|
||||
'MicOff',
|
||||
'Volume',
|
||||
'Volume2',
|
||||
'VolumeX',
|
||||
'Radio',
|
||||
'Podcast',
|
||||
|
||||
// Social & Community
|
||||
'Heart',
|
||||
'HeartHandshake',
|
||||
'Star',
|
||||
'StarOff',
|
||||
'ThumbsUp',
|
||||
'ThumbsDown',
|
||||
'Share',
|
||||
'Share2',
|
||||
'Link',
|
||||
'Link2',
|
||||
'ExternalLink',
|
||||
'AtSign',
|
||||
'Hash',
|
||||
'Hashtag',
|
||||
'Tag',
|
||||
'Tags',
|
||||
|
||||
// Navigation & Location
|
||||
'Compass',
|
||||
'Map',
|
||||
'MapPin',
|
||||
'Navigation',
|
||||
'Navigation2',
|
||||
'Route',
|
||||
'Plane',
|
||||
'Car',
|
||||
'Bike',
|
||||
'Ship',
|
||||
'Train',
|
||||
'Bus',
|
||||
|
||||
// Science & Education
|
||||
'FlaskConical',
|
||||
'FlaskRound',
|
||||
'Beaker',
|
||||
'TestTube',
|
||||
'TestTube2',
|
||||
'Microscope',
|
||||
'Atom',
|
||||
'Brain',
|
||||
'GraduationCap',
|
||||
'Book',
|
||||
'BookOpen',
|
||||
'BookMarked',
|
||||
'Library',
|
||||
'School',
|
||||
'University',
|
||||
|
||||
// Food & Health
|
||||
'Coffee',
|
||||
'Utensils',
|
||||
'UtensilsCrossed',
|
||||
'Apple',
|
||||
'Cherry',
|
||||
'Cookie',
|
||||
'Cake',
|
||||
'Pizza',
|
||||
'Beer',
|
||||
'Wine',
|
||||
'HeartPulse',
|
||||
'Dumbbell',
|
||||
'Running',
|
||||
|
||||
// Nature & Weather
|
||||
'Tree',
|
||||
'TreePine',
|
||||
'Leaf',
|
||||
'Flower',
|
||||
'Flower2',
|
||||
'Sun',
|
||||
'Moon',
|
||||
'CloudRain',
|
||||
'CloudSnow',
|
||||
'CloudLightning',
|
||||
'Droplet',
|
||||
'Wind',
|
||||
'Snowflake',
|
||||
'Umbrella',
|
||||
|
||||
// Objects & Symbols
|
||||
'Puzzle',
|
||||
'PuzzleIcon',
|
||||
'Gamepad',
|
||||
'Gamepad2',
|
||||
'Dice',
|
||||
'Dice1',
|
||||
'Dice6',
|
||||
'Gem',
|
||||
'Crown',
|
||||
'Trophy',
|
||||
'Medal',
|
||||
'Award',
|
||||
'Gift',
|
||||
'GiftIcon',
|
||||
'Bell',
|
||||
'BellOff',
|
||||
'BellRing',
|
||||
'Home',
|
||||
'House',
|
||||
'DoorOpen',
|
||||
'DoorClosed',
|
||||
'Window',
|
||||
'Lightbulb',
|
||||
'LightbulbOff',
|
||||
'Candle',
|
||||
'Flashlight',
|
||||
'FlashlightOff',
|
||||
'Battery',
|
||||
'BatteryFull',
|
||||
'BatteryLow',
|
||||
'BatteryCharging',
|
||||
'Plug',
|
||||
'PlugZap',
|
||||
'Power',
|
||||
'PowerOff',
|
||||
|
||||
// Arrows & Directions
|
||||
'ArrowRight',
|
||||
'ArrowLeft',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
'ArrowUpRight',
|
||||
'ArrowDownRight',
|
||||
'ArrowDownLeft',
|
||||
'ArrowUpLeft',
|
||||
'ChevronRight',
|
||||
'ChevronLeft',
|
||||
'ChevronUp',
|
||||
'ChevronDown',
|
||||
'Move',
|
||||
'MoveUp',
|
||||
'MoveDown',
|
||||
'MoveLeft',
|
||||
'MoveRight',
|
||||
'RotateCw',
|
||||
'RotateCcw',
|
||||
'RefreshCw',
|
||||
'RefreshCcw',
|
||||
|
||||
// Shapes & Symbols
|
||||
'Diamond',
|
||||
'Pentagon',
|
||||
'Cross',
|
||||
'Plus',
|
||||
'Minus',
|
||||
'X',
|
||||
'Check',
|
||||
'Divide',
|
||||
'Equal',
|
||||
'Infinity',
|
||||
'Percent',
|
||||
|
||||
// Miscellaneous
|
||||
'Bot',
|
||||
'Wand',
|
||||
'Wand2',
|
||||
'Magic',
|
||||
'Stars',
|
||||
'Comet',
|
||||
'Satellite',
|
||||
'SatelliteDish',
|
||||
'Radar',
|
||||
'RadarIcon',
|
||||
'Scan',
|
||||
'ScanLine',
|
||||
'QrCode',
|
||||
'Barcode',
|
||||
'ScanSearch',
|
||||
'Search',
|
||||
'SearchX',
|
||||
'ZoomIn',
|
||||
'ZoomOut',
|
||||
'Maximize',
|
||||
'Minimize',
|
||||
'Maximize2',
|
||||
'Minimize2',
|
||||
'Expand',
|
||||
'Shrink',
|
||||
'Copy',
|
||||
'CopyCheck',
|
||||
'Clipboard',
|
||||
'ClipboardCheck',
|
||||
'ClipboardCopy',
|
||||
'ClipboardList',
|
||||
'ClipboardPaste',
|
||||
'Scissors',
|
||||
'Cut',
|
||||
'FileEdit',
|
||||
'Pen',
|
||||
'Pencil',
|
||||
'Eraser',
|
||||
'Trash',
|
||||
'Trash2',
|
||||
'Delete',
|
||||
'ArchiveRestore',
|
||||
'Download',
|
||||
'Upload',
|
||||
'Save',
|
||||
'SaveAll',
|
||||
'FilePlus',
|
||||
'FileMinus',
|
||||
'FileX',
|
||||
'FileCheck',
|
||||
'FileQuestion',
|
||||
'FileWarning',
|
||||
'FileSearch',
|
||||
'FolderSearch',
|
||||
'FolderX',
|
||||
'FolderCheck',
|
||||
'FolderMinus',
|
||||
'FolderSync',
|
||||
'FolderUp',
|
||||
'FolderDown',
|
||||
];
|
||||
|
||||
export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
|
||||
@@ -94,7 +485,7 @@ export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
|
||||
)}
|
||||
|
||||
{/* Icons Grid */}
|
||||
<ScrollArea className="h-64 rounded-md border">
|
||||
<ScrollArea className="h-96 rounded-md border">
|
||||
<div className="grid grid-cols-6 gap-1 p-2">
|
||||
{filteredIcons.map((iconName) => {
|
||||
const IconComponent = getIconComponent(iconName);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Edit2, Trash2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import type { Project } from '@/lib/electron';
|
||||
|
||||
interface ProjectContextMenuProps {
|
||||
@@ -19,6 +20,7 @@ export function ProjectContextMenu({
|
||||
}: ProjectContextMenuProps) {
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { moveProjectToTrash } = useAppStore();
|
||||
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
@@ -47,57 +49,73 @@ export function ProjectContextMenu({
|
||||
};
|
||||
|
||||
const handleRemove = () => {
|
||||
if (confirm(`Remove "${project.name}" from the project list?`)) {
|
||||
moveProjectToTrash(project.id);
|
||||
}
|
||||
setShowRemoveDialog(true);
|
||||
};
|
||||
|
||||
const handleConfirmRemove = () => {
|
||||
moveProjectToTrash(project.id);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={cn(
|
||||
'fixed z-[100] min-w-48 rounded-lg',
|
||||
'bg-popover text-popover-foreground',
|
||||
'border border-border shadow-lg',
|
||||
'animate-in fade-in zoom-in-95 duration-100'
|
||||
)}
|
||||
style={{
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
}}
|
||||
data-testid="project-context-menu"
|
||||
>
|
||||
<div className="p-1">
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
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>
|
||||
<>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={cn(
|
||||
'fixed z-[100] min-w-48 rounded-lg',
|
||||
'bg-popover text-popover-foreground',
|
||||
'border border-border shadow-lg',
|
||||
'animate-in fade-in zoom-in-95 duration-100'
|
||||
)}
|
||||
style={{
|
||||
top: position.y,
|
||||
left: position.x,
|
||||
}}
|
||||
data-testid="project-context-menu"
|
||||
>
|
||||
<div className="p-1">
|
||||
<button
|
||||
onClick={handleEdit}
|
||||
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>
|
||||
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
|
||||
'text-sm font-medium text-left',
|
||||
'text-destructive hover:bg-destructive/10',
|
||||
'transition-colors',
|
||||
'focus:outline-none focus:bg-destructive/10'
|
||||
)}
|
||||
data-testid="remove-project-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Remove Project</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleRemove}
|
||||
className={cn(
|
||||
'w-full flex items-center gap-2 px-3 py-2 rounded-md',
|
||||
'text-sm font-medium text-left',
|
||||
'text-destructive hover:bg-destructive/10',
|
||||
'transition-colors',
|
||||
'focus:outline-none focus:bg-destructive/10'
|
||||
)}
|
||||
data-testid="remove-project-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Remove Project</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showRemoveDialog}
|
||||
onOpenChange={setShowRemoveDialog}
|
||||
onConfirm={handleConfirmRemove}
|
||||
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.`}
|
||||
icon={Trash2}
|
||||
iconClassName="text-destructive"
|
||||
confirmText="Remove"
|
||||
confirmVariant="destructive"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { Plus, Bug } from 'lucide-react';
|
||||
import { Plus, Bug, FolderOpen } from 'lucide-react';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useAppStore, type ThemeMode } from '@/store/app-store';
|
||||
import { useOSDetection } from '@/hooks/use-os-detection';
|
||||
import { ProjectSwitcherItem } from './components/project-switcher-item';
|
||||
import { ProjectContextMenu } from './components/project-context-menu';
|
||||
@@ -12,6 +12,9 @@ import { OnboardingDialog } from '@/components/layout/sidebar/dialogs';
|
||||
import { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks';
|
||||
import type { Project } from '@/lib/electron';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import { CreateSpecDialog } from '@/components/views/spec-view/dialogs';
|
||||
|
||||
function getOSAbbreviation(os: string): string {
|
||||
switch (os) {
|
||||
@@ -34,6 +37,8 @@ export function ProjectSwitcher() {
|
||||
setCurrentProject,
|
||||
trashedProjects,
|
||||
upsertAndSetCurrentProject,
|
||||
specCreatingForProject,
|
||||
setSpecCreatingForProject,
|
||||
} = useAppStore();
|
||||
const [contextMenuProject, setContextMenuProject] = useState<Project | null>(null);
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(
|
||||
@@ -41,6 +46,17 @@ export function ProjectSwitcher() {
|
||||
);
|
||||
const [editDialogProject, setEditDialogProject] = useState<Project | null>(null);
|
||||
|
||||
// Setup dialog state for opening existing projects
|
||||
const [showSetupDialog, setShowSetupDialog] = useState(false);
|
||||
const [setupProjectPath, setSetupProjectPath] = useState<string | null>(null);
|
||||
const [projectOverview, setProjectOverview] = useState('');
|
||||
const [generateFeatures, setGenerateFeatures] = useState(true);
|
||||
const [analyzeProject, setAnalyzeProject] = useState(true);
|
||||
const [featureCount, setFeatureCount] = useState(5);
|
||||
|
||||
// Derive isCreatingSpec from store state
|
||||
const isCreatingSpec = specCreatingForProject !== null;
|
||||
|
||||
// Version info
|
||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
|
||||
const { os } = useOSDetection();
|
||||
@@ -108,6 +124,109 @@ export function ProjectSwitcher() {
|
||||
api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Opens the system folder selection dialog and initializes the selected project.
|
||||
*/
|
||||
const handleOpenFolder = useCallback(async () => {
|
||||
const api = getElectronAPI();
|
||||
const result = await api.openDirectory();
|
||||
|
||||
if (!result.canceled && result.filePaths[0]) {
|
||||
const path = result.filePaths[0];
|
||||
// Extract folder name from path (works on both Windows and Mac/Linux)
|
||||
const name = path.split(/[/\\]/).filter(Boolean).pop() || 'Untitled Project';
|
||||
|
||||
try {
|
||||
// Check if this is a brand new project (no .automaker directory)
|
||||
const hadAutomakerDir = await hasAutomakerDir(path);
|
||||
|
||||
// Initialize the .automaker directory structure
|
||||
const initResult = await initializeProject(path);
|
||||
|
||||
if (!initResult.success) {
|
||||
toast.error('Failed to initialize project', {
|
||||
description: initResult.error || 'Unknown error occurred',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Upsert project and set as current (handles both create and update cases)
|
||||
// Theme preservation is handled by the store action
|
||||
const trashedProject = trashedProjects.find((p) => p.path === path);
|
||||
const effectiveTheme =
|
||||
(trashedProject?.theme as ThemeMode | undefined) ||
|
||||
(currentProject?.theme as ThemeMode | undefined) ||
|
||||
globalTheme;
|
||||
upsertAndSetCurrentProject(path, name, effectiveTheme);
|
||||
|
||||
// Check if app_spec.txt exists
|
||||
const specExists = await hasAppSpec(path);
|
||||
|
||||
if (!hadAutomakerDir && !specExists) {
|
||||
// This is a brand new project - show setup dialog
|
||||
setSetupProjectPath(path);
|
||||
setShowSetupDialog(true);
|
||||
toast.success('Project opened', {
|
||||
description: `Opened ${name}. Let's set up your app specification!`,
|
||||
});
|
||||
} else if (initResult.createdFiles && initResult.createdFiles.length > 0) {
|
||||
toast.success(initResult.isNewProject ? 'Project initialized' : 'Project updated', {
|
||||
description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`,
|
||||
});
|
||||
} else {
|
||||
toast.success('Project opened', {
|
||||
description: `Opened ${name}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Navigate to board view
|
||||
navigate({ to: '/board' });
|
||||
} catch (error) {
|
||||
console.error('Failed to open project:', error);
|
||||
toast.error('Failed to open project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme, navigate]);
|
||||
|
||||
// Handler for creating initial spec from the setup dialog
|
||||
const handleCreateInitialSpec = useCallback(async () => {
|
||||
if (!setupProjectPath) return;
|
||||
|
||||
setSpecCreatingForProject(setupProjectPath);
|
||||
setShowSetupDialog(false);
|
||||
|
||||
try {
|
||||
const api = getElectronAPI();
|
||||
await api.generateAppSpec({
|
||||
projectPath: setupProjectPath,
|
||||
projectOverview,
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
featureCount,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to generate spec:', error);
|
||||
toast.error('Failed to generate spec', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
setSpecCreatingForProject(null);
|
||||
}
|
||||
}, [
|
||||
setupProjectPath,
|
||||
projectOverview,
|
||||
generateFeatures,
|
||||
analyzeProject,
|
||||
featureCount,
|
||||
setSpecCreatingForProject,
|
||||
]);
|
||||
|
||||
const handleSkipSetup = useCallback(() => {
|
||||
setShowSetupDialog(false);
|
||||
setSetupProjectPath(null);
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcuts for project switching (1-9, 0)
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -204,7 +323,7 @@ export function ProjectSwitcher() {
|
||||
</div>
|
||||
|
||||
{/* Projects List */}
|
||||
<div className="flex-1 overflow-y-auto py-3 px-2 space-y-2">
|
||||
<div className="flex-1 overflow-y-auto pt-1 pb-3 px-2 space-y-2">
|
||||
{projects.map((project, index) => (
|
||||
<ProjectSwitcherItem
|
||||
key={project.id}
|
||||
@@ -219,7 +338,7 @@ export function ProjectSwitcher() {
|
||||
{/* Horizontal rule and Add Project Button - only show if there are projects */}
|
||||
{projects.length > 0 && (
|
||||
<>
|
||||
<div className="w-full h-px bg-border/40 my-2" />
|
||||
<div className="w-full h-px bg-border my-2" />
|
||||
<button
|
||||
onClick={handleNewProject}
|
||||
className={cn(
|
||||
@@ -234,25 +353,55 @@ export function ProjectSwitcher() {
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOpenFolder}
|
||||
className={cn(
|
||||
'w-full aspect-square rounded-xl flex items-center justify-center',
|
||||
'transition-all duration-200 ease-out',
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||
'hover:shadow-sm hover:scale-105 active:scale-95'
|
||||
)}
|
||||
title="Open Project"
|
||||
data-testid="open-project-button"
|
||||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Add Project Button - when no projects, show without rule */}
|
||||
{projects.length === 0 && (
|
||||
<button
|
||||
onClick={handleNewProject}
|
||||
className={cn(
|
||||
'w-full aspect-square rounded-xl flex items-center justify-center',
|
||||
'transition-all duration-200 ease-out',
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||
'hover:shadow-sm hover:scale-105 active:scale-95'
|
||||
)}
|
||||
title="New Project"
|
||||
data-testid="new-project-button"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={handleNewProject}
|
||||
className={cn(
|
||||
'w-full aspect-square rounded-xl flex items-center justify-center',
|
||||
'transition-all duration-200 ease-out',
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||
'hover:shadow-sm hover:scale-105 active:scale-95'
|
||||
)}
|
||||
title="New Project"
|
||||
data-testid="new-project-button"
|
||||
>
|
||||
<Plus className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOpenFolder}
|
||||
className={cn(
|
||||
'w-full aspect-square rounded-xl flex items-center justify-center',
|
||||
'transition-all duration-200 ease-out',
|
||||
'text-muted-foreground hover:text-foreground',
|
||||
'hover:bg-accent/50 border border-transparent hover:border-border/40',
|
||||
'hover:shadow-sm hover:scale-105 active:scale-95'
|
||||
)}
|
||||
title="Open Project"
|
||||
data-testid="open-project-button"
|
||||
>
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -312,6 +461,26 @@ export function ProjectSwitcher() {
|
||||
onSkip={handleOnboardingSkip}
|
||||
onGenerateSpec={handleOnboardingSkip}
|
||||
/>
|
||||
|
||||
{/* Setup Dialog for Open Project */}
|
||||
<CreateSpecDialog
|
||||
open={showSetupDialog}
|
||||
onOpenChange={setShowSetupDialog}
|
||||
projectOverview={projectOverview}
|
||||
onProjectOverviewChange={setProjectOverview}
|
||||
generateFeatures={generateFeatures}
|
||||
onGenerateFeaturesChange={setGenerateFeatures}
|
||||
analyzeProject={analyzeProject}
|
||||
onAnalyzeProjectChange={setAnalyzeProject}
|
||||
featureCount={featureCount}
|
||||
onFeatureCountChange={setFeatureCount}
|
||||
onCreateSpec={handleCreateInitialSpec}
|
||||
onSkip={handleSkipSetup}
|
||||
isCreatingSpec={isCreatingSpec}
|
||||
showSkipButton={true}
|
||||
title="Set Up Your Project"
|
||||
description="We didn't find an app_spec.txt file. Let us help you generate your app_spec.txt to help describe your project for our system. We'll analyze your project's tech stack and create a comprehensive specification."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Folder, LucideIcon } from 'lucide-react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { cn, isMac } from '@/lib/utils';
|
||||
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
|
||||
import type { Project } from '@/lib/electron';
|
||||
import { isElectron, type Project } from '@/lib/electron';
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
sidebarOpen: boolean;
|
||||
@@ -25,14 +25,17 @@ export function SidebarHeader({ sidebarOpen, currentProject }: SidebarHeaderProp
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 flex flex-col',
|
||||
// Add minimal padding on macOS for traffic light buttons
|
||||
isMac && 'pt-2'
|
||||
// Add padding on macOS Electron for traffic light buttons
|
||||
isMac && isElectron() && 'pt-[10px]'
|
||||
)}
|
||||
>
|
||||
{/* Project name and icon display */}
|
||||
{currentProject && (
|
||||
<div
|
||||
className={cn('flex items-center gap-3 px-4 py-3', !sidebarOpen && 'justify-center px-2')}
|
||||
className={cn(
|
||||
'flex items-center gap-3 px-4 pt-3 pb-1',
|
||||
!sidebarOpen && 'justify-center px-2'
|
||||
)}
|
||||
>
|
||||
{/* Project Icon */}
|
||||
<div className="shrink-0">
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SidebarNavigation({
|
||||
navigate,
|
||||
}: SidebarNavigationProps) {
|
||||
return (
|
||||
<nav className={cn('flex-1 overflow-y-auto px-3 pb-2', sidebarOpen ? 'mt-5' : 'mt-1')}>
|
||||
<nav className={cn('flex-1 overflow-y-auto px-3 pb-2', sidebarOpen ? 'mt-1' : 'mt-1')}>
|
||||
{!currentProject && sidebarOpen ? (
|
||||
// Placeholder when no project is selected (only in expanded state)
|
||||
<div className="flex items-center justify-center h-full px-4">
|
||||
|
||||
@@ -1324,8 +1324,6 @@ export function BoardView() {
|
||||
isCreatingSpec={isCreatingSpec}
|
||||
creatingSpecProjectPath={creatingSpecProjectPath}
|
||||
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
||||
onShowCompletedModal={() => setShowCompletedModal(true)}
|
||||
completedCount={completedFeatures.length}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
/>
|
||||
@@ -1437,6 +1435,8 @@ export function BoardView() {
|
||||
runningAutoTasks={runningAutoTasks}
|
||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||
onAddFeature={() => setShowAddDialog(true)}
|
||||
onShowCompletedModal={() => setShowCompletedModal(true)}
|
||||
completedCount={completedFeatures.length}
|
||||
pipelineConfig={pipelineConfig}
|
||||
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ImageIcon, Archive } from 'lucide-react';
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BoardControlsProps {
|
||||
isMounted: boolean;
|
||||
onShowBoardBackground: () => void;
|
||||
onShowCompletedModal: () => void;
|
||||
completedCount: number;
|
||||
}
|
||||
|
||||
export function BoardControls({
|
||||
isMounted,
|
||||
onShowBoardBackground,
|
||||
onShowCompletedModal,
|
||||
completedCount,
|
||||
}: BoardControlsProps) {
|
||||
export function BoardControls({ isMounted, onShowBoardBackground }: BoardControlsProps) {
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
@@ -23,43 +16,23 @@ export function BoardControls({
|
||||
{/* Board Background Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
<button
|
||||
onClick={onShowBoardBackground}
|
||||
className="h-8 px-2"
|
||||
className={cn(
|
||||
'inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-all duration-200 cursor-pointer',
|
||||
'text-muted-foreground hover:text-foreground hover:bg-accent',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
'border border-border'
|
||||
)}
|
||||
data-testid="board-background-button"
|
||||
>
|
||||
<ImageIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Board Background Settings</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{/* Completed/Archived Features Button */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onShowCompletedModal}
|
||||
className="h-8 px-2 relative"
|
||||
data-testid="completed-features-button"
|
||||
>
|
||||
<Archive className="w-4 h-4" />
|
||||
{completedCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[10px] font-bold rounded-full w-4 h-4 flex items-center justify-center">
|
||||
{completedCount > 99 ? '99+' : completedCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Completed Features ({completedCount})</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { useCallback } from 'react';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Bot, Wand2, Settings2, GitBranch } from 'lucide-react';
|
||||
import { Wand2, GitBranch } from 'lucide-react';
|
||||
import { UsagePopover } from '@/components/usage-popover';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { useIsMobile } from '@/hooks/use-media-query';
|
||||
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
||||
import { WorktreeSettingsDialog } from './dialogs/worktree-settings-dialog';
|
||||
import { PlanSettingsDialog } from './dialogs/plan-settings-dialog';
|
||||
import { AutoModeSettingsPopover } from './dialogs/auto-mode-settings-popover';
|
||||
import { WorktreeSettingsPopover } from './dialogs/worktree-settings-popover';
|
||||
import { PlanSettingsPopover } from './dialogs/plan-settings-popover';
|
||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||
import { BoardSearchBar } from './board-search-bar';
|
||||
import { BoardControls } from './board-controls';
|
||||
@@ -36,8 +33,6 @@ interface BoardHeaderProps {
|
||||
creatingSpecProjectPath?: string;
|
||||
// Board controls props
|
||||
onShowBoardBackground: () => void;
|
||||
onShowCompletedModal: () => void;
|
||||
completedCount: number;
|
||||
// View toggle props
|
||||
viewMode: ViewMode;
|
||||
onViewModeChange: (mode: ViewMode) => void;
|
||||
@@ -61,14 +56,9 @@ export function BoardHeader({
|
||||
isCreatingSpec,
|
||||
creatingSpecProjectPath,
|
||||
onShowBoardBackground,
|
||||
onShowCompletedModal,
|
||||
completedCount,
|
||||
viewMode,
|
||||
onViewModeChange,
|
||||
}: BoardHeaderProps) {
|
||||
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
|
||||
const [showWorktreeSettings, setShowWorktreeSettings] = useState(false);
|
||||
const [showPlanSettings, setShowPlanSettings] = useState(false);
|
||||
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
|
||||
const skipVerificationInAutoMode = useAppStore((state) => state.skipVerificationInAutoMode);
|
||||
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
|
||||
@@ -127,12 +117,7 @@ export function BoardHeader({
|
||||
currentProjectPath={projectPath}
|
||||
/>
|
||||
{isMounted && <ViewToggle viewMode={viewMode} onViewModeChange={onViewModeChange} />}
|
||||
<BoardControls
|
||||
isMounted={isMounted}
|
||||
onShowBoardBackground={onShowBoardBackground}
|
||||
onShowCompletedModal={onShowCompletedModal}
|
||||
completedCount={completedCount}
|
||||
/>
|
||||
<BoardControls isMounted={isMounted} onShowBoardBackground={onShowBoardBackground} />
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
|
||||
@@ -148,7 +133,7 @@ export function BoardHeader({
|
||||
onConcurrencyChange={onConcurrencyChange}
|
||||
isAutoModeRunning={isAutoModeRunning}
|
||||
onAutoModeToggle={onAutoModeToggle}
|
||||
onOpenAutoModeSettings={() => setShowAutoModeSettings(true)}
|
||||
onOpenAutoModeSettings={() => {}}
|
||||
onOpenPlanDialog={onOpenPlanDialog}
|
||||
showClaudeUsage={showClaudeUsage}
|
||||
showCodexUsage={showCodexUsage}
|
||||
@@ -160,7 +145,10 @@ export function BoardHeader({
|
||||
{isMounted && !isMobile && (
|
||||
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
||||
<Label htmlFor="worktrees-toggle" className="text-sm font-medium cursor-pointer">
|
||||
<Label
|
||||
htmlFor="worktrees-toggle"
|
||||
className="text-xs font-medium cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
Worktree Bar
|
||||
</Label>
|
||||
<Switch
|
||||
@@ -169,72 +157,20 @@ export function BoardHeader({
|
||||
onCheckedChange={handleWorktreePanelToggle}
|
||||
data-testid="worktrees-toggle"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowWorktreeSettings(true)}
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Worktree Settings"
|
||||
data-testid="worktree-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
<WorktreeSettingsPopover
|
||||
addFeatureUseSelectedWorktreeBranch={addFeatureUseSelectedWorktreeBranch}
|
||||
onAddFeatureUseSelectedWorktreeBranchChange={setAddFeatureUseSelectedWorktreeBranch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Worktree Settings Dialog */}
|
||||
<WorktreeSettingsDialog
|
||||
open={showWorktreeSettings}
|
||||
onOpenChange={setShowWorktreeSettings}
|
||||
addFeatureUseSelectedWorktreeBranch={addFeatureUseSelectedWorktreeBranch}
|
||||
onAddFeatureUseSelectedWorktreeBranchChange={setAddFeatureUseSelectedWorktreeBranch}
|
||||
/>
|
||||
|
||||
{/* Concurrency Control - only show after mount to prevent hydration issues */}
|
||||
{isMounted && !isMobile && (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className={`${controlContainerClass} cursor-pointer hover:bg-accent/50 transition-colors`}
|
||||
data-testid="concurrency-slider-container"
|
||||
>
|
||||
<Bot className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Agents</span>
|
||||
<span className="text-sm text-muted-foreground" data-testid="concurrency-value">
|
||||
{runningAgentsCount}/{maxConcurrency}
|
||||
</span>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-64" align="end">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Max Concurrent Agents</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Controls how many AI agents can run simultaneously. Higher values process more
|
||||
features in parallel but use more API resources.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[maxConcurrency]}
|
||||
onValueChange={(value) => onConcurrencyChange(value[0])}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
className="flex-1"
|
||||
data-testid="concurrency-slider"
|
||||
/>
|
||||
<span className="text-sm font-medium min-w-[2ch] text-right">
|
||||
{maxConcurrency}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||
{isMounted && !isMobile && (
|
||||
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
||||
<Label htmlFor="auto-mode-toggle" className="text-sm font-medium cursor-pointer">
|
||||
<Label
|
||||
htmlFor="auto-mode-toggle"
|
||||
className="text-xs font-medium cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
Auto Mode
|
||||
</Label>
|
||||
<Switch
|
||||
@@ -243,25 +179,16 @@ export function BoardHeader({
|
||||
onCheckedChange={onAutoModeToggle}
|
||||
data-testid="auto-mode-toggle"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowAutoModeSettings(true)}
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Auto Mode Settings"
|
||||
data-testid="auto-mode-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
<AutoModeSettingsPopover
|
||||
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
||||
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
||||
maxConcurrency={maxConcurrency}
|
||||
runningAgentsCount={runningAgentsCount}
|
||||
onConcurrencyChange={onConcurrencyChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Auto Mode Settings Dialog */}
|
||||
<AutoModeSettingsDialog
|
||||
open={showAutoModeSettings}
|
||||
onOpenChange={setShowAutoModeSettings}
|
||||
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
||||
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
||||
/>
|
||||
|
||||
{/* Plan Button with Settings - only show on desktop, mobile has it in the menu */}
|
||||
{isMounted && !isMobile && (
|
||||
<div className={controlContainerClass} data-testid="plan-button-container">
|
||||
@@ -273,24 +200,12 @@ export function BoardHeader({
|
||||
<Wand2 className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Plan</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowPlanSettings(true)}
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Plan Settings"
|
||||
data-testid="plan-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
<PlanSettingsPopover
|
||||
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
|
||||
onPlanUseSelectedWorktreeBranchChange={setPlanUseSelectedWorktreeBranch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Plan Settings Dialog */}
|
||||
<PlanSettingsDialog
|
||||
open={showPlanSettings}
|
||||
onOpenChange={setShowPlanSettings}
|
||||
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
|
||||
onPlanUseSelectedWorktreeBranchChange={setPlanUseSelectedWorktreeBranch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,9 +30,7 @@ export function SelectionActionBar({
|
||||
}: SelectionActionBarProps) {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
if (selectedCount === 0) return null;
|
||||
|
||||
const allSelected = selectedCount === totalCount;
|
||||
const allSelected = selectedCount === totalCount && totalCount > 0;
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
setShowDeleteDialog(true);
|
||||
@@ -55,7 +53,9 @@ export function SelectionActionBar({
|
||||
data-testid="selection-action-bar"
|
||||
>
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{selectedCount} feature{selectedCount !== 1 ? 's' : ''} selected
|
||||
{selectedCount === 0
|
||||
? 'Select features to edit'
|
||||
: `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
|
||||
</span>
|
||||
|
||||
<div className="h-4 w-px bg-border" />
|
||||
@@ -65,7 +65,8 @@ export function SelectionActionBar({
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={onEdit}
|
||||
className="h-8 bg-brand-500 hover:bg-brand-600"
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
|
||||
data-testid="selection-edit-button"
|
||||
>
|
||||
<Pencil className="w-4 h-4 mr-1.5" />
|
||||
@@ -76,7 +77,8 @@ export function SelectionActionBar({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleDeleteClick}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
disabled={selectedCount === 0}
|
||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||
data-testid="selection-delete-button"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1.5" />
|
||||
|
||||
@@ -38,7 +38,7 @@ export function ViewToggle({ viewMode, onViewModeChange, className }: ViewToggle
|
||||
data-testid="view-toggle-kanban"
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" />
|
||||
<span className="sr-only sm:not-sr-only">Kanban</span>
|
||||
<span className="sr-only">Kanban</span>
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
@@ -55,7 +55,7 @@ export function ViewToggle({ viewMode, onViewModeChange, className }: ViewToggle
|
||||
data-testid="view-toggle-list"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
<span className="sr-only sm:not-sr-only">List</span>
|
||||
<span className="sr-only">List</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { FastForward, Settings2 } from 'lucide-react';
|
||||
|
||||
interface AutoModeSettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
skipVerificationInAutoMode: boolean;
|
||||
onSkipVerificationChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function AutoModeSettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
skipVerificationInAutoMode,
|
||||
onSkipVerificationChange,
|
||||
}: AutoModeSettingsDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md" data-testid="auto-mode-settings-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings2 className="w-5 h-5" />
|
||||
Auto Mode Settings
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure how auto mode handles feature execution and dependencies.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Skip Verification Setting */}
|
||||
<div className="flex items-start space-x-3 p-3 rounded-lg bg-secondary/50">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
htmlFor="skip-verification-toggle"
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<FastForward className="w-4 h-4 text-brand-500" />
|
||||
Skip verification requirement
|
||||
</Label>
|
||||
<Switch
|
||||
id="skip-verification-toggle"
|
||||
checked={skipVerificationInAutoMode}
|
||||
onCheckedChange={onSkipVerificationChange}
|
||||
data-testid="skip-verification-toggle"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
When enabled, auto mode will grab features even if their dependencies are not
|
||||
verified, as long as they are not currently running. This allows faster pipeline
|
||||
execution without waiting for manual verification.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { FastForward, Bot, Settings2 } from 'lucide-react';
|
||||
|
||||
interface AutoModeSettingsPopoverProps {
|
||||
skipVerificationInAutoMode: boolean;
|
||||
onSkipVerificationChange: (value: boolean) => void;
|
||||
maxConcurrency: number;
|
||||
runningAgentsCount: number;
|
||||
onConcurrencyChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export function AutoModeSettingsPopover({
|
||||
skipVerificationInAutoMode,
|
||||
onSkipVerificationChange,
|
||||
maxConcurrency,
|
||||
runningAgentsCount,
|
||||
onConcurrencyChange,
|
||||
}: AutoModeSettingsPopoverProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Auto Mode Settings"
|
||||
data-testid="auto-mode-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72" align="end" sideOffset={8}>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Auto Mode Settings</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure auto mode execution and agent concurrency.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Max Concurrent Agents */}
|
||||
<div className="space-y-2 p-2 rounded-md bg-secondary/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="w-4 h-4 text-brand-500 shrink-0" />
|
||||
<Label className="text-xs font-medium">Max Concurrent Agents</Label>
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{runningAgentsCount}/{maxConcurrency}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Slider
|
||||
value={[maxConcurrency]}
|
||||
onValueChange={(value) => onConcurrencyChange(value[0])}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
className="flex-1"
|
||||
data-testid="concurrency-slider"
|
||||
/>
|
||||
<span className="text-xs font-medium min-w-[2ch] text-right">{maxConcurrency}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Higher values process more features in parallel but use more API resources.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Skip Verification Setting */}
|
||||
<div className="flex items-center justify-between gap-3 p-2 rounded-md bg-secondary/50">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<FastForward className="w-4 h-4 text-brand-500 shrink-0" />
|
||||
<Label
|
||||
htmlFor="skip-verification-toggle"
|
||||
className="text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Skip verification requirement
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="skip-verification-toggle"
|
||||
checked={skipVerificationInAutoMode}
|
||||
onCheckedChange={onSkipVerificationChange}
|
||||
data-testid="skip-verification-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
When enabled, auto mode will grab features even if their dependencies are not verified,
|
||||
as long as they are not currently running.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { GitBranch, Settings2 } from 'lucide-react';
|
||||
|
||||
interface PlanSettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
planUseSelectedWorktreeBranch: boolean;
|
||||
onPlanUseSelectedWorktreeBranchChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function PlanSettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
planUseSelectedWorktreeBranch,
|
||||
onPlanUseSelectedWorktreeBranchChange,
|
||||
}: PlanSettingsDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md" data-testid="plan-settings-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings2 className="w-5 h-5" />
|
||||
Plan Settings
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure how the Plan feature creates and organizes new features.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Use Selected Worktree Branch Setting */}
|
||||
<div className="flex items-start space-x-3 p-3 rounded-lg bg-secondary/50">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
htmlFor="plan-worktree-branch-toggle"
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="plan-worktree-branch-toggle"
|
||||
checked={planUseSelectedWorktreeBranch}
|
||||
onCheckedChange={onPlanUseSelectedWorktreeBranchChange}
|
||||
data-testid="plan-worktree-branch-toggle"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Planned features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { GitBranch, Settings2 } from 'lucide-react';
|
||||
|
||||
interface PlanSettingsPopoverProps {
|
||||
planUseSelectedWorktreeBranch: boolean;
|
||||
onPlanUseSelectedWorktreeBranchChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function PlanSettingsPopover({
|
||||
planUseSelectedWorktreeBranch,
|
||||
onPlanUseSelectedWorktreeBranchChange,
|
||||
}: PlanSettingsPopoverProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Plan Settings"
|
||||
data-testid="plan-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72" align="end" sideOffset={8}>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Plan Settings</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure how Plan creates and organizes features.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 p-2 rounded-md bg-secondary/50">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<GitBranch className="w-4 h-4 text-brand-500 shrink-0" />
|
||||
<Label
|
||||
htmlFor="plan-worktree-branch-toggle"
|
||||
className="text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="plan-worktree-branch-toggle"
|
||||
checked={planUseSelectedWorktreeBranch}
|
||||
onCheckedChange={onPlanUseSelectedWorktreeBranchChange}
|
||||
data-testid="plan-worktree-branch-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
Planned features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { GitBranch, Settings2 } from 'lucide-react';
|
||||
|
||||
interface WorktreeSettingsDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
addFeatureUseSelectedWorktreeBranch: boolean;
|
||||
onAddFeatureUseSelectedWorktreeBranchChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function WorktreeSettingsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
addFeatureUseSelectedWorktreeBranch,
|
||||
onAddFeatureUseSelectedWorktreeBranchChange,
|
||||
}: WorktreeSettingsDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md" data-testid="worktree-settings-dialog">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Settings2 className="w-5 h-5" />
|
||||
Worktree Settings
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure how worktrees affect feature creation and organization.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Use Selected Worktree Branch Setting */}
|
||||
<div className="flex items-start space-x-3 p-3 rounded-lg bg-secondary/50">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
htmlFor="worktree-branch-toggle"
|
||||
className="text-sm font-medium cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<GitBranch className="w-4 h-4 text-brand-500" />
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
<Switch
|
||||
id="worktree-branch-toggle"
|
||||
checked={addFeatureUseSelectedWorktreeBranch}
|
||||
onCheckedChange={onAddFeatureUseSelectedWorktreeBranchChange}
|
||||
data-testid="worktree-branch-toggle"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
New features will automatically use isolated worktrees, keeping changes separate
|
||||
from your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { GitBranch, Settings2 } from 'lucide-react';
|
||||
|
||||
interface WorktreeSettingsPopoverProps {
|
||||
addFeatureUseSelectedWorktreeBranch: boolean;
|
||||
onAddFeatureUseSelectedWorktreeBranchChange: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function WorktreeSettingsPopover({
|
||||
addFeatureUseSelectedWorktreeBranch,
|
||||
onAddFeatureUseSelectedWorktreeBranchChange,
|
||||
}: WorktreeSettingsPopoverProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="p-1 rounded hover:bg-accent/50 transition-colors"
|
||||
title="Worktree Settings"
|
||||
data-testid="worktree-settings-button"
|
||||
>
|
||||
<Settings2 className="w-4 h-4 text-muted-foreground" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-72" align="end" sideOffset={8}>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-sm mb-1">Worktree Settings</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure how worktrees affect feature creation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-3 p-2 rounded-md bg-secondary/50">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<GitBranch className="w-4 h-4 text-brand-500 shrink-0" />
|
||||
<Label
|
||||
htmlFor="worktree-branch-toggle"
|
||||
className="text-xs font-medium cursor-pointer"
|
||||
>
|
||||
Default to worktree mode
|
||||
</Label>
|
||||
</div>
|
||||
<Switch
|
||||
id="worktree-branch-toggle"
|
||||
checked={addFeatureUseSelectedWorktreeBranch}
|
||||
onCheckedChange={onAddFeatureUseSelectedWorktreeBranchChange}
|
||||
data-testid="worktree-branch-toggle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-[10px] text-muted-foreground leading-relaxed">
|
||||
New features will automatically use isolated worktrees, keeping changes separate from
|
||||
your main branch until you're ready to merge.
|
||||
</p>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -44,6 +44,8 @@ interface KanbanBoardProps {
|
||||
runningAutoTasks: string[];
|
||||
onArchiveAllVerified: () => void;
|
||||
onAddFeature: () => void;
|
||||
onShowCompletedModal: () => void;
|
||||
completedCount: number;
|
||||
pipelineConfig: PipelineConfig | null;
|
||||
onOpenPipelineSettings?: () => void;
|
||||
// Selection mode props
|
||||
@@ -88,6 +90,8 @@ export function KanbanBoard({
|
||||
runningAutoTasks,
|
||||
onArchiveAllVerified,
|
||||
onAddFeature,
|
||||
onShowCompletedModal,
|
||||
completedCount,
|
||||
pipelineConfig,
|
||||
onOpenPipelineSettings,
|
||||
isSelectionMode = false,
|
||||
@@ -140,17 +144,36 @@ export function KanbanBoard({
|
||||
showBorder={backgroundSettings.columnBorderEnabled}
|
||||
hideScrollbar={backgroundSettings.hideScrollbar}
|
||||
headerAction={
|
||||
column.id === 'verified' && columnFeatures.length > 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={onArchiveAllVerified}
|
||||
data-testid="archive-all-verified-button"
|
||||
>
|
||||
<Archive className="w-3 h-3 mr-1" />
|
||||
Complete All
|
||||
</Button>
|
||||
column.id === 'verified' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
{columnFeatures.length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs"
|
||||
onClick={onArchiveAllVerified}
|
||||
data-testid="archive-all-verified-button"
|
||||
>
|
||||
<Archive className="w-3 h-3 mr-1" />
|
||||
Complete All
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 relative"
|
||||
onClick={onShowCompletedModal}
|
||||
title={`Completed Features (${completedCount})`}
|
||||
data-testid="completed-features-button"
|
||||
>
|
||||
<Archive className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
{completedCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-brand-500 text-white text-[8px] font-bold rounded-full w-3.5 h-3.5 flex items-center justify-center">
|
||||
{completedCount > 99 ? '99+' : completedCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : column.id === 'backlog' ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
|
||||
@@ -267,6 +267,7 @@ export function GraphViewPage() {
|
||||
setShowAddDialog(true);
|
||||
}}
|
||||
onDeleteTask={(feature) => handleDeleteFeature(feature.id)}
|
||||
onAddFeature={() => setShowAddDialog(true)}
|
||||
/>
|
||||
|
||||
{/* Edit Feature Dialog */}
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from './hooks';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { SearchX } from 'lucide-react';
|
||||
import { SearchX, Plus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
// Define custom node and edge types - using any to avoid React Flow's strict typing
|
||||
@@ -64,6 +64,7 @@ interface GraphCanvasProps {
|
||||
onNodeDoubleClick?: (featureId: string) => void;
|
||||
nodeActionCallbacks?: NodeActionCallbacks;
|
||||
onCreateDependency?: (sourceId: string, targetId: string) => Promise<boolean>;
|
||||
onAddFeature?: () => void;
|
||||
backgroundStyle?: React.CSSProperties;
|
||||
backgroundSettings?: BackgroundSettings;
|
||||
className?: string;
|
||||
@@ -77,6 +78,7 @@ function GraphCanvasInner({
|
||||
onNodeDoubleClick,
|
||||
nodeActionCallbacks,
|
||||
onCreateDependency,
|
||||
onAddFeature,
|
||||
backgroundStyle,
|
||||
backgroundSettings,
|
||||
className,
|
||||
@@ -398,6 +400,14 @@ function GraphCanvasInner({
|
||||
|
||||
<GraphLegend />
|
||||
|
||||
{/* Add Feature Button */}
|
||||
<Panel position="top-right">
|
||||
<Button variant="default" size="sm" onClick={onAddFeature} className="gap-1.5">
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Feature
|
||||
</Button>
|
||||
</Panel>
|
||||
|
||||
{/* Empty state when all nodes are filtered out */}
|
||||
{filterResult.hasActiveFilter && filterResult.matchedNodeIds.size === 0 && (
|
||||
<Panel position="top-center" className="mt-20">
|
||||
|
||||
@@ -22,6 +22,7 @@ interface GraphViewProps {
|
||||
onUpdateFeature?: (featureId: string, updates: Partial<Feature>) => void;
|
||||
onSpawnTask?: (feature: Feature) => void;
|
||||
onDeleteTask?: (feature: Feature) => void;
|
||||
onAddFeature?: () => void;
|
||||
}
|
||||
|
||||
export function GraphView({
|
||||
@@ -40,6 +41,7 @@ export function GraphView({
|
||||
onUpdateFeature,
|
||||
onSpawnTask,
|
||||
onDeleteTask,
|
||||
onAddFeature,
|
||||
}: GraphViewProps) {
|
||||
const { currentProject } = useAppStore();
|
||||
|
||||
@@ -212,6 +214,7 @@ export function GraphView({
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
nodeActionCallbacks={nodeActionCallbacks}
|
||||
onCreateDependency={handleCreateDependency}
|
||||
onAddFeature={onAddFeature}
|
||||
backgroundStyle={backgroundImageStyle}
|
||||
backgroundSettings={backgroundSettings}
|
||||
className="h-full"
|
||||
|
||||
Reference in New Issue
Block a user