From 3652abab3e5ef9bb540df90115eeca236937f6d6 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Fri, 23 Jan 2026 13:39:28 +0000 Subject: [PATCH 1/3] codex oneshots ftw! --- .../client/pages/OrchestratorPage.test.tsx | 30 ++++++ .../src/client/pages/OrchestratorPage.tsx | 38 +++++--- .../orchestrator/OrchestratorFilters.test.tsx | 12 +++ .../orchestrator/OrchestratorFilters.tsx | 17 +++- .../orchestrator/OrchestratorHeader.test.tsx | 94 +++++++++++++++++++ .../pages/orchestrator/OrchestratorHeader.tsx | 44 +++++---- .../orchestrator/usePipelineSources.test.ts | 45 +++++++++ .../pages/orchestrator/usePipelineSources.ts | 34 +++++-- .../src/client/pages/orchestrator/utils.ts | 39 +++++++- 9 files changed, 310 insertions(+), 43 deletions(-) create mode 100644 orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx create mode 100644 orchestrator/src/client/pages/orchestrator/usePipelineSources.test.ts diff --git a/orchestrator/src/client/pages/OrchestratorPage.test.tsx b/orchestrator/src/client/pages/OrchestratorPage.test.tsx index 186700f..32a9fb2 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.test.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.test.tsx @@ -105,6 +105,16 @@ vi.mock("./orchestrator/usePipelineSources", () => ({ }), })); +vi.mock("../hooks/useSettings", () => ({ + useSettings: () => ({ + settings: { + jobspySites: ["indeed", "linkedin"], + ukvisajobsEmail: null, + ukvisajobsPasswordHint: null, + }, + }), +})); + vi.mock("./orchestrator/OrchestratorHeader", () => ({ OrchestratorHeader: () =>
, })); @@ -118,12 +128,15 @@ vi.mock("./orchestrator/OrchestratorFilters", () => ({ onTabChange, onSearchQueryChange, onSortChange, + sourcesWithJobs, }: { onTabChange: (t: FilterTab) => void; onSearchQueryChange: (q: string) => void; onSortChange: (s: any) => void; + sourcesWithJobs: string[]; }) => (
+
{sourcesWithJobs.join(",")}
@@ -274,4 +287,21 @@ describe("OrchestratorPage", () => { expect(screen.getByTestId("detail-panel")).toBeInTheDocument(); }); + + it("clears source filter when no jobs exist for it", async () => { + window.matchMedia = createMatchMedia(true) as unknown as typeof window.matchMedia; + + render( + + + + } /> + + + ); + + await waitFor(() => { + expect(screen.getByTestId("location").textContent).not.toContain("source=ukvisajobs"); + }); + }); }); diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index c6fc6ee..08fdb99 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -22,7 +22,8 @@ import { OrchestratorSummary } from "./orchestrator/OrchestratorSummary"; import { useFilteredJobs } from "./orchestrator/useFilteredJobs"; import { useOrchestratorData } from "./orchestrator/useOrchestratorData"; import { usePipelineSources } from "./orchestrator/usePipelineSources"; -import { getJobCounts } from "./orchestrator/utils"; +import { useSettings } from "@client/hooks/useSettings"; +import { getEnabledSources, getJobCounts, getSourcesWithJobs } from "./orchestrator/utils"; export const OrchestratorPage: React.FC = () => { const { tab, jobId } = useParams<{ tab: string; jobId?: string }>(); @@ -106,6 +107,7 @@ export const OrchestratorPage: React.FC = () => { } }, [tab, navigateWithContext]); + const [navOpen, setNavOpen] = useState(false); const [isManualImportOpen, setIsManualImportOpen] = useState(false); const [isDetailDrawerOpen, setIsDetailDrawerOpen] = useState(false); @@ -121,16 +123,26 @@ export const OrchestratorPage: React.FC = () => { navigateWithContext(activeTab, id); }; - const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(); + const { settings } = useSettings(); const { jobs, stats, isLoading, isPipelineRunning, setIsPipelineRunning, loadJobs } = useOrchestratorData(); + const enabledSources = useMemo(() => getEnabledSources(settings ?? null), [settings]); + const { pipelineSources, setPipelineSources, toggleSource } = usePipelineSources(enabledSources); const activeJobs = useFilteredJobs(jobs, activeTab, sourceFilter, searchQuery, sort); const counts = useMemo(() => getJobCounts(jobs), [jobs]); + const sourcesWithJobs = useMemo(() => getSourcesWithJobs(jobs), [jobs]); const selectedJob = useMemo( () => (selectedJobId ? jobs.find((job) => job.id === selectedJobId) ?? null : null), [jobs, selectedJobId], ); + useEffect(() => { + if (sourceFilter === "all") return; + if (!sourcesWithJobs.includes(sourceFilter)) { + setSourceFilter("all"); + } + }, [sourceFilter, setSourceFilter, sourcesWithJobs]); + const handleManualImported = useCallback( async (importedJobId: string) => { // Refresh jobs and navigate to the new job in discovered tab @@ -225,16 +237,17 @@ export const OrchestratorPage: React.FC = () => { return ( <> - setIsManualImportOpen(true)} - /> + setIsManualImportOpen(true)} + />
@@ -249,6 +262,7 @@ export const OrchestratorPage: React.FC = () => { onSearchQueryChange={setSearchQuery} sourceFilter={sourceFilter} onSourceFilterChange={setSourceFilter} + sourcesWithJobs={sourcesWithJobs} sort={sort} onSortChange={setSort} /> diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.test.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.test.tsx index 92f8bef..aaacea6 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.test.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.test.tsx @@ -71,6 +71,7 @@ const renderFilters = (overrides?: Partial { fireEvent.click(await screen.findByRole("menuitem", { name: /Direction:/i })); expect(props.onSortChange).toHaveBeenCalledWith({ key: "score", direction: "asc" }); }); + + it("only shows sources that exist in jobs", async () => { + renderFilters({ sourcesWithJobs: ["gradcracker", "manual"] }); + + fireEvent.pointerDown(screen.getByRole("button", { name: /all sources/i })); + + expect(await screen.findByRole("menuitemradio", { name: /Gradcracker/i })).toBeInTheDocument(); + expect(screen.queryByRole("menuitemradio", { name: /LinkedIn/i })).not.toBeInTheDocument(); + expect(screen.queryByRole("menuitemradio", { name: /UK Visa Jobs/i })).not.toBeInTheDocument(); + expect(screen.getByRole("menuitemradio", { name: /Manual/i })).toBeInTheDocument(); + }); }); diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx index 3303e10..4ded3d4 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx @@ -17,7 +17,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { sourceLabel } from "@/lib/utils"; import type { JobSource } from "../../../shared/types"; -import { defaultSortDirection, sortLabels, tabs } from "./constants"; +import { defaultSortDirection, orderedSources, sortLabels, tabs } from "./constants"; import type { FilterTab, JobSort } from "./constants"; interface OrchestratorFiltersProps { @@ -28,6 +28,7 @@ interface OrchestratorFiltersProps { onSearchQueryChange: (value: string) => void; sourceFilter: JobSource | "all"; onSourceFilterChange: (value: JobSource | "all") => void; + sourcesWithJobs: JobSource[]; sort: JobSort; onSortChange: (sort: JobSort) => void; } @@ -40,9 +41,14 @@ export const OrchestratorFilters: React.FC = ({ onSearchQueryChange, sourceFilter, onSourceFilterChange, + sourcesWithJobs, sort, onSortChange, -}) => ( +}) => { + const orderedFilterSources: JobSource[] = [...orderedSources, "manual"]; + const visibleSources = orderedFilterSources.filter((source) => sourcesWithJobs.includes(source)); + + return ( onTabChange(value as FilterTab)}>
@@ -85,9 +91,9 @@ export const OrchestratorFilters: React.FC = ({ onValueChange={(value) => onSourceFilterChange(value as JobSource | "all")} > All Sources - {(Object.keys(sourceLabel) as JobSource[]).map((key) => ( - - {sourceLabel[key]} + {visibleSources.map((source) => ( + + {sourceLabel[source]} ))} @@ -140,3 +146,4 @@ export const OrchestratorFilters: React.FC = ({
); +}; diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx new file mode 100644 index 0000000..7eee1fa --- /dev/null +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { describe, it, expect, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; + +import { OrchestratorHeader } from "./OrchestratorHeader"; + +vi.mock("@/components/ui/dropdown-menu", () => { + const React = require("react") as typeof import("react"); + + return { + DropdownMenu: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownMenuTrigger: ({ children }: { children: React.ReactNode }) => <>{children}, + DropdownMenuContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownMenuLabel: ({ children }: { children: React.ReactNode }) =>
{children}
, + DropdownMenuSeparator: () =>
, + DropdownMenuItem: ({ children, onSelect }: { children: React.ReactNode; onSelect?: (event: Event) => void }) => ( + + ), + DropdownMenuCheckboxItem: ({ + children, + onCheckedChange, + }: { + children: React.ReactNode; + onCheckedChange?: (checked: boolean) => void; + }) => ( + + ), + }; +}); + +vi.mock("@/components/ui/sheet", () => ({ + Sheet: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +const renderHeader = (overrides: Partial> = {}) => { + const props: React.ComponentProps = { + navOpen: false, + onNavOpenChange: vi.fn(), + isPipelineRunning: false, + pipelineSources: ["gradcracker"], + enabledSources: ["gradcracker"], + onToggleSource: vi.fn(), + onSetPipelineSources: vi.fn(), + onRunPipeline: vi.fn(), + onOpenManualImport: vi.fn(), + ...overrides, + }; + + return { + props, + ...render( + + + + ), + }; +}; + +describe("OrchestratorHeader", () => { + it("renders only enabled sources", () => { + renderHeader({ enabledSources: ["gradcracker", "linkedin"], pipelineSources: ["linkedin"] }); + + expect(screen.getByRole("menuitemcheckbox", { name: /Gradcracker/i })).toBeInTheDocument(); + expect(screen.getByRole("menuitemcheckbox", { name: /LinkedIn/i })).toBeInTheDocument(); + expect(screen.queryByRole("menuitemcheckbox", { name: /UK Visa Jobs/i })).not.toBeInTheDocument(); + }); + + it("uses enabled sources for the all sources action", () => { + const { props } = renderHeader({ enabledSources: ["gradcracker", "linkedin"] }); + + fireEvent.click(screen.getByRole("menuitem", { name: /All sources/i })); + + expect(props.onSetPipelineSources).toHaveBeenCalledWith(["gradcracker", "linkedin"]); + }); + + it("hides jobspy preset when no jobspy sources are enabled", () => { + renderHeader({ enabledSources: ["gradcracker"] }); + + expect(screen.queryByRole("menuitem", { name: /Indeed \+ LinkedIn only/i })).not.toBeInTheDocument(); + }); +}); diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx index a620965..5bd9a8b 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx @@ -40,6 +40,7 @@ interface OrchestratorHeaderProps { onNavOpenChange: (open: boolean) => void; isPipelineRunning: boolean; pipelineSources: JobSource[]; + enabledSources: JobSource[]; onToggleSource: (source: JobSource, checked: boolean) => void; onSetPipelineSources: (sources: JobSource[]) => void; onRunPipeline: () => void; @@ -58,6 +59,7 @@ export const OrchestratorHeader: React.FC = ({ onNavOpenChange, isPipelineRunning, pipelineSources, + enabledSources, onToggleSource, onSetPipelineSources, onRunPipeline, @@ -65,6 +67,8 @@ export const OrchestratorHeader: React.FC = ({ }) => { const location = useLocation(); const navigate = useNavigate(); + const visibleSources = orderedSources.filter((source) => enabledSources.includes(source)); + const enabledJobSpySources = visibleSources.filter((source) => source === "indeed" || source === "linkedin"); return (
@@ -162,7 +166,7 @@ export const OrchestratorHeader: React.FC = ({ Sources - {orderedSources.map((source) => ( + {visibleSources.map((source) => ( = ({ { event.preventDefault(); - onSetPipelineSources(orderedSources); + onSetPipelineSources(visibleSources); }} > All sources - { - event.preventDefault(); - onSetPipelineSources(["gradcracker"]); - }} - > - Gradcracker only - - { - event.preventDefault(); - onSetPipelineSources(["indeed", "linkedin"]); - }} - > - Indeed + LinkedIn only - + {visibleSources.includes("gradcracker") && ( + { + event.preventDefault(); + onSetPipelineSources(["gradcracker"]); + }} + > + Gradcracker only + + )} + {enabledJobSpySources.length > 0 && ( + { + event.preventDefault(); + onSetPipelineSources(enabledJobSpySources); + }} + > + Indeed + LinkedIn only + + )}
diff --git a/orchestrator/src/client/pages/orchestrator/usePipelineSources.test.ts b/orchestrator/src/client/pages/orchestrator/usePipelineSources.test.ts new file mode 100644 index 0000000..4be48e8 --- /dev/null +++ b/orchestrator/src/client/pages/orchestrator/usePipelineSources.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { renderHook, act } from "@testing-library/react"; + +import { PIPELINE_SOURCES_STORAGE_KEY } from "./constants"; +import { usePipelineSources } from "./usePipelineSources"; + +describe("usePipelineSources", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("filters stored sources to enabled sources", () => { + localStorage.setItem(PIPELINE_SOURCES_STORAGE_KEY, JSON.stringify(["gradcracker", "ukvisajobs"])); + + const enabledSources = ["gradcracker"] as const; + + const { result } = renderHook(() => usePipelineSources(enabledSources)); + + expect(result.current.pipelineSources).toEqual(["gradcracker"]); + }); + + it("falls back to the first enabled source", () => { + localStorage.setItem(PIPELINE_SOURCES_STORAGE_KEY, JSON.stringify(["ukvisajobs"])); + + const enabledSources = ["gradcracker", "linkedin"] as const; + + const { result } = renderHook(() => usePipelineSources(enabledSources)); + + expect(result.current.pipelineSources).toEqual(["gradcracker"]); + }); + + it("ignores toggles for disabled sources", () => { + localStorage.setItem(PIPELINE_SOURCES_STORAGE_KEY, JSON.stringify(["gradcracker"])); + + const enabledSources = ["gradcracker"] as const; + + const { result } = renderHook(() => usePipelineSources(enabledSources)); + + act(() => { + result.current.toggleSource("ukvisajobs", true); + }); + + expect(result.current.pipelineSources).toEqual(["gradcracker"]); + }); +}); diff --git a/orchestrator/src/client/pages/orchestrator/usePipelineSources.ts b/orchestrator/src/client/pages/orchestrator/usePipelineSources.ts index 919036a..7be198d 100644 --- a/orchestrator/src/client/pages/orchestrator/usePipelineSources.ts +++ b/orchestrator/src/client/pages/orchestrator/usePipelineSources.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import type { JobSource } from "../../../shared/types"; import { @@ -7,20 +7,39 @@ import { orderedSources, } from "./constants"; -export const usePipelineSources = () => { +const resolveAllowedSources = (enabledSources?: JobSource[]) => + enabledSources && enabledSources.length > 0 ? enabledSources : DEFAULT_PIPELINE_SOURCES; + +const normalizeSources = (sources: JobSource[], allowedSources: JobSource[]) => { + const filtered = sources.filter((value) => allowedSources.includes(value)); + return filtered.length > 0 ? filtered : allowedSources.slice(0, 1); +}; + +const sourcesMatch = (left: JobSource[], right: JobSource[]) => + left.length === right.length && left.every((value, index) => value === right[index]); + +export const usePipelineSources = (enabledSources?: JobSource[]) => { + const allowedSources = useMemo(() => resolveAllowedSources(enabledSources), [enabledSources]); const [pipelineSources, setPipelineSources] = useState(() => { try { const raw = localStorage.getItem(PIPELINE_SOURCES_STORAGE_KEY); - if (!raw) return DEFAULT_PIPELINE_SOURCES; + if (!raw) return normalizeSources(allowedSources, allowedSources); const parsed = JSON.parse(raw) as unknown; - if (!Array.isArray(parsed)) return DEFAULT_PIPELINE_SOURCES; + if (!Array.isArray(parsed)) return normalizeSources(allowedSources, allowedSources); const next = parsed.filter((value): value is JobSource => orderedSources.includes(value as JobSource)); - return next.length > 0 ? next : DEFAULT_PIPELINE_SOURCES; + return normalizeSources(next, allowedSources); } catch { - return DEFAULT_PIPELINE_SOURCES; + return normalizeSources(allowedSources, allowedSources); } }); + useEffect(() => { + setPipelineSources((current) => { + const normalized = normalizeSources(current, allowedSources); + return sourcesMatch(current, normalized) ? current : normalized; + }); + }, [allowedSources]); + useEffect(() => { try { localStorage.setItem(PIPELINE_SOURCES_STORAGE_KEY, JSON.stringify(pipelineSources)); @@ -30,6 +49,7 @@ export const usePipelineSources = () => { }, [pipelineSources]); const toggleSource = useCallback((source: JobSource, checked: boolean) => { + if (!allowedSources.includes(source)) return; setPipelineSources((current) => { const next = checked ? Array.from(new Set([...current, source])) @@ -37,7 +57,7 @@ export const usePipelineSources = () => { return next.length === 0 ? current : next; }); - }, []); + }, [allowedSources]); return { pipelineSources, setPipelineSources, toggleSource }; }; diff --git a/orchestrator/src/client/pages/orchestrator/utils.ts b/orchestrator/src/client/pages/orchestrator/utils.ts index 8e84ab6..1ddbb11 100644 --- a/orchestrator/src/client/pages/orchestrator/utils.ts +++ b/orchestrator/src/client/pages/orchestrator/utils.ts @@ -1,4 +1,5 @@ -import type { Job } from "../../../shared/types"; +import type { AppSettings, Job, JobSource } from "../../../shared/types"; +import { orderedSources } from "./constants"; import type { FilterTab, JobSort } from "./constants"; const dateValue = (value: string | null) => { @@ -87,3 +88,39 @@ export const getJobCounts = (jobs: Job[]): Record => { return byTab; }; + +const orderedSourceFilters: JobSource[] = [...orderedSources, "manual"]; + +export const getSourcesWithJobs = (jobs: Job[]): JobSource[] => { + const seen = new Set(); + for (const job of jobs) { + seen.add(job.source); + } + return orderedSourceFilters.filter((source) => seen.has(source)); +}; + +export const getEnabledSources = (settings: AppSettings | null): JobSource[] => { + if (!settings) return [...orderedSources]; + + const enabled: JobSource[] = []; + const jobspySites = settings.jobspySites ?? []; + const hasUkVisaJobsAuth = Boolean( + settings.ukvisajobsEmail?.trim() && settings.ukvisajobsPasswordHint + ); + + for (const source of orderedSources) { + if (source === "gradcracker") { + enabled.push(source); + continue; + } + if (source === "ukvisajobs") { + if (hasUkVisaJobsAuth) enabled.push(source); + continue; + } + if (source === "indeed" || source === "linkedin") { + if (jobspySites.includes(source)) enabled.push(source); + } + } + + return enabled.length > 0 ? enabled : [...orderedSources]; +}; From d7cbf81808f31fb8fc94976184e301962f83d227 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Fri, 23 Jan 2026 13:43:37 +0000 Subject: [PATCH 2/3] button shows how many selected --- .../orchestrator/OrchestratorHeader.test.tsx | 7 ++-- .../pages/orchestrator/OrchestratorHeader.tsx | 41 ++++++------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx index 7eee1fa..0e215a3 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.test.tsx @@ -81,14 +81,15 @@ describe("OrchestratorHeader", () => { it("uses enabled sources for the all sources action", () => { const { props } = renderHeader({ enabledSources: ["gradcracker", "linkedin"] }); - fireEvent.click(screen.getByRole("menuitem", { name: /All sources/i })); + fireEvent.click(screen.getByRole("menuitemcheckbox", { name: /Select all sources/i })); expect(props.onSetPipelineSources).toHaveBeenCalledWith(["gradcracker", "linkedin"]); }); - it("hides jobspy preset when no jobspy sources are enabled", () => { - renderHeader({ enabledSources: ["gradcracker"] }); + it("does not show source presets", () => { + renderHeader({ enabledSources: ["gradcracker", "linkedin"] }); + expect(screen.queryByRole("menuitem", { name: /Gradcracker only/i })).not.toBeInTheDocument(); expect(screen.queryByRole("menuitem", { name: /Indeed \+ LinkedIn only/i })).not.toBeInTheDocument(); }); }); diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx index 5bd9a8b..ddfed4a 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorHeader.tsx @@ -18,7 +18,6 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, - DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, @@ -68,7 +67,8 @@ export const OrchestratorHeader: React.FC = ({ const location = useLocation(); const navigate = useNavigate(); const visibleSources = orderedSources.filter((source) => enabledSources.includes(source)); - const enabledJobSpySources = visibleSources.filter((source) => source === "indeed" || source === "linkedin"); + const allSourcesSelected = + visibleSources.length > 0 && visibleSources.every((source) => pipelineSources.includes(source)); return (
@@ -148,7 +148,9 @@ export const OrchestratorHeader: React.FC = ({ className="gap-2" > {isPipelineRunning ? : } - {isPipelineRunning ? "Running" : "Run pipeline"} + + {isPipelineRunning ? `Running (${pipelineSources.length})` : `Run pipeline (${pipelineSources.length})`} + @@ -177,34 +179,15 @@ export const OrchestratorHeader: React.FC = ({ ))} - { - event.preventDefault(); - onSetPipelineSources(visibleSources); + { + onSetPipelineSources(checked ? visibleSources : visibleSources.slice(0, 1)); }} + onSelect={(event) => event.preventDefault()} > - All sources - - {visibleSources.includes("gradcracker") && ( - { - event.preventDefault(); - onSetPipelineSources(["gradcracker"]); - }} - > - Gradcracker only - - )} - {enabledJobSpySources.length > 0 && ( - { - event.preventDefault(); - onSetPipelineSources(enabledJobSpySources); - }} - > - Indeed + LinkedIn only - - )} + Select all sources +
From b2c42fadef86eb3944b6113110ac3c6398784011 Mon Sep 17 00:00:00 2001 From: DaKheera47 Date: Fri, 23 Jan 2026 13:47:04 +0000 Subject: [PATCH 3/3] fix: fix filter race condition and dedup filter source logic --- orchestrator/src/client/pages/OrchestratorPage.tsx | 4 ++-- .../src/client/pages/orchestrator/OrchestratorFilters.tsx | 3 +-- orchestrator/src/client/pages/orchestrator/constants.ts | 1 + orchestrator/src/client/pages/orchestrator/utils.ts | 6 ++---- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/orchestrator/src/client/pages/OrchestratorPage.tsx b/orchestrator/src/client/pages/OrchestratorPage.tsx index 08fdb99..b09df32 100644 --- a/orchestrator/src/client/pages/OrchestratorPage.tsx +++ b/orchestrator/src/client/pages/OrchestratorPage.tsx @@ -137,11 +137,11 @@ export const OrchestratorPage: React.FC = () => { ); useEffect(() => { - if (sourceFilter === "all") return; + if (isLoading || sourceFilter === "all") return; if (!sourcesWithJobs.includes(sourceFilter)) { setSourceFilter("all"); } - }, [sourceFilter, setSourceFilter, sourcesWithJobs]); + }, [isLoading, sourceFilter, setSourceFilter, sourcesWithJobs]); const handleManualImported = useCallback( async (importedJobId: string) => { diff --git a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx index 4ded3d4..20269e9 100644 --- a/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx +++ b/orchestrator/src/client/pages/orchestrator/OrchestratorFilters.tsx @@ -17,7 +17,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { sourceLabel } from "@/lib/utils"; import type { JobSource } from "../../../shared/types"; -import { defaultSortDirection, orderedSources, sortLabels, tabs } from "./constants"; +import { defaultSortDirection, orderedFilterSources, sortLabels, tabs } from "./constants"; import type { FilterTab, JobSort } from "./constants"; interface OrchestratorFiltersProps { @@ -45,7 +45,6 @@ export const OrchestratorFilters: React.FC = ({ sort, onSortChange, }) => { - const orderedFilterSources: JobSource[] = [...orderedSources, "manual"]; const visibleSources = orderedFilterSources.filter((source) => sourcesWithJobs.includes(source)); return ( diff --git a/orchestrator/src/client/pages/orchestrator/constants.ts b/orchestrator/src/client/pages/orchestrator/constants.ts index 0e05337..b79398a 100644 --- a/orchestrator/src/client/pages/orchestrator/constants.ts +++ b/orchestrator/src/client/pages/orchestrator/constants.ts @@ -4,6 +4,7 @@ export const DEFAULT_PIPELINE_SOURCES: JobSource[] = ["gradcracker", "indeed", " export const PIPELINE_SOURCES_STORAGE_KEY = "jobops.pipeline.sources"; export const orderedSources: JobSource[] = ["gradcracker", "indeed", "linkedin", "ukvisajobs"]; +export const orderedFilterSources: JobSource[] = [...orderedSources, "manual"]; export const statusTokens: Record = { discovered: { diff --git a/orchestrator/src/client/pages/orchestrator/utils.ts b/orchestrator/src/client/pages/orchestrator/utils.ts index 1ddbb11..0f9da71 100644 --- a/orchestrator/src/client/pages/orchestrator/utils.ts +++ b/orchestrator/src/client/pages/orchestrator/utils.ts @@ -1,5 +1,5 @@ import type { AppSettings, Job, JobSource } from "../../../shared/types"; -import { orderedSources } from "./constants"; +import { orderedFilterSources, orderedSources } from "./constants"; import type { FilterTab, JobSort } from "./constants"; const dateValue = (value: string | null) => { @@ -89,14 +89,12 @@ export const getJobCounts = (jobs: Job[]): Record => { return byTab; }; -const orderedSourceFilters: JobSource[] = [...orderedSources, "manual"]; - export const getSourcesWithJobs = (jobs: Job[]): JobSource[] => { const seen = new Set(); for (const job of jobs) { seen.add(job.source); } - return orderedSourceFilters.filter((source) => seen.has(source)); + return orderedFilterSources.filter((source) => seen.has(source)); }; export const getEnabledSources = (settings: AppSettings | null): JobSource[] => {