Skip to content

Add modal URL router (#modal=name&tab=key)#3924

Merged
evanpelle merged 1 commit into
mainfrom
modal-router
May 14, 2026
Merged

Add modal URL router (#modal=name&tab=key)#3924
evanpelle merged 1 commit into
mainfrom
modal-router

Conversation

@evanpelle
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle commented May 14, 2026

Description

Adds a modal URL router so modals can be opened, deep-linked, and bookmarked via the hash. URLs of the form #modal=<name>&tab=<key>&... open the named modal and pass remaining keys as args to onOpen. The reverse direction also syncs: opening a modal via the UI updates the URL, closing it clears the hash, and switching tabs updates &tab=.

Builds on the BaseModal refactor from #3923.

What's new

ModalRouter.ts — small registry + two-way sync helper.

  • register(name, { tag, pageId? }) declares a modal as router-managed
  • routeFromHash() parses #modal=... and dispatches to modal.open(args)
  • syncOpened/syncClosed/syncTab push state back into the URL via history.replaceState (no history entries)
  • A routingFromUrl flag prevents URL→modal→URL feedback loops
  • Unknown modal names silently strip the hash

BaseModal — opt-in URL sync via a routerName property.

  • When set, BaseModal calls into modalRouter.syncOpened/syncClosed/syncTab from open / close / setActiveTab
  • Modals that own their own URL state (lobby modals) just leave routerName undefined

Main.ts — registers all routable modals and wires the router.

  • handleUrl(): adds a modalRouter.routeFromHash() branch after the path-based lobby join
  • onHashUpdate: when the hash is router-managed, routes via the router instead of tearing down lobby state

Routable modals

13 inline modals: store, settings, leaderboard, clan, account, help, news, language, single-player, ranked, troubleshooting, territory-patterns, flag-input.

Excluded by design: join-lobby, host-lobby (own URL state via /game/<id>), matchmaking (no URL state).

Example uses

  • Deep link to store flags tab: /#modal=store&tab=flags
  • Settings keybinds tab: /#modal=settings&tab=keybinds
  • Cosmetics.ts now redirects to #modal=store&tab=packs when a hard-currency purchase fails for insufficient plutonium (after the alert), so users can top up directly

URL behavior

  • replaceState everywhere — no history entries added when modals open / close / switch tabs
  • Browser back/forward still works for the existing path-based game flow
  • hashchange events are router-aware so external hash changes (back button, manual edit) correctly switch between routed modals without tearing down lobby state

Please complete the following:

  • I have added screenshots for all UI updates (no visual changes; smoke-tested in dev)
  • I process any text displayed to the user through translateText() and I've added it to the en.json file (no new user-visible strings)
  • I have added relevant tests to the test directory (no test coverage; manually tested URL load, UI open, tab switch, close, hashchange, insufficient-plutonium redirect)
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

DISCORD_USERNAME

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75395286-8a46-40d7-894f-467221616c84

📥 Commits

Reviewing files that changed from the base of the PR and between f09aadf and 2ab908f.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/ClanModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/LanguageModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Main.ts
  • src/client/ModalRouter.ts
  • src/client/NewsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/BaseModal.ts
  • src/client/components/RankedModal.ts
✅ Files skipped from review due to trivial changes (3)
  • src/client/LanguageModal.ts
  • src/client/Store.ts
  • src/client/UserSettingModal.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • src/client/TroubleshootingModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/AccountModal.ts
  • src/client/Cosmetics.ts
  • src/client/SinglePlayerModal.ts
  • src/client/components/RankedModal.ts
  • src/client/FlagInputModal.ts
  • src/client/ClanModal.ts
  • src/client/HelpModal.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/NewsModal.ts
  • src/client/Main.ts
  • src/client/ModalRouter.ts

Walkthrough

BaseModal now optionally syncs modal state with modalRouter: it imports modalRouter, exposes protected routerName?: string, validates requested tabs in open(args), and calls modalRouter.syncOpened, syncClosed, and syncTab when routerName is set.

Changes

BaseModal router integration

Layer / File(s) Summary
Import and routerName field
src/client/components/BaseModal.ts
Adds import modalRouter and protected routerName?: string to BaseModal.
open(args) tab handling
src/client/components/BaseModal.ts
Validates incoming args.tab into requestedTab; sets activeTab only on initial open and routes subsequent tab changes through setActiveTab().
syncOpened call on open
src/client/components/BaseModal.ts
Calls modalRouter.syncOpened(routerName, args) after showing/opening the modal when routerName is set.
syncClosed call on close
src/client/components/BaseModal.ts
Calls modalRouter.syncClosed(routerName) during close() when routerName is set.
syncTab on tab changes
src/client/components/BaseModal.ts
Calls modalRouter.syncTab(routerName, key) inside setActiveTab() after updating the active tab when routerName is set.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

A tiny field, a routed name,
BaseModal learns the hash-based game.
Open, switch, and close with care—
modalRouter keeps state there. 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a modal URL router with hash-based navigation support (#modal=name&tab=key).
Description check ✅ Passed The description thoroughly explains the feature, implementation details, affected files, routable modals, usage examples, and testing approach—all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@evanpelle evanpelle changed the title router Add modal URL router (#modal=name&tab=key) May 14, 2026
@evanpelle evanpelle marked this pull request as ready for review May 14, 2026 22:54
@evanpelle evanpelle requested a review from a team as a code owner May 14, 2026 22:54
@evanpelle evanpelle added this to the v32 milestone May 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/client/ModalRouter.ts (1)

126-138: ⚡ Quick win

Consider clarifying or enforcing string-only args.

Line 132 checks typeof value === "string" before writing to the hash, silently dropping non-string values from args: Record<string, unknown>. If a modal calls modalRouter.syncOpened(name, {tab: 0}), the tab param won't appear in the URL.

URL query params are inherently strings, so this filter makes sense, but it's implicit. Consider:

  • Typing args as Record<string, string> in the public methods to make the contract explicit, or
  • Adding a runtime warning when non-string values are encountered and dropped
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/ModalRouter.ts` around lines 126 - 138, The writeHash function
currently accepts args: Record<string, unknown> and silently drops non-string
values (in writeHash), which hides misuse; update the public modal-router API so
args are typed as Record<string, string> (e.g., change signatures that call
writeHash/syncOpened to accept Record<string,string>) to make the contract
explicit and also add a small runtime check inside writeHash that logs a warning
(e.g., console.warn or processLogger.warn) when a non-string value is
encountered and skipped so callers get visible feedback; reference writeHash and
the public methods like syncOpened/replaceHash when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/ModalRouter.ts`:
- Around line 66-79: ModalRouter currently calls window.showPage(entry.pageId)
which already triggers the inline modal's open, then calls el?.open(args) again
causing onOpen to run twice; update ModalRouter (around the routingFromUrl
handling in ModalRouter) to avoid the duplicate open by either passing args
through to showPage (if showPage can accept them), or by changing the flow to
use the element returned/located by showPage and skip the second el?.open(args)
call; reference routines: ModalRouter, window.showPage, RoutableModal, open,
BaseModal.onOpen — implement the chosen option and ensure routingFromUrl logic
still sets/clears the flag as before.

---

Nitpick comments:
In `@src/client/ModalRouter.ts`:
- Around line 126-138: The writeHash function currently accepts args:
Record<string, unknown> and silently drops non-string values (in writeHash),
which hides misuse; update the public modal-router API so args are typed as
Record<string, string> (e.g., change signatures that call writeHash/syncOpened
to accept Record<string,string>) to make the contract explicit and also add a
small runtime check inside writeHash that logs a warning (e.g., console.warn or
processLogger.warn) when a non-string value is encountered and skipped so
callers get visible feedback; reference writeHash and the public methods like
syncOpened/replaceHash when making these changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0b03862-3ead-4634-a53b-0ab6dfcd254d

📥 Commits

Reviewing files that changed from the base of the PR and between bbe727c and f09aadf.

📒 Files selected for processing (17)
  • src/client/AccountModal.ts
  • src/client/ClanModal.ts
  • src/client/Cosmetics.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/LanguageModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Main.ts
  • src/client/ModalRouter.ts
  • src/client/NewsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/BaseModal.ts
  • src/client/components/RankedModal.ts

Comment thread src/client/ModalRouter.ts
Comment on lines +66 to +79
this.routingFromUrl = true;
try {
this.currentName = name;
if (entry.pageId) {
// Inline modal: showPage reveals the page-content container and calls
// .open() on the inline modal element automatically. We then call
// .open(args) so the args reach onOpen.
window.showPage?.(entry.pageId);
}
const el = document.querySelector(entry.tag) as RoutableModal | null;
el?.open(args);
} finally {
this.routingFromUrl = false;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Potential double-open fragility for inline modals.

For inline modals with pageId, Line 73 calls window.showPage(pageId), which (per BaseModal comments) "calls .open() on the inline modal element automatically." Then Line 76 explicitly calls el?.open(args) again.

BaseModal handles this by checking wasOpen and returning early at Line 174, but onOpen(args?) still runs twice—once with no args, once with args. This relies on BaseModal's specific wasOpen logic and assumes onOpen tolerates being called twice.

Consider a more explicit contract:

  • Either pass args directly to showPage if it supports them, or
  • Have showPage return a reference and skip the second open() call, or
  • Document this double-call requirement clearly in RoutableModal interface

This pattern is fragile if showPage or BaseModal internals change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/ModalRouter.ts` around lines 66 - 79, ModalRouter currently calls
window.showPage(entry.pageId) which already triggers the inline modal's open,
then calls el?.open(args) again causing onOpen to run twice; update ModalRouter
(around the routingFromUrl handling in ModalRouter) to avoid the duplicate open
by either passing args through to showPage (if showPage can accept them), or by
changing the flow to use the element returned/located by showPage and skip the
second el?.open(args) call; reference routines: ModalRouter, window.showPage,
RoutableModal, open, BaseModal.onOpen — implement the chosen option and ensure
routingFromUrl logic still sets/clears the flag as before.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 14, 2026
@evanpelle evanpelle merged commit bcc453e into main May 14, 2026
13 of 14 checks passed
@evanpelle evanpelle deleted the modal-router branch May 14, 2026 23:49
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant