mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +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}`);
|
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 commitHash: string | null = null;
|
||||||
let commitError: string | null = null;
|
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
const message = commitMessage || `Changes from ${branchName}`;
|
const message = commitMessage || `Changes from ${branchName}`;
|
||||||
logger.debug(`Committing changes with message: ${message}`);
|
logger.debug(`Committing changes with message: ${message}`);
|
||||||
@@ -98,14 +97,13 @@ export function createCreatePRHandler() {
|
|||||||
logger.info(`Commit successful: ${commitHash}`);
|
logger.info(`Commit successful: ${commitHash}`);
|
||||||
} catch (commitErr: unknown) {
|
} catch (commitErr: unknown) {
|
||||||
const err = commitErr as { stderr?: string; message?: string };
|
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}`);
|
logger.error(`Commit failed: ${commitError}`);
|
||||||
|
|
||||||
// Return error immediately - don't proceed with push/PR if commit fails
|
// Return error immediately - don't proceed with push/PR if commit fails
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to commit changes: ${commitError}`,
|
error: `Failed to commit changes: ${commitError}`,
|
||||||
commitError,
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -381,9 +379,8 @@ export function createCreatePRHandler() {
|
|||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
branch: branchName,
|
branch: branchName,
|
||||||
committed: hasChanges && !commitError,
|
committed: hasChanges,
|
||||||
commitHash,
|
commitHash,
|
||||||
commitError: commitError || undefined,
|
|
||||||
pushed: true,
|
pushed: true,
|
||||||
prUrl,
|
prUrl,
|
||||||
prNumber,
|
prNumber,
|
||||||
|
|||||||
@@ -10,43 +10,434 @@ interface IconPickerProps {
|
|||||||
onSelectIcon: (icon: string | null) => void;
|
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 = [
|
const POPULAR_ICONS = [
|
||||||
|
// Folders & Files
|
||||||
'Folder',
|
'Folder',
|
||||||
'FolderOpen',
|
'FolderOpen',
|
||||||
'FolderCode',
|
'FolderCode',
|
||||||
'FolderGit',
|
'FolderGit',
|
||||||
'FolderKanban',
|
'FolderKanban',
|
||||||
'Package',
|
'FolderTree',
|
||||||
'Box',
|
'FolderInput',
|
||||||
'Boxes',
|
'FolderOutput',
|
||||||
|
'FolderPlus',
|
||||||
|
'File',
|
||||||
|
'FileCode',
|
||||||
|
'FileText',
|
||||||
|
'FileJson',
|
||||||
|
'FileImage',
|
||||||
|
'FileVideo',
|
||||||
|
'FileAudio',
|
||||||
|
'FileSpreadsheet',
|
||||||
|
'Files',
|
||||||
|
'Archive',
|
||||||
|
|
||||||
|
// Code & Development
|
||||||
'Code',
|
'Code',
|
||||||
'Code2',
|
'Code2',
|
||||||
'Braces',
|
'Braces',
|
||||||
'FileCode',
|
'Brackets',
|
||||||
'Terminal',
|
'Terminal',
|
||||||
'Globe',
|
'TerminalSquare',
|
||||||
'Server',
|
'Command',
|
||||||
'Database',
|
'GitBranch',
|
||||||
|
'GitCommit',
|
||||||
|
'GitMerge',
|
||||||
|
'GitPullRequest',
|
||||||
|
'GitCompare',
|
||||||
|
'GitFork',
|
||||||
|
'GitHub',
|
||||||
|
'Gitlab',
|
||||||
|
'Bitbucket',
|
||||||
|
'Vscode',
|
||||||
|
|
||||||
|
// Packages & Containers
|
||||||
|
'Package',
|
||||||
|
'PackageSearch',
|
||||||
|
'PackageCheck',
|
||||||
|
'PackageX',
|
||||||
|
'Box',
|
||||||
|
'Boxes',
|
||||||
|
'Container',
|
||||||
|
|
||||||
|
// UI & Design
|
||||||
'Layout',
|
'Layout',
|
||||||
|
'LayoutGrid',
|
||||||
|
'LayoutList',
|
||||||
|
'LayoutDashboard',
|
||||||
|
'LayoutTemplate',
|
||||||
'Layers',
|
'Layers',
|
||||||
|
'Layers2',
|
||||||
|
'Layers3',
|
||||||
'Blocks',
|
'Blocks',
|
||||||
'Component',
|
'Component',
|
||||||
'Puzzle',
|
'Palette',
|
||||||
|
'Paintbrush',
|
||||||
|
'Brush',
|
||||||
|
'PenTool',
|
||||||
|
'Ruler',
|
||||||
|
'Grid',
|
||||||
|
'Grid3x3',
|
||||||
|
'Square',
|
||||||
|
'RectangleHorizontal',
|
||||||
|
'RectangleVertical',
|
||||||
|
'Circle',
|
||||||
|
|
||||||
|
// Tools & Settings
|
||||||
'Cog',
|
'Cog',
|
||||||
|
'Settings',
|
||||||
|
'Settings2',
|
||||||
'Wrench',
|
'Wrench',
|
||||||
'Hammer',
|
'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',
|
'Zap',
|
||||||
'Rocket',
|
'Rocket',
|
||||||
'Sparkles',
|
'Flame',
|
||||||
'Star',
|
'Lightning',
|
||||||
'Heart',
|
'Bolt',
|
||||||
|
'Target',
|
||||||
|
'Flag',
|
||||||
|
'FlagTriangleRight',
|
||||||
|
'CheckCircle',
|
||||||
|
'CheckCircle2',
|
||||||
|
'XCircle',
|
||||||
|
'AlertCircle',
|
||||||
|
'Info',
|
||||||
|
'HelpCircle',
|
||||||
|
'Clock',
|
||||||
|
'Timer',
|
||||||
|
'Stopwatch',
|
||||||
|
'Calendar',
|
||||||
|
'CalendarDays',
|
||||||
|
'CalendarCheck',
|
||||||
|
'CalendarClock',
|
||||||
|
|
||||||
|
// Security & Access
|
||||||
'Shield',
|
'Shield',
|
||||||
|
'ShieldCheck',
|
||||||
|
'ShieldAlert',
|
||||||
|
'ShieldOff',
|
||||||
'Lock',
|
'Lock',
|
||||||
|
'Unlock',
|
||||||
'Key',
|
'Key',
|
||||||
'Cpu',
|
'KeyRound',
|
||||||
'CircuitBoard',
|
'Eye',
|
||||||
'Workflow',
|
'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) {
|
export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
|
||||||
@@ -94,7 +485,7 @@ export function IconPicker({ selectedIcon, onSelectIcon }: IconPickerProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Icons Grid */}
|
{/* 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">
|
<div className="grid grid-cols-6 gap-1 p-2">
|
||||||
{filteredIcons.map((iconName) => {
|
{filteredIcons.map((iconName) => {
|
||||||
const IconComponent = getIconComponent(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 { Edit2, Trash2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import type { Project } from '@/lib/electron';
|
import type { Project } from '@/lib/electron';
|
||||||
|
|
||||||
interface ProjectContextMenuProps {
|
interface ProjectContextMenuProps {
|
||||||
@@ -19,6 +20,7 @@ export function ProjectContextMenu({
|
|||||||
}: ProjectContextMenuProps) {
|
}: ProjectContextMenuProps) {
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const { moveProjectToTrash } = useAppStore();
|
const { moveProjectToTrash } = useAppStore();
|
||||||
|
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
@@ -47,13 +49,16 @@ export function ProjectContextMenu({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
if (confirm(`Remove "${project.name}" from the project list?`)) {
|
setShowRemoveDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmRemove = () => {
|
||||||
moveProjectToTrash(project.id);
|
moveProjectToTrash(project.id);
|
||||||
}
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -99,5 +104,18 @@ export function ProjectContextMenu({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 { 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 { useNavigate } from '@tanstack/react-router';
|
||||||
import { cn } from '@/lib/utils';
|
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 { useOSDetection } from '@/hooks/use-os-detection';
|
||||||
import { ProjectSwitcherItem } from './components/project-switcher-item';
|
import { ProjectSwitcherItem } from './components/project-switcher-item';
|
||||||
import { ProjectContextMenu } from './components/project-context-menu';
|
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 { useProjectCreation, useProjectTheme } from '@/components/layout/sidebar/hooks';
|
||||||
import type { Project } from '@/lib/electron';
|
import type { Project } from '@/lib/electron';
|
||||||
import { getElectronAPI } 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 {
|
function getOSAbbreviation(os: string): string {
|
||||||
switch (os) {
|
switch (os) {
|
||||||
@@ -34,6 +37,8 @@ export function ProjectSwitcher() {
|
|||||||
setCurrentProject,
|
setCurrentProject,
|
||||||
trashedProjects,
|
trashedProjects,
|
||||||
upsertAndSetCurrentProject,
|
upsertAndSetCurrentProject,
|
||||||
|
specCreatingForProject,
|
||||||
|
setSpecCreatingForProject,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const [contextMenuProject, setContextMenuProject] = useState<Project | null>(null);
|
const [contextMenuProject, setContextMenuProject] = useState<Project | null>(null);
|
||||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(
|
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(
|
||||||
@@ -41,6 +46,17 @@ export function ProjectSwitcher() {
|
|||||||
);
|
);
|
||||||
const [editDialogProject, setEditDialogProject] = useState<Project | null>(null);
|
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
|
// Version info
|
||||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
|
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
|
||||||
const { os } = useOSDetection();
|
const { os } = useOSDetection();
|
||||||
@@ -108,6 +124,109 @@ export function ProjectSwitcher() {
|
|||||||
api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues');
|
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)
|
// Keyboard shortcuts for project switching (1-9, 0)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
@@ -204,7 +323,7 @@ export function ProjectSwitcher() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Projects List */}
|
{/* 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) => (
|
{projects.map((project, index) => (
|
||||||
<ProjectSwitcherItem
|
<ProjectSwitcherItem
|
||||||
key={project.id}
|
key={project.id}
|
||||||
@@ -219,7 +338,7 @@ export function ProjectSwitcher() {
|
|||||||
{/* Horizontal rule and Add Project Button - only show if there are projects */}
|
{/* Horizontal rule and Add Project Button - only show if there are projects */}
|
||||||
{projects.length > 0 && (
|
{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
|
<button
|
||||||
onClick={handleNewProject}
|
onClick={handleNewProject}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -234,11 +353,26 @@ export function ProjectSwitcher() {
|
|||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</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 */}
|
{/* Add Project Button - when no projects, show without rule */}
|
||||||
{projects.length === 0 && (
|
{projects.length === 0 && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={handleNewProject}
|
onClick={handleNewProject}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -253,6 +387,21 @@ export function ProjectSwitcher() {
|
|||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
@@ -312,6 +461,26 @@ export function ProjectSwitcher() {
|
|||||||
onSkip={handleOnboardingSkip}
|
onSkip={handleOnboardingSkip}
|
||||||
onGenerateSpec={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 * as LucideIcons from 'lucide-react';
|
||||||
import { cn, isMac } from '@/lib/utils';
|
import { cn, isMac } from '@/lib/utils';
|
||||||
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
|
import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
|
||||||
import type { Project } from '@/lib/electron';
|
import { isElectron, type Project } from '@/lib/electron';
|
||||||
|
|
||||||
interface SidebarHeaderProps {
|
interface SidebarHeaderProps {
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
@@ -25,14 +25,17 @@ export function SidebarHeader({ sidebarOpen, currentProject }: SidebarHeaderProp
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'shrink-0 flex flex-col',
|
'shrink-0 flex flex-col',
|
||||||
// Add minimal padding on macOS for traffic light buttons
|
// Add padding on macOS Electron for traffic light buttons
|
||||||
isMac && 'pt-2'
|
isMac && isElectron() && 'pt-[10px]'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Project name and icon display */}
|
{/* Project name and icon display */}
|
||||||
{currentProject && (
|
{currentProject && (
|
||||||
<div
|
<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 */}
|
{/* Project Icon */}
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function SidebarNavigation({
|
|||||||
navigate,
|
navigate,
|
||||||
}: SidebarNavigationProps) {
|
}: SidebarNavigationProps) {
|
||||||
return (
|
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 ? (
|
{!currentProject && sidebarOpen ? (
|
||||||
// Placeholder when no project is selected (only in expanded state)
|
// Placeholder when no project is selected (only in expanded state)
|
||||||
<div className="flex items-center justify-center h-full px-4">
|
<div className="flex items-center justify-center h-full px-4">
|
||||||
|
|||||||
@@ -1324,8 +1324,6 @@ export function BoardView() {
|
|||||||
isCreatingSpec={isCreatingSpec}
|
isCreatingSpec={isCreatingSpec}
|
||||||
creatingSpecProjectPath={creatingSpecProjectPath}
|
creatingSpecProjectPath={creatingSpecProjectPath}
|
||||||
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
onShowBoardBackground={() => setShowBoardBackgroundModal(true)}
|
||||||
onShowCompletedModal={() => setShowCompletedModal(true)}
|
|
||||||
completedCount={completedFeatures.length}
|
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onViewModeChange={setViewMode}
|
onViewModeChange={setViewMode}
|
||||||
/>
|
/>
|
||||||
@@ -1437,6 +1435,8 @@ export function BoardView() {
|
|||||||
runningAutoTasks={runningAutoTasks}
|
runningAutoTasks={runningAutoTasks}
|
||||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||||
onAddFeature={() => setShowAddDialog(true)}
|
onAddFeature={() => setShowAddDialog(true)}
|
||||||
|
onShowCompletedModal={() => setShowCompletedModal(true)}
|
||||||
|
completedCount={completedFeatures.length}
|
||||||
pipelineConfig={pipelineConfig}
|
pipelineConfig={pipelineConfig}
|
||||||
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
||||||
isSelectionMode={isSelectionMode}
|
isSelectionMode={isSelectionMode}
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
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 {
|
interface BoardControlsProps {
|
||||||
isMounted: boolean;
|
isMounted: boolean;
|
||||||
onShowBoardBackground: () => void;
|
onShowBoardBackground: () => void;
|
||||||
onShowCompletedModal: () => void;
|
|
||||||
completedCount: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BoardControls({
|
export function BoardControls({ isMounted, onShowBoardBackground }: BoardControlsProps) {
|
||||||
isMounted,
|
|
||||||
onShowBoardBackground,
|
|
||||||
onShowCompletedModal,
|
|
||||||
completedCount,
|
|
||||||
}: BoardControlsProps) {
|
|
||||||
if (!isMounted) return null;
|
if (!isMounted) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -23,43 +16,23 @@ export function BoardControls({
|
|||||||
{/* Board Background Button */}
|
{/* Board Background Button */}
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onShowBoardBackground}
|
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"
|
data-testid="board-background-button"
|
||||||
>
|
>
|
||||||
<ImageIcon className="w-4 h-4" />
|
<ImageIcon className="w-4 h-4" />
|
||||||
</Button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Board Background Settings</p>
|
<p>Board Background Settings</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</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>
|
</div>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { useState, useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Slider } from '@/components/ui/slider';
|
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Wand2, GitBranch } from 'lucide-react';
|
||||||
import { Bot, Wand2, Settings2, GitBranch } from 'lucide-react';
|
|
||||||
import { UsagePopover } from '@/components/usage-popover';
|
import { UsagePopover } from '@/components/usage-popover';
|
||||||
import { useAppStore } from '@/store/app-store';
|
import { useAppStore } from '@/store/app-store';
|
||||||
import { useSetupStore } from '@/store/setup-store';
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
import { useIsMobile } from '@/hooks/use-media-query';
|
import { useIsMobile } from '@/hooks/use-media-query';
|
||||||
import { AutoModeSettingsDialog } from './dialogs/auto-mode-settings-dialog';
|
import { AutoModeSettingsPopover } from './dialogs/auto-mode-settings-popover';
|
||||||
import { WorktreeSettingsDialog } from './dialogs/worktree-settings-dialog';
|
import { WorktreeSettingsPopover } from './dialogs/worktree-settings-popover';
|
||||||
import { PlanSettingsDialog } from './dialogs/plan-settings-dialog';
|
import { PlanSettingsPopover } from './dialogs/plan-settings-popover';
|
||||||
import { getHttpApiClient } from '@/lib/http-api-client';
|
import { getHttpApiClient } from '@/lib/http-api-client';
|
||||||
import { BoardSearchBar } from './board-search-bar';
|
import { BoardSearchBar } from './board-search-bar';
|
||||||
import { BoardControls } from './board-controls';
|
import { BoardControls } from './board-controls';
|
||||||
@@ -36,8 +33,6 @@ interface BoardHeaderProps {
|
|||||||
creatingSpecProjectPath?: string;
|
creatingSpecProjectPath?: string;
|
||||||
// Board controls props
|
// Board controls props
|
||||||
onShowBoardBackground: () => void;
|
onShowBoardBackground: () => void;
|
||||||
onShowCompletedModal: () => void;
|
|
||||||
completedCount: number;
|
|
||||||
// View toggle props
|
// View toggle props
|
||||||
viewMode: ViewMode;
|
viewMode: ViewMode;
|
||||||
onViewModeChange: (mode: ViewMode) => void;
|
onViewModeChange: (mode: ViewMode) => void;
|
||||||
@@ -61,14 +56,9 @@ export function BoardHeader({
|
|||||||
isCreatingSpec,
|
isCreatingSpec,
|
||||||
creatingSpecProjectPath,
|
creatingSpecProjectPath,
|
||||||
onShowBoardBackground,
|
onShowBoardBackground,
|
||||||
onShowCompletedModal,
|
|
||||||
completedCount,
|
|
||||||
viewMode,
|
viewMode,
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
}: BoardHeaderProps) {
|
}: BoardHeaderProps) {
|
||||||
const [showAutoModeSettings, setShowAutoModeSettings] = useState(false);
|
|
||||||
const [showWorktreeSettings, setShowWorktreeSettings] = useState(false);
|
|
||||||
const [showPlanSettings, setShowPlanSettings] = useState(false);
|
|
||||||
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
|
const claudeAuthStatus = useSetupStore((state) => state.claudeAuthStatus);
|
||||||
const skipVerificationInAutoMode = useAppStore((state) => state.skipVerificationInAutoMode);
|
const skipVerificationInAutoMode = useAppStore((state) => state.skipVerificationInAutoMode);
|
||||||
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
|
const setSkipVerificationInAutoMode = useAppStore((state) => state.setSkipVerificationInAutoMode);
|
||||||
@@ -127,12 +117,7 @@ export function BoardHeader({
|
|||||||
currentProjectPath={projectPath}
|
currentProjectPath={projectPath}
|
||||||
/>
|
/>
|
||||||
{isMounted && <ViewToggle viewMode={viewMode} onViewModeChange={onViewModeChange} />}
|
{isMounted && <ViewToggle viewMode={viewMode} onViewModeChange={onViewModeChange} />}
|
||||||
<BoardControls
|
<BoardControls isMounted={isMounted} onShowBoardBackground={onShowBoardBackground} />
|
||||||
isMounted={isMounted}
|
|
||||||
onShowBoardBackground={onShowBoardBackground}
|
|
||||||
onShowCompletedModal={onShowCompletedModal}
|
|
||||||
completedCount={completedCount}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
|
{/* Usage Popover - show if either provider is authenticated, only on desktop */}
|
||||||
@@ -148,7 +133,7 @@ export function BoardHeader({
|
|||||||
onConcurrencyChange={onConcurrencyChange}
|
onConcurrencyChange={onConcurrencyChange}
|
||||||
isAutoModeRunning={isAutoModeRunning}
|
isAutoModeRunning={isAutoModeRunning}
|
||||||
onAutoModeToggle={onAutoModeToggle}
|
onAutoModeToggle={onAutoModeToggle}
|
||||||
onOpenAutoModeSettings={() => setShowAutoModeSettings(true)}
|
onOpenAutoModeSettings={() => {}}
|
||||||
onOpenPlanDialog={onOpenPlanDialog}
|
onOpenPlanDialog={onOpenPlanDialog}
|
||||||
showClaudeUsage={showClaudeUsage}
|
showClaudeUsage={showClaudeUsage}
|
||||||
showCodexUsage={showCodexUsage}
|
showCodexUsage={showCodexUsage}
|
||||||
@@ -160,7 +145,10 @@ export function BoardHeader({
|
|||||||
{isMounted && !isMobile && (
|
{isMounted && !isMobile && (
|
||||||
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
<div className={controlContainerClass} data-testid="worktrees-toggle-container">
|
||||||
<GitBranch className="w-4 h-4 text-muted-foreground" />
|
<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
|
Worktree Bar
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -169,72 +157,20 @@ export function BoardHeader({
|
|||||||
onCheckedChange={handleWorktreePanelToggle}
|
onCheckedChange={handleWorktreePanelToggle}
|
||||||
data-testid="worktrees-toggle"
|
data-testid="worktrees-toggle"
|
||||||
/>
|
/>
|
||||||
<button
|
<WorktreeSettingsPopover
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Worktree Settings Dialog */}
|
|
||||||
<WorktreeSettingsDialog
|
|
||||||
open={showWorktreeSettings}
|
|
||||||
onOpenChange={setShowWorktreeSettings}
|
|
||||||
addFeatureUseSelectedWorktreeBranch={addFeatureUseSelectedWorktreeBranch}
|
addFeatureUseSelectedWorktreeBranch={addFeatureUseSelectedWorktreeBranch}
|
||||||
onAddFeatureUseSelectedWorktreeBranchChange={setAddFeatureUseSelectedWorktreeBranch}
|
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>
|
||||||
<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 */}
|
{/* Auto Mode Toggle - only show after mount to prevent hydration issues */}
|
||||||
{isMounted && !isMobile && (
|
{isMounted && !isMobile && (
|
||||||
<div className={controlContainerClass} data-testid="auto-mode-toggle-container">
|
<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
|
Auto Mode
|
||||||
</Label>
|
</Label>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -243,24 +179,15 @@ export function BoardHeader({
|
|||||||
onCheckedChange={onAutoModeToggle}
|
onCheckedChange={onAutoModeToggle}
|
||||||
data-testid="auto-mode-toggle"
|
data-testid="auto-mode-toggle"
|
||||||
/>
|
/>
|
||||||
<button
|
<AutoModeSettingsPopover
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Auto Mode Settings Dialog */}
|
|
||||||
<AutoModeSettingsDialog
|
|
||||||
open={showAutoModeSettings}
|
|
||||||
onOpenChange={setShowAutoModeSettings}
|
|
||||||
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
skipVerificationInAutoMode={skipVerificationInAutoMode}
|
||||||
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
onSkipVerificationChange={setSkipVerificationInAutoMode}
|
||||||
|
maxConcurrency={maxConcurrency}
|
||||||
|
runningAgentsCount={runningAgentsCount}
|
||||||
|
onConcurrencyChange={onConcurrencyChange}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Plan Button with Settings - only show on desktop, mobile has it in the menu */}
|
{/* Plan Button with Settings - only show on desktop, mobile has it in the menu */}
|
||||||
{isMounted && !isMobile && (
|
{isMounted && !isMobile && (
|
||||||
@@ -273,25 +200,13 @@ export function BoardHeader({
|
|||||||
<Wand2 className="w-4 h-4 text-muted-foreground" />
|
<Wand2 className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">Plan</span>
|
<span className="text-sm font-medium">Plan</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<PlanSettingsPopover
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Plan Settings Dialog */}
|
|
||||||
<PlanSettingsDialog
|
|
||||||
open={showPlanSettings}
|
|
||||||
onOpenChange={setShowPlanSettings}
|
|
||||||
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
|
planUseSelectedWorktreeBranch={planUseSelectedWorktreeBranch}
|
||||||
onPlanUseSelectedWorktreeBranchChange={setPlanUseSelectedWorktreeBranch}
|
onPlanUseSelectedWorktreeBranchChange={setPlanUseSelectedWorktreeBranch}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ export function SelectionActionBar({
|
|||||||
}: SelectionActionBarProps) {
|
}: SelectionActionBarProps) {
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
|
||||||
if (selectedCount === 0) return null;
|
const allSelected = selectedCount === totalCount && totalCount > 0;
|
||||||
|
|
||||||
const allSelected = selectedCount === totalCount;
|
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
setShowDeleteDialog(true);
|
setShowDeleteDialog(true);
|
||||||
@@ -55,7 +53,9 @@ export function SelectionActionBar({
|
|||||||
data-testid="selection-action-bar"
|
data-testid="selection-action-bar"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-medium text-foreground">
|
<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>
|
</span>
|
||||||
|
|
||||||
<div className="h-4 w-px bg-border" />
|
<div className="h-4 w-px bg-border" />
|
||||||
@@ -65,7 +65,8 @@ export function SelectionActionBar({
|
|||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onEdit}
|
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"
|
data-testid="selection-edit-button"
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4 mr-1.5" />
|
<Pencil className="w-4 h-4 mr-1.5" />
|
||||||
@@ -76,7 +77,8 @@ export function SelectionActionBar({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDeleteClick}
|
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"
|
data-testid="selection-delete-button"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-1.5" />
|
<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"
|
data-testid="view-toggle-kanban"
|
||||||
>
|
>
|
||||||
<LayoutGrid className="w-4 h-4" />
|
<LayoutGrid className="w-4 h-4" />
|
||||||
<span className="sr-only sm:not-sr-only">Kanban</span>
|
<span className="sr-only">Kanban</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
@@ -55,7 +55,7 @@ export function ViewToggle({ viewMode, onViewModeChange, className }: ViewToggle
|
|||||||
data-testid="view-toggle-list"
|
data-testid="view-toggle-list"
|
||||||
>
|
>
|
||||||
<List className="w-4 h-4" />
|
<List className="w-4 h-4" />
|
||||||
<span className="sr-only sm:not-sr-only">List</span>
|
<span className="sr-only">List</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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[];
|
runningAutoTasks: string[];
|
||||||
onArchiveAllVerified: () => void;
|
onArchiveAllVerified: () => void;
|
||||||
onAddFeature: () => void;
|
onAddFeature: () => void;
|
||||||
|
onShowCompletedModal: () => void;
|
||||||
|
completedCount: number;
|
||||||
pipelineConfig: PipelineConfig | null;
|
pipelineConfig: PipelineConfig | null;
|
||||||
onOpenPipelineSettings?: () => void;
|
onOpenPipelineSettings?: () => void;
|
||||||
// Selection mode props
|
// Selection mode props
|
||||||
@@ -88,6 +90,8 @@ export function KanbanBoard({
|
|||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
onArchiveAllVerified,
|
onArchiveAllVerified,
|
||||||
onAddFeature,
|
onAddFeature,
|
||||||
|
onShowCompletedModal,
|
||||||
|
completedCount,
|
||||||
pipelineConfig,
|
pipelineConfig,
|
||||||
onOpenPipelineSettings,
|
onOpenPipelineSettings,
|
||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
@@ -140,7 +144,9 @@ export function KanbanBoard({
|
|||||||
showBorder={backgroundSettings.columnBorderEnabled}
|
showBorder={backgroundSettings.columnBorderEnabled}
|
||||||
hideScrollbar={backgroundSettings.hideScrollbar}
|
hideScrollbar={backgroundSettings.hideScrollbar}
|
||||||
headerAction={
|
headerAction={
|
||||||
column.id === 'verified' && columnFeatures.length > 0 ? (
|
column.id === 'verified' ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{columnFeatures.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -151,6 +157,23 @@ export function KanbanBoard({
|
|||||||
<Archive className="w-3 h-3 mr-1" />
|
<Archive className="w-3 h-3 mr-1" />
|
||||||
Complete All
|
Complete All
|
||||||
</Button>
|
</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' ? (
|
) : column.id === 'backlog' ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ export function GraphViewPage() {
|
|||||||
setShowAddDialog(true);
|
setShowAddDialog(true);
|
||||||
}}
|
}}
|
||||||
onDeleteTask={(feature) => handleDeleteFeature(feature.id)}
|
onDeleteTask={(feature) => handleDeleteFeature(feature.id)}
|
||||||
|
onAddFeature={() => setShowAddDialog(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Edit Feature Dialog */}
|
{/* Edit Feature Dialog */}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import {
|
|||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useDebounceValue } from 'usehooks-ts';
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
import { SearchX } from 'lucide-react';
|
import { SearchX, Plus } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
// Define custom node and edge types - using any to avoid React Flow's strict typing
|
// 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;
|
onNodeDoubleClick?: (featureId: string) => void;
|
||||||
nodeActionCallbacks?: NodeActionCallbacks;
|
nodeActionCallbacks?: NodeActionCallbacks;
|
||||||
onCreateDependency?: (sourceId: string, targetId: string) => Promise<boolean>;
|
onCreateDependency?: (sourceId: string, targetId: string) => Promise<boolean>;
|
||||||
|
onAddFeature?: () => void;
|
||||||
backgroundStyle?: React.CSSProperties;
|
backgroundStyle?: React.CSSProperties;
|
||||||
backgroundSettings?: BackgroundSettings;
|
backgroundSettings?: BackgroundSettings;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -77,6 +78,7 @@ function GraphCanvasInner({
|
|||||||
onNodeDoubleClick,
|
onNodeDoubleClick,
|
||||||
nodeActionCallbacks,
|
nodeActionCallbacks,
|
||||||
onCreateDependency,
|
onCreateDependency,
|
||||||
|
onAddFeature,
|
||||||
backgroundStyle,
|
backgroundStyle,
|
||||||
backgroundSettings,
|
backgroundSettings,
|
||||||
className,
|
className,
|
||||||
@@ -398,6 +400,14 @@ function GraphCanvasInner({
|
|||||||
|
|
||||||
<GraphLegend />
|
<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 */}
|
{/* Empty state when all nodes are filtered out */}
|
||||||
{filterResult.hasActiveFilter && filterResult.matchedNodeIds.size === 0 && (
|
{filterResult.hasActiveFilter && filterResult.matchedNodeIds.size === 0 && (
|
||||||
<Panel position="top-center" className="mt-20">
|
<Panel position="top-center" className="mt-20">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface GraphViewProps {
|
|||||||
onUpdateFeature?: (featureId: string, updates: Partial<Feature>) => void;
|
onUpdateFeature?: (featureId: string, updates: Partial<Feature>) => void;
|
||||||
onSpawnTask?: (feature: Feature) => void;
|
onSpawnTask?: (feature: Feature) => void;
|
||||||
onDeleteTask?: (feature: Feature) => void;
|
onDeleteTask?: (feature: Feature) => void;
|
||||||
|
onAddFeature?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GraphView({
|
export function GraphView({
|
||||||
@@ -40,6 +41,7 @@ export function GraphView({
|
|||||||
onUpdateFeature,
|
onUpdateFeature,
|
||||||
onSpawnTask,
|
onSpawnTask,
|
||||||
onDeleteTask,
|
onDeleteTask,
|
||||||
|
onAddFeature,
|
||||||
}: GraphViewProps) {
|
}: GraphViewProps) {
|
||||||
const { currentProject } = useAppStore();
|
const { currentProject } = useAppStore();
|
||||||
|
|
||||||
@@ -212,6 +214,7 @@ export function GraphView({
|
|||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
nodeActionCallbacks={nodeActionCallbacks}
|
nodeActionCallbacks={nodeActionCallbacks}
|
||||||
onCreateDependency={handleCreateDependency}
|
onCreateDependency={handleCreateDependency}
|
||||||
|
onAddFeature={onAddFeature}
|
||||||
backgroundStyle={backgroundImageStyle}
|
backgroundStyle={backgroundImageStyle}
|
||||||
backgroundSettings={backgroundSettings}
|
backgroundSettings={backgroundSettings}
|
||||||
className="h-full"
|
className="h-full"
|
||||||
|
|||||||
Reference in New Issue
Block a user