Server side route handling
This commit is contained in:
parent
123af97b1c
commit
aed4ea63b4
|
@ -3,14 +3,14 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Component, createElement } from '@wordpress/element';
|
||||
import { parse } from 'qs';
|
||||
import { find, last, isEqual } from 'lodash';
|
||||
import { parse, stringify } from 'qs';
|
||||
import { isEqual, last } from 'lodash';
|
||||
import { applyFilters } from '@wordpress/hooks';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { getNewPath, getPersistedQuery, getHistory, stringifyQuery } from '@woocommerce/navigation';
|
||||
import { getNewPath, getPersistedQuery, getHistory } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -104,19 +104,23 @@ export class Controller extends Component {
|
|||
return query;
|
||||
}
|
||||
|
||||
// @todo What should we display or do when a route/page doesn't exist?
|
||||
render404() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Pass URL parameters (example :report -> params.report) and query string parameters
|
||||
const { path, url, params } = this.props.match;
|
||||
const query = this.getQuery( this.props.location.search );
|
||||
const page = find( getPages(), { path } );
|
||||
const { page, match, location } = this.props;
|
||||
const { url, params } = match;
|
||||
const query = this.getBaseQuery( location.search );
|
||||
|
||||
if ( ! page ) {
|
||||
return null; // @todo What should we display or do when a route/page doesn't exist?
|
||||
return this.render404();
|
||||
}
|
||||
|
||||
window.wpNavMenuUrlUpdate( page, query );
|
||||
window.wpNavMenuClassChange( page );
|
||||
return createElement( page.container, { params, path: url, pathMatch: path, query } );
|
||||
window.wpNavMenuClassChange( page, url );
|
||||
return createElement( page.container, { params, path: url, pathMatch: page.path, query } );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,38 +129,34 @@ export class Controller extends Component {
|
|||
* as is.
|
||||
*
|
||||
* @param {HTMLElement} item - Sidebar anchor link.
|
||||
* @param {string} nextQuery - A query string to be added to updated hrefs.
|
||||
* @param {object} nextQuery - A query object to be added to updated hrefs.
|
||||
* @param {Array} excludedScreens - wc-admin screens to avoid updating.
|
||||
*/
|
||||
export function updateLinkHref( item, nextQuery, excludedScreens ) {
|
||||
/**
|
||||
* Regular expression for finding any WooCommerce Admin screen.
|
||||
* The groupings are as follows:
|
||||
*
|
||||
* 0 - Full match
|
||||
* 1 - "#/" (optional)
|
||||
* 2 - "analytics/" (optional)
|
||||
* 3 - Any string, eg "orders"
|
||||
* 4 - "?" or end of line
|
||||
*/
|
||||
const _exp = /page=wc-admin(#\/)?(analytics\/)?(.*?)(\?|$)/;
|
||||
const wcAdminMatches = item.href.match( _exp );
|
||||
const isWCAdmin = /admin.php\?page=wc-admin/.test( item.href );
|
||||
|
||||
if ( wcAdminMatches ) {
|
||||
// Get fourth grouping
|
||||
const screen = wcAdminMatches[ 3 ];
|
||||
if ( isWCAdmin ) {
|
||||
const search = last( item.href.split( '?' ) );
|
||||
const query = parse( search );
|
||||
const path = query.path || 'dashboard';
|
||||
const screen = path.replace( '/analytics', '' ).replace( '/', '' );
|
||||
|
||||
if ( ! excludedScreens.includes( screen ) ) {
|
||||
const url = item.href.split( 'wc-admin' );
|
||||
const hashUrl = last( url );
|
||||
const base = hashUrl.split( '?' )[ 0 ];
|
||||
const href = `${ url[ 0 ] }wc-admin${ '#' === base[ 0 ] ? '' : '#/' }${ base }${ nextQuery }`;
|
||||
item.href = href;
|
||||
}
|
||||
const isExcludedScreen = excludedScreens.includes( screen );
|
||||
|
||||
const href =
|
||||
'admin.php?' + stringify( Object.assign( query, isExcludedScreen ? {} : nextQuery ) );
|
||||
|
||||
// Replace the href so you can see the url on hover.
|
||||
item.href = href;
|
||||
|
||||
item.onclick = e => {
|
||||
e.preventDefault();
|
||||
getHistory().push( href );
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Update links in wp-admin menu to persist time related queries
|
||||
// Update's wc-admin links in wp-admin menu
|
||||
window.wpNavMenuUrlUpdate = function( page, query ) {
|
||||
const excludedScreens = applyFilters( TIME_EXCLUDED_SCREENS_FILTER, [
|
||||
'devdocs',
|
||||
|
@ -164,7 +164,7 @@ window.wpNavMenuUrlUpdate = function( page, query ) {
|
|||
'settings',
|
||||
'customers',
|
||||
] );
|
||||
const nextQuery = stringifyQuery( getPersistedQuery( query ) );
|
||||
const nextQuery = getPersistedQuery( query );
|
||||
|
||||
Array.from( document.querySelectorAll( '#adminmenu a' ) ).forEach( item =>
|
||||
updateLinkHref( item, nextQuery, excludedScreens )
|
||||
|
@ -172,7 +172,7 @@ window.wpNavMenuUrlUpdate = function( page, query ) {
|
|||
};
|
||||
|
||||
// When the route changes, we need to update wp-admin's menu with the correct section & current link
|
||||
window.wpNavMenuClassChange = function( page ) {
|
||||
window.wpNavMenuClassChange = function( page, url ) {
|
||||
Array.from( document.getElementsByClassName( 'current' ) ).forEach( function( item ) {
|
||||
item.classList.remove( 'current' );
|
||||
} );
|
||||
|
@ -186,11 +186,14 @@ window.wpNavMenuClassChange = function( page ) {
|
|||
element.classList.add( 'menu-top' );
|
||||
} );
|
||||
|
||||
const pageHash = window.location.hash.split( '?' )[ 0 ];
|
||||
const pageUrl =
|
||||
'/' === url
|
||||
? 'admin.php?page=wc-admin'
|
||||
: 'admin.php?page=wc-admin&path=' + encodeURIComponent( url );
|
||||
const currentItemsSelector =
|
||||
pageHash === '#/'
|
||||
? `li > a[href$="${ pageHash }"], li > a[href*="${ pageHash }?"]`
|
||||
: `li > a[href*="${ pageHash }"]`;
|
||||
url === '/'
|
||||
? `li > a[href$="${ pageUrl }"], li > a[href*="${ pageUrl }?"]`
|
||||
: `li > a[href*="${ pageUrl }"]`;
|
||||
const currentItems = document.querySelectorAll( currentItemsSelector );
|
||||
|
||||
Array.from( currentItems ).forEach( function( item ) {
|
||||
|
|
|
@ -102,9 +102,15 @@ class _PageLayout extends Component {
|
|||
<Router history={ getHistory() }>
|
||||
<Switch>
|
||||
{ getPages().map( page => {
|
||||
return <Route key={ page.path } path={ page.path } exact component={ Layout } />;
|
||||
return (
|
||||
<Route
|
||||
key={ page.path }
|
||||
path={ page.path }
|
||||
exact
|
||||
render={ props => <Layout page={ page } { ...props } /> }
|
||||
/>
|
||||
);
|
||||
} ) }
|
||||
<Route component={ Layout } />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
/** @format */
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { stringifyQuery } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
@ -12,25 +7,25 @@ import { updateLinkHref } from '../controller';
|
|||
describe( 'updateLinkHref', () => {
|
||||
const timeExcludedScreens = [ 'devdocs', 'stock', 'settings', 'customers' ];
|
||||
|
||||
const REPORT_URL =
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin#/analytics/orders?period=today&compare=previous_year';
|
||||
const DASHBOARD_URL =
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin#/?period=week&compare=previous_year';
|
||||
const DASHBOARD_URL_NO_HASH = 'http://example.com/wp-admin/admin.php?page=wc-admin';
|
||||
const REPORT_URL = 'http://example.com/wp-admin/admin.php?page=wc-admin&path=/analytics/orders';
|
||||
const DASHBOARD_URL = 'http://example.com/wp-admin/admin.php?page=wc-admin';
|
||||
const REPORT_URL_TIME_EXCLUDED =
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin&path=/analytics/settings';
|
||||
const WOO_URL = 'http://example.com/wp-admin/edit.php?post_type=shop_coupon';
|
||||
const WP_ADMIN_URL = 'http://example.com/wp-admin/edit-comments.php';
|
||||
|
||||
const nextQuery = stringifyQuery( {
|
||||
const nextQuery = {
|
||||
fruit: 'apple',
|
||||
dish: 'cobbler',
|
||||
} );
|
||||
};
|
||||
|
||||
it( 'should update report urls', () => {
|
||||
const item = { href: REPORT_URL };
|
||||
updateLinkHref( item, nextQuery, timeExcludedScreens );
|
||||
const encodedPath = encodeURIComponent( '/analytics/orders' );
|
||||
|
||||
expect( item.href ).toBe(
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin#/analytics/orders?fruit=apple&dish=cobbler'
|
||||
`admin.php?page=wc-admin&path=${ encodedPath }&fruit=apple&dish=cobbler`
|
||||
);
|
||||
} );
|
||||
|
||||
|
@ -38,18 +33,15 @@ describe( 'updateLinkHref', () => {
|
|||
const item = { href: DASHBOARD_URL };
|
||||
updateLinkHref( item, nextQuery, timeExcludedScreens );
|
||||
|
||||
expect( item.href ).toBe(
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin#/?fruit=apple&dish=cobbler'
|
||||
);
|
||||
expect( item.href ).toBe( 'admin.php?page=wc-admin&fruit=apple&dish=cobbler' );
|
||||
} );
|
||||
|
||||
it( 'should update dashboard urls with no hash', () => {
|
||||
const item = { href: DASHBOARD_URL_NO_HASH };
|
||||
it( 'should not add the nextQuery to a time excluded screen', () => {
|
||||
const item = { href: REPORT_URL_TIME_EXCLUDED };
|
||||
updateLinkHref( item, nextQuery, timeExcludedScreens );
|
||||
const encodedPath = encodeURIComponent( '/analytics/settings' );
|
||||
|
||||
expect( item.href ).toBe(
|
||||
'http://example.com/wp-admin/admin.php?page=wc-admin#/?fruit=apple&dish=cobbler'
|
||||
);
|
||||
expect( item.href ).toBe( `admin.php?page=wc-admin&path=${ encodedPath }` );
|
||||
} );
|
||||
|
||||
it( 'should not update WooCommerce urls', () => {
|
||||
|
|
|
@ -37,8 +37,8 @@ function wc_admin_number_format( $number ) {
|
|||
function wc_admin_url( $path, $query = array() ) {
|
||||
if ( ! empty( $query ) ) {
|
||||
$query_string = http_build_query( $query );
|
||||
$path = $path . '?' . $query_string;
|
||||
$path = $path . '&' . $query_string;
|
||||
}
|
||||
|
||||
return admin_url( 'admin.php?page=wc-admin#' . $path, dirname( __FILE__ ) );
|
||||
return admin_url( 'admin.php?page=wc-admin&path=' . $path, dirname( __FILE__ ) );
|
||||
}
|
||||
|
|
|
@ -102,22 +102,16 @@ class WC_Admin_Page_Controller {
|
|||
$current_url = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
|
||||
}
|
||||
|
||||
$current_path = wp_parse_url( $current_url, PHP_URL_PATH );
|
||||
$current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
|
||||
$current_fragment = wp_parse_url( $current_url, PHP_URL_FRAGMENT );
|
||||
$current_query = wp_parse_url( $current_url, PHP_URL_QUERY );
|
||||
parse_str( $current_query, $current_pieces );
|
||||
$current_path = empty( $current_pieces['page'] ) ? '' : $current_pieces['page'];
|
||||
$current_path .= empty( $current_pieces['path'] ) ? '' : '&path=' . $current_pieces['path'];
|
||||
|
||||
foreach ( $this->pages as $page ) {
|
||||
if ( isset( $page['js_page'] ) && $page['js_page'] ) {
|
||||
// Check registered admin pages.
|
||||
$full_page_path = add_query_arg( 'page', $page['path'], admin_url( 'admin.php' ) );
|
||||
$page_path = wp_parse_url( $full_page_path, PHP_URL_PATH );
|
||||
$page_query = wp_parse_url( $full_page_path, PHP_URL_QUERY );
|
||||
$page_fragment = wp_parse_url( $full_page_path, PHP_URL_FRAGMENT );
|
||||
|
||||
if (
|
||||
$page_path === $current_path &&
|
||||
0 === strpos( $current_query, $page_query ) &&
|
||||
$page_fragment === $current_fragment
|
||||
$page['path'] === $current_path
|
||||
) {
|
||||
$this->current_page = $page;
|
||||
return;
|
||||
|
@ -408,7 +402,7 @@ class WC_Admin_Page_Controller {
|
|||
$options = wp_parse_args( $options, $defaults );
|
||||
|
||||
if ( 0 !== strpos( $options['path'], self::PAGE_ROOT ) ) {
|
||||
$options['path'] = self::PAGE_ROOT . '#' . $options['path'];
|
||||
$options['path'] = self::PAGE_ROOT . '&path=' . $options['path'];
|
||||
}
|
||||
|
||||
if ( is_null( $options['parent'] ) ) {
|
||||
|
|
|
@ -3,40 +3,47 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* WooCommerce dependencies
|
||||
*/
|
||||
import { getAdminLink } from '@woocommerce/navigation';
|
||||
import { getAdminLink, getHistory } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Use `Link` to create a link to another resource. It accepts a type to automatically
|
||||
* create wp-admin links, wc-admin links, and external links.
|
||||
*/
|
||||
class Link extends Component {
|
||||
// @todo Investigate further if we can use <Link /> directly.
|
||||
// With React Router 5+, <RouterLink /> cannot be used outside of the main <Router /> elements,
|
||||
// which seems to include components imported from @woocommerce/components. For now, we can use the history object directly.
|
||||
wcAdminLinkHandler( e ) {
|
||||
e.preventDefault();
|
||||
getHistory().push( e.target.closest( 'a' ).getAttribute( 'href' ) );
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, href, type, ...props } = this.props;
|
||||
if ( this.context.router && 'wc-admin' === type ) {
|
||||
return (
|
||||
<RouterLink to={ href } { ...props }>
|
||||
{ children }
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
|
||||
let path;
|
||||
if ( 'wp-admin' === type ) {
|
||||
path = getAdminLink( href );
|
||||
} else if ( 'external' === type ) {
|
||||
path = href;
|
||||
} else {
|
||||
path = getAdminLink( 'admin.php?page=wc-admin#' + href );
|
||||
path = href;
|
||||
}
|
||||
|
||||
const passProps = {
|
||||
...props,
|
||||
'data-link-type': type,
|
||||
};
|
||||
|
||||
if ( 'wc-admin' === type ) {
|
||||
passProps.onClick = this.wcAdminLinkHandler;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={ path } { ...props }>
|
||||
<a href={ path } { ...passProps }>
|
||||
{ children }
|
||||
</a>
|
||||
);
|
||||
|
|
|
@ -2,15 +2,60 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createHashHistory } from 'history';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { parse } from 'qs';
|
||||
|
||||
// See https://github.com/ReactTraining/react-router/blob/master/FAQ.md#how-do-i-access-the-history-object-outside-of-components
|
||||
|
||||
let _history;
|
||||
|
||||
/**
|
||||
* Recreate `history` to coerce React Router into accepting path arguments found in query
|
||||
* parameter `path`, allowing a url hash to be avoided. Since hash portions of the url are
|
||||
* not sent server side, full route information can be detected by the server.
|
||||
*
|
||||
* `<Router />` and `<Switch />` components use `history.location()` to match a url with a route.
|
||||
* Since they don't parse query arguments, recreate `get location` to return a `pathname` with the
|
||||
* query path argument's value.
|
||||
*
|
||||
* @returns {object} React-router history object with `get location` modified.
|
||||
*/
|
||||
function getHistory() {
|
||||
if ( ! _history ) {
|
||||
_history = createHashHistory();
|
||||
const path = document.location.pathname;
|
||||
const browserHistory = createBrowserHistory( {
|
||||
basename: path.substring( 0, path.lastIndexOf( '/' ) ),
|
||||
} );
|
||||
_history = {
|
||||
get length() {
|
||||
return browserHistory.length;
|
||||
},
|
||||
get action() {
|
||||
return browserHistory.action;
|
||||
},
|
||||
get location() {
|
||||
const { location } = browserHistory;
|
||||
const query = parse( location.search.substring( 1 ) );
|
||||
const pathname = query.path || '/';
|
||||
|
||||
return {
|
||||
...location,
|
||||
pathname,
|
||||
};
|
||||
},
|
||||
createHref: ( ...args ) => browserHistory.createHref.apply( browserHistory, args ),
|
||||
push: ( ...args ) => browserHistory.push.apply( browserHistory, args ),
|
||||
replace: ( ...args ) => browserHistory.replace.apply( browserHistory, args ),
|
||||
go: ( ...args ) => browserHistory.go.apply( browserHistory, args ),
|
||||
goBack: ( ...args ) => browserHistory.goBack.apply( browserHistory, args ),
|
||||
goForward: ( ...args ) => browserHistory.goForward.apply( browserHistory, args ),
|
||||
block: ( ...args ) => browserHistory.block.apply( browserHistory, args ),
|
||||
listen: function( listener ) {
|
||||
return browserHistory.listen( () => {
|
||||
listener( this.location, this.action );
|
||||
} );
|
||||
},
|
||||
};
|
||||
}
|
||||
return _history;
|
||||
}
|
||||
|
|
|
@ -99,8 +99,8 @@ export function getSearchWords( query = navUtils.getQuery() ) {
|
|||
* @return {String} Updated URL merging query params into existing params.
|
||||
*/
|
||||
export function getNewPath( query, path = getPath(), currentQuery = getQuery() ) {
|
||||
const queryString = stringifyQuery( { ...currentQuery, ...query } );
|
||||
return `${ path }${ queryString }`;
|
||||
const queryString = stringifyQuery( { page: 'wc-admin', path, ...currentQuery, ...query } );
|
||||
return `admin.php${ queryString }`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -141,7 +141,7 @@ class WC_Tests_API_Leaderboards extends WC_REST_Unit_Test_Case {
|
|||
$widgets_leaderboard = end( $data );
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'top_widgets', $widgets_leaderboard['id'] );
|
||||
$this->assertEquals( admin_url( 'admin.php?page=wc-admin#test/path?persisted_param=1' ), $widgets_leaderboard['rows'][0]['display'] );
|
||||
$this->assertEquals( admin_url( 'admin.php?page=wc-admin&path=test/path&persisted_param=1' ), $widgets_leaderboard['rows'][0]['display'] );
|
||||
|
||||
$request = new WP_REST_Request( 'GET', $this->endpoint . '/allowed' );
|
||||
$response = $this->server->dispatch( $request );
|
||||
|
|
Loading…
Reference in New Issue