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 && (
+
+ )}