mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
feat(admin): link email detail to its public entry page
Add a "Public page" link next to the Rendered/Raw toggle in the admin email view, opening the standalone /entries/:feedId/:entryId render. Centralize the entry route shape in a pure entryPath() builder, used by both the admin link and the RSS/Atom/JSON feed generator. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { Feed } from "feed";
|
|||||||
import { FeedConfig, EmailData } from "../types";
|
import { FeedConfig, EmailData } from "../types";
|
||||||
import { processEmailContent, htmlToText } from "./html-processor";
|
import { processEmailContent, htmlToText } from "./html-processor";
|
||||||
import { EmailAddress } from "../domain/value-objects/email-address";
|
import { EmailAddress } from "../domain/value-objects/email-address";
|
||||||
|
import { entryPath } from "./urls";
|
||||||
|
|
||||||
export { processEmailContent as extractBodyContent };
|
export { processEmailContent as extractBodyContent };
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ function buildFeed(
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const email of emails) {
|
for (const email of emails) {
|
||||||
const entryUrl = `${baseUrl}/entries/${feedId}/${email.receivedAt}`;
|
const entryUrl = `${baseUrl}${entryPath(feedId, email.receivedAt)}`;
|
||||||
// Inline images are rendered in the body, not surfaced as an enclosure.
|
// Inline images are rendered in the body, not surfaced as an enclosure.
|
||||||
const firstAttachment = email.attachments?.find((a) => !a.inline);
|
const firstAttachment = email.attachments?.find((a) => !a.inline);
|
||||||
const bodyContent = processEmailContent(
|
const bodyContent = processEmailContent(
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export function feedJsonUrl(feedId: string, env: Env): string {
|
|||||||
return `${baseUrl(env)}/json/${feedId}`;
|
return `${baseUrl(env)}/json/${feedId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Path of an email's public HTML view. The single source of truth for the
|
||||||
|
* `/entries/:feedId/:entryId` route shape (entryId = the email's receivedAt). */
|
||||||
|
export function entryPath(feedId: string, receivedAt: number): string {
|
||||||
|
return `/entries/${feedId}/${receivedAt}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function feedUrl(
|
export function feedUrl(
|
||||||
format: "rss" | "atom",
|
format: "rss" | "atom",
|
||||||
feedId: string,
|
feedId: string,
|
||||||
|
|||||||
@@ -890,6 +890,39 @@ describe("Admin Routes", () => {
|
|||||||
expect(body).not.toContain("Attachments");
|
expect(body).not.toContain("Attachments");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("links to the public entry page using the feed id and receivedAt", async () => {
|
||||||
|
const authCookie = await loginAndGetCookie();
|
||||||
|
const feedId = "detail-feed";
|
||||||
|
await mockEnv.EMAIL_STORAGE.put(
|
||||||
|
`feed:${feedId}:config`,
|
||||||
|
JSON.stringify({
|
||||||
|
title: "Detail Feed",
|
||||||
|
mailbox_id: "detail.feed.10",
|
||||||
|
language: "en",
|
||||||
|
created_at: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const emailKey = `feed:${feedId}:2`;
|
||||||
|
await mockEnv.EMAIL_STORAGE.put(
|
||||||
|
emailKey,
|
||||||
|
JSON.stringify({
|
||||||
|
subject: "Linkable",
|
||||||
|
from: "sender@example.com",
|
||||||
|
content: "<p>hello</p>",
|
||||||
|
receivedAt: 2,
|
||||||
|
headers: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await request(`/admin/emails/${emailKey}`, {
|
||||||
|
headers: { Cookie: authCookie },
|
||||||
|
});
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
const body = await res.text();
|
||||||
|
|
||||||
|
expect(body).toContain(`href="/entries/${feedId}/2"`);
|
||||||
|
});
|
||||||
|
|
||||||
it("form-based bulk-delete also removes R2 attachments", async () => {
|
it("form-based bulk-delete also removes R2 attachments", async () => {
|
||||||
const r2Env = createMockEnv({ withR2: true }) as unknown as Env;
|
const r2Env = createMockEnv({ withR2: true }) as unknown as Env;
|
||||||
const bucket = r2Env.ATTACHMENT_BUCKET as unknown as {
|
const bucket = r2Env.ATTACHMENT_BUCKET as unknown as {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
feedAtomUrl,
|
feedAtomUrl,
|
||||||
feedEmailAddress,
|
feedEmailAddress,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
|
entryPath,
|
||||||
} from "../../infrastructure/urls";
|
} from "../../infrastructure/urls";
|
||||||
import { processEmailContent } from "../../infrastructure/html-processor";
|
import { processEmailContent } from "../../infrastructure/html-processor";
|
||||||
import { formatBytes } from "../../domain/format";
|
import { formatBytes } from "../../domain/format";
|
||||||
@@ -604,6 +605,14 @@ emailsRouter.get("/emails/:emailKey", async (c) => {
|
|||||||
<button id="raw-button" class="toggle-button" onclick="showRaw()">
|
<button id="raw-button" class="toggle-button" onclick="showRaw()">
|
||||||
Raw HTML
|
Raw HTML
|
||||||
</button>
|
</button>
|
||||||
|
<a
|
||||||
|
class="toggle-view-link"
|
||||||
|
href={entryPath(feedId, emailData.receivedAt)}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
Public page ↗
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="email-content">
|
<div class="email-content">
|
||||||
|
|||||||
@@ -554,6 +554,20 @@ textarea:focus {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-view-link {
|
||||||
|
margin-left: auto;
|
||||||
|
align-self: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-view-link:hover {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Email content container */
|
/* Email content container */
|
||||||
.email-content {
|
.email-content {
|
||||||
margin-top: var(--spacing-md);
|
margin-top: var(--spacing-md);
|
||||||
|
|||||||
Reference in New Issue
Block a user