Repository navigation
fix: org and cluster card menu links no longer resolve one segment too deep - #1798
Draft
dawsontoth wants to merge 1 commit into
Draft
dawsontoth wants to merge 1 commit into
dawsontoth wants to merge 1 commit into
Conversation
…ment too deep
OrgCard and ClusterCard built their menu targets as bare relative segments (`${cluster.id}/sign-in`,
`${organizationId}/roles`, and OrgCard's whole-card `to={organizationId}`). TanStack resolves a
relative `to` against the location the Link renders under, so a card still mounted while the router
moves into a sibling entity re-resolved to /org-a/clu-b/clu-c/sign-in or /org-b/org-a — the latter
matching the cluster home with clusterId bound to an org id. Same class as #1710's instance rows.
Build all twelve targets with buildAbsoluteLinkToPage from the card's own entity ids, and narrow
EntityMenuItem.to and the helper's return to a new AbsolutePath (`/${string}`) type, so the next
relative menu target is a compile error rather than a latent bug. useInstanceMenuItems already
passes the helper's output and compiles unchanged.
Fixes #1711
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request enforces type safety and location independence for entity menu links by introducing a new AbsolutePath type and updating EntityMenuItem to require it. The ClusterCard and OrgCard components have been refactored to construct their menu links using buildAbsoluteLinkToPage instead of relative paths, and new unit tests have been added to verify correct link resolution. There are no review comments, so I have no feedback to provide.
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
⊙ Problem
The org and cluster cards built their menu targets as bare relative segments (
${cluster.id}/sign-in,${organizationId}/roles, and the org card's whole-cardto={organizationId}). TanStack resolves a relativetoagainst the location theLinkrenders under, so a card still mounted while the router moves into a sibling entity re-resolves its hrefs one segment too deep:/org-a/clu-b/clu-c/sign-in, or/org-b/org-a, which the production route tree matches as the cluster home withclusterId: "org-a". Same class as #1710 (instance rows). RUM shows no occurrences of this variant, so this closes the pattern rather than a live incident.Closes #1711
💡 Solution
All twelve targets now come from
buildAbsoluteLinkToPage, built from the card's own entity ids rather than the current location: five menu items plus the whole-card link in the org card, six menu items in the cluster card.EntityMenuItem.tois narrowed fromstringto the helper's newAbsolutePath(`/${string}`) type, and the helper earns that type with a template literal rather than a cast, so the next relative menu target failstscat its call site.useInstanceMenuItemsalready passes the helper's output and compiles unchanged.⚖️ Alternatives
renderEntityMenuItems(pass afrom, or prefix/): the renderer does not know which entity a relative string is relative to, and a blanket/would turnclu-c/sign-ininto an org route.string(exactly [RUM] Instance row links resolve relatively, producing /instance/instance/<id> and a dead-end sign-in for an instance that does not exist #1710's shape): [RUM] Instance row links resolve relatively, producing /instance/instance/<id> and a dead-end sign-in for an instance that does not exist #1710 did that for one surface and the same class survived in two more, because only a JSDoc line andDESIGN.mdstated the rule.paramsinEntityMenuItem: stronger route-shape checking, but it couples the generic menu to the production route union and migrates all three producers for a lexical invariant.🔧 Changes
src/lib/urls/buildAbsoluteLinkToPage.tsexportsAbsolutePathand returns it from a template literal, so the type is checked rather than cast; the output is unchanged.src/components/ui/entityMenu.tsxnarrowsEntityMenuItem.totoAbsolutePathand drops the JSDoc line that stated the rule in prose.src/features/organizations/components/OrgCard.tsxbuilds the org root once for the Clusters item and the whole-card link, and the Roles, Users, Billing and Settings items from the same ids.src/features/clusters/components/ClusterCard.tsxbuilds one ids object per card and uses it for all six menu targets, Direct Sign In through Deployments.DESIGN.mdadds #1711 to the routing note and records thatEntityMenuItem.tois now type-enforced.Swept
srcfor other rendered relative targets: none. The remaining relative ones (<Navigate to="../sign-in">inFinishSetup/ClusterInstanceSignIn,navigate({ to: parts.join('/') })in the org/instance list pages) fire once from the route component that owns the location, which the routing note already scopes as safe.✅ Verification
Route: router/component integration tests (not a browser-level pending-navigation replay).
orgCardLinkLocationIndependence.test.tsxandclusterCardLinkLocationIndependence.test.tsxmount each card under a real TanStack router at its own list page and at sibling/unrelated locations, open the dropdown, and assert every menu href (and the org card's overlay link) equals the absolute path for the card's own entity; they click one link per card and assert the router lands there, and match every asserted href against the productionrootRouteTreewith exact route ids and params./org-a/clu-b/clu-c/sign-in,/org-b/org-a/roles,/org-z/clu-z/instances/clu-c/edit); the own-list-page cases pass on base, as expected.buildAbsoluteLinkToPage.test.tspins the helper's output,expectTypeOfitsAbsolutePathreturn, and a@ts-expect-errorrelative menu target. Mutation-checked: looseningEntityMenuItem.toback tostringmakestsc -bfail withUnused '@ts-expect-error' directive; restoring the base card sources with the narrowed type makestsc -bfail in both cards.npx vitest run398 files / 3692 passed, 11 skipped (exit 0);npx tsc -bexit 0;npx oxlint --format stylish .exit 0;npx dprint checkexit 0; husky pre-commit (full vitest, oxlint, dprint, commitlint) passed.reacttype imports inentityMenu.tsxare pre-existing and elided underisolatedModules, unencoded ids and the helper's array join are unchanged from base, and the production route tree's loaders need auth, which is why the shape check usesmatchRoutes(the [RUM] Instance row links resolve relatively, producing /instance/instance/<id> and a dead-end sign-in for an instance that does not exist #1710 pattern). Its comment nit was taken. The domain adjudicator failed on an expired Claude OAuth session and the Cursor leg on the 1Password SSH agent, so the outside findings were not adjudicated.🤖 Generated with Claude Code
Related PRs: #1712 overlaps (merged predecessor; this extends its routing note and tightens the EntityMenuItem.to contract it documented)
Complexity: easy
Review-Coverage: authored=claude; ran=codex,gemini; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=1; full=1 @ 6ba1b9c
Review-Attention: read ~3m (raised: degraded review) @ 6ba1b9c