mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
Resolved conflicts in dialog components by keeping simplified code without modelSupportsPlanningMode conditional (always true now).
654 lines
25 KiB
TypeScript
654 lines
25 KiB
TypeScript
// @ts-nocheck
|
|
import { useState, useEffect } from 'react';
|
|
import { createLogger } from '@automaker/utils/logger';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { HotkeyButton } from '@/components/ui/hotkey-button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { CategoryAutocomplete } from '@/components/ui/category-autocomplete';
|
|
import { DependencySelector } from '@/components/ui/dependency-selector';
|
|
import {
|
|
DescriptionImageDropZone,
|
|
FeatureImagePath as DescriptionImagePath,
|
|
FeatureTextFilePath as DescriptionTextFilePath,
|
|
ImagePreviewMap,
|
|
} from '@/components/ui/description-image-dropzone';
|
|
import { GitBranch, Cpu, FolderKanban, Settings2 } from 'lucide-react';
|
|
import { useNavigate } from '@tanstack/react-router';
|
|
import { toast } from 'sonner';
|
|
import { cn, modelSupportsThinking } from '@/lib/utils';
|
|
import { Feature, ModelAlias, ThinkingLevel, useAppStore, PlanningMode } from '@/store/app-store';
|
|
import type { ReasoningEffort, PhaseModelEntry, DescriptionHistoryEntry } from '@automaker/types';
|
|
import { migrateModelId } from '@automaker/types';
|
|
import {
|
|
TestingTabContent,
|
|
PrioritySelector,
|
|
WorkModeSelector,
|
|
PlanningModeSelect,
|
|
EnhanceWithAI,
|
|
EnhancementHistoryButton,
|
|
PipelineExclusionControls,
|
|
type EnhancementMode,
|
|
} from '../shared';
|
|
import type { WorkMode } from '../shared';
|
|
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
import { DependencyTreeDialog } from './dependency-tree-dialog';
|
|
import { supportsReasoningEffort } from '@automaker/types';
|
|
|
|
const logger = createLogger('EditFeatureDialog');
|
|
|
|
interface EditFeatureDialogProps {
|
|
feature: Feature | null;
|
|
onClose: () => void;
|
|
onUpdate: (
|
|
featureId: string,
|
|
updates: {
|
|
title: string;
|
|
category: string;
|
|
description: string;
|
|
skipTests: boolean;
|
|
model: ModelAlias;
|
|
thinkingLevel: ThinkingLevel;
|
|
reasoningEffort: ReasoningEffort;
|
|
imagePaths: DescriptionImagePath[];
|
|
textFilePaths: DescriptionTextFilePath[];
|
|
branchName: string; // Can be empty string to use current branch
|
|
priority: number;
|
|
planningMode: PlanningMode;
|
|
requirePlanApproval: boolean;
|
|
dependencies?: string[];
|
|
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
|
excludedPipelineSteps?: string[]; // Pipeline step IDs to skip for this feature
|
|
},
|
|
descriptionHistorySource?: 'enhance' | 'edit',
|
|
enhancementMode?: EnhancementMode,
|
|
preEnhancementDescription?: string
|
|
) => void;
|
|
categorySuggestions: string[];
|
|
branchSuggestions: string[];
|
|
branchCardCounts?: Record<string, number>; // Map of branch name to unarchived card count
|
|
currentBranch?: string;
|
|
isMaximized: boolean;
|
|
allFeatures: Feature[];
|
|
projectPath?: string;
|
|
}
|
|
|
|
export function EditFeatureDialog({
|
|
feature,
|
|
onClose,
|
|
onUpdate,
|
|
categorySuggestions,
|
|
branchSuggestions,
|
|
branchCardCounts,
|
|
currentBranch,
|
|
isMaximized,
|
|
allFeatures,
|
|
projectPath,
|
|
}: EditFeatureDialogProps) {
|
|
const navigate = useNavigate();
|
|
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
|
// Derive initial workMode from feature's branchName
|
|
const [workMode, setWorkMode] = useState<WorkMode>(() => {
|
|
// If feature has a branchName, it's using 'custom' mode
|
|
// Otherwise, it's on 'current' branch (no worktree isolation)
|
|
return feature?.branchName ? 'custom' : 'current';
|
|
});
|
|
const [editFeaturePreviewMap, setEditFeaturePreviewMap] = useState<ImagePreviewMap>(
|
|
() => new Map()
|
|
);
|
|
const [showDependencyTree, setShowDependencyTree] = useState(false);
|
|
const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
|
|
const [requirePlanApproval, setRequirePlanApproval] = useState(
|
|
feature?.requirePlanApproval ?? false
|
|
);
|
|
|
|
// Model selection state - migrate legacy model IDs to canonical format
|
|
const [modelEntry, setModelEntry] = useState<PhaseModelEntry>(() => ({
|
|
model: migrateModelId(feature?.model) || 'claude-opus',
|
|
thinkingLevel: feature?.thinkingLevel || 'none',
|
|
reasoningEffort: feature?.reasoningEffort || 'none',
|
|
}));
|
|
|
|
// Track the source of description changes for history
|
|
const [descriptionChangeSource, setDescriptionChangeSource] = useState<
|
|
{ source: 'enhance'; mode: EnhancementMode } | 'edit' | null
|
|
>(null);
|
|
// Track the original description when the dialog opened for comparison
|
|
const [originalDescription, setOriginalDescription] = useState(feature?.description ?? '');
|
|
// Track the description before enhancement (so it can be restored)
|
|
const [preEnhancementDescription, setPreEnhancementDescription] = useState<string | null>(null);
|
|
// Local history state for real-time display (combines persisted + session history)
|
|
const [localHistory, setLocalHistory] = useState<DescriptionHistoryEntry[]>(
|
|
feature?.descriptionHistory ?? []
|
|
);
|
|
|
|
// Dependency state
|
|
const [parentDependencies, setParentDependencies] = useState<string[]>(
|
|
feature?.dependencies ?? []
|
|
);
|
|
// Child dependencies are features that have this feature in their dependencies
|
|
const [childDependencies, setChildDependencies] = useState<string[]>(() => {
|
|
if (!feature) return [];
|
|
return allFeatures.filter((f) => f.dependencies?.includes(feature.id)).map((f) => f.id);
|
|
});
|
|
// Track original child dependencies to detect changes
|
|
const [originalChildDependencies, setOriginalChildDependencies] = useState<string[]>(() => {
|
|
if (!feature) return [];
|
|
return allFeatures.filter((f) => f.dependencies?.includes(feature.id)).map((f) => f.id);
|
|
});
|
|
|
|
// Pipeline exclusion state
|
|
const [excludedPipelineSteps, setExcludedPipelineSteps] = useState<string[]>(
|
|
feature?.excludedPipelineSteps ?? []
|
|
);
|
|
|
|
useEffect(() => {
|
|
setEditingFeature(feature);
|
|
if (feature) {
|
|
setPlanningMode(feature.planningMode ?? 'skip');
|
|
setRequirePlanApproval(feature.requirePlanApproval ?? false);
|
|
// Derive workMode from feature's branchName
|
|
setWorkMode(feature.branchName ? 'custom' : 'current');
|
|
// Reset history tracking state
|
|
setOriginalDescription(feature.description ?? '');
|
|
setDescriptionChangeSource(null);
|
|
setPreEnhancementDescription(null);
|
|
setLocalHistory(feature.descriptionHistory ?? []);
|
|
// Reset model entry - migrate legacy model IDs
|
|
setModelEntry({
|
|
model: migrateModelId(feature.model) || 'claude-opus',
|
|
thinkingLevel: feature.thinkingLevel || 'none',
|
|
reasoningEffort: feature.reasoningEffort || 'none',
|
|
});
|
|
// Reset dependency state
|
|
setParentDependencies(feature.dependencies ?? []);
|
|
const childDeps = allFeatures
|
|
.filter((f) => f.dependencies?.includes(feature.id))
|
|
.map((f) => f.id);
|
|
setChildDependencies(childDeps);
|
|
setOriginalChildDependencies(childDeps);
|
|
// Reset pipeline exclusion state
|
|
setExcludedPipelineSteps(feature.excludedPipelineSteps ?? []);
|
|
} else {
|
|
setEditFeaturePreviewMap(new Map());
|
|
setDescriptionChangeSource(null);
|
|
setPreEnhancementDescription(null);
|
|
setLocalHistory([]);
|
|
setParentDependencies([]);
|
|
setChildDependencies([]);
|
|
setOriginalChildDependencies([]);
|
|
setExcludedPipelineSteps([]);
|
|
}
|
|
}, [feature, allFeatures]);
|
|
|
|
// Clear requirePlanApproval when planning mode is skip or lite
|
|
useEffect(() => {
|
|
if (planningMode === 'skip' || planningMode === 'lite') {
|
|
setRequirePlanApproval(false);
|
|
}
|
|
}, [planningMode]);
|
|
|
|
const handleModelChange = (entry: PhaseModelEntry) => {
|
|
setModelEntry(entry);
|
|
};
|
|
|
|
const handleUpdate = () => {
|
|
if (!editingFeature) return;
|
|
|
|
// Validate branch selection for custom mode
|
|
const isBranchSelectorEnabled = editingFeature.status === 'backlog';
|
|
if (isBranchSelectorEnabled && workMode === 'custom' && !editingFeature.branchName?.trim()) {
|
|
toast.error('Please select a branch name');
|
|
return;
|
|
}
|
|
|
|
const selectedModel = modelEntry.model;
|
|
const normalizedThinking: ThinkingLevel = modelSupportsThinking(selectedModel)
|
|
? (modelEntry.thinkingLevel ?? 'none')
|
|
: 'none';
|
|
const normalizedReasoning: ReasoningEffort = supportsReasoningEffort(selectedModel)
|
|
? (modelEntry.reasoningEffort ?? 'none')
|
|
: 'none';
|
|
|
|
// For 'current' mode, use empty string (work on current branch)
|
|
// For 'auto' mode, use empty string (will be auto-generated in use-board-actions)
|
|
// For 'custom' mode, use the specified branch name
|
|
const finalBranchName = workMode === 'custom' ? editingFeature.branchName || '' : '';
|
|
|
|
// Check if child dependencies changed
|
|
const childDepsChanged =
|
|
childDependencies.length !== originalChildDependencies.length ||
|
|
childDependencies.some((id) => !originalChildDependencies.includes(id)) ||
|
|
originalChildDependencies.some((id) => !childDependencies.includes(id));
|
|
|
|
const updates = {
|
|
title: editingFeature.title ?? '',
|
|
category: editingFeature.category,
|
|
description: editingFeature.description,
|
|
skipTests: editingFeature.skipTests ?? false,
|
|
model: selectedModel,
|
|
thinkingLevel: normalizedThinking,
|
|
reasoningEffort: normalizedReasoning,
|
|
imagePaths: editingFeature.imagePaths ?? [],
|
|
textFilePaths: editingFeature.textFilePaths ?? [],
|
|
branchName: finalBranchName,
|
|
priority: editingFeature.priority ?? 2,
|
|
planningMode,
|
|
requirePlanApproval,
|
|
workMode,
|
|
dependencies: parentDependencies,
|
|
childDependencies: childDepsChanged ? childDependencies : undefined,
|
|
excludedPipelineSteps: excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined,
|
|
};
|
|
|
|
// Determine if description changed and what source to use
|
|
const descriptionChanged = editingFeature.description !== originalDescription;
|
|
let historySource: 'enhance' | 'edit' | undefined;
|
|
let historyEnhancementMode: 'improve' | 'technical' | 'simplify' | 'acceptance' | undefined;
|
|
|
|
if (descriptionChanged && descriptionChangeSource) {
|
|
if (descriptionChangeSource === 'edit') {
|
|
historySource = 'edit';
|
|
} else {
|
|
historySource = 'enhance';
|
|
historyEnhancementMode = descriptionChangeSource.mode;
|
|
}
|
|
}
|
|
|
|
onUpdate(
|
|
editingFeature.id,
|
|
updates,
|
|
historySource,
|
|
historyEnhancementMode,
|
|
preEnhancementDescription ?? undefined
|
|
);
|
|
setEditFeaturePreviewMap(new Map());
|
|
onClose();
|
|
};
|
|
|
|
const handleDialogClose = (open: boolean) => {
|
|
if (!open) {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (!editingFeature) {
|
|
return null;
|
|
}
|
|
|
|
// Shared card styling
|
|
const cardClass = 'rounded-lg border border-border/50 bg-muted/30 p-4 space-y-3';
|
|
const sectionHeaderClass = 'flex items-center gap-2 text-sm font-medium text-foreground';
|
|
|
|
return (
|
|
<Dialog open={!!editingFeature} onOpenChange={handleDialogClose}>
|
|
<DialogContent
|
|
compact={!isMaximized}
|
|
data-testid="edit-feature-dialog"
|
|
onPointerDownOutside={(e: CustomEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
onInteractOutside={(e: CustomEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
|
e.preventDefault();
|
|
}
|
|
}}
|
|
>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit Feature</DialogTitle>
|
|
<DialogDescription>Modify the feature details.</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="py-4 space-y-4 overflow-y-auto flex-1 min-h-0">
|
|
{/* Task Details Section */}
|
|
<div className={cardClass}>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="edit-description">Description</Label>
|
|
{/* Version History Button - uses local history for real-time updates */}
|
|
<EnhancementHistoryButton
|
|
history={localHistory}
|
|
currentValue={editingFeature.description}
|
|
onRestore={(description) => {
|
|
setEditingFeature((prev) => (prev ? { ...prev, description } : prev));
|
|
setDescriptionChangeSource('edit');
|
|
}}
|
|
valueAccessor={(entry) => entry.description}
|
|
title="Version History"
|
|
restoreMessage="Description restored from history"
|
|
/>
|
|
</div>
|
|
<DescriptionImageDropZone
|
|
value={editingFeature.description}
|
|
onChange={(value) => {
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
description: value,
|
|
});
|
|
// Track that this change was a manual edit (unless already enhanced)
|
|
if (!descriptionChangeSource || descriptionChangeSource === 'edit') {
|
|
setDescriptionChangeSource('edit');
|
|
}
|
|
}}
|
|
images={editingFeature.imagePaths ?? []}
|
|
onImagesChange={(images) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
imagePaths: images,
|
|
})
|
|
}
|
|
textFiles={editingFeature.textFilePaths ?? []}
|
|
onTextFilesChange={(textFiles) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
textFilePaths: textFiles,
|
|
})
|
|
}
|
|
placeholder="Describe the feature..."
|
|
previewMap={editFeaturePreviewMap}
|
|
onPreviewMapChange={setEditFeaturePreviewMap}
|
|
data-testid="edit-feature-description"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="edit-title">Title (optional)</Label>
|
|
<Input
|
|
id="edit-title"
|
|
value={editingFeature.title ?? ''}
|
|
onChange={(e) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
title: e.target.value,
|
|
})
|
|
}
|
|
placeholder="Leave blank to auto-generate"
|
|
data-testid="edit-feature-title"
|
|
/>
|
|
</div>
|
|
|
|
{/* Enhancement Section */}
|
|
<EnhanceWithAI
|
|
value={editingFeature.description}
|
|
onChange={(enhanced) =>
|
|
setEditingFeature((prev) => (prev ? { ...prev, description: enhanced } : prev))
|
|
}
|
|
onHistoryAdd={({ mode, originalText, enhancedText }) => {
|
|
setDescriptionChangeSource({ source: 'enhance', mode });
|
|
setPreEnhancementDescription(originalText);
|
|
|
|
// Update local history for real-time display
|
|
const timestamp = new Date().toISOString();
|
|
setLocalHistory((prev) => {
|
|
const newHistory = [...prev];
|
|
// Add original text first (so user can restore to pre-enhancement state)
|
|
const lastEntry = prev[prev.length - 1];
|
|
if (!lastEntry || lastEntry.description !== originalText) {
|
|
newHistory.push({
|
|
description: originalText,
|
|
timestamp,
|
|
source: prev.length === 0 ? 'initial' : 'edit',
|
|
});
|
|
}
|
|
// Add enhanced text
|
|
newHistory.push({
|
|
description: enhancedText,
|
|
timestamp,
|
|
source: 'enhance',
|
|
enhancementMode: mode,
|
|
});
|
|
return newHistory;
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* AI & Execution Section */}
|
|
<div className={cardClass}>
|
|
<div className="flex items-center justify-between">
|
|
<div className={sectionHeaderClass}>
|
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
|
<span>AI & Execution</span>
|
|
</div>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onClose();
|
|
navigate({ to: '/settings', search: { view: 'defaults' } });
|
|
}}
|
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<Settings2 className="w-3.5 h-3.5" />
|
|
<span>Edit Defaults</span>
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Change default model and planning settings for new features</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Model</Label>
|
|
<PhaseModelSelector
|
|
value={modelEntry}
|
|
onChange={handleModelChange}
|
|
compact
|
|
align="end"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-3 grid-cols-2">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Planning</Label>
|
|
<PlanningModeSelect
|
|
mode={planningMode}
|
|
onModeChange={setPlanningMode}
|
|
testIdPrefix="edit-feature-planning"
|
|
compact
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Options</Label>
|
|
<div className="flex flex-col gap-2 pt-1">
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="edit-feature-skip-tests"
|
|
checked={!(editingFeature.skipTests ?? false)}
|
|
onCheckedChange={(checked) =>
|
|
setEditingFeature({ ...editingFeature, skipTests: !checked })
|
|
}
|
|
data-testid="edit-feature-skip-tests-checkbox"
|
|
/>
|
|
<Label
|
|
htmlFor="edit-feature-skip-tests"
|
|
className="text-xs font-normal cursor-pointer"
|
|
>
|
|
Run tests
|
|
</Label>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
id="edit-feature-require-approval"
|
|
checked={requirePlanApproval}
|
|
onCheckedChange={(checked) => setRequirePlanApproval(!!checked)}
|
|
disabled={planningMode === 'skip' || planningMode === 'lite'}
|
|
data-testid="edit-feature-require-approval-checkbox"
|
|
/>
|
|
<Label
|
|
htmlFor="edit-feature-require-approval"
|
|
className={cn(
|
|
'text-xs font-normal',
|
|
planningMode === 'skip' || planningMode === 'lite'
|
|
? 'cursor-not-allowed text-muted-foreground'
|
|
: 'cursor-pointer'
|
|
)}
|
|
>
|
|
Require approval
|
|
</Label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Organization Section */}
|
|
<div className={cardClass}>
|
|
<div className={sectionHeaderClass}>
|
|
<FolderKanban className="w-4 h-4 text-muted-foreground" />
|
|
<span>Organization</span>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Category</Label>
|
|
<CategoryAutocomplete
|
|
value={editingFeature.category}
|
|
onChange={(value) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
category: value,
|
|
})
|
|
}
|
|
suggestions={categorySuggestions}
|
|
placeholder="e.g., Core, UI, API"
|
|
data-testid="edit-feature-category"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">Priority</Label>
|
|
<PrioritySelector
|
|
selectedPriority={editingFeature.priority ?? 2}
|
|
onPrioritySelect={(priority) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
priority,
|
|
})
|
|
}
|
|
testIdPrefix="edit-priority"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Work Mode Selector */}
|
|
<div className="pt-2">
|
|
<WorkModeSelector
|
|
workMode={workMode}
|
|
onWorkModeChange={setWorkMode}
|
|
branchName={editingFeature.branchName ?? ''}
|
|
onBranchNameChange={(value) =>
|
|
setEditingFeature({
|
|
...editingFeature,
|
|
branchName: value,
|
|
})
|
|
}
|
|
branchSuggestions={branchSuggestions}
|
|
branchCardCounts={branchCardCounts}
|
|
currentBranch={currentBranch}
|
|
disabled={editingFeature.status !== 'backlog'}
|
|
testIdPrefix="edit-feature-work-mode"
|
|
/>
|
|
</div>
|
|
|
|
{/* Dependencies */}
|
|
{allFeatures.length > 1 && (
|
|
<div className="pt-2 space-y-3">
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Parent Dependencies (this feature depends on)
|
|
</Label>
|
|
<DependencySelector
|
|
currentFeatureId={editingFeature.id}
|
|
value={parentDependencies}
|
|
onChange={setParentDependencies}
|
|
features={allFeatures}
|
|
type="parent"
|
|
placeholder="Select features this depends on..."
|
|
data-testid="edit-feature-parent-deps"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Child Dependencies (features that depend on this)
|
|
</Label>
|
|
<DependencySelector
|
|
currentFeatureId={editingFeature.id}
|
|
value={childDependencies}
|
|
onChange={setChildDependencies}
|
|
features={allFeatures}
|
|
type="child"
|
|
placeholder="Select features that depend on this..."
|
|
data-testid="edit-feature-child-deps"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pipeline Exclusion Controls */}
|
|
<div className="pt-2">
|
|
<PipelineExclusionControls
|
|
projectPath={projectPath}
|
|
excludedPipelineSteps={excludedPipelineSteps}
|
|
onExcludedStepsChange={setExcludedPipelineSteps}
|
|
testIdPrefix="edit-feature-pipeline"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="sm:!justify-between">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowDependencyTree(true)}
|
|
className="gap-2 h-10"
|
|
>
|
|
<GitBranch className="w-4 h-4" />
|
|
View Dependency Tree
|
|
</Button>
|
|
<div className="flex gap-2">
|
|
<Button variant="ghost" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<HotkeyButton
|
|
onClick={handleUpdate}
|
|
hotkey={{ key: 'Enter', cmdCtrl: true }}
|
|
hotkeyActive={!!editingFeature}
|
|
data-testid="confirm-edit-feature"
|
|
disabled={
|
|
editingFeature.status === 'backlog' &&
|
|
workMode === 'custom' &&
|
|
!editingFeature.branchName?.trim()
|
|
}
|
|
>
|
|
Save Changes
|
|
</HotkeyButton>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
|
|
<DependencyTreeDialog
|
|
open={showDependencyTree}
|
|
onClose={() => setShowDependencyTree(false)}
|
|
feature={editingFeature}
|
|
allFeatures={allFeatures}
|
|
/>
|
|
</Dialog>
|
|
);
|
|
}
|