fix(briefs): add settings and dismissal controls

This commit is contained in:
Dotta committed 2026-05-24 02:35:26 +00:00
1 parent 602c68370c
commit f9a88c02da
16 files changed
+1060 -147

No files matched your search

@@ -9,8 +9,8 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
}),
usePluginAction: () => vi.fn(async () => ({ ok: true })),
IssueRow: ({ issue, trailingMeta }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode }) => (
<a data-plugin-issue-row={issue.identifier ?? ""} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
IssueRow: ({ issue, trailingMeta, className }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode; className?: string }) => (
<a data-plugin-issue-row={issue.identifier ?? ""} className={className} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
),
usePluginData: () => ({ data: null, loading: false, error: null, refresh: () => {} }),
usePluginToast: () => vi.fn(),
@@ -118,4 +118,13 @@ describe("BriefCardView", () => {
const unpinned = makeCard({ pinned: false });
expect(renderCard(unpinned)).toMatch(/aria-label="Pin card"/);
});
it("renders a dismiss action and removes issue-row bottom dividers", () => {
const card = makeCard();
const html = renderCard(card);
expect(html).toContain('aria-label="Dismiss briefing card"');
expect(html).toContain("Dismiss");
expect(html).toContain("!border-b-0");
});
});
@@ -0,0 +1,140 @@
// @vitest-environment jsdom
import type { ReactNode } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { BriefCard } from "../../src/contracts.js";
import { makeCard, makeSnapshot, makeTaskRow } from "./fixtures.js";
const actionCalls = vi.hoisted(() => ({
pin: vi.fn(async () => ({ ok: true })),
dismiss: vi.fn(async () => ({ ok: true })),
}));
let mockPageData: { cards: BriefCard[]; fetchedAt: string };
vi.mock("@paperclipai/plugin-sdk/ui", () => {
return {
ManagedRoutinesList: () => null,
IssueRow: ({ issue, trailingMeta, className }: { issue: { identifier?: string | null; title: string }; trailingMeta?: ReactNode; className?: string }) => (
<a data-plugin-issue-row={issue.identifier ?? ""} className={className} href={`/issues/${issue.identifier ?? ""}`}>{issue.identifier} {issue.title} {trailingMeta}</a>
),
useHostNavigation: () => ({
resolveHref: (to: string) => to,
navigate: () => {},
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
}),
usePluginAction: (key: string) => {
if (key === "pin-card") return actionCalls.pin;
if (key === "dismiss-card") return actionCalls.dismiss;
return vi.fn(async () => ({ ok: true }));
},
usePluginData: (key: string) => {
if (key === "page") {
return { data: mockPageData, loading: false, error: null, refresh: vi.fn() };
}
return { data: null, loading: false, error: null, refresh: vi.fn() };
},
usePluginToast: () => vi.fn(),
};
});
import { BriefingPage } from "../../src/ui/app.js";
const hostContext = {
companyId: "company-1",
companyPrefix: "PAP",
projectId: null,
entityId: null,
entityType: null,
userId: "user-1",
} as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function makeCards(): BriefCard[] {
const newest = makeCard({
id: "card-new",
title: "Newest unpinned work",
pinned: false,
lastMeaningfulEventAt: "2026-05-23T11:00:00.000Z",
snapshot: makeSnapshot({
taskRows: [makeTaskRow({ identifier: "PAP-1", titleLine: "Newest issue" })],
}),
});
const older = makeCard({
id: "card-old",
title: "Older unpinned work",
pinned: false,
lastMeaningfulEventAt: "2026-05-22T11:00:00.000Z",
snapshot: makeSnapshot({
taskRows: [makeTaskRow({ identifier: "PAP-2", titleLine: "Older issue" })],
}),
});
return [newest, older];
}
describe("BriefingPage interactions", () => {
let container: HTMLDivElement;
let root: ReturnType<typeof createRoot>;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
mockPageData = { cards: makeCards(), fetchedAt: "2026-05-23T12:00:00.000Z" };
});
afterEach(() => {
flushSync(() => root.unmount());
container.remove();
vi.clearAllMocks();
});
function renderPage() {
flushSync(() => {
root.render(<BriefingPage context={hostContext as never} />);
});
}
it("pins optimistically and moves the card to the top without a full refresh", async () => {
renderPage();
const oldCard = [...container.querySelectorAll("article[data-briefs-card]")]
.find((node) => node.textContent?.includes("Older unpinned work"));
const pinButton = oldCard?.querySelector('button[aria-label="Pin card"]') as HTMLButtonElement | null;
expect(pinButton).not.toBeNull();
flushSync(() => {
pinButton?.click();
});
const titles = [...container.querySelectorAll("article[data-briefs-card] h3")].map((node) => node.textContent);
expect(titles[0]).toBe("Older unpinned work");
expect(actionCalls.pin).toHaveBeenCalledWith(expect.objectContaining({
cardId: "card-old",
pinned: true,
}));
});
it("starts the dismiss fade immediately and persists the hidden card", async () => {
renderPage();
const firstCard = container.querySelector("article[data-briefs-card]") as HTMLElement | null;
const dismissButton = firstCard?.querySelector('button[aria-label="Dismiss briefing card"]') as HTMLButtonElement | null;
expect(firstCard).not.toBeNull();
expect(dismissButton).not.toBeNull();
flushSync(() => {
dismissButton?.click();
});
expect(firstCard?.style.opacity).toBe("0");
expect(actionCalls.dismiss).toHaveBeenCalledWith(expect.objectContaining({
cardId: "card-new",
}));
});
});
@@ -27,6 +27,14 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
navigate: () => {},
linkProps: (to: string) => ({ href: to, onClick: () => {} }),
}),
useHostContext: () => ({
companyId: "company-1",
companyPrefix: "PAP",
projectId: null,
entityId: null,
entityType: null,
userId: "user-1",
}),
usePluginAction: () => vi.fn(async () => ({ ok: true })),
usePluginData: (key: string) => {
if (key === "page") {
@@ -63,6 +71,16 @@ vi.mock("@paperclipai/plugin-sdk/ui", () => {
routine: { id: "routine-2", title: "Update Briefing cards for {{userId}}", status: "paused", projectId: "project-1", assigneeAgentId: "agent-1" },
},
],
preferences: {
companyId: "company-1",
userId: "user-1",
cadence: "hourly",
retentionDays: 7,
doneRetentionHours: 72,
staleAfterDays: 7,
maxUnpinnedCards: 30,
scope: "user",
},
agentOptions: [{ id: "agent-1", name: "Briefing Analyst", icon: "newspaper" }],
projectOptions: [{ id: "project-1", name: "Briefs", color: "#0f766e" }],
},
@@ -101,6 +119,8 @@ describe("BriefingPage", () => {
const html = renderPage(gallery());
expect(html).toContain("data-briefs-list");
expect(html).toContain("data-briefs-card-grid");
expect(html).toContain("grid-template-columns:repeat(2, minmax(0, 1fr))");
expect(html).toContain("Recent work and next steps");
expect(html).not.toContain("data-briefs-mobile-tabs");
expect(html).not.toContain("data-briefs-section");
@@ -136,6 +156,8 @@ describe("BriefingPage", () => {
expect(html).toContain("data-briefs-page-header");
expect(html).toContain("data-briefs-page-meta");
expect(html).toContain('aria-label="Briefing settings"');
expect(html).toContain("/PAP/instance/settings/plugins/paperclipai.plugin-briefs");
expect(html).not.toContain("Preferences");
expect(html).not.toContain("Durable cards for areas of work");
});
@@ -148,13 +170,14 @@ describe("BriefingPage", () => {
});
describe("SidebarLink", () => {
it("renders the Briefing sidebar entry with an icon and company route", () => {
it("renders the Briefing sidebar entry with an icon, company route, and no badge count", () => {
const html = renderToStaticMarkup(<SidebarLink context={hostContext as never} />);
expect(html).toContain("Briefing");
expect(html).toContain('href="/briefs"');
expect(html).toContain("data-briefs-sidebar-icon");
expect(html).toContain("color:currentColor");
expect(html).not.toContain("briefs need your attention");
});
});
@@ -163,6 +186,9 @@ describe("SettingsPage", () => {
const html = renderToStaticMarkup(<SettingsPage context={hostContext as never} />);
expect(html).toContain("Managed resources");
expect(html).toContain("Dashboard settings");
expect(html).toContain("Maximum unpinned cards");
expect(html).toContain("Mark stale after days");
expect(html).toContain("Briefing Analyst");
expect(html).toContain("data-managed-routines-list");
expect(html).toContain("Discover Briefing cards");