mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
feat: implement bulk feature verification and enhance selection mode
- Added functionality for bulk verifying features in the BoardView, allowing users to mark multiple features as verified at once. - Introduced a selection target mechanism to differentiate between 'backlog' and 'waiting_approval' features during selection mode. - Updated the KanbanCard and SelectionActionBar components to support the new selection target logic, improving user experience for bulk actions. - Enhanced the UI to provide appropriate actions based on the current selection target, including verification options for waiting approval features.
This commit is contained in:
@@ -30,19 +30,27 @@ export function createBulkDeleteHandler(featureLoader: FeatureLoader) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await Promise.all(
|
// Process in parallel batches of 20 for efficiency
|
||||||
featureIds.map(async (featureId) => {
|
const BATCH_SIZE = 20;
|
||||||
const success = await featureLoader.delete(projectPath, featureId);
|
const results: BulkDeleteResult[] = [];
|
||||||
if (success) {
|
|
||||||
return { featureId, success: true };
|
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
|
||||||
}
|
const batch = featureIds.slice(i, i + BATCH_SIZE);
|
||||||
return {
|
const batchResults = await Promise.all(
|
||||||
featureId,
|
batch.map(async (featureId) => {
|
||||||
success: false,
|
const success = await featureLoader.delete(projectPath, featureId);
|
||||||
error: 'Deletion failed. Check server logs for details.',
|
if (success) {
|
||||||
};
|
return { featureId, success: true };
|
||||||
})
|
}
|
||||||
);
|
return {
|
||||||
|
featureId,
|
||||||
|
success: false,
|
||||||
|
error: 'Deletion failed. Check server logs for details.',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
results.push(...batchResults);
|
||||||
|
}
|
||||||
|
|
||||||
const successCount = results.reduce((count, r) => count + (r.success ? 1 : 0), 0);
|
const successCount = results.reduce((count, r) => count + (r.success ? 1 : 0), 0);
|
||||||
const failureCount = results.length - successCount;
|
const failureCount = results.length - successCount;
|
||||||
|
|||||||
@@ -43,17 +43,36 @@ export function createBulkUpdateHandler(featureLoader: FeatureLoader) {
|
|||||||
const results: BulkUpdateResult[] = [];
|
const results: BulkUpdateResult[] = [];
|
||||||
const updatedFeatures: Feature[] = [];
|
const updatedFeatures: Feature[] = [];
|
||||||
|
|
||||||
for (const featureId of featureIds) {
|
// Process in parallel batches of 20 for efficiency
|
||||||
try {
|
const BATCH_SIZE = 20;
|
||||||
const updated = await featureLoader.update(projectPath, featureId, updates);
|
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
|
||||||
results.push({ featureId, success: true });
|
const batch = featureIds.slice(i, i + BATCH_SIZE);
|
||||||
updatedFeatures.push(updated);
|
const batchResults = await Promise.all(
|
||||||
} catch (error) {
|
batch.map(async (featureId) => {
|
||||||
results.push({
|
try {
|
||||||
featureId,
|
const updated = await featureLoader.update(projectPath, featureId, updates);
|
||||||
success: false,
|
return { featureId, success: true as const, feature: updated };
|
||||||
error: getErrorMessage(error),
|
} catch (error) {
|
||||||
});
|
return {
|
||||||
|
featureId,
|
||||||
|
success: false as const,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const result of batchResults) {
|
||||||
|
if (result.success) {
|
||||||
|
results.push({ featureId: result.featureId, success: true });
|
||||||
|
updatedFeatures.push(result.feature);
|
||||||
|
} else {
|
||||||
|
results.push({
|
||||||
|
featureId: result.featureId,
|
||||||
|
success: false,
|
||||||
|
error: result.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ export function BoardView() {
|
|||||||
// Selection mode hook for mass editing
|
// Selection mode hook for mass editing
|
||||||
const {
|
const {
|
||||||
isSelectionMode,
|
isSelectionMode,
|
||||||
|
selectionTarget,
|
||||||
selectedFeatureIds,
|
selectedFeatureIds,
|
||||||
selectedCount,
|
selectedCount,
|
||||||
toggleSelectionMode,
|
toggleSelectionMode,
|
||||||
@@ -684,6 +685,67 @@ export function BoardView() {
|
|||||||
isPrimaryWorktreeBranch,
|
isPrimaryWorktreeBranch,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Get waiting_approval feature IDs in current branch for "Select All"
|
||||||
|
const allSelectableWaitingApprovalFeatureIds = useMemo(() => {
|
||||||
|
return hookFeatures
|
||||||
|
.filter((f) => {
|
||||||
|
// Only waiting_approval features
|
||||||
|
if (f.status !== 'waiting_approval') 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 bulk verifying multiple features
|
||||||
|
const handleBulkVerify = useCallback(async () => {
|
||||||
|
if (!currentProject || selectedFeatureIds.size === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const api = getHttpApiClient();
|
||||||
|
const featureIds = Array.from(selectedFeatureIds);
|
||||||
|
const updates = { status: 'verified' as const };
|
||||||
|
|
||||||
|
// Use bulk update API for efficient batch processing
|
||||||
|
const result = await api.features.bulkUpdate(currentProject.path, featureIds, updates);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Update local state for all features
|
||||||
|
featureIds.forEach((featureId) => {
|
||||||
|
updateFeature(featureId, updates);
|
||||||
|
});
|
||||||
|
toast.success(`Verified ${result.updatedCount} features`);
|
||||||
|
exitSelectionMode();
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to verify some features', {
|
||||||
|
description: `${result.failedCount} features failed to verify`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Bulk verify failed:', error);
|
||||||
|
toast.error('Failed to verify features');
|
||||||
|
}
|
||||||
|
}, [currentProject, selectedFeatureIds, updateFeature, exitSelectionMode]);
|
||||||
|
|
||||||
// Handler for addressing PR comments - creates a feature and starts it automatically
|
// Handler for addressing PR comments - creates a feature and starts it automatically
|
||||||
const handleAddressPRComments = useCallback(
|
const handleAddressPRComments = useCallback(
|
||||||
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
async (worktree: WorktreeInfo, prInfo: PRInfo) => {
|
||||||
@@ -1448,6 +1510,7 @@ export function BoardView() {
|
|||||||
pipelineConfig={pipelineConfig}
|
pipelineConfig={pipelineConfig}
|
||||||
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
onOpenPipelineSettings={() => setShowPipelineSettings(true)}
|
||||||
isSelectionMode={isSelectionMode}
|
isSelectionMode={isSelectionMode}
|
||||||
|
selectionTarget={selectionTarget}
|
||||||
selectedFeatureIds={selectedFeatureIds}
|
selectedFeatureIds={selectedFeatureIds}
|
||||||
onToggleFeatureSelection={toggleFeatureSelection}
|
onToggleFeatureSelection={toggleFeatureSelection}
|
||||||
onToggleSelectionMode={toggleSelectionMode}
|
onToggleSelectionMode={toggleSelectionMode}
|
||||||
@@ -1463,11 +1526,23 @@ export function BoardView() {
|
|||||||
{isSelectionMode && (
|
{isSelectionMode && (
|
||||||
<SelectionActionBar
|
<SelectionActionBar
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
totalCount={allSelectableFeatureIds.length}
|
totalCount={
|
||||||
onEdit={() => setShowMassEditDialog(true)}
|
selectionTarget === 'waiting_approval'
|
||||||
onDelete={handleBulkDelete}
|
? allSelectableWaitingApprovalFeatureIds.length
|
||||||
|
: allSelectableFeatureIds.length
|
||||||
|
}
|
||||||
|
onEdit={selectionTarget === 'backlog' ? () => setShowMassEditDialog(true) : undefined}
|
||||||
|
onDelete={selectionTarget === 'backlog' ? handleBulkDelete : undefined}
|
||||||
|
onVerify={selectionTarget === 'waiting_approval' ? handleBulkVerify : undefined}
|
||||||
onClear={clearSelection}
|
onClear={clearSelection}
|
||||||
onSelectAll={() => selectAll(allSelectableFeatureIds)}
|
onSelectAll={() =>
|
||||||
|
selectAll(
|
||||||
|
selectionTarget === 'waiting_approval'
|
||||||
|
? allSelectableWaitingApprovalFeatureIds
|
||||||
|
: allSelectableFeatureIds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
mode={selectionTarget === 'waiting_approval' ? 'waiting_approval' : 'backlog'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ interface KanbanCardProps {
|
|||||||
isSelectionMode?: boolean;
|
isSelectionMode?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
onToggleSelect?: () => void;
|
onToggleSelect?: () => void;
|
||||||
|
selectionTarget?: 'backlog' | 'waiting_approval' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanCard = memo(function KanbanCard({
|
export const KanbanCard = memo(function KanbanCard({
|
||||||
@@ -96,6 +97,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
|
selectionTarget = null,
|
||||||
}: KanbanCardProps) {
|
}: KanbanCardProps) {
|
||||||
const { useWorktrees } = useAppStore();
|
const { useWorktrees } = useAppStore();
|
||||||
const [isLifted, setIsLifted] = useState(false);
|
const [isLifted, setIsLifted] = useState(false);
|
||||||
@@ -125,8 +127,8 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
|
|
||||||
const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity);
|
const cardStyle = getCardBorderStyle(cardBorderEnabled, cardBorderOpacity);
|
||||||
|
|
||||||
// Only allow selection for backlog features
|
// Only allow selection for features matching the selection target
|
||||||
const isSelectable = isSelectionMode && feature.status === 'backlog';
|
const isSelectable = isSelectionMode && feature.status === selectionTarget;
|
||||||
|
|
||||||
const wrapperClasses = cn(
|
const wrapperClasses = cn(
|
||||||
'relative select-none outline-none touch-none transition-transform duration-200 ease-out',
|
'relative select-none outline-none touch-none transition-transform duration-200 ease-out',
|
||||||
@@ -180,7 +182,7 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
|
|
||||||
{/* Category row with selection checkbox */}
|
{/* Category row with selection checkbox */}
|
||||||
<div className="px-3 pt-3 flex items-center gap-2">
|
<div className="px-3 pt-3 flex items-center gap-2">
|
||||||
{isSelectionMode && !isOverlay && feature.status === 'backlog' && (
|
{isSelectable && !isOverlay && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onCheckedChange={() => onToggleSelect?.()}
|
onCheckedChange={() => onToggleSelect?.()}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Pencil, X, CheckSquare, Trash2 } from 'lucide-react';
|
import { Pencil, X, CheckSquare, Trash2, CheckCircle2 } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -11,13 +11,17 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
|
export type SelectionActionMode = 'backlog' | 'waiting_approval';
|
||||||
|
|
||||||
interface SelectionActionBarProps {
|
interface SelectionActionBarProps {
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
onEdit: () => void;
|
onEdit?: () => void;
|
||||||
onDelete: () => void;
|
onDelete?: () => void;
|
||||||
|
onVerify?: () => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onSelectAll: () => void;
|
onSelectAll: () => void;
|
||||||
|
mode?: SelectionActionMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SelectionActionBar({
|
export function SelectionActionBar({
|
||||||
@@ -25,10 +29,13 @@ export function SelectionActionBar({
|
|||||||
totalCount,
|
totalCount,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onVerify,
|
||||||
onClear,
|
onClear,
|
||||||
onSelectAll,
|
onSelectAll,
|
||||||
|
mode = 'backlog',
|
||||||
}: SelectionActionBarProps) {
|
}: SelectionActionBarProps) {
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
const [showVerifyDialog, setShowVerifyDialog] = useState(false);
|
||||||
|
|
||||||
const allSelected = selectedCount === totalCount && totalCount > 0;
|
const allSelected = selectedCount === totalCount && totalCount > 0;
|
||||||
|
|
||||||
@@ -38,7 +45,16 @@ export function SelectionActionBar({
|
|||||||
|
|
||||||
const handleConfirmDelete = () => {
|
const handleConfirmDelete = () => {
|
||||||
setShowDeleteDialog(false);
|
setShowDeleteDialog(false);
|
||||||
onDelete();
|
onDelete?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyClick = () => {
|
||||||
|
setShowVerifyDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmVerify = () => {
|
||||||
|
setShowVerifyDialog(false);
|
||||||
|
onVerify?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,36 +70,56 @@ export function SelectionActionBar({
|
|||||||
>
|
>
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">
|
||||||
{selectedCount === 0
|
{selectedCount === 0
|
||||||
? 'Select features to edit'
|
? mode === 'waiting_approval'
|
||||||
|
? 'Select features to verify'
|
||||||
|
: 'Select features to edit'
|
||||||
: `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
|
: `${selectedCount} feature${selectedCount !== 1 ? 's' : ''} selected`}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="h-4 w-px bg-border" />
|
<div className="h-4 w-px bg-border" />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
{mode === 'backlog' && (
|
||||||
variant="default"
|
<>
|
||||||
size="sm"
|
<Button
|
||||||
onClick={onEdit}
|
variant="default"
|
||||||
disabled={selectedCount === 0}
|
size="sm"
|
||||||
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
|
onClick={onEdit}
|
||||||
data-testid="selection-edit-button"
|
disabled={selectedCount === 0}
|
||||||
>
|
className="h-8 bg-brand-500 hover:bg-brand-600 disabled:opacity-50"
|
||||||
<Pencil className="w-4 h-4 mr-1.5" />
|
data-testid="selection-edit-button"
|
||||||
Edit Selected
|
>
|
||||||
</Button>
|
<Pencil className="w-4 h-4 mr-1.5" />
|
||||||
|
Edit Selected
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
disabled={selectedCount === 0}
|
disabled={selectedCount === 0}
|
||||||
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
className="h-8 text-destructive hover:text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||||
data-testid="selection-delete-button"
|
data-testid="selection-delete-button"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-1.5" />
|
<Trash2 className="w-4 h-4 mr-1.5" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{mode === 'waiting_approval' && (
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleVerifyClick}
|
||||||
|
disabled={selectedCount === 0}
|
||||||
|
className="h-8 bg-green-600 hover:bg-green-700 disabled:opacity-50"
|
||||||
|
data-testid="selection-verify-button"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4 mr-1.5" />
|
||||||
|
Verify Selected
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{!allSelected && (
|
{!allSelected && (
|
||||||
<Button
|
<Button
|
||||||
@@ -146,6 +182,42 @@ export function SelectionActionBar({
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Verify Confirmation Dialog */}
|
||||||
|
<Dialog open={showVerifyDialog} onOpenChange={setShowVerifyDialog}>
|
||||||
|
<DialogContent data-testid="bulk-verify-confirmation-dialog">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-green-600">
|
||||||
|
<CheckCircle2 className="w-5 h-5" />
|
||||||
|
Verify Selected Features?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to mark {selectedCount} feature
|
||||||
|
{selectedCount !== 1 ? 's' : ''} as verified?
|
||||||
|
<span className="block mt-2 text-muted-foreground">
|
||||||
|
This will move them to the Verified column.
|
||||||
|
</span>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowVerifyDialog(false)}
|
||||||
|
data-testid="cancel-bulk-verify-button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
onClick={handleConfirmVerify}
|
||||||
|
data-testid="confirm-bulk-verify-button"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||||||
|
Verify
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export { useBoardEffects } from './use-board-effects';
|
|||||||
export { useBoardBackground } from './use-board-background';
|
export { useBoardBackground } from './use-board-background';
|
||||||
export { useBoardPersistence } from './use-board-persistence';
|
export { useBoardPersistence } from './use-board-persistence';
|
||||||
export { useFollowUpState } from './use-follow-up-state';
|
export { useFollowUpState } from './use-follow-up-state';
|
||||||
export { useSelectionMode } from './use-selection-mode';
|
export { useSelectionMode, type SelectionTarget } from './use-selection-mode';
|
||||||
export { useListViewState } from './use-list-view-state';
|
export { useListViewState } from './use-list-view-state';
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
export type SelectionTarget = 'backlog' | 'waiting_approval' | null;
|
||||||
|
|
||||||
interface UseSelectionModeReturn {
|
interface UseSelectionModeReturn {
|
||||||
isSelectionMode: boolean;
|
isSelectionMode: boolean;
|
||||||
|
selectionTarget: SelectionTarget;
|
||||||
selectedFeatureIds: Set<string>;
|
selectedFeatureIds: Set<string>;
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
toggleSelectionMode: () => void;
|
toggleSelectionMode: (target?: SelectionTarget) => void;
|
||||||
toggleFeatureSelection: (featureId: string) => void;
|
toggleFeatureSelection: (featureId: string) => void;
|
||||||
selectAll: (featureIds: string[]) => void;
|
selectAll: (featureIds: string[]) => void;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
@@ -13,21 +16,26 @@ interface UseSelectionModeReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSelectionMode(): UseSelectionModeReturn {
|
export function useSelectionMode(): UseSelectionModeReturn {
|
||||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [selectionTarget, setSelectionTarget] = useState<SelectionTarget>(null);
|
||||||
const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set());
|
const [selectedFeatureIds, setSelectedFeatureIds] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
const toggleSelectionMode = useCallback(() => {
|
const isSelectionMode = selectionTarget !== null;
|
||||||
setIsSelectionMode((prev) => {
|
|
||||||
if (prev) {
|
const toggleSelectionMode = useCallback((target: SelectionTarget = 'backlog') => {
|
||||||
|
setSelectionTarget((prev) => {
|
||||||
|
if (prev === target) {
|
||||||
// Exiting selection mode - clear selection
|
// Exiting selection mode - clear selection
|
||||||
setSelectedFeatureIds(new Set());
|
setSelectedFeatureIds(new Set());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return !prev;
|
// Switching to a different target or entering selection mode
|
||||||
|
setSelectedFeatureIds(new Set());
|
||||||
|
return target;
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const exitSelectionMode = useCallback(() => {
|
const exitSelectionMode = useCallback(() => {
|
||||||
setIsSelectionMode(false);
|
setSelectionTarget(null);
|
||||||
setSelectedFeatureIds(new Set());
|
setSelectedFeatureIds(new Set());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -70,6 +78,7 @@ export function useSelectionMode(): UseSelectionModeReturn {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isSelectionMode,
|
isSelectionMode,
|
||||||
|
selectionTarget,
|
||||||
selectedFeatureIds,
|
selectedFeatureIds,
|
||||||
selectedCount: selectedFeatureIds.size,
|
selectedCount: selectedFeatureIds.size,
|
||||||
toggleSelectionMode,
|
toggleSelectionMode,
|
||||||
|
|||||||
@@ -50,9 +50,10 @@ interface KanbanBoardProps {
|
|||||||
onOpenPipelineSettings?: () => void;
|
onOpenPipelineSettings?: () => void;
|
||||||
// Selection mode props
|
// Selection mode props
|
||||||
isSelectionMode?: boolean;
|
isSelectionMode?: boolean;
|
||||||
|
selectionTarget?: 'backlog' | 'waiting_approval' | null;
|
||||||
selectedFeatureIds?: Set<string>;
|
selectedFeatureIds?: Set<string>;
|
||||||
onToggleFeatureSelection?: (featureId: string) => void;
|
onToggleFeatureSelection?: (featureId: string) => void;
|
||||||
onToggleSelectionMode?: () => void;
|
onToggleSelectionMode?: (target?: 'backlog' | 'waiting_approval') => void;
|
||||||
// Empty state action props
|
// Empty state action props
|
||||||
onAiSuggest?: () => void;
|
onAiSuggest?: () => void;
|
||||||
/** Whether currently dragging (hides empty states during drag) */
|
/** Whether currently dragging (hides empty states during drag) */
|
||||||
@@ -95,6 +96,7 @@ export function KanbanBoard({
|
|||||||
pipelineConfig,
|
pipelineConfig,
|
||||||
onOpenPipelineSettings,
|
onOpenPipelineSettings,
|
||||||
isSelectionMode = false,
|
isSelectionMode = false,
|
||||||
|
selectionTarget = null,
|
||||||
selectedFeatureIds = new Set(),
|
selectedFeatureIds = new Set(),
|
||||||
onToggleFeatureSelection,
|
onToggleFeatureSelection,
|
||||||
onToggleSelectionMode,
|
onToggleSelectionMode,
|
||||||
@@ -189,12 +191,14 @@ export function KanbanBoard({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={`h-6 px-2 text-xs ${isSelectionMode ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
className={`h-6 px-2 text-xs ${selectionTarget === 'backlog' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||||
onClick={onToggleSelectionMode}
|
onClick={() => onToggleSelectionMode?.('backlog')}
|
||||||
title={isSelectionMode ? 'Switch to Drag Mode' : 'Select Multiple'}
|
title={
|
||||||
|
selectionTarget === 'backlog' ? 'Switch to Drag Mode' : 'Select Multiple'
|
||||||
|
}
|
||||||
data-testid="selection-mode-button"
|
data-testid="selection-mode-button"
|
||||||
>
|
>
|
||||||
{isSelectionMode ? (
|
{selectionTarget === 'backlog' ? (
|
||||||
<>
|
<>
|
||||||
<GripVertical className="w-3.5 h-3.5 mr-1" />
|
<GripVertical className="w-3.5 h-3.5 mr-1" />
|
||||||
Drag
|
Drag
|
||||||
@@ -207,6 +211,31 @@ export function KanbanBoard({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : column.id === 'waiting_approval' ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`h-6 px-2 text-xs ${selectionTarget === 'waiting_approval' ? 'text-primary bg-primary/10' : 'text-muted-foreground hover:text-foreground'}`}
|
||||||
|
onClick={() => onToggleSelectionMode?.('waiting_approval')}
|
||||||
|
title={
|
||||||
|
selectionTarget === 'waiting_approval'
|
||||||
|
? 'Switch to Drag Mode'
|
||||||
|
: 'Select Multiple'
|
||||||
|
}
|
||||||
|
data-testid="waiting-approval-selection-mode-button"
|
||||||
|
>
|
||||||
|
{selectionTarget === 'waiting_approval' ? (
|
||||||
|
<>
|
||||||
|
<GripVertical className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Drag
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckSquare className="w-3.5 h-3.5 mr-1" />
|
||||||
|
Select
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
) : column.id === 'in_progress' ? (
|
) : column.id === 'in_progress' ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -305,6 +334,7 @@ export function KanbanBoard({
|
|||||||
cardBorderEnabled={backgroundSettings.cardBorderEnabled}
|
cardBorderEnabled={backgroundSettings.cardBorderEnabled}
|
||||||
cardBorderOpacity={backgroundSettings.cardBorderOpacity}
|
cardBorderOpacity={backgroundSettings.cardBorderOpacity}
|
||||||
isSelectionMode={isSelectionMode}
|
isSelectionMode={isSelectionMode}
|
||||||
|
selectionTarget={selectionTarget}
|
||||||
isSelected={selectedFeatureIds.has(feature.id)}
|
isSelected={selectedFeatureIds.has(feature.id)}
|
||||||
onToggleSelect={() => onToggleFeatureSelection?.(feature.id)}
|
onToggleSelect={() => onToggleFeatureSelection?.(feature.id)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export const CLAUDE_MODELS: ModelOption[] = [
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Cursor models derived from CURSOR_MODEL_MAP
|
* Cursor models derived from CURSOR_MODEL_MAP
|
||||||
* ID is prefixed with "cursor-" for ProviderFactory routing
|
* ID is prefixed with "cursor-" for ProviderFactory routing (if not already prefixed)
|
||||||
*/
|
*/
|
||||||
export const CURSOR_MODELS: ModelOption[] = Object.entries(CURSOR_MODEL_MAP).map(
|
export const CURSOR_MODELS: ModelOption[] = Object.entries(CURSOR_MODEL_MAP).map(
|
||||||
([id, config]) => ({
|
([id, config]) => ({
|
||||||
id: `cursor-${id}`,
|
id: id.startsWith('cursor-') ? id : `cursor-${id}`,
|
||||||
label: config.label,
|
label: config.label,
|
||||||
description: config.description,
|
description: config.description,
|
||||||
provider: 'cursor' as ModelProvider,
|
provider: 'cursor' as ModelProvider,
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export type CursorModelId =
|
|||||||
| 'cursor-gpt-5.1-codex-high' // GPT-5.1 Codex High via Cursor
|
| 'cursor-gpt-5.1-codex-high' // GPT-5.1 Codex High via Cursor
|
||||||
| 'cursor-gpt-5.1-codex-max' // GPT-5.1 Codex Max via Cursor
|
| 'cursor-gpt-5.1-codex-max' // GPT-5.1 Codex Max via Cursor
|
||||||
| 'cursor-gpt-5.1-codex-max-high' // GPT-5.1 Codex Max High via Cursor
|
| 'cursor-gpt-5.1-codex-max-high' // GPT-5.1 Codex Max High via Cursor
|
||||||
|
| 'cursor-gpt-5.2-codex' // GPT-5.2 Codex via Cursor
|
||||||
|
| 'cursor-gpt-5.2-codex-high' // GPT-5.2 Codex High via Cursor
|
||||||
|
| 'cursor-gpt-5.2-codex-max' // GPT-5.2 Codex Max via Cursor
|
||||||
|
| 'cursor-gpt-5.2-codex-max-high' // GPT-5.2 Codex Max High via Cursor
|
||||||
| 'grok'; // Grok
|
| 'grok'; // Grok
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,6 +163,34 @@ export const CURSOR_MODEL_MAP: Record<CursorModelId, CursorModelConfig> = {
|
|||||||
hasThinking: false,
|
hasThinking: false,
|
||||||
supportsVision: false,
|
supportsVision: false,
|
||||||
},
|
},
|
||||||
|
'cursor-gpt-5.2-codex': {
|
||||||
|
id: 'cursor-gpt-5.2-codex',
|
||||||
|
label: 'GPT-5.2 Codex',
|
||||||
|
description: 'OpenAI GPT-5.2 Codex for code generation',
|
||||||
|
hasThinking: false,
|
||||||
|
supportsVision: false,
|
||||||
|
},
|
||||||
|
'cursor-gpt-5.2-codex-high': {
|
||||||
|
id: 'cursor-gpt-5.2-codex-high',
|
||||||
|
label: 'GPT-5.2 Codex High',
|
||||||
|
description: 'OpenAI GPT-5.2 Codex with high compute',
|
||||||
|
hasThinking: false,
|
||||||
|
supportsVision: false,
|
||||||
|
},
|
||||||
|
'cursor-gpt-5.2-codex-max': {
|
||||||
|
id: 'cursor-gpt-5.2-codex-max',
|
||||||
|
label: 'GPT-5.2 Codex Max',
|
||||||
|
description: 'OpenAI GPT-5.2 Codex Max capacity',
|
||||||
|
hasThinking: false,
|
||||||
|
supportsVision: false,
|
||||||
|
},
|
||||||
|
'cursor-gpt-5.2-codex-max-high': {
|
||||||
|
id: 'cursor-gpt-5.2-codex-max-high',
|
||||||
|
label: 'GPT-5.2 Codex Max High',
|
||||||
|
description: 'OpenAI GPT-5.2 Codex Max with high compute',
|
||||||
|
hasThinking: false,
|
||||||
|
supportsVision: false,
|
||||||
|
},
|
||||||
grok: {
|
grok: {
|
||||||
id: 'grok',
|
id: 'grok',
|
||||||
label: 'Grok',
|
label: 'Grok',
|
||||||
@@ -284,6 +316,34 @@ export const CURSOR_MODEL_GROUPS: GroupedModel[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// GPT-5.2 Codex group (capacity + compute matrix)
|
||||||
|
{
|
||||||
|
baseId: 'cursor-gpt-5.2-codex-group',
|
||||||
|
label: 'GPT-5.2 Codex',
|
||||||
|
description: 'OpenAI GPT-5.2 Codex for code generation',
|
||||||
|
variantType: 'capacity',
|
||||||
|
variants: [
|
||||||
|
{ id: 'cursor-gpt-5.2-codex', label: 'Standard', description: 'Default capacity' },
|
||||||
|
{
|
||||||
|
id: 'cursor-gpt-5.2-codex-high',
|
||||||
|
label: 'High',
|
||||||
|
description: 'High compute',
|
||||||
|
badge: 'Compute',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cursor-gpt-5.2-codex-max',
|
||||||
|
label: 'Max',
|
||||||
|
description: 'Maximum capacity',
|
||||||
|
badge: 'Capacity',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cursor-gpt-5.2-codex-max-high',
|
||||||
|
label: 'Max High',
|
||||||
|
description: 'Max capacity + high compute',
|
||||||
|
badge: 'Premium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// Sonnet 4.5 group (thinking mode)
|
// Sonnet 4.5 group (thinking mode)
|
||||||
{
|
{
|
||||||
baseId: 'sonnet-4.5-group',
|
baseId: 'sonnet-4.5-group',
|
||||||
|
|||||||
Reference in New Issue
Block a user