feat(admin): native feed chips + dashboard pill

Add NativeFeeds/NativeFeedChip components to admin/ui.tsx and a
NativeFeedPill rendered in both list and table dashboard views when
feed.hasNativeFeed is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-25 17:33:10 +02:00
parent 8a0dbf25b0
commit a18d9f165f
3 changed files with 101 additions and 0 deletions
+32
View File
@@ -1538,6 +1538,38 @@ describe("Admin Routes", () => {
expect(await editPage.text()).toContain("checked");
});
it("dashboard shows pill-native for feeds with hasNativeFeed", async () => {
const authCookie = await loginAndGetCookie();
const repo = FeedRepository.from(mockEnv as unknown as Env);
const feedId = FeedId.generate();
const mailboxId = MailboxId.unchecked("native.pill.08");
const feed = Feed.create(
feedId,
{
title: "N",
language: "en",
allowedSenders: [],
blockedSenders: [],
},
{ mailboxId },
);
feed.ingest(
{ key: "k1", subject: "s", receivedAt: 1, size: 10 },
{
maxBytes: 1e9,
nativeFeeds: {
senderKey: "a@x.com",
feeds: [{ url: "https://x.com/rss", type: "rss" }],
},
},
);
await repo.save(feed);
const res = await request("/admin", { headers: { Cookie: authCookie } });
const body = await res.text();
expect(body).toContain("pill-native");
});
it("clears the toggle when the checkbox is omitted (unchecked)", async () => {
const authCookie = await loginAndGetCookie();
const repo = FeedRepository.from(mockEnv as unknown as Env);
+12
View File
@@ -229,6 +229,12 @@ const ConfirmationPill = ({ feedId }: { feedId: string }) => (
</a>
);
const NativeFeedPill = ({ feedId }: { feedId: string }) => (
<a class="pill pill-native" href={`/admin/feeds/${feedId}/emails`}>
Native feed available
</a>
);
// Admin dashboard route
app.get("/", async (c) => {
// Type assertion for environment variables
@@ -639,6 +645,9 @@ app.get("/", async (c) => {
{feed.pendingConfirmation && (
<ConfirmationPill feedId={feed.id} />
)}
{feed.hasNativeFeed && (
<NativeFeedPill feedId={feed.id} />
)}
</div>
</td>
<td>
@@ -763,6 +772,9 @@ app.get("/", async (c) => {
{feed.pendingConfirmation && (
<ConfirmationPill feedId={feed.id} />
)}
{feed.hasNativeFeed && (
<NativeFeedPill feedId={feed.id} />
)}
{feed.description && (
<p class="feed-description">
<span title={descHover}>{descDisplay}</span>
+57
View File
@@ -6,6 +6,7 @@ import { interactiveScripts } from "../../scripts/index";
import { APP_VERSION } from "../../config/version";
import { FAVICON_PATH } from "../favicon";
import { Env } from "../../types";
import type { NativeFeed } from "../../types";
import {
feedFormatUrl,
feedValidatorUrl,
@@ -254,6 +255,62 @@ export const FeedFormats = ({
</div>
);
// ── Native feed chips ─────────────────────────────────────────────────────────
const NATIVE_LABELS: Record<NativeFeed["type"], string> = {
rss: "RSS",
atom: "Atom",
json: "JSON",
};
const NativeFeedChip = ({ feed }: { feed: NativeFeed }) => {
const label = NATIVE_LABELS[feed.type];
return (
<div class="format-chip" data-format={feed.type}>
<span class="format-chip-label">{label}</span>
<span class="format-chip-actions">
<span class="copyable copyable-chip">
<span
class="copyable-content"
title={`Copy native ${label} feed URL`}
aria-label={`Copy native ${label} feed URL`}
>
<span class="copyable-value" data-copy={feed.url} hidden></span>
<span class="copy-icon-container">
<CopyIcon />
<CheckIcon />
</span>
</span>
</span>
<a
class="chip-action"
href={feed.url}
target="_blank"
rel="noopener noreferrer"
title={`Open native ${label} feed in a new tab`}
aria-label={`Open native ${label} feed in a new tab`}
>
<OpenIcon />
</a>
</span>
</div>
);
};
export const NativeFeeds = ({ feeds }: { feeds: NativeFeed[] }) => {
if (feeds.length === 0) return null;
return (
<div class="feed-formats native-feeds">
<span class="feed-formats-label">Native feeds</span>
<div class="feed-formats-chips">
{feeds.map((feed) => (
<NativeFeedChip feed={feed} />
))}
</div>
</div>
);
};
// ── Expiry pill ───────────────────────────────────────────────────────────────
function formatExpiry(expiresAt: number): { label: string; expired: boolean } {