From 6008b4afef8f9b6e97ff7d0fb4eb7ba716124fde Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 9 Aug 2026 21:19:27 +0200 Subject: [PATCH] feat(email): consolidate correspondence routes --- job-tracker-ui/src/App.tsx | 3 +- .../src/correspondence-inbox-page.test.tsx | 44 ++++++++++++++++--- .../src/views/CorrespondenceInboxPage.tsx | 28 +++++++----- job-tracker-ui/src/views/GmailReviewPage.tsx | 6 +-- 4 files changed, 59 insertions(+), 22 deletions(-) diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 707e19c..7f16291 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -60,7 +60,6 @@ const AdminAuditPage = lazy(() => import("./views/AdminAuditPage")); const AdminUsersPage = lazy(() => import("./views/AdminUsersPage")); const AdminSystemPage = lazy(() => import("./views/AdminSystemPage")); const CorrespondenceInboxPage = lazy(() => import("./views/CorrespondenceInboxPage")); -const GmailReviewPage = lazy(() => import("./views/GmailReviewPage")); const OperationsPage = lazy(() => import("./views/OperationsPage")); const NotFoundPage = lazy(() => import("./views/NotFoundPage")); @@ -359,7 +358,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/job-tracker-ui/src/correspondence-inbox-page.test.tsx b/job-tracker-ui/src/correspondence-inbox-page.test.tsx index 944bcf1..5671554 100644 --- a/job-tracker-ui/src/correspondence-inbox-page.test.tsx +++ b/job-tracker-ui/src/correspondence-inbox-page.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import '@testing-library/jest-dom'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Navigate, Route, Routes } from 'react-router-dom'; import { ToastProvider } from './toast'; import { I18nProvider } from './i18n/I18nProvider'; import CorrespondenceInboxPage from './views/CorrespondenceInboxPage'; @@ -35,8 +35,8 @@ function renderPage() { describe('CorrespondenceInboxPage', () => { beforeEach(() => { - mockedApi.get.mockResolvedValue({ - data: [ + mockedApi.get.mockImplementation((url: string) => { + if (url === '/correspondence') return Promise.resolve({ data: [ { id: 1, jobApplicationId: 42, @@ -54,8 +54,13 @@ describe('CorrespondenceInboxPage', () => { labelCount: 2, attachmentCount: 1, }, - ], - } as any); + ] } as any); + if (url === '/gmail/review-candidates') return Promise.resolve({ data: { + queries: [], candidateThreadCount: 0, autoLinkThreadCount: 0, reviewThreadCount: 0, unmatchedThreadCount: 0, threads: [], + } } as any); + if (url === '/gmail/suggested-jobs') return Promise.resolve({ data: { count: 0, items: [] } } as any); + return Promise.resolve({ data: {} } as any); + }); }); afterEach(() => { @@ -65,7 +70,7 @@ describe('CorrespondenceInboxPage', () => { test('renders correspondence inbox items and reloads with filters', async () => { renderPage(); - expect(await screen.findByText(/correspondence inbox/i)).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: /job email/i })).toBeInTheDocument(); expect(await screen.findByText(/1 items/i)).toBeInTheDocument(); expect(await screen.findByText(/acme systems/i)).toBeInTheDocument(); expect(await screen.findByText(/backend engineer/i)).toBeInTheDocument(); @@ -85,4 +90,31 @@ describe('CorrespondenceInboxPage', () => { })); }); }); + + test('uses one hub for linked messages and recruitment suggestions', async () => { + renderPage(); + + fireEvent.click(await screen.findByRole('button', { name: /review suggestions/i })); + expect(await screen.findByRole('heading', { name: /recruitment message review/i })).toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledWith('/gmail/review-candidates'); + expect(screen.getByRole('button', { name: /linked messages/i })).toBeInTheDocument(); + }); + + test('legacy Gmail review route redirects into the consolidated hub filter', async () => { + render( + + + + + } /> + } /> + + + + , + ); + + expect(await screen.findByRole('heading', { name: /recruitment message review/i })).toBeInTheDocument(); + expect(mockedApi.get).not.toHaveBeenCalledWith('/correspondence', expect.anything()); + }); }); diff --git a/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx b/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx index d426be6..4e4fe7d 100644 --- a/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx +++ b/job-tracker-ui/src/views/CorrespondenceInboxPage.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { Box, Chip, @@ -17,6 +17,7 @@ import { import MailOutlineIcon from "@mui/icons-material/MailOutline"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; +import GmailReviewPage from "./GmailReviewPage"; export type CorrespondenceInboxItem = { id: number; @@ -38,6 +39,8 @@ export type CorrespondenceInboxItem = { export default function CorrespondenceInboxPage() { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const view = searchParams.get("view") === "review" ? "review" : "inbox"; const { toast } = useToast(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); @@ -64,8 +67,8 @@ export default function CorrespondenceInboxPage() { }, [direction, linkState, query, toast]); useEffect(() => { - void load(); - }, [load]); + if (view === "inbox") void load(); + }, [load, view]); const filteredSummary = useMemo(() => { const linked = items.filter((item) => item.externalThreadId).length; @@ -85,20 +88,22 @@ export default function CorrespondenceInboxPage() { > - Correspondence inbox + Job email - Cross-job view of imported correspondence and Gmail-linked history. + Review linked correspondence and suggested recruitment messages in one place. - } label={`${items.length} items`} variant="outlined" /> - 0 ? "success" : "default"} /> - - + {view === "inbox" ? } label={`${items.length} items`} variant="outlined" /> : null} + {view === "inbox" ? 0 ? "success" : "default"} /> : null} + {view === "inbox" ? : null} + + - + {view === "review" ? : <> + setQuery(e.target.value)} placeholder="Company, role, recruiter, subject" /> Direction @@ -118,7 +123,7 @@ export default function CorrespondenceInboxPage() { - + {loading ? : null} @@ -148,6 +153,7 @@ export default function CorrespondenceInboxPage() { ))} + } ); } diff --git a/job-tracker-ui/src/views/GmailReviewPage.tsx b/job-tracker-ui/src/views/GmailReviewPage.tsx index ccecce2..fd0b7ea 100644 --- a/job-tracker-ui/src/views/GmailReviewPage.tsx +++ b/job-tracker-ui/src/views/GmailReviewPage.tsx @@ -5,7 +5,7 @@ import { CreatedSuggestedGmailJobResult, GmailManualSyncResult, GmailReviewQueue import { useToast } from "../toast"; import { useNavigate } from "react-router-dom"; -export default function GmailReviewPage() { +export default function GmailReviewPage({ embedded = false }: { embedded?: boolean }) { const { toast } = useToast(); const navigate = useNavigate(); const [data, setData] = useState(null); @@ -134,7 +134,7 @@ export default function GmailReviewPage() { > - Gmail review queue + {embedded ? "Recruitment message review" : "Gmail review queue"} Manual sync, high-confidence auto-linking, medium-confidence review, and suggested jobs from unmatched Gmail threads. @@ -146,7 +146,7 @@ export default function GmailReviewPage() { - + {!embedded ? : null}