[COMMS-889] Fix the work package URL if it does not use the canoncial identifier of a work package#23818
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses STC-835 by ensuring that when a work package page is already correctly loaded but the browser URL contains an outdated work package identifier, the URL is rewritten client-side to match the canonical identifier (avoiding redirects and helping “auto-fix” old bookmarks).
Changes:
- Adds a Turbo global listener helper that compares the current
/work_packages/:idsegment with the page’s<link rel="canonical">and rewrites the URL viahistory.replaceState. - Adds a new unit spec covering several rewrite/no-rewrite cases, including preserving query/hash.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| frontend/src/turbo/turbo-global-listeners.ts | Adds the canonicalization helper and invokes it on Turbo render / DOMContentLoaded. |
| frontend/src/turbo/turbo-global-listeners.spec.ts | Adds unit tests validating URL rewrite behavior against the canonical link. |
1fce673 to
d85edf1
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
| expect(page).to have_current_path(semantic_path("relations")) | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
@myabc if you have an idea on what should be tested additionally, please let me know 🙂 The calendar service turned out to be independent of the routes that are rewritten (as far as I understand it)
820957d to
d5b4892
Compare
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
myabc
left a comment
There was a problem hiding this comment.
@judithroth I'm sorry for the glacial pace of my review. I've since introduced some basic spec coverage for the existing global listeners (#23980), which will unfortunately necessitate rebasing/updating this PR.
|
|
||
| export function canonicalizeWorkPackageIdInUrl():void { | ||
| const currentPath = window.location.pathname; | ||
| const wpIdPattern = /\/work_packages\/([^/]+)/; |
There was a problem hiding this comment.
nitpick: This could make use of the new URLPattern API.
Also consider reusing WP_ID_URL_PATTERN from core-app/shared/helpers/work-package-id-pattern..
There was a problem hiding this comment.
Good catch, thanks for pointing it out!
| describe "on initial page load (DOMContentLoaded)" do | ||
| it "rewrites a numeric work package ID to the canonical semantic ID" do | ||
| visit numeric_path | ||
| wait_for_network_idle |
There was a problem hiding this comment.
You should be able to use the more granular wait_for_turbo if this is a Turbo Drive visit.
| project_work_package_path(project, work_package.reload.identifier, tab) | ||
| end | ||
|
|
||
| describe "on initial page load (DOMContentLoaded)" do |
myabc
left a comment
There was a problem hiding this comment.
I haven't finalised my review, but this is the crux of it.
I may need to verify a few things with @HDinger . Thanks again for your patience @judithroth !
| visit project_path(project) | ||
| wait_for_reload | ||
|
|
||
| page.execute_script("Turbo.visit('#{numeric_path}')") |
There was a problem hiding this comment.
nit: can this be made more realistic, e.g. by following a numeric link? (perhaps in user-generated content?)
| window.history.replaceState( | ||
| window.history.state, | ||
| '', | ||
| newPath + window.location.search + window.location.hash, | ||
| ); |
There was a problem hiding this comment.
I've dug into this a bit more and my concerns about calling plain replaceState were somewhat valid. While passing window.history.state through does preserve the restorationIdentifier, Turbo's Session.history.location and view.lastRenderedLocation still end up holding the pre-rewrite URL.
The consequences of this are nuanced and complex (and if weren't for Claude, I doubt I would fully understand them myself). However they can lead to real broken behaviour. In detail:
- Snapshot cache is keyed by
lastRenderedLocation(the old numeric URL), while back/forwardpopstateresolves the rewritten canonical URL, leading to cache lookup misses and a full network re-fetch instead of instant restore. The feature spec only asserts the URL and doesn't observe the refresh itself. - Action misclassification at three sites (
Visit.changeHistory,#getDefaultAction,PageView.isPageRefresh): links/forms targeting the now-canonical URL get "advance" instead of "replace" (duplicate history entry), and refresh-morph detection breaks.
| window.history.replaceState( | |
| window.history.state, | |
| '', | |
| newPath + window.location.search + window.location.hash, | |
| ); | |
| const newUrl = new URL( | |
| newPath + window.location.search + window.location.hash, | |
| window.location.origin, | |
| ); | |
| // Go through Turbo's history API so Session.history.location stays in | |
| // sync; pass the current restorationIdentifier, as replace() would | |
| // otherwise mint a fresh one and orphan the recorded scroll position. | |
| Turbo.session.history.replace(newUrl, Turbo.session.history.restorationIdentifier); | |
| // turbo:render fires after Turbo has already stamped lastRenderedLocation, | |
| // so correct it too — it keys the snapshot cache and refresh detection. | |
| (Turbo.session as unknown as { view:{ lastRenderedLocation:URL } }).view.lastRenderedLocation = newUrl; |
N.B. the Turbo typings look like they are missing lastRenderedLocation - will try to fix upstream...
There was a problem hiding this comment.
FYI, typings fix upstream: DefinitelyTyped/DefinitelyTyped#75208
There was a problem hiding this comment.
Split into smaller PRs: DefinitelyTyped/DefinitelyTyped#75214
| window.history.pushState({}, '', '/'); | ||
| document.querySelectorAll('link[rel="canonical"]').forEach((element) => element.remove()); | ||
| /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-empty-function */ | ||
| replaceStateSpy = vi.spyOn(window.history, 'replaceState').mockImplementation(() => {}); |
There was a problem hiding this comment.
This should be changed to spy on Turbo.session.history.replace.
| window.history.pushState({}, '', '/'); | ||
| document.querySelectorAll('link[rel="canonical"]').forEach((element) => element.remove()); |
There was a problem hiding this comment.
afterEach.
| page.go_back | ||
| wait_for_network_idle | ||
|
|
||
| expect(page).to have_current_path(semantic_path) |
There was a problem hiding this comment.
I suspect that this will still pass, even if you remove the canonicalizeWorkPackageIdInUrl() call from runOnRenderAndLoad.. since we're only asserting the URL, this can't detect that the cache has been missed.
https://community.openproject.org/wp/COMMS-889 Since the correct page is loaded already and only the URL in the browser uses an outdated identifier, the URL is rewritten by a little javascript helper. This has the added benefit of also "auto-fixing" bookmarks of old identifiers or fixing the URL if a numeric work package ID was used while the instance is in semantic mode or vice versa. No redirects needed.
https://community.openproject.org/wp/COMMS-889 Turbo Drive calls history.pushState({ turbo: { restorationIdentifier, visitId } }, '', url) before firing turbo:render. Passing null as the state discards that metadata. Turbo uses the restorationIdentifier to restore a cached snapshot on back/forward navigation — without it, Turbo falls back to a full server fetch. The page still loads correctly, but cache-based back/forward performance is lost. Everything else about the implementation is compatible with Turbo navigation: - turbo:render fires after <head> is merged, so the canonical link is already up to date - replaceState (not pushState) corrects the current history entry without adding a new one - Angular's UI Router is restarted on turbo:load (which fires after turbo:render), so it sees the already-corrected URL
d5b4892 to
e9c5d78
Compare

https://community.openproject.org/wp/COMMS-889
Since the correct page is loaded already and only the URL in the browser uses an outdated identifier, the URL is rewritten by a little javascript helper.
This has the added benefit of also "auto-fixing" bookmarks of old identifiers or fixing the URL if a numeric work package ID was used while the instance is in semantic mode or vice versa. No redirects needed.
Merge checklist