mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 16:35:48 +01:00
PAP-10242 Phase G: implement Team Catalog UI
Add the Team Catalog browse/detail/install experience built on the Phase E teams-catalog API and the Phase F design spec. - ui/src/api/teamCatalog.ts: typed client for /teams/catalog list/detail/files and the company-scoped preview/install endpoints. - ui/src/pages/TeamCatalog.tsx: /teams page — toolbar filters (kind/category/ risk/search, URL-synced), grouped browse list, detail pane (summary grid, agent hierarchy, projects, required skills, env inputs, external sources, file inventory), and a conditional multi-step install wizard (target manager, source policy, prerequisite skills, categorized preview, apply progress, success). Surfaces unsafe-source warnings, blocked-apply states, invalid- manifest CTA, and API errors. Sidebar "Teams" entry + route registration. - Derives the design's richer affordances (required steps, source warnings, skill plan) client-side and degrades gracefully against the shipped strict API schema (design section 7). - ui/src/pages/TeamCatalog.test.tsx: renders the real components and covers the main install preview path (detail render, preview fetch + categorized plan + install submit) and target-manager gating. - scripts/screenshot-pap-10242.mjs: Playwright capture helper for QA (needs a valid board session on the running instance). Verified: ui typecheck (tsc -b), vite build, and vitest (3/3) all pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2b5feda461
commit
e0ca97e31c
7 files changed
+2413
No files matched your search
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env node
|
||||
import path from "node:path";
|
||||
import fs from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const playwrightPkgRoot = path.join(
|
||||
repoRoot,
|
||||
"node_modules/.pnpm/playwright@1.58.2/node_modules/playwright",
|
||||
);
|
||||
const { chromium } = await import(path.join(playwrightPkgRoot, "index.mjs"));
|
||||
|
||||
const HOST = process.env.PAP_HOST ?? "100.123.243.20";
|
||||
const BASE = `http://${HOST}:3100`;
|
||||
const outDir = path.join(repoRoot, "screenshots/PAP-10242");
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
|
||||
// Reuse the PAP-9704 local board session token, re-hosted onto the reachable
|
||||
// tailscale interface with a fresh client-side expiry (the server validates the
|
||||
// token value, not the cookie expiry).
|
||||
const authState = JSON.parse(
|
||||
await fs.readFile(path.join(repoRoot, "screenshots/PAP-9704-local-auth-state.json"), "utf8"),
|
||||
);
|
||||
const sessionCookie = authState.cookies.find((c) => c.name.endsWith("session_token"));
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: "/srv/paperclip/home/.cache/ms-playwright/chromium-1223/chrome-linux/chrome",
|
||||
args: ["--no-sandbox"],
|
||||
});
|
||||
|
||||
const results = [];
|
||||
try {
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||||
if (sessionCookie) {
|
||||
await context.addCookies([
|
||||
{
|
||||
name: sessionCookie.name,
|
||||
value: sessionCookie.value,
|
||||
domain: HOST,
|
||||
path: "/",
|
||||
expires: Math.floor(Date.now() / 1000) + 3600,
|
||||
httpOnly: sessionCookie.httpOnly ?? true,
|
||||
secure: false,
|
||||
sameSite: "Lax",
|
||||
},
|
||||
]);
|
||||
}
|
||||
const page = await context.newPage();
|
||||
page.on("console", (m) => {
|
||||
if (m.type() === "error") console.log(" [browser console error]", m.text().slice(0, 200));
|
||||
});
|
||||
|
||||
async function shot(slug, urlPath, prep) {
|
||||
const url = `${BASE}${urlPath}`;
|
||||
await page.goto(url, { waitUntil: "networkidle" }).catch(() => {});
|
||||
await page.waitForTimeout(1500);
|
||||
if (prep) await prep(page);
|
||||
const target = path.join(outDir, `${slug}.png`);
|
||||
await page.screenshot({ path: target, fullPage: false });
|
||||
const title = await page.title().catch(() => "");
|
||||
const bodyText = (await page.evaluate(() => document.body.innerText).catch(() => "")) || "";
|
||||
results.push({ slug, url, title, hasTeams: bodyText.includes("Teams"), sample: bodyText.slice(0, 120).replace(/\n/g, " ") });
|
||||
console.log(`captured ${target} :: title="${title}" :: ${bodyText.slice(0, 80).replace(/\n/g, " ")}`);
|
||||
}
|
||||
|
||||
// 01 browse
|
||||
await shot("01-browse", "/PAP/teams");
|
||||
|
||||
// 02 detail (click first team row)
|
||||
await shot("02-detail", "/PAP/teams", async (p) => {
|
||||
const row = p.locator('button:has-text("Team")').first();
|
||||
if (await row.count()) await row.click().catch(() => {});
|
||||
await p.waitForTimeout(800);
|
||||
});
|
||||
|
||||
// 03 install wizard (click Install team)
|
||||
await shot("03-install-wizard", "/PAP/teams", async (p) => {
|
||||
const cta = p.locator('button:has-text("Install team")').first();
|
||||
if (await cta.count()) await cta.click().catch(() => {});
|
||||
await p.waitForTimeout(1500);
|
||||
});
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
await fs.writeFile(path.join(outDir, "_capture-summary.json"), JSON.stringify(results, null, 2));
|
||||
console.log("\nSummary:", JSON.stringify(results, null, 2));
|
||||
Reference in new issue
Block a user