Allow selecting problems (#1568)
Fixes #672 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Add selectable problem rows with Select all/Clear all and Fix N selected, and update tests to cover selection behavior. > > - **UI (Problems panel)**: > - Add checkbox selection for each problem row (`ProblemItem`) with row click-to-toggle, `data-testid="problem-row"`, and accessibility attributes. > - Introduce selection state in `_Problems` with auto-select-all on report load; provide Select all / Clear all controls. > - Change Fix button to operate on selected problems only, showing dynamic label `Fix N problem(s)` and disabled when none selected. > - Wire `RecheckButton` to clear selection before rechecking; minor hover style tweaks; add `Checkbox` component. > - **E2E Tests**: > - New test: selecting specific problems and fixing only selected; add snapshots for prompt content. > - Update manual edit tests (React/Vite, Next.js) to assert Fix button enabled/disabled and counts; remove old ARIA snapshots. > - Minor import addition for `Timeout` and related expectations. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8324e26f9d2d265e7e0d1f1b7538e2a8db40f674. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
|
||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Problem, ProblemReport } from "@/ipc/ipc_types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
|
||||
import { useStreamChat } from "@/hooks/useStreamChat";
|
||||
import { useCheckProblems } from "@/hooks/useCheckProblems";
|
||||
@@ -20,11 +21,26 @@ import { showError } from "@/lib/toast";
|
||||
|
||||
interface ProblemItemProps {
|
||||
problem: Problem;
|
||||
checked: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
const ProblemItem = ({ problem }: ProblemItemProps) => {
|
||||
const ProblemItem = ({ problem, checked, onToggle }: ProblemItemProps) => {
|
||||
return (
|
||||
<div className="flex items-start gap-3 p-3 border-b border-border hover:bg-[var(--background-darkest)] transition-colors">
|
||||
<div
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
onClick={onToggle}
|
||||
className="cursor-pointer flex items-start gap-3 p-3 border-b border-border hover:bg-[var(--background-darker)] dark:hover:bg-[var(--background-lightest)] transition-colors"
|
||||
data-testid="problem-row"
|
||||
>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onCheckedChange={onToggle}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-0.5"
|
||||
aria-label="Select problem"
|
||||
/>
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
<XCircle size={16} className="text-red-500" />
|
||||
</div>
|
||||
@@ -56,6 +72,7 @@ interface RecheckButtonProps {
|
||||
| "ghost"
|
||||
| "link";
|
||||
className?: string;
|
||||
onBeforeRecheck?: () => void;
|
||||
}
|
||||
|
||||
const RecheckButton = ({
|
||||
@@ -63,11 +80,15 @@ const RecheckButton = ({
|
||||
size = "sm",
|
||||
variant = "outline",
|
||||
className = "h-7 px-3 text-xs",
|
||||
onBeforeRecheck,
|
||||
}: RecheckButtonProps) => {
|
||||
const { checkProblems, isChecking } = useCheckProblems(appId);
|
||||
const [showingFeedback, setShowingFeedback] = useState(false);
|
||||
|
||||
const handleRecheck = async () => {
|
||||
if (onBeforeRecheck) {
|
||||
onBeforeRecheck();
|
||||
}
|
||||
setShowingFeedback(true);
|
||||
|
||||
const res = await checkProblems();
|
||||
@@ -102,23 +123,24 @@ const RecheckButton = ({
|
||||
interface ProblemsSummaryProps {
|
||||
problemReport: ProblemReport;
|
||||
appId: number;
|
||||
selectedCount: number;
|
||||
onClearAll: () => void;
|
||||
onFixSelected: () => void;
|
||||
onSelectAll: () => void;
|
||||
}
|
||||
|
||||
const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => {
|
||||
const { streamMessage } = useStreamChat();
|
||||
const ProblemsSummary = ({
|
||||
problemReport,
|
||||
appId,
|
||||
selectedCount,
|
||||
onClearAll,
|
||||
onFixSelected,
|
||||
onSelectAll,
|
||||
}: ProblemsSummaryProps) => {
|
||||
const { problems } = problemReport;
|
||||
const totalErrors = problems.length;
|
||||
const [selectedChatId] = useAtom(selectedChatIdAtom);
|
||||
|
||||
const handleFixAll = () => {
|
||||
if (!selectedChatId) {
|
||||
return;
|
||||
}
|
||||
streamMessage({
|
||||
prompt: createProblemFixPrompt(problemReport),
|
||||
chatId: selectedChatId,
|
||||
});
|
||||
};
|
||||
// Keep stream hook mounted; actual fix action is provided via onFixSelected
|
||||
|
||||
if (problems.length === 0) {
|
||||
return (
|
||||
@@ -148,16 +170,36 @@ const ProblemsSummary = ({ problemReport, appId }: ProblemsSummaryProps) => {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RecheckButton appId={appId} />
|
||||
<RecheckButton appId={appId} onBeforeRecheck={onClearAll} />
|
||||
{selectedCount === 0 ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onSelectAll}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
Select all
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onClearAll}
|
||||
className="h-7 px-3 text-xs"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
onClick={handleFixAll}
|
||||
onClick={onFixSelected}
|
||||
className="h-7 px-3 text-xs"
|
||||
data-testid="fix-all-button"
|
||||
disabled={selectedCount === 0}
|
||||
>
|
||||
<Wrench size={14} className="mr-1" />
|
||||
Fix All
|
||||
{`Fix ${selectedCount} ${selectedCount === 1 ? "problem" : "problems"}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,6 +217,20 @@ export function Problems() {
|
||||
export function _Problems() {
|
||||
const selectedAppId = useAtomValue(selectedAppIdAtom);
|
||||
const { problemReport } = useCheckProblems(selectedAppId);
|
||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
||||
const problemKey = (p: Problem) =>
|
||||
`${p.file}:${p.line}:${p.column}:${p.code}`;
|
||||
const { streamMessage } = useStreamChat();
|
||||
const [selectedChatId] = useAtom(selectedChatIdAtom);
|
||||
|
||||
// Whenever the problems pane is shown or the report updates, select all problems
|
||||
useEffect(() => {
|
||||
if (problemReport?.problems?.length) {
|
||||
setSelectedKeys(new Set(problemReport.problems.map(problemKey)));
|
||||
} else {
|
||||
setSelectedKeys(new Set());
|
||||
}
|
||||
}, [problemReport]);
|
||||
|
||||
if (!selectedAppId) {
|
||||
return (
|
||||
@@ -200,21 +256,65 @@ export function _Problems() {
|
||||
<p className="text-sm text-muted-foreground max-w-md mb-4">
|
||||
Run checks to scan your app for TypeScript errors and other problems.
|
||||
</p>
|
||||
<RecheckButton appId={selectedAppId} />
|
||||
<RecheckButton
|
||||
appId={selectedAppId}
|
||||
onBeforeRecheck={() => setSelectedKeys(new Set())}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<ProblemsSummary problemReport={problemReport} appId={selectedAppId} />
|
||||
<ProblemsSummary
|
||||
problemReport={problemReport}
|
||||
appId={selectedAppId}
|
||||
selectedCount={
|
||||
[...selectedKeys].filter((key) =>
|
||||
problemReport.problems.some((p) => problemKey(p) === key),
|
||||
).length
|
||||
}
|
||||
onClearAll={() => setSelectedKeys(new Set())}
|
||||
onSelectAll={() =>
|
||||
setSelectedKeys(
|
||||
new Set(problemReport.problems.map((p) => problemKey(p))),
|
||||
)
|
||||
}
|
||||
onFixSelected={() => {
|
||||
if (!selectedChatId) return;
|
||||
const selectedProblems = problemReport.problems.filter((p) =>
|
||||
selectedKeys.has(problemKey(p)),
|
||||
);
|
||||
const subsetReport: ProblemReport = { problems: selectedProblems };
|
||||
streamMessage({
|
||||
prompt: createProblemFixPrompt(subsetReport),
|
||||
chatId: selectedChatId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{problemReport.problems.map((problem, index) => (
|
||||
<ProblemItem
|
||||
key={`${problem.file}-${problem.line}-${problem.column}-${index}`}
|
||||
problem={problem}
|
||||
/>
|
||||
))}
|
||||
{problemReport.problems.map((problem) => {
|
||||
const selKey = problemKey(problem);
|
||||
const checked = selectedKeys.has(selKey);
|
||||
return (
|
||||
<ProblemItem
|
||||
key={selKey}
|
||||
problem={problem}
|
||||
checked={checked}
|
||||
onToggle={() => {
|
||||
setSelectedKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(selKey)) {
|
||||
next.delete(selKey);
|
||||
} else {
|
||||
next.add(selKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user