Skip to content

Conversation

@lezama
Copy link
Contributor

@lezama lezama commented Jan 22, 2026

Summary

  • Fix TanStack Router hook errors (useParams/useSearch) when Forms dashboard loads as a content module in external admin contexts
  • Use URL-based parameter extraction that works in both standard Jetpack and external admin environments
  • Handle JSON-encoded responseIds arrays in URL parameters
  • Parse params embedded inside the ?p= parameter used by external admin routing

Test plan

  • Load Forms dashboard in standard Jetpack wp-admin context
  • Load Forms dashboard in external admin context via ?page=next-admin&p=/marketing/forms/inbox
  • Click "View" on a response - Inspector panel should show response details
  • Verify no console errors related to TanStack Router

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings January 22, 2026 14:42
@github-actions
Copy link
Contributor

github-actions bot commented Jan 22, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the fix/forms-external-admin-url-params branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/forms-external-admin-url-params

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • 🔴 Add a "[Status]" label (In Progress, Needs Review, ...).
  • 🔴 Add testing instructions.
  • 🔴 Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

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:

## Testing instructions:

* Go to '..'
*

🔴 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:

## Does this pull request change what data or activity we track or use?

My PR adds *x* and *y*.

Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@github-actions github-actions bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Jan 22, 2026
Copy link
Contributor

Copilot AI left a 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 responseIds arrays 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

Comment on lines 62 to 173
/**
* 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;
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed
Comment: Fix Forms dashboard URL parameter extraction to work in external admin contexts
Copy link

Copilot AI Jan 22, 2026

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).

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 78 to 97
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;
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 76
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;
}
Copy link

Copilot AI Jan 22, 2026

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.

Copilot uses AI. Check for mistakes.
…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.
@lezama lezama force-pushed the fix/forms-external-admin-url-params branch from 81a093a to 9efa835 Compare January 22, 2026 15:45
@jp-launch-control
Copy link

Code Coverage Summary

This 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. 🤷

Full summary · PHP report · JS report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Contact Form [Package] Forms [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants