fix(scale): page mail and batch roles
CI and Deploy / test (pull_request) Failing after 2m46s
CI and Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
cesnimda
2026-08-15 21:02:50 +02:00
parent 8ef8b098c8
commit e7cacad7d6
11 changed files with 284 additions and 20 deletions
@@ -44,7 +44,7 @@ describe('CorrespondenceInboxPage', () => {
{ 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: [
if (url === '/correspondence/page') return Promise.resolve({ data: { items: [
{
id: 1,
jobApplicationId: 42,
@@ -64,7 +64,7 @@ describe('CorrespondenceInboxPage', () => {
labelCount: 2,
attachmentCount: 1,
},
] } as any);
], page: 1, pageSize: 50, total: 1, totalPages: 1 } } as any);
if (url === '/email/drafts') return Promise.resolve({ data: [] } as any);
if (url === '/jobapplications/choices') return Promise.resolve({ data: [] } as any);
if (url === '/email/message') return Promise.resolve({ data: {
@@ -101,10 +101,12 @@ describe('CorrespondenceInboxPage', () => {
fireEvent.click((await screen.findAllByRole('option', { name: /Inbound/i }))[0]);
await waitFor(() => {
expect(mockedApi.get).toHaveBeenLastCalledWith('/correspondence', expect.objectContaining({
expect(mockedApi.get).toHaveBeenLastCalledWith('/correspondence/page', expect.objectContaining({
params: expect.objectContaining({
q: 'Maria',
direction: 'inbound',
page: 1,
pageSize: 50,
}),
}));
});
@@ -137,6 +139,44 @@ describe('CorrespondenceInboxPage', () => {
});
});
test('navigates beyond the first correspondence page', async () => {
const original = mockedApi.get.getMockImplementation();
mockedApi.get.mockImplementation((url: string, config?: any) => {
if (url === '/correspondence/page') {
const page = config?.params?.page ?? 1;
return Promise.resolve({ data: {
items: [{
id: page,
jobApplicationId: 42,
companyName: page === 2 ? 'Second page company' : 'First page company',
jobTitle: 'Engineer',
from: 'Recruiter',
direction: 'inbound',
subject: `Page ${page}`,
channel: 'Email',
date: new Date().toISOString(),
contentPreview: `Page ${page} message`,
labelCount: 0,
attachmentCount: 0,
}],
page,
pageSize: 50,
total: 51,
totalPages: 2,
} } as any);
}
return original!(url, config);
});
renderPage();
fireEvent.click(await screen.findByRole('button', { name: /go to page 2/i }));
expect(await screen.findByText(/second page company/i)).toBeInTheDocument();
expect(mockedApi.get).toHaveBeenCalledWith('/correspondence/page', expect.objectContaining({
params: expect.objectContaining({ page: 2, pageSize: 50 }),
}));
});
test('unlinks a Gmail thread only after confirmation and returns it to review', async () => {
mockedApi.post.mockResolvedValue({ data: { threadId: 'thread-1', jobApplicationId: 42, removedMessages: 1, decision: 'review' } } as any);
renderPage();
@@ -154,7 +194,7 @@ describe('CorrespondenceInboxPage', () => {
note: 'Unlinked from Job email hub',
nextDecision: 'review',
}));
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/correspondence', expect.anything()));
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith('/correspondence/page', expect.anything()));
expect(await screen.findByText(/returned to recruitment review/i)).toBeInTheDocument();
});
@@ -452,6 +492,6 @@ describe('CorrespondenceInboxPage', () => {
);
expect(await screen.findByRole('heading', { name: /recruitment message review/i })).toBeInTheDocument();
expect(mockedApi.get).not.toHaveBeenCalledWith('/correspondence', expect.anything());
expect(mockedApi.get).not.toHaveBeenCalledWith('/correspondence/page', expect.anything());
});
});
@@ -9,6 +9,7 @@ import {
FormControl,
InputLabel,
MenuItem,
Pagination,
Paper,
Select,
Stack,
@@ -42,6 +43,14 @@ export type CorrespondenceInboxItem = {
attachmentCount: number;
};
type CorrespondenceInboxPage = {
items: CorrespondenceInboxItem[];
page: number;
pageSize: number;
total: number;
totalPages: number;
};
type EmailProviderStatus = {
provider: string;
displayName: string;
@@ -116,6 +125,9 @@ export default function CorrespondenceInboxPage() {
const { toast } = useToast();
const { confirm } = useConfirm();
const [items, setItems] = useState<CorrespondenceInboxItem[]>([]);
const [inboxPage, setInboxPage] = useState(1);
const [inboxTotal, setInboxTotal] = useState(0);
const [inboxTotalPages, setInboxTotalPages] = useState(0);
const [providers, setProviders] = useState<EmailProviderStatus[]>([]);
const [providerStatusLoaded, setProviderStatusLoaded] = useState(false);
const [jobs, setJobs] = useState<JobChoice[]>([]);
@@ -148,20 +160,27 @@ export default function CorrespondenceInboxPage() {
const load = useCallback(async () => {
setLoading(true);
try {
const res = await api.get<CorrespondenceInboxItem[]>("/correspondence", {
const res = await api.get<CorrespondenceInboxPage>("/correspondence/page", {
params: {
q: query.trim() || undefined,
direction: direction === "all" ? undefined : direction,
linkState: linkState === "all" ? undefined : linkState,
page: inboxPage,
pageSize: 50,
},
});
setItems(res.data ?? []);
setItems(res.data?.items ?? []);
setInboxTotal(res.data?.total ?? 0);
setInboxTotalPages(res.data?.totalPages ?? 0);
if (res.data?.page && res.data.page !== inboxPage) setInboxPage(res.data.page);
setSelectedMessageId(null);
setMessageDetail(null);
} catch (error) {
toast(getApiErrorMessage(error, "Failed to load correspondence inbox."), "error");
} finally {
setLoading(false);
}
}, [direction, linkState, query, toast]);
}, [direction, inboxPage, linkState, query, toast]);
useEffect(() => {
if (view === "inbox") void load();
@@ -524,7 +543,7 @@ export default function CorrespondenceInboxPage() {
</Typography>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
{view === "inbox" ? <Chip icon={<MailOutlineIcon />} label={`${items.length} items`} variant="outlined" /> : null}
{view === "inbox" ? <Chip icon={<MailOutlineIcon />} label={`${inboxTotal} items`} variant="outlined" /> : null}
{view === "inbox" ? <Chip label={`${filteredSummary.linked} linked`} variant="outlined" color={filteredSummary.linked > 0 ? "success" : "default"} /> : null}
{view === "inbox" ? <Chip label={`${filteredSummary.inbound} inbound`} variant="outlined" /> : null}
<Button variant={view === "inbox" ? "contained" : "text"} size="small" onClick={() => setSearchParams({})}>Linked messages</Button>
@@ -596,10 +615,10 @@ export default function CorrespondenceInboxPage() {
{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" />
<TextField label="Search" value={query} onChange={(e) => { setQuery(e.target.value); setInboxPage(1); }} placeholder="Company, role, recruiter, subject" />
<FormControl fullWidth>
<InputLabel>Direction</InputLabel>
<Select value={direction} label="Direction" onChange={(e) => setDirection(String(e.target.value))}>
<Select value={direction} label="Direction" onChange={(e) => { setDirection(String(e.target.value)); setInboxPage(1); }}>
<MenuItem value="all">All</MenuItem>
<MenuItem value="inbound">Inbound</MenuItem>
<MenuItem value="outbound">Outbound</MenuItem>
@@ -608,7 +627,7 @@ export default function CorrespondenceInboxPage() {
</FormControl>
<FormControl fullWidth>
<InputLabel>Link state</InputLabel>
<Select value={linkState} label="Link state" onChange={(e) => setLinkState(String(e.target.value))}>
<Select value={linkState} label="Link state" onChange={(e) => { setLinkState(String(e.target.value)); setInboxPage(1); }}>
<MenuItem value="all">All</MenuItem>
<MenuItem value="linked">Linked threads</MenuItem>
<MenuItem value="manual">Manual/internal only</MenuItem>
@@ -711,6 +730,17 @@ export default function CorrespondenceInboxPage() {
</Paper>
))}
</Stack>
{inboxTotalPages > 1 ? (
<Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
<Pagination
page={inboxPage}
count={inboxTotalPages}
onChange={(_, value) => setInboxPage(value)}
color="primary"
aria-label="Correspondence pages"
/>
</Box>
) : null}
</>}
</Paper>
);