feat(ui): show recorded application context beside opened mail
This commit is contained in:
@@ -53,7 +53,7 @@ function pageData(items: ReturnType<typeof messageItem>[], page = 1, total = ite
|
||||
}
|
||||
|
||||
function messageRow(subject: string) {
|
||||
return within(screen.getByText(subject, { exact: true }).closest('.MuiPaper-root') as HTMLElement);
|
||||
return within(screen.getByRole('article', { name: subject }));
|
||||
}
|
||||
|
||||
function LocationProbe() {
|
||||
@@ -118,11 +118,29 @@ function renderPage(initialPath = '/') {
|
||||
}
|
||||
|
||||
describe('CorrespondenceInboxPage', () => {
|
||||
test.each(['en', 'nb'] as const)('%s opened message retains one application action in named context', async language => {
|
||||
localStorage.setItem('uiLanguage', language); renderPage();
|
||||
await screen.findByText('Interview invite', { exact: true });
|
||||
const row = messageRow('Interview invite');
|
||||
fireEvent.click(row.getByRole('button', { name: copy('emailInboxViewMessage', {}, language) }));
|
||||
const context = await row.findByRole('complementary', { name: copy('emailInboxJob', {}, language) });
|
||||
expect(context).toHaveTextContent('Acme Systems');
|
||||
expect(context).toHaveTextContent('Backend Engineer');
|
||||
expect(row.getAllByRole('button', { name: copy('emailInboxOpenJob', {}, language) })).toHaveLength(1);
|
||||
expect(within(context).getByRole('button', { name: copy('emailInboxOpenJob', {}, language) })).toBeEnabled();
|
||||
fireEvent.click(row.getByRole('button', { name: copy('emailInboxHideMessage', {}, language) }));
|
||||
expect(row.queryByRole('complementary')).not.toBeInTheDocument();
|
||||
expect(row.getAllByRole('button', { name: copy('emailInboxOpenJob', {}, language) })).toHaveLength(1);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled(); expect(mockedApi.put).not.toHaveBeenCalled(); expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
test.each(['en', 'nb'] as const)('%s inbox filters and messages support request-free keyboard navigation', async language => {
|
||||
localStorage.setItem('uiLanguage', language); renderPage();
|
||||
await screen.findByText('Interview invite', { exact: true });
|
||||
const count = mockedApi.get.mock.calls.length;
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxJumpMessages', {}, language) }));
|
||||
const jump = screen.getByRole('button', { name: copy('emailInboxJumpMessages', {}, language) });
|
||||
const search = screen.getByRole('textbox', { name: copy('correspondenceSearch', {}, language) });
|
||||
expect(jump.compareDocumentPosition(search) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
fireEvent.click(jump);
|
||||
expect(screen.getByRole('region', { name: copy('emailInboxLinkedMessages', {}, language) })).toHaveFocus();
|
||||
fireEvent.click(screen.getByRole('button', { name: copy('emailInboxReturnFilters', {}, language) }));
|
||||
expect(screen.getByRole('textbox', { name: copy('correspondenceSearch', {}, language) })).toHaveFocus();
|
||||
|
||||
@@ -675,7 +675,7 @@ export default function CorrespondenceInboxPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection surface title={t("correspondenceInbox")} description={t("emailInboxSubtitle")}>
|
||||
<PageSection title={t("correspondenceInbox")} description={t("emailInboxSubtitle")} actions={view === "inbox" ? <Button size="small" onClick={() => messagesPanel.current?.focus()}>{t("emailInboxJumpMessages")}</Button> : undefined}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap", mb: 2 }}>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
{view === "inbox" && hasCurrentInbox ? <Chip icon={<MailOutlineIcon />} label={t(inboxTotal === 1 ? "emailInboxItem" : "emailInboxItems", { count: inboxTotal })} variant="outlined" /> : null}
|
||||
@@ -777,7 +777,6 @@ export default function CorrespondenceInboxPage() {
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button variant="outlined" onClick={() => { inboxPanel.current?.focus({ preventScroll: true }); void load(); }} disabled={loading || !!unlinkingThreadId}>{loading ? t("loading") : t("refresh")}</Button>
|
||||
<Button size="small" onClick={() => messagesPanel.current?.focus()} sx={{ justifySelf: "start", gridColumn: "1 / -1" }}>{t("emailInboxJumpMessages")}</Button>
|
||||
</Box>
|
||||
|
||||
{draft ? (
|
||||
@@ -830,10 +829,10 @@ export default function CorrespondenceInboxPage() {
|
||||
|
||||
<Stack spacing={1.25}>
|
||||
{(hasCurrentInbox ? items : []).map((item) => (
|
||||
<Paper component="article" aria-labelledby={`${rowId}-${item.id}`} key={item.id} variant="outlined" sx={{ p: "var(--app-card-padding)", borderRadius: 1, minWidth: 0, "& .MuiChip-root": { maxWidth: "100%", height: "auto" }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" } }}>
|
||||
<Box component="article" aria-labelledby={`${rowId}-${item.id}`} key={item.id} sx={{ py: 1.5, borderBottom: 1, borderColor: "divider", minWidth: 0, "& .MuiChip-root": { maxWidth: "100%", height: "auto" }, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere" } }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "flex-start" }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{item.companyName || t("emailInboxUnknownCompany")} • {item.jobTitle || t("emailInboxUnknownRole")}</Typography>
|
||||
{selectedMessageId !== item.id ? <Typography sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{item.companyName || t("emailInboxUnknownCompany")} • {item.jobTitle || t("emailInboxUnknownRole")}</Typography> : null}
|
||||
<Typography id={`${rowId}-${item.id}`} component="h3" variant="subtitle2" sx={{ overflowWrap: "anywhere" }}>{item.subject || item.contentPreview || t("emailInboxNoSubject")}</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 0.5 }}>
|
||||
{item.externalFrom || item.from} {item.externalTo ? `→ ${item.externalTo}` : ""} · {new Date(item.date).toLocaleString(language === "nb" ? "nb-NO" : "en")}
|
||||
@@ -846,7 +845,7 @@ export default function CorrespondenceInboxPage() {
|
||||
{item.labelCount > 0 ? <Chip size="small" label={t("emailInboxLabels", { count: item.labelCount })} variant="outlined" /> : null}
|
||||
{item.attachmentCount > 0 ? <Chip size="small" label={t(item.attachmentCount === 1 ? "emailInboxAttachmentCount" : "emailInboxAttachments", { count: item.attachmentCount })} variant="outlined" /> : null}
|
||||
<Button size="small" variant="text" disabled={!!unlinkingThreadId} aria-expanded={selectedMessageId === item.id} aria-controls={selectedMessageId === item.id ? `${rowId}-${item.id}-message` : undefined} onClick={() => void showMessage(item)}>{selectedMessageId === item.id ? t("emailInboxHideMessage") : t("emailInboxViewMessage")}</Button>
|
||||
<Button size="small" variant="text" disabled={!!unlinkingThreadId} onClick={() => navigate(`/jobs/${item.jobApplicationId}?section=communication`, { state: { from: location.pathname + location.search + location.hash } })}>{t("emailInboxOpenJob")}</Button>
|
||||
{selectedMessageId !== item.id ? <Button size="small" variant="text" disabled={!!unlinkingThreadId} onClick={() => navigate(`/jobs/${item.jobApplicationId}?section=communication`, { state: { from: location.pathname + location.search + location.hash } })}>{t("emailInboxOpenJob")}</Button> : null}
|
||||
{item.provider === "gmail" && item.externalThreadId ? (
|
||||
<Button size="small" color="warning" variant="text" disabled={!!unlinkingThreadId} onClick={() => void unlinkGmailThread(item)}>
|
||||
{unlinkingThreadId === item.externalThreadId ? t("emailInboxUnlinking") : t("emailInboxUnlinkThread")}
|
||||
@@ -855,7 +854,14 @@ export default function CorrespondenceInboxPage() {
|
||||
</Box>
|
||||
</Box>
|
||||
{selectedMessageId === item.id ? (
|
||||
<Box ref={detailPanel} tabIndex={-1} role="group" id={`${rowId}-${item.id}-message`} aria-label={t("emailInboxMessage")} sx={{ mt: 1.5, pt: 1.5, borderTop: "1px solid", borderColor: "divider" }}>
|
||||
<Box ref={detailPanel} tabIndex={-1} role="group" id={`${rowId}-${item.id}-message`} aria-label={t("emailInboxMessage")} sx={{ mt: 1.5, pt: 1.5, borderTop: "1px solid", borderColor: "divider", display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 2, "@media (min-width:1001px)": { gridTemplateColumns: "minmax(0, 1fr) minmax(14rem, 20rem)" } }}>
|
||||
<Box component="aside" aria-label={t("emailInboxJob")} sx={{ minWidth: 0, overflowWrap: "anywhere", "@media (min-width:1001px)": { gridColumn: "2", gridRow: "1", borderLeft: 1, borderColor: "divider", pl: 2 } }}>
|
||||
<Typography component="h4" variant="sectionTitle">{t("emailInboxJob")}</Typography>
|
||||
<Typography sx={{ fontWeight: 700 }}>{item.jobTitle || t("emailInboxUnknownRole")}</Typography>
|
||||
<Typography color="text.secondary">{item.companyName || t("emailInboxUnknownCompany")}</Typography>
|
||||
<Button size="small" variant="text" disabled={!!unlinkingThreadId} sx={{ mt: 1 }} onClick={() => navigate(`/jobs/${item.jobApplicationId}?section=communication`, { state: { from: location.pathname + location.search + location.hash } })}>{t("emailInboxOpenJob")}</Button>
|
||||
</Box>
|
||||
<Box data-mail-message-content sx={{ minWidth: 0, "@media (min-width:1001px)": { gridColumn: "1", gridRow: "1" } }}>
|
||||
<ViewStateNotice compact loading={detailLoading} title={t("emailInboxMessageLoadFailed")} error={detailError ? { ...detailError, message: detailError.message || t("emailInboxMessageLoadFailed") } : null} onRetry={() => { detailPanel.current?.focus({ preventScroll: true }); return detailItem.current ? loadMessage(detailItem.current) : undefined; }} />
|
||||
{detailNotice ? <Alert severity="warning" sx={{ mb: 1 }}>{t("emailInboxProviderCopyUnavailable")}</Alert> : null}
|
||||
{messageDetail ? <>
|
||||
@@ -872,9 +878,10 @@ export default function CorrespondenceInboxPage() {
|
||||
<Alert severity="info" sx={{ mt: 1.5 }}>{t("emailInboxReconnectToReply")}</Alert>
|
||||
) : null}
|
||||
</> : null}
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Paper>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
{hasCurrentInbox && inboxTotalPages > 1 ? (
|
||||
|
||||
Reference in New Issue
Block a user