Public: anyone can read this; the crew can change it.

The home's app rail ends with `N archived`, one tap to /apps?archived=1 #6602

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/6085-the-home-s-app-rail-ends-with-n-archived → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/6085-the-home-s-app-rail-ends-with-n-archived was merged into main as 3c8aff6.

Review

Sign in to review this change.
3 files changed +22 −2
src/web/pages/apps.ts +1 −1
@@ -129,7 +129,7 @@ function appsForViewer(ctx: Ctx, archived: boolean): AppEntry[] {
129129 }
130130
131131 /** How many archived apps the viewer can see (the "N archived" line). */
132 function archivedAppCount(ctx: Ctx): number {
132 export function archivedAppCount(ctx: Ctx): number {
133133 const repos = listReposVisibleTo(ctx.app.db, ctx.auth.user, {
134134 limit: 200,
135135 archivedOnly: true,
src/web/studio/home.ts +8 −1
@@ -57,6 +57,7 @@ import { kpiRow, vitalsRow, waitingLine } from "./kpi_row.ts";
5757 import { feedCandidateCache } from "../../feed/feed.ts";
5858 import { notePhase, notePhaseRest, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
5959 import { kpiCache, panelCache, waitingCache } from "./home_cache.ts";
60 import { archivedAppCount } from "../pages/apps.ts";
6061 import { pinnedPanels } from "./pinned.ts";
6162 import { homeTabs } from "./home_tabs.ts";
6263 import { crewKeyNote } from "./crew.ts";
@@ -108,6 +109,7 @@ function yourApps(ctx: Ctx, user: User, apps = writableAppsOf(ctx, user)): Html
108109 ...(state === undefined ? {} : { trail: envStateBadge(state) }),
109110 });
110111 });
112 const archived = archivedAppCount(ctx);

This runs an uncached listReposVisibleTo query (limit 200) on every home render. If the home has a cache budget, put the count behind the panelCache.

111113 const list = html`${rows.length === 0
112114 ? html`<p class="muted small">No apps yet.</p>`
113115 : html`<ul class="list">
@@ -128,7 +130,12 @@ function yourApps(ctx: Ctx, user: User, apps = writableAppsOf(ctx, user)): Html
128130 btn("Front page", { href: "/front", size: "sm", icon: "globe" }),
129131 )}
130132 ${when(user.isAdmin && rows.length > 0, () => btn("Archive", { size: "sm" }))}
131 </div>`;
133 </div>
134 ${when(
135 archived > 0,
136 () =>
137 html`<p class="muted small"><a href="/apps?archived=1">${archived} archived</a></p>`,
138 )}`;
132139 return card(
133140 // An admin's list is a form: tick apps, then Archive.
134141 user.isAdmin && rows.length > 0
test/home-archive.test.ts +13
@@ -116,6 +116,19 @@ describe("home archive", () => {
116116 expect(twice.headers.get("location")).toBe("/");
117117 });
118118
119 test("the rail ends with `N archived` once something is put away, and it leads there", async () => {
120 expect(await full(rootCookie)).not.toMatch(/\d+ archived<\/a>/);
121 const res = await post("/home/archive", rootCookie, body(repos[2]!.id));
122 const receipt = res.headers.get("location")!;
123 const page = await full(rootCookie);
124 expect(page).toContain('<a href="/apps?archived=1">1 archived</a>');
125 const listed = await (await get("/apps?archived=1", rootCookie)).text();
126 expect(listed).toContain("keeper/gamma");
127 expect(await full(bobCookie)).toContain("1 archived</a>"); // public, so bob can see it too
128 await post(`${receipt}/undo`, rootCookie, "");
129 expect(await full(rootCookie)).not.toMatch(/\d+ archived<\/a>/);
130 });
131
119132 test("a cross-origin post is refused and archives nothing", async () => {
120133 const res = await post(
121134 "/home/archive",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs