mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: implement pipeline step exclusion functionality
- Added support for excluding specific pipeline steps in feature management, allowing users to skip certain steps during execution. - Introduced a new `PipelineExclusionControls` component for managing exclusions in the UI. - Updated relevant dialogs and components to handle excluded pipeline steps, including `AddFeatureDialog`, `EditFeatureDialog`, and `MassEditDialog`. - Enhanced the `getNextStatus` method in `PipelineService` to account for excluded steps when determining the next status in the pipeline flow. - Updated tests to cover scenarios involving excluded pipeline steps.
This commit is contained in:
@@ -636,10 +636,12 @@ export function BoardView() {
|
||||
const result = await api.features.bulkUpdate(currentProject.path, featureIds, finalUpdates);
|
||||
|
||||
if (result.success) {
|
||||
// Update local state
|
||||
// Update local Zustand state
|
||||
featureIds.forEach((featureId) => {
|
||||
updateFeature(featureId, finalUpdates);
|
||||
});
|
||||
// Invalidate React Query cache to ensure features are refetched with updated data
|
||||
loadFeatures();
|
||||
toast.success(`Updated ${result.updatedCount} features`);
|
||||
exitSelectionMode();
|
||||
} else {
|
||||
@@ -661,6 +663,7 @@ export function BoardView() {
|
||||
addAndSelectWorktree,
|
||||
currentWorktreeBranch,
|
||||
setWorktreeRefreshKey,
|
||||
loadFeatures,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1493,6 +1496,7 @@ export function BoardView() {
|
||||
branchSuggestions={branchSuggestions}
|
||||
branchCardCounts={branchCardCounts}
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
projectPath={currentProject?.path}
|
||||
/>
|
||||
|
||||
{/* Board Background Modal */}
|
||||
@@ -1542,6 +1546,7 @@ export function BoardView() {
|
||||
isMaximized={isMaximized}
|
||||
parentFeature={spawnParentFeature}
|
||||
allFeatures={hookFeatures}
|
||||
projectPath={currentProject?.path}
|
||||
// When setting is enabled and a non-main worktree is selected, pass its branch to default to 'custom' work mode
|
||||
selectedNonMainWorktreeBranch={
|
||||
addFeatureUseSelectedWorktreeBranch && currentWorktreePath !== null
|
||||
@@ -1572,6 +1577,7 @@ export function BoardView() {
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={isMaximized}
|
||||
allFeatures={hookFeatures}
|
||||
projectPath={currentProject?.path}
|
||||
/>
|
||||
|
||||
{/* Agent Output Modal */}
|
||||
|
||||
@@ -3,9 +3,10 @@ import { memo, useEffect, useMemo, useState } from 'react';
|
||||
import { Feature, useAppStore } from '@/store/app-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { AlertCircle, Lock, Hand, Sparkles } from 'lucide-react';
|
||||
import { AlertCircle, Lock, Hand, Sparkles, SkipForward } from 'lucide-react';
|
||||
import { getBlockingDependencies } from '@automaker/dependency-resolver';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { usePipelineConfig } from '@/hooks/queries/use-pipeline';
|
||||
|
||||
/** Uniform badge style for all card badges */
|
||||
const uniformBadgeClass =
|
||||
@@ -51,9 +52,13 @@ export const CardBadges = memo(function CardBadges({ feature }: CardBadgesProps)
|
||||
|
||||
interface PriorityBadgesProps {
|
||||
feature: Feature;
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
export const PriorityBadges = memo(function PriorityBadges({ feature }: PriorityBadgesProps) {
|
||||
export const PriorityBadges = memo(function PriorityBadges({
|
||||
feature,
|
||||
projectPath,
|
||||
}: PriorityBadgesProps) {
|
||||
const { enableDependencyBlocking, features } = useAppStore(
|
||||
useShallow((state) => ({
|
||||
enableDependencyBlocking: state.enableDependencyBlocking,
|
||||
@@ -62,6 +67,9 @@ export const PriorityBadges = memo(function PriorityBadges({ feature }: Priority
|
||||
);
|
||||
const [currentTime, setCurrentTime] = useState(() => Date.now());
|
||||
|
||||
// Fetch pipeline config to check if there are pipelines to exclude
|
||||
const { data: pipelineConfig } = usePipelineConfig(projectPath);
|
||||
|
||||
// Calculate blocking dependencies (if feature is in backlog and has incomplete dependencies)
|
||||
const blockingDependencies = useMemo(() => {
|
||||
if (!enableDependencyBlocking || feature.status !== 'backlog') {
|
||||
@@ -108,7 +116,19 @@ export const PriorityBadges = memo(function PriorityBadges({ feature }: Priority
|
||||
const showManualVerification =
|
||||
feature.skipTests && !feature.error && feature.status === 'backlog';
|
||||
|
||||
const showBadges = feature.priority || showManualVerification || isBlocked || isJustFinished;
|
||||
// Check if feature has excluded pipeline steps
|
||||
const excludedStepCount = feature.excludedPipelineSteps?.length || 0;
|
||||
const totalPipelineSteps = pipelineConfig?.steps?.length || 0;
|
||||
const hasPipelineExclusions =
|
||||
excludedStepCount > 0 && totalPipelineSteps > 0 && feature.status === 'backlog';
|
||||
const allPipelinesExcluded = hasPipelineExclusions && excludedStepCount >= totalPipelineSteps;
|
||||
|
||||
const showBadges =
|
||||
feature.priority ||
|
||||
showManualVerification ||
|
||||
isBlocked ||
|
||||
isJustFinished ||
|
||||
hasPipelineExclusions;
|
||||
|
||||
if (!showBadges) {
|
||||
return null;
|
||||
@@ -227,6 +247,39 @@ export const PriorityBadges = memo(function PriorityBadges({ feature }: Priority
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Pipeline exclusion badge */}
|
||||
{hasPipelineExclusions && (
|
||||
<TooltipProvider delayDuration={200}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
uniformBadgeClass,
|
||||
allPipelinesExcluded
|
||||
? 'bg-violet-500/20 border-violet-500/50 text-violet-500'
|
||||
: 'bg-violet-500/10 border-violet-500/30 text-violet-400'
|
||||
)}
|
||||
data-testid={`pipeline-exclusion-badge-${feature.id}`}
|
||||
>
|
||||
<SkipForward className="w-3.5 h-3.5" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs max-w-[250px]">
|
||||
<p className="font-medium mb-1">
|
||||
{allPipelinesExcluded
|
||||
? 'All pipelines skipped'
|
||||
: `${excludedStepCount} of ${totalPipelineSteps} pipeline${totalPipelineSteps !== 1 ? 's' : ''} skipped`}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
{allPipelinesExcluded
|
||||
? 'This feature will skip all custom pipeline steps'
|
||||
: 'Some custom pipeline steps will be skipped for this feature'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -234,7 +234,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
</div>
|
||||
|
||||
{/* Priority and Manual Verification badges */}
|
||||
<PriorityBadges feature={feature} />
|
||||
<PriorityBadges feature={feature} projectPath={currentProject?.path} />
|
||||
|
||||
{/* Card Header */}
|
||||
<CardHeaderSection
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
AncestorContextSection,
|
||||
EnhanceWithAI,
|
||||
EnhancementHistoryButton,
|
||||
PipelineExclusionControls,
|
||||
type BaseHistoryEntry,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
@@ -101,6 +102,7 @@ type FeatureData = {
|
||||
requirePlanApproval: boolean;
|
||||
dependencies?: string[];
|
||||
childDependencies?: string[]; // Feature IDs that should depend on this feature
|
||||
excludedPipelineSteps?: string[]; // Pipeline step IDs to skip for this feature
|
||||
workMode: WorkMode;
|
||||
};
|
||||
|
||||
@@ -118,6 +120,10 @@ interface AddFeatureDialogProps {
|
||||
isMaximized: boolean;
|
||||
parentFeature?: Feature | null;
|
||||
allFeatures?: Feature[];
|
||||
/**
|
||||
* Path to the current project for loading pipeline config.
|
||||
*/
|
||||
projectPath?: string;
|
||||
/**
|
||||
* When a non-main worktree is selected in the board header, this will be set to that worktree's branch.
|
||||
* When set, the dialog will default to 'custom' work mode with this branch pre-filled.
|
||||
@@ -151,6 +157,7 @@ export function AddFeatureDialog({
|
||||
isMaximized,
|
||||
parentFeature = null,
|
||||
allFeatures = [],
|
||||
projectPath,
|
||||
selectedNonMainWorktreeBranch,
|
||||
forceCurrentBranchMode,
|
||||
}: AddFeatureDialogProps) {
|
||||
@@ -194,6 +201,9 @@ export function AddFeatureDialog({
|
||||
const [parentDependencies, setParentDependencies] = useState<string[]>([]);
|
||||
const [childDependencies, setChildDependencies] = useState<string[]>([]);
|
||||
|
||||
// Pipeline exclusion state
|
||||
const [excludedPipelineSteps, setExcludedPipelineSteps] = useState<string[]>([]);
|
||||
|
||||
// Get defaults from store
|
||||
const { defaultPlanningMode, defaultRequirePlanApproval, useWorktrees, defaultFeatureModel } =
|
||||
useAppStore();
|
||||
@@ -234,6 +244,9 @@ export function AddFeatureDialog({
|
||||
// Reset dependency selections
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
|
||||
// Reset pipeline exclusions (all pipelines enabled by default)
|
||||
setExcludedPipelineSteps([]);
|
||||
}
|
||||
}, [
|
||||
open,
|
||||
@@ -328,6 +341,7 @@ export function AddFeatureDialog({
|
||||
requirePlanApproval,
|
||||
dependencies: finalDependencies,
|
||||
childDependencies: childDependencies.length > 0 ? childDependencies : undefined,
|
||||
excludedPipelineSteps: excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined,
|
||||
workMode,
|
||||
};
|
||||
};
|
||||
@@ -354,6 +368,7 @@ export function AddFeatureDialog({
|
||||
setDescriptionHistory([]);
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
setExcludedPipelineSteps([]);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
@@ -696,6 +711,16 @@ export function AddFeatureDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pipeline Exclusion Controls */}
|
||||
<div className="pt-2">
|
||||
<PipelineExclusionControls
|
||||
projectPath={projectPath}
|
||||
excludedPipelineSteps={excludedPipelineSteps}
|
||||
onExcludedStepsChange={setExcludedPipelineSteps}
|
||||
testIdPrefix="add-feature-pipeline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
PlanningModeSelect,
|
||||
EnhanceWithAI,
|
||||
EnhancementHistoryButton,
|
||||
PipelineExclusionControls,
|
||||
type EnhancementMode,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
@@ -67,6 +68,7 @@ interface EditFeatureDialogProps {
|
||||
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,
|
||||
@@ -78,6 +80,7 @@ interface EditFeatureDialogProps {
|
||||
currentBranch?: string;
|
||||
isMaximized: boolean;
|
||||
allFeatures: Feature[];
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
export function EditFeatureDialog({
|
||||
@@ -90,6 +93,7 @@ export function EditFeatureDialog({
|
||||
currentBranch,
|
||||
isMaximized,
|
||||
allFeatures,
|
||||
projectPath,
|
||||
}: EditFeatureDialogProps) {
|
||||
const navigate = useNavigate();
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(feature);
|
||||
@@ -146,6 +150,11 @@ export function EditFeatureDialog({
|
||||
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) {
|
||||
@@ -171,6 +180,8 @@ export function EditFeatureDialog({
|
||||
.map((f) => f.id);
|
||||
setChildDependencies(childDeps);
|
||||
setOriginalChildDependencies(childDeps);
|
||||
// Reset pipeline exclusion state
|
||||
setExcludedPipelineSteps(feature.excludedPipelineSteps ?? []);
|
||||
} else {
|
||||
setEditFeaturePreviewMap(new Map());
|
||||
setDescriptionChangeSource(null);
|
||||
@@ -179,6 +190,7 @@ export function EditFeatureDialog({
|
||||
setParentDependencies([]);
|
||||
setChildDependencies([]);
|
||||
setOriginalChildDependencies([]);
|
||||
setExcludedPipelineSteps([]);
|
||||
}
|
||||
}, [feature, allFeatures]);
|
||||
|
||||
@@ -232,6 +244,7 @@ export function EditFeatureDialog({
|
||||
workMode,
|
||||
dependencies: parentDependencies,
|
||||
childDependencies: childDepsChanged ? childDependencies : undefined,
|
||||
excludedPipelineSteps: excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined,
|
||||
};
|
||||
|
||||
// Determine if description changed and what source to use
|
||||
@@ -618,6 +631,16 @@ export function EditFeatureDialog({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pipeline Exclusion Controls */}
|
||||
<div className="pt-2">
|
||||
<PipelineExclusionControls
|
||||
projectPath={projectPath}
|
||||
excludedPipelineSteps={excludedPipelineSteps}
|
||||
onExcludedStepsChange={setExcludedPipelineSteps}
|
||||
testIdPrefix="edit-feature-pipeline"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,13 @@ import { Label } from '@/components/ui/label';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { modelSupportsThinking } from '@/lib/utils';
|
||||
import { Feature, ModelAlias, ThinkingLevel, PlanningMode } from '@/store/app-store';
|
||||
import { TestingTabContent, PrioritySelect, PlanningModeSelect, WorkModeSelector } from '../shared';
|
||||
import {
|
||||
TestingTabContent,
|
||||
PrioritySelect,
|
||||
PlanningModeSelect,
|
||||
WorkModeSelector,
|
||||
PipelineExclusionControls,
|
||||
} from '../shared';
|
||||
import type { WorkMode } from '../shared';
|
||||
import { PhaseModelSelector } from '@/components/views/settings-view/model-defaults/phase-model-selector';
|
||||
import { isCursorModel, isClaudeModel, type PhaseModelEntry } from '@automaker/types';
|
||||
@@ -28,6 +34,7 @@ interface MassEditDialogProps {
|
||||
branchSuggestions: string[];
|
||||
branchCardCounts?: Record<string, number>;
|
||||
currentBranch?: string;
|
||||
projectPath?: string;
|
||||
}
|
||||
|
||||
interface ApplyState {
|
||||
@@ -38,11 +45,13 @@ interface ApplyState {
|
||||
priority: boolean;
|
||||
skipTests: boolean;
|
||||
branchName: boolean;
|
||||
excludedPipelineSteps: boolean;
|
||||
}
|
||||
|
||||
function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
if (features.length === 0) return {};
|
||||
const first = features[0];
|
||||
const firstExcludedSteps = JSON.stringify(first.excludedPipelineSteps || []);
|
||||
return {
|
||||
model: !features.every((f) => f.model === first.model),
|
||||
thinkingLevel: !features.every((f) => f.thinkingLevel === first.thinkingLevel),
|
||||
@@ -53,6 +62,9 @@ function getMixedValues(features: Feature[]): Record<string, boolean> {
|
||||
priority: !features.every((f) => f.priority === first.priority),
|
||||
skipTests: !features.every((f) => f.skipTests === first.skipTests),
|
||||
branchName: !features.every((f) => f.branchName === first.branchName),
|
||||
excludedPipelineSteps: !features.every(
|
||||
(f) => JSON.stringify(f.excludedPipelineSteps || []) === firstExcludedSteps
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,6 +123,7 @@ export function MassEditDialog({
|
||||
branchSuggestions,
|
||||
branchCardCounts,
|
||||
currentBranch,
|
||||
projectPath,
|
||||
}: MassEditDialogProps) {
|
||||
const [isApplying, setIsApplying] = useState(false);
|
||||
|
||||
@@ -123,6 +136,7 @@ export function MassEditDialog({
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
excludedPipelineSteps: false,
|
||||
});
|
||||
|
||||
// Field values
|
||||
@@ -145,6 +159,11 @@ export function MassEditDialog({
|
||||
return getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
});
|
||||
|
||||
// Pipeline exclusion state
|
||||
const [excludedPipelineSteps, setExcludedPipelineSteps] = useState<string[]>(() => {
|
||||
return getInitialValue(selectedFeatures, 'excludedPipelineSteps', []) as string[];
|
||||
});
|
||||
|
||||
// Calculate mixed values
|
||||
const mixedValues = useMemo(() => getMixedValues(selectedFeatures), [selectedFeatures]);
|
||||
|
||||
@@ -159,6 +178,7 @@ export function MassEditDialog({
|
||||
priority: false,
|
||||
skipTests: false,
|
||||
branchName: false,
|
||||
excludedPipelineSteps: false,
|
||||
});
|
||||
setModel(getInitialValue(selectedFeatures, 'model', 'claude-sonnet') as ModelAlias);
|
||||
setThinkingLevel(getInitialValue(selectedFeatures, 'thinkingLevel', 'none') as ThinkingLevel);
|
||||
@@ -170,6 +190,10 @@ export function MassEditDialog({
|
||||
const initialBranchName = getInitialValue(selectedFeatures, 'branchName', '') as string;
|
||||
setBranchName(initialBranchName);
|
||||
setWorkMode(initialBranchName ? 'custom' : 'current');
|
||||
// Reset pipeline exclusions
|
||||
setExcludedPipelineSteps(
|
||||
getInitialValue(selectedFeatures, 'excludedPipelineSteps', []) as string[]
|
||||
);
|
||||
}
|
||||
}, [open, selectedFeatures]);
|
||||
|
||||
@@ -188,6 +212,10 @@ export function MassEditDialog({
|
||||
// For 'custom' mode, use the specified branch name
|
||||
updates.branchName = workMode === 'custom' ? branchName : '';
|
||||
}
|
||||
if (applyState.excludedPipelineSteps) {
|
||||
updates.excludedPipelineSteps =
|
||||
excludedPipelineSteps.length > 0 ? excludedPipelineSteps : undefined;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
onClose();
|
||||
@@ -350,6 +378,23 @@ export function MassEditDialog({
|
||||
testIdPrefix="mass-edit-work-mode"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
|
||||
{/* Pipeline Exclusion */}
|
||||
<FieldWrapper
|
||||
label="Pipeline Steps"
|
||||
isMixed={mixedValues.excludedPipelineSteps}
|
||||
willApply={applyState.excludedPipelineSteps}
|
||||
onApplyChange={(apply) =>
|
||||
setApplyState((prev) => ({ ...prev, excludedPipelineSteps: apply }))
|
||||
}
|
||||
>
|
||||
<PipelineExclusionControls
|
||||
projectPath={projectPath}
|
||||
excludedPipelineSteps={excludedPipelineSteps}
|
||||
onExcludedStepsChange={setExcludedPipelineSteps}
|
||||
testIdPrefix="mass-edit-pipeline"
|
||||
/>
|
||||
</FieldWrapper>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -11,3 +11,4 @@ export * from './planning-mode-select';
|
||||
export * from './ancestor-context-section';
|
||||
export * from './work-mode-selector';
|
||||
export * from './enhancement';
|
||||
export * from './pipeline-exclusion-controls';
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { GitBranch, Workflow } from 'lucide-react';
|
||||
import { usePipelineConfig } from '@/hooks/queries/use-pipeline';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface PipelineExclusionControlsProps {
|
||||
projectPath: string | undefined;
|
||||
excludedPipelineSteps: string[];
|
||||
onExcludedStepsChange: (excludedSteps: string[]) => void;
|
||||
testIdPrefix?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for selecting which custom pipeline steps should be excluded for a feature.
|
||||
* Each pipeline step is shown as a toggleable switch, defaulting to enabled (included).
|
||||
* Disabling a step adds it to the exclusion list.
|
||||
*/
|
||||
export function PipelineExclusionControls({
|
||||
projectPath,
|
||||
excludedPipelineSteps,
|
||||
onExcludedStepsChange,
|
||||
testIdPrefix = 'pipeline-exclusion',
|
||||
disabled = false,
|
||||
}: PipelineExclusionControlsProps) {
|
||||
const { data: pipelineConfig, isLoading } = usePipelineConfig(projectPath);
|
||||
|
||||
// Sort steps by order
|
||||
const sortedSteps = [...(pipelineConfig?.steps || [])].sort((a, b) => a.order - b.order);
|
||||
|
||||
// If no pipeline steps exist or loading, don't render anything
|
||||
if (isLoading || sortedSteps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const toggleStep = (stepId: string) => {
|
||||
const isCurrentlyExcluded = excludedPipelineSteps.includes(stepId);
|
||||
if (isCurrentlyExcluded) {
|
||||
// Remove from exclusions (enable the step)
|
||||
onExcludedStepsChange(excludedPipelineSteps.filter((id) => id !== stepId));
|
||||
} else {
|
||||
// Add to exclusions (disable the step)
|
||||
onExcludedStepsChange([...excludedPipelineSteps, stepId]);
|
||||
}
|
||||
};
|
||||
|
||||
const allExcluded = sortedSteps.every((step) => excludedPipelineSteps.includes(step.id));
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Workflow className="w-4 h-4 text-muted-foreground" />
|
||||
<Label className="text-sm font-medium">Custom Pipeline Steps</Label>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{sortedSteps.map((step) => {
|
||||
const isIncluded = !excludedPipelineSteps.includes(step.id);
|
||||
return (
|
||||
<div
|
||||
key={step.id}
|
||||
className={cn(
|
||||
'flex items-center justify-between gap-3 px-3 py-2 rounded-md border',
|
||||
isIncluded
|
||||
? 'border-border/50 bg-muted/30'
|
||||
: 'border-border/30 bg-muted/10 opacity-60'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full flex-shrink-0',
|
||||
step.colorClass || 'bg-gray-400'
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: step.colorClass?.startsWith('#') ? step.colorClass : undefined,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
'text-sm truncate',
|
||||
isIncluded ? 'text-foreground' : 'text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{step.name}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isIncluded}
|
||||
onCheckedChange={() => toggleStep(step.id)}
|
||||
disabled={disabled}
|
||||
data-testid={`${testIdPrefix}-step-${step.id}`}
|
||||
aria-label={`${isIncluded ? 'Disable' : 'Enable'} ${step.name} pipeline step`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{allExcluded && (
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1.5">
|
||||
<GitBranch className="w-3.5 h-3.5" />
|
||||
All pipeline steps disabled. Feature will skip directly to verification.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enabled steps will run after implementation. Disable steps to skip them for this feature.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -392,6 +392,7 @@ export function GraphViewPage() {
|
||||
currentBranch={currentWorktreeBranch || undefined}
|
||||
isMaximized={false}
|
||||
allFeatures={hookFeatures}
|
||||
projectPath={currentProject?.path}
|
||||
/>
|
||||
|
||||
{/* Add Feature Dialog (for spawning) */}
|
||||
@@ -414,6 +415,7 @@ export function GraphViewPage() {
|
||||
isMaximized={false}
|
||||
parentFeature={spawnParentFeature}
|
||||
allFeatures={hookFeatures}
|
||||
projectPath={currentProject?.path}
|
||||
// When setting is enabled and a non-main worktree is selected, pass its branch to default to 'custom' work mode
|
||||
selectedNonMainWorktreeBranch={
|
||||
addFeatureUseSelectedWorktreeBranch && currentWorktreePath !== null
|
||||
|
||||
Reference in New Issue
Block a user