mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-20 11:03:08 +00:00
Feat: Add ability to duplicate a feature and duplicate as a child
This commit is contained in:
@@ -24,19 +24,6 @@ export function createCreateHandler(featureLoader: FeatureLoader, events?: Event
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate title if title is provided
|
|
||||||
if (feature.title && feature.title.trim()) {
|
|
||||||
const duplicate = await featureLoader.findDuplicateTitle(projectPath, feature.title);
|
|
||||||
if (duplicate) {
|
|
||||||
res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
error: `A feature with title "${feature.title}" already exists`,
|
|
||||||
duplicateFeatureId: duplicate.id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await featureLoader.create(projectPath, feature);
|
const created = await featureLoader.create(projectPath, feature);
|
||||||
|
|
||||||
// Emit feature_created event for hooks
|
// Emit feature_created event for hooks
|
||||||
|
|||||||
@@ -40,23 +40,6 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate title if title is being updated
|
|
||||||
if (updates.title && updates.title.trim()) {
|
|
||||||
const duplicate = await featureLoader.findDuplicateTitle(
|
|
||||||
projectPath,
|
|
||||||
updates.title,
|
|
||||||
featureId // Exclude the current feature from duplicate check
|
|
||||||
);
|
|
||||||
if (duplicate) {
|
|
||||||
res.status(409).json({
|
|
||||||
success: false,
|
|
||||||
error: `A feature with title "${updates.title}" already exists`,
|
|
||||||
duplicateFeatureId: duplicate.id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current feature to detect status changes
|
// Get the current feature to detect status changes
|
||||||
const currentFeature = await featureLoader.get(projectPath, featureId);
|
const currentFeature = await featureLoader.get(projectPath, featureId);
|
||||||
const previousStatus = currentFeature?.status as FeatureStatus | undefined;
|
const previousStatus = currentFeature?.status as FeatureStatus | undefined;
|
||||||
|
|||||||
@@ -590,6 +590,7 @@ export function BoardView() {
|
|||||||
handleForceStopFeature,
|
handleForceStopFeature,
|
||||||
handleStartNextFeatures,
|
handleStartNextFeatures,
|
||||||
handleArchiveAllVerified,
|
handleArchiveAllVerified,
|
||||||
|
handleDuplicateFeature,
|
||||||
} = useBoardActions({
|
} = useBoardActions({
|
||||||
currentProject,
|
currentProject,
|
||||||
features: hookFeatures,
|
features: hookFeatures,
|
||||||
@@ -1465,6 +1466,8 @@ export function BoardView() {
|
|||||||
setSpawnParentFeature(feature);
|
setSpawnParentFeature(feature);
|
||||||
setShowAddDialog(true);
|
setShowAddDialog(true);
|
||||||
},
|
},
|
||||||
|
onDuplicate: (feature) => handleDuplicateFeature(feature, false),
|
||||||
|
onDuplicateAsChild: (feature) => handleDuplicateFeature(feature, true),
|
||||||
}}
|
}}
|
||||||
runningAutoTasks={runningAutoTasks}
|
runningAutoTasks={runningAutoTasks}
|
||||||
pipelineConfig={pipelineConfig}
|
pipelineConfig={pipelineConfig}
|
||||||
@@ -1504,6 +1507,7 @@ export function BoardView() {
|
|||||||
setSpawnParentFeature(feature);
|
setSpawnParentFeature(feature);
|
||||||
setShowAddDialog(true);
|
setShowAddDialog(true);
|
||||||
}}
|
}}
|
||||||
|
onDuplicate={handleDuplicateFeature}
|
||||||
featuresWithContext={featuresWithContext}
|
featuresWithContext={featuresWithContext}
|
||||||
runningAutoTasks={runningAutoTasks}
|
runningAutoTasks={runningAutoTasks}
|
||||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +22,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
GitFork,
|
GitFork,
|
||||||
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { CountUpTimer } from '@/components/ui/count-up-timer';
|
import { CountUpTimer } from '@/components/ui/count-up-timer';
|
||||||
@@ -35,6 +39,8 @@ interface CardHeaderProps {
|
|||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onViewOutput?: () => void;
|
onViewOutput?: () => void;
|
||||||
onSpawnTask?: () => void;
|
onSpawnTask?: () => void;
|
||||||
|
onDuplicate?: () => void;
|
||||||
|
onDuplicateAsChild?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardHeaderSection = memo(function CardHeaderSection({
|
export const CardHeaderSection = memo(function CardHeaderSection({
|
||||||
@@ -46,6 +52,8 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onViewOutput,
|
onViewOutput,
|
||||||
onSpawnTask,
|
onSpawnTask,
|
||||||
|
onDuplicate,
|
||||||
|
onDuplicateAsChild,
|
||||||
}: CardHeaderProps) {
|
}: CardHeaderProps) {
|
||||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
@@ -109,6 +117,39 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
<GitFork className="w-3 h-3 mr-2" />
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
Spawn Sub-Task
|
Spawn Sub-Task
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate();
|
||||||
|
}}
|
||||||
|
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicateAsChild();
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate as Child
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
{/* Model info in dropdown */}
|
{/* Model info in dropdown */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const ProviderIcon = getProviderIconForModel(feature.model);
|
const ProviderIcon = getProviderIconForModel(feature.model);
|
||||||
@@ -129,20 +170,6 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
{/* Backlog header */}
|
{/* Backlog header */}
|
||||||
{!isCurrentAutoTask && !isSelectionMode && feature.status === 'backlog' && (
|
{!isCurrentAutoTask && !isSelectionMode && feature.status === 'backlog' && (
|
||||||
<div className="absolute top-2 right-2 flex items-center gap-1">
|
<div className="absolute top-2 right-2 flex items-center gap-1">
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSpawnTask?.();
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
data-testid={`spawn-backlog-${feature.id}`}
|
|
||||||
title="Spawn Sub-Task"
|
|
||||||
>
|
|
||||||
<GitFork className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -153,6 +180,66 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-muted/80 rounded-md"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`menu-backlog-${feature.id}`}
|
||||||
|
>
|
||||||
|
<MoreVertical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSpawnTask?.();
|
||||||
|
}}
|
||||||
|
data-testid={`spawn-backlog-${feature.id}`}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Spawn Sub-Task
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate();
|
||||||
|
}}
|
||||||
|
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicateAsChild();
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate as Child
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -178,22 +265,6 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 hover:bg-white/10 text-muted-foreground hover:text-foreground"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSpawnTask?.();
|
|
||||||
}}
|
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
|
||||||
data-testid={`spawn-${
|
|
||||||
feature.status === 'waiting_approval' ? 'waiting' : 'verified'
|
|
||||||
}-${feature.id}`}
|
|
||||||
title="Spawn Sub-Task"
|
|
||||||
>
|
|
||||||
<GitFork className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
{onViewOutput && (
|
{onViewOutput && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -225,6 +296,70 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 w-6 p-0 hover:bg-muted/80 rounded-md"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
data-testid={`menu-${
|
||||||
|
feature.status === 'waiting_approval' ? 'waiting' : 'verified'
|
||||||
|
}-${feature.id}`}
|
||||||
|
>
|
||||||
|
<MoreVertical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-40">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSpawnTask?.();
|
||||||
|
}}
|
||||||
|
data-testid={`spawn-${
|
||||||
|
feature.status === 'waiting_approval' ? 'waiting' : 'verified'
|
||||||
|
}-${feature.id}`}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Spawn Sub-Task
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate();
|
||||||
|
}}
|
||||||
|
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicateAsChild();
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate as Child
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -293,6 +428,39 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
|||||||
<GitFork className="w-3 h-3 mr-2" />
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
Spawn Sub-Task
|
Spawn Sub-Task
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
{onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicate();
|
||||||
|
}}
|
||||||
|
className="text-xs flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="text-xs px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDuplicateAsChild();
|
||||||
|
}}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
<GitFork className="w-3 h-3 mr-2" />
|
||||||
|
Duplicate as Child
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
{/* Model info in dropdown */}
|
{/* Model info in dropdown */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const ProviderIcon = getProviderIconForModel(feature.model);
|
const ProviderIcon = getProviderIconForModel(feature.model);
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ interface KanbanCardProps {
|
|||||||
onViewPlan?: () => void;
|
onViewPlan?: () => void;
|
||||||
onApprovePlan?: () => void;
|
onApprovePlan?: () => void;
|
||||||
onSpawnTask?: () => void;
|
onSpawnTask?: () => void;
|
||||||
|
onDuplicate?: () => void;
|
||||||
|
onDuplicateAsChild?: () => void;
|
||||||
hasContext?: boolean;
|
hasContext?: boolean;
|
||||||
isCurrentAutoTask?: boolean;
|
isCurrentAutoTask?: boolean;
|
||||||
shortcutKey?: string;
|
shortcutKey?: string;
|
||||||
@@ -86,6 +88,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
onViewPlan,
|
onViewPlan,
|
||||||
onApprovePlan,
|
onApprovePlan,
|
||||||
onSpawnTask,
|
onSpawnTask,
|
||||||
|
onDuplicate,
|
||||||
|
onDuplicateAsChild,
|
||||||
hasContext,
|
hasContext,
|
||||||
isCurrentAutoTask,
|
isCurrentAutoTask,
|
||||||
shortcutKey,
|
shortcutKey,
|
||||||
@@ -249,6 +253,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onViewOutput={onViewOutput}
|
onViewOutput={onViewOutput}
|
||||||
onSpawnTask={onSpawnTask}
|
onSpawnTask={onSpawnTask}
|
||||||
|
onDuplicate={onDuplicate}
|
||||||
|
onDuplicateAsChild={onDuplicateAsChild}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CardContent className="px-3 pt-0 pb-0">
|
<CardContent className="px-3 pt-0 pb-0">
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export interface ListViewActionHandlers {
|
|||||||
onViewPlan?: (feature: Feature) => void;
|
onViewPlan?: (feature: Feature) => void;
|
||||||
onApprovePlan?: (feature: Feature) => void;
|
onApprovePlan?: (feature: Feature) => void;
|
||||||
onSpawnTask?: (feature: Feature) => void;
|
onSpawnTask?: (feature: Feature) => void;
|
||||||
|
onDuplicate?: (feature: Feature) => void;
|
||||||
|
onDuplicateAsChild?: (feature: Feature) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ListViewProps {
|
export interface ListViewProps {
|
||||||
@@ -313,6 +315,18 @@ export const ListView = memo(function ListView({
|
|||||||
if (f) actionHandlers.onSpawnTask?.(f);
|
if (f) actionHandlers.onSpawnTask?.(f);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
duplicate: actionHandlers.onDuplicate
|
||||||
|
? (id) => {
|
||||||
|
const f = allFeatures.find((f) => f.id === id);
|
||||||
|
if (f) actionHandlers.onDuplicate?.(f);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
duplicateAsChild: actionHandlers.onDuplicateAsChild
|
||||||
|
? (id) => {
|
||||||
|
const f = allFeatures.find((f) => f.id === id);
|
||||||
|
if (f) actionHandlers.onDuplicateAsChild?.(f);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[actionHandlers, allFeatures]
|
[actionHandlers, allFeatures]
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
GitFork,
|
GitFork,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Copy,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -22,6 +23,9 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import type { Feature } from '@/store/app-store';
|
import type { Feature } from '@/store/app-store';
|
||||||
@@ -43,6 +47,8 @@ export interface RowActionHandlers {
|
|||||||
onViewPlan?: () => void;
|
onViewPlan?: () => void;
|
||||||
onApprovePlan?: () => void;
|
onApprovePlan?: () => void;
|
||||||
onSpawnTask?: () => void;
|
onSpawnTask?: () => void;
|
||||||
|
onDuplicate?: () => void;
|
||||||
|
onDuplicateAsChild?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RowActionsProps {
|
export interface RowActionsProps {
|
||||||
@@ -405,6 +411,31 @@ export const RowActions = memo(function RowActions({
|
|||||||
onClick={withClose(handlers.onSpawnTask)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={withClose(handlers.onDuplicate)}
|
||||||
|
className="flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Duplicate as Child"
|
||||||
|
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
@@ -457,6 +488,31 @@ export const RowActions = memo(function RowActions({
|
|||||||
onClick={withClose(handlers.onSpawnTask)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={withClose(handlers.onDuplicate)}
|
||||||
|
className="flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Duplicate as Child"
|
||||||
|
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -503,6 +559,31 @@ export const RowActions = memo(function RowActions({
|
|||||||
onClick={withClose(handlers.onSpawnTask)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={withClose(handlers.onDuplicate)}
|
||||||
|
className="flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Duplicate as Child"
|
||||||
|
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -554,6 +635,31 @@ export const RowActions = memo(function RowActions({
|
|||||||
onClick={withClose(handlers.onSpawnTask)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={withClose(handlers.onDuplicate)}
|
||||||
|
className="flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Duplicate as Child"
|
||||||
|
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
label="Delete"
|
label="Delete"
|
||||||
@@ -581,6 +687,31 @@ export const RowActions = memo(function RowActions({
|
|||||||
onClick={withClose(handlers.onSpawnTask)}
|
onClick={withClose(handlers.onSpawnTask)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{handlers.onDuplicate && (
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={withClose(handlers.onDuplicate)}
|
||||||
|
className="flex-1 pr-0 rounded-r-none"
|
||||||
|
>
|
||||||
|
<Copy className="w-4 h-4 mr-2" />
|
||||||
|
Duplicate
|
||||||
|
</DropdownMenuItem>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubTrigger className="px-1 rounded-l-none border-l border-border/30 h-8" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{handlers.onDuplicateAsChild && (
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<MenuItem
|
||||||
|
icon={GitFork}
|
||||||
|
label="Duplicate as Child"
|
||||||
|
onClick={withClose(handlers.onDuplicateAsChild)}
|
||||||
|
/>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenuSub>
|
||||||
|
)}
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={Trash2}
|
icon={Trash2}
|
||||||
@@ -615,6 +746,8 @@ export function createRowActionHandlers(
|
|||||||
viewPlan?: (id: string) => void;
|
viewPlan?: (id: string) => void;
|
||||||
approvePlan?: (id: string) => void;
|
approvePlan?: (id: string) => void;
|
||||||
spawnTask?: (id: string) => void;
|
spawnTask?: (id: string) => void;
|
||||||
|
duplicate?: (id: string) => void;
|
||||||
|
duplicateAsChild?: (id: string) => void;
|
||||||
}
|
}
|
||||||
): RowActionHandlers {
|
): RowActionHandlers {
|
||||||
return {
|
return {
|
||||||
@@ -631,5 +764,9 @@ export function createRowActionHandlers(
|
|||||||
onViewPlan: actions.viewPlan ? () => actions.viewPlan!(featureId) : undefined,
|
onViewPlan: actions.viewPlan ? () => actions.viewPlan!(featureId) : undefined,
|
||||||
onApprovePlan: actions.approvePlan ? () => actions.approvePlan!(featureId) : undefined,
|
onApprovePlan: actions.approvePlan ? () => actions.approvePlan!(featureId) : undefined,
|
||||||
onSpawnTask: actions.spawnTask ? () => actions.spawnTask!(featureId) : undefined,
|
onSpawnTask: actions.spawnTask ? () => actions.spawnTask!(featureId) : undefined,
|
||||||
|
onDuplicate: actions.duplicate ? () => actions.duplicate!(featureId) : undefined,
|
||||||
|
onDuplicateAsChild: actions.duplicateAsChild
|
||||||
|
? () => actions.duplicateAsChild!(featureId)
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1083,6 +1083,26 @@ export function useBoardActions({
|
|||||||
});
|
});
|
||||||
}, [features, runningAutoTasks, autoMode, updateFeature, persistFeatureUpdate]);
|
}, [features, runningAutoTasks, autoMode, updateFeature, persistFeatureUpdate]);
|
||||||
|
|
||||||
|
const handleDuplicateFeature = useCallback(
|
||||||
|
async (feature: Feature, asChild: boolean = false) => {
|
||||||
|
// Copy all feature data, only override id/status (handled by create) and dependencies if as child
|
||||||
|
const { id: _id, status: _status, ...featureData } = feature;
|
||||||
|
const duplicatedFeatureData = {
|
||||||
|
...featureData,
|
||||||
|
// If duplicating as child, set source as dependency; otherwise keep existing
|
||||||
|
...(asChild && { dependencies: [feature.id] }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reuse the existing handleAddFeature logic
|
||||||
|
await handleAddFeature(duplicatedFeatureData);
|
||||||
|
|
||||||
|
toast.success(asChild ? 'Duplicated as child' : 'Feature duplicated', {
|
||||||
|
description: `Created copy of: ${truncateDescription(feature.description || feature.title || '')}`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[handleAddFeature]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleAddFeature,
|
handleAddFeature,
|
||||||
handleUpdateFeature,
|
handleUpdateFeature,
|
||||||
@@ -1103,5 +1123,6 @@ export function useBoardActions({
|
|||||||
handleForceStopFeature,
|
handleForceStopFeature,
|
||||||
handleStartNextFeatures,
|
handleStartNextFeatures,
|
||||||
handleArchiveAllVerified,
|
handleArchiveAllVerified,
|
||||||
|
handleDuplicateFeature,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ interface KanbanBoardProps {
|
|||||||
onViewPlan: (feature: Feature) => void;
|
onViewPlan: (feature: Feature) => void;
|
||||||
onApprovePlan: (feature: Feature) => void;
|
onApprovePlan: (feature: Feature) => void;
|
||||||
onSpawnTask?: (feature: Feature) => void;
|
onSpawnTask?: (feature: Feature) => void;
|
||||||
|
onDuplicate?: (feature: Feature, asChild: boolean) => void;
|
||||||
featuresWithContext: Set<string>;
|
featuresWithContext: Set<string>;
|
||||||
runningAutoTasks: string[];
|
runningAutoTasks: string[];
|
||||||
onArchiveAllVerified: () => void;
|
onArchiveAllVerified: () => void;
|
||||||
@@ -282,6 +283,7 @@ export function KanbanBoard({
|
|||||||
onViewPlan,
|
onViewPlan,
|
||||||
onApprovePlan,
|
onApprovePlan,
|
||||||
onSpawnTask,
|
onSpawnTask,
|
||||||
|
onDuplicate,
|
||||||
featuresWithContext,
|
featuresWithContext,
|
||||||
runningAutoTasks,
|
runningAutoTasks,
|
||||||
onArchiveAllVerified,
|
onArchiveAllVerified,
|
||||||
@@ -569,6 +571,8 @@ export function KanbanBoard({
|
|||||||
onViewPlan={() => onViewPlan(feature)}
|
onViewPlan={() => onViewPlan(feature)}
|
||||||
onApprovePlan={() => onApprovePlan(feature)}
|
onApprovePlan={() => onApprovePlan(feature)}
|
||||||
onSpawnTask={() => onSpawnTask?.(feature)}
|
onSpawnTask={() => onSpawnTask?.(feature)}
|
||||||
|
onDuplicate={() => onDuplicate?.(feature, false)}
|
||||||
|
onDuplicateAsChild={() => onDuplicate?.(feature, true)}
|
||||||
hasContext={featuresWithContext.has(feature.id)}
|
hasContext={featuresWithContext.has(feature.id)}
|
||||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||||
shortcutKey={shortcutKey}
|
shortcutKey={shortcutKey}
|
||||||
@@ -611,6 +615,8 @@ export function KanbanBoard({
|
|||||||
onViewPlan={() => onViewPlan(feature)}
|
onViewPlan={() => onViewPlan(feature)}
|
||||||
onApprovePlan={() => onApprovePlan(feature)}
|
onApprovePlan={() => onApprovePlan(feature)}
|
||||||
onSpawnTask={() => onSpawnTask?.(feature)}
|
onSpawnTask={() => onSpawnTask?.(feature)}
|
||||||
|
onDuplicate={() => onDuplicate?.(feature, false)}
|
||||||
|
onDuplicateAsChild={() => onDuplicate?.(feature, true)}
|
||||||
hasContext={featuresWithContext.has(feature.id)}
|
hasContext={featuresWithContext.has(feature.id)}
|
||||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||||
shortcutKey={shortcutKey}
|
shortcutKey={shortcutKey}
|
||||||
|
|||||||
Reference in New Issue
Block a user