From 2fe0dc2c2f1710479b4cb2b64cea231bb3b919fa Mon Sep 17 00:00:00 2001 From: Shaheer Sarfaraz <53654735+DaKheera47@users.noreply.github.com> Date: Sat, 7 Feb 2026 22:51:42 +0000 Subject: [PATCH] Recalculate match in batch actions (#102) * rescore in batch * more tests! * formatting! --- .../client/pages/OrchestratorPage.test.tsx | 76 +++++++++++++++++- .../src/client/pages/OrchestratorPage.tsx | 3 + .../orchestrator/FloatingBulkActionsBar.tsx | 15 ++++ .../pages/orchestrator/bulkActions.test.ts | 16 +++- .../client/pages/orchestrator/bulkActions.ts | 4 + .../orchestrator/useBulkJobSelection.test.ts | 42 ++++++++++ .../pages/orchestrator/useBulkJobSelection.ts | 12 ++- .../src/server/api/routes/jobs.test.ts | 67 ++++++++++++++++ orchestrator/src/server/api/routes/jobs.ts | 79 ++++++++++++++----- shared/src/types.ts | 2 +- 10 files changed, 293 insertions(+), 23 deletions(-) diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx index fb8123e..8333d6d 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx @@ -86,6 +86,7 @@ const jobFixture: Job = { }; const job2: Job = { ...jobFixture, id: "job-2", status: "discovered" }; +const processingJob: Job = { ...jobFixture, id: "job-3", status: "processing" }; const createMatchMedia = (matches: boolean) => vi.fn().mockImplementation((query: string) => ({ @@ -100,10 +101,10 @@ const createMatchMedia = (matches: boolean) => vi.mock("./orchestrator/useOrchestratorData", () => ({ useOrchestratorData: () => ({ - jobs: [jobFixture, job2], + jobs: [jobFixture, job2, processingJob], stats: { discovered: 1, - processing: 0, + processing: 1, ready: 1, applied: 0, skipped: 0, @@ -190,13 +191,45 @@ vi.mock("./orchestrator/JobDetailPanel", () => ({ vi.mock("./orchestrator/JobListPanel", () => ({ JobListPanel: ({ onSelectJob, + onToggleSelectJob, + onToggleSelectAll, selectedJobId, }: { onSelectJob: (id: string) => void; + onToggleSelectJob: (id: string) => void; + onToggleSelectAll: (checked: boolean) => void; selectedJobId: string | null; }) => (
{selectedJobId ?? "none"}
+ + + + +
), })); @@ -461,4 +501,36 @@ describe("OrchestratorPage", () => { setIntervalSpy.mockRestore(); }); + + it("shows and hides bulk Recalculate match based on selected statuses", async () => { + window.matchMedia = createMatchMedia( + true, + ) as unknown as typeof window.matchMedia; + + render( + + + } /> + } /> + + , + ); + + fireEvent.click(screen.getByTestId("toggle-select-all-on")); + + await waitFor(() => { + expect( + screen.queryByRole("button", { name: "Recalculate match" }), + ).not.toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId("toggle-select-all-off")); + fireEvent.click(screen.getByTestId("toggle-select-job-1")); + + await waitFor(() => { + expect( + screen.getByRole("button", { name: "Recalculate match" }), + ).toBeInTheDocument(); + }); + }); }); diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index b61a42c..0019998 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -194,6 +194,7 @@ export const OrchestratorPage: React.FC = () => { selectedJobIds, canSkipSelected, canMoveSelected, + canRescoreSelected, bulkActionInFlight, toggleSelectJob, toggleSelectAll, @@ -442,9 +443,11 @@ export const OrchestratorPage: React.FC = () => { selectedCount={selectedJobIds.size} canMoveSelected={canMoveSelected} canSkipSelected={canSkipSelected} + canRescoreSelected={canRescoreSelected} bulkActionInFlight={bulkActionInFlight !== null} onMoveToReady={() => void runBulkAction("move_to_ready")} onSkipSelected={() => void runBulkAction("skip")} + onRescoreSelected={() => void runBulkAction("rescore")} onClear={clearSelection} /> diff --git a/orchestrator/src/client/pages/orchestrator/FloatingBulkActionsBar.tsx b/orchestrator/src/client/pages/orchestrator/FloatingBulkActionsBar.tsx index 1c1b29f..b8db8a0 100644 --- a/orchestrator/src/client/pages/orchestrator/FloatingBulkActionsBar.tsx +++ b/orchestrator/src/client/pages/orchestrator/FloatingBulkActionsBar.tsx @@ -7,9 +7,11 @@ interface FloatingBulkActionsBarProps { selectedCount: number; canMoveSelected: boolean; canSkipSelected: boolean; + canRescoreSelected: boolean; bulkActionInFlight: boolean; onMoveToReady: () => void; onSkipSelected: () => void; + onRescoreSelected: () => void; onClear: () => void; } @@ -17,9 +19,11 @@ export const FloatingBulkActionsBar: React.FC = ({ selectedCount, canMoveSelected, canSkipSelected, + canRescoreSelected, bulkActionInFlight, onMoveToReady, onSkipSelected, + onRescoreSelected, onClear, }) => { const [isMounted, setIsMounted] = useState(false); @@ -73,6 +77,17 @@ export const FloatingBulkActionsBar: React.FC = ({ Skip selected )} + {canRescoreSelected && ( + + )}