From 1e6878f70ab459ce35b91154de5b34463756a0da Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 15 Sep 2021 15:32:57 +0200 Subject: [PATCH 001/120] Add Site Scan step bare bones to Onboarding Wizard --- .../components/svg/landscape-hills-cogs.js | 28 +++++++++++ assets/src/onboarding-wizard/pages/index.js | 6 +++ .../pages/site-scan/index.js | 46 +++++++++++++++++++ .../pages/site-scan/style.scss | 14 ++++++ 4 files changed, 94 insertions(+) create mode 100644 assets/src/components/svg/landscape-hills-cogs.js create mode 100644 assets/src/onboarding-wizard/pages/site-scan/index.js create mode 100644 assets/src/onboarding-wizard/pages/site-scan/style.scss diff --git a/assets/src/components/svg/landscape-hills-cogs.js b/assets/src/components/svg/landscape-hills-cogs.js new file mode 100644 index 00000000000..9c4da7d7051 --- /dev/null +++ b/assets/src/components/svg/landscape-hills-cogs.js @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +const INTRINSIC_ICON_WIDTH = 144; +const INTRINSIC_ICON_HEIGHT = 69; +const INTRINSIC_STROKE_WIDTH = 2; + +export function IconLandscapeHillsCogs( { width = INTRINSIC_ICON_WIDTH, ...props } ) { + const strokeWidth = INTRINSIC_STROKE_WIDTH * ( INTRINSIC_ICON_WIDTH / width ); + + return ( + + + + + + + + + + + ); +} +IconLandscapeHillsCogs.propTypes = { + width: PropTypes.number, +}; diff --git a/assets/src/onboarding-wizard/pages/index.js b/assets/src/onboarding-wizard/pages/index.js index 8df208b24ab..888ed663f02 100644 --- a/assets/src/onboarding-wizard/pages/index.js +++ b/assets/src/onboarding-wizard/pages/index.js @@ -7,6 +7,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import { TechnicalBackground } from './technical-background'; +import { SiteScan } from './site-scan'; import { TemplateMode } from './template-mode'; import { ChooseReaderTheme } from './choose-reader-theme'; import { Done } from './done'; @@ -28,6 +29,11 @@ export const PAGES = [ PageComponent: TechnicalBackground, showTitle: false, }, + { + slug: 'site-scan', + title: __( 'Site Scan', 'amp' ), + PageComponent: SiteScan, + }, { slug: 'template-modes', title: __( 'Template Modes', 'amp' ), diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js new file mode 100644 index 00000000000..5ff67a40865 --- /dev/null +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useContext, useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { Navigation } from '../../components/navigation-context-provider'; +import { Selectable } from '../../../components/selectable'; +import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; + +/** + * Screen for visualizing a site scan. + */ +export function SiteScan() { + const { setCanGoForward } = useContext( Navigation ); + + /** + * Allow moving forward. + */ + useEffect( () => { + // @todo: Allow moving forward once the scan is complete. + if ( true ) { + setCanGoForward( true ); + } + }, [ setCanGoForward ] ); + + return ( +
+ +
+ +

+ { __( 'Please wait a minute ...', 'amp' ) } +

+
+

+ { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

+
+
+ ); +} diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss new file mode 100644 index 00000000000..f8512e5644b --- /dev/null +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -0,0 +1,14 @@ +.site-scan__header { + align-items: center; + border-bottom: 1px solid var(--amp-settings-color-border); + display: flex; + flex-flow: row nowrap; + padding-bottom: 1rem; + padding-top: 0.5rem; +} + +.site-scan__heading { + font-size: 16px; + font-weight: 700; + margin-left: 2rem; +} From 79af03db5933f037dbf83c79ef465e1da7b5c39d Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 15 Sep 2021 16:20:44 +0200 Subject: [PATCH 002/120] Add `ProgressBar` component --- assets/src/components/progress-bar/index.js | 41 ++++++++++ assets/src/components/progress-bar/style.scss | 33 ++++++++ .../progress-bar/test/progress-bar.js | 76 +++++++++++++++++++ assets/src/css/variables.css | 1 + 4 files changed, 151 insertions(+) create mode 100644 assets/src/components/progress-bar/index.js create mode 100644 assets/src/components/progress-bar/style.scss create mode 100644 assets/src/components/progress-bar/test/progress-bar.js diff --git a/assets/src/components/progress-bar/index.js b/assets/src/components/progress-bar/index.js new file mode 100644 index 00000000000..2bc0b542f3e --- /dev/null +++ b/assets/src/components/progress-bar/index.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import './style.scss'; + +/** + * Progress bar component. + * + * @param {number} value The value of the progress bar indicator (between 0 and 100). + */ +export function ProgressBar( { value } ) { + const style = { + transform: `translateX(${ Math.max( value, 3 ) - 100 }%)`, + }; + + return ( +
+
+
+
+
+ ); +} + +ProgressBar.propTypes = { + value: PropTypes.number.isRequired, +}; diff --git a/assets/src/components/progress-bar/style.scss b/assets/src/components/progress-bar/style.scss new file mode 100644 index 00000000000..cd4a651ae4c --- /dev/null +++ b/assets/src/components/progress-bar/style.scss @@ -0,0 +1,33 @@ +:root { + --amp-progress-bar-color: var(--amp-brand); + --amp-progress-bar-height: 34px; +} + +.progress-bar { + border: 2px solid var(--amp-progress-bar-color); + border-radius: var(--amp-progress-bar-height); + height: var(--amp-progress-bar-height); + margin-bottom: 1.5rem; + margin-top: 1.5rem; + overflow: hidden; +} + +.progress-bar__track { + border-radius: var(--amp-progress-bar-height); + height: calc(100% - 8px); + margin: 4px; + overflow: hidden; + position: relative; + width: calc(100% - 8px); +} + +.progress-bar__indicator { + background-color: var(--amp-progress-bar-color); + border-radius: var(--amp-progress-bar-height); + height: 100%; + left: 0; + position: absolute; + top: 0; + transition: transform 240ms ease; + width: 100%; +} diff --git a/assets/src/components/progress-bar/test/progress-bar.js b/assets/src/components/progress-bar/test/progress-bar.js new file mode 100644 index 00000000000..04e03bafe15 --- /dev/null +++ b/assets/src/components/progress-bar/test/progress-bar.js @@ -0,0 +1,76 @@ + +/** + * External dependencies + */ +import { act } from 'react-dom/test-utils'; +import { create } from 'react-test-renderer'; + +/** + * WordPress dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ProgressBar } from '../index'; + +let container; + +describe( 'ProgressBar', () => { + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); + } ); + + afterEach( () => { + document.body.removeChild( container ); + container = null; + } ); + + it( 'matches the snapshot', () => { + const wrapper = create( ); + + expect( wrapper.toJSON() ).toMatchSnapshot(); + } ); + + it( 'renders a progress bar', () => { + act( () => { + render( + , + container, + ); + } ); + + expect( container.querySelector( '.progress-bar[role="progressbar"]' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar[aria-valuemin="0"]' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar[aria-valuemax="100"]' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar[aria-valuenow="33"]' ) ).not.toBeNull(); + } ); + + it( 'the bar is shifted correctly', () => { + act( () => { + render( + , + container, + ); + } ); + + expect( container.querySelector( '.progress-bar[aria-valuenow="75"]' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar__indicator' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar__indicator' ).style.transform ).toBe( 'translateX(-25%)' ); + } ); + + it( 'does not allow the bar to be completely out of view for low values', () => { + act( () => { + render( + , + container, + ); + } ); + + expect( container.querySelector( '.progress-bar[aria-valuenow="1"]' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar__indicator' ) ).not.toBeNull(); + expect( container.querySelector( '.progress-bar__indicator' ).style.transform ).toBe( 'translateX(-97%)' ); + } ); +} ); diff --git a/assets/src/css/variables.css b/assets/src/css/variables.css index 3c906a1e4dd..47176ddd40e 100644 --- a/assets/src/css/variables.css +++ b/assets/src/css/variables.css @@ -4,6 +4,7 @@ * @todo Make the naming of these variables more semantic. */ :root { + --amp-brand: #2459e7; --amp-settings-color-black: #212121; --amp-settings-color-dark-gray: #333; --amp-settings-color-brand: #2459e7; From 4bd7a32bcab582663ece3c2cbcaefefc1e51a1e7 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 16 Sep 2021 11:38:36 +0200 Subject: [PATCH 003/120] Move `SiteScanContextProvider` to common components folder --- .../site-scan-context-provider/index.js} | 0 assets/src/onboarding-wizard/index.js | 2 +- assets/src/onboarding-wizard/pages/template-mode/index.js | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename assets/src/{onboarding-wizard/components/site-scan-context-provider.js => components/site-scan-context-provider/index.js} (100%) diff --git a/assets/src/onboarding-wizard/components/site-scan-context-provider.js b/assets/src/components/site-scan-context-provider/index.js similarity index 100% rename from assets/src/onboarding-wizard/components/site-scan-context-provider.js rename to assets/src/components/site-scan-context-provider/index.js diff --git a/assets/src/onboarding-wizard/index.js b/assets/src/onboarding-wizard/index.js index 4571a468e65..eccf162aab7 100644 --- a/assets/src/onboarding-wizard/index.js +++ b/assets/src/onboarding-wizard/index.js @@ -33,11 +33,11 @@ import { ReaderThemesContextProvider } from '../components/reader-themes-context import { ErrorBoundary } from '../components/error-boundary'; import { ErrorContextProvider } from '../components/error-context-provider'; import { ErrorScreen } from '../components/error-screen'; +import { SiteScanContextProvider } from '../components/site-scan-context-provider'; import { UserContextProvider } from '../components/user-context-provider'; import { PAGES } from './pages'; import { SetupWizard } from './setup-wizard'; import { NavigationContextProvider } from './components/navigation-context-provider'; -import { SiteScanContextProvider } from './components/site-scan-context-provider'; import { TemplateModeOverrideContextProvider } from './components/template-mode-override-context-provider'; const { ajaxurl: wpAjaxUrl } = global; diff --git a/assets/src/onboarding-wizard/pages/template-mode/index.js b/assets/src/onboarding-wizard/pages/template-mode/index.js index 87143108857..a1fdffa7454 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/index.js +++ b/assets/src/onboarding-wizard/pages/template-mode/index.js @@ -6,8 +6,8 @@ import { useEffect, useContext } from '@wordpress/element'; * Internal dependencies */ import { Navigation } from '../../components/navigation-context-provider'; -import { SiteScan } from '../../components/site-scan-context-provider'; import { ReaderThemes } from '../../../components/reader-themes-context-provider'; +import { SiteScan } from '../../../components/site-scan-context-provider'; import { User } from '../../../components/user-context-provider'; import { Options } from '../../../components/options-context-provider'; import { Loading } from '../../../components/loading'; From df37573084d709fa4fba93592d3eddbe37578840 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 16 Sep 2021 13:42:39 +0200 Subject: [PATCH 004/120] Add `amp/v1/scannable-urls` REST endpoint --- .phpstorm.meta.php | 1 + .../site-scan-context-provider/index.js | 62 +++++++++- assets/src/onboarding-wizard/index.js | 5 +- src/Admin/OnboardingWizardSubmenuPage.php | 2 + src/AmpWpPlugin.php | 1 + .../ScannableURLsRestController.php | 107 ++++++++++++++++++ 6 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 src/Validation/ScannableURLsRestController.php diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index f9d561ad97f..ce6a4e617ba 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -50,6 +50,7 @@ 'reader_theme_loader' => \AmpProject\AmpWP\ReaderThemeLoader::class, 'reader_theme_support_features' => \AmpProject\AmpWP\ReaderThemeSupportFeatures::class, 'rest.options_controller' => \AmpProject\AmpWP\OptionsRESTController::class, + 'rest.scannable_urls_controller' => \AmpProject\AmpWP\Validation\ScannableURLsRestController::class, 'rest.validation_counts_controller' => \AmpProject\AmpWP\Validation\ValidationCountsRestController::class, 'sandboxing' => \AmpProject\AmpWP\Sandboxing::class, 'save_post_validation_event' => \AmpProject\AmpWP\Validation\SavePostValidationEvent::class, diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index cf14b71a52f..6552b8f500c 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,26 +1,75 @@ /** * WordPress dependencies */ -import { createContext, useEffect, useState } from '@wordpress/element'; +import { createContext, useEffect, useRef, useState } from '@wordpress/element'; +import { getQueryArg } from '@wordpress/url'; +import apiFetch from '@wordpress/api-fetch'; /** * External dependencies */ import PropTypes from 'prop-types'; -import { getQueryArg } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { useAsyncError } from '../../utils/use-async-error'; export const SiteScan = createContext(); /** * Context provider for site scanning. * - * @param {Object} props Component props. - * @param {?any} props.children Component children. + * @param {Object} props Component props. + * @param {?any} props.children Component children. + * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. */ -export function SiteScanContextProvider( { children } ) { +export function SiteScanContextProvider( { + children, + scannableUrlsRestPath, +} ) { const [ themeIssues, setThemeIssues ] = useState( null ); const [ pluginIssues, setPluginIssues ] = useState( null ); const [ scanningSite, setScanningSite ] = useState( true ); + const [ fetchingScannableUrls, setFetchingScannableUrls ] = useState( false ); + const [ scannableUrls, setScannableUrls ] = useState( [] ); + const { setAsyncError } = useAsyncError(); + + // This component sets state inside async functions. Use this ref to prevent state updates after unmount. + const hasUnmounted = useRef( false ); + useEffect( () => () => { + hasUnmounted.current = true; + }, [] ); + + useEffect( () => { + if ( fetchingScannableUrls || scannableUrls.length > 0 ) { + return; + } + + /** + * Fetches scannable URLs from the REST endpoint. + */ + ( async () => { + setFetchingScannableUrls( true ); + + try { + const fetchedScannableUrls = await apiFetch( { + path: scannableUrlsRestPath, + } ); + + if ( true === hasUnmounted.current ) { + return; + } + + setScannableUrls( fetchedScannableUrls ); + } catch ( e ) { + setAsyncError( e ); + return; + } + + setFetchingScannableUrls( false ); + } )(); + }, [ fetchingScannableUrls, scannableUrlsRestPath, scannableUrls.length, setAsyncError ] ); /** * @todo Note: The following effects will be updated for version 2.1 when site scan is implemented in the wizard. For now, @@ -51,8 +100,10 @@ export function SiteScanContextProvider( { children } ) { - + { children } diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index bbb4a105a09..cf0f961d4f7 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -247,6 +247,7 @@ public function enqueue_assets( $hook_suffix ) { 'url' => $theme->get( 'ThemeURI' ), ], 'USING_FALLBACK_READER_THEME' => $this->reader_themes->using_fallback_theme(), + 'SCANNABLE_URLS_REST_PATH' => '/amp/v1/scannable-urls', 'SETTINGS_LINK' => $amp_settings_link, 'OPTIONS_REST_PATH' => '/amp/v1/options', 'PREVIEW_URLS' => $this->get_preview_urls( $this->scannable_url_provider->get_urls() ), @@ -288,6 +289,7 @@ protected function add_preload_rest_paths() { $paths = [ '/amp/v1/options', '/amp/v1/reader-themes', + '/amp/v1/scannable-urls', '/wp/v2/settings', '/wp/v2/users/me', ]; diff --git a/src/AmpWpPlugin.php b/src/AmpWpPlugin.php index 4ca2da42317..7deb307216d 100644 --- a/src/AmpWpPlugin.php +++ b/src/AmpWpPlugin.php @@ -113,6 +113,7 @@ final class AmpWpPlugin extends ServiceBasedPlugin { 'reader_theme_loader' => ReaderThemeLoader::class, 'reader_theme_support_features' => ReaderThemeSupportFeatures::class, 'rest.options_controller' => OptionsRESTController::class, + 'rest.scannable_urls_controller' => Validation\ScannableURLsRestController::class, 'rest.validation_counts_controller' => Validation\ValidationCountsRestController::class, 'sandboxing' => Sandboxing::class, 'save_post_validation_event' => SavePostValidationEvent::class, diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php new file mode 100644 index 00000000000..daa6644dbeb --- /dev/null +++ b/src/Validation/ScannableURLsRestController.php @@ -0,0 +1,107 @@ +namespace = 'amp/v1'; + $this->rest_base = 'scannable-urls'; + $this->scannable_url_provider = $scannable_url_provider; + } + + /** + * Registers all routes for the controller. + */ + public function register() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => WP_REST_Server::READABLE, + 'callback' => [ $this, 'get_items' ], + 'args' => [], + 'permission_callback' => '__return_true', + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); + } + + /** + * Retrieves total unreviewed count for validation URLs and errors. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return rest_ensure_response( $this->scannable_url_provider->get_urls() ); + } + + /** + * Retrieves the block type' schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + return [ + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'amp-wp-' . $this->rest_base, + 'type' => 'object', + 'properties' => [ + 'url' => [ + 'description' => __( 'Scannable URL.' ), + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + 'context' => [ 'view' ], + ], + 'type' => [ + 'description' => __( 'Type of scannable URL.' ), + 'type' => 'string', + 'readonly' => true, + 'context' => [ 'view' ], + ], + ], + ]; + } +} From b84e7b068acb5146106b9db5ad8b4630198a3646 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 16 Sep 2021 15:33:01 +0200 Subject: [PATCH 005/120] Refactor validation endpoint to allow validating by URL --- src/Validation/URLValidationProvider.php | 5 +- .../URLValidationRESTController.php | 91 +++++++++++++++++-- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/Validation/URLValidationProvider.php b/src/Validation/URLValidationProvider.php index 949723a83eb..394a610ff40 100644 --- a/src/Validation/URLValidationProvider.php +++ b/src/Validation/URLValidationProvider.php @@ -109,7 +109,10 @@ public function get_url_validation( $url, $type ) { return $validity; } - $this->update_state_from_validity( $validity, $type ); + if ( ! empty( $type ) ) { + $this->update_state_from_validity( $validity, $type ); + } + return $validity; } diff --git a/src/Validation/URLValidationRESTController.php b/src/Validation/URLValidationRESTController.php index 945b198393a..63ed57c8b7d 100644 --- a/src/Validation/URLValidationRESTController.php +++ b/src/Validation/URLValidationRESTController.php @@ -104,7 +104,27 @@ public function register() { ] ); - // @todo Additional endpoint to validate a URL (from a URL rather than a post ID). + register_rest_route( + $this->namespace, + '/validate-url', + [ + 'args' => [ + 'url' => [ + 'description' => __( 'URL for the page to validate.', 'amp' ), + 'required' => true, + 'type' => 'string', + 'validate_callback' => [ $this, 'validate_url_param' ], + ], + ], + [ + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => [ $this, 'validate_url' ], + 'permission_callback' => [ $this, 'create_item_permissions_check' ], + ], + 'schema' => [ $this, 'get_public_item_schema' ], + ] + ); } /** @@ -143,6 +163,32 @@ public function validate_post_id_param( $id, $request, $param ) { return true; } + /** + * Validate URL param. + * + * @param string $url URL param value. + * @param WP_REST_Request $request REST request. + * @param string $param Param name ('url'). + * @return true|WP_Error True on valid, WP_Error otherwise. + */ + public function validate_url_param( $url, $request, $param ) { + // First enforce the schema to ensure $url is a string. + $validity = rest_validate_request_arg( $url, $request, $param ); + if ( is_wp_error( $validity ) ) { + return $validity; + } + + if ( esc_url_raw( $url ) !== $url || ! wp_http_validate_url( $url ) ) { + return new WP_Error( + 'rest_invalid_url', + __( 'Invalid URL.', 'amp' ), + [ 'status' => 404 ] + ); + } + + return true; + } + /** * Checks whether the current user can view AMP validation results. * @@ -207,16 +253,45 @@ public function validate_post_url( $request ) { ); } - $validity = $this->url_validation_provider->get_url_validation( $url, get_post_type( $post_id ) ); + $post_type = get_post_type( $post_id ); + $data = $this->get_validation_data( $url, $post_type ); + + return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); + } + + /** + * Validate page by URL. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function validate_url( $request ) { + $data = $this->get_validation_data( $request['url'] ); + + return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); + } + + /** + * Get validation data for a URL. + * + * @param string $url URL for which the validation data should be retrieved. + * @param bool|string $post_type (optional) Type of post that is served at the `$url`. + * + * @return array|WP_Error + */ + protected function get_validation_data( $url, $post_type = false ) { + $validity = $this->url_validation_provider->get_url_validation( $url, $post_type ); + if ( is_wp_error( $validity ) ) { return $validity; } - $data = [ - 'results' => [], - 'review_link' => get_edit_post_link( $validity['post_id'], 'raw' ), - 'support_link' => admin_url( 'admin.php?page=amp-support&post_id=' . $validity['post_id'] ), - ]; + $data['results'] = []; + + if ( ! empty( $post_id ) ) { + $data['review_link'] = get_edit_post_link( $validity['post_id'], 'raw' ); + $data['support_link'] = admin_url( 'admin.php?page=amp-support&post_id=' . $validity['post_id'] ); + } foreach ( AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( $validity['post_id'] ) as $result ) { @@ -238,7 +313,7 @@ public function validate_post_url( $request ) { ]; } - return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); + return $data; } /** From 241e1b8f4b336209d8bb0e613cf9855efc754a86 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 20 Sep 2021 11:03:24 +0200 Subject: [PATCH 006/120] Apply suggestions from code review Co-authored-by: Weston Ruter --- .../ScannableURLsRestController.php | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index daa6644dbeb..3e5abd18b83 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -60,7 +60,9 @@ public function register() { 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_items' ], 'args' => [], - 'permission_callback' => '__return_true', + 'permission_callback' => static function () { + return current_user_can( \AMP_Validation_Manager::VALIDATE_CAPABILITY ); + }, ], 'schema' => [ $this, 'get_public_item_schema' ], ] @@ -74,7 +76,20 @@ public function register() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - return rest_ensure_response( $this->scannable_url_provider->get_urls() ); + $nonce = AMP_Validation_Manager::get_amp_validate_nonce(); + return rest_ensure_response( + array_map( + static function ( $entry ) use ( $nonce ) { + $entry['validate_url'] = add_query_arg( + [ + AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, + ], + $entry['validate_url'] + ); + }, + $this->scannable_url_provider->get_urls() + ) + ); } /** From 5bd05750fd8ce21f847d13c84b13c77afc5f27fc Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 20 Sep 2021 11:26:52 +0200 Subject: [PATCH 007/120] Revert "Refactor validation endpoint to allow validating by URL" This reverts commit 8cfa8bef6d026173114d42d1432b5fcb53277ffd. --- src/Validation/URLValidationProvider.php | 5 +- .../URLValidationRESTController.php | 91 ++----------------- 2 files changed, 9 insertions(+), 87 deletions(-) diff --git a/src/Validation/URLValidationProvider.php b/src/Validation/URLValidationProvider.php index 394a610ff40..949723a83eb 100644 --- a/src/Validation/URLValidationProvider.php +++ b/src/Validation/URLValidationProvider.php @@ -109,10 +109,7 @@ public function get_url_validation( $url, $type ) { return $validity; } - if ( ! empty( $type ) ) { - $this->update_state_from_validity( $validity, $type ); - } - + $this->update_state_from_validity( $validity, $type ); return $validity; } diff --git a/src/Validation/URLValidationRESTController.php b/src/Validation/URLValidationRESTController.php index 63ed57c8b7d..945b198393a 100644 --- a/src/Validation/URLValidationRESTController.php +++ b/src/Validation/URLValidationRESTController.php @@ -104,27 +104,7 @@ public function register() { ] ); - register_rest_route( - $this->namespace, - '/validate-url', - [ - 'args' => [ - 'url' => [ - 'description' => __( 'URL for the page to validate.', 'amp' ), - 'required' => true, - 'type' => 'string', - 'validate_callback' => [ $this, 'validate_url_param' ], - ], - ], - [ - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => [ $this, 'validate_url' ], - 'permission_callback' => [ $this, 'create_item_permissions_check' ], - ], - 'schema' => [ $this, 'get_public_item_schema' ], - ] - ); + // @todo Additional endpoint to validate a URL (from a URL rather than a post ID). } /** @@ -163,32 +143,6 @@ public function validate_post_id_param( $id, $request, $param ) { return true; } - /** - * Validate URL param. - * - * @param string $url URL param value. - * @param WP_REST_Request $request REST request. - * @param string $param Param name ('url'). - * @return true|WP_Error True on valid, WP_Error otherwise. - */ - public function validate_url_param( $url, $request, $param ) { - // First enforce the schema to ensure $url is a string. - $validity = rest_validate_request_arg( $url, $request, $param ); - if ( is_wp_error( $validity ) ) { - return $validity; - } - - if ( esc_url_raw( $url ) !== $url || ! wp_http_validate_url( $url ) ) { - return new WP_Error( - 'rest_invalid_url', - __( 'Invalid URL.', 'amp' ), - [ 'status' => 404 ] - ); - } - - return true; - } - /** * Checks whether the current user can view AMP validation results. * @@ -253,45 +207,16 @@ public function validate_post_url( $request ) { ); } - $post_type = get_post_type( $post_id ); - $data = $this->get_validation_data( $url, $post_type ); - - return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); - } - - /** - * Validate page by URL. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function validate_url( $request ) { - $data = $this->get_validation_data( $request['url'] ); - - return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); - } - - /** - * Get validation data for a URL. - * - * @param string $url URL for which the validation data should be retrieved. - * @param bool|string $post_type (optional) Type of post that is served at the `$url`. - * - * @return array|WP_Error - */ - protected function get_validation_data( $url, $post_type = false ) { - $validity = $this->url_validation_provider->get_url_validation( $url, $post_type ); - + $validity = $this->url_validation_provider->get_url_validation( $url, get_post_type( $post_id ) ); if ( is_wp_error( $validity ) ) { return $validity; } - $data['results'] = []; - - if ( ! empty( $post_id ) ) { - $data['review_link'] = get_edit_post_link( $validity['post_id'], 'raw' ); - $data['support_link'] = admin_url( 'admin.php?page=amp-support&post_id=' . $validity['post_id'] ); - } + $data = [ + 'results' => [], + 'review_link' => get_edit_post_link( $validity['post_id'], 'raw' ), + 'support_link' => admin_url( 'admin.php?page=amp-support&post_id=' . $validity['post_id'] ), + ]; foreach ( AMP_Validated_URL_Post_Type::get_invalid_url_validation_errors( $validity['post_id'] ) as $result ) { @@ -313,7 +238,7 @@ protected function get_validation_data( $url, $post_type = false ) { ]; } - return $data; + return rest_ensure_response( $this->filter_response_by_context( $data, $request['context'] ) ); } /** From 97dfb1fa90f8fc27761bd30df1012ac592e8b987 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 20 Sep 2021 13:25:10 +0200 Subject: [PATCH 008/120] Fix issues and update docs --- .../ScannableURLsRestController.php | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index 3e5abd18b83..01d1c87b7e1 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -8,6 +8,7 @@ namespace AmpProject\AmpWP\Validation; +use AMP_Validation_Manager; use WP_Error; use WP_REST_Controller; use AmpProject\AmpWP\Infrastructure\Delayed; @@ -61,7 +62,7 @@ public function register() { 'callback' => [ $this, 'get_items' ], 'args' => [], 'permission_callback' => static function () { - return current_user_can( \AMP_Validation_Manager::VALIDATE_CAPABILITY ); + return current_user_can( \AMP_Validation_Manager::VALIDATE_CAPABILITY ); }, ], 'schema' => [ $this, 'get_public_item_schema' ], @@ -70,13 +71,18 @@ public function register() { } /** - * Retrieves total unreviewed count for validation URLs and errors. + * Retrieves a list of scannable URLs. + * + * Each item contains a page `type` (e.g. 'home' or 'search') and a + * `validate_url` prop for accessing validation data for a given URL. * * @param WP_REST_Request $request Full details about the request. + * * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable $nonce = AMP_Validation_Manager::get_amp_validate_nonce(); + return rest_ensure_response( array_map( static function ( $entry ) use ( $nonce ) { @@ -84,8 +90,10 @@ static function ( $entry ) use ( $nonce ) { [ AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, ], - $entry['validate_url'] + $entry['url'] ); + + return $entry; }, $this->scannable_url_provider->get_urls() ) @@ -103,19 +111,26 @@ public function get_item_schema() { 'title' => 'amp-wp-' . $this->rest_base, 'type' => 'object', 'properties' => [ - 'url' => [ - 'description' => __( 'Scannable URL.' ), + 'url' => [ + 'description' => __( 'Page URL.' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => [ 'view' ], ], - 'type' => [ - 'description' => __( 'Type of scannable URL.' ), + 'type' => [ + 'description' => __( 'Page type.' ), 'type' => 'string', 'readonly' => true, 'context' => [ 'view' ], ], + 'validate_url' => [ + 'description' => __( 'URL for accessing validation data for a given page.' ), + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + 'context' => [ 'view' ], + ], ], ]; } From 8815b9ada45b5ef1b801cb1e553be585bd4487ed Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 20 Sep 2021 15:11:01 +0200 Subject: [PATCH 009/120] Ensure scannable URLs are AMP endpoints --- src/Validation/ScannableURLsRestController.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index 01d1c87b7e1..0d1a6db8314 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -86,11 +86,13 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna return rest_ensure_response( array_map( static function ( $entry ) use ( $nonce ) { - $entry['validate_url'] = add_query_arg( - [ - AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, - ], - $entry['url'] + $entry['validate_url'] = amp_add_paired_endpoint( + add_query_arg( + [ + AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, + ], + $entry['url'] + ) ); return $entry; From 900503f22b60e681ea16dc44264bf75f0a22c0de Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 20 Sep 2021 17:59:14 +0200 Subject: [PATCH 010/120] Scan pages sequentially and visualize current scan state --- .../site-scan-context-provider/index.js | 81 ++++++++++++------- .../pages/site-scan/complete.js | 31 +++++++ .../pages/site-scan/in-progress.js | 53 ++++++++++++ .../pages/site-scan/index.js | 45 ++++++----- 4 files changed, 162 insertions(+), 48 deletions(-) create mode 100644 assets/src/onboarding-wizard/pages/site-scan/complete.js create mode 100644 assets/src/onboarding-wizard/pages/site-scan/in-progress.js diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 6552b8f500c..fbe5247cbab 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { createContext, useEffect, useRef, useState } from '@wordpress/element'; -import { getQueryArg } from '@wordpress/url'; import apiFetch from '@wordpress/api-fetch'; /** @@ -17,6 +16,12 @@ import { useAsyncError } from '../../utils/use-async-error'; export const SiteScan = createContext(); +const SITE_SCAN_STATE_INITIALIZING = 'SITE_SCAN_STATE_INITIALIZING'; +const SITE_SCAN_STATE_READY = 'SITE_SCAN_STATE_READY'; +const SITE_SCAN_STATE_IDLE = 'SITE_SCAN_STATE_IDLE'; +const SITE_SCAN_STATE_IN_PROGRESS = 'SITE_SCAN_STATE_IN_PROGRESS'; +const SITE_SCAN_STATE_COMPLETE = 'SITE_SCAN_STATE_COMPLETE'; + /** * Context provider for site scanning. * @@ -28,10 +33,12 @@ export function SiteScanContextProvider( { children, scannableUrlsRestPath, } ) { - const [ themeIssues, setThemeIssues ] = useState( null ); - const [ pluginIssues, setPluginIssues ] = useState( null ); - const [ scanningSite, setScanningSite ] = useState( true ); + const [ themeIssues, setThemeIssues ] = useState( [] ); + const [ pluginIssues, setPluginIssues ] = useState( [] ); + const [ siteScanState, setSiteScanState ] = useState( SITE_SCAN_STATE_INITIALIZING ); + const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 0 ); const [ fetchingScannableUrls, setFetchingScannableUrls ] = useState( false ); + const [ fetchedScannableUrls, setFetchedScannableUrls ] = useState( false ); const [ scannableUrls, setScannableUrls ] = useState( [] ); const { setAsyncError } = useAsyncError(); @@ -42,7 +49,7 @@ export function SiteScanContextProvider( { }, [] ); useEffect( () => { - if ( fetchingScannableUrls || scannableUrls.length > 0 ) { + if ( fetchingScannableUrls || fetchedScannableUrls ) { return; } @@ -53,7 +60,7 @@ export function SiteScanContextProvider( { setFetchingScannableUrls( true ); try { - const fetchedScannableUrls = await apiFetch( { + const response = await apiFetch( { path: scannableUrlsRestPath, } ); @@ -61,7 +68,9 @@ export function SiteScanContextProvider( { return; } - setScannableUrls( fetchedScannableUrls ); + setScannableUrls( response ); + setSiteScanState( SITE_SCAN_STATE_READY ); + setFetchedScannableUrls( true ); } catch ( e ) { setAsyncError( e ); return; @@ -69,41 +78,59 @@ export function SiteScanContextProvider( { setFetchingScannableUrls( false ); } )(); - }, [ fetchingScannableUrls, scannableUrlsRestPath, scannableUrls.length, setAsyncError ] ); + }, [ fetchedScannableUrls, fetchingScannableUrls, scannableUrlsRestPath, setAsyncError ] ); /** - * @todo Note: The following effects will be updated for version 2.1 when site scan is implemented in the wizard. For now, - * we will keep themeIssues and pluginIssues set to null, emulating an unsuccessful site scan. The wizard will then make - * a mode recommendation based only on how the user has answered the technical question. + * Scan site URLs sequentially. */ useEffect( () => { - if ( ! scanningSite && ! themeIssues ) { - setThemeIssues( getQueryArg( global.location.href, 'amp-theme-issues' ) ? [ 'Theme issue 1' ] : null ); // URL param is for testing. + if ( siteScanState !== SITE_SCAN_STATE_IDLE ) { + return; } - }, [ scanningSite, themeIssues ] ); - // See note above. - useEffect( () => { - if ( ! scanningSite && ! pluginIssues ) { - setPluginIssues( getQueryArg( global.location.href, 'amp-plugin-issues' ) ? [ 'Plugin issue 1' ] : null ); // URL param is for testing. - } - }, [ scanningSite, pluginIssues ] ); + /** + * Validates the next URL in the queue. + */ + ( async () => { + setSiteScanState( SITE_SCAN_STATE_IN_PROGRESS ); - // See note above. - useEffect( () => { - if ( true === scanningSite ) { - setScanningSite( false ); - } - }, [ scanningSite ] ); + try { + const { validate_url: validateUrl } = scannableUrls[ currentlyScannedUrlIndex ]; + const validationResults = await apiFetch( { url: validateUrl } ); + + if ( true === hasUnmounted.current ) { + return; + } + + setPluginIssues( ( issues ) => [ ...issues, validationResults ] ); + } catch ( e ) { + setAsyncError( e ); + return; + } + + /** + * Finish the scan if there are no more URLs to validate. + */ + if ( currentlyScannedUrlIndex === scannableUrls.length - 1 ) { + setSiteScanState( SITE_SCAN_STATE_COMPLETE ); + } else { + setCurrentlyScannedUrlIndex( ( index ) => index + 1 ); + setSiteScanState( SITE_SCAN_STATE_IDLE ); + } + } )(); + }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, siteScanState ] ); return ( setSiteScanState( SITE_SCAN_STATE_IDLE ), themeIssues, } } diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js new file mode 100644 index 00000000000..1baa5843c3f --- /dev/null +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -0,0 +1,31 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { Selectable } from '../../../components/selectable'; +import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; + +/** + * Screen with site scan summary. + */ +export function SiteScanComplete() { + return ( +
+ +
+ +

+ { __( 'Scan complete', 'amp' ) } +

+
+

+ { __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) } +

+
+
+ ); +} diff --git a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js new file mode 100644 index 00000000000..0cf3527348f --- /dev/null +++ b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { SiteScan as SiteScanContext } from '../../../components/site-scan-context-provider'; +import { Selectable } from '../../../components/selectable'; +import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; +import { ProgressBar } from '../../../components/progress-bar'; + +/** + * Screen for visualizing a site scan progress state. + */ +export function SiteScanInProgress() { + const { + currentlyScannedUrlIndex, + scannableUrls, + siteScanComplete, + } = useContext( SiteScanContext ); + + return ( +
+ +
+ +

+ { __( 'Please wait a minute…', 'amp' ) } +

+
+

+ { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

+ +

+ { sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.type, + ) } +

+
+
+ ); +} diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index 5ff67a40865..d5a69aab18f 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { useContext, useEffect } from '@wordpress/element'; /** @@ -9,38 +8,42 @@ import { useContext, useEffect } from '@wordpress/element'; */ import './style.scss'; import { Navigation } from '../../components/navigation-context-provider'; -import { Selectable } from '../../../components/selectable'; -import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; +import { SiteScan as SiteScanContext } from '../../../components/site-scan-context-provider'; +import { SiteScanComplete } from './complete'; +import { SiteScanInProgress } from './in-progress'; /** * Screen for visualizing a site scan. */ export function SiteScan() { const { setCanGoForward } = useContext( Navigation ); + const { + canScanSite, + startSiteScan, + siteScanComplete, + } = useContext( SiteScanContext ); + + /** + * Start site scan. + */ + useEffect( () => { + if ( canScanSite ) { + startSiteScan(); + } + }, [ canScanSite, startSiteScan ] ); /** * Allow moving forward. */ useEffect( () => { - // @todo: Allow moving forward once the scan is complete. - if ( true ) { + if ( siteScanComplete ) { setCanGoForward( true ); } - }, [ setCanGoForward ] ); + }, [ setCanGoForward, siteScanComplete ] ); + + if ( siteScanComplete ) { + return ; + } - return ( -
- -
- -

- { __( 'Please wait a minute ...', 'amp' ) } -

-
-

- { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } -

-
-
- ); + return ; } From 947ba1825cf212c980f4530c241b38b75d723ec3 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 22 Sep 2021 14:57:04 +0200 Subject: [PATCH 011/120] Improve scan progress animation --- assets/src/components/progress-bar/index.js | 1 + assets/src/components/progress-bar/style.scss | 2 +- .../pages/site-scan/in-progress.js | 17 ++++++++------ .../pages/site-scan/index.js | 23 +++++++++++++++++-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/assets/src/components/progress-bar/index.js b/assets/src/components/progress-bar/index.js index 2bc0b542f3e..2a71b039da3 100644 --- a/assets/src/components/progress-bar/index.js +++ b/assets/src/components/progress-bar/index.js @@ -16,6 +16,7 @@ import './style.scss'; export function ProgressBar( { value } ) { const style = { transform: `translateX(${ Math.max( value, 3 ) - 100 }%)`, + transitionDuration: `${ value < 100 ? 800 : 200 }ms`, }; return ( diff --git a/assets/src/components/progress-bar/style.scss b/assets/src/components/progress-bar/style.scss index cd4a651ae4c..74b425ef39a 100644 --- a/assets/src/components/progress-bar/style.scss +++ b/assets/src/components/progress-bar/style.scss @@ -28,6 +28,6 @@ left: 0; position: absolute; top: 0; - transition: transform 240ms ease; + transition: transform 800ms ease-out; width: 100%; } diff --git a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js index 0cf3527348f..32a9fdeb3d0 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js +++ b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js @@ -39,13 +39,16 @@ export function SiteScanInProgress() { : ( currentlyScannedUrlIndex / scannableUrls.length * 100 ) } />

- { sprintf( - // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), - currentlyScannedUrlIndex + 1, - scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.type, - ) } + { siteScanComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.type, + ) + }

diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index d5a69aab18f..754e7c62648 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useContext, useEffect } from '@wordpress/element'; +import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -22,6 +22,7 @@ export function SiteScan() { startSiteScan, siteScanComplete, } = useContext( SiteScanContext ); + const [ canShowScanSummary, setCanShowScanSummary ] = useState( false ); /** * Start site scan. @@ -32,6 +33,24 @@ export function SiteScan() { } }, [ canScanSite, startSiteScan ] ); + /** + * Show scan summary with a delay so that the progress bar has a chance to + * complete. + */ + useEffect( () => { + let delay; + + if ( siteScanComplete ) { + delay = setTimeout( () => setCanShowScanSummary( true ), 500 ); + } + + return () => { + if ( delay ) { + clearTimeout( delay ); + } + }; + }, [ siteScanComplete ] ); + /** * Allow moving forward. */ @@ -41,7 +60,7 @@ export function SiteScan() { } }, [ setCanGoForward, siteScanComplete ] ); - if ( siteScanComplete ) { + if ( canShowScanSummary ) { return ; } From 29c2e28b567841cd6dc8410a1bd7c0b424d0d662 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 22 Sep 2021 15:07:44 +0200 Subject: [PATCH 012/120] Add checked page labels --- .../pages/site-scan/in-progress.js | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js index 32a9fdeb3d0..04605c3cdfa 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js +++ b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js @@ -12,6 +12,40 @@ import { Selectable } from '../../../components/selectable'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { ProgressBar } from '../../../components/progress-bar'; +/** + * Gets the currently checked page type label. + * + * @param {string} type The page type slug. + */ +function getPageTypeLabel( type ) { + switch ( type ) { + case 'home': + return __( 'Checking homepage', 'amp' ); + + case 'post': + return __( 'Checking sample post', 'amp' ); + + case 'page': + return __( 'Checking sample page', 'amp' ); + + case 'author': + return __( 'Checking author archive', 'amp' ); + + case 'date': + return __( 'Checking date archive', 'amp' ); + + case 'search': + return __( 'Checking search results page', 'amp' ); + + default: + return sprintf( + // translators: %s is a page type label. + __( 'Checking %s', 'amp' ), + type, + ); + } +} + /** * Screen for visualizing a site scan progress state. */ @@ -43,10 +77,10 @@ export function SiteScanInProgress() { ? __( 'Scan complete', 'amp' ) : sprintf( // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + __( 'Scanning %1$d/%2$d URLs: %3$s…', 'amp' ), currentlyScannedUrlIndex + 1, scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.type, + getPageTypeLabel( scannableUrls[ currentlyScannedUrlIndex ]?.type ), ) }

From 577ccac69681239d284018616cc2c4cf37e3ac35 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 23 Sep 2021 14:19:57 +0200 Subject: [PATCH 013/120] Retrieve theme and plugin names that cause AMP validation errors --- .../get-site-issues.js | 31 ++++++++ .../site-scan-context-provider/index.js | 8 +- .../test/get-site-issues.js | 73 +++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 assets/src/components/site-scan-context-provider/get-site-issues.js create mode 100644 assets/src/components/site-scan-context-provider/test/get-site-issues.js diff --git a/assets/src/components/site-scan-context-provider/get-site-issues.js b/assets/src/components/site-scan-context-provider/get-site-issues.js new file mode 100644 index 00000000000..0040cc4e799 --- /dev/null +++ b/assets/src/components/site-scan-context-provider/get-site-issues.js @@ -0,0 +1,31 @@ +/** + * Retrieve plugin and theme issues from the validation results. + * + * @param {Array} validationResults + * @return {Object} An object consisting of arrays with plugin and theme issues. + */ +export function getSiteIssues( validationResults = [] ) { + const pluginIssues = new Set(); + const themeIssues = new Set(); + + for ( const result of validationResults ) { + const { error } = result; + + if ( ! error?.sources ) { + continue; + } + + for ( const source of error.sources ) { + if ( source.type === 'plugin' && source.name !== 'amp' ) { + pluginIssues.add( source.name ); + } else if ( source.type === 'theme' ) { + themeIssues.add( source.name ); + } + } + } + + return { + pluginIssues: [ ...pluginIssues ], + themeIssues: [ ...themeIssues ], + }; +} diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index fbe5247cbab..c788aa0c6ab 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -13,6 +13,7 @@ import PropTypes from 'prop-types'; * Internal dependencies */ import { useAsyncError } from '../../utils/use-async-error'; +import { getSiteIssues } from './get-site-issues'; export const SiteScan = createContext(); @@ -102,7 +103,12 @@ export function SiteScanContextProvider( { return; } - setPluginIssues( ( issues ) => [ ...issues, validationResults ] ); + if ( validationResults.results.length > 0 ) { + const siteIssues = getSiteIssues( validationResults.results ); + + setPluginIssues( ( issues ) => [ ...new Set( [ ...issues, ...siteIssues.pluginIssues ] ) ] ); + setThemeIssues( ( issues ) => [ ...new Set( [ ...issues, ...siteIssues.themeIssues ] ) ] ); + } } catch ( e ) { setAsyncError( e ); return; diff --git a/assets/src/components/site-scan-context-provider/test/get-site-issues.js b/assets/src/components/site-scan-context-provider/test/get-site-issues.js new file mode 100644 index 00000000000..f56c8fc7cc0 --- /dev/null +++ b/assets/src/components/site-scan-context-provider/test/get-site-issues.js @@ -0,0 +1,73 @@ +/** + * Internal dependencies + */ +import { getSiteIssues } from '../get-site-issues'; + +describe( 'getSiteIssues', () => { + it( 'returns empty arrays if no validation results are passed', () => { + expect( getSiteIssues() ).toMatchObject( { pluginIssues: [], themeIssues: [] } ); + } ); + + it( 'returns plugin and theme issues', () => { + const validationResult = [ + { + error: { + sources: [ + { type: 'core', name: 'wp-includes' }, + { type: 'plugin', name: 'amp' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'plugin', name: 'gutenberg' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'core', name: 'wp-includes' }, + { type: 'plugin', name: 'jetpack' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'plugin', name: 'jetpack' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'theme', name: 'twentytwenty' }, + { type: 'core', name: 'wp-includes' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'core', name: 'wp-includes' }, + ], + }, + }, + { + error: { + sources: [ + { type: 'theme', name: 'twentytwenty' }, + { type: 'core', name: 'wp-includes' }, + ], + }, + }, + ]; + + const issues = getSiteIssues( validationResult ); + + expect( issues.pluginIssues ).toStrictEqual( expect.arrayContaining( [ 'gutenberg', 'jetpack' ] ) ); + expect( issues.themeIssues ).toStrictEqual( expect.arrayContaining( [ 'twentytwenty' ] ) ); + } ); +} ); From 9bc44c000a665d6b5d1cb8c2ecf70b5b8e2acf67 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 23 Sep 2021 14:50:16 +0200 Subject: [PATCH 014/120] Update look and feel of scan complete Onboarding Wizard step --- assets/src/components/svg/laptop-plug.js | 25 ++++++++ .../src/components/svg/website-paint-brush.js | 27 ++++++++ assets/src/css/variables.css | 5 +- .../pages/site-scan/complete.js | 62 ++++++++++++++++++- .../pages/site-scan/style.scss | 42 +++++++++++++ 5 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 assets/src/components/svg/laptop-plug.js create mode 100644 assets/src/components/svg/website-paint-brush.js diff --git a/assets/src/components/svg/laptop-plug.js b/assets/src/components/svg/laptop-plug.js new file mode 100644 index 00000000000..730590a2e52 --- /dev/null +++ b/assets/src/components/svg/laptop-plug.js @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +const INTRINSIC_ICON_WIDTH = 58; +const INTRINSIC_ICON_HEIGHT = 40; +const INTRINSIC_STROKE_WIDTH = 2; + +export function IconLaptopPlug( { width = INTRINSIC_ICON_WIDTH, ...props } ) { + const strokeWidth = INTRINSIC_STROKE_WIDTH * ( INTRINSIC_ICON_WIDTH / width ); + + return ( + + + + + + + + ); +} +IconLaptopPlug.propTypes = { + width: PropTypes.number, +}; diff --git a/assets/src/components/svg/website-paint-brush.js b/assets/src/components/svg/website-paint-brush.js new file mode 100644 index 00000000000..2f41ff2fbbc --- /dev/null +++ b/assets/src/components/svg/website-paint-brush.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +const INTRINSIC_ICON_WIDTH = 58; +const INTRINSIC_ICON_HEIGHT = 38; +const INTRINSIC_STROKE_WIDTH = 2; + +export function IconWebsitePaintBrush( { width = INTRINSIC_ICON_WIDTH, ...props } ) { + const strokeWidth = INTRINSIC_STROKE_WIDTH * ( INTRINSIC_ICON_WIDTH / width ); + + return ( + + + + + + + + + + ); +} +IconWebsitePaintBrush.propTypes = { + width: PropTypes.number, +}; diff --git a/assets/src/css/variables.css b/assets/src/css/variables.css index 47176ddd40e..ae62aeefe60 100644 --- a/assets/src/css/variables.css +++ b/assets/src/css/variables.css @@ -4,14 +4,15 @@ * @todo Make the naming of these variables more semantic. */ :root { + --gray: #6c7781; + --light-gray: #c4c4c4; + --very-light-gray: #fafafc; --amp-brand: #2459e7; --amp-settings-color-black: #212121; --amp-settings-color-dark-gray: #333; --amp-settings-color-brand: #2459e7; --amp-settings-color-muted: #48525c; - --gray: #6c7781; --amp-settings-color-border: #e8e8e8; - --very-light-gray: #fafafc; --amp-settings-color-background: #fff; --amp-settings-color-warning: #ff9f00; --font-noto: "Noto Sans", sans-serif; diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 1baa5843c3f..477323c0b7f 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -2,30 +2,86 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useContext } from '@wordpress/element'; +import { VisuallyHidden } from '@wordpress/components'; /** * Internal dependencies */ import { Selectable } from '../../../components/selectable'; +import { SiteScan } from '../../../components/site-scan-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; +import { IconWebsitePaintBrush } from '../../../components/svg/website-paint-brush'; +import { IconLaptopPlug } from '../../../components/svg/laptop-plug'; /** * Screen with site scan summary. */ export function SiteScanComplete() { + const { + pluginIssues, + themeIssues, + } = useContext( SiteScan ); + return (
- +
-

+

{ __( 'Scan complete', 'amp' ) } -

+

{ __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) }

+ { themeIssues.length > 0 && ( + +
+ +

+ { __( 'Themes with AMP incompatibilty', 'amp' ) } + + { `(${ themeIssues.length })` } + +

+
+
+ { themeIssues.map( ( issue ) => ( +
+ { issue } +
+ ) ) } +
+
+ ) } + { pluginIssues.length > 0 && ( + +
+ +

+ { __( 'Plugins with AMP incompatibility', 'amp' ) } + + { `(${ pluginIssues.length })` } + +

+
+
+ { pluginIssues.map( ( issue ) => ( +
+ { issue } +
+ ) ) } +
+
+ ) }
); } diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss index f8512e5644b..5a9e81a0fe3 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/style.scss +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -1,3 +1,7 @@ +.site-scan__section + .site-scan__section { + margin-top: 1.5rem; +} + .site-scan__header { align-items: center; border-bottom: 1px solid var(--amp-settings-color-border); @@ -11,4 +15,42 @@ font-size: 16px; font-weight: 700; margin-left: 2rem; + + &[data-badge-content]::after { + align-items: center; + background-color: var(--light-gray); + border-radius: 50%; + content: attr(data-badge-content); + display: inline-flex; + height: 30px; + justify-content: center; + letter-spacing: -0.05em; + margin: 0 0.5rem; + width: 30px; + } } + +.site-scan__section--compact { + padding: 0; + + .site-scan__header { + padding: 0.5rem; + + @media (min-width: 783px) { + padding: 1rem 2rem; + } + } + + .site-scan__heading { + margin-left: 1rem; + } + + .site-scan__content { + padding: 1rem 0.5rem; + + @media (min-width: 783px) { + padding: 1.25rem 2rem; + } + } +} + From a88316ae83704b008c34cdc9b4a8fefa4ff26b43 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 23 Sep 2021 14:50:32 +0200 Subject: [PATCH 015/120] Add missing snapshot --- .../test/__snapshots__/progress-bar.js.snap | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 assets/src/components/progress-bar/test/__snapshots__/progress-bar.js.snap diff --git a/assets/src/components/progress-bar/test/__snapshots__/progress-bar.js.snap b/assets/src/components/progress-bar/test/__snapshots__/progress-bar.js.snap new file mode 100644 index 00000000000..33dd0801414 --- /dev/null +++ b/assets/src/components/progress-bar/test/__snapshots__/progress-bar.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProgressBar matches the snapshot 1`] = ` +
+
+
+
+
+`; From f8343804df98f1ad31cee369299f6698a8430bd1 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 23 Sep 2021 15:36:19 +0200 Subject: [PATCH 016/120] Add `SourcesList` sub-component --- assets/src/css/variables.css | 1 + .../pages/site-scan/complete.js | 13 ++--- .../pages/site-scan/sources-list.js | 50 +++++++++++++++++++ .../pages/site-scan/style.scss | 36 +++++++++++++ 4 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 assets/src/onboarding-wizard/pages/site-scan/sources-list.js diff --git a/assets/src/css/variables.css b/assets/src/css/variables.css index ae62aeefe60..1f0867b9881 100644 --- a/assets/src/css/variables.css +++ b/assets/src/css/variables.css @@ -14,6 +14,7 @@ --amp-settings-color-muted: #48525c; --amp-settings-color-border: #e8e8e8; --amp-settings-color-background: #fff; + --amp-settings-color-background-light: #f8f8f8; --amp-settings-color-warning: #ff9f00; --font-noto: "Noto Sans", sans-serif; --font-poppins: poppins, sans-serif; diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 477323c0b7f..9e55f61ed18 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -13,6 +13,7 @@ import { SiteScan } from '../../../components/site-scan-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { IconWebsitePaintBrush } from '../../../components/svg/website-paint-brush'; import { IconLaptopPlug } from '../../../components/svg/laptop-plug'; +import { SourcesList } from './sources-list'; /** * Screen with site scan summary. @@ -51,11 +52,7 @@ export function SiteScanComplete() {

- { themeIssues.map( ( issue ) => ( -
- { issue } -
- ) ) } +
) } @@ -74,11 +71,7 @@ export function SiteScanComplete() {

- { pluginIssues.map( ( issue ) => ( -
- { issue } -
- ) ) } +
) } diff --git a/assets/src/onboarding-wizard/pages/site-scan/sources-list.js b/assets/src/onboarding-wizard/pages/site-scan/sources-list.js new file mode 100644 index 00000000000..12ecf47b9e5 --- /dev/null +++ b/assets/src/onboarding-wizard/pages/site-scan/sources-list.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Sources list component. + * + * @param {Object} props Component props. + * @param {Array} props.issues List of sources causing issues. + */ +export function SourcesList( { issues } ) { + return ( +
    + { issues.map( ( slug ) => ( +
  • + + { slug } + + + { sprintf( + // translators: %s is an author name. + __( 'by %s', 'amp' ), + 'Foo Bar', + ) } + + + { sprintf( + // translators: %s is a version number. + __( 'Version %s', 'amp' ), + '1.0', + ) } + +
  • + ) ) } +
+ ); +} + +SourcesList.propTypes = { + issues: PropTypes.array.isRequired, +}; diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss index 5a9e81a0fe3..13b3d59b4ca 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/style.scss +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -54,3 +54,39 @@ } } +.site-scan__sources { + border: 2px solid var(--amp-settings-color-border); +} +.site-scan__source { + align-items: center; + display: flex; + flex-flow: row nowrap; + font-size: 14px; + margin: 0; + padding: 1rem; + + &:nth-child(even) { + background-color: var(--amp-settings-color-background-light); + } + + & + & { + border-top: 2px solid var(--amp-settings-color-border); + } +} + +.site-scan__source-name { + font-weight: 700; +} + +.site-scan__source-author::before { + border-left: 1px solid; + content: ""; + display: inline-block; + height: 1em; + margin: 0 0.5em; + vertical-align: middle; +} + +.site-scan__source-version { + margin-left: auto; +} From fbe3bf39d8de5ed083e42b25296f2540a94a727e Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 15:33:04 +0200 Subject: [PATCH 017/120] Omit optional `.php` plugin extension in site scan results --- .../site-scan-context-provider/get-site-issues.js | 2 +- .../site-scan-context-provider/test/get-site-issues.js | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/get-site-issues.js b/assets/src/components/site-scan-context-provider/get-site-issues.js index 0040cc4e799..88ee90af64d 100644 --- a/assets/src/components/site-scan-context-provider/get-site-issues.js +++ b/assets/src/components/site-scan-context-provider/get-site-issues.js @@ -17,7 +17,7 @@ export function getSiteIssues( validationResults = [] ) { for ( const source of error.sources ) { if ( source.type === 'plugin' && source.name !== 'amp' ) { - pluginIssues.add( source.name ); + pluginIssues.add( source.name.match( /(.*?)(?:\.php)?$/ )[ 1 ] ); } else if ( source.type === 'theme' ) { themeIssues.add( source.name ); } diff --git a/assets/src/components/site-scan-context-provider/test/get-site-issues.js b/assets/src/components/site-scan-context-provider/test/get-site-issues.js index f56c8fc7cc0..d22250fcc0c 100644 --- a/assets/src/components/site-scan-context-provider/test/get-site-issues.js +++ b/assets/src/components/site-scan-context-provider/test/get-site-issues.js @@ -40,6 +40,13 @@ describe( 'getSiteIssues', () => { ], }, }, + { + error: { + sources: [ + { type: 'plugin', name: 'foo-bar.php' }, + ], + }, + }, { error: { sources: [ @@ -67,7 +74,7 @@ describe( 'getSiteIssues', () => { const issues = getSiteIssues( validationResult ); - expect( issues.pluginIssues ).toStrictEqual( expect.arrayContaining( [ 'gutenberg', 'jetpack' ] ) ); + expect( issues.pluginIssues ).toStrictEqual( expect.arrayContaining( [ 'gutenberg', 'jetpack', 'foo-bar' ] ) ); expect( issues.themeIssues ).toStrictEqual( expect.arrayContaining( [ 'twentytwenty' ] ) ); } ); } ); From 96438ba3ed32cabe16111f7fe133611049d1302e Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 15:34:51 +0200 Subject: [PATCH 018/120] Add plugins context provider and use in site scan summary --- .../__mocks__/index.js | 41 +++++ .../plugins-context-provider/index.js | 105 +++++++++++ .../test/use-normalized-plugins-data.js | 163 ++++++++++++++++++ .../use-normalized-plugins-data.js | 45 +++++ assets/src/onboarding-wizard/index.js | 33 ++-- .../pages/site-scan/complete.js | 13 +- .../pages/site-scan/sources-list.js | 46 ++--- 7 files changed, 406 insertions(+), 40 deletions(-) create mode 100644 assets/src/components/plugins-context-provider/__mocks__/index.js create mode 100644 assets/src/components/plugins-context-provider/index.js create mode 100644 assets/src/components/plugins-context-provider/test/use-normalized-plugins-data.js create mode 100644 assets/src/components/plugins-context-provider/use-normalized-plugins-data.js diff --git a/assets/src/components/plugins-context-provider/__mocks__/index.js b/assets/src/components/plugins-context-provider/__mocks__/index.js new file mode 100644 index 00000000000..0b57ff8ec22 --- /dev/null +++ b/assets/src/components/plugins-context-provider/__mocks__/index.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +export const Plugins = createContext(); + +/** + * MOCK. + * + * @param {Object} props + * @param {any} props.children Component children. + * @param {boolean} props.fetchingPlugins Whether fetching plugins or not. + * @param {Array} props.plugins An array of fetched plugins. + */ +export function PluginsContextProvider( { + children, + fetchingPlugins = false, + plugins = [], +} ) { + return ( + + { children } + + ); +} +PluginsContextProvider.propTypes = { + children: PropTypes.any, + fetchingPlugins: PropTypes.bool, + plugins: PropTypes.array, +}; diff --git a/assets/src/components/plugins-context-provider/index.js b/assets/src/components/plugins-context-provider/index.js new file mode 100644 index 00000000000..b0a64e9f9b9 --- /dev/null +++ b/assets/src/components/plugins-context-provider/index.js @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { + createContext, + useContext, + useEffect, + useRef, + useState, +} from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { ErrorContext } from '../error-context-provider'; +import { useAsyncError } from '../../utils/use-async-error'; + +export const Plugins = createContext(); + +/** + * Plugins context provider. + * + * @param {Object} props Component props. + * @param {any} props.children Component children. + * @param {boolean} props.hasErrorBoundary Whether the component is wrapped in an error boundary. + */ +export function PluginsContextProvider( { + children, + hasErrorBoundary = false, +} ) { + const [ plugins, setPlugins ] = useState(); + const [ fetchingPlugins, setFetchingPlugins ] = useState( null ); + + const { error, setError } = useContext( ErrorContext ); + const { setAsyncError } = useAsyncError(); + + /** + * This component sets state inside async functions. + * Use this ref to prevent state updates after unmount. + */ + const hasUnmounted = useRef( false ); + useEffect( () => () => { + hasUnmounted.current = true; + }, [] ); + + /** + * Fetches validated URL data. + */ + useEffect( () => { + if ( error || plugins || fetchingPlugins ) { + return; + } + + ( async () => { + setFetchingPlugins( true ); + + try { + const fetchedPlugins = await apiFetch( { + path: '/wp/v2/plugins', + } ); + + if ( hasUnmounted.current === true ) { + return; + } + + setPlugins( fetchedPlugins ); + } catch ( e ) { + if ( hasUnmounted.current === true ) { + return; + } + + setError( e ); + + if ( hasErrorBoundary ) { + setAsyncError( e ); + } + + return; + } + + setFetchingPlugins( false ); + } )(); + }, [ error, fetchingPlugins, hasErrorBoundary, plugins, setAsyncError, setError ] ); + + return ( + + { children } + + ); +} +PluginsContextProvider.propTypes = { + children: PropTypes.any, + hasErrorBoundary: PropTypes.bool, +}; diff --git a/assets/src/components/plugins-context-provider/test/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/test/use-normalized-plugins-data.js new file mode 100644 index 00000000000..08950bb4252 --- /dev/null +++ b/assets/src/components/plugins-context-provider/test/use-normalized-plugins-data.js @@ -0,0 +1,163 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import { act } from 'react-dom/test-utils'; + +/** + * WordPress dependencies + */ +import { render, unmountComponentAtNode } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ErrorContextProvider } from '../../error-context-provider'; +import { PluginsContextProvider } from '../index'; +import { useNormalizedPluginsData } from '../use-normalized-plugins-data'; + +jest.mock( '../index' ); + +let returnValue = {}; + +function ComponentContainingHook( { skipInactive } ) { + returnValue = useNormalizedPluginsData( { skipInactive } ); + return null; +} +ComponentContainingHook.propTypes = { + skipInactive: PropTypes.bool, +}; + +const Providers = ( { children, fetchingPlugins, plugins = [] } ) => ( + + + { children } + + +); +Providers.propTypes = { + children: PropTypes.any, + fetchingPlugins: PropTypes.bool, + plugins: PropTypes.array, +}; + +describe( 'useNormalizedPluginsData', () => { + let container = null; + + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); + } ); + + afterEach( () => { + unmountComponentAtNode( container ); + container.remove(); + container = null; + returnValue = {}; + } ); + + it( 'returns empty an array if plugins are being fetched', () => { + act( () => { + render( + + + , + container, + ); + } ); + + expect( returnValue ).toHaveLength( 0 ); + } ); + + it( 'returns a normalized array of plugins', () => { + act( () => { + render( + Acme Inc.', + }, + author_uri: { + raw: 'http://example.com', + rendered: 'http://example.com', + }, + name: 'Acme Plugin', + plugin: 'acme-inc', + status: 'inactive', + version: '1.0.1', + }, + { + author: 'AMP Project Contributors', + author_uri: 'https://github.com/ampproject/amp-wp/graphs/contributors', + name: { + raw: 'AMP', + rendered: 'AMP', + }, + plugin: 'amp/amp', + status: 'active', + version: '2.2.0-alpha', + }, + ] } + > + + , + container, + ); + } ); + + expect( returnValue ).toMatchObject( { + 'acme-inc': { + author: 'Acme Inc.', + author_uri: 'http://example.com', + name: 'Acme Plugin', + plugin: 'acme-inc', + status: 'inactive', + version: '1.0.1', + }, + amp: { + author: 'AMP Project Contributors', + author_uri: 'https://github.com/ampproject/amp-wp/graphs/contributors', + name: 'AMP', + plugin: 'amp/amp', + status: 'active', + version: '2.2.0-alpha', + }, + } ); + } ); + + it( 'skips inactive plugins', () => { + act( () => { + render( + + + , + container, + ); + } ); + + expect( returnValue ).toMatchObject( { + amp: { + plugin: 'amp/amp', + status: 'active', + }, + } ); + } ); +} ); diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js new file mode 100644 index 00000000000..3663e3aca9c --- /dev/null +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { useContext, useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Plugins } from './index'; + +export function useNormalizedPluginsData( { skipInactive = true } = {} ) { + const { fetchingPlugins, plugins } = useContext( Plugins ); + const [ normalizedPluginsData, setNormalizedPluginsData ] = useState( [] ); + + useEffect( () => { + if ( fetchingPlugins || plugins.length === 0 ) { + return; + } + + setNormalizedPluginsData( () => plugins.reduce( ( acc, source ) => { + const { status, plugin } = source; + + if ( skipInactive && status !== 'active' ) { + return acc; + } + + const pluginSlug = plugin.match( /^(?:[^\/]*\/)?(.*?)$/ )[ 1 ]; + + if ( ! pluginSlug ) { + return acc; + } + + return { + ...acc, + [ pluginSlug ]: Object.keys( source ).reduce( ( props, key ) => ( { + ...props, + // Flatten every prop that contains a `raw` member. + [ key ]: source[ key ]?.raw ?? source[ key ], + } ), {} ), + }; + }, {} ) ); + }, [ fetchingPlugins, plugins, skipInactive ] ); + + return normalizedPluginsData; +} diff --git a/assets/src/onboarding-wizard/index.js b/assets/src/onboarding-wizard/index.js index 4137a449120..182cfc6b8e8 100644 --- a/assets/src/onboarding-wizard/index.js +++ b/assets/src/onboarding-wizard/index.js @@ -36,6 +36,7 @@ import { ErrorContextProvider } from '../components/error-context-provider'; import { ErrorScreen } from '../components/error-screen'; import { SiteScanContextProvider } from '../components/site-scan-context-provider'; import { UserContextProvider } from '../components/user-context-provider'; +import { PluginsContextProvider } from '../components/plugins-context-provider'; import { PAGES } from './pages'; import { SetupWizard } from './setup-wizard'; import { NavigationContextProvider } from './components/navigation-context-provider'; @@ -72,21 +73,23 @@ export function Providers( { children } ) { usersResourceRestPath={ USERS_RESOURCE_REST_PATH } > - - - - { children } - - - + + + + + { children } + + + + diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 9e55f61ed18..3c696dfe861 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -13,6 +13,8 @@ import { SiteScan } from '../../../components/site-scan-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { IconWebsitePaintBrush } from '../../../components/svg/website-paint-brush'; import { IconLaptopPlug } from '../../../components/svg/laptop-plug'; +import { useNormalizedPluginsData } from '../../../components/plugins-context-provider/use-normalized-plugins-data'; +import { Loading } from '../../../components/loading'; import { SourcesList } from './sources-list'; /** @@ -24,6 +26,9 @@ export function SiteScanComplete() { themeIssues, } = useContext( SiteScan ); + const pluginsData = useNormalizedPluginsData(); + const pluginsWithIssues = pluginIssues.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ); + return (
@@ -51,9 +56,7 @@ export function SiteScanComplete() {

-
- -
+
) } { pluginIssues.length > 0 && ( @@ -71,7 +74,9 @@ export function SiteScanComplete() {

- + { pluginsWithIssues.length === 0 + ? + : }
) } diff --git a/assets/src/onboarding-wizard/pages/site-scan/sources-list.js b/assets/src/onboarding-wizard/pages/site-scan/sources-list.js index 12ecf47b9e5..b9547b90f1a 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/sources-list.js +++ b/assets/src/onboarding-wizard/pages/site-scan/sources-list.js @@ -11,34 +11,38 @@ import { __, sprintf } from '@wordpress/i18n'; /** * Sources list component. * - * @param {Object} props Component props. - * @param {Array} props.issues List of sources causing issues. + * @param {Object} props Component props. + * @param {Array} props.sources Sources data. */ -export function SourcesList( { issues } ) { +export function SourcesList( { sources } ) { return (
    - { issues.map( ( slug ) => ( + { sources.map( ( { name, author, version } ) => (
  • - { slug } - - - { sprintf( - // translators: %s is an author name. - __( 'by %s', 'amp' ), - 'Foo Bar', - ) } - - - { sprintf( - // translators: %s is a version number. - __( 'Version %s', 'amp' ), - '1.0', - ) } + { name } + { author && ( + + { sprintf( + // translators: %s is an author name. + __( 'by %s', 'amp' ), + author, + ) } + + ) } + { version && ( + + { sprintf( + // translators: %s is a version number. + __( 'Version %s', 'amp' ), + version, + ) } + + ) }
  • ) ) }
@@ -46,5 +50,5 @@ export function SourcesList( { issues } ) { } SourcesList.propTypes = { - issues: PropTypes.array.isRequired, + sources: PropTypes.array.isRequired, }; From bfd3d5f6f1033669935f4ce4a4e9720ad2c94f78 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 15:58:14 +0200 Subject: [PATCH 019/120] Add themes context provider and use in site scan summary --- .../__mocks__/index.js | 41 +++++ .../themes-context-provider/index.js | 105 +++++++++++ .../test/use-normalized-themes-data.js | 163 ++++++++++++++++++ .../use-normalized-themes-data.js | 43 +++++ assets/src/onboarding-wizard/index.js | 33 ++-- .../pages/site-scan/complete.js | 14 +- 6 files changed, 381 insertions(+), 18 deletions(-) create mode 100644 assets/src/components/themes-context-provider/__mocks__/index.js create mode 100644 assets/src/components/themes-context-provider/index.js create mode 100644 assets/src/components/themes-context-provider/test/use-normalized-themes-data.js create mode 100644 assets/src/components/themes-context-provider/use-normalized-themes-data.js diff --git a/assets/src/components/themes-context-provider/__mocks__/index.js b/assets/src/components/themes-context-provider/__mocks__/index.js new file mode 100644 index 00000000000..484a976435a --- /dev/null +++ b/assets/src/components/themes-context-provider/__mocks__/index.js @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +export const Themes = createContext(); + +/** + * MOCK. + * + * @param {Object} props + * @param {any} props.children Component children. + * @param {boolean} props.fetchingThemes Whether fetching themes or not. + * @param {Array} props.themes An array of fetched themes. + */ +export function ThemesContextProvider( { + children, + fetchingThemes = false, + themes = [], +} ) { + return ( + + { children } + + ); +} +ThemesContextProvider.propTypes = { + children: PropTypes.any, + fetchingThemes: PropTypes.bool, + themes: PropTypes.array, +}; diff --git a/assets/src/components/themes-context-provider/index.js b/assets/src/components/themes-context-provider/index.js new file mode 100644 index 00000000000..f5b356195e0 --- /dev/null +++ b/assets/src/components/themes-context-provider/index.js @@ -0,0 +1,105 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { + createContext, + useContext, + useEffect, + useRef, + useState, +} from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { ErrorContext } from '../error-context-provider'; +import { useAsyncError } from '../../utils/use-async-error'; + +export const Themes = createContext(); + +/** + * Themes context provider. + * + * @param {Object} props Component props. + * @param {any} props.children Component children. + * @param {boolean} props.hasErrorBoundary Whether the component is wrapped in an error boundary. + */ +export function ThemesContextProvider( { + children, + hasErrorBoundary = false, +} ) { + const [ themes, setThemes ] = useState(); + const [ fetchingThemes, setFetchingThemes ] = useState( null ); + + const { error, setError } = useContext( ErrorContext ); + const { setAsyncError } = useAsyncError(); + + /** + * This component sets state inside async functions. + * Use this ref to prevent state updates after unmount. + */ + const hasUnmounted = useRef( false ); + useEffect( () => () => { + hasUnmounted.current = true; + }, [] ); + + /** + * Fetches validated URL data. + */ + useEffect( () => { + if ( error || themes || fetchingThemes ) { + return; + } + + ( async () => { + setFetchingThemes( true ); + + try { + const fetchedThemes = await apiFetch( { + path: '/wp/v2/themes', + } ); + + if ( hasUnmounted.current === true ) { + return; + } + + setThemes( fetchedThemes ); + } catch ( e ) { + if ( hasUnmounted.current === true ) { + return; + } + + setError( e ); + + if ( hasErrorBoundary ) { + setAsyncError( e ); + } + + return; + } + + setFetchingThemes( false ); + } )(); + }, [ error, fetchingThemes, hasErrorBoundary, themes, setAsyncError, setError ] ); + + return ( + + { children } + + ); +} +ThemesContextProvider.propTypes = { + children: PropTypes.any, + hasErrorBoundary: PropTypes.bool, +}; diff --git a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js new file mode 100644 index 00000000000..2d2ed4c90b4 --- /dev/null +++ b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js @@ -0,0 +1,163 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import { act } from 'react-dom/test-utils'; + +/** + * WordPress dependencies + */ +import { render, unmountComponentAtNode } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ErrorContextProvider } from '../../error-context-provider'; +import { ThemesContextProvider } from '../index'; +import { useNormalizedThemesData } from '../use-normalized-themes-data'; + +jest.mock( '../index' ); + +let returnValue = {}; + +function ComponentContainingHook( { skipInactive } ) { + returnValue = useNormalizedThemesData( { skipInactive } ); + return null; +} +ComponentContainingHook.propTypes = { + skipInactive: PropTypes.bool, +}; + +const Providers = ( { children, fetchingThemes, themes = [] } ) => ( + + + { children } + + +); +Providers.propTypes = { + children: PropTypes.any, + fetchingThemes: PropTypes.bool, + themes: PropTypes.array, +}; + +describe( 'useNormalizedThemesData', () => { + let container = null; + + beforeEach( () => { + container = document.createElement( 'div' ); + document.body.appendChild( container ); + } ); + + afterEach( () => { + unmountComponentAtNode( container ); + container.remove(); + container = null; + returnValue = {}; + } ); + + it( 'returns empty an array if themes are being fetched', () => { + act( () => { + render( + + + , + container, + ); + } ); + + expect( returnValue ).toHaveLength( 0 ); + } ); + + it( 'returns a normalized array of themes', () => { + act( () => { + render( + the WordPress team', + }, + author_uri: { + raw: 'https://wordpress.org/', + rendered: 'https://wordpress.org/', + }, + name: 'Twenty Fifteen', + stylesheet: 'twentyfifteen', + status: 'inactive', + version: '3.0', + }, + { + author: 'the WordPress team', + author_uri: 'https://wordpress.org/', + name: { + raw: 'Twenty Twenty', + rendered: 'Twenty Twenty', + }, + stylesheet: 'twentytwenty', + status: 'active', + version: '1.7', + }, + ] } + > + + , + container, + ); + } ); + + expect( returnValue ).toMatchObject( { + twentyfifteen: { + author: 'the WordPress team', + author_uri: 'https://wordpress.org/', + name: 'Twenty Fifteen', + stylesheet: 'twentyfifteen', + status: 'inactive', + version: '3.0', + }, + twentytwenty: { + author: 'the WordPress team', + author_uri: 'https://wordpress.org/', + name: 'Twenty Twenty', + stylesheet: 'twentytwenty', + status: 'active', + version: '1.7', + }, + } ); + } ); + + it( 'skips inactive plugins', () => { + act( () => { + render( + + + , + container, + ); + } ); + + expect( returnValue ).toMatchObject( { + twentytwenty: { + stylesheet: 'twentytwenty', + status: 'active', + }, + } ); + } ); +} ); diff --git a/assets/src/components/themes-context-provider/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/use-normalized-themes-data.js new file mode 100644 index 00000000000..36a35c9ce2a --- /dev/null +++ b/assets/src/components/themes-context-provider/use-normalized-themes-data.js @@ -0,0 +1,43 @@ +/** + * WordPress dependencies + */ +import { useContext, useEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { Themes } from './index'; + +export function useNormalizedThemesData( { skipInactive = true } = {} ) { + const { fetchingThemes, themes } = useContext( Themes ); + const [ normalizedThemesData, setNormalizedThemesData ] = useState( [] ); + + useEffect( () => { + if ( fetchingThemes || themes.length === 0 ) { + return; + } + + setNormalizedThemesData( () => themes.reduce( ( acc, source ) => { + const { status, stylesheet } = source; + + if ( ! stylesheet ) { + return acc; + } + + if ( skipInactive && status !== 'active' ) { + return acc; + } + + return { + ...acc, + [ stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { + ...props, + // Flatten every prop that contains a `raw` member. + [ key ]: source[ key ]?.raw ?? source[ key ], + } ), {} ), + }; + }, {} ) ); + }, [ skipInactive, fetchingThemes, themes ] ); + + return normalizedThemesData; +} diff --git a/assets/src/onboarding-wizard/index.js b/assets/src/onboarding-wizard/index.js index 182cfc6b8e8..40d15fefc06 100644 --- a/assets/src/onboarding-wizard/index.js +++ b/assets/src/onboarding-wizard/index.js @@ -37,6 +37,7 @@ import { ErrorScreen } from '../components/error-screen'; import { SiteScanContextProvider } from '../components/site-scan-context-provider'; import { UserContextProvider } from '../components/user-context-provider'; import { PluginsContextProvider } from '../components/plugins-context-provider'; +import { ThemesContextProvider } from '../components/themes-context-provider'; import { PAGES } from './pages'; import { SetupWizard } from './setup-wizard'; import { NavigationContextProvider } from './components/navigation-context-provider'; @@ -74,21 +75,23 @@ export function Providers( { children } ) { > - - - - { children } - - - + + + + + { children } + + + + diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 3c696dfe861..00940f729e1 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -13,8 +13,9 @@ import { SiteScan } from '../../../components/site-scan-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { IconWebsitePaintBrush } from '../../../components/svg/website-paint-brush'; import { IconLaptopPlug } from '../../../components/svg/laptop-plug'; -import { useNormalizedPluginsData } from '../../../components/plugins-context-provider/use-normalized-plugins-data'; import { Loading } from '../../../components/loading'; +import { useNormalizedPluginsData } from '../../../components/plugins-context-provider/use-normalized-plugins-data'; +import { useNormalizedThemesData } from '../../../components/themes-context-provider/use-normalized-themes-data'; import { SourcesList } from './sources-list'; /** @@ -26,8 +27,11 @@ export function SiteScanComplete() { themeIssues, } = useContext( SiteScan ); + const themesData = useNormalizedThemesData(); + const themesWithIssues = themeIssues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; + const pluginsData = useNormalizedPluginsData(); - const pluginsWithIssues = pluginIssues.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ); + const pluginsWithIssues = pluginIssues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; return (
@@ -56,7 +60,11 @@ export function SiteScanComplete() {

-
+
+ { themesWithIssues.length === 0 + ? + : } +
) } { pluginIssues.length > 0 && ( From 7b7f296b886d6e34a639bae9fa736620d5b5277a Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 16:25:12 +0200 Subject: [PATCH 020/120] Extract themes and plugins with issues lists to sub-components --- .../plugins-context-provider/index.js | 4 +- .../themes-context-provider/index.js | 4 +- .../pages/site-scan/complete.js | 66 ++++++++++++++----- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/assets/src/components/plugins-context-provider/index.js b/assets/src/components/plugins-context-provider/index.js index b0a64e9f9b9..e80c57e982a 100644 --- a/assets/src/components/plugins-context-provider/index.js +++ b/assets/src/components/plugins-context-provider/index.js @@ -34,7 +34,7 @@ export function PluginsContextProvider( { children, hasErrorBoundary = false, } ) { - const [ plugins, setPlugins ] = useState(); + const [ plugins, setPlugins ] = useState( [] ); const [ fetchingPlugins, setFetchingPlugins ] = useState( null ); const { error, setError } = useContext( ErrorContext ); @@ -53,7 +53,7 @@ export function PluginsContextProvider( { * Fetches validated URL data. */ useEffect( () => { - if ( error || plugins || fetchingPlugins ) { + if ( error || plugins.length > 0 || fetchingPlugins ) { return; } diff --git a/assets/src/components/themes-context-provider/index.js b/assets/src/components/themes-context-provider/index.js index f5b356195e0..537dbbbbd4a 100644 --- a/assets/src/components/themes-context-provider/index.js +++ b/assets/src/components/themes-context-provider/index.js @@ -34,7 +34,7 @@ export function ThemesContextProvider( { children, hasErrorBoundary = false, } ) { - const [ themes, setThemes ] = useState(); + const [ themes, setThemes ] = useState( [] ); const [ fetchingThemes, setFetchingThemes ] = useState( null ); const { error, setError } = useContext( ErrorContext ); @@ -53,7 +53,7 @@ export function ThemesContextProvider( { * Fetches validated URL data. */ useEffect( () => { - if ( error || themes || fetchingThemes ) { + if ( error || themes.length > 0 || fetchingThemes ) { return; } diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 00940f729e1..8fc4fbe1c5e 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + /** * WordPress dependencies */ @@ -22,16 +27,7 @@ import { SourcesList } from './sources-list'; * Screen with site scan summary. */ export function SiteScanComplete() { - const { - pluginIssues, - themeIssues, - } = useContext( SiteScan ); - - const themesData = useNormalizedThemesData(); - const themesWithIssues = themeIssues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; - - const pluginsData = useNormalizedPluginsData(); - const pluginsWithIssues = pluginIssues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; + const { pluginIssues, themeIssues } = useContext( SiteScan ); return (
@@ -61,9 +57,7 @@ export function SiteScanComplete() {

- { themesWithIssues.length === 0 - ? - : } +
) } @@ -82,12 +76,52 @@ export function SiteScanComplete() {

- { pluginsWithIssues.length === 0 - ? - : } +
) } ); } + +/** + * Render a list of themes that cause issues. + * + * @param {Object} props Component props. + * @param {Array} props.issues List of theme issues. + */ +function ThemesWithIssues( { issues = [] } ) { + const themesData = useNormalizedThemesData(); + const themesWithIssues = issues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; + + if ( themesWithIssues.length === 0 ) { + return ; + } + + return ; +} + +ThemesWithIssues.propTypes = { + issues: PropTypes.array.isRequired, +}; + +/** + * Render a list of plugins that cause issues. + * + * @param {Object} props Component props. + * @param {Array} props.issues List of plugins issues. + */ +function PluginsWithIssues( { issues = [] } ) { + const pluginsData = useNormalizedPluginsData(); + const pluginsWithIssues = issues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; + + if ( pluginsWithIssues.length === 0 ) { + return ; + } + + return ; +} + +PluginsWithIssues.propTypes = { + issues: PropTypes.array.isRequired, +}; From bbaaac0c1845181daee4da1caedef544518d92cf Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 17:23:10 +0200 Subject: [PATCH 021/120] Improve site scan flow and allow cancelling --- .../site-scan-context-provider/index.js | 29 +++++++++++++++---- .../pages/site-scan/index.js | 18 ++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index c788aa0c6ab..d7d0fbcedee 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, useEffect, useRef, useState } from '@wordpress/element'; +import { createContext, useCallback, useEffect, useRef, useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; /** @@ -37,7 +37,7 @@ export function SiteScanContextProvider( { const [ themeIssues, setThemeIssues ] = useState( [] ); const [ pluginIssues, setPluginIssues ] = useState( [] ); const [ siteScanState, setSiteScanState ] = useState( SITE_SCAN_STATE_INITIALIZING ); - const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 0 ); + const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 5 ); const [ fetchingScannableUrls, setFetchingScannableUrls ] = useState( false ); const [ fetchedScannableUrls, setFetchedScannableUrls ] = useState( false ); const [ scannableUrls, setScannableUrls ] = useState( [] ); @@ -49,14 +49,23 @@ export function SiteScanContextProvider( { hasUnmounted.current = true; }, [] ); + /** + * Allows cancelling a scan that is in progress. + */ + const hasCanceled = useRef( false ); + const cancelSiteScan = useCallback( () => { + setCurrentlyScannedUrlIndex( 0 ); + hasCanceled.current = true; + }, [] ); + + /** + * Fetch scannable URLs from the REST endpoint. + */ useEffect( () => { if ( fetchingScannableUrls || fetchedScannableUrls ) { return; } - /** - * Fetches scannable URLs from the REST endpoint. - */ ( async () => { setFetchingScannableUrls( true ); @@ -103,6 +112,15 @@ export function SiteScanContextProvider( { return; } + if ( true === hasCanceled.current ) { + hasCanceled.current = false; + setSiteScanState( SITE_SCAN_STATE_READY ); + setThemeIssues( [] ); + setPluginIssues( [] ); + + return; + } + if ( validationResults.results.length > 0 ) { const siteIssues = getSiteIssues( validationResults.results ); @@ -130,6 +148,7 @@ export function SiteScanContextProvider( { () => cancelSiteScan(), [ cancelSiteScan ] ); + /** * Start site scan. */ - useEffect( () => { + useLayoutEffect( () => { if ( canScanSite ) { startSiteScan(); + } else if ( siteScanComplete ) { + setCanShowScanSummary( true ); } - }, [ canScanSite, startSiteScan ] ); + }, [ canScanSite, siteScanComplete, startSiteScan ] ); /** * Show scan summary with a delay so that the progress bar has a chance to @@ -40,7 +48,7 @@ export function SiteScan() { useEffect( () => { let delay; - if ( siteScanComplete ) { + if ( siteScanComplete && ! canShowScanSummary ) { delay = setTimeout( () => setCanShowScanSummary( true ), 500 ); } @@ -49,7 +57,7 @@ export function SiteScan() { clearTimeout( delay ); } }; - }, [ siteScanComplete ] ); + }, [ canShowScanSummary, siteScanComplete ] ); /** * Allow moving forward. From a6484437f9e5e1046163f5dccf05279840f1fcb1 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 24 Sep 2021 17:23:59 +0200 Subject: [PATCH 022/120] Fix stylelint issue --- assets/src/onboarding-wizard/pages/site-scan/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss index 13b3d59b4ca..cd57f2bf75c 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/style.scss +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -57,6 +57,7 @@ .site-scan__sources { border: 2px solid var(--amp-settings-color-border); } + .site-scan__source { align-items: center; display: flex; From 49cf60ed22dc3118cc4846a738380948f738ebad Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 14:51:24 +0200 Subject: [PATCH 023/120] Add final message for when site scan reveals no issues --- .../src/onboarding-wizard/pages/site-scan/complete.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 8fc4fbe1c5e..87939a2a016 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -28,6 +28,8 @@ import { SourcesList } from './sources-list'; */ export function SiteScanComplete() { const { pluginIssues, themeIssues } = useContext( SiteScan ); + const hasThemeIssues = themeIssues.length > 0; + const hasPluginIssues = pluginIssues.length > 0; return (
@@ -39,10 +41,13 @@ export function SiteScanComplete() {

- { __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) } + { hasThemeIssues || hasPluginIssues + ? __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) + : __( 'Site scan found no issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) + }

- { themeIssues.length > 0 && ( + { hasThemeIssues && (
@@ -61,7 +66,7 @@ export function SiteScanComplete() {
) } - { pluginIssues.length > 0 && ( + { hasPluginIssues && (
From a42e8c32d4750f66297be59fbdbb773c7acc7ff1 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 16:00:30 +0200 Subject: [PATCH 024/120] Add links to Validated URL page to Site Scan summary step --- .../onboarding-wizard/pages/site-scan/complete.js | 13 ++++++++++++- .../onboarding-wizard/pages/site-scan/style.scss | 9 +++++++++ src/Admin/OnboardingWizardSubmenuPage.php | 8 +++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 87939a2a016..662ebe92e97 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -2,13 +2,14 @@ * External dependencies */ import PropTypes from 'prop-types'; +import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { useContext } from '@wordpress/element'; -import { VisuallyHidden } from '@wordpress/components'; +import { ExternalLink, VisuallyHidden } from '@wordpress/components'; /** * Internal dependencies @@ -63,6 +64,11 @@ export function SiteScanComplete() {
+

+ + { __( 'AMP Validated URLs page', 'amp' ) } + +

) } @@ -82,6 +88,11 @@ export function SiteScanComplete() {
+

+ + { __( 'AMP Validated URLs page', 'amp' ) } + +

) } diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss index cd57f2bf75c..03430f0dded 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/style.scss +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -91,3 +91,12 @@ .site-scan__source-version { margin-left: auto; } + +.site-scan__cta.site-scan__cta { + font-size: 14px; + margin-bottom: 0; + + .components-external-link__icon { + fill: var(--amp-settings-color-brand); + } +} diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index cf0f961d4f7..0af2f247754 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -9,6 +9,7 @@ namespace AmpProject\AmpWP\Admin; use AMP_Options_Manager; +use AMP_Validated_URL_Post_Type; use AmpProject\AmpWP\DevTools\UserAccess; use AmpProject\AmpWP\Infrastructure\Delayed; use AmpProject\AmpWP\Infrastructure\Registerable; @@ -224,7 +225,11 @@ public function enqueue_assets( $hook_suffix ) { $theme = wp_get_theme(); $is_reader_theme = $this->reader_themes->theme_data_exists( get_stylesheet() ); - $amp_settings_link = menu_page_url( AMP_Options_Manager::OPTION_NAME, false ); + $amp_settings_link = menu_page_url( AMP_Options_Manager::OPTION_NAME, false ); + $amp_validated_urls_link = admin_url( add_query_arg( + [ 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ], + 'edit.php' + ) ); $setup_wizard_data = [ 'AMP_OPTIONS_KEY' => AMP_Options_Manager::OPTION_NAME, @@ -255,6 +260,7 @@ public function enqueue_assets( $hook_suffix ) { 'UPDATES_NONCE' => wp_create_nonce( 'updates' ), 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', + 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, ]; wp_add_inline_script( From 38121897e455125028b91c3ab38e133883802d60 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 17:49:34 +0200 Subject: [PATCH 025/120] Revert debug code --- assets/src/components/site-scan-context-provider/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index d7d0fbcedee..3488b998e54 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -37,7 +37,7 @@ export function SiteScanContextProvider( { const [ themeIssues, setThemeIssues ] = useState( [] ); const [ pluginIssues, setPluginIssues ] = useState( [] ); const [ siteScanState, setSiteScanState ] = useState( SITE_SCAN_STATE_INITIALIZING ); - const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 5 ); + const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 0 ); const [ fetchingScannableUrls, setFetchingScannableUrls ] = useState( false ); const [ fetchedScannableUrls, setFetchedScannableUrls ] = useState( false ); const [ scannableUrls, setScannableUrls ] = useState( [] ); From c0a18af8392052cde3ba7247fc31ed4be237f6c8 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 17:50:08 +0200 Subject: [PATCH 026/120] Allow going forward from theme selection step --- .../onboarding-wizard/pages/template-mode/index.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/template-mode/index.js b/assets/src/onboarding-wizard/pages/template-mode/index.js index a1fdffa7454..01a199e5ff1 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/index.js +++ b/assets/src/onboarding-wizard/pages/template-mode/index.js @@ -10,7 +10,6 @@ import { ReaderThemes } from '../../../components/reader-themes-context-provider import { SiteScan } from '../../../components/site-scan-context-provider'; import { User } from '../../../components/user-context-provider'; import { Options } from '../../../components/options-context-provider'; -import { Loading } from '../../../components/loading'; import { TemplateModeOverride } from '../../components/template-mode-override-context-provider'; import { ScreenUI } from './screen-ui'; @@ -21,7 +20,7 @@ export function TemplateMode() { const { setCanGoForward } = useContext( Navigation ); const { editedOptions, originalOptions, updateOptions } = useContext( Options ); const { developerToolsOption } = useContext( User ); - const { pluginIssues, themeIssues, scanningSite } = useContext( SiteScan ); + const { pluginIssues, themeIssues } = useContext( SiteScan ); const { currentTheme } = useContext( ReaderThemes ); const { technicalQuestionChangedAtLeastOnce } = useContext( TemplateModeOverride ); @@ -31,14 +30,10 @@ export function TemplateMode() { * Allow moving forward. */ useEffect( () => { - if ( false === scanningSite && undefined !== themeSupport ) { + if ( undefined !== themeSupport ) { setCanGoForward( true ); } - }, [ setCanGoForward, scanningSite, themeSupport ] ); - - if ( scanningSite ) { - return ; - } + }, [ setCanGoForward, themeSupport ] ); // The actual display component should avoid using global context directly. This will facilitate developing and testing the UI using different options. return ( From e500f702ed9c07969f44a9b6bbe4c2a3987b18d6 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 17:56:30 +0200 Subject: [PATCH 027/120] Minor code cleanup --- .../site-scan-context-provider/index.js | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 3488b998e54..1f76734af5d 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -49,6 +49,10 @@ export function SiteScanContextProvider( { hasUnmounted.current = true; }, [] ); + const startSiteScan = useCallback( () => { + setSiteScanState( SITE_SCAN_STATE_IDLE ); + }, [] ); + /** * Allows cancelling a scan that is in progress. */ @@ -146,19 +150,16 @@ export function SiteScanContextProvider( { return ( setSiteScanState( SITE_SCAN_STATE_IDLE ), - themeIssues, - } - } + value={ { + canScanSite: siteScanState === SITE_SCAN_STATE_READY, + cancelSiteScan, + currentlyScannedUrlIndex, + pluginIssues, + scannableUrls, + siteScanComplete: siteScanState === SITE_SCAN_STATE_COMPLETE, + startSiteScan, + themeIssues, + } } > { children } From c3f616c68248f75e68125cf6f03651134096b0b3 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 27 Sep 2021 18:40:44 +0200 Subject: [PATCH 028/120] Fix phpcs issues --- src/Admin/OnboardingWizardSubmenuPage.php | 10 ++++++---- src/Validation/ScannableURLsRestController.php | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index 0af2f247754..6fa65dc358c 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -226,10 +226,12 @@ public function enqueue_assets( $hook_suffix ) { $is_reader_theme = $this->reader_themes->theme_data_exists( get_stylesheet() ); $amp_settings_link = menu_page_url( AMP_Options_Manager::OPTION_NAME, false ); - $amp_validated_urls_link = admin_url( add_query_arg( - [ 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ], - 'edit.php' - ) ); + $amp_validated_urls_link = admin_url( + add_query_arg( + [ 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ], + 'edit.php' + ) + ); $setup_wizard_data = [ 'AMP_OPTIONS_KEY' => AMP_Options_Manager::OPTION_NAME, diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index 0d1a6db8314..d57212629c8 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -114,20 +114,20 @@ public function get_item_schema() { 'type' => 'object', 'properties' => [ 'url' => [ - 'description' => __( 'Page URL.' ), + 'description' => __( 'Page URL.', 'amp' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => [ 'view' ], ], 'type' => [ - 'description' => __( 'Page type.' ), + 'description' => __( 'Page type.', 'amp' ), 'type' => 'string', 'readonly' => true, 'context' => [ 'view' ], ], 'validate_url' => [ - 'description' => __( 'URL for accessing validation data for a given page.' ), + 'description' => __( 'URL for accessing validation data for a given page.', 'amp' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, From 317138546e5312ed20ed1d050bddac2d1c93a909 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 30 Sep 2021 14:55:36 +0200 Subject: [PATCH 029/120] Use scannable URL label provided by backend --- .../pages/site-scan/in-progress.js | 38 +------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js index 04605c3cdfa..df96590d2df 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js +++ b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js @@ -12,40 +12,6 @@ import { Selectable } from '../../../components/selectable'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { ProgressBar } from '../../../components/progress-bar'; -/** - * Gets the currently checked page type label. - * - * @param {string} type The page type slug. - */ -function getPageTypeLabel( type ) { - switch ( type ) { - case 'home': - return __( 'Checking homepage', 'amp' ); - - case 'post': - return __( 'Checking sample post', 'amp' ); - - case 'page': - return __( 'Checking sample page', 'amp' ); - - case 'author': - return __( 'Checking author archive', 'amp' ); - - case 'date': - return __( 'Checking date archive', 'amp' ); - - case 'search': - return __( 'Checking search results page', 'amp' ); - - default: - return sprintf( - // translators: %s is a page type label. - __( 'Checking %s', 'amp' ), - type, - ); - } -} - /** * Screen for visualizing a site scan progress state. */ @@ -77,10 +43,10 @@ export function SiteScanInProgress() { ? __( 'Scan complete', 'amp' ) : sprintf( // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: %3$s…', 'amp' ), + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), currentlyScannedUrlIndex + 1, scannableUrls.length, - getPageTypeLabel( scannableUrls[ currentlyScannedUrlIndex ]?.type ), + scannableUrls[ currentlyScannedUrlIndex ]?.label, ) }

From 1ab1b7384a783fa9ebbf3486d3d5d7f9112bedc9 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 30 Sep 2021 15:07:05 +0200 Subject: [PATCH 030/120] Use ScannableURLs REST endpoint for getting preview URLs --- .../pages/done/use-preview.js | 17 +++---- src/Admin/OnboardingWizardSubmenuPage.php | 45 ++++------------ .../ScannableURLsRestController.php | 20 +++++--- .../Admin/OnboardingWizardSubmenuPageTest.php | 51 ------------------- 4 files changed, 29 insertions(+), 104 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/done/use-preview.js b/assets/src/onboarding-wizard/pages/done/use-preview.js index e9a2b708f75..4834e729346 100644 --- a/assets/src/onboarding-wizard/pages/done/use-preview.js +++ b/assets/src/onboarding-wizard/pages/done/use-preview.js @@ -3,33 +3,30 @@ */ import { useContext, useMemo, useState } from '@wordpress/element'; -/** - * External dependencies - */ -import { PREVIEW_URLS } from 'amp-settings'; // From WP inline script. - /** * Internal dependencies */ import { Options } from '../../../components/options-context-provider'; +import { SiteScan } from '../../../components/site-scan-context-provider'; import { STANDARD } from '../../../common/constants'; export function usePreview() { - const hasPreview = PREVIEW_URLS.length > 0; - + const { scannableUrls } = useContext( SiteScan ); const { editedOptions: { theme_support: themeSupport } } = useContext( Options ); + + const hasPreview = scannableUrls.length > 0; const [ isPreviewingAMP, setIsPreviewingAMP ] = useState( themeSupport !== STANDARD ); - const [ previewedPageType, setPreviewedPageType ] = useState( hasPreview ? PREVIEW_URLS[ 0 ].type : null ); + const [ previewedPageType, setPreviewedPageType ] = useState( hasPreview ? scannableUrls[ 0 ].type : null ); const toggleIsPreviewingAMP = () => setIsPreviewingAMP( ( mode ) => ! mode ); const setActivePreviewLink = ( link ) => setPreviewedPageType( link.type ); - const previewLinks = useMemo( () => PREVIEW_URLS.map( ( { url, amp_url: ampUrl, type, label } ) => ( { + const previewLinks = useMemo( () => scannableUrls.map( ( { url, amp_url: ampUrl, type, label } ) => ( { type, label, url: isPreviewingAMP ? ampUrl : url, isActive: type === previewedPageType, - } ) ), [ isPreviewingAMP, previewedPageType ] ); + } ) ), [ isPreviewingAMP, previewedPageType, scannableUrls ] ); const previewUrl = useMemo( () => previewLinks.find( ( link ) => link.isActive )?.url, [ previewLinks ] ); diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index 6fa65dc358c..ebb89472be1 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -70,28 +70,19 @@ final class OnboardingWizardSubmenuPage implements Delayed, Registerable, Servic */ private $loading_error; - /** - * ScannableURLProvider instance. - * - * @var ScannableURLProvider - */ - private $scannable_url_provider; - /** * OnboardingWizardSubmenuPage constructor. * - * @param GoogleFonts $google_fonts An instance of the GoogleFonts service. - * @param ReaderThemes $reader_themes An instance of the ReaderThemes class. - * @param RESTPreloader $rest_preloader An instance of the RESTPreloader class. - * @param LoadingError $loading_error An instance of the LoadingError class. - * @param ScannableURLProvider $scannable_url_provider An instance of the ScannableURLProvider class. + * @param GoogleFonts $google_fonts An instance of the GoogleFonts service. + * @param ReaderThemes $reader_themes An instance of the ReaderThemes class. + * @param RESTPreloader $rest_preloader An instance of the RESTPreloader class. + * @param LoadingError $loading_error An instance of the LoadingError class. */ - public function __construct( GoogleFonts $google_fonts, ReaderThemes $reader_themes, RESTPreloader $rest_preloader, LoadingError $loading_error, ScannableURLProvider $scannable_url_provider ) { - $this->google_fonts = $google_fonts; - $this->reader_themes = $reader_themes; - $this->rest_preloader = $rest_preloader; - $this->loading_error = $loading_error; - $this->scannable_url_provider = $scannable_url_provider; + public function __construct( GoogleFonts $google_fonts, ReaderThemes $reader_themes, RESTPreloader $rest_preloader, LoadingError $loading_error ) { + $this->google_fonts = $google_fonts; + $this->reader_themes = $reader_themes; + $this->rest_preloader = $rest_preloader; + $this->loading_error = $loading_error; } /** @@ -257,7 +248,6 @@ public function enqueue_assets( $hook_suffix ) { 'SCANNABLE_URLS_REST_PATH' => '/amp/v1/scannable-urls', 'SETTINGS_LINK' => $amp_settings_link, 'OPTIONS_REST_PATH' => '/amp/v1/options', - 'PREVIEW_URLS' => $this->get_preview_urls( $this->scannable_url_provider->get_urls() ), 'READER_THEMES_REST_PATH' => '/amp/v1/reader-themes', 'UPDATES_NONCE' => wp_create_nonce( 'updates' ), 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, @@ -322,21 +312,4 @@ public function get_close_link() { // Default to the AMP Settings page if a referrer link could not be determined. return menu_page_url( AMP_Options_Manager::OPTION_NAME, false ); } - - /** - * Add AMP URLs to the list of scannable URLs. - * - * @since 2.2 - * - * @param array $scannable_urls Array of scannable URLs. - * - * @return array Preview URLs. - */ - public function get_preview_urls( $scannable_urls ) { - foreach ( $scannable_urls as &$scannable_url ) { - $scannable_url['amp_url'] = amp_add_paired_endpoint( $scannable_url['url'] ); - } - - return $scannable_urls; - } } diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index d57212629c8..0097793cfd0 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -86,13 +86,12 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna return rest_ensure_response( array_map( static function ( $entry ) use ( $nonce ) { - $entry['validate_url'] = amp_add_paired_endpoint( - add_query_arg( - [ - AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, - ], - $entry['url'] - ) + $entry['amp_url'] = amp_add_paired_endpoint( $entry['url'] ); + $entry['validate_url'] = add_query_arg( + [ + AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, + ], + $entry['amp_url'] ); return $entry; @@ -120,6 +119,13 @@ public function get_item_schema() { 'readonly' => true, 'context' => [ 'view' ], ], + 'amp_url' => [ + 'description' => __( 'AMP URL.', 'amp' ), + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + 'context' => [ 'view' ], + ], 'type' => [ 'description' => __( 'Page type.', 'amp' ), 'type' => 'string', diff --git a/tests/php/src/Admin/OnboardingWizardSubmenuPageTest.php b/tests/php/src/Admin/OnboardingWizardSubmenuPageTest.php index e85a493979b..a520f2d6367 100644 --- a/tests/php/src/Admin/OnboardingWizardSubmenuPageTest.php +++ b/tests/php/src/Admin/OnboardingWizardSubmenuPageTest.php @@ -167,55 +167,4 @@ public function test_get_close_link( $referrer_link_callback, $expected_referrer $this->onboarding_wizard_submenu_page->get_close_link() ); } - - /** - * Tests OnboardingWizardSubmenuPage::get_preview_urls() - * - * @covers ::get_preview_urls() - */ - public function test_get_preview_urls() { - $scannable_urls = [ - [ - 'type' => 'home', - 'url' => 'https://example.com', - 'label' => 'Homepage', - ], - [ - 'type' => 'page', - 'url' => 'https://example.com/sample-page', - 'label' => 'Page', - ], - [ - 'type' => 'search', - 'url' => 'https://example.com/?s=foobar', - 'label' => 'Search Results', - ], - ]; - - $expected_urls = [ - [ - 'type' => 'home', - 'url' => 'https://example.com', - 'amp_url' => amp_add_paired_endpoint( 'https://example.com' ), - 'label' => 'Homepage', - ], - [ - 'type' => 'page', - 'url' => 'https://example.com/sample-page', - 'amp_url' => amp_add_paired_endpoint( 'https://example.com/sample-page' ), - 'label' => 'Page', - ], - [ - 'type' => 'search', - 'url' => 'https://example.com/?s=foobar', - 'amp_url' => amp_add_paired_endpoint( 'https://example.com/?s=foobar' ), - 'label' => 'Search Results', - ], - ]; - - $this->assertEquals( - $expected_urls, - $this->onboarding_wizard_submenu_page->get_preview_urls( $scannable_urls ) - ); - } } From faea80f3696e1d3a4c0e9113fabe39f7ccb970af Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 30 Sep 2021 16:22:22 +0200 Subject: [PATCH 031/120] Extract site scan results tables to standalone components --- .../src/components/site-scan-results/index.js | 3 + .../site-scan-results/plugins-with-issues.js | 43 +++++++ .../site-scan-results/site-scan-results.js | 70 ++++++++++++ .../site-scan-results/themes-with-issues.js | 43 +++++++ .../pages/site-scan/complete.js | 106 ++---------------- 5 files changed, 171 insertions(+), 94 deletions(-) create mode 100644 assets/src/components/site-scan-results/index.js create mode 100644 assets/src/components/site-scan-results/plugins-with-issues.js create mode 100644 assets/src/components/site-scan-results/site-scan-results.js create mode 100644 assets/src/components/site-scan-results/themes-with-issues.js diff --git a/assets/src/components/site-scan-results/index.js b/assets/src/components/site-scan-results/index.js new file mode 100644 index 00000000000..00901b12911 --- /dev/null +++ b/assets/src/components/site-scan-results/index.js @@ -0,0 +1,3 @@ +export { SiteScanResults } from './site-scan-results'; +export { ThemesWithIssues } from './themes-with-issues'; +export { PluginsWithIssues } from './plugins-with-issues'; diff --git a/assets/src/components/site-scan-results/plugins-with-issues.js b/assets/src/components/site-scan-results/plugins-with-issues.js new file mode 100644 index 00000000000..5dd1946742c --- /dev/null +++ b/assets/src/components/site-scan-results/plugins-with-issues.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useNormalizedPluginsData } from '../plugins-context-provider/use-normalized-plugins-data'; +import { IconLaptopPlug } from '../svg/laptop-plug'; +import { SiteScanResults } from './index'; + +/** + * Render a list of plugins that cause issues. + * + * @param {Object} props Component props. + * @param {Array} props.issues List of plugins issues. + * @param {Array} props.validatedUrlsLink URL to the Validated URLs page. + */ +export function PluginsWithIssues( { issues = [], validatedUrlsLink } ) { + const pluginsData = useNormalizedPluginsData(); + const sources = issues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; + + return ( + } + count={ issues.length } + sources={ sources } + validatedUrlsLink={ validatedUrlsLink } + /> + ); +} + +PluginsWithIssues.propTypes = { + issues: PropTypes.array.isRequired, + validatedUrlsLink: PropTypes.string, +}; diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js new file mode 100644 index 00000000000..8c7459f5c85 --- /dev/null +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { ExternalLink, VisuallyHidden } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * Internal dependencies + */ +import { Loading } from '../loading'; +import { Selectable } from '../selectable'; +import { SourcesList } from '../../onboarding-wizard/pages/site-scan/sources-list'; + +/** + * Renders a panel with a site scan results. + * + * @param {Object} props Component props. + * @param {Element} props.count Issues count. + * @param {Element} props.icon Panel icon. + * @param {Array} props.sources Array of issues sources data. + * @param {string} props.title Panel title. + * @param {string} props.validatedUrlsLink URL to the Validated URLs page. + */ +export function SiteScanResults( { + count, + icon, + sources, + title, + validatedUrlsLink, +} ) { + return ( + +
+ { icon } +

+ { title } + + { `(${ count })` } + +

+
+
+ { sources.length === 0 + ? + : } +

+ + { __( 'AMP Validated URLs page', 'amp' ) } + +

+
+
+ ); +} + +SiteScanResults.propTypes = { + count: PropTypes.number, + icon: PropTypes.element, + sources: PropTypes.array, + title: PropTypes.string, + validatedUrlsLink: PropTypes.string, +}; diff --git a/assets/src/components/site-scan-results/themes-with-issues.js b/assets/src/components/site-scan-results/themes-with-issues.js new file mode 100644 index 00000000000..619268cd706 --- /dev/null +++ b/assets/src/components/site-scan-results/themes-with-issues.js @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useNormalizedThemesData } from '../themes-context-provider/use-normalized-themes-data'; +import { IconWebsitePaintBrush } from '../svg/website-paint-brush'; +import { SiteScanResults } from './index'; + +/** + * Renders a list of themes that cause issues. + * + * @param {Object} props Component props. + * @param {Array} props.issues List of theme issues. + * @param {Array} props.validatedUrlsLink URL to the Validated URLs page. + */ +export function ThemesWithIssues( { issues = [], validatedUrlsLink } ) { + const themesData = useNormalizedThemesData(); + const sources = issues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; + + return ( + } + count={ issues.length } + sources={ sources } + validatedUrlsLink={ validatedUrlsLink } + /> + ); +} + +ThemesWithIssues.propTypes = { + issues: PropTypes.array.isRequired, + validatedUrlsLink: PropTypes.string, +}; diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 662ebe92e97..5775d3fc363 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import PropTypes from 'prop-types'; import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. /** @@ -9,7 +8,6 @@ import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. */ import { __ } from '@wordpress/i18n'; import { useContext } from '@wordpress/element'; -import { ExternalLink, VisuallyHidden } from '@wordpress/components'; /** * Internal dependencies @@ -17,12 +15,10 @@ import { ExternalLink, VisuallyHidden } from '@wordpress/components'; import { Selectable } from '../../../components/selectable'; import { SiteScan } from '../../../components/site-scan-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; -import { IconWebsitePaintBrush } from '../../../components/svg/website-paint-brush'; -import { IconLaptopPlug } from '../../../components/svg/laptop-plug'; -import { Loading } from '../../../components/loading'; -import { useNormalizedPluginsData } from '../../../components/plugins-context-provider/use-normalized-plugins-data'; -import { useNormalizedThemesData } from '../../../components/themes-context-provider/use-normalized-themes-data'; -import { SourcesList } from './sources-list'; +import { + PluginsWithIssues, + ThemesWithIssues, +} from '../../../components/site-scan-results'; /** * Screen with site scan summary. @@ -49,95 +45,17 @@ export function SiteScanComplete() {

{ hasThemeIssues && ( - -
- -

- { __( 'Themes with AMP incompatibilty', 'amp' ) } - - { `(${ themeIssues.length })` } - -

-
-
- -

- - { __( 'AMP Validated URLs page', 'amp' ) } - -

-
-
+ ) } { hasPluginIssues && ( - -
- -

- { __( 'Plugins with AMP incompatibility', 'amp' ) } - - { `(${ pluginIssues.length })` } - -

-
-
- -

- - { __( 'AMP Validated URLs page', 'amp' ) } - -

-
-
+ ) } ); } - -/** - * Render a list of themes that cause issues. - * - * @param {Object} props Component props. - * @param {Array} props.issues List of theme issues. - */ -function ThemesWithIssues( { issues = [] } ) { - const themesData = useNormalizedThemesData(); - const themesWithIssues = issues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; - - if ( themesWithIssues.length === 0 ) { - return ; - } - - return ; -} - -ThemesWithIssues.propTypes = { - issues: PropTypes.array.isRequired, -}; - -/** - * Render a list of plugins that cause issues. - * - * @param {Object} props Component props. - * @param {Array} props.issues List of plugins issues. - */ -function PluginsWithIssues( { issues = [] } ) { - const pluginsData = useNormalizedPluginsData(); - const pluginsWithIssues = issues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; - - if ( pluginsWithIssues.length === 0 ) { - return ; - } - - return ; -} - -PluginsWithIssues.propTypes = { - issues: PropTypes.array.isRequired, -}; From bdba45179c3150a9f2cd88642bd89380a0cabf7e Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 30 Sep 2021 18:38:02 +0200 Subject: [PATCH 032/120] Display Validated URL link only to technical users --- .../site-scan-results/site-scan-results.js | 12 +++++++----- .../onboarding-wizard/pages/site-scan/complete.js | 10 +++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js index 8c7459f5c85..5fd52e6a1fb 100644 --- a/assets/src/components/site-scan-results/site-scan-results.js +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -51,11 +51,13 @@ export function SiteScanResults( { { sources.length === 0 ? : } -

- - { __( 'AMP Validated URLs page', 'amp' ) } - -

+ { validatedUrlsLink && ( +

+ + { __( 'AMP Validated URLs page', 'amp' ) } + +

+ ) } ); diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index 5775d3fc363..cedd035ae11 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -7,13 +7,14 @@ import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useContext } from '@wordpress/element'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies */ import { Selectable } from '../../../components/selectable'; import { SiteScan } from '../../../components/site-scan-context-provider'; +import { User } from '../../../components/user-context-provider'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { PluginsWithIssues, @@ -28,6 +29,9 @@ export function SiteScanComplete() { const hasThemeIssues = themeIssues.length > 0; const hasPluginIssues = pluginIssues.length > 0; + const { developerToolsOption } = useContext( User ); + const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); + return (
@@ -47,13 +51,13 @@ export function SiteScanComplete() { { hasThemeIssues && ( ) } { hasPluginIssues && ( ) }
From 147a5f15cb5e72344433ef78d1f0f1f02e90b4e6 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 1 Oct 2021 14:11:05 +0200 Subject: [PATCH 033/120] Migrate `SiteScanResults` related CSS to component folder --- .../site-scan-results/plugins-with-issues.js | 3 +- .../site-scan-results/site-scan-results.js | 19 ++-- .../site-scan-results}/sources-list.js | 10 +- .../components/site-scan-results/style.scss | 93 +++++++++++++++++++ .../site-scan-results/themes-with-issues.js | 3 +- .../pages/site-scan/complete.js | 2 + .../pages/site-scan/style.scss | 84 ----------------- 7 files changed, 116 insertions(+), 98 deletions(-) rename assets/src/{onboarding-wizard/pages/site-scan => components/site-scan-results}/sources-list.js (77%) create mode 100644 assets/src/components/site-scan-results/style.scss diff --git a/assets/src/components/site-scan-results/plugins-with-issues.js b/assets/src/components/site-scan-results/plugins-with-issues.js index 5dd1946742c..417e54ab187 100644 --- a/assets/src/components/site-scan-results/plugins-with-issues.js +++ b/assets/src/components/site-scan-results/plugins-with-issues.js @@ -22,7 +22,7 @@ import { SiteScanResults } from './index'; * @param {Array} props.issues List of plugins issues. * @param {Array} props.validatedUrlsLink URL to the Validated URLs page. */ -export function PluginsWithIssues( { issues = [], validatedUrlsLink } ) { +export function PluginsWithIssues( { issues = [], validatedUrlsLink, ...props } ) { const pluginsData = useNormalizedPluginsData(); const sources = issues?.map( ( slug ) => pluginsData?.[ slug ] ?? { name: slug } ) || []; @@ -33,6 +33,7 @@ export function PluginsWithIssues( { issues = [], validatedUrlsLink } ) { count={ issues.length } sources={ sources } validatedUrlsLink={ validatedUrlsLink } + { ...props } /> ); } diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js index 5fd52e6a1fb..7a65a5cead3 100644 --- a/assets/src/components/site-scan-results/site-scan-results.js +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -7,20 +7,23 @@ import { __ } from '@wordpress/i18n'; /** * External dependencies */ +import classnames from 'classnames'; import PropTypes from 'prop-types'; /** * Internal dependencies */ +import './style.scss'; import { Loading } from '../loading'; import { Selectable } from '../selectable'; -import { SourcesList } from '../../onboarding-wizard/pages/site-scan/sources-list'; +import { SourcesList } from './sources-list'; /** * Renders a panel with a site scan results. * * @param {Object} props Component props. - * @param {Element} props.count Issues count. + * @param {number} props.count Issues count. + * @param {string} props.className Additional class names. * @param {Element} props.icon Panel icon. * @param {Array} props.sources Array of issues sources data. * @param {string} props.title Panel title. @@ -28,17 +31,18 @@ import { SourcesList } from '../../onboarding-wizard/pages/site-scan/sources-lis */ export function SiteScanResults( { count, + className, icon, sources, title, validatedUrlsLink, } ) { return ( - -
+ +
{ icon }

{ title } @@ -47,12 +51,12 @@ export function SiteScanResults( {

-
+
{ sources.length === 0 ? : } { validatedUrlsLink && ( -

+

{ __( 'AMP Validated URLs page', 'amp' ) } @@ -65,6 +69,7 @@ export function SiteScanResults( { SiteScanResults.propTypes = { count: PropTypes.number, + className: PropTypes.string, icon: PropTypes.element, sources: PropTypes.array, title: PropTypes.string, diff --git a/assets/src/onboarding-wizard/pages/site-scan/sources-list.js b/assets/src/components/site-scan-results/sources-list.js similarity index 77% rename from assets/src/onboarding-wizard/pages/site-scan/sources-list.js rename to assets/src/components/site-scan-results/sources-list.js index b9547b90f1a..e3eccbb03b8 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/sources-list.js +++ b/assets/src/components/site-scan-results/sources-list.js @@ -16,17 +16,17 @@ import { __, sprintf } from '@wordpress/i18n'; */ export function SourcesList( { sources } ) { return ( -

    +
      { sources.map( ( { name, author, version } ) => (
    • - + { name } { author && ( - + { sprintf( // translators: %s is an author name. __( 'by %s', 'amp' ), @@ -35,7 +35,7 @@ export function SourcesList( { sources } ) { ) } { version && ( - + { sprintf( // translators: %s is a version number. __( 'Version %s', 'amp' ), diff --git a/assets/src/components/site-scan-results/style.scss b/assets/src/components/site-scan-results/style.scss new file mode 100644 index 00000000000..78fc2db3256 --- /dev/null +++ b/assets/src/components/site-scan-results/style.scss @@ -0,0 +1,93 @@ +.site-scan-results { + padding: 0; +} + +.site-scan-results + .site-scan-results { + margin-top: 1.5rem; +} + +.site-scan-results__header { + align-items: center; + border-bottom: 1px solid var(--amp-settings-color-border); + display: flex; + flex-flow: row nowrap; + padding: 0.5rem; + + @media (min-width: 783px) { + padding: 1rem 2rem; + } +} + +.site-scan-results__heading { + font-size: 16px; + font-weight: 700; + margin-left: 1rem; + + &[data-badge-content]::after { + align-items: center; + background-color: var(--light-gray); + border-radius: 50%; + content: attr(data-badge-content); + display: inline-flex; + height: 30px; + justify-content: center; + letter-spacing: -0.05em; + margin: 0 0.5rem; + width: 30px; + } +} + +.site-scan-results__content { + padding: 1rem 0.5rem; + + @media (min-width: 783px) { + padding: 1.25rem 2rem; + } +} + +.site-scan-results__sources { + border: 2px solid var(--amp-settings-color-border); +} + +.site-scan-results__source { + align-items: center; + display: flex; + flex-flow: row nowrap; + font-size: 14px; + margin: 0; + padding: 1rem; + + &:nth-child(even) { + background-color: var(--amp-settings-color-background-light); + } + + & + & { + border-top: 2px solid var(--amp-settings-color-border); + } +} + +.site-scan-results__source-name { + font-weight: 700; +} + +.site-scan-results__source-author::before { + border-left: 1px solid; + content: ""; + display: inline-block; + height: 1em; + margin: 0 0.5em; + vertical-align: middle; +} + +.site-scan-results__source-version { + margin-left: auto; +} + +.site-scan-results__cta.site-scan-results__cta { + font-size: 14px; + margin-bottom: 0; + + .components-external-link__icon { + fill: var(--amp-settings-color-brand); + } +} diff --git a/assets/src/components/site-scan-results/themes-with-issues.js b/assets/src/components/site-scan-results/themes-with-issues.js index 619268cd706..45b5d06f42e 100644 --- a/assets/src/components/site-scan-results/themes-with-issues.js +++ b/assets/src/components/site-scan-results/themes-with-issues.js @@ -22,7 +22,7 @@ import { SiteScanResults } from './index'; * @param {Array} props.issues List of theme issues. * @param {Array} props.validatedUrlsLink URL to the Validated URLs page. */ -export function ThemesWithIssues( { issues = [], validatedUrlsLink } ) { +export function ThemesWithIssues( { issues = [], validatedUrlsLink, ...props } ) { const themesData = useNormalizedThemesData(); const sources = issues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; @@ -33,6 +33,7 @@ export function ThemesWithIssues( { issues = [], validatedUrlsLink } ) { count={ issues.length } sources={ sources } validatedUrlsLink={ validatedUrlsLink } + { ...props } /> ); } diff --git a/assets/src/onboarding-wizard/pages/site-scan/complete.js b/assets/src/onboarding-wizard/pages/site-scan/complete.js index cedd035ae11..cd87156672b 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/complete.js +++ b/assets/src/onboarding-wizard/pages/site-scan/complete.js @@ -50,12 +50,14 @@ export function SiteScanComplete() { { hasThemeIssues && ( ) } { hasPluginIssues && ( diff --git a/assets/src/onboarding-wizard/pages/site-scan/style.scss b/assets/src/onboarding-wizard/pages/site-scan/style.scss index 03430f0dded..c21749ac334 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/style.scss +++ b/assets/src/onboarding-wizard/pages/site-scan/style.scss @@ -15,88 +15,4 @@ font-size: 16px; font-weight: 700; margin-left: 2rem; - - &[data-badge-content]::after { - align-items: center; - background-color: var(--light-gray); - border-radius: 50%; - content: attr(data-badge-content); - display: inline-flex; - height: 30px; - justify-content: center; - letter-spacing: -0.05em; - margin: 0 0.5rem; - width: 30px; - } -} - -.site-scan__section--compact { - padding: 0; - - .site-scan__header { - padding: 0.5rem; - - @media (min-width: 783px) { - padding: 1rem 2rem; - } - } - - .site-scan__heading { - margin-left: 1rem; - } - - .site-scan__content { - padding: 1rem 0.5rem; - - @media (min-width: 783px) { - padding: 1.25rem 2rem; - } - } -} - -.site-scan__sources { - border: 2px solid var(--amp-settings-color-border); -} - -.site-scan__source { - align-items: center; - display: flex; - flex-flow: row nowrap; - font-size: 14px; - margin: 0; - padding: 1rem; - - &:nth-child(even) { - background-color: var(--amp-settings-color-background-light); - } - - & + & { - border-top: 2px solid var(--amp-settings-color-border); - } -} - -.site-scan__source-name { - font-weight: 700; -} - -.site-scan__source-author::before { - border-left: 1px solid; - content: ""; - display: inline-block; - height: 1em; - margin: 0 0.5em; - vertical-align: middle; -} - -.site-scan__source-version { - margin-left: auto; -} - -.site-scan__cta.site-scan__cta { - font-size: 14px; - margin-bottom: 0; - - .components-external-link__icon { - fill: var(--amp-settings-color-brand); - } } From c0f118399fe317fb94fdef35fa3ee2dd14dec981 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 1 Oct 2021 14:14:14 +0200 Subject: [PATCH 034/120] Add site scan panel on AMP settings page --- assets/src/settings-page/index.js | 16 ++- assets/src/settings-page/site-scan.js | 155 ++++++++++++++++++++++++++ assets/src/settings-page/style.css | 28 +++++ src/Admin/OptionsMenu.php | 11 ++ 4 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 assets/src/settings-page/site-scan.js diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 0e5026932db..2b43e2be2d8 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -7,6 +7,7 @@ import { HAS_DEPENDENCY_SUPPORT, OPTIONS_REST_PATH, READER_THEMES_REST_PATH, + SCANNABLE_URLS_REST_PATH, UPDATES_NONCE, USER_FIELD_DEVELOPER_TOOLS_ENABLED, USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, @@ -38,6 +39,9 @@ import { AMPDrawer } from '../components/amp-drawer'; import { AMPNotice, NOTICE_SIZE_LARGE } from '../components/amp-notice'; import { ErrorScreen } from '../components/error-screen'; import { User, UserContextProvider } from '../components/user-context-provider'; +import { PluginsContextProvider } from '../components/plugins-context-provider'; +import { ThemesContextProvider } from '../components/themes-context-provider'; +import { SiteScanContextProvider } from '../components/site-scan-context-provider'; import { Welcome } from './welcome'; import { TemplateModes } from './template-modes'; import { SupportedTemplates } from './supported-templates'; @@ -48,6 +52,7 @@ import { Analytics } from './analytics'; import { PairedUrlStructure } from './paired-url-structure'; import { MobileRedirection } from './mobile-redirection'; import { DeveloperTools } from './developer-tools'; +import { SiteScan } from './site-scan'; import { DeleteDataAtUninstall } from './delete-data-at-uninstall'; const { ajaxurl: wpAjaxUrl } = global; @@ -86,7 +91,15 @@ function Providers( { children } ) { updatesNonce={ UPDATES_NONCE } wpAjaxUrl={ wpAjaxUrl } > - { children } + + + + { children } + + + @@ -194,6 +207,7 @@ function Root( { appRoot } ) { ) } +
      diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js new file mode 100644 index 00000000000..dc6186ab1af --- /dev/null +++ b/assets/src/settings-page/site-scan.js @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { useContext, useEffect, useState } from '@wordpress/element'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { AMPDrawer } from '../components/amp-drawer'; +import { IconLandscapeHillsCogs } from '../components/svg/landscape-hills-cogs'; +import { ProgressBar } from '../components/progress-bar'; +import { PluginsWithIssues, ThemesWithIssues } from '../components/site-scan-results'; +import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; + +/** + * Site Scan component on the settings screen. + */ +export function SiteScan() { + const { + cancelSiteScan, + canScanSite, + startSiteScan, + siteScanComplete, + } = useContext( SiteScanContext ); + const [ requestSiteRescan, setRequestSiteRescan ] = useState( false ); + const [ showScanSummary, setShowScanSummary ] = useState( false ); + + /** + * Cancel scan on component unmount. + */ + useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); + + /** + * Start site scan. + */ + useEffect( () => { + if ( canScanSite && requestSiteRescan ) { + startSiteScan(); + } + }, [ canScanSite, requestSiteRescan, startSiteScan ] ); + + /** + * Show scan summary with a delay so that the progress bar has a chance to + * complete. + */ + useEffect( () => { + let delay; + + if ( siteScanComplete && ! showScanSummary ) { + delay = setTimeout( () => setShowScanSummary( true ), 500 ); + } + + return () => { + if ( delay ) { + clearTimeout( delay ); + } + }; + }, [ showScanSummary, siteScanComplete ] ); + + return ( + + + { __( 'Site Scan', 'amp' ) } + + ) } + hiddenTitle={ __( 'Site Scan', 'amp' ) } + id="site-scan" + initialOpen={ true } + > + { ( showScanSummary || ! requestSiteRescan ) + ? + : } + { ! requestSiteRescan && ( +
      + +
      + ) } +
      + ); +} + +/** + * Scan in progress screen. + */ +function SiteScanInProgress() { + const { + currentlyScannedUrlIndex, + scannableUrls, + siteScanComplete, + } = useContext( SiteScanContext ); + + return ( +
      +

      + { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

      + +

      + { siteScanComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.label, + ) + } +

      +
      + ); +} + +/** + * Scan summary screen. + */ +function SiteScanSummary() { + const { pluginIssues, themeIssues } = useContext( SiteScanContext ); + const hasThemeIssues = themeIssues.length > 0; + const hasPluginIssues = pluginIssues.length > 0; + + return ( +
      + { hasThemeIssues && ( + + ) } + { hasPluginIssues && ( + + ) } +
      + ); +} diff --git a/assets/src/settings-page/style.css b/assets/src/settings-page/style.css index da63d62e668..14b047cc294 100644 --- a/assets/src/settings-page/style.css +++ b/assets/src/settings-page/style.css @@ -367,6 +367,7 @@ li.error-kept { } } +#site-scan .amp-drawer__panel-body-inner, #site-review .amp-drawer__panel-body-inner, #plugin-suppression .amp-drawer__panel-body-inner, .amp-analytics .amp-drawer__panel-body-inner, @@ -379,6 +380,7 @@ li.error-kept { } } +#site-scan .amp-drawer__panel-body-inner > p, #site-review .amp-drawer__panel-body-inner > p, #plugin-suppression .amp-drawer__panel-body-inner > p, #paired-url-structure .amp-drawer__panel-body-inner > p, @@ -542,6 +544,32 @@ li.error-kept { width: 24px; } +/* Site Scan. */ +#site-scan { + margin-bottom: 2.5rem; +} + +#site-scan .amp-drawer__heading { + font-size: 1.2rem; +} + +#site-scan .amp-drawer__heading svg { + fill: transparent; + width: 55px; +} + +.settings-site-scan__footer { + align-items: center; + display: flex; + flex-flow: row nowrap; +} + +.amp .settings-site-scan__footer .components-button { + height: 40px; + padding-left: 2.25rem; + padding-right: 2.25rem; +} + /* Review. */ #site-review { margin-bottom: 2.5rem; diff --git a/src/Admin/OptionsMenu.php b/src/Admin/OptionsMenu.php index f22c24c0132..71977d11661 100644 --- a/src/Admin/OptionsMenu.php +++ b/src/Admin/OptionsMenu.php @@ -10,6 +10,7 @@ use AMP_Core_Theme_Sanitizer; use AMP_Options_Manager; use AMP_Theme_Support; +use AMP_Validated_URL_Post_Type; use AmpProject\AmpWP\DependencySupport; use AmpProject\AmpWP\DevTools\UserAccess; use AmpProject\AmpWP\Infrastructure\Conditional; @@ -227,6 +228,13 @@ public function enqueue_assets( $hook_suffix ) { $theme = wp_get_theme(); $is_reader_theme = $this->reader_themes->theme_data_exists( get_stylesheet() ); + $amp_validated_urls_link = admin_url( + add_query_arg( + [ 'post_type' => AMP_Validated_URL_Post_Type::POST_TYPE_SLUG ], + 'edit.php' + ) + ); + $js_data = [ 'AMP_QUERY_VAR' => amp_get_slug(), 'CURRENT_THEME' => [ @@ -240,6 +248,7 @@ public function enqueue_assets( $hook_suffix ) { 'HOME_URL' => home_url( '/' ), 'OPTIONS_REST_PATH' => '/amp/v1/options', 'READER_THEMES_REST_PATH' => '/amp/v1/reader-themes', + 'SCANNABLE_URLS_REST_PATH' => '/amp/v1/scannable-urls', 'IS_CORE_THEME' => in_array( get_stylesheet(), AMP_Core_Theme_Sanitizer::get_supported_themes(), @@ -253,6 +262,7 @@ public function enqueue_assets( $hook_suffix ) { 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE' => UserRESTEndpointExtension::USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', + 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, 'HAS_PAGE_CACHING' => $this->site_health->has_page_caching( true ), ]; @@ -309,6 +319,7 @@ protected function add_preload_rest_paths() { $paths = [ '/amp/v1/options', '/amp/v1/reader-themes', + '/amp/v1/scannable-urls', '/wp/v2/settings', '/wp/v2/users/me', ]; From 9fa914669499149ab1b8fc801d0a2aa9010ceca0 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 1 Oct 2021 14:21:43 +0200 Subject: [PATCH 035/120] Use correct icon in Site Scan panel --- .../svg/landscape-hills-cogs-alt.js | 38 +++++++++++++++++++ assets/src/settings-page/site-scan.js | 4 +- 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 assets/src/components/svg/landscape-hills-cogs-alt.js diff --git a/assets/src/components/svg/landscape-hills-cogs-alt.js b/assets/src/components/svg/landscape-hills-cogs-alt.js new file mode 100644 index 00000000000..15dd7a74caf --- /dev/null +++ b/assets/src/components/svg/landscape-hills-cogs-alt.js @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; + +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; + +const INTRINSIC_ICON_WIDTH = 59; +const INTRINSIC_ICON_HEIGHT = 44; +const INTRINSIC_STROKE_WIDTH = 2; + +export function IconLandscapeHillsCogsAlt( { width = INTRINSIC_ICON_WIDTH, ...props } ) { + const clipPathId = `clip-icon-landscape-hills-cogs-alt-${ useInstanceId( IconLandscapeHillsCogsAlt ) }`; + const strokeWidth = INTRINSIC_STROKE_WIDTH * ( INTRINSIC_ICON_WIDTH / width ); + + return ( + + + + + + + + + + + + + + + ); +} +IconLandscapeHillsCogsAlt.propTypes = { + width: PropTypes.number, +}; diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index dc6186ab1af..12c969c4e9e 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -14,7 +14,7 @@ import { Button } from '@wordpress/components'; * Internal dependencies */ import { AMPDrawer } from '../components/amp-drawer'; -import { IconLandscapeHillsCogs } from '../components/svg/landscape-hills-cogs'; +import { IconLandscapeHillsCogsAlt } from '../components/svg/landscape-hills-cogs-alt'; import { ProgressBar } from '../components/progress-bar'; import { PluginsWithIssues, ThemesWithIssues } from '../components/site-scan-results'; import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; @@ -68,7 +68,7 @@ export function SiteScan() { - + { __( 'Site Scan', 'amp' ) } ) } From e2b6f143140a81880b667197b33008f7ed1526fd Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 6 Oct 2021 12:31:48 +0200 Subject: [PATCH 036/120] Reverse order of template mode recommendations in Onboarding Wizard --- .../pages/template-mode/screen-ui.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 6f1541a1039..2470d04aeb7 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -63,14 +63,14 @@ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOptio return ( - - { sectionText.standard.compatibility } + + { sectionText.reader.compatibility } @@ -87,14 +87,14 @@ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOptio - - { sectionText.reader.compatibility } + + { sectionText.standard.compatibility } From 58d1b48d31d1cac981d21fafe3b8eda0c6768714 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 6 Oct 2021 12:49:00 +0200 Subject: [PATCH 037/120] Add header copy and links to Template Modes step --- assets/src/onboarding-wizard/pages/index.js | 1 + .../pages/template-mode/index.js | 45 +++++++++++++------ .../pages/template-mode/style.scss | 3 ++ 3 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 assets/src/onboarding-wizard/pages/template-mode/style.scss diff --git a/assets/src/onboarding-wizard/pages/index.js b/assets/src/onboarding-wizard/pages/index.js index 888ed663f02..64e5bee4001 100644 --- a/assets/src/onboarding-wizard/pages/index.js +++ b/assets/src/onboarding-wizard/pages/index.js @@ -38,6 +38,7 @@ export const PAGES = [ slug: 'template-modes', title: __( 'Template Modes', 'amp' ), PageComponent: TemplateMode, + showTitle: false, }, { slug: 'theme-selection', diff --git a/assets/src/onboarding-wizard/pages/template-mode/index.js b/assets/src/onboarding-wizard/pages/template-mode/index.js index 01a199e5ff1..904f57aa886 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/index.js +++ b/assets/src/onboarding-wizard/pages/template-mode/index.js @@ -2,9 +2,12 @@ * WordPress dependencies */ import { useEffect, useContext } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + /** * Internal dependencies */ +import './style.scss'; import { Navigation } from '../../components/navigation-context-provider'; import { ReaderThemes } from '../../../components/reader-themes-context-provider'; import { SiteScan } from '../../../components/site-scan-context-provider'; @@ -37,18 +40,34 @@ export function TemplateMode() { // The actual display component should avoid using global context directly. This will facilitate developing and testing the UI using different options. return ( - { - updateOptions( { theme_support: mode } ); - } } - technicalQuestionChanged={ technicalQuestionChangedAtLeastOnce } - themeIssues={ themeIssues } - /> +
      +
      +

      + { __( 'Template Modes', 'amp' ) } +

      + { /* dangerouslySetInnerHTML reason: Injection of links. */ } +

      AMP experience with different modes and availability of AMP components in the ecosystem.', 'amp' ), + 'https://amp-wp.org/documentation/getting-started/template-modes/', + 'https://amp-wp.org/ecosystem/', + ), + } } /> +

      + { + updateOptions( { theme_support: mode } ); + } } + technicalQuestionChanged={ technicalQuestionChangedAtLeastOnce } + themeIssues={ themeIssues } + /> +
      ); } diff --git a/assets/src/onboarding-wizard/pages/template-mode/style.scss b/assets/src/onboarding-wizard/pages/template-mode/style.scss new file mode 100644 index 00000000000..10ca8f0cce7 --- /dev/null +++ b/assets/src/onboarding-wizard/pages/template-mode/style.scss @@ -0,0 +1,3 @@ +.template-modes__header { + margin-bottom: 1.75rem; +} From 695fe0313543a88f60b7d5a20af3c7ebc95ce70f Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 6 Oct 2021 18:17:33 +0200 Subject: [PATCH 038/120] Update CSS and copy of template mode selection Wizard step --- .../components/template-mode-option/index.js | 48 +++- .../components/template-mode-option/style.css | 31 +-- .../template-mode/get-selection-details.js | 263 ++++++++---------- .../pages/template-mode/screen-ui.js | 106 ++++--- .../test/get-selection-details.js | 28 +- 5 files changed, 228 insertions(+), 248 deletions(-) diff --git a/assets/src/components/template-mode-option/index.js b/assets/src/components/template-mode-option/index.js index 417b158ae60..5ff86ddcfdf 100644 --- a/assets/src/components/template-mode-option/index.js +++ b/assets/src/components/template-mode-option/index.js @@ -12,16 +12,15 @@ import { useContext } from '@wordpress/element'; /** * Internal dependencies */ +import './style.css'; +import { READER, STANDARD, TRANSITIONAL } from '../../common/constants'; +import { AMPDrawer, HANDLE_TYPE_RIGHT } from '../amp-drawer'; import { AMPInfo } from '../amp-info'; import { Standard } from '../svg/standard'; import { Transitional } from '../svg/transitional'; import { Reader } from '../svg/reader'; import { Options } from '../options-context-provider'; -import './style.css'; -import { READER, STANDARD, TRANSITIONAL } from '../../common/constants'; -import { AMPDrawer, HANDLE_TYPE_RIGHT } from '../amp-drawer'; - /** * Mode-specific illustration. * @@ -82,7 +81,7 @@ export function getId( mode ) { * * @param {Object} props Component props. * @param {string|Object} props.children Section content. - * @param {string} props.details Mode details. + * @param {string|Array} props.details The template mode details. * @param {string} props.detailsUrl Mode details URL. * @param {string} props.mode The template mode. * @param {boolean} props.previouslySelected Optional. Whether the option was selected previously. @@ -137,13 +136,31 @@ export function TemplateModeOption( { children, details, detailsUrl, initialOpen selected={ mode === themeSupport } >
      -

      - - { ' ' } - - { __( 'Learn more.', 'amp' ) } - -

      + { Array.isArray( details ) && ( +
        + { details.map( ( detail, index ) => ( +
      • + ) ) } +
      + ) } + { ! Array.isArray( details ) && ( +

      + + { detailsUrl && ( + <> + { ' ' } + + { __( 'Learn more.', 'amp' ) } + + + ) } +

      + ) } { children }
      @@ -152,8 +169,11 @@ export function TemplateModeOption( { children, details, detailsUrl, initialOpen TemplateModeOption.propTypes = { children: PropTypes.any, - details: PropTypes.string.isRequired, - detailsUrl: PropTypes.string.isRequired, + details: PropTypes.oneOfType( [ + PropTypes.string, + PropTypes.arrayOf( PropTypes.string ), + ] ).isRequired, + detailsUrl: PropTypes.string, initialOpen: PropTypes.bool, labelExtra: PropTypes.node, mode: PropTypes.oneOf( [ READER, STANDARD, TRANSITIONAL ] ).isRequired, diff --git a/assets/src/components/template-mode-option/style.css b/assets/src/components/template-mode-option/style.css index b9a99817037..7756cab9e6f 100644 --- a/assets/src/components/template-mode-option/style.css +++ b/assets/src/components/template-mode-option/style.css @@ -1,11 +1,3 @@ -.template-mode-selection__details { - padding: 1rem 1.5rem; - - @media (min-width: 783px) { - padding: 1rem 3rem; - } -} - .template-mode-option__label { align-items: center; background-color: var(--amp-settings-color-background); @@ -19,16 +11,6 @@ } } - -.template-mode-selection__illustration svg { - height: auto; - width: 45px; - - @media (min-width: 783px) { - width: 66px; - } -} - .template-mode-selection__input-container { margin-right: 0.75rem; @@ -54,7 +36,7 @@ } .template-mode-option .amp-info { - margin-bottom: 0.5rem; + margin-bottom: 0; @media screen and (min-width: 783px) { margin-left: 1.5rem; @@ -62,7 +44,18 @@ } .template-mode-selection__details { + font-size: 14px; margin-bottom: 1rem; + padding: 1rem 1.5rem; + + @media (min-width: 783px) { + padding: 1rem 3rem; + } +} + +.template-mode-selection__details-list { + list-style: disc; + padding-left: 1.625rem; } .template-mode-option .components-panel__row { diff --git a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js b/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js index a27f065e513..d02b7710b7b 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js +++ b/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js @@ -1,24 +1,18 @@ /* eslint-disable complexity */ -/* eslint-disable jsdoc/check-param-names */ /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import isShallowEqual from '@wordpress/is-shallow-equal'; /** * Internal dependencies */ import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; -// Sections. -export const COMPATIBILITY = 'compatibility'; -export const DETAILS = 'details'; - // Recommendation levels. -export const MOST_RECOMMENDED = 'mostRecommended'; -export const NOT_RECOMMENDED = 'notRecommended'; export const RECOMMENDED = 'recommended'; +export const NEUTRAL = 'neutral'; +export const NOT_RECOMMENDED = 'notRecommended'; // Technical levels. export const TECHNICAL = 'technical'; @@ -37,63 +31,146 @@ export const NON_TECHNICAL = 'nonTechnical'; export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, userIsTechnical, hasPluginIssues, hasThemeIssues, hasScanResults = true } ) { // Handle case where scanning has failed or did not run. if ( ! hasScanResults ) { - if ( userIsTechnical ) { - return { - [ READER ]: currentThemeIsAmongReaderThemes ? MOST_RECOMMENDED : RECOMMENDED, - [ STANDARD ]: RECOMMENDED, - [ TRANSITIONAL ]: currentThemeIsAmongReaderThemes ? MOST_RECOMMENDED : RECOMMENDED, - }; - } return { - [ READER ]: MOST_RECOMMENDED, - [ STANDARD ]: RECOMMENDED, - [ TRANSITIONAL ]: currentThemeIsAmongReaderThemes ? MOST_RECOMMENDED : RECOMMENDED, + [ READER ]: { + level: ( userIsTechnical || currentThemeIsAmongReaderThemes ) ? RECOMMENDED : NEUTRAL, + details: [ + __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: currentThemeIsAmongReaderThemes ? RECOMMENDED : NEUTRAL, + details: [ + __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: NEUTRAL, + details: [ + __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), + ], + }, }; } switch ( true ) { case hasThemeIssues && hasPluginIssues && userIsTechnical: case hasThemeIssues && ! hasPluginIssues && userIsTechnical: + case ! hasThemeIssues && hasPluginIssues && userIsTechnical: return { - [ READER ]: MOST_RECOMMENDED, - [ STANDARD ]: NOT_RECOMMENDED, - [ TRANSITIONAL ]: RECOMMENDED, + [ READER ]: { + level: NEUTRAL, + details: [ + __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: NEUTRAL, + details: [ + __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: RECOMMENDED, + details: [ + __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), + __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), + ], + }, }; case hasThemeIssues && hasPluginIssues && ! userIsTechnical: - case hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: - return { - [ READER ]: MOST_RECOMMENDED, - [ STANDARD ]: NOT_RECOMMENDED, - [ TRANSITIONAL ]: NOT_RECOMMENDED, - }; - - case ! hasThemeIssues && hasPluginIssues && userIsTechnical: + case ! hasThemeIssues && hasPluginIssues && ! userIsTechnical: return { - [ READER ]: NOT_RECOMMENDED, - [ STANDARD ]: NOT_RECOMMENDED, - [ TRANSITIONAL ]: MOST_RECOMMENDED, + [ READER ]: { + level: RECOMMENDED, + details: [ + __( 'Recommended as an easy way to enable AMP on your site despite the issues detected during site scanning.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, }; - case ! hasThemeIssues && hasPluginIssues && ! userIsTechnical: + case hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: return { - [ READER ]: NOT_RECOMMENDED, - [ STANDARD ]: NOT_RECOMMENDED, - [ TRANSITIONAL ]: MOST_RECOMMENDED, + [ READER ]: { + level: RECOMMENDED, + details: [ + __( 'Recommended to easily enable AMP on your site despite the issues detected on your theme.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: NEUTRAL, + details: [ + __( 'Choose this mode if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, }; case ! hasThemeIssues && ! hasPluginIssues && userIsTechnical: return { - [ READER ]: NOT_RECOMMENDED, - [ STANDARD ]: MOST_RECOMMENDED, - [ TRANSITIONAL ]: NOT_RECOMMENDED, + [ READER ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: RECOMMENDED, + details: [ + __( 'Recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, }; case ! hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: return { - [ READER ]: NOT_RECOMMENDED, - [ STANDARD ]: NOT_RECOMMENDED, - [ TRANSITIONAL ]: MOST_RECOMMENDED, + [ READER ]: { + level: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + level: NEUTRAL, + details: [ + __( 'Recommended choice if you can’t commit to choosing plugins that are AMP compatible when extending your site. This mode will make it easy to keep AMP content even if non-AMP-compatible plugins are used later on.', 'amp' ), + ], + }, + [ STANDARD ]: { + level: NEUTRAL, + details: [ + __( 'Recommended choice if you can commit to always choosing plugins that are AMP compatible when extending your site.', 'amp' ), + ], + }, }; default: { @@ -102,106 +179,4 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user } } -/** - * Provides details on copy and UI for the template modes screen. - * - * @param {Array} args Function args. - * @param {string} section The section for which to provide text. - * @param {string} mode The mode to generate text for. - * @param {string} recommendationLevel String representing whether the mode is not recommended, recommended, or most recommended. - * @param {string} technicalLevel String representing whether the user is technical. - */ -export function getSelectionText( ...args ) { - const match = ( ...test ) => isShallowEqual( test, args ); - - switch ( true ) { - case match( COMPATIBILITY, READER, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, READER, MOST_RECOMMENDED, TECHNICAL ): - return __( 'Reader mode is the best choice if you don\'t have a technical background or would like a simpler setup.', 'amp' ); - - case match( COMPATIBILITY, READER, RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, READER, RECOMMENDED, TECHNICAL ): - return __( 'Reader mode makes it easy to bring AMP content to your site, but your site will use two different themes.', 'amp' ); - - case match( COMPATIBILITY, READER, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, READER, NOT_RECOMMENDED, TECHNICAL ): - case match( COMPATIBILITY, TRANSITIONAL, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, TRANSITIONAL, NOT_RECOMMENDED, TECHNICAL ): - return __( 'There is no reason to use this mode, as you have an AMP-compatible theme that you can use for both the non-AMP and AMP versions of your site.', 'amp' ); - - case match( COMPATIBILITY, STANDARD, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, STANDARD, NOT_RECOMMENDED, TECHNICAL ): - return __( 'Standard mode is not recommended as key functionality may be missing and development work might be required. ', 'amp' ); - - case match( COMPATIBILITY, STANDARD, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, STANDARD, MOST_RECOMMENDED, TECHNICAL ): - return __( 'Standard mode is the best choice for your site because you are using an AMP-compatible theme and no plugin issues were detected.', 'amp' ); - - case match( COMPATIBILITY, TRANSITIONAL, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, TRANSITIONAL, MOST_RECOMMENDED, TECHNICAL ): - return __( 'Transitional mode is recommended because it makes it easy to keep your content as valid AMP even if non-AMP-compatible plugins are installed later.', 'amp' ); - - case match( COMPATIBILITY, TRANSITIONAL, RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, TRANSITIONAL, RECOMMENDED, TECHNICAL ): - return __( 'Transitional mode is a good choice if you are willing and able to address any issues around AMP-compatibility that may arise as your site evolves.', 'amp' ); - - case match( DETAILS, READER, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, READER, NOT_RECOMMENDED, TECHNICAL ): - case match( DETAILS, READER, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, READER, MOST_RECOMMENDED, TECHNICAL ): - case match( DETAILS, READER, RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, READER, RECOMMENDED, TECHNICAL ): - return __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ); - - case match( DETAILS, TRANSITIONAL, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, TRANSITIONAL, NOT_RECOMMENDED, TECHNICAL ): - case match( DETAILS, TRANSITIONAL, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, TRANSITIONAL, MOST_RECOMMENDED, TECHNICAL ): - case match( DETAILS, TRANSITIONAL, RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, TRANSITIONAL, RECOMMENDED, TECHNICAL ): - return __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content. ', 'amp' ); - - case match( DETAILS, STANDARD, RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, STANDARD, RECOMMENDED, TECHNICAL ): - case match( DETAILS, STANDARD, MOST_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, STANDARD, MOST_RECOMMENDED, TECHNICAL ): - case match( DETAILS, STANDARD, NOT_RECOMMENDED, NON_TECHNICAL ): - case match( DETAILS, STANDARD, NOT_RECOMMENDED, TECHNICAL ): - return __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme. ', 'amp' ); - - // Cases potentially never used. - case match( COMPATIBILITY, STANDARD, RECOMMENDED, NON_TECHNICAL ): - case match( COMPATIBILITY, STANDARD, RECOMMENDED, TECHNICAL ): - return 'Standard mode is a good choice if your site uses an AMP-compatible theme and only uses AMP-compatible plugins. If you\'re not sure of the compatibility of your themes and plugins, Reader mode may be a better option.'; - - default: { - throw new Error( __( 'A selection text recommendation was not accounted for. ', 'amp' ) + JSON.stringify( args ) ); - } - } -} - -/** - * Gets all the selection text for the ScreenUI component. - * - * @param {Object} recommendationLevels Result of getRecommendationLevels. - * @param {string} technicalLevel A technical level. - */ -export function getAllSelectionText( recommendationLevels, technicalLevel ) { - return { - [ READER ]: { - [ COMPATIBILITY ]: getSelectionText( COMPATIBILITY, READER, recommendationLevels[ READER ], technicalLevel ), - [ DETAILS ]: getSelectionText( DETAILS, READER, recommendationLevels[ READER ], technicalLevel ), - }, - [ STANDARD ]: { - [ COMPATIBILITY ]: getSelectionText( COMPATIBILITY, STANDARD, recommendationLevels[ STANDARD ], technicalLevel ), - [ DETAILS ]: getSelectionText( DETAILS, STANDARD, recommendationLevels[ STANDARD ], technicalLevel ), - }, - [ TRANSITIONAL ]: { - [ COMPATIBILITY ]: getSelectionText( COMPATIBILITY, TRANSITIONAL, recommendationLevels[ TRANSITIONAL ], technicalLevel ), - [ DETAILS ]: getSelectionText( DETAILS, TRANSITIONAL, recommendationLevels[ TRANSITIONAL ], technicalLevel ), - }, - }; -} - /* eslint-enable complexity */ -/* eslint-enable jsdoc/check-param-names */ diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 2470d04aeb7..4f45c0086ea 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; /** * External dependencies @@ -12,10 +13,57 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ -import { AMPNotice, NOTICE_TYPE_SUCCESS, NOTICE_TYPE_INFO, NOTICE_TYPE_ERROR, NOTICE_SIZE_LARGE } from '../../../components/amp-notice'; +import { + AMPNotice, + NOTICE_TYPE_SUCCESS, + NOTICE_SIZE_SMALL, +} from '../../../components/amp-notice'; import { TemplateModeOption } from '../../../components/template-mode-option'; import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; -import { MOST_RECOMMENDED, RECOMMENDED, getRecommendationLevels, getAllSelectionText, TECHNICAL, NON_TECHNICAL } from './get-selection-details'; +import { + RECOMMENDED, + NOT_RECOMMENDED, + getRecommendationLevels, +} from './get-selection-details'; + +/** + * Small notice indicating a mode is recommended. + */ +function RecommendedNotice() { + return ( + + { __( 'Recommended', 'amp' ) } + + ); +} + +/** + * Determine if a template mode option should be initially open. + * + * @param {string} mode Template mode to check. + * @param {Array} recommendationLevels Recommendation levels. + * @param {string} savedCurrentMode Currently saved template mode. + */ +function isInitiallyOpen( mode, recommendationLevels, savedCurrentMode ) { + if ( savedCurrentMode === mode ) { + return true; + } + + switch ( recommendationLevels[ mode ].level ) { + case RECOMMENDED: + return true; + + case NOT_RECOMMENDED: + return false; + + /** + * For NEUTRAL, the option should be initially open if no other mode is + * RECOMMENDED. + */ + default: + return ! Boolean( Object.values( recommendationLevels ).find( ( item ) => item.level === RECOMMENDED ) ); + } +} /** * The interface for the mode selection screen. Avoids using context for easier testing. @@ -42,61 +90,31 @@ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOptio }, ), [ currentThemeIsAmongReaderThemes, themeIssues, pluginIssues, userIsTechnical ] ); - const sectionText = useMemo( - () => getAllSelectionText( recommendationLevels, userIsTechnical ? TECHNICAL : NON_TECHNICAL ), - [ recommendationLevels, userIsTechnical ], - ); - - const getRecommendationLevelType = ( recommended ) => { - switch ( recommended ) { - case MOST_RECOMMENDED: - return NOTICE_TYPE_SUCCESS; - - case RECOMMENDED: - return NOTICE_TYPE_INFO; - - default: - return NOTICE_TYPE_ERROR; - } - }; - return (
      - - { sectionText.reader.compatibility } - - + labelExtra={ recommendationLevels[ READER ].level === RECOMMENDED ? : null } + /> - - { sectionText.transitional.compatibility } - - + labelExtra={ recommendationLevels[ TRANSITIONAL ].level === RECOMMENDED ? : null } + /> - - { sectionText.standard.compatibility } - - + labelExtra={ recommendationLevels[ STANDARD ].level === RECOMMENDED ? : null } + /> ); } diff --git a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js b/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js index 0df4f8e30d2..f7def87b08f 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js +++ b/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js @@ -1,18 +1,7 @@ /** * Internal dependencies */ -import { - getRecommendationLevels, - getSelectionText, - COMPATIBILITY, - DETAILS, - MOST_RECOMMENDED, - NOT_RECOMMENDED, - RECOMMENDED, - TECHNICAL, - NON_TECHNICAL, -} from '../get-selection-details'; -import { READER, STANDARD, TRANSITIONAL } from '../../../../common/constants'; +import { getRecommendationLevels } from '../get-selection-details'; describe( 'getRecommendationLevels', () => { it( 'throws no errors', () => { @@ -26,18 +15,3 @@ describe( 'getRecommendationLevels', () => { } ); } ); } ); - -describe( 'getSelectionText', () => { - it( 'throws no errors', () => { - [ COMPATIBILITY, DETAILS ].forEach( ( section ) => { - [ READER, STANDARD, TRANSITIONAL ].forEach( ( mode ) => { - [ MOST_RECOMMENDED, NOT_RECOMMENDED, RECOMMENDED ].forEach( ( recommendationLevel ) => { - [ NON_TECHNICAL, TECHNICAL ].forEach( ( technicalLevel ) => { - const cb = () => getSelectionText( section, mode, recommendationLevel, technicalLevel ); - expect( cb ).not.toThrow(); - } ); - } ); - } ); - } ); - } ); -} ); From 6e47aa6ddba2678027bca094d2ca03430c4a2d1e Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 6 Oct 2021 18:24:55 +0200 Subject: [PATCH 039/120] Update function and prop names related to template mode selection --- .../template-mode/get-selection-details.js | 38 +++++++++---------- .../pages/template-mode/screen-ui.js | 34 ++++++++--------- .../test/get-selection-details.js | 6 +-- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js b/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js index d02b7710b7b..f9fb34c3dcb 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js +++ b/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js @@ -28,24 +28,24 @@ export const NON_TECHNICAL = 'nonTechnical'; * @param {boolean} args.hasThemeIssues Whether the site scan found theme issues. * @param {boolean} args.hasScanResults Whether there are available scan results. */ -export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, userIsTechnical, hasPluginIssues, hasThemeIssues, hasScanResults = true } ) { +export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTechnical, hasPluginIssues, hasThemeIssues, hasScanResults = true } ) { // Handle case where scanning has failed or did not run. if ( ! hasScanResults ) { return { [ READER ]: { - level: ( userIsTechnical || currentThemeIsAmongReaderThemes ) ? RECOMMENDED : NEUTRAL, + recommendationLevel: ( userIsTechnical || currentThemeIsAmongReaderThemes ) ? RECOMMENDED : NEUTRAL, details: [ __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: currentThemeIsAmongReaderThemes ? RECOMMENDED : NEUTRAL, + recommendationLevel: currentThemeIsAmongReaderThemes ? RECOMMENDED : NEUTRAL, details: [ __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), ], }, [ STANDARD ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), ], @@ -59,21 +59,21 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user case ! hasThemeIssues && hasPluginIssues && userIsTechnical: return { [ READER ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), ], }, [ STANDARD ]: { - level: RECOMMENDED, + recommendationLevel: RECOMMENDED, details: [ __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), @@ -85,20 +85,20 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user case ! hasThemeIssues && hasPluginIssues && ! userIsTechnical: return { [ READER ]: { - level: RECOMMENDED, + recommendationLevel: RECOMMENDED, details: [ __( 'Recommended as an easy way to enable AMP on your site despite the issues detected during site scanning.', 'amp' ), __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), ], }, [ STANDARD ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), ], @@ -108,21 +108,21 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user case hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: return { [ READER ]: { - level: RECOMMENDED, + recommendationLevel: RECOMMENDED, details: [ __( 'Recommended to easily enable AMP on your site despite the issues detected on your theme.', 'amp' ), __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'Choose this mode if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), ], }, [ STANDARD ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), ], @@ -132,19 +132,19 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user case ! hasThemeIssues && ! hasPluginIssues && userIsTechnical: return { [ READER ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), ], }, [ STANDARD ]: { - level: RECOMMENDED, + recommendationLevel: RECOMMENDED, details: [ __( 'Recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), ], @@ -154,19 +154,19 @@ export function getRecommendationLevels( { currentThemeIsAmongReaderThemes, user case ! hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: return { [ READER ]: { - level: NOT_RECOMMENDED, + recommendationLevel: NOT_RECOMMENDED, details: [ __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), ], }, [ TRANSITIONAL ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'Recommended choice if you can’t commit to choosing plugins that are AMP compatible when extending your site. This mode will make it easy to keep AMP content even if non-AMP-compatible plugins are used later on.', 'amp' ), ], }, [ STANDARD ]: { - level: NEUTRAL, + recommendationLevel: NEUTRAL, details: [ __( 'Recommended choice if you can commit to always choosing plugins that are AMP compatible when extending your site.', 'amp' ), ], diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 4f45c0086ea..501079af06c 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -23,7 +23,7 @@ import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; import { RECOMMENDED, NOT_RECOMMENDED, - getRecommendationLevels, + getSelectionDetails, } from './get-selection-details'; /** @@ -40,16 +40,16 @@ function RecommendedNotice() { /** * Determine if a template mode option should be initially open. * - * @param {string} mode Template mode to check. - * @param {Array} recommendationLevels Recommendation levels. - * @param {string} savedCurrentMode Currently saved template mode. + * @param {string} mode Template mode to check. + * @param {Array} selectionDetails Selection details. + * @param {string} savedCurrentMode Currently saved template mode. */ -function isInitiallyOpen( mode, recommendationLevels, savedCurrentMode ) { +function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { if ( savedCurrentMode === mode ) { return true; } - switch ( recommendationLevels[ mode ].level ) { + switch ( selectionDetails[ mode ].recommendationLevel ) { case RECOMMENDED: return true; @@ -61,7 +61,7 @@ function isInitiallyOpen( mode, recommendationLevels, savedCurrentMode ) { * RECOMMENDED. */ default: - return ! Boolean( Object.values( recommendationLevels ).find( ( item ) => item.level === RECOMMENDED ) ); + return ! Boolean( Object.values( selectionDetails ).find( ( item ) => item.recommendationLevel === RECOMMENDED ) ); } } @@ -80,7 +80,7 @@ function isInitiallyOpen( mode, recommendationLevels, savedCurrentMode ) { export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOption, firstTimeInWizard, technicalQuestionChanged, pluginIssues, savedCurrentMode, themeIssues } ) { const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); - const recommendationLevels = useMemo( () => getRecommendationLevels( + const selectionDetails = useMemo( () => getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTechnical, @@ -93,27 +93,27 @@ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOptio return (
      : null } + labelExtra={ selectionDetails[ READER ].recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ selectionDetails[ TRANSITIONAL ].recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ selectionDetails[ STANDARD ].recommendationLevel === RECOMMENDED ? : null } /> ); diff --git a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js b/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js index f7def87b08f..ee025282570 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js +++ b/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js @@ -1,14 +1,14 @@ /** * Internal dependencies */ -import { getRecommendationLevels } from '../get-selection-details'; +import { getSelectionDetails } from '../get-selection-details'; -describe( 'getRecommendationLevels', () => { +describe( 'getSelectionDetails', () => { it( 'throws no errors', () => { [ true, false ].forEach( ( hasPluginIssues ) => { [ true, false ].forEach( ( hasThemeIssues ) => { [ true, false ].forEach( ( userIsTechnical ) => { - const cb = () => getRecommendationLevels( { hasPluginIssues, hasThemeIssues, userIsTechnical } ); + const cb = () => getSelectionDetails( { hasPluginIssues, hasThemeIssues, userIsTechnical } ); expect( cb ).not.toThrow(); } ); } ); From a300ffb5e12cb00116e62e35fd88567be86b4912 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 7 Oct 2021 15:11:37 +0200 Subject: [PATCH 040/120] Construct AMP validation URL on the client-side - Use `amp-first` query arg to force validation in Standard mode - Use `omit_stylesheet` and `cache` flags --- .../site-scan-context-provider/index.js | 26 ++++++++++++++-- assets/src/onboarding-wizard/index.js | 4 +++ assets/src/settings-page/index.js | 4 +++ src/Admin/OnboardingWizardSubmenuPage.php | 3 ++ src/Admin/OptionsMenu.php | 3 ++ .../ScannableURLsRestController.php | 30 +++++-------------- 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 1f76734af5d..6f36c1c92af 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -3,6 +3,7 @@ */ import { createContext, useCallback, useEffect, useRef, useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; /** * External dependencies @@ -29,10 +30,14 @@ const SITE_SCAN_STATE_COMPLETE = 'SITE_SCAN_STATE_COMPLETE'; * @param {Object} props Component props. * @param {?any} props.children Component children. * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. + * @param {string} props.validateNonce The AMP validate nonce. + * @param {string} props.validateQueryVar The AMP validate query variable name. */ export function SiteScanContextProvider( { children, scannableUrlsRestPath, + validateNonce, + validateQueryVar, } ) { const [ themeIssues, setThemeIssues ] = useState( [] ); const [ pluginIssues, setPluginIssues ] = useState( [] ); @@ -98,6 +103,10 @@ export function SiteScanContextProvider( { * Scan site URLs sequentially. */ useEffect( () => { + if ( ! validateQueryVar || ! validateNonce ) { + return; + } + if ( siteScanState !== SITE_SCAN_STATE_IDLE ) { return; } @@ -109,8 +118,17 @@ export function SiteScanContextProvider( { setSiteScanState( SITE_SCAN_STATE_IN_PROGRESS ); try { - const { validate_url: validateUrl } = scannableUrls[ currentlyScannedUrlIndex ]; - const validationResults = await apiFetch( { url: validateUrl } ); + const { url } = scannableUrls[ currentlyScannedUrlIndex ]; + const validationResults = await apiFetch( { + url: addQueryArgs( url, { + 'amp-first': true, + [ validateQueryVar ]: { + nonce: validateNonce, + omit_stylesheets: true, + cache: true, + }, + } ), + } ); if ( true === hasUnmounted.current ) { return; @@ -146,7 +164,7 @@ export function SiteScanContextProvider( { setSiteScanState( SITE_SCAN_STATE_IDLE ); } } )(); - }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, siteScanState ] ); + }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, siteScanState, validateNonce, validateQueryVar ] ); return ( { children } diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 2b43e2be2d8..0cb699115ed 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -12,6 +12,8 @@ import { USER_FIELD_DEVELOPER_TOOLS_ENABLED, USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, USERS_RESOURCE_REST_PATH, + VALIDATE_NONCE, + VALIDATE_QUERY_VAR, } from 'amp-settings'; /** @@ -95,6 +97,8 @@ function Providers( { children } ) { { children } diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index ebb89472be1..88c3e41a70f 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -10,6 +10,7 @@ use AMP_Options_Manager; use AMP_Validated_URL_Post_Type; +use AMP_Validation_Manager; use AmpProject\AmpWP\DevTools\UserAccess; use AmpProject\AmpWP\Infrastructure\Delayed; use AmpProject\AmpWP\Infrastructure\Registerable; @@ -252,6 +253,8 @@ public function enqueue_assets( $hook_suffix ) { 'UPDATES_NONCE' => wp_create_nonce( 'updates' ), 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', + 'VALIDATE_NONCE' => AMP_Validation_Manager::get_amp_validate_nonce(), + 'VALIDATE_QUERY_VAR' => AMP_Validation_Manager::VALIDATE_QUERY_VAR, 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, ]; diff --git a/src/Admin/OptionsMenu.php b/src/Admin/OptionsMenu.php index 71977d11661..e9f9324daeb 100644 --- a/src/Admin/OptionsMenu.php +++ b/src/Admin/OptionsMenu.php @@ -11,6 +11,7 @@ use AMP_Options_Manager; use AMP_Theme_Support; use AMP_Validated_URL_Post_Type; +use AMP_Validation_Manager; use AmpProject\AmpWP\DependencySupport; use AmpProject\AmpWP\DevTools\UserAccess; use AmpProject\AmpWP\Infrastructure\Conditional; @@ -262,6 +263,8 @@ public function enqueue_assets( $hook_suffix ) { 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE' => UserRESTEndpointExtension::USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', + 'VALIDATE_NONCE' => AMP_Validation_Manager::get_amp_validate_nonce(), + 'VALIDATE_QUERY_VAR' => AMP_Validation_Manager::VALIDATE_QUERY_VAR, 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, 'HAS_PAGE_CACHING' => $this->site_health->has_page_caching( true ), ]; diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index 0097793cfd0..17d67c1a5de 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -8,7 +8,6 @@ namespace AmpProject\AmpWP\Validation; -use AMP_Validation_Manager; use WP_Error; use WP_REST_Controller; use AmpProject\AmpWP\Infrastructure\Delayed; @@ -73,26 +72,18 @@ public function register() { /** * Retrieves a list of scannable URLs. * - * Each item contains a page `type` (e.g. 'home' or 'search') and a - * `validate_url` prop for accessing validation data for a given URL. + * Besides the page URL, each item contains a page `type` (e.g. 'home' or + * 'search') and a URL to a corresponding AMP page (`amp_url`). * * @param WP_REST_Request $request Full details about the request. * * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - $nonce = AMP_Validation_Manager::get_amp_validate_nonce(); - return rest_ensure_response( array_map( - static function ( $entry ) use ( $nonce ) { - $entry['amp_url'] = amp_add_paired_endpoint( $entry['url'] ); - $entry['validate_url'] = add_query_arg( - [ - AMP_Validation_Manager::VALIDATE_QUERY_VAR => $nonce, - ], - $entry['amp_url'] - ); + static function ( $entry ) { + $entry['amp_url'] = amp_add_paired_endpoint( $entry['url'] ); return $entry; }, @@ -112,33 +103,26 @@ public function get_item_schema() { 'title' => 'amp-wp-' . $this->rest_base, 'type' => 'object', 'properties' => [ - 'url' => [ + 'url' => [ 'description' => __( 'Page URL.', 'amp' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => [ 'view' ], ], - 'amp_url' => [ + 'amp_url' => [ 'description' => __( 'AMP URL.', 'amp' ), 'type' => 'string', 'format' => 'uri', 'readonly' => true, 'context' => [ 'view' ], ], - 'type' => [ + 'type' => [ 'description' => __( 'Page type.', 'amp' ), 'type' => 'string', 'readonly' => true, 'context' => [ 'view' ], ], - 'validate_url' => [ - 'description' => __( 'URL for accessing validation data for a given page.', 'amp' ), - 'type' => 'string', - 'format' => 'uri', - 'readonly' => true, - 'context' => [ 'view' ], - ], ], ]; } From 4d5e265b623b1f96904795b984c5508e2b4742cc Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 8 Oct 2021 16:49:20 +0200 Subject: [PATCH 041/120] Revamp site scan context provider and UI components - Replace `useState`s with a single `useReducer` - Colocate site-scan-related sub-components --- .../site-scan-context-provider/index.js | 189 +++++++++++++----- .../pages/site-scan/complete.js | 67 ------- .../pages/site-scan/in-progress.js | 56 ------ .../pages/site-scan/index.js | 172 +++++++++++++--- assets/src/settings-page/site-scan.js | 188 ++++++++--------- 5 files changed, 375 insertions(+), 297 deletions(-) delete mode 100644 assets/src/onboarding-wizard/pages/site-scan/complete.js delete mode 100644 assets/src/onboarding-wizard/pages/site-scan/in-progress.js diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 6f36c1c92af..4354db82934 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import { createContext, useCallback, useEffect, useReducer, useRef } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; @@ -18,11 +18,92 @@ import { getSiteIssues } from './get-site-issues'; export const SiteScan = createContext(); -const SITE_SCAN_STATE_INITIALIZING = 'SITE_SCAN_STATE_INITIALIZING'; -const SITE_SCAN_STATE_READY = 'SITE_SCAN_STATE_READY'; -const SITE_SCAN_STATE_IDLE = 'SITE_SCAN_STATE_IDLE'; -const SITE_SCAN_STATE_IN_PROGRESS = 'SITE_SCAN_STATE_IN_PROGRESS'; -const SITE_SCAN_STATE_COMPLETE = 'SITE_SCAN_STATE_COMPLETE'; +const ACTION_SCANNABLE_URLS_REQUEST = 'ACTION_SCANNABLE_URLS_REQUEST'; +const ACTION_SCANNABLE_URLS_FETCH = 'ACTION_SCANNABLE_URLS_FETCH'; +const ACTION_SCANNABLE_URLS_RECEIVE = 'ACTION_SCANNABLE_URLS_RECEIVE'; +const ACTION_START_SITE_SCAN = 'ACTION_START_SITE_SCAN'; +const ACTION_SCAN_IN_PROGRESS = 'ACTION_SCAN_IN_PROGRESS'; +const ACTION_SCAN_RECEIVE_ISSUES = 'ACTION_SCAN_RECEIVE_ISSUES'; +const ACTION_SCAN_NEXT_URL = 'ACTION_SCAN_NEXT_URL'; +const ACTION_SCAN_CANCEL = 'ACTION_SCAN_CANCEL'; + +const STATUS_REQUEST_SCANNABLE_URLS = 'STATUS_REQUEST_SCANNABLE_URLS'; +const STATUS_FETCHING_SCANNABLE_URLS = 'STATUS_FETCHING_SCANNABLE_URLS'; +const STATUS_READY = 'STATUS_READY'; +const STATUS_IDLE = 'STATUS_IDLE'; +const STATUS_IN_PROGRESS = 'STATUS_IN_PROGRESS'; +const STATUS_COMPLETE = 'STATUS_COMPLETE'; + +function siteScanReducer( state, action ) { + switch ( action.type ) { + case ACTION_SCANNABLE_URLS_REQUEST: { + return { + ...state, + status: STATUS_REQUEST_SCANNABLE_URLS, + }; + } + case ACTION_SCANNABLE_URLS_FETCH: { + return { + ...state, + status: STATUS_FETCHING_SCANNABLE_URLS, + }; + } + case ACTION_SCANNABLE_URLS_RECEIVE: { + return { + ...state, + status: STATUS_READY, + scannableUrls: action.scannableUrls, + }; + } + case ACTION_START_SITE_SCAN: { + return { + ...state, + status: STATUS_IDLE, + themeIssues: [], + pluginIssues: [], + currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, + }; + } + case ACTION_SCAN_IN_PROGRESS: { + return { + ...state, + status: STATUS_IN_PROGRESS, + }; + } + case ACTION_SCAN_RECEIVE_ISSUES: { + return { + ...state, + pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...action.pluginIssues ] ) ], + themeIssues: [ ...new Set( [ ...state.themeIssues, ...action.themeIssues ] ) ], + }; + } + case ACTION_SCAN_NEXT_URL: { + const hasNextUrl = state.currentlyScannedUrlIndex < state.scannableUrls.length - 1; + return { + ...state, + status: hasNextUrl ? STATUS_IDLE : STATUS_COMPLETE, + currentlyScannedUrlIndex: hasNextUrl ? state.currentlyScannedUrlIndex + 1 : state.currentlyScannedUrlIndex, + }; + } + case ACTION_SCAN_CANCEL: { + return { + ...state, + status: STATUS_READY, + }; + } + default: { + throw new Error( `Unhandled action type: ${ action.type }` ); + } + } +} + +const initialState = { + themeIssues: [], + pluginIssues: [], + status: '', + scannableUrls: [], + currentlyScannedUrlIndex: 0, +}; /** * Context provider for site scanning. @@ -39,31 +120,48 @@ export function SiteScanContextProvider( { validateNonce, validateQueryVar, } ) { - const [ themeIssues, setThemeIssues ] = useState( [] ); - const [ pluginIssues, setPluginIssues ] = useState( [] ); - const [ siteScanState, setSiteScanState ] = useState( SITE_SCAN_STATE_INITIALIZING ); - const [ currentlyScannedUrlIndex, setCurrentlyScannedUrlIndex ] = useState( 0 ); - const [ fetchingScannableUrls, setFetchingScannableUrls ] = useState( false ); - const [ fetchedScannableUrls, setFetchedScannableUrls ] = useState( false ); - const [ scannableUrls, setScannableUrls ] = useState( [] ); const { setAsyncError } = useAsyncError(); + const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); + const { + currentlyScannedUrlIndex, + pluginIssues, + scannableUrls, + status, + themeIssues, + } = state; + + useEffect( () => { + if ( status ) { + return; + } - // This component sets state inside async functions. Use this ref to prevent state updates after unmount. + if ( ! validateQueryVar || ! validateNonce ) { + throw new Error( 'Invalid site scan configuration' ); + } + + dispatch( { type: ACTION_SCANNABLE_URLS_REQUEST } ); + }, [ status, validateNonce, validateQueryVar ] ); + + /** + * This component sets state inside async functions. Use this ref to prevent + * state updates after unmount. + */ const hasUnmounted = useRef( false ); useEffect( () => () => { hasUnmounted.current = true; }, [] ); const startSiteScan = useCallback( () => { - setSiteScanState( SITE_SCAN_STATE_IDLE ); - }, [] ); + if ( status === STATUS_READY ) { + dispatch( { type: ACTION_START_SITE_SCAN } ); + } + }, [ status ] ); /** * Allows cancelling a scan that is in progress. */ const hasCanceled = useRef( false ); const cancelSiteScan = useCallback( () => { - setCurrentlyScannedUrlIndex( 0 ); hasCanceled.current = true; }, [] ); @@ -71,12 +169,12 @@ export function SiteScanContextProvider( { * Fetch scannable URLs from the REST endpoint. */ useEffect( () => { - if ( fetchingScannableUrls || fetchedScannableUrls ) { + if ( status !== STATUS_REQUEST_SCANNABLE_URLS ) { return; } ( async () => { - setFetchingScannableUrls( true ); + dispatch( { type: ACTION_SCANNABLE_URLS_FETCH } ); try { const response = await apiFetch( { @@ -87,27 +185,21 @@ export function SiteScanContextProvider( { return; } - setScannableUrls( response ); - setSiteScanState( SITE_SCAN_STATE_READY ); - setFetchedScannableUrls( true ); + dispatch( { + type: ACTION_SCANNABLE_URLS_RECEIVE, + scannableUrls: response, + } ); } catch ( e ) { setAsyncError( e ); - return; } - - setFetchingScannableUrls( false ); } )(); - }, [ fetchedScannableUrls, fetchingScannableUrls, scannableUrlsRestPath, setAsyncError ] ); + }, [ scannableUrlsRestPath, setAsyncError, status ] ); /** * Scan site URLs sequentially. */ useEffect( () => { - if ( ! validateQueryVar || ! validateNonce ) { - return; - } - - if ( siteScanState !== SITE_SCAN_STATE_IDLE ) { + if ( status !== STATUS_IDLE ) { return; } @@ -115,7 +207,7 @@ export function SiteScanContextProvider( { * Validates the next URL in the queue. */ ( async () => { - setSiteScanState( SITE_SCAN_STATE_IN_PROGRESS ); + dispatch( { type: ACTION_SCAN_IN_PROGRESS } ); try { const { url } = scannableUrls[ currentlyScannedUrlIndex ]; @@ -125,7 +217,6 @@ export function SiteScanContextProvider( { [ validateQueryVar ]: { nonce: validateNonce, omit_stylesheets: true, - cache: true, }, } ), } ); @@ -136,9 +227,7 @@ export function SiteScanContextProvider( { if ( true === hasCanceled.current ) { hasCanceled.current = false; - setSiteScanState( SITE_SCAN_STATE_READY ); - setThemeIssues( [] ); - setPluginIssues( [] ); + dispatch( { type: ACTION_SCAN_CANCEL } ); return; } @@ -146,35 +235,31 @@ export function SiteScanContextProvider( { if ( validationResults.results.length > 0 ) { const siteIssues = getSiteIssues( validationResults.results ); - setPluginIssues( ( issues ) => [ ...new Set( [ ...issues, ...siteIssues.pluginIssues ] ) ] ); - setThemeIssues( ( issues ) => [ ...new Set( [ ...issues, ...siteIssues.themeIssues ] ) ] ); + dispatch( { + type: ACTION_SCAN_RECEIVE_ISSUES, + themeIssues: siteIssues.themeIssues, + pluginIssues: siteIssues.pluginIssues, + } ); } + + dispatch( { type: ACTION_SCAN_NEXT_URL } ); } catch ( e ) { setAsyncError( e ); - return; - } - - /** - * Finish the scan if there are no more URLs to validate. - */ - if ( currentlyScannedUrlIndex === scannableUrls.length - 1 ) { - setSiteScanState( SITE_SCAN_STATE_COMPLETE ); - } else { - setCurrentlyScannedUrlIndex( ( index ) => index + 1 ); - setSiteScanState( SITE_SCAN_STATE_IDLE ); } } )(); - }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, siteScanState, validateNonce, validateQueryVar ] ); + }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); return ( 0; - const hasPluginIssues = pluginIssues.length > 0; - - const { developerToolsOption } = useContext( User ); - const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); - - return ( -
      - -
      - -

      - { __( 'Scan complete', 'amp' ) } -

      -
      -

      - { hasThemeIssues || hasPluginIssues - ? __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) - : __( 'Site scan found no issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) - } -

      -
      - { hasThemeIssues && ( - - ) } - { hasPluginIssues && ( - - ) } -
      - ); -} diff --git a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js b/assets/src/onboarding-wizard/pages/site-scan/in-progress.js deleted file mode 100644 index df96590d2df..00000000000 --- a/assets/src/onboarding-wizard/pages/site-scan/in-progress.js +++ /dev/null @@ -1,56 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { useContext } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { SiteScan as SiteScanContext } from '../../../components/site-scan-context-provider'; -import { Selectable } from '../../../components/selectable'; -import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; -import { ProgressBar } from '../../../components/progress-bar'; - -/** - * Screen for visualizing a site scan progress state. - */ -export function SiteScanInProgress() { - const { - currentlyScannedUrlIndex, - scannableUrls, - siteScanComplete, - } = useContext( SiteScanContext ); - - return ( -
      - -
      - -

      - { __( 'Please wait a minute…', 'amp' ) } -

      -
      -

      - { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } -

      - -

      - { siteScanComplete - ? __( 'Scan complete', 'amp' ) - : sprintf( - // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), - currentlyScannedUrlIndex + 1, - scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.label, - ) - } -

      -
      -
      - ); -} diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index df12e8a030b..c53290579c7 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -1,7 +1,14 @@ +/** + * External dependencies + */ +import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. +import PropTypes from 'prop-types'; + /** * WordPress dependencies */ -import { useContext, useEffect, useLayoutEffect, useState } from '@wordpress/element'; +import { useContext, useEffect, useMemo, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -9,8 +16,12 @@ import { useContext, useEffect, useLayoutEffect, useState } from '@wordpress/ele import './style.scss'; import { Navigation } from '../../components/navigation-context-provider'; import { SiteScan as SiteScanContext } from '../../../components/site-scan-context-provider'; -import { SiteScanComplete } from './complete'; -import { SiteScanInProgress } from './in-progress'; +import { User } from '../../../components/user-context-provider'; +import { Loading } from '../../../components/loading'; +import { Selectable } from '../../../components/selectable'; +import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; +import { ProgressBar } from '../../../components/progress-bar'; +import { PluginsWithIssues, ThemesWithIssues } from '../../../components/site-scan-results'; /** * Screen for visualizing a site scan. @@ -18,59 +29,158 @@ import { SiteScanInProgress } from './in-progress'; export function SiteScan() { const { setCanGoForward } = useContext( Navigation ); const { + isInitializing, + isReady, + isComplete, cancelSiteScan, - canScanSite, + currentlyScannedUrlIndex, + pluginIssues, + scannableUrls, startSiteScan, - siteScanComplete, + themeIssues, } = useContext( SiteScanContext ); - const [ canShowScanSummary, setCanShowScanSummary ] = useState( false ); - + const { developerToolsOption } = useContext( User ); + const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); /** * Cancel scan on component unmount. */ useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); + useEffect( () => { + if ( isReady ) { + startSiteScan(); + } + }, [ isReady, startSiteScan ] ); + /** - * Start site scan. + * Allow moving forward. */ - useLayoutEffect( () => { - if ( canScanSite ) { - startSiteScan(); - } else if ( siteScanComplete ) { - setCanShowScanSummary( true ); + useEffect( () => { + if ( isComplete ) { + setCanGoForward( true ); } - }, [ canScanSite, siteScanComplete, startSiteScan ] ); + }, [ isComplete, setCanGoForward ] ); /** * Show scan summary with a delay so that the progress bar has a chance to * complete. */ + const [ showSummary, setShowSummary ] = useState( isComplete ); + useEffect( () => { - let delay; + let timeout; - if ( siteScanComplete && ! canShowScanSummary ) { - delay = setTimeout( () => setCanShowScanSummary( true ), 500 ); + if ( isComplete && ! showSummary ) { + timeout = setTimeout( () => setShowSummary( true ), 500 ); } return () => { - if ( delay ) { - clearTimeout( delay ); + if ( timeout ) { + clearTimeout( timeout ); } }; - }, [ canShowScanSummary, siteScanComplete ] ); + }, [ showSummary, isComplete ] ); - /** - * Allow moving forward. - */ - useEffect( () => { - if ( siteScanComplete ) { - setCanGoForward( true ); - } - }, [ setCanGoForward, siteScanComplete ] ); + if ( isInitializing ) { + return ( + } + /> + ); + } - if ( canShowScanSummary ) { - return ; + if ( showSummary ) { + return ( + + { themeIssues.length > 0 || pluginIssues.length > 0 + ? __( 'Site scan found issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) + : __( 'Site scan found no issues on your site. Proceed to the next step to follow recommendations for choosing a template mode.', 'amp' ) + } +

      + ) } + > + { themeIssues.length > 0 && ( + + ) } + { pluginIssues.length > 0 && ( + + ) } +
      + ); } - return ; + return ( + +

      + { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

      + +

      + { isComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.label, + ) + } +

      + + ) } + /> + ); +} + +/** + * Site Scan panel. + * + * @param {Object} props Component props. + * @param {any} props.children Component children. + * @param {any} props.headerContent Component header content. + * @param {string} props.title Component title. + */ +function SiteScanPanel( { + children, + headerContent, + title, +} ) { + return ( +
      + +
      + +

      + { title } +

      +
      + { headerContent } +
      + { children } +
      + ); } +SiteScanPanel.propTypes = { + children: PropTypes.any, + headerContent: PropTypes.any, + title: PropTypes.string, +}; diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 12c969c4e9e..e62c72bb8d3 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -2,13 +2,14 @@ * External dependencies */ import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. +import PropTypes from 'prop-types'; /** * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { useContext, useEffect, useState } from '@wordpress/element'; import { Button } from '@wordpress/components'; +import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -18,138 +19,143 @@ import { IconLandscapeHillsCogsAlt } from '../components/svg/landscape-hills-cog import { ProgressBar } from '../components/progress-bar'; import { PluginsWithIssues, ThemesWithIssues } from '../components/site-scan-results'; import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; +import { Loading } from '../components/loading'; /** * Site Scan component on the settings screen. */ export function SiteScan() { const { + isInitializing, + isReady, + isBusy, + isComplete, cancelSiteScan, - canScanSite, + currentlyScannedUrlIndex, + pluginIssues, + scannableUrls, startSiteScan, - siteScanComplete, + themeIssues, } = useContext( SiteScanContext ); - const [ requestSiteRescan, setRequestSiteRescan ] = useState( false ); - const [ showScanSummary, setShowScanSummary ] = useState( false ); /** * Cancel scan on component unmount. */ useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); - /** - * Start site scan. - */ - useEffect( () => { - if ( canScanSite && requestSiteRescan ) { - startSiteScan(); - } - }, [ canScanSite, requestSiteRescan, startSiteScan ] ); - /** * Show scan summary with a delay so that the progress bar has a chance to * complete. */ + const [ showSummary, setShowSummary ] = useState( true ); + useEffect( () => { - let delay; + let timeout; - if ( siteScanComplete && ! showScanSummary ) { - delay = setTimeout( () => setShowScanSummary( true ), 500 ); + if ( ( isReady || isComplete ) && ! showSummary ) { + timeout = setTimeout( () => setShowSummary( true ), 500 ); } return () => { - if ( delay ) { - clearTimeout( delay ); + if ( timeout ) { + clearTimeout( timeout ); } }; - }, [ showScanSummary, siteScanComplete ] ); + }, [ isComplete, isReady, showSummary ] ); - return ( - - - { __( 'Site Scan', 'amp' ) } - - ) } - hiddenTitle={ __( 'Site Scan', 'amp' ) } - id="site-scan" - initialOpen={ true } - > - { ( showScanSummary || ! requestSiteRescan ) - ? - : } - { ! requestSiteRescan && ( + useEffect( () => { + if ( showSummary && isBusy ) { + setShowSummary( false ); + } + }, [ isBusy, showSummary ] ); + + if ( isInitializing ) { + return ( + + + + ); + } + + if ( showSummary ) { + return ( + +
      + { themeIssues.length > 0 && ( + + ) } + { pluginIssues.length > 0 && ( + + ) } +
      - ) } -
      - ); -} - -/** - * Scan in progress screen. - */ -function SiteScanInProgress() { - const { - currentlyScannedUrlIndex, - scannableUrls, - siteScanComplete, - } = useContext( SiteScanContext ); + + ); + } return ( -
      -

      - { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } -

      - -

      - { siteScanComplete - ? __( 'Scan complete', 'amp' ) - : sprintf( - // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), - currentlyScannedUrlIndex + 1, - scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.label, - ) - } -

      -
      + +
      +

      + { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

      + +

      + { isComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.label, + ) + } +

      +
      +
      ); } /** - * Scan summary screen. + * Site Scan drawer (settings panel). + * + * @param {Object} props Component props. + * @param {any} props.children Component children. */ -function SiteScanSummary() { - const { pluginIssues, themeIssues } = useContext( SiteScanContext ); - const hasThemeIssues = themeIssues.length > 0; - const hasPluginIssues = pluginIssues.length > 0; - +function SiteScanDrawer( { children } ) { return ( -
      - { hasThemeIssues && ( - - ) } - { hasPluginIssues && ( - + + + { __( 'Site Scan', 'amp' ) } + ) } -
      + hiddenTitle={ __( 'Site Scan', 'amp' ) } + id="site-scan" + initialOpen={ true } + > + { children } + ); } +SiteScanDrawer.propTypes = { + children: PropTypes.any, +}; From 98bc4353f2458fb81bfd6f24771bf7b64f139780 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 7 Oct 2021 12:38:18 -0700 Subject: [PATCH 042/120] Add validation_errors, validated_url_post, and stale to scannable-urls --- .../ScannableURLsRestController.php | 23 +++++++++++++++++-- .../URLValidationRESTController.php | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Validation/ScannableURLsRestController.php b/src/Validation/ScannableURLsRestController.php index 17d67c1a5de..e4c835f9939 100644 --- a/src/Validation/ScannableURLsRestController.php +++ b/src/Validation/ScannableURLsRestController.php @@ -8,11 +8,13 @@ namespace AmpProject\AmpWP\Validation; -use WP_Error; -use WP_REST_Controller; +use AMP_Validated_URL_Post_Type; use AmpProject\AmpWP\Infrastructure\Delayed; use AmpProject\AmpWP\Infrastructure\Registerable; use AmpProject\AmpWP\Infrastructure\Service; +use WP_Error; +use WP_Post; +use WP_REST_Controller; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; @@ -85,6 +87,23 @@ public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAna static function ( $entry ) { $entry['amp_url'] = amp_add_paired_endpoint( $entry['url'] ); + $validated_url_post = AMP_Validated_URL_Post_Type::get_invalid_url_post( $entry['url'] ); + if ( $validated_url_post instanceof WP_Post ) { + $entry['validation_errors'] = []; + + $data = json_decode( $validated_url_post->post_content, true ); + if ( is_array( $data ) ) { + $entry['validation_errors'] = wp_list_pluck( $data, 'data' ); + } + + $entry['validated_url_post'] = [ + 'id' => $validated_url_post->ID, + 'edit_link' => get_edit_post_link( $validated_url_post->ID, 'raw' ), + ]; + + $entry['stale'] = ( count( AMP_Validated_URL_Post_Type::get_post_staleness( $validated_url_post ) ) > 0 ); + } + return $entry; }, $this->scannable_url_provider->get_urls() diff --git a/src/Validation/URLValidationRESTController.php b/src/Validation/URLValidationRESTController.php index 945b198393a..c624dde3acd 100644 --- a/src/Validation/URLValidationRESTController.php +++ b/src/Validation/URLValidationRESTController.php @@ -24,6 +24,8 @@ /** * URLValidationRESTController class. * + * @todo This can now be eliminated in favor of making validate requests to the frontend with `?amp_validate[cache]=true`. + * * @since 2.1 * @internal */ From f861d55178e41dd80d47c645f9ddd089c45f0abe Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 11 Oct 2021 15:33:17 +0200 Subject: [PATCH 043/120] Fetch cached validation errors in Site Scan on Settings page --- .../get-site-issues.js | 6 +- .../site-scan-context-provider/index.js | 59 +++++++++++++------ assets/src/settings-page/index.js | 1 + src/Admin/OptionsMenu.php | 6 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/get-site-issues.js b/assets/src/components/site-scan-context-provider/get-site-issues.js index 88ee90af64d..28964a4d80f 100644 --- a/assets/src/components/site-scan-context-provider/get-site-issues.js +++ b/assets/src/components/site-scan-context-provider/get-site-issues.js @@ -9,13 +9,13 @@ export function getSiteIssues( validationResults = [] ) { const themeIssues = new Set(); for ( const result of validationResults ) { - const { error } = result; + const sources = result?.error?.sources ?? result?.sources; - if ( ! error?.sources ) { + if ( ! sources ) { continue; } - for ( const source of error.sources ) { + for ( const source of sources ) { if ( source.type === 'plugin' && source.name !== 'amp' ) { pluginIssues.add( source.name.match( /(.*?)(?:\.php)?$/ )[ 1 ] ); } else if ( source.type === 'theme' ) { diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 4354db82934..de3fcaac1cd 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -49,10 +49,23 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCANNABLE_URLS_RECEIVE: { + if ( ! action?.scannableUrls?.length || action.scannableUrls.length === 0 ) { + return { + ...state, + status: STATUS_COMPLETE, + }; + } + + const validationErrors = action.scannableUrls.reduce( ( acc, data ) => [ ...acc, ...data?.validation_errors ?? [] ], [] ); + const siteIssues = getSiteIssues( validationErrors ); + return { ...state, status: STATUS_READY, scannableUrls: action.scannableUrls, + stale: Boolean( action.scannableUrls.find( ( error ) => error?.stale === true ) ), + pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...siteIssues.pluginIssues ] ) ], + themeIssues: [ ...new Set( [ ...state.themeIssues, ...siteIssues.themeIssues ] ) ], }; } case ACTION_START_SITE_SCAN: { @@ -71,10 +84,16 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCAN_RECEIVE_ISSUES: { + if ( ! action?.validationResults?.length || action.validationResults.length === 0 ) { + return state; + } + + const siteIssues = getSiteIssues( action.validationResults ); + return { ...state, - pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...action.pluginIssues ] ) ], - themeIssues: [ ...new Set( [ ...state.themeIssues, ...action.themeIssues ] ) ], + pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...siteIssues.pluginIssues ] ) ], + themeIssues: [ ...new Set( [ ...state.themeIssues, ...siteIssues.themeIssues ] ) ], }; } case ACTION_SCAN_NEXT_URL: { @@ -102,20 +121,23 @@ const initialState = { pluginIssues: [], status: '', scannableUrls: [], + stale: false, currentlyScannedUrlIndex: 0, }; /** * Context provider for site scanning. * - * @param {Object} props Component props. - * @param {?any} props.children Component children. - * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. - * @param {string} props.validateNonce The AMP validate nonce. - * @param {string} props.validateQueryVar The AMP validate query variable name. + * @param {Object} props Component props. + * @param {?any} props.children Component children. + * @param {boolean} props.fetchCachedValidationErrors Whether to fetch cached validation errors on mount. + * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. + * @param {string} props.validateNonce The AMP validate nonce. + * @param {string} props.validateQueryVar The AMP validate query variable name. */ export function SiteScanContextProvider( { children, + fetchCachedValidationErrors = false, scannableUrlsRestPath, validateNonce, validateQueryVar, @@ -126,6 +148,7 @@ export function SiteScanContextProvider( { currentlyScannedUrlIndex, pluginIssues, scannableUrls, + stale, status, themeIssues, } = state; @@ -177,8 +200,11 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCANNABLE_URLS_FETCH } ); try { + const fields = [ 'url', 'amp_url', 'type', 'label' ]; const response = await apiFetch( { - path: scannableUrlsRestPath, + path: addQueryArgs( scannableUrlsRestPath, { + _fields: fetchCachedValidationErrors ? [ ...fields, 'validation_errors', 'stale' ] : fields, + } ), } ); if ( true === hasUnmounted.current ) { @@ -193,7 +219,7 @@ export function SiteScanContextProvider( { setAsyncError( e ); } } )(); - }, [ scannableUrlsRestPath, setAsyncError, status ] ); + }, [ fetchCachedValidationErrors, scannableUrlsRestPath, setAsyncError, status ] ); /** * Scan site URLs sequentially. @@ -232,15 +258,10 @@ export function SiteScanContextProvider( { return; } - if ( validationResults.results.length > 0 ) { - const siteIssues = getSiteIssues( validationResults.results ); - - dispatch( { - type: ACTION_SCAN_RECEIVE_ISSUES, - themeIssues: siteIssues.themeIssues, - pluginIssues: siteIssues.pluginIssues, - } ); - } + dispatch( { + type: ACTION_SCAN_RECEIVE_ISSUES, + validationResults: validationResults.results, + } ); dispatch( { type: ACTION_SCAN_NEXT_URL } ); } catch ( e ) { @@ -258,6 +279,7 @@ export function SiteScanContextProvider( { isComplete: status === STATUS_COMPLETE, isInitializing: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), isReady: status === STATUS_READY, + stale, pluginIssues, scannableUrls, startSiteScan, @@ -271,6 +293,7 @@ export function SiteScanContextProvider( { SiteScanContextProvider.propTypes = { children: PropTypes.any, + fetchCachedValidationErrors: PropTypes.bool, scannableUrlsRestPath: PropTypes.string, validateNonce: PropTypes.string, validateQueryVar: PropTypes.string, diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 0cb699115ed..7fe6ee2ebcd 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -96,6 +96,7 @@ function Providers( { children } ) { Date: Mon, 11 Oct 2021 17:24:12 +0200 Subject: [PATCH 044/120] Add pre- and post-scan messages; add staleness notices --- assets/src/components/amp-drawer/index.js | 40 ++++++-- assets/src/components/amp-drawer/style.css | 29 ++++-- assets/src/components/amp-notice/index.js | 3 +- assets/src/components/amp-notice/style.css | 3 +- .../src/components/amp-notice/test/index.js | 21 ++++- .../site-scan-context-provider/index.js | 12 ++- .../components/template-mode-option/style.css | 6 ++ assets/src/css/core-components.css | 5 - assets/src/css/variables.css | 1 + assets/src/settings-page/site-scan.js | 93 +++++++++++++++---- assets/src/settings-page/style.css | 8 ++ 11 files changed, 179 insertions(+), 42 deletions(-) diff --git a/assets/src/components/amp-drawer/index.js b/assets/src/components/amp-drawer/index.js index 7b4118969cc..6b81cd6f71b 100644 --- a/assets/src/components/amp-drawer/index.js +++ b/assets/src/components/amp-drawer/index.js @@ -28,11 +28,22 @@ export const HANDLE_TYPE_RIGHT = 'right'; * @param {any} props.heading Content for the drawer heading. * @param {string} props.id A unique ID for the component. * @param {boolean} props.initialOpen Whether the drawer should be initially open. + * @param {Object} props.labelExtra Optional. Extra content to display on the right side of the option label. * @param {boolean} props.selected Whether to apply the selectable components selected CSS class. * @param {string} props.hiddenTitle A title to go with the button that expands the drawer. * @param {string} props.handleType Display style for the drawer handle. Either 'full-width' or 'right'. */ -export function AMPDrawer( { children = null, className, heading, handleType = HANDLE_TYPE_FULL_WIDTH, id, initialOpen = false, selected = false, hiddenTitle } ) { +export function AMPDrawer( { + children = null, + className, + heading, + handleType = HANDLE_TYPE_FULL_WIDTH, + id, + initialOpen = false, + labelExtra = null, + selected = false, + hiddenTitle, +} ) { const [ opened, setOpened ] = useState( initialOpen ); const [ resetStatus, setResetStatus ] = useState( null ); @@ -94,9 +105,16 @@ export function AMPDrawer( { children = null, className, heading, handleType = H selected={ selected } > { handleType === HANDLE_TYPE_RIGHT && ( -
      - { heading } -
      + <> +
      + { heading } +
      + { labelExtra && ( +
      + { labelExtra } +
      + ) } + ) } { 'resetting' !== resetStatus && ( ) : ( -
      - { heading } -
      + <> +
      + { heading } +
      + { labelExtra && ( +
      + { labelExtra } +
      + ) } + ) } className="amp-drawer__panel-body" initialOpen={ initialOpen } @@ -129,5 +154,6 @@ AMPDrawer.propTypes = { hiddenTitle: PropTypes.node.isRequired, id: PropTypes.string.isRequired, initialOpen: PropTypes.bool, + labelExtra: PropTypes.node, selected: PropTypes.bool, }; diff --git a/assets/src/components/amp-drawer/style.css b/assets/src/components/amp-drawer/style.css index f1c80b96f5c..7d55e366754 100644 --- a/assets/src/components/amp-drawer/style.css +++ b/assets/src/components/amp-drawer/style.css @@ -25,9 +25,7 @@ padding-left: 0.75rem; padding-right: 0.75rem; overflow: hidden; - position: absolute; - top: 0; - z-index: 1; + flex-grow: 1; @media (min-width: 783px) { padding-left: 3rem; @@ -45,6 +43,14 @@ margin-bottom: 0; } +.amp-drawer__heading svg { + margin-right: 1rem; +} + +.amp-drawer__label-extra svg { + fill: none; +} + .amp .amp-drawer .components-panel__body-title { height: var(--heading-height); margin: 0 0 0 auto; @@ -63,7 +69,6 @@ border-radius: 5px; display: flex; align-items: center; - justify-content: center; } .amp .amp-drawer .components-panel__body-title button { @@ -77,16 +82,15 @@ } -.amp .amp-drawer .components-panel__body-title button span { +.amp .amp-drawer .components-panel__body-title > button > span { align-items: center; display: flex; - height: 100%; - margin-left: auto; justify-content: center; width: var(--panel-button-width); + order: 100; } -.amp .amp-drawer .components-panel__body-title svg { +.amp .amp-drawer .components-panel__body-toggle.components-button .components-panel__arrow { height: 30px; position: static; transform: none; @@ -98,7 +102,8 @@ } } -.amp .amp-drawer--handle-type-full-width .components-panel__body-title svg { + +.amp .amp-drawer--handle-type-full-width .components-panel__body-title > button > svg { margin-left: auto; @media (min-width: 783px) { @@ -110,6 +115,11 @@ border-radius: 5px; } +.amp .amp-drawer .components-panel__body-title .amp-notice { + font-family: var(--font-default); + font-weight: 400; +} + .amp .amp-drawer__panel-body { padding: 0; border-top-width: 0; @@ -135,6 +145,7 @@ .amp .amp-drawer--handle-type-right .amp-drawer__heading { right: var(--panel-button-width); + width: calc(100% - var(--panel-button-width)); } .amp-drawer--handle-type-right .components-panel__body-title { diff --git a/assets/src/components/amp-notice/index.js b/assets/src/components/amp-notice/index.js index 187858f094d..869edad0525 100644 --- a/assets/src/components/amp-notice/index.js +++ b/assets/src/components/amp-notice/index.js @@ -12,6 +12,7 @@ import './style.css'; export const NOTICE_TYPE_ERROR = 'error'; export const NOTICE_TYPE_WARNING = 'warning'; export const NOTICE_TYPE_INFO = 'info'; +export const NOTICE_TYPE_PLAIN = 'plain'; export const NOTICE_TYPE_SUCCESS = 'success'; export const NOTICE_SIZE_SMALL = 'small'; @@ -103,5 +104,5 @@ AMPNotice.propTypes = { children: PropTypes.node, className: PropTypes.string, size: PropTypes.oneOf( [ NOTICE_SIZE_LARGE, NOTICE_SIZE_SMALL ] ), - type: PropTypes.oneOf( [ NOTICE_TYPE_INFO, NOTICE_TYPE_SUCCESS, NOTICE_TYPE_ERROR, NOTICE_TYPE_WARNING ] ), + type: PropTypes.oneOf( [ NOTICE_TYPE_PLAIN, NOTICE_TYPE_INFO, NOTICE_TYPE_SUCCESS, NOTICE_TYPE_ERROR, NOTICE_TYPE_WARNING ] ), }; diff --git a/assets/src/components/amp-notice/style.css b/assets/src/components/amp-notice/style.css index 7a30bae1f0e..d9f1d4f1033 100644 --- a/assets/src/components/amp-notice/style.css +++ b/assets/src/components/amp-notice/style.css @@ -34,7 +34,8 @@ background-color: #effbff; } -.amp-notice--info svg { +.amp-notice--info svg, +.amp-notice--plain svg { color: var(--amp-settings-color-brand); } diff --git a/assets/src/components/amp-notice/test/index.js b/assets/src/components/amp-notice/test/index.js index 4dcfa4848d7..a898f57d966 100644 --- a/assets/src/components/amp-notice/test/index.js +++ b/assets/src/components/amp-notice/test/index.js @@ -12,7 +12,15 @@ import { render } from '@wordpress/element'; /** * Internal dependencies */ -import { AMPNotice, NOTICE_TYPE_SUCCESS, NOTICE_SIZE_LARGE, NOTICE_TYPE_ERROR, NOTICE_SIZE_SMALL, NOTICE_TYPE_INFO } from '..'; +import { + AMPNotice, + NOTICE_TYPE_SUCCESS, + NOTICE_SIZE_LARGE, + NOTICE_TYPE_ERROR, + NOTICE_SIZE_SMALL, + NOTICE_TYPE_INFO, + NOTICE_TYPE_PLAIN, +} from '..'; let container; @@ -78,5 +86,16 @@ describe( 'AMPNotice', () => { } ); expect( container.querySelector( 'div' ).getAttribute( 'class' ) ).toBe( 'amp-notice amp-notice--info amp-notice--small' ); + + act( () => { + render( + + { 'children' } + , + container, + ); + } ); + + expect( container.querySelector( 'div' ).getAttribute( 'class' ) ).toBe( 'amp-notice amp-notice--plain amp-notice--small' ); } ); } ); diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index de3fcaac1cd..ff67df4e699 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -72,6 +72,8 @@ function siteScanReducer( state, action ) { return { ...state, status: STATUS_IDLE, + stale: false, + cache: action.cache, themeIssues: [], pluginIssues: [], currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, @@ -122,6 +124,7 @@ const initialState = { status: '', scannableUrls: [], stale: false, + cache: false, currentlyScannedUrlIndex: 0, }; @@ -145,6 +148,7 @@ export function SiteScanContextProvider( { const { setAsyncError } = useAsyncError(); const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); const { + cache, currentlyScannedUrlIndex, pluginIssues, scannableUrls, @@ -174,9 +178,12 @@ export function SiteScanContextProvider( { hasUnmounted.current = true; }, [] ); - const startSiteScan = useCallback( () => { + const startSiteScan = useCallback( ( args = {} ) => { if ( status === STATUS_READY ) { - dispatch( { type: ACTION_START_SITE_SCAN } ); + dispatch( { + type: ACTION_START_SITE_SCAN, + cache: args?.cache, + } ); } }, [ status ] ); @@ -243,6 +250,7 @@ export function SiteScanContextProvider( { [ validateQueryVar ]: { nonce: validateNonce, omit_stylesheets: true, + cache, }, } ), } ); diff --git a/assets/src/components/template-mode-option/style.css b/assets/src/components/template-mode-option/style.css index 7756cab9e6f..e60aded4a73 100644 --- a/assets/src/components/template-mode-option/style.css +++ b/assets/src/components/template-mode-option/style.css @@ -111,6 +111,12 @@ right: 0; } +.template-mode-option .components-panel__body-title { + position: absolute; + top: 0; + right: 0; +} + .template-mode-option .components-panel__body-title:hover { background: rgba(0, 0, 0, 0); } diff --git a/assets/src/css/core-components.css b/assets/src/css/core-components.css index b140e4e6daf..1501e83fb51 100644 --- a/assets/src/css/core-components.css +++ b/assets/src/css/core-components.css @@ -26,11 +26,6 @@ background: var(--amp-settings-color-background); } -.amp .components-button.components-panel__body-toggle svg { - height: 30px; - width: 30px; -} - .amp .components-button.is-link, .amp .components-button.is-link:hover, .amp .components-button.is-link:hover:not(:disabled), diff --git a/assets/src/css/variables.css b/assets/src/css/variables.css index 1f0867b9881..9e1d3f62d23 100644 --- a/assets/src/css/variables.css +++ b/assets/src/css/variables.css @@ -18,6 +18,7 @@ --amp-settings-color-warning: #ff9f00; --font-noto: "Noto Sans", sans-serif; --font-poppins: poppins, sans-serif; + --font-default: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; --color-valid: #46b450; --amp-settings-color-danger: #dc3232; --color-gray-medium: rgba(0, 0, 0, 0.54); diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index e62c72bb8d3..e4b2d5d4336 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. +import { HOME_URL, VALIDATED_URLS_LINK } from 'amp-settings'; // From WP inline script. import PropTypes from 'prop-types'; /** @@ -14,12 +14,21 @@ import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ +import { STANDARD } from '../common/constants'; import { AMPDrawer } from '../components/amp-drawer'; import { IconLandscapeHillsCogsAlt } from '../components/svg/landscape-hills-cogs-alt'; import { ProgressBar } from '../components/progress-bar'; import { PluginsWithIssues, ThemesWithIssues } from '../components/site-scan-results'; +import { Options } from '../components/options-context-provider'; import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; import { Loading } from '../components/loading'; +import { + AMPNotice, + NOTICE_SIZE_LARGE, + NOTICE_SIZE_SMALL, + NOTICE_TYPE_INFO, + NOTICE_TYPE_PLAIN, +} from '../components/amp-notice'; /** * Site Scan component on the settings screen. @@ -34,9 +43,16 @@ export function SiteScan() { currentlyScannedUrlIndex, pluginIssues, scannableUrls, + stale, startSiteScan, themeIssues, } = useContext( SiteScanContext ); + const { originalOptions } = useContext( Options ); + const { + paired_url_examples: pairedUrlExamples, + paired_url_structure: pairedUrlStructure, + theme_support: themeSupport, + } = originalOptions; /** * Cancel scan on component unmount. @@ -71,43 +87,88 @@ export function SiteScan() { if ( isInitializing ) { return ( - + ); } + const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; + const previewPermalink = STANDARD === themeSupport ? HOME_URL : pairedUrlExamples[ pairedUrlStructure ][ 0 ]; + if ( showSummary ) { return ( - + + { __( 'Stale results', 'amp' ) } + + ) : null } + >
      + { ! isComplete && ( + +

      + { stale + ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) + : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) + } +

      +
      + ) } + { isComplete && ! hasSiteIssues && ( +

      template mode recommendations below. Because of plugin issues, you may also want to review and suppress plugins.', 'amp' ), + '#template-modes', + '#plugin-suppression', + ), + } } + /> + ) } + { isComplete && hasSiteIssues && ( +

      + { __( 'Site scan found no issues on your site.', 'amp' ) } +

      + ) } { themeIssues.length > 0 && ( ) } { pluginIssues.length > 0 && ( ) } -
      -
      - +
      + { isComplete + ? ( + + ) + : ( + + ) } +
      ); } return ( - +

      { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } @@ -139,7 +200,7 @@ export function SiteScan() { * @param {Object} props Component props. * @param {any} props.children Component children. */ -function SiteScanDrawer( { children } ) { +function SiteScanDrawer( { children, ...props } ) { return ( { children } diff --git a/assets/src/settings-page/style.css b/assets/src/settings-page/style.css index 14b047cc294..a6f6d14d314 100644 --- a/assets/src/settings-page/style.css +++ b/assets/src/settings-page/style.css @@ -555,12 +555,20 @@ li.error-kept { #site-scan .amp-drawer__heading svg { fill: transparent; +} + +#site-scan .amp-drawer__heading > svg { width: 55px; } +.settings-site-scan > * + * { + margin-top: 1.5rem; +} + .settings-site-scan__footer { align-items: center; display: flex; + justify-content: flex-end; flex-flow: row nowrap; } From 1ccee271a63eb453b9c705acc8a2a9e84c07d653 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 11 Oct 2021 17:37:12 +0200 Subject: [PATCH 045/120] Fix lint errors --- assets/src/components/site-scan-context-provider/index.js | 2 +- assets/src/css/variables.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index ff67df4e699..a15c79df693 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -276,7 +276,7 @@ export function SiteScanContextProvider( { setAsyncError( e ); } } )(); - }, [ currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); + }, [ cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); return ( Date: Mon, 11 Oct 2021 15:30:41 -0700 Subject: [PATCH 046/120] Add ampFirst prop to SiteScanContextProvider and set as false on settings screen --- .../site-scan-context-provider/index.js | 24 ++++++++++++------- assets/src/onboarding-wizard/index.js | 1 + assets/src/settings-page/index.js | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index a15c79df693..1f7e5879d59 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -137,6 +137,7 @@ const initialState = { * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. * @param {string} props.validateNonce The AMP validate nonce. * @param {string} props.validateQueryVar The AMP validate query variable name. + * @param {boolean} props.ampFirst Whether scanning should be done with Standard mode being forced. */ export function SiteScanContextProvider( { children, @@ -144,6 +145,7 @@ export function SiteScanContextProvider( { scannableUrlsRestPath, validateNonce, validateQueryVar, + ampFirst = false, } ) { const { setAsyncError } = useAsyncError(); const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); @@ -244,15 +246,18 @@ export function SiteScanContextProvider( { try { const { url } = scannableUrls[ currentlyScannedUrlIndex ]; + const args = { + [ validateQueryVar ]: { + nonce: validateNonce, + omit_stylesheets: true, + cache, + }, + }; + if ( ampFirst ) { + args[ 'amp-first' ] = true; + } const validationResults = await apiFetch( { - url: addQueryArgs( url, { - 'amp-first': true, - [ validateQueryVar ]: { - nonce: validateNonce, - omit_stylesheets: true, - cache, - }, - } ), + url: addQueryArgs( url, args ), } ); if ( true === hasUnmounted.current ) { @@ -276,7 +281,7 @@ export function SiteScanContextProvider( { setAsyncError( e ); } } )(); - }, [ cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); + }, [ cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar, ampFirst ] ); return ( { children } diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 7fe6ee2ebcd..3197f9174ab 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -100,6 +100,7 @@ function Providers( { children } ) { scannableUrlsRestPath={ SCANNABLE_URLS_REST_PATH } validateNonce={ VALIDATE_NONCE } validateQueryVar={ VALIDATE_QUERY_VAR } + ampFirst={ false } > { children } From 428be9289123ec14dbcf2e11efdf83c56f18d4a6 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 12 Oct 2021 10:32:19 +0200 Subject: [PATCH 047/120] Ignore some validation error codes in site scan --- .../site-scan-context-provider/index.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 1f7e5879d59..d672ff17b7a 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -132,20 +132,20 @@ const initialState = { * Context provider for site scanning. * * @param {Object} props Component props. + * @param {boolean} props.ampFirst Whether scanning should be done with Standard mode being forced. * @param {?any} props.children Component children. * @param {boolean} props.fetchCachedValidationErrors Whether to fetch cached validation errors on mount. * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. * @param {string} props.validateNonce The AMP validate nonce. * @param {string} props.validateQueryVar The AMP validate query variable name. - * @param {boolean} props.ampFirst Whether scanning should be done with Standard mode being forced. */ export function SiteScanContextProvider( { + ampFirst = false, children, fetchCachedValidationErrors = false, scannableUrlsRestPath, validateNonce, validateQueryVar, - ampFirst = false, } ) { const { setAsyncError } = useAsyncError(); const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); @@ -247,15 +247,13 @@ export function SiteScanContextProvider( { try { const { url } = scannableUrls[ currentlyScannedUrlIndex ]; const args = { + 'amp-first': ampFirst || undefined, [ validateQueryVar ]: { + cache: cache || undefined, nonce: validateNonce, omit_stylesheets: true, - cache, }, }; - if ( ampFirst ) { - args[ 'amp-first' ] = true; - } const validationResults = await apiFetch( { url: addQueryArgs( url, args ), } ); @@ -278,10 +276,15 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_NEXT_URL } ); } catch ( e ) { - setAsyncError( e ); + const ignoredErrorCodes = [ 'AMP_NOT_REQUESTED', 'AMP_NOT_AVAILABLE' ]; + if ( ! e.code || ! ignoredErrorCodes.includes( e.code ) ) { + setAsyncError( e ); + } } + + dispatch( { type: ACTION_SCAN_NEXT_URL } ); } )(); - }, [ cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar, ampFirst ] ); + }, [ ampFirst, cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); return ( Date: Tue, 12 Oct 2021 10:33:31 +0200 Subject: [PATCH 048/120] Swap conditions --- assets/src/settings-page/site-scan.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index e4b2d5d4336..b082cd6918d 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -117,7 +117,7 @@ export function SiteScan() {

      ) } - { isComplete && ! hasSiteIssues && ( + { isComplete && hasSiteIssues && (

      ) } - { isComplete && hasSiteIssues && ( + { isComplete && ! hasSiteIssues && (

      { __( 'Site scan found no issues on your site.', 'amp' ) }

      From 290d42fa008fb1a94f7fa205f6588319c2263cdd Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 12 Oct 2021 11:11:22 +0200 Subject: [PATCH 049/120] Use default page URL only when validating in standard mode --- .../src/components/site-scan-context-provider/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index d672ff17b7a..b46e1451167 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, useCallback, useEffect, useReducer, useRef } from '@wordpress/element'; +import { createContext, useCallback, useContext, useEffect, useReducer, useRef } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; @@ -14,6 +14,8 @@ import PropTypes from 'prop-types'; * Internal dependencies */ import { useAsyncError } from '../../utils/use-async-error'; +import { Options } from '../options-context-provider'; +import { STANDARD } from '../../common/constants'; import { getSiteIssues } from './get-site-issues'; export const SiteScan = createContext(); @@ -147,6 +149,7 @@ export function SiteScanContextProvider( { validateNonce, validateQueryVar, } ) { + const { originalOptions } = useContext( Options ); const { setAsyncError } = useAsyncError(); const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); const { @@ -245,7 +248,8 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_IN_PROGRESS } ); try { - const { url } = scannableUrls[ currentlyScannedUrlIndex ]; + const urlType = ampFirst || originalOptions?.theme_support === STANDARD ? 'url' : 'amp_url'; + const url = scannableUrls[ currentlyScannedUrlIndex ][ urlType ]; const args = { 'amp-first': ampFirst || undefined, [ validateQueryVar ]: { @@ -284,7 +288,7 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_NEXT_URL } ); } )(); - }, [ ampFirst, cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); + }, [ ampFirst, cache, currentlyScannedUrlIndex, originalOptions?.theme_support, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); return ( Date: Tue, 12 Oct 2021 11:23:21 +0200 Subject: [PATCH 050/120] Remove redundant constant and prop --- .../components/site-scan-context-provider/index.js | 14 +++++++------- assets/src/onboarding-wizard/index.js | 4 +--- assets/src/settings-page/index.js | 4 +--- src/Admin/OnboardingWizardSubmenuPage.php | 1 - src/Admin/OptionsMenu.php | 1 - 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index b46e1451167..49489a76289 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -139,7 +139,6 @@ const initialState = { * @param {boolean} props.fetchCachedValidationErrors Whether to fetch cached validation errors on mount. * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. * @param {string} props.validateNonce The AMP validate nonce. - * @param {string} props.validateQueryVar The AMP validate query variable name. */ export function SiteScanContextProvider( { ampFirst = false, @@ -147,7 +146,6 @@ export function SiteScanContextProvider( { fetchCachedValidationErrors = false, scannableUrlsRestPath, validateNonce, - validateQueryVar, } ) { const { originalOptions } = useContext( Options ); const { setAsyncError } = useAsyncError(); @@ -162,17 +160,20 @@ export function SiteScanContextProvider( { themeIssues, } = state; + /** + * Preflight check. + */ useEffect( () => { if ( status ) { return; } - if ( ! validateQueryVar || ! validateNonce ) { + if ( ! validateNonce ) { throw new Error( 'Invalid site scan configuration' ); } dispatch( { type: ACTION_SCANNABLE_URLS_REQUEST } ); - }, [ status, validateNonce, validateQueryVar ] ); + }, [ status, validateNonce ] ); /** * This component sets state inside async functions. Use this ref to prevent @@ -252,7 +253,7 @@ export function SiteScanContextProvider( { const url = scannableUrls[ currentlyScannedUrlIndex ][ urlType ]; const args = { 'amp-first': ampFirst || undefined, - [ validateQueryVar ]: { + amp_validate: { cache: cache || undefined, nonce: validateNonce, omit_stylesheets: true, @@ -288,7 +289,7 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_NEXT_URL } ); } )(); - }, [ ampFirst, cache, currentlyScannedUrlIndex, originalOptions?.theme_support, scannableUrls, setAsyncError, status, validateNonce, validateQueryVar ] ); + }, [ ampFirst, cache, currentlyScannedUrlIndex, originalOptions?.theme_support, scannableUrls, setAsyncError, status, validateNonce ] ); return ( { children } diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 3197f9174ab..5abbffe045c 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -13,7 +13,6 @@ import { USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, USERS_RESOURCE_REST_PATH, VALIDATE_NONCE, - VALIDATE_QUERY_VAR, } from 'amp-settings'; /** @@ -96,11 +95,10 @@ function Providers( { children } ) { { children } diff --git a/src/Admin/OnboardingWizardSubmenuPage.php b/src/Admin/OnboardingWizardSubmenuPage.php index 88c3e41a70f..7577b796cb3 100644 --- a/src/Admin/OnboardingWizardSubmenuPage.php +++ b/src/Admin/OnboardingWizardSubmenuPage.php @@ -254,7 +254,6 @@ public function enqueue_assets( $hook_suffix ) { 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', 'VALIDATE_NONCE' => AMP_Validation_Manager::get_amp_validate_nonce(), - 'VALIDATE_QUERY_VAR' => AMP_Validation_Manager::VALIDATE_QUERY_VAR, 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, ]; diff --git a/src/Admin/OptionsMenu.php b/src/Admin/OptionsMenu.php index 8a4b7dbe17c..6d6be333298 100644 --- a/src/Admin/OptionsMenu.php +++ b/src/Admin/OptionsMenu.php @@ -264,7 +264,6 @@ public function enqueue_assets( $hook_suffix ) { 'USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE' => UserRESTEndpointExtension::USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, 'USERS_RESOURCE_REST_PATH' => '/wp/v2/users', 'VALIDATE_NONCE' => AMP_Validation_Manager::get_amp_validate_nonce(), - 'VALIDATE_QUERY_VAR' => AMP_Validation_Manager::VALIDATE_QUERY_VAR, 'VALIDATED_URLS_LINK' => $amp_validated_urls_link, 'HAS_PAGE_CACHING' => $this->site_health->has_page_caching( true ), ]; From d77ce62cf3d8a61e1a1627711902753f4b4b4b3e Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 12 Oct 2021 11:23:53 +0200 Subject: [PATCH 051/120] Minor styling updates --- assets/src/settings-page/site-scan.js | 9 ++++++--- assets/src/settings-page/style.css | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index b082cd6918d..3c13d6007ae 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -28,6 +28,7 @@ import { NOTICE_SIZE_SMALL, NOTICE_TYPE_INFO, NOTICE_TYPE_PLAIN, + NOTICE_TYPE_SUCCESS, } from '../components/amp-notice'; /** @@ -130,9 +131,11 @@ export function SiteScan() { /> ) } { isComplete && ! hasSiteIssues && ( -

      - { __( 'Site scan found no issues on your site.', 'amp' ) } -

      + +

      + { __( 'Site scan found no issues on your site.', 'amp' ) } +

      +
      ) } { themeIssues.length > 0 && ( Date: Tue, 12 Oct 2021 15:09:09 +0200 Subject: [PATCH 052/120] Cancel and invalidate scan when theme support changes --- .../site-scan-context-provider/index.js | 100 +++++---- assets/src/settings-page/site-scan.js | 200 +++++++++++------- 2 files changed, 186 insertions(+), 114 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 49489a76289..34e571185f7 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -3,6 +3,7 @@ */ import { createContext, useCallback, useContext, useEffect, useReducer, useRef } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; +import { usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; /** @@ -27,6 +28,7 @@ const ACTION_START_SITE_SCAN = 'ACTION_START_SITE_SCAN'; const ACTION_SCAN_IN_PROGRESS = 'ACTION_SCAN_IN_PROGRESS'; const ACTION_SCAN_RECEIVE_ISSUES = 'ACTION_SCAN_RECEIVE_ISSUES'; const ACTION_SCAN_NEXT_URL = 'ACTION_SCAN_NEXT_URL'; +const ACTION_SCAN_INVALIDATE = 'ACTION_SCAN_INVALIDATE'; const ACTION_SCAN_CANCEL = 'ACTION_SCAN_CANCEL'; const STATUS_REQUEST_SCANNABLE_URLS = 'STATUS_REQUEST_SCANNABLE_URLS'; @@ -35,6 +37,7 @@ const STATUS_READY = 'STATUS_READY'; const STATUS_IDLE = 'STATUS_IDLE'; const STATUS_IN_PROGRESS = 'STATUS_IN_PROGRESS'; const STATUS_COMPLETE = 'STATUS_COMPLETE'; +const STATUS_CANCELLED = 'STATUS_CANCELLED'; function siteScanReducer( state, action ) { switch ( action.type ) { @@ -71,6 +74,10 @@ function siteScanReducer( state, action ) { }; } case ACTION_START_SITE_SCAN: { + if ( ! [ STATUS_READY, STATUS_COMPLETE, STATUS_CANCELLED ].includes( state.status ) ) { + return state; + } + return { ...state, status: STATUS_IDLE, @@ -101,6 +108,10 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCAN_NEXT_URL: { + if ( state.status === STATUS_CANCELLED ) { + return state; + } + const hasNextUrl = state.currentlyScannedUrlIndex < state.scannableUrls.length - 1; return { ...state, @@ -108,10 +119,27 @@ function siteScanReducer( state, action ) { currentlyScannedUrlIndex: hasNextUrl ? state.currentlyScannedUrlIndex + 1 : state.currentlyScannedUrlIndex, }; } + case ACTION_SCAN_INVALIDATE: { + if ( state.status !== STATUS_COMPLETE ) { + return state; + } + + return { + ...state, + stale: true, + }; + } case ACTION_SCAN_CANCEL: { + if ( ! [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( state.status ) ) { + return state; + } + return { ...state, - status: STATUS_READY, + status: STATUS_CANCELLED, + themeIssues: [], + pluginIssues: [], + currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, }; } default: { @@ -147,7 +175,7 @@ export function SiteScanContextProvider( { scannableUrlsRestPath, validateNonce, } ) { - const { originalOptions } = useContext( Options ); + const { originalOptions: { theme_support: themeSupport } } = useContext( Options ); const { setAsyncError } = useAsyncError(); const [ state, dispatch ] = useReducer( siteScanReducer, initialState ); const { @@ -185,31 +213,36 @@ export function SiteScanContextProvider( { }, [] ); const startSiteScan = useCallback( ( args = {} ) => { - if ( status === STATUS_READY ) { - dispatch( { - type: ACTION_START_SITE_SCAN, - cache: args?.cache, - } ); - } - }, [ status ] ); + dispatch( { + type: ACTION_START_SITE_SCAN, + cache: args?.cache, + } ); + }, [] ); - /** - * Allows cancelling a scan that is in progress. - */ - const hasCanceled = useRef( false ); const cancelSiteScan = useCallback( () => { - hasCanceled.current = true; + dispatch( { type: ACTION_SCAN_CANCEL } ); }, [] ); /** - * Fetch scannable URLs from the REST endpoint. + * Cancel scan and invalidate current results whenever theme mode changes. */ + const previousThemeSupport = usePrevious( themeSupport ); useEffect( () => { - if ( status !== STATUS_REQUEST_SCANNABLE_URLS ) { - return; + if ( previousThemeSupport && previousThemeSupport !== themeSupport ) { + dispatch( { type: ACTION_SCAN_CANCEL } ); + dispatch( { type: ACTION_SCAN_INVALIDATE } ); } + }, [ previousThemeSupport, themeSupport ] ); + /** + * Fetch scannable URLs from the REST endpoint. + */ + useEffect( () => { ( async () => { + if ( status !== STATUS_REQUEST_SCANNABLE_URLS ) { + return; + } + dispatch( { type: ACTION_SCANNABLE_URLS_FETCH } ); try { @@ -238,18 +271,15 @@ export function SiteScanContextProvider( { * Scan site URLs sequentially. */ useEffect( () => { - if ( status !== STATUS_IDLE ) { - return; - } - - /** - * Validates the next URL in the queue. - */ ( async () => { + if ( status !== STATUS_IDLE ) { + return; + } + dispatch( { type: ACTION_SCAN_IN_PROGRESS } ); try { - const urlType = ampFirst || originalOptions?.theme_support === STANDARD ? 'url' : 'amp_url'; + const urlType = ampFirst || themeSupport === STANDARD ? 'url' : 'amp_url'; const url = scannableUrls[ currentlyScannedUrlIndex ][ urlType ]; const args = { 'amp-first': ampFirst || undefined, @@ -267,29 +297,24 @@ export function SiteScanContextProvider( { return; } - if ( true === hasCanceled.current ) { - hasCanceled.current = false; - dispatch( { type: ACTION_SCAN_CANCEL } ); - - return; - } - dispatch( { type: ACTION_SCAN_RECEIVE_ISSUES, validationResults: validationResults.results, } ); - - dispatch( { type: ACTION_SCAN_NEXT_URL } ); } catch ( e ) { + if ( true === hasUnmounted.current ) { + return; + } + const ignoredErrorCodes = [ 'AMP_NOT_REQUESTED', 'AMP_NOT_AVAILABLE' ]; if ( ! e.code || ! ignoredErrorCodes.includes( e.code ) ) { setAsyncError( e ); } + } finally { + dispatch( { type: ACTION_SCAN_NEXT_URL } ); } - - dispatch( { type: ACTION_SCAN_NEXT_URL } ); } )(); - }, [ ampFirst, cache, currentlyScannedUrlIndex, originalOptions?.theme_support, scannableUrls, setAsyncError, status, validateNonce ] ); + }, [ ampFirst, cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, themeSupport, validateNonce ] ); return ( 0 || pluginIssues.length > 0; - const previewPermalink = STANDARD === themeSupport ? HOME_URL : pairedUrlExamples[ pairedUrlStructure ][ 0 ]; - - if ( showSummary ) { + if ( isCancelled ) { return ( - { __( 'Stale results', 'amp' ) } - - ) : null } + initialOpen={ true } + footerContent={ ( + + ) } > -
      - { ! isComplete && ( + +

      + { __( 'Site scan has been cancelled. Try again.', 'amp' ) } +

      +
      + + ); + } + + if ( showSummary ) { + const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; + const previewPermalink = STANDARD === themeSupport ? HOME_URL : pairedUrlExamples[ pairedUrlStructure ][ 0 ]; + const footerContent = isComplete && ! stale + ? ( + + ) + : ( + + ); + + const getMessage = () => { + if ( isReady ) { + return ( + +

      + { stale + ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) + : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) + } +

      +
      + ); + } + + return ( + <> + { stale && (

      - { stale - ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) - : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) - } + { __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) }

      ) } - { isComplete && hasSiteIssues && ( + { hasSiteIssues && (

      template mode recommendations below. Because of plugin issues, you may also want to review and suppress plugins.', 'amp' ), '#template-modes', '#plugin-suppression', @@ -130,69 +171,65 @@ export function SiteScan() { } } /> ) } - { isComplete && ! hasSiteIssues && ( + { ! hasSiteIssues && ! stale && (

      { __( 'Site scan found no issues on your site.', 'amp' ) }

      ) } - { themeIssues.length > 0 && ( - - ) } - { pluginIssues.length > 0 && ( - - ) } -
      - { isComplete - ? ( - - ) - : ( - - ) } -
      -
      + + ); + }; + + return ( + + { __( 'Stale results', 'amp' ) } + + ) : null } + footerContent={ footerContent } + > + { getMessage() } + { themeIssues.length > 0 && ( + + ) } + { pluginIssues.length > 0 && ( + + ) } ); } return ( -
      -

      - { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } -

      - -

      - { isComplete - ? __( 'Scan complete', 'amp' ) - : sprintf( - // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), - currentlyScannedUrlIndex + 1, - scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.label, - ) - } -

      -
      +

      + { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

      + +

      + { isComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.label, + ) + } +

      ); } @@ -200,10 +237,11 @@ export function SiteScan() { /** * Site Scan drawer (settings panel). * - * @param {Object} props Component props. - * @param {any} props.children Component children. + * @param {Object} props Component props. + * @param {any} props.children Component children. + * @param {Object} props.footerContent Component footer content. */ -function SiteScanDrawer( { children, ...props } ) { +function SiteScanDrawer( { children, footerContent, ...props } ) { return ( - { children } +
      + { children } + { footerContent && ( +
      + { footerContent } +
      + ) } +
      ); } SiteScanDrawer.propTypes = { children: PropTypes.any, + footerContent: PropTypes.node, }; From a8e4719137e28cf7f0ecc61aaefdd42461194dee Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 12 Oct 2021 16:27:03 +0200 Subject: [PATCH 053/120] Prevent Site Scan container re-rendering on status change --- assets/src/settings-page/site-scan.js | 305 +++++++++++++------------- 1 file changed, 148 insertions(+), 157 deletions(-) diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 2d0e07a3526..11895cab3b9 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -38,17 +38,12 @@ import { export function SiteScan() { const { cancelSiteScan, - currentlyScannedUrlIndex, - isBusy, isCancelled, isComplete, isInitializing, isReady, - pluginIssues, - scannableUrls, stale, startSiteScan, - themeIssues, } = useContext( SiteScanContext ); const { originalOptions } = useContext( Options ); const { @@ -56,180 +51,72 @@ export function SiteScan() { paired_url_structure: pairedUrlStructure, theme_support: themeSupport, } = originalOptions; + const previewPermalink = STANDARD === themeSupport ? HOME_URL : pairedUrlExamples[ pairedUrlStructure ][ 0 ]; /** - * Cancel scan on component unmount. + * Cancel scan when component unmounts. */ useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); /** - * Show scan summary with a delay so that the progress bar has a chance to - * complete. + * Delay the `isComplete` state so that the progress bar stays at 100% for a + * brief moment. */ - const [ showSummary, setShowSummary ] = useState( true ); + const [ isDelayedComplete, setIsDelayedComplete ] = useState( isComplete ); useEffect( () => { - let timeout; + let cleanup = () => {}; - if ( ( isReady || isComplete ) && ! showSummary ) { - timeout = setTimeout( () => setShowSummary( true ), 500 ); + if ( isComplete && ! isDelayedComplete ) { + cleanup = setTimeout( () => setIsDelayedComplete( true ), 500 ); + } else if ( ! isComplete && isDelayedComplete ) { + setIsDelayedComplete( false ); } - return () => { - if ( timeout ) { - clearTimeout( timeout ); - } - }; - }, [ isComplete, isReady, showSummary ] ); + return cleanup; + }, [ isComplete, isDelayedComplete ] ); - useEffect( () => { - if ( showSummary && isBusy ) { - setShowSummary( false ); - } - }, [ isBusy, showSummary ] ); + /** + * Determine footer content. + */ + let footerContent = null; - if ( isInitializing ) { - return ( - - - + if ( isCancelled || ( stale && ( isReady || isDelayedComplete ) ) ) { + footerContent = ( + + ); + } else if ( ! stale && isDelayedComplete ) { + footerContent = ( + ); } - if ( isCancelled ) { - return ( - startSiteScan( { cache: true } ) } - isPrimary={ true } - > - { __( 'Rescan Site', 'amp' ) } - - ) } - > + return ( + + { __( 'Stale results', 'amp' ) } + + ) : null } + footerContent={ footerContent } + > + { isInitializing && } + { isCancelled && (

      { __( 'Site scan has been cancelled. Try again.', 'amp' ) }

      -
      - ); - } - - if ( showSummary ) { - const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; - const previewPermalink = STANDARD === themeSupport ? HOME_URL : pairedUrlExamples[ pairedUrlStructure ][ 0 ]; - const footerContent = isComplete && ! stale - ? ( - - ) - : ( - - ); - - const getMessage = () => { - if ( isReady ) { - return ( - -

      - { stale - ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) - : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) - } -

      -
      - ); - } - - return ( - <> - { stale && ( - -

      - { __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) } -

      -
      - ) } - { hasSiteIssues && ( -

      template mode recommendations below. Because of plugin issues, you may also want to review and suppress plugins.', 'amp' ), - '#template-modes', - '#plugin-suppression', - ), - } } - /> - ) } - { ! hasSiteIssues && ! stale && ( - -

      - { __( 'Site scan found no issues on your site.', 'amp' ) } -

      - - ) } - - ); - }; - - return ( - - { __( 'Stale results', 'amp' ) } - - ) : null } - footerContent={ footerContent } - > - { getMessage() } - { themeIssues.length > 0 && ( - - ) } - { pluginIssues.length > 0 && ( - - ) } - - ); - } - - return ( - -

      - { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } -

      - -

      - { isComplete - ? __( 'Scan complete', 'amp' ) - : sprintf( - // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. - __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), - currentlyScannedUrlIndex + 1, - scannableUrls.length, - scannableUrls[ currentlyScannedUrlIndex ]?.label, - ) - } -

      + ) } + { ( isReady || isDelayedComplete ) ? : }
      ); } @@ -269,3 +156,107 @@ SiteScanDrawer.propTypes = { children: PropTypes.any, footerContent: PropTypes.node, }; + +/** + * Site Scan - in progress state. + */ +function SiteScanInProgress() { + const { + currentlyScannedUrlIndex, + isComplete, + scannableUrls, + } = useContext( SiteScanContext ); + + return ( + <> +

      + { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) } +

      + +

      + { isComplete + ? __( 'Scan complete', 'amp' ) + : sprintf( + // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. + __( 'Scanning %1$d/%2$d URLs: Checking %3$s…', 'amp' ), + currentlyScannedUrlIndex + 1, + scannableUrls.length, + scannableUrls[ currentlyScannedUrlIndex ]?.label, + ) + } +

      + + ); +} + +/** + * Site Scan - summary state. + */ +function SiteScanSummary() { + const { + isReady, + pluginIssues, + stale, + themeIssues, + } = useContext( SiteScanContext ); + const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; + + return ( + <> + { isReady ? ( + +

      + { stale + ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) + : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) + } +

      +
      + ) : ( + <> + { stale && ( + +

      + { __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) } +

      +
      + ) } + { hasSiteIssues && ( +

      template mode recommendations below. Because of plugin issues, you may also want to review and suppress plugins.', 'amp' ), + '#template-modes', + '#plugin-suppression', + ), + } } + /> + ) } + { ! hasSiteIssues && ! stale && ( + +

      + { __( 'Site scan found no issues on your site.', 'amp' ) } +

      + + ) } + + ) } + { themeIssues.length > 0 && ( + + ) } + { pluginIssues.length > 0 && ( + + ) } + + ); +} From ee1417156f5d356e912849d4c98859fea7695164 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 12 Oct 2021 17:01:54 +0200 Subject: [PATCH 054/120] Unify naming convention; refactor delayed flag into a custom hook --- .../site-scan-context-provider/index.js | 26 +++++------ .../pages/site-scan/index.js | 44 +++++++------------ assets/src/settings-page/site-scan.js | 35 +++++---------- assets/src/utils/use-delayed-flag.js | 33 ++++++++++++++ 4 files changed, 74 insertions(+), 64 deletions(-) create mode 100644 assets/src/utils/use-delayed-flag.js diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 34e571185f7..99423f66256 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, useCallback, useContext, useEffect, useReducer, useRef } from '@wordpress/element'; +import { createContext, useCallback, useContext, useEffect, useReducer, useRef, useState } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; @@ -24,8 +24,8 @@ export const SiteScan = createContext(); const ACTION_SCANNABLE_URLS_REQUEST = 'ACTION_SCANNABLE_URLS_REQUEST'; const ACTION_SCANNABLE_URLS_FETCH = 'ACTION_SCANNABLE_URLS_FETCH'; const ACTION_SCANNABLE_URLS_RECEIVE = 'ACTION_SCANNABLE_URLS_RECEIVE'; -const ACTION_START_SITE_SCAN = 'ACTION_START_SITE_SCAN'; -const ACTION_SCAN_IN_PROGRESS = 'ACTION_SCAN_IN_PROGRESS'; +const ACTION_SCAN_INITIALIZE = 'ACTION_SCAN_INITIALIZE'; +const ACTION_SCAN_VALIDATE_URL = 'ACTION_SCAN_VALIDATE_URL'; const ACTION_SCAN_RECEIVE_ISSUES = 'ACTION_SCAN_RECEIVE_ISSUES'; const ACTION_SCAN_NEXT_URL = 'ACTION_SCAN_NEXT_URL'; const ACTION_SCAN_INVALIDATE = 'ACTION_SCAN_INVALIDATE'; @@ -36,7 +36,7 @@ const STATUS_FETCHING_SCANNABLE_URLS = 'STATUS_FETCHING_SCANNABLE_URLS'; const STATUS_READY = 'STATUS_READY'; const STATUS_IDLE = 'STATUS_IDLE'; const STATUS_IN_PROGRESS = 'STATUS_IN_PROGRESS'; -const STATUS_COMPLETE = 'STATUS_COMPLETE'; +const STATUS_COMPLETED = 'STATUS_COMPLETED'; const STATUS_CANCELLED = 'STATUS_CANCELLED'; function siteScanReducer( state, action ) { @@ -57,7 +57,7 @@ function siteScanReducer( state, action ) { if ( ! action?.scannableUrls?.length || action.scannableUrls.length === 0 ) { return { ...state, - status: STATUS_COMPLETE, + status: STATUS_COMPLETED, }; } @@ -73,8 +73,8 @@ function siteScanReducer( state, action ) { themeIssues: [ ...new Set( [ ...state.themeIssues, ...siteIssues.themeIssues ] ) ], }; } - case ACTION_START_SITE_SCAN: { - if ( ! [ STATUS_READY, STATUS_COMPLETE, STATUS_CANCELLED ].includes( state.status ) ) { + case ACTION_SCAN_INITIALIZE: { + if ( ! [ STATUS_READY, STATUS_COMPLETED, STATUS_CANCELLED ].includes( state.status ) ) { return state; } @@ -88,7 +88,7 @@ function siteScanReducer( state, action ) { currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, }; } - case ACTION_SCAN_IN_PROGRESS: { + case ACTION_SCAN_VALIDATE_URL: { return { ...state, status: STATUS_IN_PROGRESS, @@ -115,12 +115,12 @@ function siteScanReducer( state, action ) { const hasNextUrl = state.currentlyScannedUrlIndex < state.scannableUrls.length - 1; return { ...state, - status: hasNextUrl ? STATUS_IDLE : STATUS_COMPLETE, + status: hasNextUrl ? STATUS_IDLE : STATUS_COMPLETED, currentlyScannedUrlIndex: hasNextUrl ? state.currentlyScannedUrlIndex + 1 : state.currentlyScannedUrlIndex, }; } case ACTION_SCAN_INVALIDATE: { - if ( state.status !== STATUS_COMPLETE ) { + if ( state.status !== STATUS_COMPLETED ) { return state; } @@ -214,7 +214,7 @@ export function SiteScanContextProvider( { const startSiteScan = useCallback( ( args = {} ) => { dispatch( { - type: ACTION_START_SITE_SCAN, + type: ACTION_SCAN_INITIALIZE, cache: args?.cache, } ); }, [] ); @@ -276,7 +276,7 @@ export function SiteScanContextProvider( { return; } - dispatch( { type: ACTION_SCAN_IN_PROGRESS } ); + dispatch( { type: ACTION_SCAN_VALIDATE_URL } ); try { const urlType = ampFirst || themeSupport === STANDARD ? 'url' : 'amp_url'; @@ -323,7 +323,7 @@ export function SiteScanContextProvider( { currentlyScannedUrlIndex, isBusy: [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( status ), isCancelled: status === STATUS_CANCELLED, - isComplete: status === STATUS_COMPLETE, + isCompleted: status === STATUS_COMPLETED, isInitializing: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), isReady: status === STATUS_READY, stale, diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index c53290579c7..5e0543ffdca 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; /** * WordPress dependencies */ -import { useContext, useEffect, useMemo, useState } from '@wordpress/element'; +import { useContext, useEffect, useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -22,6 +22,7 @@ import { Selectable } from '../../../components/selectable'; import { IconLandscapeHillsCogs } from '../../../components/svg/landscape-hills-cogs'; import { ProgressBar } from '../../../components/progress-bar'; import { PluginsWithIssues, ThemesWithIssues } from '../../../components/site-scan-results'; +import useDelayedFlag from '../../../utils/use-delayed-flag'; /** * Screen for visualizing a site scan. @@ -29,11 +30,12 @@ import { PluginsWithIssues, ThemesWithIssues } from '../../../components/site-sc export function SiteScan() { const { setCanGoForward } = useContext( Navigation ); const { - isInitializing, - isReady, - isComplete, cancelSiteScan, currentlyScannedUrlIndex, + isCancelled, + isCompleted, + isInitializing, + isReady, pluginIssues, scannableUrls, startSiteScan, @@ -47,39 +49,25 @@ export function SiteScan() { useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); useEffect( () => { - if ( isReady ) { + if ( isReady || isCancelled ) { startSiteScan(); } - }, [ isReady, startSiteScan ] ); + }, [ isCancelled, isReady, startSiteScan ] ); /** * Allow moving forward. */ useEffect( () => { - if ( isComplete ) { + if ( isCompleted ) { setCanGoForward( true ); } - }, [ isComplete, setCanGoForward ] ); + }, [ isCompleted, setCanGoForward ] ); /** - * Show scan summary with a delay so that the progress bar has a chance to - * complete. + * Delay the `isCompleted` flag so that the progress bar stays at 100% for a + * brief moment. */ - const [ showSummary, setShowSummary ] = useState( isComplete ); - - useEffect( () => { - let timeout; - - if ( isComplete && ! showSummary ) { - timeout = setTimeout( () => setShowSummary( true ), 500 ); - } - - return () => { - if ( timeout ) { - clearTimeout( timeout ); - } - }; - }, [ showSummary, isComplete ] ); + const isDelayedCompleted = useDelayedFlag( isCompleted ); if ( isInitializing ) { return ( @@ -90,7 +78,7 @@ export function SiteScan() { ); } - if ( showSummary ) { + if ( isDelayedCompleted ) { return ( { __( 'Site scan is checking if there are AMP compatibility issues with your active theme and plugins. We’ll then recommend how to use the AMP plugin.', 'amp' ) }

      -

      - { isComplete + { isCompleted ? __( 'Scan complete', 'amp' ) : sprintf( // translators: 1: currently scanned URL index; 2: scannable URLs count; 3: scanned page type. diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 11895cab3b9..7bd74f31ccd 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; */ import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { useContext, useEffect, useState } from '@wordpress/element'; +import { useContext, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -31,6 +31,7 @@ import { NOTICE_TYPE_PLAIN, NOTICE_TYPE_SUCCESS, } from '../components/amp-notice'; +import useDelayedFlag from '../utils/use-delayed-flag'; /** * Site Scan component on the settings screen. @@ -39,7 +40,7 @@ export function SiteScan() { const { cancelSiteScan, isCancelled, - isComplete, + isCompleted, isInitializing, isReady, stale, @@ -59,29 +60,17 @@ export function SiteScan() { useEffect( () => () => cancelSiteScan(), [ cancelSiteScan ] ); /** - * Delay the `isComplete` state so that the progress bar stays at 100% for a + * Delay the `isCompleted` flag so that the progress bar stays at 100% for a * brief moment. */ - const [ isDelayedComplete, setIsDelayedComplete ] = useState( isComplete ); - - useEffect( () => { - let cleanup = () => {}; - - if ( isComplete && ! isDelayedComplete ) { - cleanup = setTimeout( () => setIsDelayedComplete( true ), 500 ); - } else if ( ! isComplete && isDelayedComplete ) { - setIsDelayedComplete( false ); - } - - return cleanup; - }, [ isComplete, isDelayedComplete ] ); + const isDelayedCompleted = useDelayedFlag( isCompleted ); /** * Determine footer content. */ let footerContent = null; - if ( isCancelled || ( stale && ( isReady || isDelayedComplete ) ) ) { + if ( isCancelled || ( stale && ( isReady || isDelayedCompleted ) ) ) { footerContent = ( ); - } else if ( ! stale && isDelayedComplete ) { + } else if ( ! stale && isDelayedCompleted ) { footerContent = ( - ); - } else if ( ! stale && isDelayedCompleted ) { - footerContent = ( - - ); - } + const getFooterContent = useCallback( () => { + if ( isCancelled || ( stale && ( isReady || isDelayedCompleted ) ) ) { + return ( + + ); + } + + if ( ! stale && isDelayedCompleted ) { + return ( + + ); + } + + return null; + }, [ isCancelled, isDelayedCompleted, isReady, previewPermalink, stale, startSiteScan ] ); + + /** + * Get main content. + */ + const getContent = useCallback( () => { + if ( isInitializing ) { + return ; + } + + if ( isCancelled ) { + return ( + +

      + { __( 'Site scan has been cancelled. Try again.', 'amp' ) } +

      + + ); + } + + if ( isReady || isDelayedCompleted ) { + return ; + } + + return ; + }, [ isCancelled, isDelayedCompleted, isInitializing, isReady ] ); return ( ) : null } - footerContent={ footerContent } + footerContent={ getFooterContent() } > - { isInitializing && } - { isCancelled && ( - -

      - { __( 'Site scan has been cancelled. Try again.', 'amp' ) } -

      -
      - ) } - { ( isReady || isDelayedCompleted ) ? : } + { getContent() }
      ); } From 8c1350c6fc4a0578d7761a1532b8deac45219711 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 13 Oct 2021 11:58:34 +0200 Subject: [PATCH 058/120] Apply suggestions from code review Co-authored-by: Weston Ruter --- assets/src/components/site-scan-context-provider/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 89195d8a649..8e1b967fbae 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -287,11 +287,10 @@ export function SiteScanContextProvider( { cache: cache || undefined, nonce: validateNonce, omit_stylesheets: true, + cache_bust: Math.random(), }, }; - const validationResults = await apiFetch( { - url: addQueryArgs( url, args ), - } ); + const validationResults = await fetch( addQueryArgs( url, args ) ); if ( true === hasUnmounted.current ) { return; From 25a552a898975de24d15af1782c10b1c36ebde04 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 13 Oct 2021 15:21:56 +0200 Subject: [PATCH 059/120] Simplify site scan reducer and add failed scan state - Store validation errors directly inside the `scannableUrl` array and derive theme and plugin issues, and staleness flag from that array. - Store HTTP request errors in the state so that it's possible to find out if the entire scan was unsuccessful (every `scannableUrl` has an `error` set). - Handle `isFailed` status in the UI. --- .../get-site-issues.js | 6 +- .../site-scan-context-provider/index.js | 134 ++++++++++-------- .../pages/site-scan/index.js | 23 ++- assets/src/settings-page/site-scan.js | 17 ++- 4 files changed, 115 insertions(+), 65 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/get-site-issues.js b/assets/src/components/site-scan-context-provider/get-site-issues.js index 28964a4d80f..529376e6ad4 100644 --- a/assets/src/components/site-scan-context-provider/get-site-issues.js +++ b/assets/src/components/site-scan-context-provider/get-site-issues.js @@ -9,13 +9,11 @@ export function getSiteIssues( validationResults = [] ) { const themeIssues = new Set(); for ( const result of validationResults ) { - const sources = result?.error?.sources ?? result?.sources; - - if ( ! sources ) { + if ( ! result.sources ) { continue; } - for ( const source of sources ) { + for ( const source of result.sources ) { if ( source.type === 'plugin' && source.name !== 'amp' ) { pluginIssues.add( source.name.match( /(.*?)(?:\.php)?$/ )[ 1 ] ); } else if ( source.type === 'theme' ) { diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 8e1b967fbae..86e914c542f 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createContext, useCallback, useContext, useEffect, useReducer, useRef } from '@wordpress/element'; +import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { usePrevious } from '@wordpress/compose'; import { addQueryArgs } from '@wordpress/url'; @@ -14,9 +14,9 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ +import { STANDARD } from '../../common/constants'; import { useAsyncError } from '../../utils/use-async-error'; import { Options } from '../options-context-provider'; -import { STANDARD } from '../../common/constants'; import { getSiteIssues } from './get-site-issues'; export const SiteScan = createContext(); @@ -26,7 +26,7 @@ const ACTION_SCANNABLE_URLS_FETCH = 'ACTION_SCANNABLE_URLS_FETCH'; const ACTION_SCANNABLE_URLS_RECEIVE = 'ACTION_SCANNABLE_URLS_RECEIVE'; const ACTION_SCAN_INITIALIZE = 'ACTION_SCAN_INITIALIZE'; const ACTION_SCAN_VALIDATE_URL = 'ACTION_SCAN_VALIDATE_URL'; -const ACTION_SCAN_RECEIVE_ISSUES = 'ACTION_SCAN_RECEIVE_ISSUES'; +const ACTION_SCAN_RECEIVE_VALIDATION_ERRORS = 'ACTION_SCAN_RECEIVE_VALIDATION_ERRORS'; const ACTION_SCAN_NEXT_URL = 'ACTION_SCAN_NEXT_URL'; const ACTION_SCAN_INVALIDATE = 'ACTION_SCAN_INVALIDATE'; const ACTION_SCAN_CANCEL = 'ACTION_SCAN_CANCEL'; @@ -37,6 +37,7 @@ const STATUS_READY = 'STATUS_READY'; const STATUS_IDLE = 'STATUS_IDLE'; const STATUS_IN_PROGRESS = 'STATUS_IN_PROGRESS'; const STATUS_COMPLETED = 'STATUS_COMPLETED'; +const STATUS_FAILED = 'STATUS_FAILED'; const STATUS_CANCELLED = 'STATUS_CANCELLED'; function siteScanReducer( state, action ) { @@ -54,37 +55,28 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCANNABLE_URLS_RECEIVE: { - if ( ! action?.scannableUrls?.length || action.scannableUrls.length === 0 ) { + if ( action?.scannableUrls?.length > 0 ) { return { ...state, - status: STATUS_COMPLETED, + status: STATUS_READY, + scannableUrls: action.scannableUrls, }; } - const validationErrors = action.scannableUrls.reduce( ( acc, data ) => [ ...acc, ...data?.validation_errors ?? [] ], [] ); - const siteIssues = getSiteIssues( validationErrors ); - return { ...state, - status: STATUS_READY, - scannableUrls: action.scannableUrls, - stale: Boolean( action.scannableUrls.find( ( error ) => error?.stale === true ) ), - pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...siteIssues.pluginIssues ] ) ], - themeIssues: [ ...new Set( [ ...state.themeIssues, ...siteIssues.themeIssues ] ) ], + status: STATUS_COMPLETED, }; } case ACTION_SCAN_INITIALIZE: { - if ( ! [ STATUS_READY, STATUS_COMPLETED, STATUS_CANCELLED ].includes( state.status ) ) { + if ( ! [ STATUS_READY, STATUS_COMPLETED, STATUS_FAILED, STATUS_CANCELLED ].includes( state.status ) ) { return state; } return { ...state, status: STATUS_IDLE, - stale: false, cache: action.cache, - themeIssues: [], - pluginIssues: [], currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, }; } @@ -94,17 +86,21 @@ function siteScanReducer( state, action ) { status: STATUS_IN_PROGRESS, }; } - case ACTION_SCAN_RECEIVE_ISSUES: { - if ( ! action?.validationResults?.length || action.validationResults.length === 0 ) { - return state; - } - - const siteIssues = getSiteIssues( action.validationResults ); - + case ACTION_SCAN_RECEIVE_VALIDATION_ERRORS: { return { ...state, - pluginIssues: [ ...new Set( [ ...state.pluginIssues, ...siteIssues.pluginIssues ] ) ], - themeIssues: [ ...new Set( [ ...state.themeIssues, ...siteIssues.themeIssues ] ) ], + scannableUrls: [ + ...state.scannableUrls.slice( 0, action.scannedUrlIndex ), + { + ...state.scannableUrls[ action.scannedUrlIndex ], + stale: false, + error: action.error ?? false, + revalidated: ! Boolean( action.error ), + validated_url_post: action.error ? {} : action.validatedUrlPost, + validation_errors: action.error ? [] : action.validationErrors, + }, + ...state.scannableUrls.slice( action.scannedUrlIndex + 1 ), + ], }; } case ACTION_SCAN_NEXT_URL: { @@ -112,21 +108,33 @@ function siteScanReducer( state, action ) { return state; } - const hasNextUrl = state.currentlyScannedUrlIndex < state.scannableUrls.length - 1; + if ( state.currentlyScannedUrlIndex < state.scannableUrls.length - 1 ) { + return { + ...state, + status: STATUS_IDLE, + currentlyScannedUrlIndex: state.currentlyScannedUrlIndex + 1, + }; + } + + const hasFailed = state.scannableUrls.every( ( scannableUrl ) => Boolean( scannableUrl.error ) ); + return { ...state, - status: hasNextUrl ? STATUS_IDLE : STATUS_COMPLETED, - currentlyScannedUrlIndex: hasNextUrl ? state.currentlyScannedUrlIndex + 1 : state.currentlyScannedUrlIndex, + status: hasFailed ? STATUS_FAILED : STATUS_COMPLETED, }; } case ACTION_SCAN_INVALIDATE: { - if ( state.status !== STATUS_COMPLETED ) { + if ( ! [ STATUS_COMPLETED, STATUS_FAILED ].includes( state.status ) ) { return state; } return { ...state, - stale: true, + scannableUrls: state.scannableUrls.map( ( scannableUrl ) => ( { + ...scannableUrl, + stale: true, + revalidated: false, + } ) ), }; } case ACTION_SCAN_CANCEL: { @@ -137,8 +145,6 @@ function siteScanReducer( state, action ) { return { ...state, status: STATUS_CANCELLED, - themeIssues: [], - pluginIssues: [], currentlyScannedUrlIndex: initialState.currentlyScannedUrlIndex, }; } @@ -149,13 +155,10 @@ function siteScanReducer( state, action ) { } const initialState = { - themeIssues: [], - pluginIssues: [], - status: '', - scannableUrls: [], - stale: false, cache: false, currentlyScannedUrlIndex: 0, + scannableUrls: [], + status: '', }; /** @@ -181,13 +184,24 @@ export function SiteScanContextProvider( { const { cache, currentlyScannedUrlIndex, - pluginIssues, scannableUrls, - stale, status, - themeIssues, } = state; + /** + * Memoize plugin and theme issues. + */ + const { pluginIssues, themeIssues, stale } = useMemo( () => { + const validationErrors = scannableUrls.reduce( ( acc, scannableUrl ) => [ ...acc, ...scannableUrl?.validation_errors ?? [] ], [] ); + const siteIssues = getSiteIssues( validationErrors ); + + return { + pluginIssues: siteIssues.pluginIssues, + themeIssues: siteIssues.themeIssues, + stale: Boolean( scannableUrls.find( ( scannableUrl ) => scannableUrl?.stale === true ) ), + }; + }, [ scannableUrls ] ); + /** * Preflight check. */ @@ -290,28 +304,35 @@ export function SiteScanContextProvider( { cache_bust: Math.random(), }, }; - const validationResults = await fetch( addQueryArgs( url, args ) ); - if ( true === hasUnmounted.current ) { - return; - } + const response = await fetch( addQueryArgs( url, args ) ); + const data = await response.json(); - dispatch( { - type: ACTION_SCAN_RECEIVE_ISSUES, - validationResults: validationResults.results, - } ); - } catch ( e ) { if ( true === hasUnmounted.current ) { return; } - const ignoredErrorCodes = [ 'AMP_NOT_REQUESTED', 'AMP_NOT_AVAILABLE' ]; - if ( ! e.code || ! ignoredErrorCodes.includes( e.code ) ) { - setAsyncError( e ); + if ( response.ok ) { + dispatch( { + type: ACTION_SCAN_RECEIVE_VALIDATION_ERRORS, + scannedUrlIndex: currentlyScannedUrlIndex, + revalidated: data.revalidated, + validatedUrlPost: data.validated_url_post, + validationErrors: data.results.map( ( { error } ) => error ), + } ); + } else { + dispatch( { + type: ACTION_SCAN_RECEIVE_VALIDATION_ERRORS, + scannedUrlIndex: currentlyScannedUrlIndex, + error: data?.code || true, + } ); } - } finally { - dispatch( { type: ACTION_SCAN_NEXT_URL } ); + } catch ( e ) { + // Note that this doesn't catch failed HTTP responses. + setAsyncError( e ); } + + dispatch( { type: ACTION_SCAN_NEXT_URL } ); } )(); }, [ ampFirst, cache, currentlyScannedUrlIndex, scannableUrls, setAsyncError, status, themeSupport, validateNonce ] ); @@ -323,11 +344,12 @@ export function SiteScanContextProvider( { isBusy: [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( status ), isCancelled: status === STATUS_CANCELLED, isCompleted: status === STATUS_COMPLETED, + isFailed: status === STATUS_FAILED, isInitializing: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), isReady: status === STATUS_READY, - stale, pluginIssues, scannableUrls, + stale, startSiteScan, themeIssues, } } diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index 5e0543ffdca..c1f65321a17 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -34,6 +34,7 @@ export function SiteScan() { currentlyScannedUrlIndex, isCancelled, isCompleted, + isFailed, isInitializing, isReady, pluginIssues, @@ -58,10 +59,10 @@ export function SiteScan() { * Allow moving forward. */ useEffect( () => { - if ( isCompleted ) { + if ( isCompleted || isFailed ) { setCanGoForward( true ); } - }, [ isCompleted, setCanGoForward ] ); + }, [ isCompleted, isFailed, setCanGoForward ] ); /** * Delay the `isCompleted` flag so that the progress bar stays at 100% for a @@ -78,6 +79,24 @@ export function SiteScan() { ); } + if ( isFailed ) { + return ( + +

      + { __( 'Site scan was unsuccessful.', 'amp' ) } +

      +

      + { __( 'You can trigger the site scan again on the AMP Settings page after completing the Wizard.', 'amp' ) } +

      + + ) } + /> + ); + } + if ( isDelayedCompleted ) { return ( { - if ( isCancelled || ( stale && ( isReady || isDelayedCompleted ) ) ) { + if ( isCancelled || isFailed || ( stale && ( isReady || isDelayedCompleted ) ) ) { return (
      - { sources.length === 0 - ? - : } + { children } { validatedUrlsLink && (

      @@ -68,10 +64,10 @@ export function SiteScanResults( { } SiteScanResults.propTypes = { + children: PropTypes.any, count: PropTypes.number, className: PropTypes.string, icon: PropTypes.element, - sources: PropTypes.array, title: PropTypes.string, validatedUrlsLink: PropTypes.string, }; diff --git a/assets/src/components/site-scan-results/site-scan-sources-list.js b/assets/src/components/site-scan-results/site-scan-sources-list.js new file mode 100644 index 00000000000..e95eef14566 --- /dev/null +++ b/assets/src/components/site-scan-results/site-scan-sources-list.js @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import PropTypes from 'prop-types'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { Loading } from '../loading'; +import { AMPNotice, NOTICE_TYPE_PLAIN, NOTICE_SIZE_SMALL } from '../amp-notice'; + +/** + * Site Scan sources list component. + * + * @param {Object} props Component props. + * @param {Array} props.sources Sources data. + * @param {string} props.inactiveSourceNotice Message to show next to an inactive source. + * @param {string} props.uninstalledSourceNotice Message to show next to an uninstalled source. + */ +export function SiteScanSourcesList( { + sources, + inactiveSourceNotice, + uninstalledSourceNotice, +} ) { + if ( sources.length === 0 ) { + return ; + } + + return ( +

        + { sources.map( ( { author, name, slug, status, version } ) => ( +
      • + { name && ( + + { name } + + ) } + { ! name && slug && ( + + { slug } + + ) } + { status === 'active' ? ( + <> + { author && ( + + { sprintf( + // translators: %s is an author name. + __( 'by %s', 'amp' ), + author, + ) } + + ) } + { version && ( + + { sprintf( + // translators: %s is a version number. + __( 'Version %s', 'amp' ), + version, + ) } + + ) } + + ) : ( + + { status === 'inactive' ? inactiveSourceNotice : null } + { status === 'uninstalled' ? uninstalledSourceNotice : null } + + ) } +
      • + ) ) } +
      + ); +} + +SiteScanSourcesList.propTypes = { + sources: PropTypes.array.isRequired, + inactiveSourceNotice: PropTypes.string, + uninstalledSourceNotice: PropTypes.string, +}; diff --git a/assets/src/components/site-scan-results/sources-list.js b/assets/src/components/site-scan-results/sources-list.js deleted file mode 100644 index e3eccbb03b8..00000000000 --- a/assets/src/components/site-scan-results/sources-list.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * External dependencies - */ -import PropTypes from 'prop-types'; - -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; - -/** - * Sources list component. - * - * @param {Object} props Component props. - * @param {Array} props.sources Sources data. - */ -export function SourcesList( { sources } ) { - return ( -
        - { sources.map( ( { name, author, version } ) => ( -
      • - - { name } - - { author && ( - - { sprintf( - // translators: %s is an author name. - __( 'by %s', 'amp' ), - author, - ) } - - ) } - { version && ( - - { sprintf( - // translators: %s is a version number. - __( 'Version %s', 'amp' ), - version, - ) } - - ) } -
      • - ) ) } -
      - ); -} - -SourcesList.propTypes = { - sources: PropTypes.array.isRequired, -}; diff --git a/assets/src/components/site-scan-results/style.scss b/assets/src/components/site-scan-results/style.scss index 78fc2db3256..c8868462765 100644 --- a/assets/src/components/site-scan-results/style.scss +++ b/assets/src/components/site-scan-results/style.scss @@ -55,6 +55,7 @@ flex-flow: row nowrap; font-size: 14px; margin: 0; + min-height: 3.5rem; padding: 1rem; &:nth-child(even) { @@ -70,6 +71,10 @@ font-weight: 700; } +.site-scan-results__source-name--inactive { + color: var(--gray); +} + .site-scan-results__source-author::before { border-left: 1px solid; content: ""; @@ -79,7 +84,8 @@ vertical-align: middle; } -.site-scan-results__source-version { +.site-scan-results__source-version, +.site-scan-results__source-notice { margin-left: auto; } diff --git a/assets/src/components/site-scan-results/themes-with-issues.js b/assets/src/components/site-scan-results/themes-with-issues.js index 7cb1324de9a..e874a6385d4 100644 --- a/assets/src/components/site-scan-results/themes-with-issues.js +++ b/assets/src/components/site-scan-results/themes-with-issues.js @@ -8,25 +8,29 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { useMemo } from '@wordpress/element'; /** * Internal dependencies */ import { useNormalizedThemesData } from '../themes-context-provider/use-normalized-themes-data'; import { IconWebsitePaintBrush } from '../svg/website-paint-brush'; +import { SiteScanSourcesList } from './site-scan-sources-list'; import { SiteScanResults } from './index'; /** * Renders a list of themes that cause issues. * - * @param {Object} props Component props. - * @param {string} props.className Component class name. - * @param {Array} props.issues List of theme issues. - * @param {string} props.validatedUrlsLink URL to the Validated URLs page. + * @param {Object} props Component props. + * @param {string} props.className Component class name. + * @param {Array} props.issues List of theme issues. */ -export function ThemesWithIssues( { issues = [], validatedUrlsLink, className, ...props } ) { +export function ThemesWithIssues( { issues = [], className, ...props } ) { const themesData = useNormalizedThemesData(); - const sources = issues?.map( ( slug ) => themesData?.[ slug ] ?? { name: slug } ) || []; + const sources = useMemo( () => issues?.map( ( slug ) => themesData?.[ slug ] ?? { + slug, + status: 'uninstalled', + } ) || [], [ issues, themesData ] ); return ( } count={ issues.length } sources={ sources } - validatedUrlsLink={ validatedUrlsLink } className={ classnames( 'site-scan-results--themes', className ) } { ...props } - /> + > + + ); } diff --git a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js index 2d2ed4c90b4..7e107ff877b 100644 --- a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js @@ -20,13 +20,10 @@ jest.mock( '../index' ); let returnValue = {}; -function ComponentContainingHook( { skipInactive } ) { - returnValue = useNormalizedThemesData( { skipInactive } ); +function ComponentContainingHook() { + returnValue = useNormalizedThemesData(); return null; } -ComponentContainingHook.propTypes = { - skipInactive: PropTypes.bool, -}; const Providers = ( { children, fetchingThemes, themes = [] } ) => ( @@ -105,17 +102,18 @@ describe( 'useNormalizedThemesData', () => { }, ] } > - + , container, ); } ); - expect( returnValue ).toMatchObject( { + expect( returnValue ).toStrictEqual( { twentyfifteen: { author: 'the WordPress team', author_uri: 'https://wordpress.org/', name: 'Twenty Fifteen', + slug: 'twentyfifteen', stylesheet: 'twentyfifteen', status: 'inactive', version: '3.0', @@ -124,40 +122,11 @@ describe( 'useNormalizedThemesData', () => { author: 'the WordPress team', author_uri: 'https://wordpress.org/', name: 'Twenty Twenty', + slug: 'twentytwenty', stylesheet: 'twentytwenty', status: 'active', version: '1.7', }, } ); } ); - - it( 'skips inactive plugins', () => { - act( () => { - render( - - - , - container, - ); - } ); - - expect( returnValue ).toMatchObject( { - twentytwenty: { - stylesheet: 'twentytwenty', - status: 'active', - }, - } ); - } ); } ); diff --git a/assets/src/components/themes-context-provider/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/use-normalized-themes-data.js index 36a35c9ce2a..b22ac40167f 100644 --- a/assets/src/components/themes-context-provider/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/use-normalized-themes-data.js @@ -8,7 +8,7 @@ import { useContext, useEffect, useState } from '@wordpress/element'; */ import { Themes } from './index'; -export function useNormalizedThemesData( { skipInactive = true } = {} ) { +export function useNormalizedThemesData() { const { fetchingThemes, themes } = useContext( Themes ); const [ normalizedThemesData, setNormalizedThemesData ] = useState( [] ); @@ -18,26 +18,21 @@ export function useNormalizedThemesData( { skipInactive = true } = {} ) { } setNormalizedThemesData( () => themes.reduce( ( acc, source ) => { - const { status, stylesheet } = source; - - if ( ! stylesheet ) { - return acc; - } - - if ( skipInactive && status !== 'active' ) { + if ( ! source?.stylesheet ) { return acc; } return { ...acc, - [ stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { + [ source.stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { ...props, + slug: source.stylesheet, // Flatten every prop that contains a `raw` member. [ key ]: source[ key ]?.raw ?? source[ key ], } ), {} ), }; }, {} ) ); - }, [ skipInactive, fetchingThemes, themes ] ); + }, [ fetchingThemes, themes ] ); return normalizedThemesData; } diff --git a/tests/e2e/specs/admin/site-scan-panel.js b/tests/e2e/specs/admin/site-scan-panel.js index 9eb068dc201..2617c08e7b4 100644 --- a/tests/e2e/specs/admin/site-scan-panel.js +++ b/tests/e2e/specs/admin/site-scan-panel.js @@ -127,4 +127,34 @@ describe( 'AMP settings screen Site Scan panel', () => { await deactivatePlugin( 'autoptimize' ); } ); + + it( 'displays a notice if a plugin has been deactivated or removed', async () => { + await activateTheme( 'twentytwenty' ); + await activatePlugin( 'autoptimize' ); + + await visitAdminPage( 'admin.php', 'page=amp-options' ); + + await triggerSiteRescan(); + + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-name', { text: /Autoptimize/ } ); + + // Deactivate the plugin and test. + await deactivatePlugin( 'autoptimize' ); + + await visitAdminPage( 'admin.php', 'page=amp-options' ); + + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-name', { text: /Autoptimize/ } ); + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-notice', { text: /This plugin has been deactivated since last site scan./ } ); + + // Uninstall the plugin and test. + await uninstallPlugin( 'autoptimize' ); + + await visitAdminPage( 'admin.php', 'page=amp-options' ); + + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-slug', { text: /autoptimize/ } ); + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-notice', { text: /This plugin has been uninstalled since last site scan./ } ); + + // Clean up. + await installPlugin( 'autoptimize' ); + } ); } ); From 37eeac8da3055f5c80fa660553e8646e6cf7ab19 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Fri, 22 Oct 2021 18:13:08 +0200 Subject: [PATCH 081/120] Show correct message if there are no site scan results yet --- .../site-scan-context-provider/index.js | 5 ++ assets/src/settings-page/site-scan.js | 88 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 4cf5d26b459..7bce022f495 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -250,6 +250,10 @@ export function SiteScanContextProvider( { ); }, [ frozenModifiedOptions, modifiedOptions ] ); + const didSiteScan = useMemo( () => { + return Boolean( scannableUrls.find( ( scannableUrl ) => scannableUrl?.validated_url_post ) ); + }, [ scannableUrls ] ); + const stale = hasModifiedOptions || hasStaleResults; const previewPermalink = useMemo( () => { @@ -398,6 +402,7 @@ export function SiteScanContextProvider( { value={ { cancelSiteScan, currentlyScannedUrlIndex, + didSiteScan, isBusy: [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( status ), isCancelled: status === STATUS_CANCELLED, isCompleted: status === STATUS_COMPLETED, diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index ad715144490..a29a4f79c17 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -40,14 +40,15 @@ import useDelayedFlag from '../utils/use-delayed-flag'; export function SiteScan( { onSiteScan } ) { const { cancelSiteScan, + didSiteScan, isCancelled, isCompleted, isFailed, isInitializing, isReady, + previewPermalink, stale, startSiteScan, - previewPermalink, } = useContext( SiteScanContext ); /** @@ -61,37 +62,6 @@ export function SiteScan( { onSiteScan } ) { */ const isDelayedCompleted = useDelayedFlag( isCompleted ); - /** - * Get footer content. - */ - const getFooterContent = useCallback( () => { - if ( isCancelled || isFailed || ( stale && ( isReady || isDelayedCompleted ) ) ) { - return ( - - ); - } - - if ( ! stale && isDelayedCompleted ) { - return ( - - ); - } - - return null; - }, [ isCancelled, isDelayedCompleted, isFailed, isReady, onSiteScan, previewPermalink, stale, startSiteScan ] ); - /** * Get main content. */ @@ -127,9 +97,47 @@ export function SiteScan( { onSiteScan } ) { return ; }, [ isCancelled, isDelayedCompleted, isFailed, isInitializing, isReady ] ); + /** + * Get footer content. + */ + const getFooterContent = useCallback( () => { + function triggerSiteScan() { + if ( typeof onSiteScan === 'function' ) { + onSiteScan(); + } + startSiteScan( { cache: true } ); + } + + if ( isCancelled || isFailed || ( stale && ( isReady || isDelayedCompleted ) ) ) { + return ( + + ); + } + + if ( ! didSiteScan ) { + return ( + + ); + } + + if ( ! stale && isDelayedCompleted ) { + return ( + + ); + } + + return null; + }, [ didSiteScan, isCancelled, isDelayedCompleted, isFailed, isReady, onSiteScan, previewPermalink, stale, startSiteScan ] ); + return ( { __( 'Stale results', 'amp' ) } @@ -221,6 +229,7 @@ function SiteScanInProgress() { */ function SiteScanSummary() { const { + didSiteScan, isReady, pluginIssues, stale, @@ -228,6 +237,19 @@ function SiteScanSummary() { } = useContext( SiteScanContext ); const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; + if ( isReady && ! didSiteScan ) { + return ( + +

      + { __( 'The site has not been scanned yet. Scan your site to ensure everything is working properly.', 'amp' ) } +

      +
      + ); + } + return ( <> { isReady ? ( From cd7fb88f0b4486a3ff61426f9830e50fe01a98e8 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Mon, 25 Oct 2021 19:34:30 +0200 Subject: [PATCH 082/120] Invalidate scan results based on staleness of scannable URLs --- .../site-scan-context-provider/index.js | 95 +++++++------------ assets/src/onboarding-wizard/index.js | 2 - assets/src/settings-page/index.js | 2 - assets/src/settings-page/site-review.js | 8 +- src/Admin/OptionsMenu.php | 1 - tests/e2e/specs/admin/site-review-panel.js | 65 +++++++++---- tests/e2e/utils/amp-settings-utils.js | 11 +-- 7 files changed, 83 insertions(+), 101 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 7bce022f495..70df30abc06 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -1,10 +1,18 @@ /** * WordPress dependencies */ -import { createContext, useCallback, useContext, useEffect, useMemo, useReducer, useRef } from '@wordpress/element'; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useRef, +} from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; -import isShallowEqual from '@wordpress/is-shallow-equal'; import { addQueryArgs } from '@wordpress/url'; +import { usePrevious } from '@wordpress/compose'; /** * External dependencies @@ -14,27 +22,13 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ -import { READER, STANDARD } from '../../common/constants'; +import { STANDARD } from '../../common/constants'; import { useAsyncError } from '../../utils/use-async-error'; import { Options } from '../options-context-provider'; import { getSiteIssues } from './get-site-issues'; export const SiteScan = createContext(); -/** - * Array containing option keys that - when changed on the client side - should - * make the scan results stale. - * - * @type {string[]} - */ -const OPTIONS_INVALIDATING_SITE_SCAN = [ - 'all_templates_supported', - 'supported_post_types', - 'supported_templates', - 'suppressed_plugins', - 'theme_support', -]; - /** * Site Scan Actions. */ @@ -67,7 +61,6 @@ const STATUS_CANCELLED = 'STATUS_CANCELLED'; const INITIAL_STATE = { cache: false, currentlyScannedUrlIndex: 0, - frozenModifiedOptions: {}, scannableUrls: [], status: '', }; @@ -85,6 +78,7 @@ function siteScanReducer( state, action ) { return { ...state, status: STATUS_REQUEST_SCANNABLE_URLS, + currentlyScannedUrlIndex: INITIAL_STATE.currentlyScannedUrlIndex, }; } case ACTION_SCANNABLE_URLS_FETCH: { @@ -94,17 +88,10 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCANNABLE_URLS_RECEIVE: { - if ( action?.scannableUrls?.length > 0 ) { - return { - ...state, - status: STATUS_READY, - scannableUrls: action.scannableUrls, - }; - } - return { ...state, - status: STATUS_COMPLETED, + status: action.scannableUrls?.length > 0 ? STATUS_READY : STATUS_COMPLETED, + scannableUrls: action.scannableUrls, }; } case ACTION_SCAN_INITIALIZE: { @@ -117,7 +104,6 @@ function siteScanReducer( state, action ) { status: STATUS_IDLE, cache: action.cache, currentlyScannedUrlIndex: INITIAL_STATE.currentlyScannedUrlIndex, - frozenModifiedOptions: action.modifiedOptions, }; } case ACTION_SCAN_VALIDATE_URL: { @@ -135,7 +121,6 @@ function siteScanReducer( state, action ) { ...state.scannableUrls[ action.scannedUrlIndex ], stale: false, error: action.error ?? false, - revalidated: ! Boolean( action.error ), validated_url_post: action.error ? {} : action.validatedUrlPost, validation_errors: action.error ? [] : action.validationErrors, }, @@ -187,7 +172,6 @@ function siteScanReducer( state, action ) { * @param {boolean} props.ampFirst Whether scanning should be done with Standard mode being forced. * @param {?any} props.children Component children. * @param {boolean} props.fetchCachedValidationErrors Whether to fetch cached validation errors on mount. - * @param {string} props.homeUrl Site home URL. * @param {string} props.scannableUrlsRestPath The REST path for interacting with the scannable URL resources. * @param {string} props.validateNonce The AMP validate nonce. */ @@ -195,12 +179,11 @@ export function SiteScanContextProvider( { ampFirst = false, children, fetchCachedValidationErrors = false, - homeUrl, scannableUrlsRestPath, validateNonce, } ) { const { - modifiedOptions, + didSaveOptions, originalOptions: { theme_support: themeSupport, }, @@ -210,22 +193,23 @@ export function SiteScanContextProvider( { const { cache, currentlyScannedUrlIndex, - frozenModifiedOptions, scannableUrls, status, } = state; const urlType = ampFirst || themeSupport === STANDARD ? 'url' : 'amp_url'; + const previewPermalink = scannableUrls?.[ 0 ]?.[ urlType ] ?? ''; /** * Memoize properties. */ - const { pluginIssues, themeIssues, hasStaleResults } = useMemo( () => { + const { pluginIssues, themeIssues, didSiteScan, stale } = useMemo( () => { // Skip if the scan is in progress. if ( ! [ STATUS_READY, STATUS_COMPLETED ].includes( status ) ) { return { pluginIssues: [], themeIssues: [], - hasStaleResults: false, + didSiteScan: false, + stale: false, }; } @@ -235,33 +219,11 @@ export function SiteScanContextProvider( { return { pluginIssues: siteIssues.pluginIssues, themeIssues: siteIssues.themeIssues, - hasStaleResults: Boolean( scannableUrls.find( ( scannableUrl ) => scannableUrl?.stale === true ) ), + didSiteScan: scannableUrls.some( ( scannableUrl ) => Boolean( scannableUrl?.validation_errors ) ), + stale: scannableUrls.some( ( scannableUrl ) => scannableUrl?.stale === true ), }; }, [ scannableUrls, status ] ); - const hasModifiedOptions = useMemo( () => { - return Boolean( - Object - .keys( modifiedOptions ) - .find( ( key ) => - OPTIONS_INVALIDATING_SITE_SCAN.includes( key ) && - ! isShallowEqual( modifiedOptions[ key ], frozenModifiedOptions[ key ] ), - ), - ); - }, [ frozenModifiedOptions, modifiedOptions ] ); - - const didSiteScan = useMemo( () => { - return Boolean( scannableUrls.find( ( scannableUrl ) => scannableUrl?.validated_url_post ) ); - }, [ scannableUrls ] ); - - const stale = hasModifiedOptions || hasStaleResults; - - const previewPermalink = useMemo( () => { - const pageTypes = themeSupport === READER ? [ 'post', 'page' ] : [ 'home' ]; - - return scannableUrls.find( ( { type } ) => pageTypes.includes( type ) )?.[ urlType ] || homeUrl; - }, [ homeUrl, scannableUrls, themeSupport, urlType ] ); - /** * Preflight check. */ @@ -290,9 +252,8 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_INITIALIZE, cache: args?.cache, - modifiedOptions, } ); - }, [ modifiedOptions ] ); + }, [] ); const cancelSiteScan = useCallback( () => { dispatch( { type: ACTION_SCAN_CANCEL } ); @@ -307,6 +268,16 @@ export function SiteScanContextProvider( { } }, [ stale, status ] ); + /** + * Monitor changes to the options. + */ + const previousDidSaveOptions = usePrevious( didSaveOptions ); + useEffect( () => { + if ( ! previousDidSaveOptions && didSaveOptions ) { + dispatch( { type: ACTION_SCANNABLE_URLS_REQUEST } ); + } + }, [ didSaveOptions, previousDidSaveOptions ] ); + /** * Fetch scannable URLs from the REST endpoint. */ @@ -374,7 +345,6 @@ export function SiteScanContextProvider( { dispatch( { type: ACTION_SCAN_RECEIVE_VALIDATION_ERRORS, scannedUrlIndex: currentlyScannedUrlIndex, - revalidated: data.revalidated, validatedUrlPost: data.validated_url_post, validationErrors: data.results.map( ( { error } ) => error ), } ); @@ -426,7 +396,6 @@ SiteScanContextProvider.propTypes = { ampFirst: PropTypes.bool, children: PropTypes.any, fetchCachedValidationErrors: PropTypes.bool, - homeUrl: PropTypes.string, scannableUrlsRestPath: PropTypes.string, validateNonce: PropTypes.string, }; diff --git a/assets/src/onboarding-wizard/index.js b/assets/src/onboarding-wizard/index.js index 5eaa799ce38..ff85ed54142 100644 --- a/assets/src/onboarding-wizard/index.js +++ b/assets/src/onboarding-wizard/index.js @@ -12,7 +12,6 @@ import { APP_ROOT_ID, CLOSE_LINK, CURRENT_THEME, - HOME_URL, SETTINGS_LINK, OPTIONS_REST_PATH, READER_THEMES_REST_PATH, @@ -88,7 +87,6 @@ export function Providers( { children } ) { diff --git a/assets/src/settings-page/index.js b/assets/src/settings-page/index.js index 27d9895cf80..7eb610580a8 100644 --- a/assets/src/settings-page/index.js +++ b/assets/src/settings-page/index.js @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import { CURRENT_THEME, HAS_DEPENDENCY_SUPPORT, - HOME_URL, OPTIONS_REST_PATH, READER_THEMES_REST_PATH, SCANNABLE_URLS_REST_PATH, @@ -98,7 +97,6 @@ function Providers( { children } ) { diff --git a/assets/src/settings-page/site-review.js b/assets/src/settings-page/site-review.js index e4ce13fcf39..044f122bd7d 100644 --- a/assets/src/settings-page/site-review.js +++ b/assets/src/settings-page/site-review.js @@ -100,9 +100,11 @@ export function SiteReview() { } } />
    - + { previewPermalink && ( + + ) }
    ); diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 501079af06c..1b6879acbaf 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -68,27 +68,35 @@ function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { /** * The interface for the mode selection screen. Avoids using context for easier testing. * - * @param {Object} props Component props. - * @param {boolean} props.currentThemeIsAmongReaderThemes Whether the currently active theme is in the list of reader themes. - * @param {boolean} props.developerToolsOption Whether the user has enabled developer tools. - * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. - * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. - * @param {Array} props.pluginIssues The plugin issues found in the site scan. - * @param {string} props.savedCurrentMode The current selected mode saved in the database. - * @param {Array} props.themeIssues The theme issues found in the site scan. + * @param {Object} props Component props. + * @param {boolean} props.currentThemeIsAmongReaderThemes Whether the currently active theme is in the list of reader themes. + * @param {boolean} props.developerToolsOption Whether the user has enabled developer tools. + * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. + * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. + * @param {string[]} props.pluginsWithAMPIncompatibility A list of plugin slugs causing AMP incompatibility. + * @param {string} props.savedCurrentMode The current selected mode saved in the database. + * @param {string[]} props.themesWithAMPIncompatibility A list of theme slugs causing AMP incompatibility. */ -export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOption, firstTimeInWizard, technicalQuestionChanged, pluginIssues, savedCurrentMode, themeIssues } ) { +export function ScreenUI( { + currentThemeIsAmongReaderThemes, + developerToolsOption, + firstTimeInWizard, + technicalQuestionChanged, + pluginsWithAMPIncompatibility, + savedCurrentMode, + themesWithAMPIncompatibility, +} ) { const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); const selectionDetails = useMemo( () => getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTechnical, - hasScanResults: null !== pluginIssues && null !== themeIssues, - hasPluginIssues: pluginIssues && 0 < pluginIssues.length, - hasThemeIssues: themeIssues && 0 < themeIssues.length, + hasScanResults: null !== pluginsWithAMPIncompatibility && null !== themesWithAMPIncompatibility, + hasPluginsWithAMPIncompatibility: pluginsWithAMPIncompatibility && 0 < pluginsWithAMPIncompatibility.length, + hasThemesWithAMPIncompatibility: themesWithAMPIncompatibility && 0 < themesWithAMPIncompatibility.length, }, - ), [ currentThemeIsAmongReaderThemes, themeIssues, pluginIssues, userIsTechnical ] ); + ), [ currentThemeIsAmongReaderThemes, themesWithAMPIncompatibility, pluginsWithAMPIncompatibility, userIsTechnical ] ); return ( @@ -124,7 +132,7 @@ ScreenUI.propTypes = { developerToolsOption: PropTypes.bool, firstTimeInWizard: PropTypes.bool, technicalQuestionChanged: PropTypes.bool, - pluginIssues: PropTypes.arrayOf( PropTypes.string ), + pluginsWithAMPIncompatibility: PropTypes.arrayOf( PropTypes.string ), savedCurrentMode: PropTypes.string, - themeIssues: PropTypes.arrayOf( PropTypes.string ), + themesWithAMPIncompatibility: PropTypes.arrayOf( PropTypes.string ), }; diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index a29a4f79c17..61002f76430 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -17,7 +17,7 @@ import { useCallback, useContext, useEffect } from '@wordpress/element'; import { AMPDrawer } from '../components/amp-drawer'; import { IconLandscapeHillsCogsAlt } from '../components/svg/landscape-hills-cogs-alt'; import { ProgressBar } from '../components/progress-bar'; -import { PluginsWithIssues, ThemesWithIssues } from '../components/site-scan-results'; +import { PluginsWithAmpIncompatibility, ThemesWithAmpIncompatibility } from '../components/site-scan-results'; import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; import { Loading } from '../components/loading'; import { @@ -231,11 +231,11 @@ function SiteScanSummary() { const { didSiteScan, isReady, - pluginIssues, + pluginsWithAMPIncompatibility, stale, - themeIssues, + themesWithAMPIncompatibility, } = useContext( SiteScanContext ); - const hasSiteIssues = themeIssues.length > 0 || pluginIssues.length > 0; + const hasSiteIssues = themesWithAMPIncompatibility.length > 0 || pluginsWithAMPIncompatibility.length > 0; if ( isReady && ! didSiteScan ) { return ( @@ -294,15 +294,15 @@ function SiteScanSummary() { ) } ) } - { themeIssues.length > 0 && ( - 0 && ( + ) } - { pluginIssues.length > 0 && ( - 0 && ( + ) } From cfcbc929de36b29ea62b842e7cadb8d442c8be8c Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 16:21:27 +0200 Subject: [PATCH 092/120] Improve plugin sources normalization and slug retrieval logic --- assets/src/common/helpers/index.js | 10 +++++++ .../helpers/test/getPluginSlugFromPath.js | 17 ++++++++++++ .../use-normalized-plugins-data.js | 3 ++- .../get-slugs-from-validation-results.js | 24 +++++++++++------ .../test/get-slugs-from-validation-results.js | 27 ++++++++++++++++--- 5 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 assets/src/common/helpers/test/getPluginSlugFromPath.js diff --git a/assets/src/common/helpers/index.js b/assets/src/common/helpers/index.js index dc94f04b790..a09649b745a 100644 --- a/assets/src/common/helpers/index.js +++ b/assets/src/common/helpers/index.js @@ -221,3 +221,13 @@ export const setImageFromURL = ( { url, id, width, height, onSelect, dispatchIma onSelect( data ); // @todo Does this do anything? dispatchImage( id ); }; + +/** + * Retrieve plugin slug out of the source path. + * + * @param {string} path Plugin source path. + * @return {string} Plugin slug. + */ +export function getPluginSlugFromPath( path = '' ) { + return path?.match( /^(?:[^\/]*\/)*([^.]*)/ )?.[ 1 ] || ''; +} diff --git a/assets/src/common/helpers/test/getPluginSlugFromPath.js b/assets/src/common/helpers/test/getPluginSlugFromPath.js new file mode 100644 index 00000000000..7a19833b778 --- /dev/null +++ b/assets/src/common/helpers/test/getPluginSlugFromPath.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { getPluginSlugFromPath } from '../index'; + +describe( 'getPluginSlugFromPath', () => { + it( 'should return an empty string if no path is provided', () => { + expect( getPluginSlugFromPath() ).toBe( '' ); + } ); + + it( 'should return correct plugin slug', () => { + expect( getPluginSlugFromPath( 'foo' ) ).toBe( 'foo' ); + expect( getPluginSlugFromPath( 'foo.php' ) ).toBe( 'foo' ); + expect( getPluginSlugFromPath( 'foo/bar' ) ).toBe( 'bar' ); + expect( getPluginSlugFromPath( 'foo/baz.php' ) ).toBe( 'baz' ); + } ); +} ); diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index 1f08a085f81..6f5ff6d65d6 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -6,6 +6,7 @@ import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ +import { getPluginSlugFromPath } from '../../common/helpers'; import { Plugins } from './index'; export function useNormalizedPluginsData() { @@ -18,7 +19,7 @@ export function useNormalizedPluginsData() { } setNormalizedPluginsData( plugins.reduce( ( acc, source ) => { - const slug = source?.plugin?.match( /^(?:[^\/]*\/)?(.*?)$/ )?.[ 1 ]; + const slug = getPluginSlugFromPath( source?.plugin ); if ( ! slug ) { return acc; diff --git a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js index d86f4f32dd7..7ad8dc98cd0 100644 --- a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js @@ -1,9 +1,14 @@ +/** + * Internal dependencies + */ +import { getPluginSlugFromPath } from '../../common/helpers'; + /** * Retrieve slugs of plugins and themes from a list of validation results. * * See the corresponding PHP logic in `\AMP_Validated_URL_Post_Type::render_sources_column()`. * - * @param {Array} validationResults + * @param {Object[]} validationResults * @return {Object} An object consisting of `pluginSlugs` and `themeSlugs` arrays. */ export function getSlugsFromValidationResults( validationResults = [] ) { @@ -16,19 +21,22 @@ export function getSlugsFromValidationResults( validationResults = [] ) { } for ( const source of result.sources ) { - // Skip including Gutenberg in the summary if there is another plugin, since Gutenberg is like core. - if ( result.sources.length > 1 && source.type === 'plugin' && source.name === 'gutenberg' ) { - continue; - } - - if ( source.type === 'plugin' && source.name !== 'amp' ) { - plugins.add( source.name.match( /(.*?)(?:\.php)?$/ )[ 1 ] ); + if ( source.type === 'plugin' ) { + plugins.add( getPluginSlugFromPath( source.name ) ); } else if ( source.type === 'theme' ) { themes.add( source.name ); } } } + // Skip including AMP in the summary, since AMP is like core. + plugins.delete( 'amp' ); + + // Skip including Gutenberg in the summary if there is another plugin, since Gutenberg is like core. + if ( plugins.size > 1 && plugins.has( 'gutenberg' ) ) { + plugins.delete( 'gutenberg' ); + } + return { plugins: [ ...plugins ], themes: [ ...themes ], diff --git a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js index 6c334481d36..885923b9b41 100644 --- a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js @@ -34,7 +34,7 @@ describe( 'getSlugsFromValidationResults', () => { }, { sources: [ - { type: 'plugin', name: 'foo-bar.php' }, + { type: 'plugin', name: 'foo-bar/foo-bar.php' }, ], }, { @@ -58,7 +58,28 @@ describe( 'getSlugsFromValidationResults', () => { const slugs = getSlugsFromValidationResults( validationResult ); - expect( slugs.plugins ).toStrictEqual( [ 'gutenberg', 'jetpack', 'foo-bar' ] ); - expect( slugs.themes ).toStrictEqual( expect.arrayContaining( [ 'twentytwenty' ] ) ); + expect( slugs.plugins ).toStrictEqual( [ 'jetpack', 'foo-bar' ] ); + expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); + } ); + + it( 'returns Gutenberg if it is the only plugin', () => { + const validationResult = [ + { + sources: [ + { type: 'plugin', name: 'gutenberg' }, + ], + }, + { + sources: [ + { type: 'theme', name: 'twentytwenty' }, + { type: 'core', name: 'wp-includes' }, + ], + }, + ]; + + const slugs = getSlugsFromValidationResults( validationResult ); + + expect( slugs.plugins ).toStrictEqual( [ 'gutenberg' ] ); + expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); } ); } ); From a797f78dd7b578313ac67886a4e19ce2452fce47 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 16:31:21 +0200 Subject: [PATCH 093/120] Keep new helper in separate file to prevent build errors --- assets/src/common/helpers/get-plugin-slug-from-path.js | 9 +++++++++ assets/src/common/helpers/index.js | 10 ---------- ...ginSlugFromPath.js => get-plugin-slug-from-path.js} | 2 +- .../use-normalized-plugins-data.js | 2 +- .../get-slugs-from-validation-results.js | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 assets/src/common/helpers/get-plugin-slug-from-path.js rename assets/src/common/helpers/test/{getPluginSlugFromPath.js => get-plugin-slug-from-path.js} (87%) diff --git a/assets/src/common/helpers/get-plugin-slug-from-path.js b/assets/src/common/helpers/get-plugin-slug-from-path.js new file mode 100644 index 00000000000..0d975ba1094 --- /dev/null +++ b/assets/src/common/helpers/get-plugin-slug-from-path.js @@ -0,0 +1,9 @@ +/** + * Retrieve plugin slug out of the source path. + * + * @param {string} path Plugin source path. + * @return {string} Plugin slug. + */ +export function getPluginSlugFromPath( path = '' ) { + return path?.match( /^(?:[^\/]*\/)*([^.]*)/ )?.[ 1 ] || ''; +} diff --git a/assets/src/common/helpers/index.js b/assets/src/common/helpers/index.js index a09649b745a..dc94f04b790 100644 --- a/assets/src/common/helpers/index.js +++ b/assets/src/common/helpers/index.js @@ -221,13 +221,3 @@ export const setImageFromURL = ( { url, id, width, height, onSelect, dispatchIma onSelect( data ); // @todo Does this do anything? dispatchImage( id ); }; - -/** - * Retrieve plugin slug out of the source path. - * - * @param {string} path Plugin source path. - * @return {string} Plugin slug. - */ -export function getPluginSlugFromPath( path = '' ) { - return path?.match( /^(?:[^\/]*\/)*([^.]*)/ )?.[ 1 ] || ''; -} diff --git a/assets/src/common/helpers/test/getPluginSlugFromPath.js b/assets/src/common/helpers/test/get-plugin-slug-from-path.js similarity index 87% rename from assets/src/common/helpers/test/getPluginSlugFromPath.js rename to assets/src/common/helpers/test/get-plugin-slug-from-path.js index 7a19833b778..5b5c3041bb1 100644 --- a/assets/src/common/helpers/test/getPluginSlugFromPath.js +++ b/assets/src/common/helpers/test/get-plugin-slug-from-path.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../index'; +import { getPluginSlugFromPath } from '../get-plugin-slug-from-path'; describe( 'getPluginSlugFromPath', () => { it( 'should return an empty string if no path is provided', () => { diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index 6f5ff6d65d6..b83cb1afadb 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -6,7 +6,7 @@ import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../../common/helpers'; +import { getPluginSlugFromPath } from '../../common/helpers/get-plugin-slug-from-path'; import { Plugins } from './index'; export function useNormalizedPluginsData() { diff --git a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js index 7ad8dc98cd0..e574f91b562 100644 --- a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../../common/helpers'; +import { getPluginSlugFromPath } from '../../common/helpers/get-plugin-slug-from-path'; /** * Retrieve slugs of plugins and themes from a list of validation results. From 7ad39982f2135e968197f1591a83d0a37c475627 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 16:35:36 +0200 Subject: [PATCH 094/120] Use 'AMP incompatibility' instead of 'issues' --- .../test/get-slugs-from-validation-results.js | 4 ++-- assets/src/components/site-scan-results/site-scan-results.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js index 885923b9b41..ef2f15b93e4 100644 --- a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js @@ -8,7 +8,7 @@ describe( 'getSlugsFromValidationResults', () => { expect( getSlugsFromValidationResults() ).toMatchObject( { plugins: [], themes: [] } ); } ); - it( 'returns plugin and theme issues', () => { + it( 'returns plugin and theme slugs', () => { const validationResult = [ { sources: [ @@ -62,7 +62,7 @@ describe( 'getSlugsFromValidationResults', () => { expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); } ); - it( 'returns Gutenberg if it is the only plugin', () => { + it( 'returns Gutenberg if it is the only plugin in the list', () => { const validationResult = [ { sources: [ diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js index 407e7502324..b1bae00efdb 100644 --- a/assets/src/components/site-scan-results/site-scan-results.js +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -21,7 +21,7 @@ import { Selectable } from '../selectable'; * * @param {Object} props Component props. * @param {Object} props.children Component children. - * @param {number} props.count Issues count. + * @param {number} props.count Incompatibilities count. * @param {string} props.className Additional class names. * @param {Element} props.icon Panel icon. * @param {string} props.title Panel title. From 5ed62f841c46b8be2010736deabf4088cae11189 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 16:44:55 +0200 Subject: [PATCH 095/120] Use clearer name for `Array.reduce` accumulator property --- .../plugins-context-provider/use-normalized-plugins-data.js | 6 +++--- assets/src/components/site-scan-context-provider/index.js | 2 +- .../themes-context-provider/use-normalized-themes-data.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index b83cb1afadb..dbf1c0a7605 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -18,15 +18,15 @@ export function useNormalizedPluginsData() { return; } - setNormalizedPluginsData( plugins.reduce( ( acc, source ) => { + setNormalizedPluginsData( plugins.reduce( ( accumulatedPluginsData, source ) => { const slug = getPluginSlugFromPath( source?.plugin ); if ( ! slug ) { - return acc; + return accumulatedPluginsData; } return { - ...acc, + ...accumulatedPluginsData, [ slug ]: Object.keys( source ).reduce( ( props, key ) => ( { ...props, slug, diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 42c7e719d83..4db5cf56cea 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -218,7 +218,7 @@ export function SiteScanContextProvider( { }; } - const validationErrors = scannableUrls.reduce( ( acc, scannableUrl ) => [ ...acc, ...scannableUrl?.validation_errors ?? [] ], [] ); + const validationErrors = scannableUrls.reduce( ( accumulatedValidationErrors, scannableUrl ) => [ ...accumulatedValidationErrors, ...scannableUrl?.validation_errors ?? [] ], [] ); const slugs = getSlugsFromValidationResults( validationErrors ); return { diff --git a/assets/src/components/themes-context-provider/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/use-normalized-themes-data.js index b22ac40167f..9c035945418 100644 --- a/assets/src/components/themes-context-provider/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/use-normalized-themes-data.js @@ -17,13 +17,13 @@ export function useNormalizedThemesData() { return; } - setNormalizedThemesData( () => themes.reduce( ( acc, source ) => { + setNormalizedThemesData( () => themes.reduce( ( accumulatedThemesData, source ) => { if ( ! source?.stylesheet ) { - return acc; + return accumulatedThemesData; } return { - ...acc, + ...accumulatedThemesData, [ source.stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { ...props, slug: source.stylesheet, From 1313a0f2b048569a8cd5772a5cc01a922bae8307 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 18:23:00 +0200 Subject: [PATCH 096/120] Always allow re-running Site Scan It also improves the logic that renders Site Scan info notices. --- .../site-scan-context-provider/index.js | 14 +-- assets/src/settings-page/site-scan.js | 113 +++++++++--------- 2 files changed, 64 insertions(+), 63 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 4db5cf56cea..c63b456dde8 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -203,18 +203,18 @@ export function SiteScanContextProvider( { * Memoize properties. */ const { + hasSiteScanResults, pluginsWithAMPIncompatibility, - themesWithAMPIncompatibility, - didSiteScan, stale, + themesWithAMPIncompatibility, } = useMemo( () => { // Skip if the scan is in progress. if ( ! [ STATUS_READY, STATUS_COMPLETED ].includes( status ) ) { return { + hasSiteScanResults: false, pluginsWithAMPIncompatibility: [], - themesWithAMPIncompatibility: [], - didSiteScan: false, stale: false, + themesWithAMPIncompatibility: [], }; } @@ -222,10 +222,10 @@ export function SiteScanContextProvider( { const slugs = getSlugsFromValidationResults( validationErrors ); return { + hasSiteScanResults: scannableUrls.some( ( scannableUrl ) => Boolean( scannableUrl?.validation_errors ) ), pluginsWithAMPIncompatibility: slugs.plugins, - themesWithAMPIncompatibility: slugs.themes, - didSiteScan: scannableUrls.some( ( scannableUrl ) => Boolean( scannableUrl?.validation_errors ) ), stale: scannableUrls.some( ( scannableUrl ) => scannableUrl?.stale === true ), + themesWithAMPIncompatibility: slugs.themes, }; }, [ scannableUrls, status ] ); @@ -377,7 +377,7 @@ export function SiteScanContextProvider( { value={ { cancelSiteScan, currentlyScannedUrlIndex, - didSiteScan, + hasSiteScanResults, isBusy: [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( status ), isCancelled: status === STATUS_CANCELLED, isCompleted: status === STATUS_COMPLETED, diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 61002f76430..6e0b0d43794 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; */ import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { useCallback, useContext, useEffect } from '@wordpress/element'; +import { useCallback, useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -40,16 +40,20 @@ import useDelayedFlag from '../utils/use-delayed-flag'; export function SiteScan( { onSiteScan } ) { const { cancelSiteScan, - didSiteScan, + hasSiteScanResults, + isBusy, isCancelled, isCompleted, isFailed, isInitializing, isReady, + pluginsWithAMPIncompatibility, previewPermalink, stale, startSiteScan, + themesWithAMPIncompatibility, } = useContext( SiteScanContext ); + const hasSiteIssues = themesWithAMPIncompatibility.length > 0 || pluginsWithAMPIncompatibility.length > 0; /** * Cancel scan when component unmounts. @@ -61,6 +65,18 @@ export function SiteScan( { onSiteScan } ) { * brief moment. */ const isDelayedCompleted = useDelayedFlag( isCompleted ); + const isSummary = isReady || isDelayedCompleted; + + /** + * Check if the scanner has been triggered at least once in the current + * session by a user. + */ + const [ hasSiteScanBeenTriggered, setHasSiteScanBeenTriggered ] = useState( false ); + useEffect( () => { + if ( ! hasSiteScanBeenTriggered && isBusy ) { + setHasSiteScanBeenTriggered( true ); + } + }, [ hasSiteScanBeenTriggered, isBusy ] ); /** * Get main content. @@ -90,60 +106,41 @@ export function SiteScan( { onSiteScan } ) { ); } - if ( isReady || isDelayedCompleted ) { + if ( isSummary ) { return ; } return ; - }, [ isCancelled, isDelayedCompleted, isFailed, isInitializing, isReady ] ); - - /** - * Get footer content. - */ - const getFooterContent = useCallback( () => { - function triggerSiteScan() { - if ( typeof onSiteScan === 'function' ) { - onSiteScan(); - } - startSiteScan( { cache: true } ); - } - - if ( isCancelled || isFailed || ( stale && ( isReady || isDelayedCompleted ) ) ) { - return ( - - ); - } - - if ( ! didSiteScan ) { - return ( - - ); - } - - if ( ! stale && isDelayedCompleted ) { - return ( - - ); - } - - return null; - }, [ didSiteScan, isCancelled, isDelayedCompleted, isFailed, isReady, onSiteScan, previewPermalink, stale, startSiteScan ] ); + }, [ isCancelled, isFailed, isInitializing, isSummary ] ); return ( { __( 'Stale results', 'amp' ) } ) : null } - footerContent={ getFooterContent() } + footerContent={ ( isSummary || isFailed || isCancelled ) && ( + <> + + { hasSiteScanResults && ( + + ) } + + ) } > { getContent() } @@ -229,7 +226,7 @@ function SiteScanInProgress() { */ function SiteScanSummary() { const { - didSiteScan, + hasSiteScanResults, isReady, pluginsWithAMPIncompatibility, stale, @@ -237,12 +234,9 @@ function SiteScanSummary() { } = useContext( SiteScanContext ); const hasSiteIssues = themesWithAMPIncompatibility.length > 0 || pluginsWithAMPIncompatibility.length > 0; - if ( isReady && ! didSiteScan ) { + if ( isReady && ! hasSiteScanResults ) { return ( - +

    { __( 'The site has not been scanned yet. Scan your site to ensure everything is working properly.', 'amp' ) }

    @@ -250,17 +244,24 @@ function SiteScanSummary() { ); } + if ( isReady && ! hasSiteIssues ) { + return ( + +

    + { __( 'Site scan found no issues on your site. Browse your site to ensure everything is working as expected.', 'amp' ) } +

    +
    + ); + } + return ( <> { isReady ? ( - +

    { stale ? __( 'Stale results. Rescan your site to ensure everything is working properly.', 'amp' ) - : __( 'No changes since your last scan. Browse your site to ensure everything is working as expected.', 'amp' ) + : __( 'No changes since your last scan.', 'amp' ) }

    @@ -288,7 +289,7 @@ function SiteScanSummary() { { ! hasSiteIssues && ! stale && (

    - { __( 'Site scan found no issues on your site.', 'amp' ) } + { __( 'Site scan found no issues on your site. Browse your site to ensure everything is working as expected.', 'amp' ) }

    ) } From 16586ff33cfc783c021fd520e96c13fa1a2d10ad Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 20:46:37 +0200 Subject: [PATCH 097/120] Allow scanning only if there are scannable URLs --- .../site-scan-context-provider/index.js | 1 + assets/src/settings-page/site-scan.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index c63b456dde8..e3c552a698b 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -384,6 +384,7 @@ export function SiteScanContextProvider( { isFailed: status === STATUS_FAILED, isInitializing: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), isReady: status === STATUS_READY, + isSiteScannable: scannableUrls.length > 0, pluginsWithAMPIncompatibility, previewPermalink, scannableUrls, diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 6e0b0d43794..388c31012b1 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -47,6 +47,7 @@ export function SiteScan( { onSiteScan } ) { isFailed, isInitializing, isReady, + isSiteScannable, pluginsWithAMPIncompatibility, previewPermalink, stale, @@ -106,22 +107,32 @@ export function SiteScan( { onSiteScan } ) { ); } + if ( ! isSiteScannable ) { + return ( + +

    + { __( 'Your site cannot be scanned. There are no AMP-enabled URLs available.', 'amp' ) } +

    +
    + ); + } + if ( isSummary ) { return ; } return ; - }, [ isCancelled, isFailed, isInitializing, isSummary ] ); + }, [ isCancelled, isFailed, isInitializing, isSiteScannable, isSummary ] ); return ( { __( 'Stale results', 'amp' ) }
    ) : null } - footerContent={ ( isSummary || isFailed || isCancelled ) && ( + footerContent={ isSiteScannable && ( isSummary || isFailed || isCancelled ) && ( <>
); diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 1b6879acbaf..cd96693f8a5 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -73,18 +73,18 @@ function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { * @param {boolean} props.developerToolsOption Whether the user has enabled developer tools. * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. - * @param {string[]} props.pluginsWithAMPIncompatibility A list of plugin slugs causing AMP incompatibility. + * @param {string[]} props.pluginsWithAmpIncompatibility A list of plugin slugs causing AMP incompatibility. * @param {string} props.savedCurrentMode The current selected mode saved in the database. - * @param {string[]} props.themesWithAMPIncompatibility A list of theme slugs causing AMP incompatibility. + * @param {string[]} props.themesWithAmpIncompatibility A list of theme slugs causing AMP incompatibility. */ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOption, firstTimeInWizard, technicalQuestionChanged, - pluginsWithAMPIncompatibility, + pluginsWithAmpIncompatibility, savedCurrentMode, - themesWithAMPIncompatibility, + themesWithAmpIncompatibility, } ) { const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); @@ -92,11 +92,11 @@ export function ScreenUI( { { currentThemeIsAmongReaderThemes, userIsTechnical, - hasScanResults: null !== pluginsWithAMPIncompatibility && null !== themesWithAMPIncompatibility, - hasPluginsWithAMPIncompatibility: pluginsWithAMPIncompatibility && 0 < pluginsWithAMPIncompatibility.length, - hasThemesWithAMPIncompatibility: themesWithAMPIncompatibility && 0 < themesWithAMPIncompatibility.length, + hasScanResults: null !== pluginsWithAmpIncompatibility && null !== themesWithAmpIncompatibility, + hasPluginsWithAMPIncompatibility: pluginsWithAmpIncompatibility && 0 < pluginsWithAmpIncompatibility.length, + hasThemesWithAMPIncompatibility: themesWithAmpIncompatibility && 0 < themesWithAmpIncompatibility.length, }, - ), [ currentThemeIsAmongReaderThemes, themesWithAMPIncompatibility, pluginsWithAMPIncompatibility, userIsTechnical ] ); + ), [ currentThemeIsAmongReaderThemes, themesWithAmpIncompatibility, pluginsWithAmpIncompatibility, userIsTechnical ] ); return ( @@ -132,7 +132,7 @@ ScreenUI.propTypes = { developerToolsOption: PropTypes.bool, firstTimeInWizard: PropTypes.bool, technicalQuestionChanged: PropTypes.bool, - pluginsWithAMPIncompatibility: PropTypes.arrayOf( PropTypes.string ), + pluginsWithAmpIncompatibility: PropTypes.arrayOf( PropTypes.string ), savedCurrentMode: PropTypes.string, - themesWithAMPIncompatibility: PropTypes.arrayOf( PropTypes.string ), + themesWithAmpIncompatibility: PropTypes.arrayOf( PropTypes.string ), }; diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 388c31012b1..5cac2cf1bb6 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -48,13 +48,13 @@ export function SiteScan( { onSiteScan } ) { isInitializing, isReady, isSiteScannable, - pluginsWithAMPIncompatibility, + pluginsWithAmpIncompatibility, previewPermalink, stale, startSiteScan, - themesWithAMPIncompatibility, + themesWithAmpIncompatibility, } = useContext( SiteScanContext ); - const hasSiteIssues = themesWithAMPIncompatibility.length > 0 || pluginsWithAMPIncompatibility.length > 0; + const hasSiteIssues = themesWithAmpIncompatibility.length > 0 || pluginsWithAmpIncompatibility.length > 0; /** * Cancel scan when component unmounts. @@ -239,11 +239,11 @@ function SiteScanSummary() { const { hasSiteScanResults, isReady, - pluginsWithAMPIncompatibility, + pluginsWithAmpIncompatibility, stale, - themesWithAMPIncompatibility, + themesWithAmpIncompatibility, } = useContext( SiteScanContext ); - const hasSiteIssues = themesWithAMPIncompatibility.length > 0 || pluginsWithAMPIncompatibility.length > 0; + const hasSiteIssues = themesWithAmpIncompatibility.length > 0 || pluginsWithAmpIncompatibility.length > 0; if ( isReady && ! hasSiteScanResults ) { return ( @@ -306,15 +306,15 @@ function SiteScanSummary() { ) } ) } - { themesWithAMPIncompatibility.length > 0 && ( + { themesWithAmpIncompatibility.length > 0 && ( ) } - { pluginsWithAMPIncompatibility.length > 0 && ( + { pluginsWithAmpIncompatibility.length > 0 && ( ) } From 2b0207170f9a96722599004ccfe361f1cf31187f Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 21:44:58 +0200 Subject: [PATCH 099/120] Get plugin slug using similar logic as in PHP --- .../common/helpers/get-plugin-slug-from-file.js | 16 ++++++++++++++++ .../common/helpers/get-plugin-slug-from-path.js | 9 --------- .../helpers/test/get-plugin-slug-from-path.js | 14 +++++++------- .../use-normalized-plugins-data.js | 4 ++-- .../get-slugs-from-validation-results.js | 4 ++-- 5 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 assets/src/common/helpers/get-plugin-slug-from-file.js delete mode 100644 assets/src/common/helpers/get-plugin-slug-from-path.js diff --git a/assets/src/common/helpers/get-plugin-slug-from-file.js b/assets/src/common/helpers/get-plugin-slug-from-file.js new file mode 100644 index 00000000000..2475000c3a5 --- /dev/null +++ b/assets/src/common/helpers/get-plugin-slug-from-file.js @@ -0,0 +1,16 @@ +/** + * Get plugin slug from file path. + * + * If the plugin file is in a directory, then the slug is just the directory name. Otherwise, if the file is not + * inside of a directory and is just a single-file plugin, then the slug is the filename of the PHP file. + * + * If the file path contains a file extension, it will be stripped as well. + * + * See the corresponding PHP logic in `\AmpProject\AmpWP\get_plugin_slug_from_file()`. + * + * @param {string} path Plugin file path. + * @return {string} Plugin slug. + */ +export function getPluginSlugFromFile( path = '' ) { + return path.split( '/' )?.[ 0 ]?.split( '.' )?.[ 0 ]; +} diff --git a/assets/src/common/helpers/get-plugin-slug-from-path.js b/assets/src/common/helpers/get-plugin-slug-from-path.js deleted file mode 100644 index 0d975ba1094..00000000000 --- a/assets/src/common/helpers/get-plugin-slug-from-path.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Retrieve plugin slug out of the source path. - * - * @param {string} path Plugin source path. - * @return {string} Plugin slug. - */ -export function getPluginSlugFromPath( path = '' ) { - return path?.match( /^(?:[^\/]*\/)*([^.]*)/ )?.[ 1 ] || ''; -} diff --git a/assets/src/common/helpers/test/get-plugin-slug-from-path.js b/assets/src/common/helpers/test/get-plugin-slug-from-path.js index 5b5c3041bb1..4d5fe9990b7 100644 --- a/assets/src/common/helpers/test/get-plugin-slug-from-path.js +++ b/assets/src/common/helpers/test/get-plugin-slug-from-path.js @@ -1,17 +1,17 @@ /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../get-plugin-slug-from-path'; +import { getPluginSlugFromFile } from '../get-plugin-slug-from-file'; -describe( 'getPluginSlugFromPath', () => { +describe( 'getPluginSlugFromFile', () => { it( 'should return an empty string if no path is provided', () => { - expect( getPluginSlugFromPath() ).toBe( '' ); + expect( getPluginSlugFromFile() ).toBe( '' ); } ); it( 'should return correct plugin slug', () => { - expect( getPluginSlugFromPath( 'foo' ) ).toBe( 'foo' ); - expect( getPluginSlugFromPath( 'foo.php' ) ).toBe( 'foo' ); - expect( getPluginSlugFromPath( 'foo/bar' ) ).toBe( 'bar' ); - expect( getPluginSlugFromPath( 'foo/baz.php' ) ).toBe( 'baz' ); + expect( getPluginSlugFromFile( 'foo' ) ).toBe( 'foo' ); + expect( getPluginSlugFromFile( 'foo.php' ) ).toBe( 'foo' ); + expect( getPluginSlugFromFile( 'foo/bar' ) ).toBe( 'foo' ); + expect( getPluginSlugFromFile( 'foo/baz.php' ) ).toBe( 'foo' ); } ); } ); diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index dbf1c0a7605..344bbe7cf5c 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -6,7 +6,7 @@ import { useContext, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../../common/helpers/get-plugin-slug-from-path'; +import { getPluginSlugFromFile } from '../../common/helpers/get-plugin-slug-from-file'; import { Plugins } from './index'; export function useNormalizedPluginsData() { @@ -19,7 +19,7 @@ export function useNormalizedPluginsData() { } setNormalizedPluginsData( plugins.reduce( ( accumulatedPluginsData, source ) => { - const slug = getPluginSlugFromPath( source?.plugin ); + const slug = getPluginSlugFromFile( source?.plugin ); if ( ! slug ) { return accumulatedPluginsData; diff --git a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js index e574f91b562..591011bcd58 100644 --- a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { getPluginSlugFromPath } from '../../common/helpers/get-plugin-slug-from-path'; +import { getPluginSlugFromFile } from '../../common/helpers/get-plugin-slug-from-file'; /** * Retrieve slugs of plugins and themes from a list of validation results. @@ -22,7 +22,7 @@ export function getSlugsFromValidationResults( validationResults = [] ) { for ( const source of result.sources ) { if ( source.type === 'plugin' ) { - plugins.add( getPluginSlugFromPath( source.name ) ); + plugins.add( getPluginSlugFromFile( source.name ) ); } else if ( source.type === 'theme' ) { themes.add( source.name ); } From d5a3103698b10d423dfe13237128776ccc2c6bf2 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 22:00:36 +0200 Subject: [PATCH 100/120] Don't use external link for Validated URLs page on Settings screen --- .../site-scan-results/site-scan-results.js | 28 +++++++++---------- .../pages/site-scan/index.js | 13 +++++++-- assets/src/settings-page/site-scan.js | 17 +++++++++-- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js index b1bae00efdb..58d2bdd2e60 100644 --- a/assets/src/components/site-scan-results/site-scan-results.js +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -19,21 +19,21 @@ import { Selectable } from '../selectable'; /** * Renders a panel with a site scan results. * - * @param {Object} props Component props. - * @param {Object} props.children Component children. - * @param {number} props.count Incompatibilities count. - * @param {string} props.className Additional class names. - * @param {Element} props.icon Panel icon. - * @param {string} props.title Panel title. - * @param {string} props.validatedUrlsLink URL to the Validated URLs page. + * @param {Object} props Component props. + * @param {Object} props.callToAction A call to action element. + * @param {Object} props.children Component children. + * @param {number} props.count Incompatibilities count. + * @param {string} props.className Additional class names. + * @param {Element} props.icon Panel icon. + * @param {string} props.title Panel title. */ export function SiteScanResults( { + callToAction, children, - count, className, + count, icon, title, - validatedUrlsLink, } ) { return ( @@ -51,11 +51,9 @@ export function SiteScanResults( {
{ children } - { validatedUrlsLink && ( + { callToAction && (

- - { __( 'AMP Validated URLs page', 'amp' ) } - + { callToAction }

) }
@@ -64,10 +62,10 @@ export function SiteScanResults( { } SiteScanResults.propTypes = { + callToAction: PropTypes.element, children: PropTypes.any, - count: PropTypes.number, className: PropTypes.string, + count: PropTypes.number, icon: PropTypes.element, title: PropTypes.string, - validatedUrlsLink: PropTypes.string, }; diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index 3a12e79d8c6..b3cdb590d08 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -9,6 +9,7 @@ import PropTypes from 'prop-types'; */ import { useContext, useEffect, useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { ExternalLink } from '@wordpress/components'; /** * Internal dependencies @@ -114,14 +115,22 @@ export function SiteScan() { + { __( 'AMP Validated URLs page', 'amp' ) } + + ) } /> ) } { pluginsWithAmpIncompatibility.length > 0 && ( + { __( 'AMP Validated URLs page', 'amp' ) } + + ) } /> ) } diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 5cac2cf1bb6..2ba7bd070bd 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -9,7 +9,7 @@ import PropTypes from 'prop-types'; */ import { __, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; -import { useCallback, useContext, useEffect, useState } from '@wordpress/element'; +import { useCallback, useContext, useEffect, useMemo, useState } from '@wordpress/element'; /** * Internal dependencies @@ -19,6 +19,7 @@ import { IconLandscapeHillsCogsAlt } from '../components/svg/landscape-hills-cog import { ProgressBar } from '../components/progress-bar'; import { PluginsWithAmpIncompatibility, ThemesWithAmpIncompatibility } from '../components/site-scan-results'; import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; +import { User } from '../components/user-context-provider'; import { Loading } from '../components/loading'; import { AMPNotice, @@ -244,6 +245,8 @@ function SiteScanSummary() { themesWithAmpIncompatibility, } = useContext( SiteScanContext ); const hasSiteIssues = themesWithAmpIncompatibility.length > 0 || pluginsWithAmpIncompatibility.length > 0; + const { developerToolsOption } = useContext( User ); + const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); if ( isReady && ! hasSiteScanResults ) { return ( @@ -309,13 +312,21 @@ function SiteScanSummary() { { themesWithAmpIncompatibility.length > 0 && ( + { __( 'AMP Validated URLs page', 'amp' ) } + + ) : null } /> ) } { pluginsWithAmpIncompatibility.length > 0 && ( + { __( 'AMP Validated URLs page', 'amp' ) } + + ) : null } /> ) } From 3f80f6e031c3ef936e824200c2b14d1ad8b2df82 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 22:11:03 +0200 Subject: [PATCH 101/120] Ensure site scan is cancelled whenever AMP options are changed --- .../components/site-scan-context-provider/index.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index 801aa21e872..fa72538c398 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -129,7 +129,7 @@ function siteScanReducer( state, action ) { }; } case ACTION_SCAN_NEXT_URL: { - if ( state.status === STATUS_CANCELLED ) { + if ( ! [ STATUS_IDLE, STATUS_IN_PROGRESS ].includes( state.status ) ) { return state; } @@ -265,16 +265,8 @@ export function SiteScanContextProvider( { }, [] ); /** - * Cancel scan and invalidate current results whenever options change. - */ - useEffect( () => { - if ( stale && [ STATUS_IN_PROGRESS, STATUS_IDLE ].includes( status ) ) { - dispatch( { type: ACTION_SCAN_CANCEL } ); - } - }, [ stale, status ] ); - - /** - * Monitor changes to the options. + * Whenever options change, cancel the current scan (if in progress) and + * refetch the scannable URLs. */ const previousDidSaveOptions = usePrevious( didSaveOptions ); useEffect( () => { From 22ec5979e4619d94438b4bed638ccdff28a19087 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Tue, 26 Oct 2021 22:11:58 +0200 Subject: [PATCH 102/120] Remove unused imports --- assets/src/components/site-scan-results/site-scan-results.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/src/components/site-scan-results/site-scan-results.js b/assets/src/components/site-scan-results/site-scan-results.js index 58d2bdd2e60..0a8bb2537cd 100644 --- a/assets/src/components/site-scan-results/site-scan-results.js +++ b/assets/src/components/site-scan-results/site-scan-results.js @@ -1,8 +1,7 @@ /** * WordPress dependencies */ -import { ExternalLink, VisuallyHidden } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { VisuallyHidden } from '@wordpress/components'; /** * External dependencies From 021df6f03738217367a24ff84a55110ca7fa00ea Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 12:33:25 +0200 Subject: [PATCH 103/120] Improve logic for getting plugin slug from file path Co-authored-by: Weston Ruter --- assets/src/common/helpers/get-plugin-slug-from-file.js | 2 +- .../plugins-context-provider/use-normalized-plugins-data.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/src/common/helpers/get-plugin-slug-from-file.js b/assets/src/common/helpers/get-plugin-slug-from-file.js index 2475000c3a5..bfced3fde52 100644 --- a/assets/src/common/helpers/get-plugin-slug-from-file.js +++ b/assets/src/common/helpers/get-plugin-slug-from-file.js @@ -12,5 +12,5 @@ * @return {string} Plugin slug. */ export function getPluginSlugFromFile( path = '' ) { - return path.split( '/' )?.[ 0 ]?.split( '.' )?.[ 0 ]; + return path.replace( /\/.*$/, '' ).replace( /\.php$/, '' ); } diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index 344bbe7cf5c..d62cc8113fe 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -19,12 +19,12 @@ export function useNormalizedPluginsData() { } setNormalizedPluginsData( plugins.reduce( ( accumulatedPluginsData, source ) => { - const slug = getPluginSlugFromFile( source?.plugin ); - - if ( ! slug ) { + if ( ! source?.plugin ) { return accumulatedPluginsData; } + const slug = getPluginSlugFromFile( source.plugin ); + return { ...accumulatedPluginsData, [ slug ]: Object.keys( source ).reduce( ( props, key ) => ( { From 68011a8782e4f8e7f46857846609763bdbda8d7d Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 12:37:08 +0200 Subject: [PATCH 104/120] Ensure `gutenberg` is excluded only if there are other error sources --- .../get-slugs-from-validation-results.js | 9 ++++----- .../test/get-slugs-from-validation-results.js | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js index 591011bcd58..d27f4898179 100644 --- a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js @@ -22,7 +22,10 @@ export function getSlugsFromValidationResults( validationResults = [] ) { for ( const source of result.sources ) { if ( source.type === 'plugin' ) { - plugins.add( getPluginSlugFromFile( source.name ) ); + const pluginSlug = getPluginSlugFromFile( source.name ); + if ( 'gutenberg' !== pluginSlug || result.sources.length === 1 ) { + plugins.add( pluginSlug ); + } } else if ( source.type === 'theme' ) { themes.add( source.name ); } @@ -32,10 +35,6 @@ export function getSlugsFromValidationResults( validationResults = [] ) { // Skip including AMP in the summary, since AMP is like core. plugins.delete( 'amp' ); - // Skip including Gutenberg in the summary if there is another plugin, since Gutenberg is like core. - if ( plugins.size > 1 && plugins.has( 'gutenberg' ) ) { - plugins.delete( 'gutenberg' ); - } return { plugins: [ ...plugins ], diff --git a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js index ef2f15b93e4..2615440ad41 100644 --- a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js @@ -58,7 +58,7 @@ describe( 'getSlugsFromValidationResults', () => { const slugs = getSlugsFromValidationResults( validationResult ); - expect( slugs.plugins ).toStrictEqual( [ 'jetpack', 'foo-bar' ] ); + expect( slugs.plugins ).toStrictEqual( [ 'gutenberg', 'jetpack', 'foo-bar' ] ); expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); } ); From 4b4d7eac1141dcac77b761e3ea197c2218c1a2b8 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 12:39:15 +0200 Subject: [PATCH 105/120] Update "Validated URLs" link text Co-authored-by: Weston Ruter --- assets/src/onboarding-wizard/pages/site-scan/index.js | 4 ++-- assets/src/settings-page/site-scan.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index b3cdb590d08..52b11554fd2 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -117,7 +117,7 @@ export function SiteScan() { slugs={ themesWithAmpIncompatibility } callToAction={ userIsTechnical && ( - { __( 'AMP Validated URLs page', 'amp' ) } + { __( 'Review Validated URLs', 'amp' ) } ) } /> @@ -128,7 +128,7 @@ export function SiteScan() { slugs={ pluginsWithAmpIncompatibility } callToAction={ userIsTechnical && ( - { __( 'AMP Validated URLs page', 'amp' ) } + { __( 'Review Validated URLs', 'amp' ) } ) } /> diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 2ba7bd070bd..7b82221876f 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -314,7 +314,7 @@ function SiteScanSummary() { slugs={ themesWithAmpIncompatibility } callToAction={ userIsTechnical && ! stale ? ( - { __( 'AMP Validated URLs page', 'amp' ) } + { __( 'Review Validated URLs', 'amp' ) } ) : null } /> @@ -324,7 +324,7 @@ function SiteScanSummary() { slugs={ pluginsWithAmpIncompatibility } callToAction={ userIsTechnical && ! stale ? ( - { __( 'AMP Validated URLs page', 'amp' ) } + { __( 'Review Validated URLs', 'amp' ) } ) : null } /> From bd39a6e67806c52dbe96fe1b1953f5888ed41886 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 14:17:40 +0200 Subject: [PATCH 106/120] Rename test to match tested file and remove impossible test case --- ...-plugin-slug-from-path.js => get-plugin-slug-from-file.js} | 4 ---- 1 file changed, 4 deletions(-) rename assets/src/common/helpers/test/{get-plugin-slug-from-path.js => get-plugin-slug-from-file.js} (78%) diff --git a/assets/src/common/helpers/test/get-plugin-slug-from-path.js b/assets/src/common/helpers/test/get-plugin-slug-from-file.js similarity index 78% rename from assets/src/common/helpers/test/get-plugin-slug-from-path.js rename to assets/src/common/helpers/test/get-plugin-slug-from-file.js index 4d5fe9990b7..4d4813448fe 100644 --- a/assets/src/common/helpers/test/get-plugin-slug-from-path.js +++ b/assets/src/common/helpers/test/get-plugin-slug-from-file.js @@ -4,10 +4,6 @@ import { getPluginSlugFromFile } from '../get-plugin-slug-from-file'; describe( 'getPluginSlugFromFile', () => { - it( 'should return an empty string if no path is provided', () => { - expect( getPluginSlugFromFile() ).toBe( '' ); - } ); - it( 'should return correct plugin slug', () => { expect( getPluginSlugFromFile( 'foo' ) ).toBe( 'foo' ); expect( getPluginSlugFromFile( 'foo.php' ) ).toBe( 'foo' ); From 7d2f1d112004e206b71b8702242be78c3ad07d67 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 14:22:16 +0200 Subject: [PATCH 107/120] Add test for when `gutenberg` isn't the only plugin for validation error --- .../get-slugs-from-validation-results.js | 1 - .../test/get-slugs-from-validation-results.js | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js index d27f4898179..3da8237e2f3 100644 --- a/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/get-slugs-from-validation-results.js @@ -35,7 +35,6 @@ export function getSlugsFromValidationResults( validationResults = [] ) { // Skip including AMP in the summary, since AMP is like core. plugins.delete( 'amp' ); - return { plugins: [ ...plugins ], themes: [ ...themes ], diff --git a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js index 2615440ad41..cd75c1cb687 100644 --- a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js @@ -62,7 +62,7 @@ describe( 'getSlugsFromValidationResults', () => { expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); } ); - it( 'returns Gutenberg if it is the only plugin in the list', () => { + it( 'returns Gutenberg if it is the only plugin for a single validation error', () => { const validationResult = [ { sources: [ @@ -82,4 +82,19 @@ describe( 'getSlugsFromValidationResults', () => { expect( slugs.plugins ).toStrictEqual( [ 'gutenberg' ] ); expect( slugs.themes ).toStrictEqual( [ 'twentytwenty' ] ); } ); + + it( 'does not return Gutenberg if there are other plugins for the same validation error', () => { + const validationResult = [ + { + sources: [ + { type: 'plugin', name: 'gutenberg' }, + { type: 'plugin', name: 'jetpack' }, + ], + }, + ]; + + const slugs = getSlugsFromValidationResults( validationResult ); + + expect( slugs.plugins ).toStrictEqual( [ 'jetpack' ] ); + } ); } ); From cf218eaba9e4544a8bb806373058e9234a439666 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 17:02:23 +0200 Subject: [PATCH 108/120] Refactor template mode recommendation logic; use it on AMP Settings --- .../get-template-mode-recommendation.js} | 35 ++-- .../test/get-template-mode-recommendation.js | 17 ++ .../site-scan-context-provider/index.js | 2 +- .../components/template-mode-option/index.js | 6 +- .../pages/site-scan/index.js | 12 +- .../pages/template-mode/index.js | 11 +- .../pages/template-mode/screen-ui.js | 41 ++--- .../test/get-selection-details.js | 17 -- assets/src/settings-page/site-scan.js | 6 +- assets/src/settings-page/template-modes.js | 157 +++++++----------- src/Admin/OptionsMenu.php | 9 - 11 files changed, 141 insertions(+), 172 deletions(-) rename assets/src/{onboarding-wizard/pages/template-mode/get-selection-details.js => common/helpers/get-template-mode-recommendation.js} (87%) create mode 100644 assets/src/common/helpers/test/get-template-mode-recommendation.js delete mode 100644 assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js diff --git a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js b/assets/src/common/helpers/get-template-mode-recommendation.js similarity index 87% rename from assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js rename to assets/src/common/helpers/get-template-mode-recommendation.js index b6a95f0ff3d..8f68ef72b7c 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/get-selection-details.js +++ b/assets/src/common/helpers/get-template-mode-recommendation.js @@ -4,10 +4,11 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; + /** * Internal dependencies */ -import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; +import { READER, STANDARD, TRANSITIONAL } from '../constants'; // Recommendation levels. export const RECOMMENDED = 'recommended'; @@ -23,14 +24,20 @@ export const NON_TECHNICAL = 'nonTechnical'; * * @param {Object} args * @param {boolean} args.currentThemeIsAmongReaderThemes Whether the currently active theme is in the reader themes list. + * @param {boolean} args.hasPluginsWithAmpIncompatibility Whether the site scan found plugins with AMP incompatibility. + * @param {boolean} args.hasSiteScanResults Whether there are available site scan results. + * @param {boolean} args.hasThemesWithAmpIncompatibility Whether the site scan found themes with AMP incompatibility. * @param {boolean} args.userIsTechnical Whether the user answered yes to the technical question. - * @param {boolean} args.hasPluginsWithAMPIncompatibility Whether the site scan found plugins with AMP incompatibility. - * @param {boolean} args.hasThemesWithAMPIncompatibility Whether the site scan found themes with AMP incompatibility. - * @param {boolean} args.hasScanResults Whether there are available scan results. */ -export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTechnical, hasPluginsWithAMPIncompatibility, hasThemesWithAMPIncompatibility, hasScanResults = true } ) { +export function getTemplateModeRecommendation( { + currentThemeIsAmongReaderThemes, + hasPluginsWithAmpIncompatibility, + hasSiteScanResults, + hasThemesWithAmpIncompatibility, + userIsTechnical, +} ) { // Handle case where scanning has failed or did not run. - if ( ! hasScanResults ) { + if ( ! hasSiteScanResults ) { return { [ READER ]: { recommendationLevel: ( userIsTechnical || currentThemeIsAmongReaderThemes ) ? RECOMMENDED : NEUTRAL, @@ -54,9 +61,9 @@ export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTe } switch ( true ) { - case hasThemesWithAMPIncompatibility && hasPluginsWithAMPIncompatibility && userIsTechnical: - case hasThemesWithAMPIncompatibility && ! hasPluginsWithAMPIncompatibility && userIsTechnical: - case ! hasThemesWithAMPIncompatibility && hasPluginsWithAMPIncompatibility && userIsTechnical: + case hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && userIsTechnical: + case hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && userIsTechnical: + case ! hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && userIsTechnical: return { [ READER ]: { recommendationLevel: NEUTRAL, @@ -81,8 +88,8 @@ export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTe }, }; - case hasThemesWithAMPIncompatibility && hasPluginsWithAMPIncompatibility && ! userIsTechnical: - case ! hasThemesWithAMPIncompatibility && hasPluginsWithAMPIncompatibility && ! userIsTechnical: + case hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && ! userIsTechnical: + case ! hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && ! userIsTechnical: return { [ READER ]: { recommendationLevel: RECOMMENDED, @@ -105,7 +112,7 @@ export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTe }, }; - case hasThemesWithAMPIncompatibility && ! hasPluginsWithAMPIncompatibility && ! userIsTechnical: + case hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && ! userIsTechnical: return { [ READER ]: { recommendationLevel: RECOMMENDED, @@ -129,7 +136,7 @@ export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTe }, }; - case ! hasThemesWithAMPIncompatibility && ! hasPluginsWithAMPIncompatibility && userIsTechnical: + case ! hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && userIsTechnical: return { [ READER ]: { recommendationLevel: NOT_RECOMMENDED, @@ -151,7 +158,7 @@ export function getSelectionDetails( { currentThemeIsAmongReaderThemes, userIsTe }, }; - case ! hasThemesWithAMPIncompatibility && ! hasPluginsWithAMPIncompatibility && ! userIsTechnical: + case ! hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && ! userIsTechnical: return { [ READER ]: { recommendationLevel: NOT_RECOMMENDED, diff --git a/assets/src/common/helpers/test/get-template-mode-recommendation.js b/assets/src/common/helpers/test/get-template-mode-recommendation.js new file mode 100644 index 00000000000..e78a4086bda --- /dev/null +++ b/assets/src/common/helpers/test/get-template-mode-recommendation.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { getTemplateModeRecommendation } from '../get-template-mode-recommendation'; + +describe( 'getTemplateModeRecommendation', () => { + it( 'throws no errors', () => { + [ true, false ].forEach( ( hasPluginsWithAmpIncompatibility ) => { + [ true, false ].forEach( ( hasThemesWithAmpIncompatibility ) => { + [ true, false ].forEach( ( userIsTechnical ) => { + const cb = () => getTemplateModeRecommendation( { hasPluginsWithAmpIncompatibility, hasThemesWithAmpIncompatibility, userIsTechnical } ); + expect( cb ).not.toThrow(); + } ); + } ); + } ); + } ); +} ); diff --git a/assets/src/components/site-scan-context-provider/index.js b/assets/src/components/site-scan-context-provider/index.js index fa72538c398..cb339538b69 100644 --- a/assets/src/components/site-scan-context-provider/index.js +++ b/assets/src/components/site-scan-context-provider/index.js @@ -374,7 +374,7 @@ export function SiteScanContextProvider( { isCancelled: status === STATUS_CANCELLED, isCompleted: status === STATUS_COMPLETED, isFailed: status === STATUS_FAILED, - isInitializing: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), + isFetchingScannableUrls: [ STATUS_REQUEST_SCANNABLE_URLS, STATUS_FETCHING_SCANNABLE_URLS ].includes( status ), isReady: status === STATUS_READY, isSiteScannable: scannableUrls.length > 0, pluginsWithAmpIncompatibility, diff --git a/assets/src/components/template-mode-option/index.js b/assets/src/components/template-mode-option/index.js index 5ff86ddcfdf..ff7dbba6288 100644 --- a/assets/src/components/template-mode-option/index.js +++ b/assets/src/components/template-mode-option/index.js @@ -136,6 +136,7 @@ export function TemplateModeOption( { children, details, detailsUrl, initialOpen selected={ mode === themeSupport } >
+ { children } { Array.isArray( details ) && (
    { details.map( ( detail, index ) => ( @@ -148,7 +149,7 @@ export function TemplateModeOption( { children, details, detailsUrl, initialOpen ) ) }
) } - { ! Array.isArray( details ) && ( + { details && ! Array.isArray( details ) && (

{ detailsUrl && ( @@ -161,7 +162,6 @@ export function TemplateModeOption( { children, details, detailsUrl, initialOpen ) }

) } - { children }
); @@ -172,7 +172,7 @@ TemplateModeOption.propTypes = { details: PropTypes.oneOfType( [ PropTypes.string, PropTypes.arrayOf( PropTypes.string ), - ] ).isRequired, + ] ), detailsUrl: PropTypes.string, initialOpen: PropTypes.bool, labelExtra: PropTypes.node, diff --git a/assets/src/onboarding-wizard/pages/site-scan/index.js b/assets/src/onboarding-wizard/pages/site-scan/index.js index 52b11554fd2..e42ae50eebe 100644 --- a/assets/src/onboarding-wizard/pages/site-scan/index.js +++ b/assets/src/onboarding-wizard/pages/site-scan/index.js @@ -36,7 +36,7 @@ export function SiteScan() { isCancelled, isCompleted, isFailed, - isInitializing, + isFetchingScannableUrls, isReady, pluginsWithAmpIncompatibility, scannableUrls, @@ -71,7 +71,7 @@ export function SiteScan() { */ const isDelayedCompleted = useDelayedFlag( isCompleted ); - if ( isInitializing ) { + if ( isFetchingScannableUrls ) { return ( { __( 'Review Validated URLs', 'amp' ) } - ) } + ) : null } /> ) } { pluginsWithAmpIncompatibility.length > 0 && ( { __( 'Review Validated URLs', 'amp' ) } - ) } + ) : null } /> ) } diff --git a/assets/src/onboarding-wizard/pages/template-mode/index.js b/assets/src/onboarding-wizard/pages/template-mode/index.js index 034ca6af774..a35374d7a34 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/index.js +++ b/assets/src/onboarding-wizard/pages/template-mode/index.js @@ -21,14 +21,12 @@ import { ScreenUI } from './screen-ui'; */ export function TemplateMode() { const { setCanGoForward } = useContext( Navigation ); - const { editedOptions, originalOptions, updateOptions } = useContext( Options ); + const { editedOptions: { theme_support: themeSupport }, originalOptions, updateOptions } = useContext( Options ); const { developerToolsOption } = useContext( User ); - const { pluginsWithAmpIncompatibility, themesWithAmpIncompatibility } = useContext( SiteScan ); - const { currentTheme } = useContext( ReaderThemes ); + const { hasSiteScanResults, pluginsWithAmpIncompatibility, themesWithAmpIncompatibility } = useContext( SiteScan ); + const { currentTheme: { is_reader_theme: currentThemeIsAmongReaderThemes } } = useContext( ReaderThemes ); const { technicalQuestionChangedAtLeastOnce } = useContext( TemplateModeOverride ); - const { theme_support: themeSupport } = editedOptions; - /** * Allow moving forward. */ @@ -57,9 +55,10 @@ export function TemplateMode() {
{ diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index cd96693f8a5..9678c24294e 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -23,8 +23,8 @@ import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; import { RECOMMENDED, NOT_RECOMMENDED, - getSelectionDetails, -} from './get-selection-details'; + getTemplateModeRecommendation, +} from '../../../common/helpers/get-template-mode-recommendation'; /** * Small notice indicating a mode is recommended. @@ -72,6 +72,7 @@ function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { * @param {boolean} props.currentThemeIsAmongReaderThemes Whether the currently active theme is in the list of reader themes. * @param {boolean} props.developerToolsOption Whether the user has enabled developer tools. * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. + * @param {boolean} props.hasSiteScanResults Whether there are available site scan results. * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. * @param {string[]} props.pluginsWithAmpIncompatibility A list of plugin slugs causing AMP incompatibility. * @param {string} props.savedCurrentMode The current selected mode saved in the database. @@ -81,47 +82,46 @@ export function ScreenUI( { currentThemeIsAmongReaderThemes, developerToolsOption, firstTimeInWizard, - technicalQuestionChanged, + hasSiteScanResults, pluginsWithAmpIncompatibility, savedCurrentMode, + technicalQuestionChanged, themesWithAmpIncompatibility, } ) { - const userIsTechnical = useMemo( () => developerToolsOption === true, [ developerToolsOption ] ); - - const selectionDetails = useMemo( () => getSelectionDetails( + const templateModeRecommendation = useMemo( () => getTemplateModeRecommendation( { currentThemeIsAmongReaderThemes, - userIsTechnical, - hasScanResults: null !== pluginsWithAmpIncompatibility && null !== themesWithAmpIncompatibility, - hasPluginsWithAMPIncompatibility: pluginsWithAmpIncompatibility && 0 < pluginsWithAmpIncompatibility.length, - hasThemesWithAMPIncompatibility: themesWithAmpIncompatibility && 0 < themesWithAmpIncompatibility.length, + hasPluginsWithAmpIncompatibility: pluginsWithAmpIncompatibility?.length > 0, + hasSiteScanResults, + hasThemesWithAmpIncompatibility: themesWithAmpIncompatibility?.length > 0, + userIsTechnical: developerToolsOption === true, }, - ), [ currentThemeIsAmongReaderThemes, themesWithAmpIncompatibility, pluginsWithAmpIncompatibility, userIsTechnical ] ); + ), [ currentThemeIsAmongReaderThemes, developerToolsOption, hasSiteScanResults, pluginsWithAmpIncompatibility, themesWithAmpIncompatibility ] ); return ( : null } + labelExtra={ templateModeRecommendation[ READER ].recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ templateModeRecommendation[ TRANSITIONAL ].recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ templateModeRecommendation[ STANDARD ].recommendationLevel === RECOMMENDED ? : null } /> ); @@ -131,6 +131,7 @@ ScreenUI.propTypes = { currentThemeIsAmongReaderThemes: PropTypes.bool.isRequired, developerToolsOption: PropTypes.bool, firstTimeInWizard: PropTypes.bool, + hasSiteScanResults: PropTypes.bool, technicalQuestionChanged: PropTypes.bool, pluginsWithAmpIncompatibility: PropTypes.arrayOf( PropTypes.string ), savedCurrentMode: PropTypes.string, diff --git a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js b/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js deleted file mode 100644 index ee025282570..00000000000 --- a/assets/src/onboarding-wizard/pages/template-mode/test/get-selection-details.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Internal dependencies - */ -import { getSelectionDetails } from '../get-selection-details'; - -describe( 'getSelectionDetails', () => { - it( 'throws no errors', () => { - [ true, false ].forEach( ( hasPluginIssues ) => { - [ true, false ].forEach( ( hasThemeIssues ) => { - [ true, false ].forEach( ( userIsTechnical ) => { - const cb = () => getSelectionDetails( { hasPluginIssues, hasThemeIssues, userIsTechnical } ); - expect( cb ).not.toThrow(); - } ); - } ); - } ); - } ); -} ); diff --git a/assets/src/settings-page/site-scan.js b/assets/src/settings-page/site-scan.js index 7b82221876f..3c380d06c86 100644 --- a/assets/src/settings-page/site-scan.js +++ b/assets/src/settings-page/site-scan.js @@ -46,7 +46,7 @@ export function SiteScan( { onSiteScan } ) { isCancelled, isCompleted, isFailed, - isInitializing, + isFetchingScannableUrls, isReady, isSiteScannable, pluginsWithAmpIncompatibility, @@ -84,7 +84,7 @@ export function SiteScan( { onSiteScan } ) { * Get main content. */ const getContent = useCallback( () => { - if ( isInitializing ) { + if ( isFetchingScannableUrls ) { return ; } @@ -123,7 +123,7 @@ export function SiteScan( { onSiteScan } ) { } return ; - }, [ isCancelled, isFailed, isInitializing, isSiteScannable, isSummary ] ); + }, [ isCancelled, isFailed, isFetchingScannableUrls, isSiteScannable, isSummary ] ); return ( : null, - readerNoticeLarge: ( - - { __( 'Your active theme is known to work well in standard mode.', 'amp' ) } - - ), - }; - - // Theme has built-in support or has declared theme support with the paired flag set to true. - case selected && ( IS_CORE_THEME || ( 'object' === typeof THEME_SUPPORT_ARGS && false !== THEME_SUPPORT_ARGS.paired ) ): - return { - readerNoticeSmall: selected ? : null, - readerNoticeLarge: ( - - { __( 'Your active theme is known to work well in standard and transitional mode.', 'amp' ) } - - ) }; - - // Support for reader mode was detected. - case THEME_SUPPORTS_READER_MODE: - return { - readerNoticeSmall: , - readerNoticeLarge: ( - - { __( 'Your theme indicates it has special support for the legacy templates in Reader mode.', 'amp' ) } - - ) }; - - default: - return { readerNoticeSmall: null, readerNoticeLarge: null }; - } -} - /** * Template modes section of the settings page. * @@ -101,15 +58,52 @@ function getReaderNotice( selected ) { * @param {boolean} props.focusReaderThemes Whether the reader themes drawer should be opened and focused. */ export function TemplateModes( { focusReaderThemes } ) { - const { editedOptions, updateOptions } = useContext( Options ); - const { selectedTheme, templateModeWasOverridden } = useContext( ReaderThemes ); + const { + editedOptions: { + sandboxing_level: sandboxingLevel, + theme_support: editedThemeSupport, + }, + updateOptions, + } = useContext( Options ); + const { + currentTheme: { + is_reader_theme: currentThemeIsAmongReaderThemes, + }, + selectedTheme, + templateModeWasOverridden, + } = useContext( ReaderThemes ); + const { + hasSiteScanResults, + isFetchingScannableUrls, + pluginsWithAmpIncompatibility, + themesWithAmpIncompatibility, + } = useContext( SiteScanContext ); + const { developerToolsOption } = useContext( User ); - const { theme_support: themeSupport, sandboxing_level: sandboxingLevel } = editedOptions; + const templateModeRecommendation = useMemo( () => getTemplateModeRecommendation( + { + currentThemeIsAmongReaderThemes, + hasPluginsWithAmpIncompatibility: pluginsWithAmpIncompatibility?.length > 0, + hasSiteScanResults, + hasThemesWithAmpIncompatibility: themesWithAmpIncompatibility?.length > 0, + userIsTechnical: developerToolsOption === true, + }, + ), [ currentThemeIsAmongReaderThemes, developerToolsOption, hasSiteScanResults, pluginsWithAmpIncompatibility?.length, themesWithAmpIncompatibility?.length ] ); - const { readerNoticeSmall, readerNoticeLarge } = useMemo( - () => getReaderNotice( READER === themeSupport ), - [ themeSupport ], - ); + const getLabelForTemplateMode = useCallback( ( mode ) => { + if ( isFetchingScannableUrls ) { + return null; + } + + switch ( templateModeRecommendation[ mode ].recommendationLevel ) { + case RECOMMENDED: + return ; + case NOT_RECOMMENDED: + return ; + default: + return null; + } + }, [ isFetchingScannableUrls, templateModeRecommendation ] ); return (
@@ -122,22 +116,12 @@ export function TemplateModes( { focusReaderThemes } ) { ) } : null } + labelExtra={ getLabelForTemplateMode( STANDARD ) } > - { - // Plugin is not configured; active theme has built-in support or has declared theme support without the paired flag. - ( IS_CORE_THEME || 'object' === typeof THEME_SUPPORT_ARGS ) && ( - -

- { __( 'Your active theme is known to work well in standard mode.', 'amp' ) } -

-
- ) - } { sandboxingLevel && (
@@ -203,33 +187,20 @@ export function TemplateModes( { focusReaderThemes } ) { } : null } - > - { - // Plugin is not configured; active theme has built-in support or has declared theme support with the paired flag. - ( IS_CORE_THEME || ( 'object' === typeof THEME_SUPPORT_ARGS && true === THEME_SUPPORT_ARGS.paired ) ) && ( - -

- { __( 'Your active theme is known to work well in transitional mode.', 'amp' ) } -

-
- ) - } -
+ labelExtra={ getLabelForTemplateMode( TRANSITIONAL ) } + /> - { readerNoticeLarge } - - { READER === themeSupport && ( + labelExtra={ getLabelForTemplateMode( READER ) } + /> + { READER === editedThemeSupport && ( '/amp/v1/options', 'READER_THEMES_REST_PATH' => '/amp/v1/reader-themes', 'SCANNABLE_URLS_REST_PATH' => '/amp/v1/scannable-urls', - 'IS_CORE_THEME' => in_array( - get_stylesheet(), - AMP_Core_Theme_Sanitizer::get_supported_themes(), - true - ), 'LEGACY_THEME_SLUG' => ReaderThemes::DEFAULT_READER_THEME, 'USING_FALLBACK_READER_THEME' => $this->reader_themes->using_fallback_theme(), - 'THEME_SUPPORT_ARGS' => AMP_Theme_Support::get_theme_support_args(), - 'THEME_SUPPORTS_READER_MODE' => AMP_Theme_Support::supports_reader_mode(), 'UPDATES_NONCE' => wp_create_nonce( 'updates' ), 'USER_FIELD_DEVELOPER_TOOLS_ENABLED' => UserAccess::USER_FIELD_DEVELOPER_TOOLS_ENABLED, 'USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE' => UserRESTEndpointExtension::USER_FIELD_REVIEW_PANEL_DISMISSED_FOR_TEMPLATE_MODE, From 27a11e2f44a1bfa0d976af203c04deeaeefc17c9 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 17:51:59 +0200 Subject: [PATCH 109/120] Prevent running E2E tests on empty WordPress instance --- tests/e2e/config/bootstrap.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/e2e/config/bootstrap.js b/tests/e2e/config/bootstrap.js index 6237d5d1704..433002117ba 100644 --- a/tests/e2e/config/bootstrap.js +++ b/tests/e2e/config/bootstrap.js @@ -12,6 +12,7 @@ import { isOfflineMode, setBrowserViewport, trashAllPosts, + visitAdminPage, } from '@wordpress/e2e-test-utils'; /** @@ -213,6 +214,17 @@ async function setupBrowser() { } ); } +/** + * Create test posts so that the WordPress instance has some data. + */ +async function createTestData() { + await visitAdminPage( 'admin.php', 'page=amp-options' ); + await Promise.all( [ + page.evaluate( () => wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 1', status: 'publish' } } ) ), + page.evaluate( () => wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 2', status: 'publish' } } ) ), + ] ); +} + /** * Before every test suite run, delete all content created by the test. This ensures * other posts/comments/etc. aren't dirtying tests and tests don't depend on @@ -225,6 +237,7 @@ beforeAll( async () => { observeConsoleLogging(); await setupBrowser(); await trashAllPosts(); + await createTestData(); await cleanUpSettings(); await page.setDefaultNavigationTimeout( 10000 ); await page.setDefaultTimeout( 10000 ); From 4cb359797cad78e45319b84520f21dd135a70d1a Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 17:53:02 +0200 Subject: [PATCH 110/120] Update Onboarding Wizard Site Scan E2E test --- tests/e2e/specs/amp-onboarding/site-scan.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/specs/amp-onboarding/site-scan.js b/tests/e2e/specs/amp-onboarding/site-scan.js index c3cc7f260cc..3a54563c4b0 100644 --- a/tests/e2e/specs/amp-onboarding/site-scan.js +++ b/tests/e2e/specs/amp-onboarding/site-scan.js @@ -23,7 +23,7 @@ import { uninstallPlugin, } from '../../utils/amp-settings-utils'; -describe( 'Site Scan', () => { +describe( 'Onboarding Wizard Site Scan Step', () => { beforeAll( async () => { await installTheme( 'hestia' ); await installPlugin( 'autoptimize' ); @@ -48,11 +48,11 @@ describe( 'Site Scan', () => { } ), ] ); - await testNextButton( { text: 'Next' } ); - await testPreviousButton( { text: 'Previous' } ); - await expect( page ).toMatchElement( '.site-scan__heading', { text: 'Scan complete', timeout: 10000 } ); await expect( page ).toMatchElement( '.site-scan__section p', { text: /Site scan found no issues/ } ); + + await testNextButton( { text: 'Next' } ); + await testPreviousButton( { text: 'Previous' } ); } ); it( 'should list out plugin and theme issues after the scan', async () => { From 807532c6c89262ac16785328e298bb633e7288fc Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 19:12:34 +0200 Subject: [PATCH 111/120] Fix E2E tests for Site Scan panel on AMP Settings screen --- tests/e2e/config/bootstrap.js | 11 +++-- tests/e2e/specs/admin/site-scan-panel.js | 54 ++++++++++++------------ tests/e2e/utils/amp-settings-utils.js | 2 +- tests/e2e/utils/site-scan-utils.js | 2 + 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/tests/e2e/config/bootstrap.js b/tests/e2e/config/bootstrap.js index 433002117ba..a7239b498d9 100644 --- a/tests/e2e/config/bootstrap.js +++ b/tests/e2e/config/bootstrap.js @@ -219,10 +219,13 @@ async function setupBrowser() { */ async function createTestData() { await visitAdminPage( 'admin.php', 'page=amp-options' ); - await Promise.all( [ - page.evaluate( () => wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 1', status: 'publish' } } ) ), - page.evaluate( () => wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 2', status: 'publish' } } ) ), - ] ); + await page.waitForSelector( '.amp-settings-nav' ); + await page.evaluate( async () => { + await Promise.all( [ + wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 1', status: 'publish' } } ), + wp.apiFetch( { path: '/wp/v2/posts', method: 'POST', data: { title: 'Test Post 2', status: 'publish' } } ), + ] ); + } ); } /** diff --git a/tests/e2e/specs/admin/site-scan-panel.js b/tests/e2e/specs/admin/site-scan-panel.js index 2617c08e7b4..596edd07a1a 100644 --- a/tests/e2e/specs/admin/site-scan-panel.js +++ b/tests/e2e/specs/admin/site-scan-panel.js @@ -22,29 +22,32 @@ import { cleanUpSettings, scrollToElement } from '../../utils/onboarding-wizard- import { testSiteScanning } from '../../utils/site-scan-utils'; describe( 'AMP settings screen Site Scan panel', () => { + const timeout = 10000; + beforeAll( async () => { await installTheme( 'hestia' ); await installPlugin( 'autoptimize' ); + + await cleanUpSettings(); + + await visitAdminPage( 'admin.php', 'page=amp-options' ); + await setTemplateMode( 'transitional' ); } ); afterAll( async () => { - await cleanUpSettings(); await deleteTheme( 'hestia', { newThemeSlug: 'twentytwenty' } ); await uninstallPlugin( 'autoptimize' ); - } ); - beforeEach( async () => { await cleanUpSettings(); } ); async function triggerSiteRescan() { - await page.waitForSelector( '#template-modes' ); - - // Switch template mode so that we have stale results for sure. - await setTemplateMode( 'transitional' ); + await expect( page ).toMatchElement( '#site-scan h2', { text: 'Site Scan' } ); - await page.waitForSelector( '.settings-site-scan' ); - await expect( page ).toMatchElement( 'h2', { text: 'Site Scan' } ); + const isPanelCollapsed = await page.$eval( '#site-scan .components-panel__body-toggle', ( el ) => el.ariaExpanded === 'false' ); + if ( isPanelCollapsed ) { + await scrollToElement( { selector: '#site-scan .components-panel__body-toggle', click: true } ); + } // Start the site scan. await Promise.all( [ @@ -53,25 +56,30 @@ describe( 'AMP settings screen Site Scan panel', () => { statusElementClassName: 'settings-site-scan__status', isAmpFirst: false, } ), - expect( page ).toMatchElement( '.settings-site-scan__footer a.is-primary', { text: 'Browse Site', timeout: 10000 } ), ] ); + + await expect( page ).toMatchElement( '.settings-site-scan__footer .is-primary', { text: 'Rescan Site', timeout } ); + await expect( page ).toMatchElement( '.settings-site-scan__footer .is-link', { text: 'Browse Site' } ); } it( 'does not list issues if an AMP compatible theme is activated', async () => { - await activateTheme( 'twentytwenty' ); - await visitAdminPage( 'admin.php', 'page=amp-options' ); await triggerSiteRescan(); - await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--success' ); + await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--success', { timeout } ); await expect( page ).not.toMatchElement( '.site-scan-results--themes' ); await expect( page ).not.toMatchElement( '.site-scan-results--plugins' ); - // Switch template mode to check if the scan results are marked as stale. + // Reload the page and confirm that the panel is collapsed. + await page.reload(); + await expect( page ).toMatchElement( '#site-scan .components-panel__body-toggle[aria-expanded="false"]' ); + + // Switch template mode to check if the scan results are marked as stale and the panel is initially expanded. await setTemplateMode( 'standard' ); + await expect( page ).toMatchElement( '#site-scan .components-panel__body-toggle[aria-expanded="true"]', { timeout } ); await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--info', { text: /^Stale results/ } ); } ); @@ -80,11 +88,9 @@ describe( 'AMP settings screen Site Scan panel', () => { await visitAdminPage( 'admin.php', 'page=amp-options' ); - await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--info', { text: /^Stale results/ } ); - await triggerSiteRescan(); - await expect( page ).toMatchElement( '.site-scan-results--themes .site-scan-results__heading[data-badge-content="1"]', { text: /^Themes/ } ); + await expect( page ).toMatchElement( '.site-scan-results--themes .site-scan-results__heading[data-badge-content="1"]', { text: /^Themes/, timeout } ); await expect( page ).toMatchElement( '.site-scan-results--themes .site-scan-results__source-name', { text: /Hestia/ } ); } ); @@ -94,15 +100,13 @@ describe( 'AMP settings screen Site Scan panel', () => { await visitAdminPage( 'admin.php', 'page=amp-options' ); - await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--info', { text: /^Stale results/ } ); - await triggerSiteRescan(); - await expect( page ).not.toMatchElement( '.site-scan-results--themes' ); - - await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__heading[data-badge-content="1"]', { text: /^Plugins/ } ); + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__heading[data-badge-content="1"]', { text: /^Plugins/, timeout } ); await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-name', { text: /Autoptimize/ } ); + await expect( page ).not.toMatchElement( '.site-scan-results--themes' ); + await deactivatePlugin( 'autoptimize' ); } ); @@ -112,11 +116,9 @@ describe( 'AMP settings screen Site Scan panel', () => { await visitAdminPage( 'admin.php', 'page=amp-options' ); - await expect( page ).toMatchElement( '.settings-site-scan .amp-notice--info', { text: /^Stale results/ } ); - await triggerSiteRescan(); - await expect( page ).toMatchElement( '.site-scan-results--themes' ); + await expect( page ).toMatchElement( '.site-scan-results--themes', { timeout } ); await expect( page ).toMatchElement( '.site-scan-results--plugins' ); const totalIssuesCount = await page.$$eval( '.site-scan-results__source', ( sources ) => sources.length ); @@ -136,7 +138,7 @@ describe( 'AMP settings screen Site Scan panel', () => { await triggerSiteRescan(); - await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-name', { text: /Autoptimize/ } ); + await expect( page ).toMatchElement( '.site-scan-results--plugins .site-scan-results__source-name', { text: /Autoptimize/, timeout } ); // Deactivate the plugin and test. await deactivatePlugin( 'autoptimize' ); diff --git a/tests/e2e/utils/amp-settings-utils.js b/tests/e2e/utils/amp-settings-utils.js index 3835c55aadb..67b61a57adb 100644 --- a/tests/e2e/utils/amp-settings-utils.js +++ b/tests/e2e/utils/amp-settings-utils.js @@ -23,7 +23,7 @@ export async function setTemplateMode( mode ) { // Save options and wait for the request to succeed. await Promise.all( [ scrollToElement( { selector: '.amp-settings-nav button[type="submit"]', click: true } ), - page.waitForResponse( ( response ) => response.url().includes( '/wp-json/amp/v1/options' ), { timeout: 10000 } ), + page.waitForResponse( ( response ) => response.url().includes( '/wp-json/amp/v1/options' ) ), ] ); } diff --git a/tests/e2e/utils/site-scan-utils.js b/tests/e2e/utils/site-scan-utils.js index f75b3ef0382..d01f8b8fa95 100644 --- a/tests/e2e/utils/site-scan-utils.js +++ b/tests/e2e/utils/site-scan-utils.js @@ -1,4 +1,6 @@ export async function testSiteScanning( { statusElementClassName, isAmpFirst } ) { + await page.waitForSelector( `.${ statusElementClassName }` ); + const statusTextRegex = /^Scanning ([\d])+\/([\d]+) URLs/; const statusText = await page.$eval( `.${ statusElementClassName }`, ( el ) => el.innerText ); From c83a2a9471e76deeb76d8d736321cc5e520db3f6 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 19:28:47 +0200 Subject: [PATCH 112/120] Fix E2E tests for Site Review panel on AMP Settings screen --- tests/e2e/specs/admin/site-review-panel.js | 41 ++++++++-------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/tests/e2e/specs/admin/site-review-panel.js b/tests/e2e/specs/admin/site-review-panel.js index 6533e1c9f45..fdcb2cfeb25 100644 --- a/tests/e2e/specs/admin/site-review-panel.js +++ b/tests/e2e/specs/admin/site-review-panel.js @@ -10,32 +10,13 @@ import { cleanUpSettings, scrollToElement } from '../../utils/onboarding-wizard- import { setTemplateMode } from '../../utils/amp-settings-utils'; describe( 'AMP settings screen Review panel', () => { - let testPost; + const timeout = 10000; beforeAll( async () => { - await visitAdminPage( 'admin.php', 'page=amp-options' ); - - testPost = await page.evaluate( () => wp.apiFetch( { - path: '/wp/v2/posts', - method: 'POST', - data: { title: 'Test Post', status: 'publish' }, - } ) ); - } ); - - afterAll( async () => { - await visitAdminPage( 'admin.php', 'page=amp-options' ); - - if ( testPost?.id ) { - await page.evaluate( ( id ) => wp.apiFetch( { - path: `/wp/v2/posts/${ id }`, - method: 'DELETE', - data: { force: true }, - } ), testPost.id ); - } + await cleanUpSettings(); } ); beforeEach( async () => { - await cleanUpSettings(); await visitAdminPage( 'admin.php', 'page=amp-options' ); } ); @@ -55,16 +36,20 @@ describe( 'AMP settings screen Review panel', () => { it( 'button redirects to an AMP page in transitional mode', async () => { await setTemplateMode( 'transitional' ); - await expect( page ).toClick( 'a', { text: 'Browse Site' } ); - await page.waitForNavigation(); + await Promise.all( [ + scrollToElement( { selector: '.settings-site-review__actions .is-primary', click: true, timeout } ), + page.waitForNavigation( { timeout } ), + ] ); const htmlAttributes = await page.$eval( 'html', ( el ) => el.getAttributeNames() ); await expect( htmlAttributes ).toContain( 'amp' ); } ); it( 'button redirects to an AMP page in reader mode', async () => { - await expect( page ).toClick( 'a', { text: 'Browse Site' } ); - await page.waitForNavigation(); + await Promise.all( [ + scrollToElement( { selector: '.settings-site-review__actions .is-primary', click: true, timeout } ), + page.waitForNavigation( { timeout } ), + ] ); const htmlAttributes = await page.$eval( 'html', ( el ) => el.getAttributeNames() ); await expect( htmlAttributes ).toContain( 'amp' ); @@ -73,8 +58,10 @@ describe( 'AMP settings screen Review panel', () => { it( 'button redirects to an AMP page in standard mode', async () => { await setTemplateMode( 'standard' ); - await expect( page ).toClick( 'a', { text: 'Browse Site' } ); - await page.waitForNavigation(); + await Promise.all( [ + scrollToElement( { selector: '.settings-site-review__actions .is-primary', click: true, timeout } ), + page.waitForNavigation( { timeout } ), + ] ); const htmlAttributes = await page.$eval( 'html', ( el ) => el.getAttributeNames() ); await expect( htmlAttributes ).toContain( 'amp' ); From cc9653c7364645c5f281f7a890ea20e1c81d8de0 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 19:32:21 +0200 Subject: [PATCH 113/120] Disable Template Mode E2E tests on AMP Settings screen Reason: the Template Mode recommendation logic underwent an overhaul when Site Scan feature had been added. New, tests that would be shared between the Settings screen and the Onboarding Wizard need to be written. --- tests/e2e/specs/admin/amp-options.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/e2e/specs/admin/amp-options.js b/tests/e2e/specs/admin/amp-options.js index 7140a83969e..83886515d71 100644 --- a/tests/e2e/specs/admin/amp-options.js +++ b/tests/e2e/specs/admin/amp-options.js @@ -63,18 +63,7 @@ describe( 'Settings screen when reader theme is active theme', () => { } ); describe( 'Mode info notices', () => { - it( 'shows expected notices for theme with built-in support', async () => { - await activateTheme( 'twentytwenty' ); - await visitAdminPage( 'admin.php', 'page=amp-options' ); - - await expect( page ).toMatchElement( '#template-mode-standard-container .amp-notice--info' ); - await expect( page ).toMatchElement( '#template-mode-transitional-container .amp-notice--info' ); - - await clickMode( 'reader' ); - - await expect( page ).toMatchElement( '#template-mode-reader-container .amp-notice--warning' ); - } ); - + it.todo( 'shows expected notices for theme with built-in support' ); it.todo( 'shows expected notices for theme with paired flag false' ); it.todo( 'shows expected notices for theme that only supports reader mode' ); } ); From 891b1d5585a739f2617d5ba1a7537b45c808b6c6 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Wed, 27 Oct 2021 19:36:32 +0200 Subject: [PATCH 114/120] E2E: Ensure Reader Themes drawer is expanded before testing contents --- tests/e2e/specs/admin/amp-options.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/specs/admin/amp-options.js b/tests/e2e/specs/admin/amp-options.js index 83886515d71..291b2d0f0d6 100644 --- a/tests/e2e/specs/admin/amp-options.js +++ b/tests/e2e/specs/admin/amp-options.js @@ -55,7 +55,7 @@ describe( 'Settings screen when reader theme is active theme', () => { await clickMode( 'reader' ); await scrollToElement( { selector: '#template-mode-reader-container .components-panel__body-toggle', click: true } ); - await scrollToElement( { selector: '#reader-themes .amp-notice__body' } ); + await scrollToElement( { selector: '#reader-themes .components-panel__body-toggle', click: true } ); await expect( page ).toMatchElement( '.amp-notice__body', { text: /^Your active theme/ } ); await activateTheme( 'twentytwenty' ); From 7920d78cb9849fe812969a09d44a62f0eab9e656 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 28 Oct 2021 12:18:08 +0200 Subject: [PATCH 115/120] Convert template mode recommendation function into custom hook --- .../get-template-mode-recommendation.js | 189 --------- .../use-template-mode-recommendation/index.js | 388 ++++++++++++++++++ .../test/get-template-mode-recommendation.js | 8 +- .../pages/template-mode/index.js | 19 +- .../pages/template-mode/screen-ui.js | 82 ++-- assets/src/settings-page/template-modes.js | 51 +-- 6 files changed, 446 insertions(+), 291 deletions(-) delete mode 100644 assets/src/common/helpers/get-template-mode-recommendation.js create mode 100644 assets/src/components/use-template-mode-recommendation/index.js rename assets/src/{common/helpers => components/use-template-mode-recommendation}/test/get-template-mode-recommendation.js (50%) diff --git a/assets/src/common/helpers/get-template-mode-recommendation.js b/assets/src/common/helpers/get-template-mode-recommendation.js deleted file mode 100644 index 8f68ef72b7c..00000000000 --- a/assets/src/common/helpers/get-template-mode-recommendation.js +++ /dev/null @@ -1,189 +0,0 @@ -/* eslint-disable complexity */ - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { READER, STANDARD, TRANSITIONAL } from '../constants'; - -// Recommendation levels. -export const RECOMMENDED = 'recommended'; -export const NEUTRAL = 'neutral'; -export const NOT_RECOMMENDED = 'notRecommended'; - -// Technical levels. -export const TECHNICAL = 'technical'; -export const NON_TECHNICAL = 'nonTechnical'; - -/** - * Returns the degree to which each mode is recommended for the current site and user. - * - * @param {Object} args - * @param {boolean} args.currentThemeIsAmongReaderThemes Whether the currently active theme is in the reader themes list. - * @param {boolean} args.hasPluginsWithAmpIncompatibility Whether the site scan found plugins with AMP incompatibility. - * @param {boolean} args.hasSiteScanResults Whether there are available site scan results. - * @param {boolean} args.hasThemesWithAmpIncompatibility Whether the site scan found themes with AMP incompatibility. - * @param {boolean} args.userIsTechnical Whether the user answered yes to the technical question. - */ -export function getTemplateModeRecommendation( { - currentThemeIsAmongReaderThemes, - hasPluginsWithAmpIncompatibility, - hasSiteScanResults, - hasThemesWithAmpIncompatibility, - userIsTechnical, -} ) { - // Handle case where scanning has failed or did not run. - if ( ! hasSiteScanResults ) { - return { - [ READER ]: { - recommendationLevel: ( userIsTechnical || currentThemeIsAmongReaderThemes ) ? RECOMMENDED : NEUTRAL, - details: [ - __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: currentThemeIsAmongReaderThemes ? RECOMMENDED : NEUTRAL, - details: [ - __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), - ], - }, - }; - } - - switch ( true ) { - case hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && userIsTechnical: - case hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && userIsTechnical: - case ! hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && userIsTechnical: - return { - [ READER ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), - __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), - __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: RECOMMENDED, - details: [ - __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), - __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), - ], - }, - }; - - case hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && ! userIsTechnical: - case ! hasThemesWithAmpIncompatibility && hasPluginsWithAmpIncompatibility && ! userIsTechnical: - return { - [ READER ]: { - recommendationLevel: RECOMMENDED, - details: [ - __( 'Recommended as an easy way to enable AMP on your site despite the issues detected during site scanning.', 'amp' ), - __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), - ], - }, - }; - - case hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && ! userIsTechnical: - return { - [ READER ]: { - recommendationLevel: RECOMMENDED, - details: [ - __( 'Recommended to easily enable AMP on your site despite the issues detected on your theme.', 'amp' ), - __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'Choose this mode if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), - __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), - ], - }, - }; - - case ! hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && userIsTechnical: - return { - [ READER ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: RECOMMENDED, - details: [ - __( 'Recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), - ], - }, - }; - - case ! hasThemesWithAmpIncompatibility && ! hasPluginsWithAmpIncompatibility && ! userIsTechnical: - return { - [ READER ]: { - recommendationLevel: NOT_RECOMMENDED, - details: [ - __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), - ], - }, - [ TRANSITIONAL ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'Recommended choice if you can’t commit to choosing plugins that are AMP compatible when extending your site. This mode will make it easy to keep AMP content even if non-AMP-compatible plugins are used later on.', 'amp' ), - ], - }, - [ STANDARD ]: { - recommendationLevel: NEUTRAL, - details: [ - __( 'Recommended choice if you can commit to always choosing plugins that are AMP compatible when extending your site.', 'amp' ), - ], - }, - }; - - default: { - throw new Error( __( 'A template mode recommendation case was not accounted for.', 'amp' ) ); - } - } -} - -/* eslint-enable complexity */ diff --git a/assets/src/components/use-template-mode-recommendation/index.js b/assets/src/components/use-template-mode-recommendation/index.js new file mode 100644 index 00000000000..1addf727f5e --- /dev/null +++ b/assets/src/components/use-template-mode-recommendation/index.js @@ -0,0 +1,388 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useContext, useLayoutEffect, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { READER, STANDARD, TRANSITIONAL } from '../../common/constants'; +import { ReaderThemes } from '../reader-themes-context-provider'; +import { SiteScan as SiteScanContext } from '../site-scan-context-provider'; +import { User } from '../user-context-provider'; +import { Options } from '../options-context-provider'; + +// Recommendation levels. +export const RECOMMENDED = 'recommended'; +export const NEUTRAL = 'neutral'; +export const NOT_RECOMMENDED = 'notRecommended'; + +// Technical levels. +export const TECHNICAL = 'technical'; +export const NON_TECHNICAL = 'nonTechnical'; + +export function useTemplateModeRecommendation() { + const { currentTheme: { is_reader_theme: currentThemeIsAmongReaderThemes } } = useContext( ReaderThemes ); + const { + hasSiteScanResults, + isBusy, + isFetchingScannableUrls, + pluginsWithAmpIncompatibility, + stale, + themesWithAmpIncompatibility, + } = useContext( SiteScanContext ); + const { originalDeveloperToolsOption } = useContext( User ); + const { fetchingOptions, savingOptions } = useContext( Options ); + const [ templateModeRecommendation, setTemplateModeRecommendation ] = useState( null ); + + useLayoutEffect( () => { + if ( isBusy || isFetchingScannableUrls || fetchingOptions || savingOptions ) { + return; + } + + setTemplateModeRecommendation( getTemplateModeRecommendation( { + currentThemeIsAmongReaderThemes, + hasPluginIssues: pluginsWithAmpIncompatibility?.length > 0, + hasSiteScanResults: hasSiteScanResults && ! stale, + hasThemeIssues: themesWithAmpIncompatibility?.length > 0, + userIsTechnical: originalDeveloperToolsOption === true, + } ) ); + }, [ currentThemeIsAmongReaderThemes, fetchingOptions, hasSiteScanResults, isBusy, isFetchingScannableUrls, originalDeveloperToolsOption, pluginsWithAmpIncompatibility?.length, savingOptions, stale, themesWithAmpIncompatibility?.length ] ); + + return { + templateModeRecommendation, + staleTemplateModeRecommendation: stale, + }; +} + +/* eslint-disable complexity */ + +/** + * Returns the degree to which each mode is recommended for the current site and user. + * + * @param {Object} args + * @param {boolean} args.currentThemeIsAmongReaderThemes Whether the currently active theme is in the reader themes list. + * @param {boolean} args.hasPluginIssues Whether the site scan found plugins with AMP incompatibility. + * @param {boolean} args.hasSiteScanResults Whether there are available site scan results. + * @param {boolean} args.hasThemeIssues Whether the site scan found themes with AMP incompatibility. + * @param {boolean} args.userIsTechnical Whether the user answered yes to the technical question. + */ +export function getTemplateModeRecommendation( { + currentThemeIsAmongReaderThemes, + hasPluginIssues, + hasSiteScanResults, + hasThemeIssues, + userIsTechnical, +} ) { + switch ( true ) { + /** + * #1 + */ + case hasThemeIssues && hasPluginIssues && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), + __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), + ], + }, + }; + + /** + * #2 + */ + case hasThemeIssues && hasPluginIssues && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended as an easy way to enable AMP on your site despite the issues detected during site scanning.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + }; + + /** + * #3 + */ + case hasThemeIssues && ! hasPluginIssues && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), + __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), + ], + }, + }; + + /** + * #4 + */ + case hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended to easily enable AMP on your site despite the issues detected on your theme.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Choose this mode if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + }; + + /** + * #5 + */ + case ! hasThemeIssues && hasPluginIssues && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Possible choice if you want to enable AMP on your site despite the compatibility issues found.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each with its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Choose this mode temporarily if issues can be fixed or if your theme degrades gracefully when JavaScript is disabled.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions with the same theme.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended, if you can fix the issues detected with plugins with and your theme.', 'amp' ), + __( 'Your site will be completely AMP (except where you opt-out of AMP for specific areas), and will use a single theme.', 'amp' ), + ], + }, + }; + + /** + * #6 + */ + case ! hasThemeIssues && hasPluginIssues && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended as an easy way to enable AMP on your site despite the issues detected during site scanning.', 'amp' ), + __( 'Your site will have non-AMP and AMP versions, each using its own theme.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as key functionality may be missing and development work might be required.', 'amp' ), + ], + }, + }; + + /** + * #7 + */ + case ! hasThemeIssues && ! hasPluginIssues && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'Recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + }; + + /** + * #8 + */ + case ! hasThemeIssues && ! hasPluginIssues && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NOT_RECOMMENDED, + details: [ + __( 'Not recommended as you have an AMP-compatible theme and no issues were detected with any of the plugins on your site.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Recommended choice if you can’t commit to choosing plugins that are AMP compatible when extending your site. This mode will make it easy to keep AMP content even if non-AMP-compatible plugins are used later on.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'Recommended choice if you can commit to always choosing plugins that are AMP compatible when extending your site.', 'amp' ), + ], + }, + }; + + /** + * No site scan scenarios. + */ + case ! hasSiteScanResults && currentThemeIsAmongReaderThemes && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), + ], + }, + }; + + case ! hasSiteScanResults && currentThemeIsAmongReaderThemes && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), + ], + }, + }; + + case ! hasSiteScanResults && ! currentThemeIsAmongReaderThemes && ! userIsTechnical: + return { + [ READER ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), + ], + }, + }; + + case ! hasSiteScanResults && ! currentThemeIsAmongReaderThemes && userIsTechnical: + return { + [ READER ]: { + recommendationLevel: RECOMMENDED, + details: [ + __( 'In Reader mode your site will have a non-AMP and an AMP version, and each version will use its own theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ TRANSITIONAL ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Transitional mode your site will have a non-AMP and an AMP version, and both will use the same theme. If automatic mobile redirection is enabled, the AMP version of the content will be served on mobile devices. If AMP-to-AMP linking is enabled, once users are on an AMP page, they will continue navigating your AMP content.', 'amp' ), + ], + }, + [ STANDARD ]: { + recommendationLevel: NEUTRAL, + details: [ + __( 'In Standard mode your site will be completely AMP (except in cases where you opt-out of AMP for specific parts of your site), and it will use a single theme.', 'amp' ), + ], + }, + }; + + default: + throw new Error( __( 'A template mode recommendation case was not accounted for.', 'amp' ) ); + } +} + +/* eslint-enable complexity */ diff --git a/assets/src/common/helpers/test/get-template-mode-recommendation.js b/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js similarity index 50% rename from assets/src/common/helpers/test/get-template-mode-recommendation.js rename to assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js index e78a4086bda..d10d46cf4f7 100644 --- a/assets/src/common/helpers/test/get-template-mode-recommendation.js +++ b/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js @@ -1,14 +1,14 @@ /** * Internal dependencies */ -import { getTemplateModeRecommendation } from '../get-template-mode-recommendation'; +import { getTemplateModeRecommendation } from '../index'; describe( 'getTemplateModeRecommendation', () => { it( 'throws no errors', () => { - [ true, false ].forEach( ( hasPluginsWithAmpIncompatibility ) => { - [ true, false ].forEach( ( hasThemesWithAmpIncompatibility ) => { + [ true, false ].forEach( ( hasPluginIssues ) => { + [ true, false ].forEach( ( hasThemeIssues ) => { [ true, false ].forEach( ( userIsTechnical ) => { - const cb = () => getTemplateModeRecommendation( { hasPluginsWithAmpIncompatibility, hasThemesWithAmpIncompatibility, userIsTechnical } ); + const cb = () => getTemplateModeRecommendation( { hasPluginIssues, hasThemeIssues, userIsTechnical, hasSiteScanResults: true } ); expect( cb ).not.toThrow(); } ); } ); diff --git a/assets/src/onboarding-wizard/pages/template-mode/index.js b/assets/src/onboarding-wizard/pages/template-mode/index.js index a35374d7a34..fbf552e3d39 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/index.js +++ b/assets/src/onboarding-wizard/pages/template-mode/index.js @@ -8,10 +8,8 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import './style.scss'; +import { useTemplateModeRecommendation } from '../../../components/use-template-mode-recommendation'; import { Navigation } from '../../components/navigation-context-provider'; -import { ReaderThemes } from '../../../components/reader-themes-context-provider'; -import { SiteScan } from '../../../components/site-scan-context-provider'; -import { User } from '../../../components/user-context-provider'; import { Options } from '../../../components/options-context-provider'; import { TemplateModeOverride } from '../../components/template-mode-override-context-provider'; import { ScreenUI } from './screen-ui'; @@ -21,11 +19,9 @@ import { ScreenUI } from './screen-ui'; */ export function TemplateMode() { const { setCanGoForward } = useContext( Navigation ); - const { editedOptions: { theme_support: themeSupport }, originalOptions, updateOptions } = useContext( Options ); - const { developerToolsOption } = useContext( User ); - const { hasSiteScanResults, pluginsWithAmpIncompatibility, themesWithAmpIncompatibility } = useContext( SiteScan ); - const { currentTheme: { is_reader_theme: currentThemeIsAmongReaderThemes } } = useContext( ReaderThemes ); + const { editedOptions: { theme_support: themeSupport }, originalOptions } = useContext( Options ); const { technicalQuestionChangedAtLeastOnce } = useContext( TemplateModeOverride ); + const { templateModeRecommendation } = useTemplateModeRecommendation(); /** * Allow moving forward. @@ -55,17 +51,10 @@ export function TemplateMode() { { - updateOptions( { theme_support: mode } ); - } } technicalQuestionChanged={ technicalQuestionChangedAtLeastOnce } - themesWithAmpIncompatibility={ themesWithAmpIncompatibility } + templateModeRecommendation={ templateModeRecommendation } /> ); diff --git a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js index 9678c24294e..92215f400a0 100644 --- a/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js +++ b/assets/src/onboarding-wizard/pages/template-mode/screen-ui.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /** @@ -13,18 +12,10 @@ import PropTypes from 'prop-types'; /** * Internal dependencies */ -import { - AMPNotice, - NOTICE_TYPE_SUCCESS, - NOTICE_SIZE_SMALL, -} from '../../../components/amp-notice'; +import { AMPNotice, NOTICE_TYPE_SUCCESS, NOTICE_SIZE_SMALL } from '../../../components/amp-notice'; import { TemplateModeOption } from '../../../components/template-mode-option'; import { READER, STANDARD, TRANSITIONAL } from '../../../common/constants'; -import { - RECOMMENDED, - NOT_RECOMMENDED, - getTemplateModeRecommendation, -} from '../../../common/helpers/get-template-mode-recommendation'; +import { RECOMMENDED, NOT_RECOMMENDED } from '../../../components/use-template-mode-recommendation'; /** * Small notice indicating a mode is recommended. @@ -41,11 +32,11 @@ function RecommendedNotice() { * Determine if a template mode option should be initially open. * * @param {string} mode Template mode to check. - * @param {Array} selectionDetails Selection details. + * @param {Object} selectionDetails Selection details. * @param {string} savedCurrentMode Currently saved template mode. */ function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { - if ( savedCurrentMode === mode ) { + if ( savedCurrentMode === mode || ! selectionDetails ) { return true; } @@ -61,79 +52,70 @@ function isInitiallyOpen( mode, selectionDetails, savedCurrentMode ) { * RECOMMENDED. */ default: - return ! Boolean( Object.values( selectionDetails ).find( ( item ) => item.recommendationLevel === RECOMMENDED ) ); + return ! Object.values( selectionDetails ).some( ( item ) => item.recommendationLevel === RECOMMENDED ); } } /** * The interface for the mode selection screen. Avoids using context for easier testing. * - * @param {Object} props Component props. - * @param {boolean} props.currentThemeIsAmongReaderThemes Whether the currently active theme is in the list of reader themes. - * @param {boolean} props.developerToolsOption Whether the user has enabled developer tools. - * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. - * @param {boolean} props.hasSiteScanResults Whether there are available site scan results. - * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. - * @param {string[]} props.pluginsWithAmpIncompatibility A list of plugin slugs causing AMP incompatibility. - * @param {string} props.savedCurrentMode The current selected mode saved in the database. - * @param {string[]} props.themesWithAmpIncompatibility A list of theme slugs causing AMP incompatibility. + * @param {Object} props Component props. + * @param {boolean} props.firstTimeInWizard Whether the wizard is running for the first time. + * @param {boolean} props.technicalQuestionChanged Whether the user changed their technical question from the previous option. + * @param {Object} props.templateModeRecommendation Recommendations for each template mode. + * @param {string} props.savedCurrentMode The current selected mode saved in the database. */ export function ScreenUI( { - currentThemeIsAmongReaderThemes, - developerToolsOption, firstTimeInWizard, - hasSiteScanResults, - pluginsWithAmpIncompatibility, savedCurrentMode, technicalQuestionChanged, - themesWithAmpIncompatibility, + templateModeRecommendation, } ) { - const templateModeRecommendation = useMemo( () => getTemplateModeRecommendation( - { - currentThemeIsAmongReaderThemes, - hasPluginsWithAmpIncompatibility: pluginsWithAmpIncompatibility?.length > 0, - hasSiteScanResults, - hasThemesWithAmpIncompatibility: themesWithAmpIncompatibility?.length > 0, - userIsTechnical: developerToolsOption === true, - }, - ), [ currentThemeIsAmongReaderThemes, developerToolsOption, hasSiteScanResults, pluginsWithAmpIncompatibility, themesWithAmpIncompatibility ] ); - return (
: null } + labelExtra={ templateModeRecommendation?.[ READER ]?.recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ templateModeRecommendation?.[ TRANSITIONAL ]?.recommendationLevel === RECOMMENDED ? : null } /> : null } + labelExtra={ templateModeRecommendation?.[ STANDARD ]?.recommendationLevel === RECOMMENDED ? : null } /> ); } ScreenUI.propTypes = { - currentThemeIsAmongReaderThemes: PropTypes.bool.isRequired, - developerToolsOption: PropTypes.bool, firstTimeInWizard: PropTypes.bool, - hasSiteScanResults: PropTypes.bool, - technicalQuestionChanged: PropTypes.bool, - pluginsWithAmpIncompatibility: PropTypes.arrayOf( PropTypes.string ), savedCurrentMode: PropTypes.string, - themesWithAmpIncompatibility: PropTypes.arrayOf( PropTypes.string ), + technicalQuestionChanged: PropTypes.bool, + templateModeRecommendation: PropTypes.shape( { + [ READER ]: PropTypes.shape( { + recommendationLevel: PropTypes.string, + details: PropTypes.array, + } ), + [ TRANSITIONAL ]: PropTypes.shape( { + recommendationLevel: PropTypes.string, + details: PropTypes.array, + } ), + [ STANDARD ]: PropTypes.shape( { + recommendationLevel: PropTypes.string, + details: PropTypes.array, + } ), + } ), }; diff --git a/assets/src/settings-page/template-modes.js b/assets/src/settings-page/template-modes.js index 8b526756a88..624d3d0eb55 100644 --- a/assets/src/settings-page/template-modes.js +++ b/assets/src/settings-page/template-modes.js @@ -7,7 +7,7 @@ import PropTypes from 'prop-types'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { useCallback, useContext, useMemo } from '@wordpress/element'; +import { useCallback, useContext } from '@wordpress/element'; /** * Internal dependencies @@ -21,13 +21,15 @@ import { NOTICE_TYPE_WARNING, } from '../components/amp-notice'; import { Options } from '../components/options-context-provider'; -import { User } from '../components/user-context-provider'; -import { SiteScan as SiteScanContext } from '../components/site-scan-context-provider'; import { READER, STANDARD, TRANSITIONAL } from '../common/constants'; import { AMPDrawer } from '../components/amp-drawer'; import { ReaderThemes } from '../components/reader-themes-context-provider'; import { ReaderThemeCarousel } from '../components/reader-theme-carousel'; -import { getTemplateModeRecommendation, NOT_RECOMMENDED, RECOMMENDED } from '../common/helpers/get-template-mode-recommendation'; +import { + NOT_RECOMMENDED, + RECOMMENDED, + useTemplateModeRecommendation, +} from '../components/use-template-mode-recommendation'; /** * Small notice indicating a mode is recommended. @@ -65,33 +67,11 @@ export function TemplateModes( { focusReaderThemes } ) { }, updateOptions, } = useContext( Options ); - const { - currentTheme: { - is_reader_theme: currentThemeIsAmongReaderThemes, - }, - selectedTheme, - templateModeWasOverridden, - } = useContext( ReaderThemes ); - const { - hasSiteScanResults, - isFetchingScannableUrls, - pluginsWithAmpIncompatibility, - themesWithAmpIncompatibility, - } = useContext( SiteScanContext ); - const { developerToolsOption } = useContext( User ); - - const templateModeRecommendation = useMemo( () => getTemplateModeRecommendation( - { - currentThemeIsAmongReaderThemes, - hasPluginsWithAmpIncompatibility: pluginsWithAmpIncompatibility?.length > 0, - hasSiteScanResults, - hasThemesWithAmpIncompatibility: themesWithAmpIncompatibility?.length > 0, - userIsTechnical: developerToolsOption === true, - }, - ), [ currentThemeIsAmongReaderThemes, developerToolsOption, hasSiteScanResults, pluginsWithAmpIncompatibility?.length, themesWithAmpIncompatibility?.length ] ); + const { selectedTheme, templateModeWasOverridden } = useContext( ReaderThemes ); + const { templateModeRecommendation, staleTemplateModeRecommendation } = useTemplateModeRecommendation(); const getLabelForTemplateMode = useCallback( ( mode ) => { - if ( isFetchingScannableUrls ) { + if ( ! templateModeRecommendation ) { return null; } @@ -103,7 +83,7 @@ export function TemplateModes( { focusReaderThemes } ) { default: return null; } - }, [ isFetchingScannableUrls, templateModeRecommendation ] ); + }, [ templateModeRecommendation ] ); return (
@@ -115,8 +95,13 @@ export function TemplateModes( { focusReaderThemes } ) { { __( 'Because you selected a Reader theme that is the same as your site\'s active theme, your site has automatically been switched to Transitional template mode.', 'amp' ) } ) } + { staleTemplateModeRecommendation && ( + + { __( 'Because the Site Scan results are stale, the Template Mode recommendation may not be accurate. Rescan your site to ensure the recommendation is up to date.', 'amp' ) } + + ) } Date: Thu, 28 Oct 2021 15:14:20 +0200 Subject: [PATCH 116/120] E2E: test if template mode recommendation is marked as stale after scan results --- tests/e2e/specs/admin/amp-options.js | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/e2e/specs/admin/amp-options.js b/tests/e2e/specs/admin/amp-options.js index 291b2d0f0d6..1da65899de1 100644 --- a/tests/e2e/specs/admin/amp-options.js +++ b/tests/e2e/specs/admin/amp-options.js @@ -7,6 +7,7 @@ import { visitAdminPage, activateTheme, installTheme } from '@wordpress/e2e-test * Internal dependencies */ import { completeWizard, cleanUpSettings, clickMode, scrollToElement } from '../../utils/onboarding-wizard-utils'; +import { setTemplateMode } from '../../utils/amp-settings-utils'; describe( 'AMP settings screen newly activated', () => { beforeEach( async () => { @@ -63,6 +64,34 @@ describe( 'Settings screen when reader theme is active theme', () => { } ); describe( 'Mode info notices', () => { + it( 'show information in the Template Mode section if site scan results are stale', async () => { + await cleanUpSettings(); + await visitAdminPage( 'admin.php', 'page=amp-options' ); + + // Trigger a site scan. + await page.waitForSelector( '#site-scan' ); + + const isPanelCollapsed = await page.$eval( '#site-scan .components-panel__body-toggle', ( el ) => el.ariaExpanded === 'false' ); + if ( isPanelCollapsed ) { + await scrollToElement( { selector: '#site-scan .components-panel__body-toggle', click: true } ); + } + + await scrollToElement( { selector: '#site-scan .settings-site-scan__footer .is-primary', click: true } ); + await expect( page ).toMatchElement( '#site-scan .settings-site-scan__footer .is-primary', { text: 'Rescan Site', timeout: 10000 } ); + + // Confirm there is no notice about stale results. + const noticeXpath = '//*[@id="template-modes"]/*[contains(@class, "amp-notice--info")]/*[contains(text(), "Site Scan results are stale")]'; + const noticeBefore = await page.$x( noticeXpath ); + expect( noticeBefore ).toHaveLength( 0 ); + + // Change template mode to make the scan results stale. + await setTemplateMode( 'transitional' ); + await page.waitForSelector( '.settings-site-scan__footer .is-primary', { timeout: 10000 } ); + + const noticeAfter = await page.$x( noticeXpath ); + expect( noticeAfter ).toHaveLength( 1 ); + } ); + it.todo( 'shows expected notices for theme with built-in support' ); it.todo( 'shows expected notices for theme with paired flag false' ); it.todo( 'shows expected notices for theme that only supports reader mode' ); From 4a9f875366461657afc57bd9bfbfdec6ab18bc57 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 28 Oct 2021 15:24:31 +0200 Subject: [PATCH 117/120] Increase JS unit test coverage, remove impossible cases --- .../use-normalized-plugins-data.js | 4 ---- .../test/get-slugs-from-validation-results.js | 1 + .../test/use-normalized-themes-data.js | 2 +- .../use-normalized-themes-data.js | 24 +++++++------------ .../test/get-template-mode-recommendation.js | 11 ++++++++- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js index d62cc8113fe..aebc41c95fb 100644 --- a/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js +++ b/assets/src/components/plugins-context-provider/use-normalized-plugins-data.js @@ -19,10 +19,6 @@ export function useNormalizedPluginsData() { } setNormalizedPluginsData( plugins.reduce( ( accumulatedPluginsData, source ) => { - if ( ! source?.plugin ) { - return accumulatedPluginsData; - } - const slug = getPluginSlugFromFile( source.plugin ); return { diff --git a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js index cd75c1cb687..657e2b3fcbe 100644 --- a/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js +++ b/assets/src/components/site-scan-context-provider/test/get-slugs-from-validation-results.js @@ -6,6 +6,7 @@ import { getSlugsFromValidationResults } from '../get-slugs-from-validation-resu describe( 'getSlugsFromValidationResults', () => { it( 'returns empty arrays if no validation results are passed', () => { expect( getSlugsFromValidationResults() ).toMatchObject( { plugins: [], themes: [] } ); + expect( getSlugsFromValidationResults( [ { foo: 'bar' } ] ) ).toMatchObject( { plugins: [], themes: [] } ); } ); it( 'returns plugin and theme slugs', () => { diff --git a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js index 7e107ff877b..d9e4c72a31d 100644 --- a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js @@ -99,7 +99,7 @@ describe( 'useNormalizedThemesData', () => { stylesheet: 'twentytwenty', status: 'active', version: '1.7', - }, + } ] } > diff --git a/assets/src/components/themes-context-provider/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/use-normalized-themes-data.js index 9c035945418..b4d63b5e79b 100644 --- a/assets/src/components/themes-context-provider/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/use-normalized-themes-data.js @@ -17,21 +17,15 @@ export function useNormalizedThemesData() { return; } - setNormalizedThemesData( () => themes.reduce( ( accumulatedThemesData, source ) => { - if ( ! source?.stylesheet ) { - return accumulatedThemesData; - } - - return { - ...accumulatedThemesData, - [ source.stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { - ...props, - slug: source.stylesheet, - // Flatten every prop that contains a `raw` member. - [ key ]: source[ key ]?.raw ?? source[ key ], - } ), {} ), - }; - }, {} ) ); + setNormalizedThemesData( () => themes.reduce( ( accumulatedThemesData, source ) => ( { + ...accumulatedThemesData, + [ source.stylesheet ]: Object.keys( source ).reduce( ( props, key ) => ( { + ...props, + slug: source.stylesheet, + // Flatten every prop that contains a `raw` member. + [ key ]: source[ key ]?.raw ?? source[ key ], + } ), {} ), + } ), {} ) ); }, [ fetchingThemes, themes ] ); return normalizedThemesData; diff --git a/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js b/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js index d10d46cf4f7..4f47245e7a6 100644 --- a/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js +++ b/assets/src/components/use-template-mode-recommendation/test/get-template-mode-recommendation.js @@ -8,7 +8,16 @@ describe( 'getTemplateModeRecommendation', () => { [ true, false ].forEach( ( hasPluginIssues ) => { [ true, false ].forEach( ( hasThemeIssues ) => { [ true, false ].forEach( ( userIsTechnical ) => { - const cb = () => getTemplateModeRecommendation( { hasPluginIssues, hasThemeIssues, userIsTechnical, hasSiteScanResults: true } ); + const cb = () => getTemplateModeRecommendation( { hasPluginIssues, hasThemeIssues, userIsTechnical } ); + expect( cb ).not.toThrow(); + } ); + } ); + } ); + + [ true, false ].forEach( ( hasSiteScanResults ) => { + [ true, false ].forEach( ( currentThemeIsAmongReaderThemes ) => { + [ true, false ].forEach( ( userIsTechnical ) => { + const cb = () => getTemplateModeRecommendation( { userIsTechnical, hasSiteScanResults, currentThemeIsAmongReaderThemes } ); expect( cb ).not.toThrow(); } ); } ); From 9949c8aa20ecb36474b7f4253d65c33d09489dc0 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 28 Oct 2021 15:39:58 +0200 Subject: [PATCH 118/120] Use current user option in Wizard template mode recommendation --- .../components/use-template-mode-recommendation/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/src/components/use-template-mode-recommendation/index.js b/assets/src/components/use-template-mode-recommendation/index.js index 1addf727f5e..301b716fbe3 100644 --- a/assets/src/components/use-template-mode-recommendation/index.js +++ b/assets/src/components/use-template-mode-recommendation/index.js @@ -32,12 +32,12 @@ export function useTemplateModeRecommendation() { stale, themesWithAmpIncompatibility, } = useContext( SiteScanContext ); - const { originalDeveloperToolsOption } = useContext( User ); + const { developerToolsOption, fetchingUser, savingDeveloperToolsOption } = useContext( User ); const { fetchingOptions, savingOptions } = useContext( Options ); const [ templateModeRecommendation, setTemplateModeRecommendation ] = useState( null ); useLayoutEffect( () => { - if ( isBusy || isFetchingScannableUrls || fetchingOptions || savingOptions ) { + if ( isBusy || isFetchingScannableUrls || fetchingOptions || savingOptions || fetchingUser || savingDeveloperToolsOption ) { return; } @@ -46,9 +46,9 @@ export function useTemplateModeRecommendation() { hasPluginIssues: pluginsWithAmpIncompatibility?.length > 0, hasSiteScanResults: hasSiteScanResults && ! stale, hasThemeIssues: themesWithAmpIncompatibility?.length > 0, - userIsTechnical: originalDeveloperToolsOption === true, + userIsTechnical: developerToolsOption === true, } ) ); - }, [ currentThemeIsAmongReaderThemes, fetchingOptions, hasSiteScanResults, isBusy, isFetchingScannableUrls, originalDeveloperToolsOption, pluginsWithAmpIncompatibility?.length, savingOptions, stale, themesWithAmpIncompatibility?.length ] ); + }, [ currentThemeIsAmongReaderThemes, developerToolsOption, fetchingOptions, fetchingUser, hasSiteScanResults, isBusy, isFetchingScannableUrls, pluginsWithAmpIncompatibility?.length, savingDeveloperToolsOption, savingOptions, stale, themesWithAmpIncompatibility?.length ] ); return { templateModeRecommendation, From 88a1fd998b3bdae6fd7104a8f915d52d3d5e0ec8 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 28 Oct 2021 15:44:46 +0200 Subject: [PATCH 119/120] Add missing comma --- .../themes-context-provider/test/use-normalized-themes-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js index d9e4c72a31d..7e107ff877b 100644 --- a/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js +++ b/assets/src/components/themes-context-provider/test/use-normalized-themes-data.js @@ -99,7 +99,7 @@ describe( 'useNormalizedThemesData', () => { stylesheet: 'twentytwenty', status: 'active', version: '1.7', - } + }, ] } > From 8836b54bf240b176613ae38ff893b2003bcd7db9 Mon Sep 17 00:00:00 2001 From: Piotr Delawski Date: Thu, 28 Oct 2021 16:22:45 +0200 Subject: [PATCH 120/120] Try fixing E2E tests --- tests/e2e/specs/admin/amp-options.js | 20 ++++++++-- tests/e2e/specs/amp-onboarding/done.js | 52 -------------------------- 2 files changed, 17 insertions(+), 55 deletions(-) diff --git a/tests/e2e/specs/admin/amp-options.js b/tests/e2e/specs/admin/amp-options.js index 1da65899de1..e4ec32ae16f 100644 --- a/tests/e2e/specs/admin/amp-options.js +++ b/tests/e2e/specs/admin/amp-options.js @@ -64,10 +64,18 @@ describe( 'Settings screen when reader theme is active theme', () => { } ); describe( 'Mode info notices', () => { - it( 'show information in the Template Mode section if site scan results are stale', async () => { + const timeout = 10000; + + beforeEach( async () => { await cleanUpSettings(); await visitAdminPage( 'admin.php', 'page=amp-options' ); + } ); + afterEach( async () => { + await cleanUpSettings(); + } ); + + it( 'show information in the Template Mode section if site scan results are stale', async () => { // Trigger a site scan. await page.waitForSelector( '#site-scan' ); @@ -77,16 +85,22 @@ describe( 'Mode info notices', () => { } await scrollToElement( { selector: '#site-scan .settings-site-scan__footer .is-primary', click: true } ); - await expect( page ).toMatchElement( '#site-scan .settings-site-scan__footer .is-primary', { text: 'Rescan Site', timeout: 10000 } ); + await expect( page ).toMatchElement( '#site-scan .settings-site-scan__footer .is-primary', { text: 'Rescan Site', timeout } ); + + await scrollToElement( { selector: '#template-modes' } ); // Confirm there is no notice about stale results. const noticeXpath = '//*[@id="template-modes"]/*[contains(@class, "amp-notice--info")]/*[contains(text(), "Site Scan results are stale")]'; + const noticeBefore = await page.$x( noticeXpath ); expect( noticeBefore ).toHaveLength( 0 ); // Change template mode to make the scan results stale. await setTemplateMode( 'transitional' ); - await page.waitForSelector( '.settings-site-scan__footer .is-primary', { timeout: 10000 } ); + + await page.waitForSelector( '.settings-site-scan__footer .is-primary', { timeout } ); + + await scrollToElement( { selector: '#template-modes' } ); const noticeAfter = await page.$x( noticeXpath ); expect( noticeAfter ).toHaveLength( 1 ); diff --git a/tests/e2e/specs/amp-onboarding/done.js b/tests/e2e/specs/amp-onboarding/done.js index c80fb1d2d0d..be424688f6b 100644 --- a/tests/e2e/specs/amp-onboarding/done.js +++ b/tests/e2e/specs/amp-onboarding/done.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { trashAllPosts, visitAdminPage } from '@wordpress/e2e-test-utils'; - /** * Internal dependencies */ @@ -34,43 +29,6 @@ async function testCommonDoneStepElements() { } describe( 'Done', () => { - let testPost; - let testPage; - - beforeAll( async () => { - await visitAdminPage( 'admin.php', 'page=amp-options' ); - - testPost = await page.evaluate( () => wp.apiFetch( { - path: '/wp/v2/posts', - method: 'POST', - data: { title: 'Test Post', status: 'publish' }, - } ) ); - testPage = await page.evaluate( () => wp.apiFetch( { - path: '/wp/v2/pages', - method: 'POST', - data: { title: 'Test Page', status: 'publish' }, - } ) ); - } ); - - afterAll( async () => { - await visitAdminPage( 'admin.php', 'page=amp-options' ); - - if ( testPost?.id ) { - await page.evaluate( ( id ) => wp.apiFetch( { - path: `/wp/v2/posts/${ id }`, - method: 'DELETE', - data: { force: true }, - } ), testPost.id ); - } - if ( testPage?.id ) { - await page.evaluate( ( id ) => wp.apiFetch( { - path: `/wp/v2/pages/${ id }`, - method: 'DELETE', - data: { force: true }, - } ), testPage.id ); - } - } ); - afterEach( async () => { await cleanUpSettings(); } ); @@ -119,14 +77,4 @@ describe( 'Done', () => { await expect( page ).toMatchElement( 'p', { text: /Reader mode/i } ); await expect( page ).toMatchElement( '.done__preview-container input[type="checkbox"]' ); } ); - - it( 'does not render site preview in reader mode if there are no posts and pages', async () => { - await trashAllPosts(); - await trashAllPosts( 'page' ); - - await moveToDoneScreen( { mode: 'reader' } ); - - await expect( page ).toMatchElement( 'h1', { text: 'Done' } ); - await expect( page ).not.toMatchElement( '.done__preview-iframe' ); - } ); } );