feat: add mass edit feature for backlog kanban cards

Add ability to select multiple backlog features and edit their configuration
in bulk. Selection is limited to backlog column features in the current
branch/worktree only.

Changes:
- Add selection mode toggle in board controls
- Add checkbox selection on kanban cards (backlog only)
- Disable drag and drop during selection mode
- Hide action buttons during selection mode
- Add floating selection action bar with Edit/Clear/Select All
- Add mass edit dialog with all configuration options in single scroll view
- Add server endpoint for bulk feature updates
This commit is contained in:
Kacper
2026-01-04 22:24:03 +01:00
parent 4f3ac27534
commit 06c02de1cb
15 changed files with 840 additions and 15 deletions

View File

@@ -56,7 +56,10 @@ import {
useBoardBackground,
useBoardPersistence,
useFollowUpState,
useSelectionMode,
} from './board-view/hooks';
import { SelectionActionBar } from './board-view/components';
import { MassEditDialog } from './board-view/dialogs';
// Stable empty array to avoid infinite loop in selector
const EMPTY_WORKTREES: ReturnType<ReturnType<typeof useAppStore.getState>['getWorktrees']> = [];
@@ -154,6 +157,19 @@ export function BoardView() {
handleFollowUpDialogChange,
} = useFollowUpState();
// Selection mode hook for mass editing
const {
isSelectionMode,
selectedFeatureIds,
selectedCount,
toggleSelectionMode,
toggleFeatureSelection,
selectAll,
clearSelection,
exitSelectionMode,
} = useSelectionMode();
const [showMassEditDialog, setShowMassEditDialog] = useState(false);
// Search filter for Kanban cards
const [searchQuery, setSearchQuery] = useState('');
// Plan approval loading state
@@ -447,6 +463,72 @@ export function BoardView() {
currentWorktreeBranch,
});
// Handler for bulk updating multiple features
const handleBulkUpdate = useCallback(
async (updates: Partial<Feature>) => {
if (!currentProject || selectedFeatureIds.size === 0) return;
try {
const api = getHttpApiClient();
const featureIds = Array.from(selectedFeatureIds);
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
if (result.success) {
// Update local state
featureIds.forEach((featureId) => {
updateFeature(featureId, updates);
});
toast.success(`Updated ${result.updatedCount} features`);
exitSelectionMode();
} else {
toast.error('Failed to update some features', {
description: `${result.failedCount} features failed to update`,
});
}
} catch (error) {
logger.error('Bulk update failed:', error);
toast.error('Failed to update features');
}
},
[currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]
);
// Get selected features for mass edit dialog
const selectedFeatures = useMemo(() => {
return hookFeatures.filter((f) => selectedFeatureIds.has(f.id));
}, [hookFeatures, selectedFeatureIds]);
// Get backlog feature IDs in current branch for "Select All"
const allSelectableFeatureIds = useMemo(() => {
return hookFeatures
.filter((f) => {
// Only backlog features
if (f.status !== 'backlog') return false;
// Filter by current worktree branch
const featureBranch = f.branchName;
if (!featureBranch) {
// No branch assigned - only selectable on primary worktree
return currentWorktreePath === null;
}
if (currentWorktreeBranch === null) {
// Viewing main but branch hasn't been initialized
return currentProject?.path
? isPrimaryWorktreeBranch(currentProject.path, featureBranch)
: false;
}
// Match by branch name
return featureBranch === currentWorktreeBranch;
})
.map((f) => f.id);
}, [
hookFeatures,
currentWorktreePath,
currentWorktreeBranch,
currentProject?.path,
isPrimaryWorktreeBranch,
]);
// Handler for addressing PR comments - creates a feature and starts it automatically
const handleAddressPRComments = useCallback(
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
@@ -1069,6 +1151,8 @@ export function BoardView() {
onDetailLevelChange={setKanbanCardDetailLevel}
boardViewMode={boardViewMode}
onBoardViewModeChange={setBoardViewMode}
isSelectionMode={isSelectionMode}
onToggleSelectionMode={toggleSelectionMode}
/>
</div>
{/* View Content - Kanban or Graph */}
@@ -1109,6 +1193,9 @@ export function BoardView() {
currentProject?.path ? pipelineConfigByProject[currentProject.path] || null : null
}
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
isSelectionMode={isSelectionMode}
selectedFeatureIds={selectedFeatureIds}
onToggleFeatureSelection={toggleFeatureSelection}
/>
) : (
<GraphView
@@ -1134,6 +1221,27 @@ export function BoardView() {
)}
</div>
{/* Selection Action Bar */}
{isSelectionMode && (
<SelectionActionBar
selectedCount={selectedCount}
totalCount={allSelectableFeatureIds.length}
onEdit={() => setShowMassEditDialog(true)}
onClear={clearSelection}
onSelectAll={() => selectAll(allSelectableFeatureIds)}
/>
)}
{/* Mass Edit Dialog */}
<MassEditDialog
open={showMassEditDialog}
onClose={() => setShowMassEditDialog(false)}
selectedFeatures={selectedFeatures}
onApply={handleBulkUpdate}
showProfilesOnly={showProfilesOnly}
aiProfiles={aiProfiles}
/>
{/* Board Background Modal */}
<BoardBackgroundModal
open={showBoardBackgroundModal}