refactor: update mass edit dialog and introduce new select components

- Removed advanced options toggle and related state from the mass edit dialog for a cleaner UI.
- Replaced ProfileQuickSelect with ProfileSelect for better profile management.
- Introduced new PlanningModeSelect and PrioritySelect components for streamlined selection of planning modes and priorities.
- Updated imports in shared index to include new select components.
- Enhanced the mass edit dialog to utilize the new components, improving user experience during bulk edits.
This commit is contained in:
Shirone
2026-01-04 23:24:24 +01:00
parent 06c02de1cb
commit 1117afc37a
6 changed files with 507 additions and 148 deletions

View File

@@ -2,8 +2,11 @@ export * from './model-constants';
export * from './model-selector';
export * from './thinking-level-selector';
export * from './profile-quick-select';
export * from './profile-select';
export * from './testing-tab-content';
export * from './priority-selector';
export * from './priority-select';
export * from './branch-selector';
export * from './planning-mode-selector';
export * from './planning-mode-select';
export * from './ancestor-context-section';

View File

@@ -0,0 +1,148 @@
import { Zap, ClipboardList, FileText, ScrollText } from 'lucide-react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { cn } from '@/lib/utils';
import type { PlanningMode } from '@automaker/types';
interface PlanningModeSelectProps {
mode: PlanningMode;
onModeChange: (mode: PlanningMode) => void;
requireApproval?: boolean;
onRequireApprovalChange?: (require: boolean) => void;
testIdPrefix?: string;
className?: string;
disabled?: boolean;
}
const modes = [
{
value: 'skip' as const,
label: 'Skip',
description: 'Direct implementation, no upfront planning',
icon: Zap,
color: 'text-emerald-500',
},
{
value: 'lite' as const,
label: 'Lite',
description: 'Think through approach, create task list',
icon: ClipboardList,
color: 'text-blue-500',
},
{
value: 'spec' as const,
label: 'Spec',
description: 'Generate spec with acceptance criteria',
icon: FileText,
color: 'text-purple-500',
},
{
value: 'full' as const,
label: 'Full',
description: 'Comprehensive spec with phased plan',
icon: ScrollText,
color: 'text-amber-500',
},
];
/**
* PlanningModeSelect - Compact dropdown selector for planning modes
*
* A lightweight alternative to PlanningModeSelector for contexts where
* spec management UI is not needed (e.g., mass edit, bulk operations).
*
* Shows icon + label in dropdown, with description text below.
* Does not include spec generation, approval, or require-approval checkbox.
*
* @example
* ```tsx
* <PlanningModeSelect
* mode={planningMode}
* onModeChange={(mode) => {
* setPlanningMode(mode);
* setRequireApproval(mode === 'spec' || mode === 'full');
* }}
* testIdPrefix="mass-edit-planning"
* />
* ```
*/
export function PlanningModeSelect({
mode,
onModeChange,
requireApproval,
onRequireApprovalChange,
testIdPrefix = 'planning-mode',
className,
disabled = false,
}: PlanningModeSelectProps) {
const selectedMode = modes.find((m) => m.value === mode);
// Disable approval checkbox for skip/lite modes since they don't use planning
const isApprovalDisabled = disabled || mode === 'skip' || mode === 'lite';
return (
<div className={cn('space-y-2', className)}>
<Select
value={mode}
onValueChange={(value: string) => onModeChange(value as PlanningMode)}
disabled={disabled}
>
<SelectTrigger className="h-9" data-testid={`${testIdPrefix}-select-trigger`}>
<SelectValue>
{selectedMode && (
<div className="flex items-center gap-2">
<selectedMode.icon className={cn('h-4 w-4', selectedMode.color)} />
<span>{selectedMode.label}</span>
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{modes.map((m) => {
const Icon = m.icon;
return (
<SelectItem
key={m.value}
value={m.value}
data-testid={`${testIdPrefix}-option-${m.value}`}
>
<div className="flex items-center gap-2">
<Icon className={cn('h-3.5 w-3.5', m.color)} />
<span>{m.label}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedMode && <p className="text-xs text-muted-foreground">{selectedMode.description}</p>}
{onRequireApprovalChange && (
<div className="flex items-center gap-2 pt-1">
<Checkbox
id={`${testIdPrefix}-require-approval`}
checked={requireApproval && !isApprovalDisabled}
onCheckedChange={(checked) => onRequireApprovalChange(!!checked)}
disabled={isApprovalDisabled}
data-testid={`${testIdPrefix}-require-approval-checkbox`}
/>
<Label
htmlFor={`${testIdPrefix}-require-approval`}
className={cn(
'text-sm font-normal',
isApprovalDisabled ? 'cursor-not-allowed text-muted-foreground' : 'cursor-pointer'
)}
>
Require plan approval before execution
</Label>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,112 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils';
interface PrioritySelectProps {
selectedPriority: number;
onPrioritySelect: (priority: number) => void;
testIdPrefix?: string;
className?: string;
disabled?: boolean;
}
const priorities = [
{
value: 1,
label: 'High',
description: 'Urgent, needs immediate attention',
icon: ChevronUp,
color: 'text-red-500',
bgColor: 'bg-red-500/10',
},
{
value: 2,
label: 'Medium',
description: 'Normal priority, standard workflow',
icon: AlertCircle,
color: 'text-yellow-500',
bgColor: 'bg-yellow-500/10',
},
{
value: 3,
label: 'Low',
description: 'Can wait, not time-sensitive',
icon: ChevronDown,
color: 'text-blue-500',
bgColor: 'bg-blue-500/10',
},
];
/**
* PrioritySelect - Compact dropdown selector for feature priority
*
* A lightweight alternative to PrioritySelector for contexts where
* space is limited (e.g., mass edit, bulk operations).
*
* Shows icon + priority level in dropdown, with description below.
*
* @example
* ```tsx
* <PrioritySelect
* selectedPriority={priority}
* onPrioritySelect={setPriority}
* testIdPrefix="mass-edit-priority"
* />
* ```
*/
export function PrioritySelect({
selectedPriority,
onPrioritySelect,
testIdPrefix = 'priority',
className,
disabled = false,
}: PrioritySelectProps) {
const selectedPriorityObj = priorities.find((p) => p.value === selectedPriority);
return (
<div className={cn('space-y-2', className)}>
<Select
value={selectedPriority.toString()}
onValueChange={(value: string) => onPrioritySelect(parseInt(value, 10))}
disabled={disabled}
>
<SelectTrigger className="h-9" data-testid={`${testIdPrefix}-select-trigger`}>
<SelectValue>
{selectedPriorityObj && (
<div className="flex items-center gap-2">
<selectedPriorityObj.icon className={cn('h-4 w-4', selectedPriorityObj.color)} />
<span>{selectedPriorityObj.label}</span>
</div>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{priorities.map((p) => {
const Icon = p.icon;
return (
<SelectItem
key={p.value}
value={p.value.toString()}
data-testid={`${testIdPrefix}-option-${p.label.toLowerCase()}`}
>
<div className="flex items-center gap-2">
<Icon className={cn('h-3.5 w-3.5', p.color)} />
<span>{p.label}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedPriorityObj && (
<p className="text-xs text-muted-foreground">{selectedPriorityObj.description}</p>
)}
</div>
);
}

View File

@@ -0,0 +1,175 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Brain, Terminal } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ModelAlias, ThinkingLevel, AIProfile, CursorModelId } from '@automaker/types';
import { CURSOR_MODEL_MAP, profileHasThinking, PROVIDER_PREFIXES } from '@automaker/types';
import { PROFILE_ICONS } from './model-constants';
/**
* Get display string for a profile's model configuration
*/
function getProfileModelDisplay(profile: AIProfile): string {
if (profile.provider === 'cursor') {
const cursorModel = profile.cursorModel || 'auto';
const modelConfig = CURSOR_MODEL_MAP[cursorModel];
return modelConfig?.label || cursorModel;
}
// Claude
return profile.model || 'sonnet';
}
/**
* Get display string for a profile's thinking configuration
*/
function getProfileThinkingDisplay(profile: AIProfile): string | null {
if (profile.provider === 'cursor') {
// For Cursor, thinking is embedded in the model
return profileHasThinking(profile) ? 'thinking' : null;
}
// Claude
return profile.thinkingLevel && profile.thinkingLevel !== 'none' ? profile.thinkingLevel : null;
}
interface ProfileSelectProps {
profiles: AIProfile[];
selectedModel: ModelAlias | CursorModelId;
selectedThinkingLevel: ThinkingLevel;
selectedCursorModel?: string; // For detecting cursor profile selection
onSelect: (profile: AIProfile) => void;
testIdPrefix?: string;
className?: string;
disabled?: boolean;
}
/**
* ProfileSelect - Compact dropdown selector for AI profiles
*
* A lightweight alternative to ProfileQuickSelect for contexts where
* space is limited (e.g., mass edit, bulk operations).
*
* Shows icon + profile name in dropdown, with model details below.
*
* @example
* ```tsx
* <ProfileSelect
* profiles={aiProfiles}
* selectedModel={model}
* selectedThinkingLevel={thinkingLevel}
* selectedCursorModel={isCurrentModelCursor ? model : undefined}
* onSelect={handleProfileSelect}
* testIdPrefix="mass-edit-profile"
* />
* ```
*/
export function ProfileSelect({
profiles,
selectedModel,
selectedThinkingLevel,
selectedCursorModel,
onSelect,
testIdPrefix = 'profile-select',
className,
disabled = false,
}: ProfileSelectProps) {
if (profiles.length === 0) {
return null;
}
// Check if a profile is selected
const isProfileSelected = (profile: AIProfile): boolean => {
if (profile.provider === 'cursor') {
// For cursor profiles, check if cursor model matches
const profileCursorModel = `${PROVIDER_PREFIXES.cursor}${profile.cursorModel || 'auto'}`;
return selectedCursorModel === profileCursorModel;
}
// For Claude profiles
return selectedModel === profile.model && selectedThinkingLevel === profile.thinkingLevel;
};
const selectedProfile = profiles.find(isProfileSelected);
return (
<div className={cn('space-y-2', className)}>
<Select
value={selectedProfile?.id || 'none'}
onValueChange={(value: string) => {
if (value !== 'none') {
const profile = profiles.find((p) => p.id === value);
if (profile) {
onSelect(profile);
}
}
}}
disabled={disabled}
>
<SelectTrigger className="h-9" data-testid={`${testIdPrefix}-select-trigger`}>
<SelectValue>
{selectedProfile ? (
<div className="flex items-center gap-2">
{selectedProfile.provider === 'cursor' ? (
<Terminal className="h-4 w-4 text-amber-500" />
) : (
(() => {
const IconComponent = selectedProfile.icon
? PROFILE_ICONS[selectedProfile.icon]
: Brain;
return <IconComponent className="h-4 w-4 text-primary" />;
})()
)}
<span>{selectedProfile.name}</span>
</div>
) : (
<span className="text-muted-foreground">Select a profile...</span>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="none" className="text-muted-foreground">
No profile selected
</SelectItem>
{profiles.map((profile) => {
const isCursorProfile = profile.provider === 'cursor';
const IconComponent = profile.icon ? PROFILE_ICONS[profile.icon] : Brain;
return (
<SelectItem
key={profile.id}
value={profile.id}
data-testid={`${testIdPrefix}-option-${profile.id}`}
>
<div className="flex items-center gap-2">
{isCursorProfile ? (
<Terminal className="h-3.5 w-3.5 text-amber-500" />
) : (
<IconComponent className="h-3.5 w-3.5 text-primary" />
)}
<div className="flex flex-col">
<span className="text-sm">{profile.name}</span>
<span className="text-[10px] text-muted-foreground">
{getProfileModelDisplay(profile)}
{getProfileThinkingDisplay(profile) &&
` + ${getProfileThinkingDisplay(profile)}`}
</span>
</div>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
{selectedProfile && (
<p className="text-xs text-muted-foreground">
{getProfileModelDisplay(selectedProfile)}
{getProfileThinkingDisplay(selectedProfile) &&
` + ${getProfileThinkingDisplay(selectedProfile)}`}
</p>
)}
</div>
);
}