Skip to content

Improve post Site Scan action items #6698

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

Merged
merged 16 commits into from
Nov 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions assets/src/common/helpers/is-external-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Check if the provided URL is external.
*
* @param {string} url URL to be checked.
* @return {boolean} True if the URL is external, false otherwise.
*/
export const isExternalUrl = ( url ) => global?.location?.host !== new URL( url ).host;
11 changes: 11 additions & 0 deletions assets/src/common/helpers/test/is-external-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Internal dependencies
*/
import { isExternalUrl } from '../is-external-url';

describe( 'isExternalUrl', () => {
it( 'should return true if of a URL is external and false otherwise', () => {
expect( isExternalUrl( 'https://example.com/' ) ).toBe( true );
expect( isExternalUrl( 'https://localhost/' ) ).toBe( false );
} );
} );
56 changes: 54 additions & 2 deletions assets/src/components/options-context-provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { createContext, useEffect, useState, useRef, useCallback, useContext } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';

/**
* External dependencies
Expand Down Expand Up @@ -38,7 +39,9 @@ function waitASecond() {
*/
export function OptionsContextProvider( { children, optionsRestPath, populateDefaultValues, hasErrorBoundary = false, delaySave = false } ) {
const [ updates, setUpdates ] = useState( {} );
const [ fetchingPluginSuppression, setFetchingPluginSuppression ] = useState( false );
const [ fetchingOptions, setFetchingOptions ] = useState( null );
const [ savedOptions, setSavedOptions ] = useState( {} );
const [ savingOptions, setSavingOptions ] = useState( false );
const [ didSaveOptions, setDidSaveOptions ] = useState( false );
const [ originalOptions, setOriginalOptions ] = useState( {} );
Expand All @@ -55,6 +58,52 @@ export function OptionsContextProvider( { children, optionsRestPath, populateDef
hasUnmounted.current = true;
}, [] );

/**
* Fetches plugin suppression data.
*/
const refetchPluginSuppression = useCallback( () => {
if ( error || fetchingPluginSuppression ) {
return;
}

/**
* Fetches suppression data from the REST endpoint.
*/
( async () => {
setFetchingPluginSuppression( true );

try {
const fetchedPluginSuppression = await apiFetch( {
path: addQueryArgs( optionsRestPath, {
_fields: [ 'suppressed_plugins', 'suppressible_plugins' ],
} ),
} );
Comment on lines +76 to +80
Copy link
Member

Choose a reason for hiding this comment

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

❤️


if ( true === hasUnmounted.current ) {
return;
}

setOriginalOptions( {
...originalOptions,
...fetchedPluginSuppression,
} );
Comment on lines +86 to +89
Copy link
Member

Choose a reason for hiding this comment

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

This is great. I was thinking that this could have the effect of overriding a user's unsaved changes to plugin suppression, but this won't be the case the original options are updated, not the modified otpions.

} catch ( e ) {
if ( true === hasUnmounted.current ) {
return;
}

setError( e );

if ( hasErrorBoundary ) {
setAsyncError( e );
}
return;
}

setFetchingPluginSuppression( false );
} )();
}, [ error, fetchingPluginSuppression, hasErrorBoundary, optionsRestPath, originalOptions, setAsyncError, setError ] );

/**
* Fetches options.
*/
Expand Down Expand Up @@ -129,7 +178,7 @@ export function OptionsContextProvider( { children, optionsRestPath, populateDef

// Ensure this promise lasts at least a second so that the "Saving Options" load screen is
// visible long enough for the user to see it is happening.
const [ savedOptions ] = await Promise.all(
const [ retrievedOptions ] = await Promise.all(
[
apiFetch(
{
Expand All @@ -146,7 +195,7 @@ export function OptionsContextProvider( { children, optionsRestPath, populateDef
return;
}

setOriginalOptions( savedOptions );
setOriginalOptions( retrievedOptions );
setError( null );
} catch ( e ) {
if ( true === hasUnmounted.current ) {
Expand All @@ -164,6 +213,7 @@ export function OptionsContextProvider( { children, optionsRestPath, populateDef
}

setModifiedOptions( { ...modifiedOptions, ...updates } );
setSavedOptions( updates );

setUpdates( {} );
setDidSaveOptions( true );
Expand Down Expand Up @@ -198,10 +248,12 @@ export function OptionsContextProvider( { children, optionsRestPath, populateDef
updates,
originalOptions,
saveOptions,
savedOptions,
savingOptions,
unsetOption,
updateOptions,
readerModeWasOverridden,
refetchPluginSuppression,
setReaderModeWasOverridden,
modifiedOptions,
}
Expand Down
48 changes: 38 additions & 10 deletions assets/src/components/site-scan-context-provider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
} from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
import { usePrevious } from '@wordpress/compose';

/**
* External dependencies
Expand All @@ -40,13 +39,15 @@ export const ACTION_SCAN_INITIALIZE = 'ACTION_SCAN_INITIALIZE';
export const ACTION_SCAN_URL = 'ACTION_SCAN_URL';
export const ACTION_SCAN_RECEIVE_RESULTS = 'ACTION_SCAN_RECEIVE_RESULTS';
export const ACTION_SCAN_COMPLETE = 'ACTION_SCAN_COMPLETE';
export const ACTION_SCAN_SUCCESS = 'ACTION_SCAN_SUCCESS';
export const ACTION_SCAN_CANCEL = 'ACTION_SCAN_CANCEL';

/**
* Site Scan Statuses.
*/
export const STATUS_REQUEST_SCANNABLE_URLS = 'STATUS_REQUEST_SCANNABLE_URLS';
export const STATUS_FETCHING_SCANNABLE_URLS = 'STATUS_FETCHING_SCANNABLE_URLS';
export const STATUS_REFETCHING_PLUGIN_SUPPRESSION = 'STATUS_REFETCHING_PLUGIN_SUPPRESSION';
export const STATUS_READY = 'STATUS_READY';
export const STATUS_IDLE = 'STATUS_IDLE';
export const STATUS_IN_PROGRESS = 'STATUS_IN_PROGRESS';
Expand Down Expand Up @@ -164,7 +165,13 @@ export function siteScanReducer( state, action ) {

return {
...state,
status: hasFailed ? STATUS_FAILED : STATUS_COMPLETED,
status: hasFailed ? STATUS_FAILED : STATUS_REFETCHING_PLUGIN_SUPPRESSION,
};
}
case ACTION_SCAN_SUCCESS: {
return {
...state,
status: STATUS_COMPLETED,
};
}
case ACTION_SCAN_CANCEL: {
Expand Down Expand Up @@ -201,10 +208,11 @@ export function SiteScanContextProvider( {
validateNonce,
} ) {
const {
didSaveOptions,
originalOptions: {
theme_support: themeSupport,
},
savedOptions,
refetchPluginSuppression,
} = useContext( Options );
const { setAsyncError } = useAsyncError();
const [ state, dispatch ] = useReducer( siteScanReducer, INITIAL_STATE );
Expand Down Expand Up @@ -283,13 +291,22 @@ export function SiteScanContextProvider( {
* Whenever options change, cancel the current scan (if in progress) and
* refetch the scannable URLs.
*/
const previousDidSaveOptions = usePrevious( didSaveOptions );
useEffect( () => {
if ( ! previousDidSaveOptions && didSaveOptions ) {
cancelSiteScan();
fetchScannableUrls();
if ( Object.keys( savedOptions ).length > 0 ) {
dispatch( { type: ACTION_SCAN_CANCEL } );
dispatch( { type: ACTION_SCANNABLE_URLS_REQUEST } );
}
}, [ savedOptions ] );

/**
* Trigger site scan if the suppressed plugins list has changed and the
* scanner is ready to start a scan.
*/
useEffect( () => {
if ( status === STATUS_READY && Object.keys( savedOptions.suppressed_plugins || {} ).length > 0 ) {
dispatch( { type: ACTION_SCAN_INITIALIZE } );
}
}, [ cancelSiteScan, didSaveOptions, fetchScannableUrls, previousDidSaveOptions ] );
}, [ savedOptions?.suppressed_plugins, status ] );

/**
* Delay concurrent validation requests.
Expand All @@ -310,6 +327,17 @@ export function SiteScanContextProvider( {
return clearTimeout;
}, [ shouldDelayValidationRequest ] );

/**
* Once the site scan is complete, refetch the plugin suppression data so
* that the suppressed table is updated with the latest validation errors.
*/
useEffect( () => {
if ( status === STATUS_REFETCHING_PLUGIN_SUPPRESSION ) {
refetchPluginSuppression();
dispatch( { type: ACTION_SCAN_SUCCESS } );
}
}, [ refetchPluginSuppression, status ] );

/**
* Fetch scannable URLs from the REST endpoint.
*/
Expand Down Expand Up @@ -370,7 +398,7 @@ export function SiteScanContextProvider( {

setShouldDelayValidationRequest( true );

const currentlyScannedUrlIndex = urlIndexesPendingScan.shift();
const currentlyScannedUrlIndex = urlIndexesPendingScan[ 0 ];

dispatch( {
type: ACTION_SCAN_URL,
Expand Down Expand Up @@ -428,7 +456,7 @@ export function SiteScanContextProvider( {
hasSiteScanResults,
isBusy: [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( status ),
isCancelled: status === STATUS_CANCELLED,
isCompleted: status === STATUS_COMPLETED,
isCompleted: [ STATUS_REFETCHING_PLUGIN_SUPPRESSION, STATUS_COMPLETED ].includes( status ),
isFailed: status === STATUS_FAILED,
isFetchingScannableUrls: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ),
isReady: status === STATUS_READY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
ACTION_SCAN_URL,
ACTION_SCAN_RECEIVE_RESULTS,
ACTION_SCAN_COMPLETE,
ACTION_SCAN_SUCCESS,
ACTION_SCAN_CANCEL,
STATUS_REQUEST_SCANNABLE_URLS,
STATUS_FETCHING_SCANNABLE_URLS,
STATUS_REFETCHING_PLUGIN_SUPPRESSION,
STATUS_READY,
STATUS_IDLE,
STATUS_IN_PROGRESS,
Expand Down Expand Up @@ -269,7 +271,7 @@ describe( 'siteScanReducer', () => {
}, {
type: ACTION_SCAN_COMPLETE,
} ) ).toStrictEqual( {
status: STATUS_COMPLETED,
status: STATUS_REFETCHING_PLUGIN_SUPPRESSION,
scannableUrls: [
{ error: false },
{ error: true },
Expand All @@ -292,6 +294,17 @@ describe( 'siteScanReducer', () => {
} );
} );

/**
* ACTION_SCAN_SUCCESS
*/
it( 'returns previous state for ACTION_SCAN_SUCCESS', () => {
expect( siteScanReducer( { status }, {
type: ACTION_SCAN_SUCCESS,
} ) ).toStrictEqual( {
status: STATUS_COMPLETED,
} );
} );

/**
* ACTION_SCAN_CANCEL
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,38 @@
*/
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { AMP_COMPATIBLE_PLUGINS_URL } from 'amp-settings'; // From WP inline script.

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import { createInterpolateElement, useMemo } from '@wordpress/element';
Copy link
Member

Choose a reason for hiding this comment

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

Fancy.

import { ExternalLink } from '@wordpress/components';

/**
* Internal dependencies
*/
import { useNormalizedPluginsData } from '../plugins-context-provider/use-normalized-plugins-data';
import { IconLaptopPlug } from '../svg/laptop-plug';
import { isExternalUrl } from '../../common/helpers/is-external-url';
import { SiteScanSourcesList } from './site-scan-sources-list';
import { SiteScanResults } from './index';

/**
* Render a list of plugins that cause AMP Incompatibility.
*
* @param {Object} props Component props.
* @param {string} props.className Component class name.
* @param {string[]} props.slugs List of plugins slugs.
* @param {Object} props Component props.
* @param {string} props.className Component class name.
* @param {boolean} props.showHelpText Show additional help text above the issues list.
* @param {string[]} props.slugs List of plugins slugs.
*/
export function PluginsWithAmpIncompatibility( { className, slugs = [], ...props } ) {
export function PluginsWithAmpIncompatibility( {
className,
showHelpText = false,
slugs = [],
...props
} ) {
const pluginsData = useNormalizedPluginsData();
const sources = useMemo( () => slugs?.map( ( slug ) => pluginsData?.[ slug ] ?? {
slug,
Expand All @@ -40,6 +49,26 @@ export function PluginsWithAmpIncompatibility( { className, slugs = [], ...props
className={ classnames( 'site-scan-results--plugins', className ) }
{ ...props }
>
{ showHelpText && (
<p>
{ createInterpolateElement(
__( 'Because of plugin issue(s) detected, you may want to <a>review and suppress plugins</a>.', 'amp' ),
{
// eslint-disable-next-line jsx-a11y/anchor-has-content -- Anchor has content defined in the translated string.
a: <a href="#plugin-suppression" />,
},
) }
{ AMP_COMPATIBLE_PLUGINS_URL ? createInterpolateElement(
` ${ __( 'You may also want to browse alternative <a>AMP compatible plugins</a>.', 'amp' ) }`,
{
a: isExternalUrl( AMP_COMPATIBLE_PLUGINS_URL )
? <ExternalLink href={ AMP_COMPATIBLE_PLUGINS_URL } />
// eslint-disable-next-line jsx-a11y/anchor-has-content -- Anchor has content defined in the translated string.
: <a href={ AMP_COMPATIBLE_PLUGINS_URL } />,
},
) : '' }
</p>
) }
<SiteScanSourcesList
sources={ sources }
inactiveSourceNotice={ __( 'This plugin has been deactivated since last site scan.' ) }
Expand All @@ -51,5 +80,6 @@ export function PluginsWithAmpIncompatibility( { className, slugs = [], ...props

PluginsWithAmpIncompatibility.propTypes = {
className: PropTypes.string,
showHelpText: PropTypes.bool,
slugs: PropTypes.arrayOf( PropTypes.string ).isRequired,
};
Loading