-
Notifications
You must be signed in to change notification settings - Fork 851
Forms: Fix dashboard URL parameter extraction for external admin contexts #46728
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖 🔴 Action required: Please include detailed testing steps, explaining how to test your change, like so: 🔴 Action required: We would recommend that you add a section to the PR description to specify whether this PR includes any changes to data or privacy, like so: Follow this PR Review Process:
If you have questions about anything, reach out in #jetpack-developers for guidance! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request addresses TanStack Router hook errors that occur when the Forms dashboard loads as a content module in external admin contexts. The solution replaces the framework-specific useParams and useSearch hooks with custom URL parsing utilities that directly read from window.location, making them compatible with both standard Jetpack and external admin environments.
Changes:
- Replace TanStack Router hooks with custom URL parameter extraction functions
- Add support for parsing parameters embedded in the
?p=query parameter used by external admin routing - Handle JSON-encoded
responseIdsarrays in URL parameters
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| projects/packages/forms/routes/responses/stage.tsx | Replaces useParams/useSearch with custom hooks useViewParam/useSearchParams that parse URL parameters directly; adds helper functions for URL parsing |
| projects/packages/forms/routes/responses/inspector/index.tsx | Applies the same URL parsing solution as stage.tsx to fix parameter extraction in the inspector panel |
| projects/packages/forms/changelog/fix-forms-external-admin-url-params | Adds changelog entry for the bug fix |
| /** | ||
| * Parse URL to get the effective pathname (handles external admin's ?p= parameter). | ||
| * | ||
| * @return The effective pathname. | ||
| */ | ||
| function getEffectivePathname(): string { | ||
| const url = new URL( window.location.href ); | ||
| return url.searchParams.get( 'p' ) || url.pathname; | ||
| } | ||
|
|
||
| /** | ||
| * Extract the view parameter from the current URL. | ||
| * Works in both standard Jetpack and external admin contexts. | ||
| * | ||
| * @return The current view parameter. | ||
| */ | ||
| function useViewParam(): string { | ||
| const [ view, setView ] = useState( () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| return match?.[ 1 ] || 'inbox'; | ||
| } ); | ||
|
|
||
| useEffect( () => { | ||
| const handleUrlChange = () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| setView( match?.[ 1 ] || 'inbox' ); | ||
| }; | ||
|
|
||
| window.addEventListener( 'popstate', handleUrlChange ); | ||
| return () => window.removeEventListener( 'popstate', handleUrlChange ); | ||
| }, [] ); | ||
|
|
||
| return view; | ||
| } | ||
|
|
||
| /** | ||
| * Parse responseIds from raw values, handling JSON-encoded arrays. | ||
| * | ||
| * @param rawIds - Raw responseIds values from URL. | ||
| * @return Parsed array of ID strings. | ||
| */ | ||
| function parseResponseIds( rawIds: string[] ): string[] { | ||
| const result: string[] = []; | ||
| for ( const raw of rawIds ) { | ||
| // Check if it's a JSON-encoded array (e.g., '["61416"]') | ||
| if ( raw.startsWith( '[' ) ) { | ||
| try { | ||
| const parsed = JSON.parse( raw ); | ||
| if ( Array.isArray( parsed ) ) { | ||
| result.push( ...parsed.map( String ) ); | ||
| continue; | ||
| } | ||
| } catch { | ||
| // Not valid JSON, treat as regular value | ||
| } | ||
| } | ||
| result.push( raw ); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Parse search parameters from the URL. | ||
| * In external admin contexts, params may be embedded inside the ?p= value. | ||
| * | ||
| * @return Object with responseIds and search parameters. | ||
| */ | ||
| function parseSearchParams(): { responseIds?: string[]; search?: string } { | ||
| const url = new URL( window.location.href ); | ||
|
|
||
| // First check direct query params (Jetpack context) | ||
| let rawResponseIds = url.searchParams.getAll( 'responseIds' ); | ||
| let search = url.searchParams.get( 'search' ) || undefined; | ||
|
|
||
| // If not found, check inside the p parameter (external admin context) | ||
| if ( rawResponseIds.length === 0 ) { | ||
| const pValue = url.searchParams.get( 'p' ); | ||
| if ( pValue && pValue.includes( '?' ) ) { | ||
| const pUrl = new URL( pValue, window.location.origin ); | ||
| rawResponseIds = pUrl.searchParams.getAll( 'responseIds' ); | ||
| if ( ! search ) { | ||
| search = pUrl.searchParams.get( 'search' ) || undefined; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const responseIds = parseResponseIds( rawResponseIds ); | ||
| return { | ||
| responseIds: responseIds.length > 0 ? responseIds : undefined, | ||
| search, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Extract search parameters from the URL. | ||
| * Works in both standard Jetpack and external admin contexts. | ||
| * | ||
| * @return Object with responseIds and search parameters. | ||
| */ | ||
| function useSearchParams(): { responseIds?: string[]; search?: string } { | ||
| const [ searchState, setSearchState ] = useState( parseSearchParams ); | ||
|
|
||
| useEffect( () => { | ||
| const handleUrlChange = () => setSearchState( parseSearchParams() ); | ||
| window.addEventListener( 'popstate', handleUrlChange ); | ||
| return () => window.removeEventListener( 'popstate', handleUrlChange ); | ||
| }, [] ); | ||
|
|
||
| return searchState; | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The helper functions getEffectivePathname, useViewParam, parseResponseIds, parseSearchParams, and useSearchParams are duplicated identically in both stage.tsx and inspector/index.tsx files (113 lines of duplicated code). This violates DRY principles and creates a maintenance burden. Consider extracting these functions into a shared utility file (e.g., src/hooks/use-url-params.ts or src/util/url-params.ts) and importing them in both files.
| @@ -0,0 +1,4 @@ | |||
| Significance: patch | |||
| Type: fixed | |||
| Comment: Fix Forms dashboard URL parameter extraction to work in external admin contexts | |||
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changelog entry describes implementation details ("URL parameter extraction") rather than the user-facing impact. According to the coding guidelines, changelog entries should describe the change from a user's perspective. Consider revising to focus on what users experience, such as "Dashboard: Fix console errors when loading Forms dashboard in external admin contexts." This makes it clear what users would notice (errors are fixed) rather than how it was fixed (URL parameter extraction).
| function useViewParam(): string { | ||
| const [ view, setView ] = useState( () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| return match?.[ 1 ] || 'inbox'; | ||
| } ); | ||
|
|
||
| useEffect( () => { | ||
| const handleUrlChange = () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| setView( match?.[ 1 ] || 'inbox' ); | ||
| }; | ||
|
|
||
| window.addEventListener( 'popstate', handleUrlChange ); | ||
| return () => window.removeEventListener( 'popstate', handleUrlChange ); | ||
| }, [] ); | ||
|
|
||
| return view; | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The custom hooks useViewParam and useSearchParams only update their state on popstate events (browser back/forward navigation). However, when navigate() is called programmatically (e.g., line 1076 for tab changes, lines 345 and 358 for search/selection changes), the URL changes but popstate doesn't fire. This means the hooks won't reflect the new URL state, leading to stale values in currentView and searchParams. Consider also listening to URL changes after programmatic navigation, or use a different approach such as a custom event that fires whenever the URL is programmatically changed, or poll window.location on a timer/animation frame if the router doesn't provide a way to subscribe to programmatic navigation changes.
| function useViewParam(): string { | ||
| const [ view, setView ] = useState( () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| return match?.[ 1 ] || 'inbox'; | ||
| } ); | ||
|
|
||
| useEffect( () => { | ||
| const handleUrlChange = () => { | ||
| const pathname = getEffectivePathname(); | ||
| const match = pathname.match( /(?:\/responses\/|\/marketing\/forms\/|\/forms\/)([^/?#]+)/ ); | ||
| setView( match?.[ 1 ] || 'inbox' ); | ||
| }; | ||
|
|
||
| window.addEventListener( 'popstate', handleUrlChange ); | ||
| return () => window.removeEventListener( 'popstate', handleUrlChange ); | ||
| }, [] ); | ||
|
|
||
| return view; | ||
| } |
Copilot
AI
Jan 22, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The custom hooks useViewParam and useSearchParams only update their state on popstate events (browser back/forward navigation). However, when navigate() is called programmatically elsewhere in the component, the URL changes but popstate doesn't fire. This means the hooks won't reflect the new URL state, leading to stale values in currentView and searchParams. Consider also listening to URL changes after programmatic navigation, or use a different approach such as a custom event that fires whenever the URL is programmatically changed.
…exts Use URL-based parameter extraction instead of TanStack Router hooks (useParams, useSearch) that fail when loaded as a content module in external admin shells that don't establish the expected router context.
81a093a to
9efa835
Compare
Code Coverage SummaryThis PR did not change code coverage! That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷 |
Summary
useParams/useSearch) when Forms dashboard loads as a content module in external admin contextsresponseIdsarrays in URL parameters?p=parameter used by external admin routingTest plan
?page=next-admin&p=/marketing/forms/inbox🤖 Generated with Claude Code