diff --git a/apps/server/src/routes/features/index.ts b/apps/server/src/routes/features/index.ts index 4f62ee17..e0435f35 100644 --- a/apps/server/src/routes/features/index.ts +++ b/apps/server/src/routes/features/index.ts @@ -10,6 +10,7 @@ import { createGetHandler } from './routes/get.js'; import { createCreateHandler } from './routes/create.js'; import { createUpdateHandler } from './routes/update.js'; import { createBulkUpdateHandler } from './routes/bulk-update.js'; +import { createBulkDeleteHandler } from './routes/bulk-delete.js'; import { createDeleteHandler } from './routes/delete.js'; import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js'; import { createGenerateTitleHandler } from './routes/generate-title.js'; @@ -26,6 +27,11 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router { validatePathParams('projectPath'), createBulkUpdateHandler(featureLoader) ); + router.post( + '/bulk-delete', + validatePathParams('projectPath'), + createBulkDeleteHandler(featureLoader) + ); router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader)); router.post('/agent-output', createAgentOutputHandler(featureLoader)); router.post('/raw-output', createRawOutputHandler(featureLoader)); diff --git a/apps/server/src/routes/features/routes/bulk-delete.ts b/apps/server/src/routes/features/routes/bulk-delete.ts new file mode 100644 index 00000000..6966d69a --- /dev/null +++ b/apps/server/src/routes/features/routes/bulk-delete.ts @@ -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 => { + 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) }); + } + }; +} diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 36479784..3aa2e47a 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -505,6 +505,34 @@ export function BoardView() { [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 const selectedFeatures = useMemo(() => { return hookFeatures.filter((f) => selectedFeatureIds.has(f.id)); @@ -1257,6 +1285,7 @@ export function BoardView() { selectedCount={selectedCount} totalCount={allSelectableFeatureIds.length} onEdit={() => setShowMassEditDialog(true)} + onDelete={handleBulkDelete} onClear={clearSelection} onSelectAll={() => selectAll(allSelectableFeatureIds)} /> diff --git a/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx b/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx index 7f4a553a..7938d05e 100644 --- a/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx +++ b/apps/ui/src/components/views/board-view/components/selection-action-bar.tsx @@ -1,11 +1,21 @@ +import { useState } from 'react'; 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 { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; interface SelectionActionBarProps { selectedCount: number; totalCount: number; onEdit: () => void; + onDelete: () => void; onClear: () => void; onSelectAll: () => void; } @@ -14,65 +24,126 @@ export function SelectionActionBar({ selectedCount, totalCount, onEdit, + onDelete, onClear, onSelectAll, }: SelectionActionBarProps) { + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + if (selectedCount === 0) return null; const allSelected = selectedCount === totalCount; + const handleDeleteClick = () => { + setShowDeleteDialog(true); + }; + + const handleConfirmDelete = () => { + setShowDeleteDialog(false); + onDelete(); + }; + return ( -
- - {selectedCount} feature{selectedCount !== 1 ? 's' : ''} selected - + <> +
+ + {selectedCount} feature{selectedCount !== 1 ? 's' : ''} selected + -
+
-
- +
+ - {!allSelected && ( - )} - + {!allSelected && ( + + )} + + +
-
+ + {/* Delete Confirmation Dialog */} + + + + + + Delete Selected Features? + + + Are you sure you want to permanently delete {selectedCount} feature + {selectedCount !== 1 ? 's' : ''}? + + This action cannot be undone. + + + + + + + + + + ); } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index b4957c4a..7d442836 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -1438,6 +1438,16 @@ export class HttpApiClient implements ElectronAPI { features?: Feature[]; 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 }), get: (projectPath: string, featureId: string) => @@ -1466,6 +1476,8 @@ export class HttpApiClient implements ElectronAPI { this.post('/api/features/generate-title', { description }), bulkUpdate: (projectPath: string, featureIds: string[], updates: Partial) => 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