diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx index 84e0014..86fb483 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx @@ -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"); }); }); diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 27642d0..ff4617b 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -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 = () => { {