Uh oh!
There was an error while loading. Please reload this page.
fix(console): route /accept-invitation to DefaultAcceptInvitationPage, retire the duplicate acceptInvitation.* namespace (#3811) - #4168
Merged
Conversation
…#3811) Two components shipped for `/accept-invitation/:invitationId` under two i18n namespaces, and console routed the weaker one: a thin accept/decline pair that never showed which organization, which role or when the link expired, and left the user in whatever organization they were already in. App-shell's page — exported as `DefaultAcceptInvitationPage`, routed by nobody — does all of that plus `switchOrganization` on accept. Maintainer ruling on #3811 is option A: console routes the richer page; the thin page and its `acceptInvitation.*` namespace (12 keys x 10 packs) are deleted. Nothing published is removed — `DefaultAcceptInvitationPage` keeps its export and becomes the routed one. The #3546 slice-three assertion that pinned "two namespaces stay separate" now pins the inverse, negatively: no pack may define any of the 12 retired keys or an emptied namespace root, and neither consuming package may ask `t()` for one. One repair was required before the swap was safe. `?redirect=` is a basename-stripped path by contract here (`LoginPage.withConsoleBase` re-prefixes the mount); the thin page built it from the route param and app-shell's built it from `window.location.pathname`, which already carries the mount. A console under `<base href="/console/">` would have returned the user to `/console/console/accept-invitation/…`. It now reads the router, like every other producer of that parameter in this repo. Measured, not argued: the basename case fails on the pre-fix source and passes after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 10, 2026 15:27
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 10, 2026
Merged
Closed
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 freeto 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.
Fixes#3811
Option A, per the maintainer ruling of 2026-08-09 and the delegated ruling carried in the PM claim: one route, one component, one namespace.
What changed
apps/console/src/App.tsxnow routes/accept-invitation/:invitationIdto app-shell'sDefaultAcceptInvitationPage. The console-local thin page is deleted, and itsacceptInvitation.*namespace (12 keys in each of the ten packs, 120 lines) goes with it. Nothing published was removed —DefaultAcceptInvitationPagekeeps its export and simply becomes the routed implementation.The user-visible gain is the whole point of the ruling: the old page offered an Accept and a Decline button and nothing else. The new one fetches the invitation and shows which organization, which role, and when the link expires — then switches the user into that organization on accept.
Premise check (all claims held on
origin/main@f7c6430ec)apps/console/src/pages/auth/AcceptInvitationPage.tsxandpackages/app-shell/src/console/organizations/manage/AcceptInvitationPage.tsxApp.tsx:186-189acceptInvitation12,organization.accept14, in all ten packsacceptInvitation.*t()forms found only the deleted page, the slice-three test, andCHANGELOG.mdhistoryBinding condition 1 — the real-render verification
The ruling presumed the app-shell page renders acceptably inside console's layout. That presumption is now measured rather than assumed, in
apps/console/src/pages/auth/__tests__/AcceptInvitationRoute.test.tsx.First, what console's route actually is: bare.
App.tsxwraps this route in no layout at all —AuthLayoutexists andLoginPage/ResetPasswordPage/SetPasswordPageuse it, but/accept-invitationnever did, and neither page (old or new) references it. Both paint their own full-viewport shell. So the page is mounted in the test exactly as the route mounts it: inside a realBrowserRouter, on the real path, with no wrapper.Second, it is the shipped export that is mounted — imported from
@object-ui/app-shell, not from a source path — and rendered with the real@object-ui/componentsButtonand real lucide icons, not passthrough stubs. A stubbed primitive can tell you the component called it; it can never tell you the result is a coherent screen. Asserted: the card keeps its chrome (rounded-xl border bg-card p-8 shadow-sm), it centres inside amin-h-svhshell, both buttons render as real enabledbuttonelements carrying cva-generated Shadcn classes, and the title is a realh1.Result: the render is fine. Six of the eight new cases passed against the unmodified app-shell page on the first run. The ruling's presumption holds — no styling work was needed.
Binding condition 5 — behavior pins, and one repair they turned up
Pinned at route level: anonymous visitors still bounce to
/login?redirect=; an authenticated visitor sees organization, role and expiry; Accept callsacceptInvitationand thenswitchOrganization(order asserted, not just both-were-called) and lands on/home; Decline rejects and lands on/organizations; and rendering inzhproves the copy now arrives from the survivingorganization.accept.*pack.The redirect pin is the one that found something, and it is a deliberate deviation from the "app-shell untouched" expectation in the dispatch note — flagged here so it can be rejected on its own:
?redirect=is a basename-stripped path by contract in this console.LoginPage.withConsoleBase()re-prefixes the mount before its full-page navigation, and its own doc comment spells out the failure mode;App.tsx'sLoginRedirectproduces the parameter fromuseLocation(). The deleted thin page built it from the route param — correct. App-shell's page built it fromwindow.location.pathname, which already carries the mount. A console served under abase hrefmount would therefore have sent a user who signed in from an invitation link back to/console/console/accept-invitation/….That is a regression this swap would have introduced, so it is repaired here rather than filed: the page now reads the router. Under the default
/mount both spellings are byte-identical, which is why the plain case alone would have shipped it green — the basename case is what separates them, and it is measured in both directions:Binding condition 4 — the #3546 slice-three assertion
packages/i18n/src/__tests__/auth-namespace-3546.test.tsxheld an assertion titled "acceptInvitationandorganization.acceptstay separate namespaces". It was right to exist: slice three noticed the duplication and pinned the fact in place rather than guessing which page should win. It now pins the inverse, and negatively, because the defect it guards is silent by construction — two namespaces for one screen read as perfectly healthy from inside either half, at full key parity with real translations in ten packs, and are visible only when both halves are laid side by side, which no gate does.Three assertions replace the one:
acceptInvitation: {}root — the shape a partial revert leaves behind. The retired keys are listed by name rather than counted, so a single key returning alone fails just as a full restore does.organization.accept.*is intact in all ten packs, checked on the four keys the thin page never had (accept,organization,role,expiresAt) — i.e. the deletion did not hollow out the surviving half.t()for anacceptInvitation.*key. The scan is call-shaped (t('acceptInvitation.and the template form), not a bare substring, because this file andApp.tsxboth name the retired namespace in prose to explain why it is retired. Pinning the reader matters becausecheck:i18n-keyswould report a revived call site as "key missing fromen", and the obvious repair for a missing key is to backfill it — which is exactly the move that rebuilds the second namespace.The rest of the file re-scopes from 54 keys to the 42 that survive (
auth26 +oauth16);42 + 12 = 54is asserted so the slice's arithmetic stays legible. Thefrandkocases that sampled retired keys moved to other user-facing buttons in the same packs, keeping the writing-system spread intact.Reverse-verified: reviving
acceptInvitation.acceptinzhalone turns two independent gates red — this pin (reporting both the key and the namespace root) andall-locales-key-parity's "zh defines no key that en lacks".Verification
vitest run apps/console/ packages/app-shell/vitest run packages/i18n/+ the new console testcheck:i18n-keys(all five classes)encheck:i18n-driftall-locales-key-paritytype-check, downstream sweep--filter '...@object-ui/i18n'(prefix = consumers)examples/console-starterandexamples/byo-backend-consoletype-check— console / i18n / app-shelllint— console / i18n / app-shellsetIsLoadinginside the fetch effect) is unchanged fromorigin/mainand only shifted line numbercheck-control-bytes,check-changeset-presence,check-changeset-fixed,check-changeset-no-major,check-skills-paths,docs:check-linksTranslationKeysistypeof en, so removing 12 keys narrows an exported type. The downstream sweep above is the prefix direction on purpose. It was also reverse-verified against a stale-.d.tsfalse green — a temporary probe confirmedTranslationKeys['acceptInvitation']now errors withTS2339: Property 'acceptInvitation' does not exist, whileTranslationKeys['organization']['accept']['accept']still resolves to the literal"Accept invitation"(so the check is not vacuous). Probe removed.Incidental
skills/objectui/guides/console-development.md:65already documented this route as going toDefaultAcceptInvitationPage. The documentation was ahead of the code; this PR makes it true. No doc edit needed.Generated by Claude Code