Fix ready-job selection jumping after PDF regeneration (#276)
* Preserve selected ready job during refresh * show processsing in ready
This commit is contained in:
parent
4e91a5ffcd
commit
26275e4ee8
@ -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(
|
||||
<MemoryRouter initialEntries={["/jobs/ready/job-1"]}>
|
||||
<LocationWatcher />
|
||||
<Routes>
|
||||
<Route path="/jobs/:tab" element={<OrchestratorPage />} />
|
||||
<Route path="/jobs/:tab/:jobId" element={<OrchestratorPage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("location")).toHaveTextContent("/ready/job-1");
|
||||
});
|
||||
|
||||
mockJobs = [createJob({ ...jobFixture, id: "job-2", status: "ready" })];
|
||||
mockSelectedJob = null;
|
||||
|
||||
rerender(
|
||||
<MemoryRouter initialEntries={["/jobs/ready/job-1"]}>
|
||||
<LocationWatcher />
|
||||
<Routes>
|
||||
<Route path="/jobs/:tab" element={<OrchestratorPage />} />
|
||||
<Route path="/jobs/:tab/:jobId" element={<OrchestratorPage />} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user