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:
parent
4894711396
commit
f19471ab58
@ -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"));
|
fireEvent.click(screen.getByTestId("select-job-2"));
|
||||||
|
|
||||||
pressKey("r");
|
pressKey("r");
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(toast.message).toHaveBeenCalledWith("Moving job to Ready...");
|
expect(toast.message).toHaveBeenCalledWith("Moving job to Ready...");
|
||||||
// Mock useOrchestratorData returns selectedJob as job-1 always
|
expect(api.processJob).toHaveBeenCalledWith("job-2");
|
||||||
expect(api.processJob).toHaveBeenCalledWith("job-1");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { Drawer, DrawerClose, DrawerContent } from "@/components/ui/drawer";
|
|||||||
import { KeyboardShortcutBar } from "../components/KeyboardShortcutBar";
|
import { KeyboardShortcutBar } from "../components/KeyboardShortcutBar";
|
||||||
import { KeyboardShortcutDialog } from "../components/KeyboardShortcutDialog";
|
import { KeyboardShortcutDialog } from "../components/KeyboardShortcutDialog";
|
||||||
import { useDemoInfo } from "../hooks/useDemoInfo";
|
import { useDemoInfo } from "../hooks/useDemoInfo";
|
||||||
import type { FilterTab } from "./orchestrator/constants";
|
import { type FilterTab, tabs } from "./orchestrator/constants";
|
||||||
import { FloatingJobActionsBar } from "./orchestrator/FloatingJobActionsBar";
|
import { FloatingJobActionsBar } from "./orchestrator/FloatingJobActionsBar";
|
||||||
import { JobCommandBar } from "./orchestrator/JobCommandBar";
|
import { JobCommandBar } from "./orchestrator/JobCommandBar";
|
||||||
import { JobDetailPanel } from "./orchestrator/JobDetailPanel";
|
import { JobDetailPanel } from "./orchestrator/JobDetailPanel";
|
||||||
@ -93,13 +93,6 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
: false,
|
: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
(newTab: FilterTab) => {
|
|
||||||
navigateWithContext(newTab, selectedJobId);
|
|
||||||
},
|
|
||||||
[navigateWithContext, selectedJobId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSelectJobId = useCallback(
|
const handleSelectJobId = useCallback(
|
||||||
(id: string | null) => {
|
(id: string | null) => {
|
||||||
navigateWithContext(activeTab, id);
|
navigateWithContext(activeTab, id);
|
||||||
@ -154,6 +147,35 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
salaryFilter,
|
salaryFilter,
|
||||||
sort,
|
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 counts = useMemo(() => getJobCounts(jobs), [jobs]);
|
||||||
const sourcesWithJobs = useMemo(() => getSourcesWithJobs(jobs), [jobs]);
|
const sourcesWithJobs = useMemo(() => getSourcesWithJobs(jobs), [jobs]);
|
||||||
const {
|
const {
|
||||||
@ -222,7 +244,7 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
activeTab,
|
activeTab,
|
||||||
activeJobs,
|
activeJobs,
|
||||||
selectedJobId,
|
selectedJobId,
|
||||||
selectedJob,
|
selectedJob: visibleSelectedJob,
|
||||||
selectedJobIds,
|
selectedJobIds,
|
||||||
isDesktop,
|
isDesktop,
|
||||||
handleSelectJobId,
|
handleSelectJobId,
|
||||||
@ -394,7 +416,7 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
<JobDetailPanel
|
<JobDetailPanel
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
activeJobs={activeJobs}
|
activeJobs={activeJobs}
|
||||||
selectedJob={selectedJob}
|
selectedJob={visibleSelectedJob}
|
||||||
onSelectJobId={handleSelectJobId}
|
onSelectJobId={handleSelectJobId}
|
||||||
onJobUpdated={loadJobs}
|
onJobUpdated={loadJobs}
|
||||||
onPauseRefreshChange={setIsRefreshPaused}
|
onPauseRefreshChange={setIsRefreshPaused}
|
||||||
@ -449,7 +471,7 @@ export const OrchestratorPage: React.FC = () => {
|
|||||||
<JobDetailPanel
|
<JobDetailPanel
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
activeJobs={activeJobs}
|
activeJobs={activeJobs}
|
||||||
selectedJob={selectedJob}
|
selectedJob={visibleSelectedJob}
|
||||||
onSelectJobId={handleSelectJobId}
|
onSelectJobId={handleSelectJobId}
|
||||||
onJobUpdated={loadJobs}
|
onJobUpdated={loadJobs}
|
||||||
onPauseRefreshChange={setIsRefreshPaused}
|
onPauseRefreshChange={setIsRefreshPaused}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user