fix(webapp): confirm before minting an org invite, then show the link (BEA-192) - #202
Open
ssowonny wants to merge 3 commits into
Open
Conversation
… (BEA-192) One click on "Invite a teammate" in Project settings minted a live, redeemable org invite and then showed it nowhere on the page. On the copy-failure branch the toast pointed at Organization settings — which lists the ORG-scoped invite, without the "?p=" that is the whole feature, so following the advice silently downgraded the link. Replace the one-click mint with the two-phase dialog ShareDialog already uses, moved a step earlier: phase 1 names what the link grants and offers an expiry, and nothing is POSTed until the user confirms; phase 2 renders the project-scoped URL on the page with Copy / Open / Done. A failed copy now only changes a button label — the link never depended on the clipboard. The "copy it from Organization settings" promise is retired rather than repaired: making the Org list carry "?p=" needs the project persisted on the invite record, which the spec puts out of scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e (BEA-192)
disabled={busy} only lands after a repaint, so two clicks dispatched in one
task both ran mint() — verified: the new e2e case sees 2 invites without the
latch and 1 with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
TL;DR
?p=<project-id>, so following the advice quietly handed you a link that no longer opened the project. That promise is gone.?p=form still exists only in this dialog. Recovering it later means persisting the project on the invite record — out of scope here, worth its own issue.The bug, and which half of it was actually broken
One click, two paths, and the reported one was the bad half:
flowchart TB Click["click <b>Invite a teammate</b><br/>(no dialog, no confirmation)"] Mint["POST /api/orgs/<org>/invites<br/><b>a live, redeemable invite exists now</b>"] Copy{"copyText(url + "?p=" + project.id)"} OK["<b>copy ok</b><br/>toast: "Invite link copied"<br/>link is on the clipboard<br/>and nowhere on screen"] Bad["<b>copy failed</b><br/>toast: "copy it from Organization settings"<br/>not on screen. not on the clipboard.<br/>and the copy in Org settings<br/>has no "?p=""] Click --> Mint --> Copy Copy -->|ok| OK Copy -->|failed| Bad classDef bad fill:#ef444422,stroke:#ef4444,stroke-width:2px classDef warn fill:#eab30822,stroke:#eab308,stroke-width:2px class Bad bad class Mint warnPriya's report named the failure branch's toast. That branch is real — her automated browser had no clipboard, and it is the one with no recovery at all — but two things are true on both paths, and they're the substance of the fix: nothing asks before minting, and the link is never rendered.
The third defect isn't in the original report; it surfaced reading
ProjectSettings.tsxagainstOrgAdmin.tsx. The stored invite is org-scoped.OrgAdminlists it byinv.url, which carries no?p=. So the toast's own advice downgrades the link — and per the code comment that?p="is the whole feature": it lands the recipient on this project's install page instead of a nameless project list.What changed
New
components/InviteDialog.tsx,ShareDialog's two-phase shape moved a step earlier so nothing exists before you say yes:POSTat all?p=URL on the page, Copy / Open / DoneThe one thing worth a second look:
disabled={busy}is not enough on a control that mints a credential. Two clicks dispatched inside one task both runmint()before React repaints the button — I wrote the test expecting it to pass and it reported 2 invites. There is now auseReflatch, and the test fails without it (verified by removing it and re-running).ProjectSettingskeeps itsorg?.role === "owner"gate and its#ps-invitehandle untouched — the gate is on the org role deliberately, sincehandleInviteCreate403s a plain member, and a project admin who is a plain org member is exactly who'd otherwise be shown a button that fails.Expiry sits pre-mint here, the opposite of
ShareDialog. That's deliberate and the comment says why:ShareDialog's ordering is aboutShareDB.Createreusing an existing permanent link, so minting with a TTL would hand out a second one.CreateInvitealways mints a fresh token, so there's no second link to dodge.Why the Org-settings promise was retired rather than repaired (AC 4 allowed either): making that list carry
?p=needs anOrgInvite.Projectfield,handleInviteCreateaccepting it,handleInviteListemitting it, and bothMetaStorebackends including a schema migration — for a convenience. The spec put it out of scope.OrgAdmin's own mint button keeps its toast: its copy-failure branch points at the list directly below it, which really does show the link.Evidence
Driving the real UI on the seeded e2e hub and counting invites through
GET /api/orgs/<org>/invites:The handoff phase, in a context with no clipboard — the link is on the page regardless:
What was run
npm run e2e— 205 passed, 1 pre-existing skip.admin.spec.ts's invite spec rewritten (asserts the count is unchanged while the dialog is open, then that the visible.modal-urlcarries/join/and?p=<pid>, then that a double-clicked Copy still leaves exactly one invite), plus three new cases: Cancel mints nothing, a synchronous double-click on Create mints one, and apermissions: []context still shows the link.org admin: members with roles…whilego test ./...was saturating the machine; it passes 4/4 since, and onorigin/maintoo. Untouched by this branch.go test ./...— all packages pass.go vet ./...,go build ./...— clean.npm test— 115 frontend unit tests pass.npm run buildre-committedinternal/webapp/static;frontend/check-dist.shreports fresh.Architecture changes
architecture/webapp-frontend.md: thecomponentsclass gainsInviteDialog, and one note records the seam — that it isShareDialog's shape moved earlier, that the?p=form of the link exists only there, and why expiry sits pre-mint.ProjectSettings' click handler changes from "POST + copy + toast" to "open the dialog". No other diagram is in scope; nothing ininternal/webapp's Go types moved.flowchart TB components["<div style='text-align:left'><b>components</b> (excerpt)<br/>Insights ShareDialog NewProjectDialog<br/>OrgAdmin HubSettings ProjectSettings<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ InviteDialog</span></div>"] InviteDialog["<div style='text-align:left'><b>InviteDialog</b><br/>props: orgId, orgName, projectId, projectName<br/>phase 1: names the grant + expiry <select><br/>phase 1: Cancel ⇒ no POST<br/>phase 2: the "?p=" URL in .modal-url<br/>phase 2: Copy link / Open / Done</div>"] ProjectSettings["<div style='text-align:left'><b>ProjectSettings</b><br/>#ps-invite, org?.role === "owner"<br/><span style='background:#22c55e55;padding:0 4px;border-radius:3px'>✅ onClick ⇒ setInviting(true)</span><br/><span style='background:#ef444455;padding:0 4px;border-radius:3px'>❌ <s>onClick ⇒ postJSON + copyText + toast</s></span></div>"] ShareDialog["<div style='text-align:left'><b>ShareDialog</b><br/>the shape InviteDialog follows<br/>(expiry stays POST-mint here)</div>"] Note["InviteDialog is ShareDialog's two-phase shape,<br/>moved one step EARLIER: nothing is minted<br/>until the user confirms.<br/>The "?p=<project-id>" form exists ONLY here —<br/>the stored invite OrgAdmin lists is org-scoped,<br/>so the retired copy-failure toast pointed at a<br/>link that had silently lost its project.<br/>Expiry is pre-mint (opposite of ShareDialog):<br/>CreateInvite always mints a fresh token, so<br/>there is no already-copied link to keep valid."] components --> InviteDialog components --> ProjectSettings components --> ShareDialog ProjectSettings -- "<span style='background:#22c55e55;padding:0 5px;border-radius:3px'>✅ opens on confirm-first click</span>" --> InviteDialog InviteDialog -. modelled on .-> ShareDialog InviteDialog -.- Note classDef added fill:#22c55e22,stroke:#22c55e,stroke-width:2px classDef noteBox fill:#88888822,stroke:#888888,stroke-dasharray:2 2 class InviteDialog added class Note noteBox linkStyle 3 stroke:#22c55e,stroke-width:2pxDeviations from the reviewed plan
One, and it's cosmetic: the plan had the post-mint Copy button toast "Select and copy the link above" on failure. It still does — but the button label also reverts to "Copy link" rather than claiming "Copied ✓", which the plan didn't specify. Everything else follows the plan as written.
Not in this PR
OrgInvitegaining a project, so a link re-copied from Organization settings keeps its?p=. That is a schema migration across bothMetaStorebackends and the spec ruled it out; the dialog is now the one place the project-scoped link is handed over. Worth its own issue if the recovery path matters.Build session
(Only works on the machine this ran on.)