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;