mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
Feat: Add ability to duplicate a feature and duplicate as a child
This commit is contained in:
@@ -590,6 +590,7 @@ export function BoardView() {
|
||||
handleForceStopFeature,
|
||||
handleStartNextFeatures,
|
||||
handleArchiveAllVerified,
|
||||
handleDuplicateFeature,
|
||||
} = useBoardActions({
|
||||
currentProject,
|
||||
features: hookFeatures,
|
||||
@@ -1465,6 +1466,8 @@ export function BoardView() {
|
||||
setSpawnParentFeature(feature);
|
||||
setShowAddDialog(true);
|
||||
},
|
||||
onDuplicate: (feature) => handleDuplicateFeature(feature, false),
|
||||
onDuplicateAsChild: (feature) => handleDuplicateFeature(feature, true),
|
||||
}}
|
||||
runningAutoTasks={runningAutoTasks}
|
||||
pipelineConfig={pipelineConfig}
|
||||
@@ -1504,6 +1507,7 @@ export function BoardView() {
|
||||
setSpawnParentFeature(feature);
|
||||
setShowAddDialog(true);
|
||||
}}
|
||||
onDuplicate={handleDuplicateFeature}
|
||||
featuresWithContext={featuresWithContext}
|
||||
runningAutoTasks={runningAutoTasks}
|
||||
onArchiveAllVerified={() => setShowArchiveAllVerifiedDialog(true)}
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
@@ -19,6 +22,7 @@ import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
GitFork,
|
||||
Copy,
|
||||
} from 'lucide-react';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { CountUpTimer } from '@/components/ui/count-up-timer';
|
||||
@@ -35,6 +39,8 @@ interface CardHeaderProps {
|
||||
onDelete: () => void;
|
||||
onViewOutput?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
onDuplicateAsChild?: () => void;
|
||||
}
|
||||
|
||||
export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
@@ -46,6 +52,8 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
onDelete,
|
||||
onViewOutput,
|
||||
onSpawnTask,
|
||||
onDuplicate,
|
||||
onDuplicateAsChild,
|
||||
}: CardHeaderProps) {
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = 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" />
|
||||
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>
|
||||
)}
|
||||
{/* Model info in dropdown */}
|
||||
{(() => {
|
||||
const ProviderIcon = getProviderIconForModel(feature.model);
|
||||
@@ -129,20 +170,6 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
{/* Backlog header */}
|
||||
{!isCurrentAutoTask && !isSelectionMode && feature.status === 'backlog' && (
|
||||
<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
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -153,6 +180,66 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</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>
|
||||
)}
|
||||
|
||||
@@ -178,22 +265,6 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</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 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -225,6 +296,70 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
@@ -293,6 +428,39 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
<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>
|
||||
)}
|
||||
{/* Model info in dropdown */}
|
||||
{(() => {
|
||||
const ProviderIcon = getProviderIconForModel(feature.model);
|
||||
|
||||
@@ -52,6 +52,8 @@ interface KanbanCardProps {
|
||||
onViewPlan?: () => void;
|
||||
onApprovePlan?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
onDuplicateAsChild?: () => void;
|
||||
hasContext?: boolean;
|
||||
isCurrentAutoTask?: boolean;
|
||||
shortcutKey?: string;
|
||||
@@ -86,6 +88,8 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onViewPlan,
|
||||
onApprovePlan,
|
||||
onSpawnTask,
|
||||
onDuplicate,
|
||||
onDuplicateAsChild,
|
||||
hasContext,
|
||||
isCurrentAutoTask,
|
||||
shortcutKey,
|
||||
@@ -249,6 +253,8 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onDelete={onDelete}
|
||||
onViewOutput={onViewOutput}
|
||||
onSpawnTask={onSpawnTask}
|
||||
onDuplicate={onDuplicate}
|
||||
onDuplicateAsChild={onDuplicateAsChild}
|
||||
/>
|
||||
|
||||
<CardContent className="px-3 pt-0 pb-0">
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface ListViewActionHandlers {
|
||||
onViewPlan?: (feature: Feature) => void;
|
||||
onApprovePlan?: (feature: Feature) => void;
|
||||
onSpawnTask?: (feature: Feature) => void;
|
||||
onDuplicate?: (feature: Feature) => void;
|
||||
onDuplicateAsChild?: (feature: Feature) => void;
|
||||
}
|
||||
|
||||
export interface ListViewProps {
|
||||
@@ -313,6 +315,18 @@ export const ListView = memo(function ListView({
|
||||
if (f) actionHandlers.onSpawnTask?.(f);
|
||||
}
|
||||
: 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]
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
GitBranch,
|
||||
GitFork,
|
||||
ExternalLink,
|
||||
Copy,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -22,6 +23,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import type { Feature } from '@/store/app-store';
|
||||
@@ -43,6 +47,8 @@ export interface RowActionHandlers {
|
||||
onViewPlan?: () => void;
|
||||
onApprovePlan?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
onDuplicate?: () => void;
|
||||
onDuplicateAsChild?: () => void;
|
||||
}
|
||||
|
||||
export interface RowActionsProps {
|
||||
@@ -405,6 +411,31 @@ export const RowActions = memo(function RowActions({
|
||||
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 />
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
@@ -457,6 +488,31 @@ export const RowActions = memo(function RowActions({
|
||||
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
|
||||
icon={Trash2}
|
||||
label="Delete"
|
||||
@@ -503,6 +559,31 @@ export const RowActions = memo(function RowActions({
|
||||
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
|
||||
icon={Trash2}
|
||||
label="Delete"
|
||||
@@ -554,6 +635,31 @@ export const RowActions = memo(function RowActions({
|
||||
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
|
||||
icon={Trash2}
|
||||
label="Delete"
|
||||
@@ -581,6 +687,31 @@ export const RowActions = memo(function RowActions({
|
||||
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 />
|
||||
<MenuItem
|
||||
icon={Trash2}
|
||||
@@ -615,6 +746,8 @@ export function createRowActionHandlers(
|
||||
viewPlan?: (id: string) => void;
|
||||
approvePlan?: (id: string) => void;
|
||||
spawnTask?: (id: string) => void;
|
||||
duplicate?: (id: string) => void;
|
||||
duplicateAsChild?: (id: string) => void;
|
||||
}
|
||||
): RowActionHandlers {
|
||||
return {
|
||||
@@ -631,5 +764,9 @@ export function createRowActionHandlers(
|
||||
onViewPlan: actions.viewPlan ? () => actions.viewPlan!(featureId) : undefined,
|
||||
onApprovePlan: actions.approvePlan ? () => actions.approvePlan!(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]);
|
||||
|
||||
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 {
|
||||
handleAddFeature,
|
||||
handleUpdateFeature,
|
||||
@@ -1103,5 +1123,6 @@ export function useBoardActions({
|
||||
handleForceStopFeature,
|
||||
handleStartNextFeatures,
|
||||
handleArchiveAllVerified,
|
||||
handleDuplicateFeature,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ interface KanbanBoardProps {
|
||||
onViewPlan: (feature: Feature) => void;
|
||||
onApprovePlan: (feature: Feature) => void;
|
||||
onSpawnTask?: (feature: Feature) => void;
|
||||
onDuplicate?: (feature: Feature, asChild: boolean) => void;
|
||||
featuresWithContext: Set<string>;
|
||||
runningAutoTasks: string[];
|
||||
onArchiveAllVerified: () => void;
|
||||
@@ -282,6 +283,7 @@ export function KanbanBoard({
|
||||
onViewPlan,
|
||||
onApprovePlan,
|
||||
onSpawnTask,
|
||||
onDuplicate,
|
||||
featuresWithContext,
|
||||
runningAutoTasks,
|
||||
onArchiveAllVerified,
|
||||
@@ -569,6 +571,8 @@ export function KanbanBoard({
|
||||
onViewPlan={() => onViewPlan(feature)}
|
||||
onApprovePlan={() => onApprovePlan(feature)}
|
||||
onSpawnTask={() => onSpawnTask?.(feature)}
|
||||
onDuplicate={() => onDuplicate?.(feature, false)}
|
||||
onDuplicateAsChild={() => onDuplicate?.(feature, true)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||
shortcutKey={shortcutKey}
|
||||
@@ -611,6 +615,8 @@ export function KanbanBoard({
|
||||
onViewPlan={() => onViewPlan(feature)}
|
||||
onApprovePlan={() => onApprovePlan(feature)}
|
||||
onSpawnTask={() => onSpawnTask?.(feature)}
|
||||
onDuplicate={() => onDuplicate?.(feature, false)}
|
||||
onDuplicateAsChild={() => onDuplicate?.(feature, true)}
|
||||
hasContext={featuresWithContext.has(feature.id)}
|
||||
isCurrentAutoTask={runningAutoTasks.includes(feature.id)}
|
||||
shortcutKey={shortcutKey}
|
||||
|
||||
Reference in New Issue
Block a user