fix(email): clarify provider capability states

This commit is contained in:
cesnimda
2026-08-10 09:45:30 +02:00
parent 1e514272e1
commit f9e641c23b
2 changed files with 37 additions and 5 deletions
@@ -90,8 +90,9 @@ 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();
expect(screen.getByText(/Gmail: owner@gmail\.test · Read only; reconnect to enable send/i)).toBeInTheDocument();
expect(screen.getByText(/^Outlook: Not connected$/i)).toBeInTheDocument();
expect(screen.queryByText(/Outlook: Not connected.*Read only/i)).not.toBeInTheDocument();
fireEvent.change(screen.getByLabelText(/search/i), { target: { value: 'Maria' } });
fireEvent.mouseDown(screen.getAllByRole('combobox')[0]);
@@ -107,6 +108,20 @@ describe('CorrespondenceInboxPage', () => {
});
});
test('keeps saved correspondence visible when provider status is unavailable', async () => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => {
if (url === '/email/providers') return Promise.reject(new Error('provider status unavailable'));
return original!(url, config);
});
renderPage();
expect(await screen.findByText(/email provider status is temporarily unavailable/i)).toBeInTheDocument();
expect(await screen.findByText(/acme systems/i)).toBeInTheDocument();
expect(screen.getByText(/saved jobtracker correspondence remains available/i)).toBeInTheDocument();
});
test('opens provider-backed plain-text message detail in the hub', async () => {
renderPage();
@@ -50,6 +50,13 @@ type EmailProviderStatus = {
canSend: boolean;
};
export function emailProviderStatusLabel(provider: EmailProviderStatus) {
if (!provider.connected) return `${provider.displayName}: Not connected`;
const identity = provider.address || "Connected account";
if (!provider.canRead) return `${provider.displayName}: ${identity} · Mailbox access unavailable`;
return `${provider.displayName}: ${identity}${provider.canSend ? " · Read + send" : " · Read only; reconnect to enable send"}`;
}
type EmailMessageDetail = {
id: string;
threadId: string;
@@ -94,6 +101,7 @@ export default function CorrespondenceInboxPage() {
const { confirm } = useConfirm();
const [items, setItems] = useState<CorrespondenceInboxItem[]>([]);
const [providers, setProviders] = useState<EmailProviderStatus[]>([]);
const [providerStatusError, setProviderStatusError] = useState(false);
const [selectedMessageId, setSelectedMessageId] = useState<number | null>(null);
const [messageDetail, setMessageDetail] = useState<EmailMessageDetail | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
@@ -134,8 +142,14 @@ export default function CorrespondenceInboxPage() {
useEffect(() => {
api.get<EmailProviderStatus[]>("/email/providers")
.then((response) => setProviders(response.data ?? []))
.catch(() => setProviders([]));
.then((response) => {
setProviders(response.data ?? []);
setProviderStatusError(false);
})
.catch(() => {
setProviders([]);
setProviderStatusError(true);
});
}, []);
const filteredSummary = useMemo(() => {
@@ -346,10 +360,13 @@ export default function CorrespondenceInboxPage() {
size="small"
color={provider.connected ? "success" : "default"}
variant="outlined"
label={`${provider.displayName}: ${provider.connected ? provider.address || "Connected" : "Not connected"}${provider.canSend ? " · Send access granted" : " · Read only"}`}
label={emailProviderStatusLabel(provider)}
/>
))}
</Box>
{providerStatusError ? (
<Alert severity="warning" sx={{ mb: 2 }}>Email provider status is temporarily unavailable. Saved JobTracker correspondence remains available.</Alert>
) : null}
{view === "review" ? <GmailReviewPage embedded /> : <>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "2fr 1fr 1fr auto" }, gap: 1.25, mb: 2 }}>