From 7226e718f7b74b678af022ce37851c99832fb7fd Mon Sep 17 00:00:00 2001 From: Julien Herr Date: Sat, 23 May 2026 15:10:24 +0200 Subject: [PATCH] feat(admin): paperclip indicator for emails with attachments Show an inline paperclip icon before the subject in the admin email list when an email has attachments, with the count in a tooltip. Uses the attachmentIds already stored in metadata, so no extra fetch. Co-Authored-By: Claude Opus 4.7 --- src/routes/admin.test.ts | 52 +++++++++++++++++++++++++++++++++++++ src/routes/admin/emails.tsx | 48 ++++++++++++++++++++++++++-------- src/styles/components.css | 18 +++++++++++++ 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/src/routes/admin.test.ts b/src/routes/admin.test.ts index 2963c4d..52e9fae 100644 --- a/src/routes/admin.test.ts +++ b/src/routes/admin.test.ts @@ -676,6 +676,58 @@ describe("Admin Routes", () => { } | null; expect(metadataAfter?.emails.length).toBe(0); }); + + it("should show a paperclip indicator only for emails with attachments", async () => { + const authCookie = await loginAndGetCookie(); + const formData = new FormData(); + formData.append("title", "Email Feed"); + + const createRes = await request("/admin/feeds/create", { + method: "POST", + headers: { + Cookie: authCookie, + Origin: "https://test.getmynews.app", + }, + body: formData, + }); + expect(createRes.status).toBe(302); + + const feedList = (await mockEnv.EMAIL_STORAGE.get( + "feeds:list", + "json", + )) as { feeds: Array<{ id: string; title: string }> } | null; + const feedId = feedList?.feeds[0].id as string; + + await mockEnv.EMAIL_STORAGE.put( + `feed:${feedId}:metadata`, + JSON.stringify({ + emails: [ + { + key: `feed:${feedId}:1`, + subject: "With attachments", + receivedAt: 2, + attachmentIds: ["att-1", "att-2"], + }, + { + key: `feed:${feedId}:2`, + subject: "No attachments", + receivedAt: 1, + }, + ], + }), + ); + + const res = await request(`/admin/feeds/${feedId}/emails`, { + headers: { Cookie: authCookie }, + }); + expect(res.status).toBe(200); + const body = await res.text(); + + expect(body).toContain("2 attachments"); + const indicatorCount = (body.match(/attachment-indicator/g) || []) + .length; + expect(indicatorCount).toBe(1); + }); }); }); diff --git a/src/routes/admin/emails.tsx b/src/routes/admin/emails.tsx index c4e18f8..ce24dee 100644 --- a/src/routes/admin/emails.tsx +++ b/src/routes/admin/emails.tsx @@ -349,6 +349,10 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => { {feedMetadata.emails.map((email: EmailMetadata) => { const subjectDisplay = clampText(email.subject, 180); const subjectHover = clampText(email.subject, 1000); + const attachmentCount = email.attachmentIds?.length ?? 0; + const attachmentLabel = `${attachmentCount} attachment${ + attachmentCount > 1 ? "s" : "" + }`; const sortSubject = subjectHover.toLowerCase(); const sortReceivedAt = String(email.receivedAt); const searchHaystack = clampText( @@ -373,9 +377,32 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => { /> - - {subjectDisplay} - +
+ {attachmentCount > 0 ? ( + + + + ) : null} + + {subjectDisplay} + +
{new Date(email.receivedAt).toLocaleString()} @@ -415,7 +442,11 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => { {/* Config bootstrap — injects dynamic server-side data before the static compiled script */} -