Public: anyone can read this; the crew can change it.
A chip on the home feed is drawn only when pressing it changes the posts on the page #6866
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6849-a-chip-on-the-home-feed-is-drawn-only-wh → main 1 commit fork
3 files
changed +48
−1
src/feed/chips.ts +10
| @@ -29,3 +29,13 @@ export function chipHides( | ||
| 29 | 29 | if (chip !== undefined && chips.kinds.has(chip)) return true; |
| 30 | 30 | return item.actorId !== null && chips.people.has(item.actorId); |
| 31 | 31 | } |
| 32 | ||
| 33 | /** How many of the posts on the page each kind chip covers: what pressing it would take away. */ | |
| 34 | export function chipCounts(items: readonly { readonly kind: FeedKind }[]): Map<KindChip, number> { | |
| 35 | const counts = new Map<KindChip, number>(); | |
| 36 | for (const item of items) { | |
| 37 | const chip = CHIP_OF[item.kind]; | |
| 38 | if (chip !== undefined) counts.set(chip, (counts.get(chip) ?? 0) + 1); | |
| 39 | } | |
| 40 | return counts; | |
| 41 | } |
src/web/studio/feed.ts +8 −1
| @@ -8,6 +8,7 @@ | ||
| 8 | 8 | * Every string on a card is plain text and is escaped here, once. |
| 9 | 9 | */ |
| 10 | 10 | |
| 11 | import { chipCounts } from "../../feed/chips.ts"; | |
| 11 | 12 | import { chipsOf, KIND_CHIPS, type KindChip } from "../../core/feed_chips.ts"; |
| 12 | 13 | import { marksByName, type Mark } from "../../core/marks.ts"; |
| 13 | 14 | import { getUsersByIds, type User } from "../../core/users.ts"; |
| @@ -275,6 +276,7 @@ function chipsBar(user: User, ctx: Ctx, items: readonly FeedItem[]): Html { | ||
| 275 | 276 | ]); |
| 276 | 277 | names.delete(user.username); |
| 277 | 278 | const hiddenNames = new Set(hidden.map((u) => u.username)); |
| 279 | const counts = chipCounts(items); | |
| 278 | 280 | const chip = (kind: string, value: string, label: string, shown: boolean): Html => |
| 279 | 281 | html`<button |
| 280 | 282 | type="button" |
| @@ -284,7 +286,12 @@ function chipsBar(user: User, ctx: Ctx, items: readonly FeedItem[]): Html { | ||
| 284 | 286 | aria-pressed="${String(shown)}" |
| 285 | 287 | >${label}</button>`; |
| 286 | 288 | return html`<div class="bar feed-chips" role="group" aria-label="Filter the feed"> |
| 287 | ${KIND_CHIPS.map((k) => chip("kind", k, CHIP_LABEL[k], !chips.kinds.has(k)))} | |
| 289 | ${KIND_CHIPS.flatMap((k) => { | |
| 290 | const count = counts.get(k) ?? 0; | |
| 291 | // Nothing on the page to filter: not drawn, unless switched off (it must come back on). | |
| 292 | if (count === 0 && !chips.kinds.has(k)) return []; | |
| 293 | return [chip("kind", k, count === 0 ? CHIP_LABEL[k] : `${CHIP_LABEL[k]} ${count}`, !chips.kinds.has(k))]; | |
| 294 | })} | |
| 288 | 295 | ${[...names].sort().map((n) => chip("person", n, n, !hiddenNames.has(n)))} |
| 289 | 296 | </div>`; |
| 290 | 297 | } |
test/feed-chips.test.ts +30
| @@ -215,4 +215,34 @@ describe("the chips of a forge", () => { | ||
| 215 | 215 | expect(((await tool(bobToken, "feed", { hide_kinds: ["x"] })) as any).error).toContain("hide_kinds"); |
| 216 | 216 | expect(((await tool(bobToken, "feed", { hide_people: [5] })) as any).error).toContain("hide_people"); |
| 217 | 217 | }); |
| 218 | ||
| 219 | test("every drawn kind chip shows what pressing it takes off the page, and pressing it changes the posts", async () => { | |
| 220 | const cookie = { cookie: `forge_session=${createSession(app.db, bob.id).token}` }; | |
| 221 | const region = async () => await (await fetch(`${servers.url}/live/feed`, { headers: cookie })).text(); | |
| 222 | const posts = (html: string) => [...html.matchAll(/data-item="[^"]*"[^>]*data-kind="(\w+)"/g)].map((m) => m[1]!); | |
| 223 | const drawn = (html: string) => | |
| 224 | [...html.matchAll(/data-chip="kind"\s+data-value="(\w+)"\s+aria-pressed="true"\s*>([^<]*)</g)].map((m) => [m[1]!, m[2]!.trim()] as const); | |
| 225 | const before = await region(); | |
| 226 | const chips = drawn(before); | |
| 227 | // Nothing decided on this page, so no Decisions chip; the others carry a count. | |
| 228 | expect(chips.map(([k]) => k).sort()).toEqual(["crew", "releases", "reviews"]); | |
| 229 | expect(chips.map(([, label]) => label).sort()).toEqual(["Crew words 1", "Pull requests 1", "Releases 1"]); | |
| 230 | for (const [chip] of chips) { | |
| 231 | const had = posts(before); | |
| 232 | await call(bobToken, "POST", "/api/v1/feed/chips", { kind: "kind", value: chip, hidden: true }); | |
| 233 | const after = await region(); | |
| 234 | expect(posts(after).length).toBe(had.length - 1); | |
| 235 | expect(after).toMatch(new RegExp(`data-value="${chip}"\\s+aria-pressed="false"`)); | |
| 236 | await call(bobToken, "POST", "/api/v1/feed/chips", { kind: "kind", value: chip, hidden: false }); | |
| 237 | } | |
| 238 | expect(posts(await region())).toEqual(posts(before)); | |
| 239 | }); | |
| 240 | ||
| 241 | test("a count is the posts drawn of its kind, whatever asks the home pins aside", async () => { | |
| 242 | const cookie = { cookie: `forge_session=${createSession(app.db, alice.id).token}` }; | |
| 243 | emit("pull.opened", carol, { number: 2, title: "Wait for me" }); | |
| 244 | const page = await (await fetch(`${servers.url}/live/feed`, { headers: cookie })).text(); | |
| 245 | const drawnPulls = [...page.matchAll(/data-item="[^"]*"[^>]*data-kind="pull"/g)].length; | |
| 246 | expect(page).toMatch(new RegExp(`Pull requests ${drawnPulls}\\s*<`)); | |
| 247 | }); | |
| 218 | 248 | }); |
new above
offline