Fix stale job detail showing wrong tab actions on tab switch (#286)

When switching between job tabs (e.g. Discovered → Applied), the detail
panel would briefly show the previously selected job with the new tab's
action buttons — confusing and error-prone.

Three coordinated fixes:
- setActiveTab now checks if the selected job's status fits the target
  tab, keeping it when valid (e.g. Discovered → All Jobs) and clearing
  it otherwise.
- New visibleSelectedJob useMemo guard synchronously nulls out the
  selected job when it doesn't belong to the active tab, eliminating
  the one-frame flash caused by the data hook's useEffect lag.
- Auto-select effect now handles the case where a job passes the status
  check but gets filtered out by source/sponsor/salary filters.
This commit is contained in:
0x1355 2026-03-19 10:39:13 +01:00 committed by GitHub
parent 4894711396
commit f19471ab58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 13 deletions

View File

@ -1090,13 +1090,16 @@ describe("OrchestratorPage", () => {
);
});
// Update mock so selectedJob matches the discovered tab — visibleSelectedJob
// filters out jobs whose status doesn't belong to the active tab.
mockSelectedJob = job2;
fireEvent.click(screen.getByTestId("select-job-2"));
pressKey("r");
await waitFor(() => {
expect(toast.message).toHaveBeenCalledWith("Moving job to Ready...");
// Mock useOrchestratorData returns selectedJob as job-1 always
expect(api.processJob).toHaveBeenCalledWith("job-1");
expect(api.processJob).toHaveBeenCalledWith("job-2");
});
});

View File

@ -7,7 +7,7 @@ import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
import { KeyboardShortcutBar } from "../components/KeyboardShortcutBar";
import { KeyboardShortcutDialog } from "../components/KeyboardShortcutDialog";
import { useDemoInfo } from "../hooks/useDemoInfo";
import type { FilterTab } from "./orchestrator/constants";
import { type FilterTab, tabs } from "./orchestrator/constants";
import { FloatingJobActionsBar } from "./orchestrator/FloatingJobActionsBar";
import { JobCommandBar } from "./orchestrator/JobCommandBar";
import { JobDetailPanel } from "./orchestrator/JobDetailPanel";
@ -93,13 +93,6 @@ export const OrchestratorPage: React.FC = () => {
: false,
);
const setActiveTab = useCallback(
(newTab: FilterTab) => {
navigateWithContext(newTab, selectedJobId);
},
[navigateWithContext, selectedJobId],
);
const handleSelectJobId = useCallback(
(id: string | null) => {
navigateWithContext(activeTab, id);
@ -154,6 +147,35 @@ export const OrchestratorPage: React.FC = () => {
salaryFilter,
sort,
);
const setActiveTab = useCallback(
(newTab: FilterTab) => {
// Keep selected job if it belongs to the target tab, otherwise clear it.
// The auto-select effect will pick the first job on desktop when cleared.
const tabDef = tabs.find((t) => t.id === newTab);
const selectedItem = selectedJobId
? jobs.find((j) => j.id === selectedJobId)
: null;
const jobFitsTab =
selectedItem &&
(tabDef?.statuses.length === 0 ||
tabDef?.statuses.includes(selectedItem.status));
navigateWithContext(newTab, jobFitsTab ? selectedJobId : null);
},
[navigateWithContext, selectedJobId, jobs],
);
// Synchronously null-out selectedJob when it doesn't belong to the current
// tab. The data hook resolves selectedJob from the full (unfiltered) job list
// via useEffect, so it lags by one render frame after a tab switch — without
// this guard the detail panel would briefly show the old job with the new
// tab's action buttons.
const visibleSelectedJob = useMemo(() => {
if (!selectedJob) return null;
const tabDef = tabs.find((t) => t.id === activeTab);
if (!tabDef || tabDef.statuses.length === 0) return selectedJob;
return tabDef.statuses.includes(selectedJob.status) ? selectedJob : null;
}, [selectedJob, activeTab]);
const counts = useMemo(() => getJobCounts(jobs), [jobs]);
const sourcesWithJobs = useMemo(() => getSourcesWithJobs(jobs), [jobs]);
const {
@ -222,7 +244,7 @@ export const OrchestratorPage: React.FC = () => {
activeTab,
activeJobs,
selectedJobId,
selectedJob,
selectedJob: visibleSelectedJob,
selectedJobIds,
isDesktop,
handleSelectJobId,
@ -394,7 +416,7 @@ export const OrchestratorPage: React.FC = () => {
<JobDetailPanel
activeTab={activeTab}
activeJobs={activeJobs}
selectedJob={selectedJob}
selectedJob={visibleSelectedJob}
onSelectJobId={handleSelectJobId}
onJobUpdated={loadJobs}
onPauseRefreshChange={setIsRefreshPaused}
@ -449,7 +471,7 @@ export const OrchestratorPage: React.FC = () => {
<JobDetailPanel
activeTab={activeTab}
activeJobs={activeJobs}
selectedJob={selectedJob}
selectedJob={visibleSelectedJob}
onSelectJobId={handleSelectJobId}
onJobUpdated={loadJobs}
onPauseRefreshChange={setIsRefreshPaused}