feat(email): expose provider-neutral reads
CI and Deploy / test (pull_request) Failing after 1m37s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-09 21:35:04 +02:00
parent 0e2a59a8ef
commit 536d403b08
4 changed files with 274 additions and 0 deletions
@@ -36,6 +36,10 @@ function renderPage() {
describe('CorrespondenceInboxPage', () => {
beforeEach(() => {
mockedApi.get.mockImplementation((url: string) => {
if (url === '/email/providers') return Promise.resolve({ data: [
{ provider: 'gmail', displayName: 'Gmail', connected: true, address: 'owner@gmail.test', canRead: true, canSend: false },
{ provider: 'microsoft', displayName: 'Outlook', connected: false, address: null, canRead: false, canSend: false },
] } as any);
if (url === '/correspondence') return Promise.resolve({ data: [
{
id: 1,
@@ -76,6 +80,8 @@ describe('CorrespondenceInboxPage', () => {
expect(await screen.findByText(/backend engineer/i)).toBeInTheDocument();
expect(screen.getByText(/2 labels/i)).toBeInTheDocument();
expect(screen.getByText(/1 attachments/i)).toBeInTheDocument();
expect(screen.getByText(/Gmail: owner@gmail\.test · Read only/i)).toBeInTheDocument();
expect(screen.getByText(/Outlook: Not connected · Read only/i)).toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/search/i), { target: { value: 'Maria' } });
fireEvent.mouseDown(screen.getAllByRole('combobox')[0]);
@@ -37,12 +37,22 @@ export type CorrespondenceInboxItem = {
attachmentCount: number;
};
type EmailProviderStatus = {
provider: string;
displayName: string;
connected: boolean;
address?: string | null;
canRead: boolean;
canSend: boolean;
};
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<CorrespondenceInboxItem[]>([]);
const [providers, setProviders] = useState<EmailProviderStatus[]>([]);
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState("");
const [direction, setDirection] = useState<string>("all");
@@ -70,6 +80,12 @@ export default function CorrespondenceInboxPage() {
if (view === "inbox") void load();
}, [load, view]);
useEffect(() => {
api.get<EmailProviderStatus[]>("/email/providers")
.then((response) => setProviders(response.data ?? []))
.catch(() => setProviders([]));
}, []);
const filteredSummary = useMemo(() => {
const linked = items.filter((item) => item.externalThreadId).length;
const inbound = items.filter((item) => item.direction === "inbound").length;
@@ -102,6 +118,18 @@ export default function CorrespondenceInboxPage() {
</Box>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mb: 2 }} aria-label="Email provider status">
{providers.map((provider) => (
<Chip
key={provider.provider}
size="small"
color={provider.connected ? "success" : "default"}
variant="outlined"
label={`${provider.displayName}: ${provider.connected ? provider.address || "Connected" : "Not connected"}${provider.canSend ? " · Send enabled" : " · Read only"}`}
/>
))}
</Box>
{view === "review" ? <GmailReviewPage embedded /> : <>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "2fr 1fr 1fr auto" }, gap: 1.25, mb: 2 }}>
<TextField label="Search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Company, role, recruiter, subject" />