Merge pull request #413 from AutoMaker-Org/feat/bulk-delete-features-in-backlog

feat: bulk delete in backlog mass select
This commit is contained in:
Shirone
2026-01-11 09:19:15 +00:00
committed by GitHub
5 changed files with 223 additions and 43 deletions

View File

@@ -10,6 +10,7 @@ import { createGetHandler } from './routes/get.js';
import { createCreateHandler } from './routes/create.js'; import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from './routes/update.js'; import { createUpdateHandler } from './routes/update.js';
import { createBulkUpdateHandler } from './routes/bulk-update.js'; import { createBulkUpdateHandler } from './routes/bulk-update.js';
import { createBulkDeleteHandler } from './routes/bulk-delete.js';
import { createDeleteHandler } from './routes/delete.js'; import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js'; import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js'; import { createGenerateTitleHandler } from './routes/generate-title.js';
@@ -26,6 +27,11 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
validatePathParams('projectPath'), validatePathParams('projectPath'),
createBulkUpdateHandler(featureLoader) createBulkUpdateHandler(featureLoader)
); );
router.post(
'/bulk-delete',
validatePathParams('projectPath'),
createBulkDeleteHandler(featureLoader)
);
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader)); router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader)); router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/raw-output', createRawOutputHandler(featureLoader)); router.post('/raw-output', createRawOutputHandler(featureLoader));

View File

