From 26275e4ee8e9a9cd3bb8caf6486afc7aa8ed92d8 Mon Sep 17 00:00:00 2001 From: Shaheer Sarfaraz <53654735+DaKheera47@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:48:43 +0000 Subject: [PATCH] Fix ready-job selection jumping after PDF regeneration (#276) * Preserve selected ready job during refresh * show processsing in ready --- .../client/pages/OrchestratorPage.test.tsx | 47 ++++++++++++++++++- .../src/client/pages/OrchestratorPage.tsx | 4 +- .../orchestrator/useFilteredJobs.test.ts | 34 +++++++++++++- .../pages/orchestrator/useFilteredJobs.ts | 8 ++-- .../client/pages/orchestrator/utils.test.ts | 20 +++++++- .../src/client/pages/orchestrator/utils.ts | 2 +- 6 files changed, 102 insertions(+), 13 deletions(-) diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx index 6ed10b7..e514c82 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx @@ -1,4 +1,5 @@ import { createJob } from "@shared/testing/factories.js"; +import type { Job } from "@shared/types.js"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom"; import { toast } from "sonner"; @@ -94,6 +95,9 @@ const processingJob = createJob({ status: "processing", }); +let mockJobs = [jobFixture, job2, processingJob]; +let mockSelectedJob: Job | null = jobFixture; + const createMatchMedia = (matches: boolean) => vi.fn().mockImplementation((query: string) => ({ matches, @@ -107,8 +111,8 @@ const createMatchMedia = (matches: boolean) => vi.mock("./orchestrator/useOrchestratorData", () => ({ useOrchestratorData: () => ({ - jobs: [jobFixture, job2, processingJob], - selectedJob: jobFixture, + jobs: mockJobs, + selectedJob: mockSelectedJob, stats: { discovered: 1, processing: 1, @@ -389,6 +393,8 @@ describe("OrchestratorPage", () => { mockIsPipelineRunning = false; mockPipelineTerminalEvent = null; mockPipelineSources = ["linkedin"]; + mockJobs = [jobFixture, job2, processingJob]; + mockSelectedJob = jobFixture; mockAutomaticRunValues = { topN: 12, minSuitabilityScore: 55, @@ -476,6 +482,43 @@ describe("OrchestratorPage", () => { }); }); + it("preserves the selected job id when a refresh temporarily excludes it", async () => { + window.matchMedia = createMatchMedia( + true, + ) as unknown as typeof window.matchMedia; + + const { rerender } = render( + + + + } /> + } /> + + , + ); + + await waitFor(() => { + expect(screen.getByTestId("location")).toHaveTextContent("/ready/job-1"); + }); + + mockJobs = [createJob({ ...jobFixture, id: "job-2", status: "ready" })]; + mockSelectedJob = null; + + rerender( + + + + } /> + } /> + + , + ); + + await waitFor(() => { + expect(screen.getByTestId("location")).toHaveTextContent("/ready/job-1"); + }); + }); + it("opens the command bar when the filters search button is clicked", () => { window.matchMedia = createMatchMedia( true, diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 31cd5c7..27642d0 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -264,8 +264,8 @@ export const OrchestratorPage: React.FC = () => { if (selectedJobId) handleSelectJobId(null); return; } - if (!selectedJobId || !activeJobs.some((job) => job.id === selectedJobId)) { - // Auto-select first job ONLY on desktop + if (!selectedJobId) { + // Auto-select first job ONLY on desktop when nothing is currently selected. if (isDesktop) { navigateWithContext(activeTab, activeJobs[0].id, true); } diff --git a/orchestrator/src/client/pages/orchestrator/useFilteredJobs.test.ts b/orchestrator/src/client/pages/orchestrator/useFilteredJobs.test.ts index bfee144..92ad166 100644 --- a/orchestrator/src/client/pages/orchestrator/useFilteredJobs.test.ts +++ b/orchestrator/src/client/pages/orchestrator/useFilteredJobs.test.ts @@ -15,7 +15,7 @@ const baseJob = createJob({ }); describe("useFilteredJobs", () => { - it("keeps in-progress jobs in the all jobs tab", () => { + it("keeps processing jobs visible in the all jobs tab", () => { const jobs: Job[] = [ { ...baseJob, id: "in-progress", status: "in_progress" }, { ...baseJob, id: "processing", status: "processing" }, @@ -41,7 +41,37 @@ describe("useFilteredJobs", () => { ), ); - expect(result.current.map((job) => job.id)).toEqual(["in-progress"]); + expect(result.current.map((job) => job.id)).toEqual([ + "in-progress", + "processing", + ]); + }); + + it("keeps processing jobs visible in the ready tab", () => { + const jobs: Job[] = [ + { ...baseJob, id: "ready", status: "ready" }, + { ...baseJob, id: "processing", status: "processing" }, + { ...baseJob, id: "discovered", status: "discovered" }, + ]; + + const { result } = renderHook(() => + useFilteredJobs( + jobs, + "ready", + "all", + "all", + { mode: "at_least", min: null, max: null }, + { + key: "score", + direction: "desc", + }, + ), + ); + + expect(result.current.map((job) => job.id)).toEqual( + expect.arrayContaining(["ready", "processing"]), + ); + expect(result.current).toHaveLength(2); }); it("filters by sponsor status categories", () => { diff --git a/orchestrator/src/client/pages/orchestrator/useFilteredJobs.ts b/orchestrator/src/client/pages/orchestrator/useFilteredJobs.ts index ebebbeb..da9a20e 100644 --- a/orchestrator/src/client/pages/orchestrator/useFilteredJobs.ts +++ b/orchestrator/src/client/pages/orchestrator/useFilteredJobs.ts @@ -27,7 +27,9 @@ export const useFilteredJobs = ( let filtered = [...jobs]; if (activeTab === "ready") { - filtered = filtered.filter((job) => job.status === "ready"); + filtered = filtered.filter( + (job) => job.status === "ready" || job.status === "processing", + ); } else if (activeTab === "discovered") { filtered = filtered.filter( (job) => job.status === "discovered" || job.status === "processing", @@ -35,9 +37,7 @@ export const useFilteredJobs = ( } else if (activeTab === "applied") { filtered = filtered.filter((job) => job.status === "applied"); } else if (activeTab === "all") { - filtered = filtered.filter( - (job) => job.status !== "processing" && job.closedAt == null, - ); + filtered = filtered.filter((job) => job.closedAt == null); } if (activeTab !== "all") { diff --git a/orchestrator/src/client/pages/orchestrator/utils.test.ts b/orchestrator/src/client/pages/orchestrator/utils.test.ts index 3de056d..81b1cf4 100644 --- a/orchestrator/src/client/pages/orchestrator/utils.test.ts +++ b/orchestrator/src/client/pages/orchestrator/utils.test.ts @@ -1,6 +1,6 @@ -import { createAppSettings } from "@shared/testing/factories.js"; +import { createAppSettings, createJob } from "@shared/testing/factories.js"; import { describe, expect, it } from "vitest"; -import { getEnabledSources } from "./utils"; +import { getEnabledSources, getJobCounts } from "./utils"; describe("orchestrator utils", () => { it("enables adzuna only when both app id and key are configured", () => { @@ -16,4 +16,20 @@ describe("orchestrator utils", () => { expect(getEnabledSources(withCreds)).toContain("adzuna"); expect(getEnabledSources(withoutKey)).not.toContain("adzuna"); }); + + it("counts processing jobs in ready and discovered tabs", () => { + const jobs = [ + createJob({ id: "ready", status: "ready", closedAt: null }), + createJob({ id: "processing", status: "processing", closedAt: null }), + createJob({ id: "discovered", status: "discovered", closedAt: null }), + createJob({ id: "applied", status: "applied", closedAt: null }), + ]; + + expect(getJobCounts(jobs)).toEqual({ + ready: 2, + discovered: 2, + applied: 1, + all: 4, + }); + }); }); diff --git a/orchestrator/src/client/pages/orchestrator/utils.ts b/orchestrator/src/client/pages/orchestrator/utils.ts index 74ade1e..11caf7e 100644 --- a/orchestrator/src/client/pages/orchestrator/utils.ts +++ b/orchestrator/src/client/pages/orchestrator/utils.ts @@ -148,7 +148,7 @@ export const getJobCounts = ( for (const job of jobs) { if (job.closedAt != null) continue; if (job.status === "in_progress") continue; - if (job.status === "ready") byTab.ready += 1; + if (job.status === "ready" || job.status === "processing") byTab.ready += 1; if (job.status === "applied") byTab.applied += 1; if (job.status === "discovered" || job.status === "processing") byTab.discovered += 1;