feat(web): show assignee avatars on the board - #175
Open
cloudbridgeuy wants to merge 13 commits into
Open
Conversation
storyOwner() picks the first assignee (GitHub's order, not sorted) and reports how many are left over, returning null when a story has no assignees so no call site can render an "unassigned" placeholder by accident. issue-row.tsx renders "@login" plus "+N" between the label chips and the relative-time stamp, and nothing when the story is unassigned.
Render the story's lead assignee beside the label chips in the story header, using the same @login (+N) grammar already used on the Backlog row. The header now reads story.assignees from StoryDetail, which previously arrived from the API and was dropped on the floor.
Adds ownedBy() and boardHref() as pure helpers in lib/pipeline.ts, and uses boardHref for all four board filter chips (all, stage, status clear, mine) instead of hand-built URL strings. The mine chip narrows each stage's stories to the signed-in viewer's GitHub login, composes with the existing stage/status filters, and only renders when the viewer has a GitHub login to match against. Stage chip counts and the active-open-stories subtitle now read from the mine-scoped stories so they never go stale relative to what's shown.
mineOn previously read straight from the mine=1 query param, so a viewer with no GitHub login (a key principal, or any user whose principal.githubLogin is unset) who arrived at ?mine=1 via a shared link, bookmark, or browser history landed on a board with every story filtered out by ownedBy(), no mine chip to undo it (it only renders when a login exists), and no other chip to recover with, since all four preserve mine. Lift the derivation into mineFilterOn(mine, login) in lib/pipeline.ts, which is false whenever the viewer has no login to match against regardless of the raw query param. The board now renders normally for such a viewer even with ?mine=1 in the URL, and every chip link emits a clean, mine-free href.
The board named the assignee in text but did not show them. Add a shared Avatar primitive and use it on the story row, immediately left of @login. The image is painted as a CSS background rather than as an <img>. An <img> that fails to load makes every browser draw its own broken-image glyph over the letter beneath it, and alt="" does not suppress it; a background that fails to load paints nothing. So a deployment whose browsers cannot reach github.com falls back to the initial letter on its own, with nothing to configure. The avatar URL and the fallback letter are derived in apps/web, not in packages/ui, which keeps the primitive free of any knowledge of GitHub and puts the rules where there is a test runner to pin them. Refs theam#174
The same primitive as the story row, at 16px to sit with the header's larger type. The header is a server component and the row is a client component, so this is also what proves one primitive serves both. Refs theam#174
The topbar rendered an image when the principal had an avatar URL and nothing at all when it did not, so a user whose GitHub identity has no avatar saw an empty space. It now uses the same primitive as the board and falls back to the initial letter of the login, or of the email when there is no login. Two visible consequences, both deliberate: - The avatar is square rather than round. PillTag is documented as the only pill-shaped element in the design system, and the board avatars are square, so one shape now serves all three sites. - The image is no longer an <img>, so it can no longer carry referrerPolicy="no-referrer". Referrer policy belongs to the fetch initiator and CSS cannot set one; measured across Chromium, Firefox and WebKit, a pseudo-element, a child element, an inline style, an external stylesheet carrying referrerpolicy, and a custom-property indirection all send the origin. The avatar host therefore now learns the deployment's origin. The only alternatives are document-wide and would change every other request the app makes. Refs theam#174
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #174. Also finishes the avatar half of #101, which #171 deliberately left out.
apps/webandpackages/uionly. No new dependency, nopackage.jsonchange, nonext.config.tschange, no API or schema change. Nothing new travels on the wire.What you get
One square avatar, immediately left of
@login. The+Ngrammar #171 introducedis untouched.
On the story row (
components/issues/issue-row.tsx, a client component), at 14px:In the story header (
stories/[number]/page.tsx, a server component), at 16pxto match the header's larger type:
Unassigned rows and headers still render nothing at all — no placeholder, no
reserved column.
storyOwnerkeeps its{ login, extra }shape and its tests;this PR does not touch it.
And the topbar (
components/shell/topbar.tsx) now uses the same primitive.That is the part that closes item 2 of #174: the topbar used to render an image
when
avatarUrlwas set and nothing when it was not, which is exactly the gap#101 believed the topbar had already closed. It now renders the initial letter:
The fallback, and why it is a background image
This is the part worth reviewing closely, because it is what answers #101's own
question about deployments that must not let the browser reach github.com.
packages/ui/src/avatar.tsxpaints the initial letter, then layers the remoteimage over it as a CSS
background-image— not as an<img>.An
<img>that fails to load makes every engine paint its own broken-image glyphon top of the letter.
alt=""does not suppress it, and neither doescolor: transparent. A background image that fails to load paints nothing, sothe letter underneath is untouched. A failed avatar becomes pixel-identical to no
avatar at all. I measured this in Chromium, Firefox and WebKit before choosing it.
So a deployment whose browsers cannot reach github.com degrades to the letter with
nothing to configure — no env var, no
NEXT_PUBLIC_*convention, and no changeto
.env.example, the Dockerfile, compose or the self-host docs. Here is the sameboard with the connection to both avatar hosts aborted:
Same rows, same spacing, no broken-image glyph anywhere. Close up, online and then
with the hosts unreachable:
The letter comes from one neutral background, never a colour hashed from the login.
packages/ui's tones (agent,human,ok,bad,info,machine,muted) area status palette, so hashing a login into them would hand someone the colour of
"failed" or "running".
The primitive is
aria-hidden. The login it stands for is always written out besideit, so the avatar adds nothing for a screen reader to announce and nothing new to
translate.
Where the rules live
Two helpers in
lib/pipeline.ts, both covered intest/pipeline-story.test.ts—the same file #171 uses, and no new test file:
avatarUrlFor(login)https://github.com/{login}.png?size=40. Login trimmed and URL-encoded.nullfor a blank login, so nothing is drawn.avatarInitial(value)?when there is nothing at all. Spread rather than[0], so an astral character survives whole.?size=40rather than the 14–20 CSS px actually drawn, so 2× displays stay sharp.packages/ui/src/avatar.tsxtakessrc,initial,sizeandclassNameand hasno framework import — it is the same component in the client row and in the two
server components. Its one piece of logic worth naming is
cssUrl, whichpercent-encodes
",\and whitespace so a login can never end the CSS string orthe
url()token early.Three things called out so they are not mistaken for refactors
1. The topbar loses
referrerPolicy="no-referrer". github.com will now learnthe deployment's origin when it serves the viewer's own avatar. Referrer policy is
a property of the fetch initiator, and CSS gives no way to set one. I measured the
alternatives before accepting this: a pseudo-element, a child element, an inline
style, an external stylesheet carrying
referrerpolicy="no-referrer", and acustom-property indirection all send the origin, in all three engines. The only
levers are document-wide —
<meta name="referrer">or aReferrer-Policyheader —and either would change every other request the app makes.
github.com learns nothing else; the browser connects from the user's own machine
either way. If that trade is not acceptable, the API-proxy approach in #174 is the
alternative, and I would rather hear it now than after merge.
2. The topbar avatar changes from round to square. It was
rounded-full.packages/ui/src/primitives.tsxdocumentsPillTagas "the only pill-shapedelement in the system", so the round avatar was the exception, and making the new
shared primitive round would have spread that exception to three sites instead of
retiring it from one.
3.
next/imagegoes.next.config.tshas noimageskey today, so keeping theoptimiser would have meant adding
remotePatternsplus a caching story, andunoptimizedwould have kept the broken-image glyph. Removing it also keepspackages/uifree of a framework dependency.Worth knowing either way:
https://github.com/{login}.png?size=40302s tohttps://avatars.githubusercontent.com/u/{id}?v=4. Two requests, two hosts —relevant only if a Content-Security-Policy is ever added. There is none today.
Testing
apps/websuite: 11 files, 88 tests, up from 80. Eight new cases pin the twohelpers: a normal login, a login needing trimming, a login needing escaping, a
blank login, an initial from a login, an initial from an email, an astral first
character, and the
?when there is nothing to draw.tsc --noEmitclean in bothpackages/uiandapps/web.Biome (
pnpm lint) run and confirmed rather than assumed, sinceAvataristhe first component of its kind in
packages/ui: 406 files, no fixes.pnpm verifyrun on this branch. Everything passes except two tests underscripts/, which I do not believe are mine and which I checked rather thanassumed:
scripts/dependencies-security.test.mjs— patched image-size rejects non-advancing JXL/HEIF boxestimeout: 1_500, which a loaded machine misses.scripts/deploy-aws.test.mjs— AWS CLI adapter maps missing images … to stable denialsA full
pnpm verifyon the base branch fails both, so the diff here does notmake anything worse. It also cannot: it touches no file under
scripts/and addsno dependency — the whole diff is seven files in
apps/webandpackages/ui.Verified by hand on a live instance against GitHub-synced issues, which is where
every screenshot above comes from. Online: all three sites draw the avatar. With
both avatar hosts aborted at the network layer — and the blocked URLs read back to
confirm both hops of the 302 were really refused — all three degrade to the letter
with no glyph and no layout shift. And with
avatar_urland the GitHub login bothcleared on the principal, the topbar renders the letter taken from the email where
it used to render nothing.
Two limits, stated rather than rounded up:
+Nwas exercised live at N=1 only,as in #171 and for the same reason — GitHub assigns only users who can reach the
repository, and the test instance had two accounts. The count itself is covered by
unit test. And the cross-engine fallback result is from a standalone harness in all
three engines; the live checks above were Chromium only.
🤖 Generated with Claude Code