@@ -0,0 +1,62 @@
/**
* POST /bulk-delete endpoint - Delete multiple features at once
*/
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
interface BulkDeleteRequest {
projectPath: string;
featureIds: string[];
}
interface BulkDeleteResult {
featureId: string;
success: boolean;
error?: string;
}
export function createBulkDeleteHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureIds } = req.body as BulkDeleteRequest;
if (!projectPath || !featureIds || !Array.isArray(featureIds) || featureIds.length === 0) {
res.status(400).json({
success: false,
error: 'projectPath and featureIds (non-empty array) are required',
});
return;
}
const results: BulkDeleteResult[] = [];
for (const featureId of featureIds) {
try {
const success = await featureLoader.delete(projectPath, featureId);
results.push({ featureId, success });
} catch (error) {
results.push({
featureId,
success: false,
error: getErrorMessage(error),
});
}
}
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;
res.json({
success: failureCount === 0,
deletedCount: successCount,
failedCount: failureCount,
results,
});
} catch (error) {
logError(error, 'Bulk delete features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -505,6 +505,34 @@ export function BoardView() {
[currentProject, selectedFeatureIds, updateFeature, exitSelectionMode] [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]
); );
// Handler for bulk deleting multiple features
const handleBulkDelete = useCallback(async () => {
if (!currentProject || selectedFeatureIds.size === 0) return;
try {
const api = getHttpApiClient();
const featureIds = Array.from(selectedFeatureIds);
const result = await api.features.bulkDelete(currentProject.path, featureIds);
if (result.success) {
// Delete from local state
featureIds.forEach((featureId) => {
persistFeatureDelete(featureId);
});
toast.success(`Deleted ${result.deletedCount} features`);
exitSelectionMode();
loadFeatures();
} else {
toast.error('Failed to delete some features', {
description: `${result.failedCount} features failed to delete`,
});
}
} catch (error) {
logger.error('Bulk delete failed:', error);
toast.error('Failed to delete features');
}
}, [currentProject, selectedFeatureIds, persistFeatureDelete, exitSelectionMode, loadFeatures]);
// Get selected features for mass edit dialog // Get selected features for mass edit dialog
const selectedFeatures = useMemo(() => { const selectedFeatures = useMemo(() => {
return hookFeatures.filter((f) => selectedFeatureIds.has(f.id)); return hookFeatures.filter((f) => selectedFeatureIds.has(f.id));
@@ -1257,6 +1285,7 @@ export function BoardView() {
selectedCount={selectedCount} selectedCount={selectedCount}
totalCount={allSelectableFeatureIds.length} totalCount={allSelectableFeatureIds.length}
onEdit={() => setShowMassEditDialog(true)} onEdit={() => setShowMassEditDialog(true)}
onDelete={handleBulkDelete}
onClear={clearSelection} onClear={clearSelection}
onSelectAll={() => selectAll(allSelectableFeatureIds)} onSelectAll={() => selectAll(allSelectableFeatureIds)}
/> />

View File

@@ -1,11 +1,21 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Pencil, X, CheckSquare } from 'lucide-react'; import { Pencil, X, CheckSquare, Trash2 } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
interface SelectionActionBarProps { interface SelectionActionBarProps {
selectedCount: number; selectedCount: number;
totalCount: number; totalCount: number;
onEdit: () => void; onEdit: () => void;
onDelete: () => void;
onClear: () => void; onClear: () => void;
onSelectAll: () => void; onSelectAll: () => void;
} }
@@ -14,14 +24,27 @@ export function SelectionActionBar({
selectedCount, selectedCount,
totalCount, totalCount,
onEdit, onEdit,
onDelete,
onClear, onClear,
onSelectAll, onSelectAll,
}: SelectionActionBarProps) { }: SelectionActionBarProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
if (selectedCount === 0) return null; if (selectedCount === 0) return null;
const allSelected = selectedCount === totalCount; const allSelected = selectedCount === totalCount;
const handleDeleteClick = () => {
setShowDeleteDialog(true);
};
const handleConfirmDelete = () => {
setShowDeleteDialog(false);
onDelete();
};
return ( return (
<>
<div <div
className={cn( className={cn(
'fixed bottom-6 left-1/2 -translate-x-1/2 z-50', 'fixed bottom-6 left-1/2 -translate-x-1/2 z-50',
@@ -49,6 +72,17 @@ export function SelectionActionBar({
Edit Selected Edit Selected
</Button> </Button>
<Button
variant="outline"
size="sm"
onClick={handleDeleteClick}
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10"
data-testid="selection-delete-button"
>
<Trash2 className="w-4 h-4 mr-1.5" />
Delete
</Button>
{!allSelected && ( {!allSelected && (
<Button <Button
variant="outline" variant="outline"
@@ -74,5 +108,42 @@ export function SelectionActionBar({
</Button> </Button>
</div> </div>
</div> </div>
{/* Delete Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent data-testid="bulk-delete-confirmation-dialog">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<Trash2 className="w-5 h-5" />
Delete Selected Features?
</DialogTitle>
<DialogDescription>
Are you sure you want to permanently delete {selectedCount} feature
{selectedCount !== 1 ? 's' : ''}?
<span className="block mt-2 text-destructive font-medium">
This action cannot be undone.
</span>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="ghost"
onClick={() => setShowDeleteDialog(false)}
data-testid="cancel-bulk-delete-button"
>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleConfirmDelete}
data-testid="confirm-bulk-delete-button"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
); );
} }

View File

@@ -1438,6 +1438,16 @@ export class HttpApiClient implements ElectronAPI {
features?: Feature[]; features?: Feature[];
error?: string; error?: string;
}>; }>;
bulkDelete: (
projectPath: string,
featureIds: string[]
) => Promise<{
success: boolean;
deletedCount?: number;
failedCount?: number;
results?: Array<{ featureId: string; success: boolean; error?: string }>;
error?: string;
}>;
} = { } = {
getAll: (projectPath: string) => this.post('/api/features/list', { projectPath }), getAll: (projectPath: string) => this.post('/api/features/list', { projectPath }),
get: (projectPath: string, featureId: string) => get: (projectPath: string, featureId: string) =>
@@ -1466,6 +1476,8 @@ export class HttpApiClient implements ElectronAPI {
this.post('/api/features/generate-title', { description }), this.post('/api/features/generate-title', { description }),
bulkUpdate: (projectPath: string, featureIds: string[], updates: Partial<Feature>) => bulkUpdate: (projectPath: string, featureIds: string[], updates: Partial<Feature>) =>
this.post('/api/features/bulk-update', { projectPath, featureIds, updates }), this.post('/api/features/bulk-update', { projectPath, featureIds, updates }),
bulkDelete: (projectPath: string, featureIds: string[]) =>
this.post('/api/features/bulk-delete', { projectPath, featureIds }),
}; };
// Auto Mode API // Auto Mode API