From a001f71d00bcebd99aad3ea319423ca86445180c Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 11 Nov 2024 13:33:48 +0200 Subject: [PATCH 001/132] Add post type and taxonomy filters in the script data --- .../content-types-repository.php | 75 ++++++++++++++++++ .../taxonomies/taxonomies-repository.php | 79 +++++++++++++++++++ .../domain/content-types/content-type-map.php | 27 +++++++ .../domain/content-types/content-type.php | 52 ++++++++++++ .../domain/taxonomies/taxonomy-map.php | 44 +++++++++++ src/general/domain/taxonomies/taxonomy.php | 52 ++++++++++++ .../product-category-filter.php | 27 +++++++ .../taxonomy-filter-interface.php | 23 ++++++ .../general-page-integration.php | 42 ++++++---- 9 files changed, 406 insertions(+), 15 deletions(-) create mode 100644 src/general/application/content-types/content-types-repository.php create mode 100644 src/general/application/taxonomies/taxonomies-repository.php create mode 100644 src/general/domain/content-types/content-type-map.php create mode 100644 src/general/domain/content-types/content-type.php create mode 100644 src/general/domain/taxonomies/taxonomy-map.php create mode 100644 src/general/domain/taxonomies/taxonomy.php create mode 100644 src/general/domain/taxonomy-filters/product-category-filter.php create mode 100644 src/general/domain/taxonomy-filters/taxonomy-filter-interface.php diff --git a/src/general/application/content-types/content-types-repository.php b/src/general/application/content-types/content-types-repository.php new file mode 100644 index 00000000000..a67490fc363 --- /dev/null +++ b/src/general/application/content-types/content-types-repository.php @@ -0,0 +1,75 @@ +post_type_helper = $post_type_helper; + $this->content_type_map = $content_type_map; + $this->taxonomies_repository = $taxonomies_repository; + } + + /** + * Returns the content types object. + * + * @return array The content types object. + */ + public function get_content_types(): array { + $content_types = []; + $post_types = $this->post_type_helper->get_indexable_post_types(); + + foreach ( $post_types as $post_type ) { + $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. + $content_type_instance = new Content_Type( $post_type_object->name, $post_type_object->label ); + + $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type_instance ); + $content_types[] = $this->content_type_map->map_to_array( $content_type_instance, $content_type_taxonomy ); + } + + return $content_types; + } +} diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php new file mode 100644 index 00000000000..6f1b3992a9b --- /dev/null +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -0,0 +1,79 @@ +taxonomy_map = $taxonomy_map; + $this->taxonomy_filters = $taxonomy_filters; + } + + /** + * Returns the object of the filtering taxonomy of a content type. + * + * @param Content_Type $content_type The content type the taxonomy filters. + * + * @return array The filtering taxonomy of the content type. + */ + public function get_content_type_taxonomy( Content_Type $content_type ): array { + // @TODO: First we check if there's a filter that overrides the filtering taxonomy for this content type. + + // Then we check if we have made an explicit filter for this content type. + // @TODO: Maybe: $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters( $content_type );. + foreach ( $this->taxonomy_filters as $taxonomy_filter ) { + if ( $taxonomy_filter->get_filtered_content_type() === $content_type->get_name() ) { + $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); + if ( \is_a( $taxonomy, 'WP_Taxonomy' ) ) { + $taxonomy_instance = new Taxonomy( $taxonomy->name, $taxonomy->label ); + $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); + + return $this->taxonomy_map->map_to_array(); + } + } + } + + // As a fallback, we check if the content type has a category taxonomy and we make it the filtering taxonomy if so. + if ( \in_array( 'category', \get_object_taxonomies( $content_type->get_name() ), true ) ) { + $taxonomy_instance = new Taxonomy( 'category', \__( 'Category', 'wordpress-seo' ) ); + $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); + return $this->taxonomy_map->map_to_array(); + } + + return []; + } +} diff --git a/src/general/domain/content-types/content-type-map.php b/src/general/domain/content-types/content-type-map.php new file mode 100644 index 00000000000..de51df18c88 --- /dev/null +++ b/src/general/domain/content-types/content-type-map.php @@ -0,0 +1,27 @@ + $content_type_taxonomy The filtering taxonomy of the content type. + * + * @return array The expected key value representation. + */ + public function map_to_array( Content_Type $content_type, array $content_type_taxonomy ): array { + $content_type_array = []; + + $content_type_array['name'] = $content_type->get_name(); + $content_type_array['label'] = $content_type->get_label(); + $content_type_array['taxonomy'] = ( \count( $content_type_taxonomy ) === 0 ) ? null : $content_type_taxonomy; + + return $content_type_array; + } +} diff --git a/src/general/domain/content-types/content-type.php b/src/general/domain/content-types/content-type.php new file mode 100644 index 00000000000..e90838c8f72 --- /dev/null +++ b/src/general/domain/content-types/content-type.php @@ -0,0 +1,52 @@ +name = $name; + $this->label = $label; + } + + /** + * Gets the content type name. + * + * @return string The content type name. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Gets the content type label. + * + * @return string The content type label. + */ + public function get_label(): string { + return $this->label; + } +} diff --git a/src/general/domain/taxonomies/taxonomy-map.php b/src/general/domain/taxonomies/taxonomy-map.php new file mode 100644 index 00000000000..28d5b25331d --- /dev/null +++ b/src/general/domain/taxonomies/taxonomy-map.php @@ -0,0 +1,44 @@ +taxonomy = $taxonomy; + } + + /** + * Maps all taxonomy information to the expected key value representation. + * + * @return array The expected key value representation. + */ + public function map_to_array(): array { + if ( $this->taxonomy === null ) { + return []; + } + + $array = [ + 'name' => $this->taxonomy->get_name(), + 'label' => $this->taxonomy->get_label(), + ]; + return $array; + } +} diff --git a/src/general/domain/taxonomies/taxonomy.php b/src/general/domain/taxonomies/taxonomy.php new file mode 100644 index 00000000000..db6a0d9105d --- /dev/null +++ b/src/general/domain/taxonomies/taxonomy.php @@ -0,0 +1,52 @@ +name = $name; + $this->label = $label; + } + + /** + * Gets the taxonomy name. + * + * @return string The taxonomy name. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Gets the taxonomy label. + * + * @return string The taxonomy label. + */ + public function get_label(): string { + return $this->label; + } +} diff --git a/src/general/domain/taxonomy-filters/product-category-filter.php b/src/general/domain/taxonomy-filters/product-category-filter.php new file mode 100644 index 00000000000..a50655f7fef --- /dev/null +++ b/src/general/domain/taxonomy-filters/product-category-filter.php @@ -0,0 +1,27 @@ +asset_manager = $asset_manager; - $this->current_page_helper = $current_page_helper; - $this->product_helper = $product_helper; - $this->shortlink_helper = $shortlink_helper; - $this->notification_helper = $notification_helper; - $this->alert_dismissal_action = $alert_dismissal_action; - $this->promotion_manager = $promotion_manager; + $this->asset_manager = $asset_manager; + $this->current_page_helper = $current_page_helper; + $this->product_helper = $product_helper; + $this->shortlink_helper = $shortlink_helper; + $this->notification_helper = $notification_helper; + $this->alert_dismissal_action = $alert_dismissal_action; + $this->promotion_manager = $promotion_manager; + $this->content_types_repository = $content_types_repository; } /** @@ -202,6 +213,7 @@ private function get_script_data() { 'alerts' => $this->notification_helper->get_alerts(), 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'dismissedAlerts' => $this->alert_dismissal_action->all_dismissed(), + 'contentTypes' => $this->content_types_repository->get_content_types(), ]; } } From 488a5dda30255f52223c9f1686ceefee8a1e4e3d Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 11 Nov 2024 13:03:44 +0100 Subject: [PATCH 002/132] Setup dash route * add frame for the initial widgets * refactor routes to constant to not repeat paths --- packages/js/src/dash/components/dashboard.js | 52 +++++++++++++++++++ packages/js/src/dash/components/page-title.js | 27 ++++++++++ packages/js/src/dash/components/seo-scores.js | 39 ++++++++++++++ packages/js/src/dash/index.js | 1 + packages/js/src/general/app.js | 18 +++++-- packages/js/src/general/initialize.js | 37 ++++++++++--- packages/js/src/general/routes/index.js | 6 +++ 7 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 packages/js/src/dash/components/dashboard.js create mode 100644 packages/js/src/dash/components/page-title.js create mode 100644 packages/js/src/dash/components/seo-scores.js create mode 100644 packages/js/src/dash/index.js diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dashboard.js new file mode 100644 index 00000000000..3f3ca77af3e --- /dev/null +++ b/packages/js/src/dash/components/dashboard.js @@ -0,0 +1,52 @@ +import PropTypes from "prop-types"; +import { PageTitle } from "./page-title"; +import { SeoScores } from "./seo-scores"; + +/** + * @typedef {Object} Taxonomy A taxonomy. + * @property {string} name The unique identifier. + * @property {string} label The user-facing label. + * @property {Object} links The links. + * @property {string} [links.search] The search link, might not exist. + */ + +/** + * @typedef {Object} ContentType A content type. + * @property {string} name The unique identifier. + * @property {string} label The user-facing label. + * @property {Taxonomy|null} taxonomy The (main) taxonomy or null. + */ + +/** + * @param {ContentType[]} contentTypes The content types. + * @param {string} userName The user name. + * @returns {JSX.Element} The element. + */ +export const Dashboard = ( { contentTypes, userName } ) => { + return ( +
+ +
+ + +
+
+ ); +}; + +Dashboard.propTypes = { + contentTypes: PropTypes.arrayOf( + PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + taxonomy: PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + links: PropTypes.shape( { + search: PropTypes.string, + } ).isRequired, + } ), + } ) + ).isRequired, + userName: PropTypes.string.isRequired, +}; diff --git a/packages/js/src/dash/components/page-title.js b/packages/js/src/dash/components/page-title.js new file mode 100644 index 00000000000..bcbad90630d --- /dev/null +++ b/packages/js/src/dash/components/page-title.js @@ -0,0 +1,27 @@ +import { __, sprintf } from "@wordpress/i18n"; +import { Paper, Title } from "@yoast/ui-library"; +import PropTypes from "prop-types"; + +/** + * @param {string} userName The user name. + * @returns {JSX.Element} The element. + */ +export const PageTitle = ( { userName } ) => ( + + + + { sprintf( + __( "Hi %s!", "wordpress-seo" ), + userName + ) } + +

+ { __( "Welcome to your SEO dashboard! Don't forget to check it regularly to see how your site is performing and if there are any important tasks waiting for you.", "wordpress-seo" ) } +

+
+
+); + +PageTitle.propTypes = { + userName: PropTypes.string.isRequired, +}; diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js new file mode 100644 index 00000000000..3ccfcaad3d1 --- /dev/null +++ b/packages/js/src/dash/components/seo-scores.js @@ -0,0 +1,39 @@ +import { __ } from "@wordpress/i18n"; +import { AutocompleteField, Paper, Title } from "@yoast/ui-library"; +import PropTypes from "prop-types"; + +/** + * @returns {JSX.Element} The element. + */ +export const SeoScores = ( { contentTypes } ) => { // eslint-disable-line no-unused-vars + return ( + + { __( "SEO scores", "wordpress-seo" ) } +
+ + +
+

{ __( "description", "wordpress-seo" ) }

+
+
Scores
+
chart
+
+
+ ); +}; + +SeoScores.propTypes = { + contentTypes: PropTypes.arrayOf( + PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + taxonomy: PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + links: PropTypes.shape( { + search: PropTypes.string, + } ).isRequired, + } ), + } ) + ).isRequired, +}; diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js new file mode 100644 index 00000000000..9b89b509e88 --- /dev/null +++ b/packages/js/src/dash/index.js @@ -0,0 +1 @@ +export { Dashboard } from "./components/dashboard"; diff --git a/packages/js/src/general/app.js b/packages/js/src/general/app.js index 69ae0efea4f..ce178815884 100644 --- a/packages/js/src/general/app.js +++ b/packages/js/src/general/app.js @@ -1,7 +1,7 @@ /* eslint-disable complexity */ import { Transition } from "@headlessui/react"; -import { AdjustmentsIcon, BellIcon } from "@heroicons/react/outline"; +import { AdjustmentsIcon, BellIcon, ChartPieIcon } from "@heroicons/react/outline"; import { useDispatch, useSelect } from "@wordpress/data"; import { useCallback, useEffect, useMemo } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; @@ -16,6 +16,7 @@ import { MenuItemLink, YoastLogo } from "../shared-admin/components"; import { Notice } from "./components"; import { STORE_NAME } from "./constants"; import { useNotificationCountSync, useSelectGeneralPage } from "./hooks"; +import { ROUTES } from "./routes"; /** * @param {string} [idSuffix] Extra id suffix. Can prevent double IDs on the page. @@ -39,7 +40,16 @@ const Menu = ( { idSuffix = "" } ) => {
    + + { __( "Dashboard", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> + { __( "Alert center", "wordpress-seo" ) } @@ -48,7 +58,7 @@ const Menu = ( { idSuffix = "" } ) => { className="yst-gap-3" /> { __( "First-time configuration", "wordpress-seo" ) } @@ -118,7 +128,7 @@ const App = () => { enterFrom="yst-opacity-0" enterTo="yst-opacity-100" > - { pathname !== "/first-time-configuration" &&
    + { pathname !== ROUTES.firstTimeConfiguration &&
    { shouldShowWebinarPromotionNotificationInDashboard( STORE_NAME ) && } diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 7a5399cf1b8..071da574c17 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -5,14 +5,15 @@ import { render } from "@wordpress/element"; import { Root } from "@yoast/ui-library"; import { get } from "lodash"; import { createHashRouter, createRoutesFromElements, Navigate, Route, RouterProvider } from "react-router-dom"; +import { Dashboard } from "../dash"; import { LINK_PARAMS_NAME } from "../shared-admin/store"; -import { FTC_NAME } from "./store/first-time-configuration"; import App from "./app"; import { RouteErrorFallback } from "./components"; import { STORE_NAME } from "./constants"; -import { AlertCenter, FirstTimeConfiguration } from "./routes"; +import { AlertCenter, FirstTimeConfiguration, ROUTES } from "./routes"; import registerStore from "./store"; import { ALERT_CENTER_NAME } from "./store/alert-center"; +import { FTC_NAME } from "./store/first-time-configuration"; domReady( () => { const root = document.getElementById( "yoast-seo-general" ); @@ -31,19 +32,43 @@ domReady( () => { } ); const isRtl = select( STORE_NAME ).selectPreference( "isRtl", false ); + const contentTypes = get( window, "wpseoScriptData.dash.contentTypes", [ + { + name: "post", + label: "Posts", + taxonomy: { + name: "category", + label: "Categories", + links: { + search: "http://basic.wordpress.test/wp-json/wp/v2/categories", + }, + }, + }, + { + name: "page", + label: "Pages", + taxonomy: null, + }, + ] ); + const userName = get( window, "wpseoScriptData.dash.userName", "User" ); + const router = createHashRouter( createRoutesFromElements( } errorElement={ }> - } errorElement={ } /> - } errorElement={ } /> + } errorElement={ } + /> + } errorElement={ } /> + } errorElement={ } /> { /** - * Fallback route: redirect to the root (alert center). + * Fallback route: redirect to the dashboard. * A redirect is used to support the activePath in the menu. E.g. `pathname` matches exactly. * It replaces the current path to not introduce invalid history in the browser (that would just redirect again). */ } - } /> + } /> ) ); diff --git a/packages/js/src/general/routes/index.js b/packages/js/src/general/routes/index.js index 0816e934173..78e8320fdc5 100644 --- a/packages/js/src/general/routes/index.js +++ b/packages/js/src/general/routes/index.js @@ -1,2 +1,8 @@ export { FirstTimeConfiguration } from "./first-time-configuration"; export { AlertCenter } from "./alert-center"; + +export const ROUTES = { + dashboard: "/", + alertCenter: "/alert-center", + firstTimeConfiguration: "/first-time-configuration", +}; From d44fb28e8e89df9f53c9d9a6793a9116d34346a0 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 11 Nov 2024 15:16:59 +0200 Subject: [PATCH 003/132] Get taxonomy filters from its own repo --- .../content-types-repository.php | 2 - .../taxonomies/taxonomies-repository.php | 24 +++++------ .../taxonomy-filters-repository.php | 41 +++++++++++++++++++ 3 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 src/general/application/taxonomy-filters/taxonomy-filters-repository.php diff --git a/src/general/application/content-types/content-types-repository.php b/src/general/application/content-types/content-types-repository.php index a67490fc363..c05f8c02fcc 100644 --- a/src/general/application/content-types/content-types-repository.php +++ b/src/general/application/content-types/content-types-repository.php @@ -10,8 +10,6 @@ /** * The repository to get all content types. - * - * @makePublic */ class Content_Types_Repository { diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 6f1b3992a9b..1aff6ec301e 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -3,15 +3,13 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomies; +use Yoast\WP\SEO\General\Application\Taxonomy_Filters\Taxonomy_Filters_Repository; use Yoast\WP\SEO\General\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy_Map; -use Yoast\WP\SEO\General\Domain\Taxonomy_Filters\Taxonomy_Filter_Interface; /** * The repository to get all content types. - * - * @makePublic */ class Taxonomies_Repository { @@ -23,24 +21,24 @@ class Taxonomies_Repository { private $taxonomy_map; /** - * The taxonomy filter repository. + * The taxonomy filters repository. * - * @var Taxonomy_Filter_Interface[] + * @var Taxonomy_Filters_Repository */ - private $taxonomy_filters; + private $taxonomy_filters_repository; /** * The constructor. * - * @param Taxonomy_Map $taxonomy_map The taxonomy map. - * @param Taxonomy_Filter_Interface ...$taxonomy_filters All taxonomies. + * @param Taxonomy_Map $taxonomy_map The taxonomy map. + * @param Taxonomy_Filters_Repository $taxonomy_filters_repository The taxonomy filters repository. */ public function __construct( Taxonomy_Map $taxonomy_map, - Taxonomy_Filter_Interface ...$taxonomy_filters + Taxonomy_Filters_Repository $taxonomy_filters_repository ) { - $this->taxonomy_map = $taxonomy_map; - $this->taxonomy_filters = $taxonomy_filters; + $this->taxonomy_map = $taxonomy_map; + $this->taxonomy_filters_repository = $taxonomy_filters_repository; } /** @@ -54,8 +52,8 @@ public function get_content_type_taxonomy( Content_Type $content_type ): array { // @TODO: First we check if there's a filter that overrides the filtering taxonomy for this content type. // Then we check if we have made an explicit filter for this content type. - // @TODO: Maybe: $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters( $content_type );. - foreach ( $this->taxonomy_filters as $taxonomy_filter ) { + $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters(); + foreach ( $taxonomy_filters as $taxonomy_filter ) { if ( $taxonomy_filter->get_filtered_content_type() === $content_type->get_name() ) { $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); if ( \is_a( $taxonomy, 'WP_Taxonomy' ) ) { diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php new file mode 100644 index 00000000000..a1e07699c43 --- /dev/null +++ b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php @@ -0,0 +1,41 @@ +taxonomy_filters = $taxonomy_filters; + } + + /** + * Returns the object of the filtering taxonomy of a content type. + * + * @return Taxonomy_Filter_Interface[] The filtering taxonomy of the content type. + */ + public function get_taxonomy_filters(): array { + return $this->taxonomy_filters; + } +} From 3cdfc2dd480f7a1933397281dfc7ca804b73ba95 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:04:48 +0100 Subject: [PATCH 004/132] Add content type filter * move types to index --- .../dash/components/content-type-filter.js | 61 +++++++++++++++++++ packages/js/src/dash/components/dashboard.js | 14 +---- packages/js/src/dash/components/seo-scores.js | 18 +++++- packages/js/src/dash/index.js | 15 +++++ 4 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 packages/js/src/dash/components/content-type-filter.js diff --git a/packages/js/src/dash/components/content-type-filter.js b/packages/js/src/dash/components/content-type-filter.js new file mode 100644 index 00000000000..940edd47719 --- /dev/null +++ b/packages/js/src/dash/components/content-type-filter.js @@ -0,0 +1,61 @@ +import { useCallback, useState } from "@wordpress/element"; +import { __ } from "@wordpress/i18n"; +import { AutocompleteField } from "@yoast/ui-library"; +import PropTypes from "prop-types"; + +/** + * @typedef {import("./dashboard").ContentType} ContentType + */ + +/** + * @param {string} idSuffix The suffix for the ID. + * @param {ContentType[]} contentTypes The content types. + * @param {ContentType?} selected The selected content type. + * @param {function(ContentType?)} onChange The callback. Expects it changes the `selected` prop. + * @returns {JSX.Element} The element. + */ +export const ContentTypeFilter = ( { idSuffix, contentTypes, selected, onChange } ) => { + const [ filtered, setFiltered ] = useState( () => contentTypes ); + + const handleChange = useCallback( ( value ) => { + onChange( contentTypes.find( ( { name } ) => name === value ) ); + }, [ contentTypes ] ); + const handleQueryChange = useCallback( ( event ) => { + const query = event.target.value.trim().toLowerCase(); + setFiltered( query + ? contentTypes.filter( ( { name, label } ) => label.toLowerCase().includes( query ) || name.toLowerCase().includes( query ) ) + : contentTypes ); + }, [ contentTypes ] ); + + return ( + + { filtered.map( ( { name, label } ) => ( + + { label } + + ) ) } + + ); +}; + +ContentTypeFilter.propTypes = { + idSuffix: PropTypes.string.isRequired, + contentTypes: PropTypes.arrayOf( + PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + } ) + ).isRequired, + selected: PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + } ), + onChange: PropTypes.func.isRequired, +}; diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dashboard.js index 3f3ca77af3e..379f126cce4 100644 --- a/packages/js/src/dash/components/dashboard.js +++ b/packages/js/src/dash/components/dashboard.js @@ -3,18 +3,8 @@ import { PageTitle } from "./page-title"; import { SeoScores } from "./seo-scores"; /** - * @typedef {Object} Taxonomy A taxonomy. - * @property {string} name The unique identifier. - * @property {string} label The user-facing label. - * @property {Object} links The links. - * @property {string} [links.search] The search link, might not exist. - */ - -/** - * @typedef {Object} ContentType A content type. - * @property {string} name The unique identifier. - * @property {string} label The user-facing label. - * @property {Taxonomy|null} taxonomy The (main) taxonomy or null. + * @typedef {import("./dashboard").ContentType} ContentType + * @typedef {import("./dashboard").Taxonomy} Taxonomy */ /** diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index 3ccfcaad3d1..429635b9cba 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -1,16 +1,30 @@ +import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Paper, Title } from "@yoast/ui-library"; import PropTypes from "prop-types"; +import { ContentTypeFilter } from "./content-type-filter"; + +/** + * @typedef {import("./dashboard").ContentType} ContentType + * @typedef {import("./dashboard").Taxonomy} Taxonomy + */ /** * @returns {JSX.Element} The element. */ -export const SeoScores = ( { contentTypes } ) => { // eslint-disable-line no-unused-vars +export const SeoScores = ( { contentTypes } ) => { + const [ selectedContentType, setSelectedContentType ] = useState( () => contentTypes[ 0 ] ); + return ( { __( "SEO scores", "wordpress-seo" ) }
    - +

    { __( "description", "wordpress-seo" ) }

    diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js index 9b89b509e88..dfd8803248c 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dash/index.js @@ -1 +1,16 @@ export { Dashboard } from "./components/dashboard"; + +/** + * @typedef {Object} Taxonomy A taxonomy. + * @property {string} name The unique identifier. + * @property {string} label The user-facing label. + * @property {Object} links The links. + * @property {string} [links.search] The search link, might not exist. + */ + +/** + * @typedef {Object} ContentType A content type. + * @property {string} name The unique identifier. + * @property {string} label The user-facing label. + * @property {Taxonomy|null} taxonomy The (main) taxonomy or null. + */ From 7700c7c42e74fbf540ec1bba35024f4013bbbd0a Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 11 Nov 2024 15:18:43 +0100 Subject: [PATCH 005/132] Add check for taxonomy. --- packages/js/src/dash/components/seo-scores.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index 429635b9cba..2f1f994d3b3 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -25,7 +25,9 @@ export const SeoScores = ( { contentTypes } ) => { selected={ selectedContentType } onChange={ setSelectedContentType } /> - + { selectedContentType.taxonomy && + + }

    { __( "description", "wordpress-seo" ) }

    From 42a0a686ebd29067a3e016c1bd5785593c3c360e Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 11 Nov 2024 17:05:13 +0200 Subject: [PATCH 006/132] Add a way for users to enable filtering taxonomies for CPTs via filter --- .../taxonomies/taxonomies-repository.php | 69 ++++++++++++++++--- .../domain/taxonomies/taxonomy-map.php | 2 +- 2 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 1aff6ec301e..0cabbcb0610 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -49,29 +49,78 @@ public function __construct( * @return array The filtering taxonomy of the content type. */ public function get_content_type_taxonomy( Content_Type $content_type ): array { - // @TODO: First we check if there's a filter that overrides the filtering taxonomy for this content type. + $content_type_name = $content_type->get_name(); + + // First we check if there's a filter that overrides the filtering taxonomy for this content type. + + /** + * Filter: 'wpseo_{$content_type_name}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. + * + * @param string $filtering_taxonomy The taxonomy that filters the content type. + */ + $filtering_taxonomy = \apply_filters( "wpseo_{$content_type_name}_filtering_taxonomy", '' ); + if ( $filtering_taxonomy !== '' ) { + $taxonomy = \get_taxonomy( $filtering_taxonomy ); + + if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { + return $this->get_taxonomy_map( $taxonomy->name, $taxonomy->label ); + } + + \_doing_it_wrong( + 'Filter: \'wpseo_{$content_type}_filtering_taxonomy\'', + 'The `wpseo_{$content_type}_filtering_taxonomy` filter should return a public taxonomy, available in REST API, that is associated with that content type.', + 'YoastSEO v24.1' + ); + } // Then we check if we have made an explicit filter for this content type. $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters(); foreach ( $taxonomy_filters as $taxonomy_filter ) { - if ( $taxonomy_filter->get_filtered_content_type() === $content_type->get_name() ) { + if ( $taxonomy_filter->get_filtered_content_type() === $content_type_name ) { $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); - if ( \is_a( $taxonomy, 'WP_Taxonomy' ) ) { - $taxonomy_instance = new Taxonomy( $taxonomy->name, $taxonomy->label ); - $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); - return $this->taxonomy_map->map_to_array(); + if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { + return $this->get_taxonomy_map( $taxonomy->name, $taxonomy->label ); } } } // As a fallback, we check if the content type has a category taxonomy and we make it the filtering taxonomy if so. - if ( \in_array( 'category', \get_object_taxonomies( $content_type->get_name() ), true ) ) { - $taxonomy_instance = new Taxonomy( 'category', \__( 'Category', 'wordpress-seo' ) ); - $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); - return $this->taxonomy_map->map_to_array(); + $taxonomy = \get_taxonomy( 'category' ); + + if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { + return $this->get_taxonomy_map( 'category', \__( 'Category', 'wordpress-seo' ) ); } return []; } + + /** + * Returns the map of the filtering taxonomy. + * + * @param string $taxonomy_name The name of the taxonomy. + * @param string $taxonomy_label The label of the taxonomy. + * + * @return array The filtering taxonomy of the content type. + */ + private function get_taxonomy_map( string $taxonomy_name, string $taxonomy_label ): array { + $taxonomy_instance = new Taxonomy( $taxonomy_name, $taxonomy_label ); + $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); + return $this->taxonomy_map->map_to_array(); + } + + /** + * Returns whether the taxonomy in question is valid and associated with a given content type. + * + * @param WP_Taxonomy|false $taxonomy The taxonomy to check. + * @param string $content_type The name of the content type to check. + * + * @return bool Whether the taxonomy in question is valid. + */ + private function is_taxonomy_valid( $taxonomy, $content_type ): bool { + return \is_a( $taxonomy, 'WP_Taxonomy' ) + && $taxonomy->public + && $taxonomy->show_in_rest + && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); + } } diff --git a/src/general/domain/taxonomies/taxonomy-map.php b/src/general/domain/taxonomies/taxonomy-map.php index 28d5b25331d..4463fac3711 100644 --- a/src/general/domain/taxonomies/taxonomy-map.php +++ b/src/general/domain/taxonomies/taxonomy-map.php @@ -3,7 +3,7 @@ namespace Yoast\WP\SEO\General\Domain\Taxonomies; /** - * This class describes a map of taxonomies. + * This class describes a map of a taxonomy. */ class Taxonomy_Map { From 67d02ddc1fdf9a37e3938ddbea2123aef4c11381 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 12 Nov 2024 10:03:06 +0200 Subject: [PATCH 007/132] Simplify classes --- .../content-types-repository.php | 17 ++----- .../taxonomies/taxonomies-repository.php | 48 +++++++------------ .../domain/content-types/content-type-map.php | 27 ----------- .../domain/content-types/content-type.php | 46 +++++++----------- .../domain/taxonomies/taxonomy-map.php | 44 ----------------- src/general/domain/taxonomies/taxonomy.php | 45 +++++++---------- 6 files changed, 53 insertions(+), 174 deletions(-) delete mode 100644 src/general/domain/content-types/content-type-map.php delete mode 100644 src/general/domain/taxonomies/taxonomy-map.php diff --git a/src/general/application/content-types/content-types-repository.php b/src/general/application/content-types/content-types-repository.php index c05f8c02fcc..073a1829788 100644 --- a/src/general/application/content-types/content-types-repository.php +++ b/src/general/application/content-types/content-types-repository.php @@ -5,7 +5,6 @@ use Yoast\WP\SEO\General\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\General\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\General\Domain\Content_Types\Content_Type_Map; use Yoast\WP\SEO\Helpers\Post_Type_Helper; /** @@ -20,13 +19,6 @@ class Content_Types_Repository { */ protected $post_type_helper; - /** - * The map of content types. - * - * @var Content_Type_Map - */ - private $content_type_map; - /** * The taxonomies repository. * @@ -38,16 +30,13 @@ class Content_Types_Repository { * The constructor. * * @param Post_Type_Helper $post_type_helper The post type helper. - * @param Content_Type_Map $content_type_map The map of content types. * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. */ public function __construct( Post_Type_Helper $post_type_helper, - Content_Type_Map $content_type_map, Taxonomies_Repository $taxonomies_repository ) { $this->post_type_helper = $post_type_helper; - $this->content_type_map = $content_type_map; $this->taxonomies_repository = $taxonomies_repository; } @@ -62,10 +51,10 @@ public function get_content_types(): array { foreach ( $post_types as $post_type ) { $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. - $content_type_instance = new Content_Type( $post_type_object->name, $post_type_object->label ); + $content_type_instance = new Content_Type( $post_type_object ); - $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type_instance ); - $content_types[] = $this->content_type_map->map_to_array( $content_type_instance, $content_type_taxonomy ); + $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $post_type_object->name ); + $content_types[] = $content_type_instance->map_to_array( $content_type_taxonomy ); } return $content_types; diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 0cabbcb0610..ce72e2fd7eb 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -3,23 +3,15 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomies; +use WP_Taxonomy; use Yoast\WP\SEO\General\Application\Taxonomy_Filters\Taxonomy_Filters_Repository; -use Yoast\WP\SEO\General\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy_Map; /** * The repository to get all content types. */ class Taxonomies_Repository { - /** - * The map of the taxonomy. - * - * @var Taxonomy_Map - */ - private $taxonomy_map; - /** * The taxonomy filters repository. * @@ -30,40 +22,35 @@ class Taxonomies_Repository { /** * The constructor. * - * @param Taxonomy_Map $taxonomy_map The taxonomy map. * @param Taxonomy_Filters_Repository $taxonomy_filters_repository The taxonomy filters repository. */ public function __construct( - Taxonomy_Map $taxonomy_map, Taxonomy_Filters_Repository $taxonomy_filters_repository ) { - $this->taxonomy_map = $taxonomy_map; $this->taxonomy_filters_repository = $taxonomy_filters_repository; } /** * Returns the object of the filtering taxonomy of a content type. * - * @param Content_Type $content_type The content type the taxonomy filters. + * @param string $content_type The content type the taxonomy filters. * * @return array The filtering taxonomy of the content type. */ - public function get_content_type_taxonomy( Content_Type $content_type ): array { - $content_type_name = $content_type->get_name(); - + public function get_content_type_taxonomy( string $content_type ): array { // First we check if there's a filter that overrides the filtering taxonomy for this content type. /** - * Filter: 'wpseo_{$content_type_name}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. + * Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. * * @param string $filtering_taxonomy The taxonomy that filters the content type. */ - $filtering_taxonomy = \apply_filters( "wpseo_{$content_type_name}_filtering_taxonomy", '' ); + $filtering_taxonomy = \apply_filters( "wpseo_{$content_type}_filtering_taxonomy", '' ); if ( $filtering_taxonomy !== '' ) { $taxonomy = \get_taxonomy( $filtering_taxonomy ); - if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { - return $this->get_taxonomy_map( $taxonomy->name, $taxonomy->label ); + if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + return $this->get_taxonomy_map( $taxonomy ); } \_doing_it_wrong( @@ -76,20 +63,19 @@ public function get_content_type_taxonomy( Content_Type $content_type ): array { // Then we check if we have made an explicit filter for this content type. $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters(); foreach ( $taxonomy_filters as $taxonomy_filter ) { - if ( $taxonomy_filter->get_filtered_content_type() === $content_type_name ) { + if ( $taxonomy_filter->get_filtered_content_type() === $content_type ) { $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); - if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { - return $this->get_taxonomy_map( $taxonomy->name, $taxonomy->label ); + if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + return $this->get_taxonomy_map( $taxonomy ); } } } // As a fallback, we check if the content type has a category taxonomy and we make it the filtering taxonomy if so. $taxonomy = \get_taxonomy( 'category' ); - - if ( $this->is_taxonomy_valid( $taxonomy, $content_type_name ) ) { - return $this->get_taxonomy_map( 'category', \__( 'Category', 'wordpress-seo' ) ); + if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + return $this->get_taxonomy_map( $taxonomy ); } return []; @@ -98,15 +84,13 @@ public function get_content_type_taxonomy( Content_Type $content_type ): array { /** * Returns the map of the filtering taxonomy. * - * @param string $taxonomy_name The name of the taxonomy. - * @param string $taxonomy_label The label of the taxonomy. + * @param WP_Taxonomy $taxonomy The taxonomy. * * @return array The filtering taxonomy of the content type. */ - private function get_taxonomy_map( string $taxonomy_name, string $taxonomy_label ): array { - $taxonomy_instance = new Taxonomy( $taxonomy_name, $taxonomy_label ); - $this->taxonomy_map->add_taxonomy( $taxonomy_instance ); - return $this->taxonomy_map->map_to_array(); + private function get_taxonomy_map( WP_Taxonomy $taxonomy ): array { + $taxonomy_instance = new Taxonomy( $taxonomy ); + return $taxonomy_instance->map_to_array(); } /** diff --git a/src/general/domain/content-types/content-type-map.php b/src/general/domain/content-types/content-type-map.php deleted file mode 100644 index de51df18c88..00000000000 --- a/src/general/domain/content-types/content-type-map.php +++ /dev/null @@ -1,27 +0,0 @@ - $content_type_taxonomy The filtering taxonomy of the content type. - * - * @return array The expected key value representation. - */ - public function map_to_array( Content_Type $content_type, array $content_type_taxonomy ): array { - $content_type_array = []; - - $content_type_array['name'] = $content_type->get_name(); - $content_type_array['label'] = $content_type->get_label(); - $content_type_array['taxonomy'] = ( \count( $content_type_taxonomy ) === 0 ) ? null : $content_type_taxonomy; - - return $content_type_array; - } -} diff --git a/src/general/domain/content-types/content-type.php b/src/general/domain/content-types/content-type.php index e90838c8f72..defefa93966 100644 --- a/src/general/domain/content-types/content-type.php +++ b/src/general/domain/content-types/content-type.php @@ -2,51 +2,41 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\General\Domain\Content_Types; +use WP_Post_Type; + /** - * This class describes a single Content Type. + * This class describes a Content Type. */ class Content_Type { /** - * The name of the content type. - * - * @var string - */ - private $name; - - /** - * The label of the content type. + * The content type. * - * @var string + * @var WP_Post_Type */ - private $label; + private $content_type; /** * The constructor. * - * @param string $name The name of the content type. - * @param string $label The label of the content type. + * @param WP_Post_Type $content_type The the content type. */ - public function __construct( string $name, string $label ) { - $this->name = $name; - $this->label = $label; + public function __construct( WP_Post_Type $content_type ) { + $this->content_type = $content_type; } /** - * Gets the content type name. + * Maps all content type information to the expected key value representation. * - * @return string The content type name. - */ - public function get_name(): string { - return $this->name; - } - - /** - * Gets the content type label. + * @param array $content_type_taxonomy The filtering taxonomy of the content type. * - * @return string The content type label. + * @return array The expected key value representation. */ - public function get_label(): string { - return $this->label; + public function map_to_array( array $content_type_taxonomy ): array { + return [ + 'name' => $this->content_type->name, + 'label' => $this->content_type->label, + 'taxonomy' => ( \count( $content_type_taxonomy ) === 0 ) ? null : $content_type_taxonomy, + ]; } } diff --git a/src/general/domain/taxonomies/taxonomy-map.php b/src/general/domain/taxonomies/taxonomy-map.php deleted file mode 100644 index 4463fac3711..00000000000 --- a/src/general/domain/taxonomies/taxonomy-map.php +++ /dev/null @@ -1,44 +0,0 @@ -taxonomy = $taxonomy; - } - - /** - * Maps all taxonomy information to the expected key value representation. - * - * @return array The expected key value representation. - */ - public function map_to_array(): array { - if ( $this->taxonomy === null ) { - return []; - } - - $array = [ - 'name' => $this->taxonomy->get_name(), - 'label' => $this->taxonomy->get_label(), - ]; - return $array; - } -} diff --git a/src/general/domain/taxonomies/taxonomy.php b/src/general/domain/taxonomies/taxonomy.php index db6a0d9105d..d17a27d28af 100644 --- a/src/general/domain/taxonomies/taxonomy.php +++ b/src/general/domain/taxonomies/taxonomy.php @@ -2,51 +2,38 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\General\Domain\Taxonomies; +use WP_Taxonomy; + /** - * This class describes a single Taxonomy. + * This class describes a Taxonomy. */ class Taxonomy { /** - * The name of the taxonomy. - * - * @var bool - */ - private $name; - - /** - * The label of the taxonomy. + * The taxonomy. * - * @var string + * @var WP_Taxonomy */ - private $label; + private $taxonomy; /** * The constructor. * - * @param string $name The name of the taxonomy. - * @param string $label The label of the taxonomy. - */ - public function __construct( string $name, string $label ) { - $this->name = $name; - $this->label = $label; - } - - /** - * Gets the taxonomy name. - * - * @return string The taxonomy name. + * @param WP_Taxonomy $taxonomy The taxonomy. */ - public function get_name(): string { - return $this->name; + public function __construct( WP_Taxonomy $taxonomy ) { + $this->taxonomy = $taxonomy; } /** - * Gets the taxonomy label. + * Maps all taxonomy information to the expected key value representation. * - * @return string The taxonomy label. + * @return array The expected key value representation. */ - public function get_label(): string { - return $this->label; + public function map_to_array(): array { + return [ + 'name' => $this->taxonomy->name, + 'label' => $this->taxonomy->label, + ]; } } From 88d4712a22f6e16bb8a248413cdd9f32c5540945 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 12 Nov 2024 10:37:27 +0200 Subject: [PATCH 008/132] Get explicit taxonomy filters via the respective repository --- .../taxonomies/taxonomies-repository.php | 13 +++---------- .../taxonomy-filters-repository.php | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index ce72e2fd7eb..6710191d7f0 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -39,7 +39,6 @@ public function __construct( */ public function get_content_type_taxonomy( string $content_type ): array { // First we check if there's a filter that overrides the filtering taxonomy for this content type. - /** * Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. * @@ -61,15 +60,9 @@ public function get_content_type_taxonomy( string $content_type ): array { } // Then we check if we have made an explicit filter for this content type. - $taxonomy_filters = $this->taxonomy_filters_repository->get_taxonomy_filters(); - foreach ( $taxonomy_filters as $taxonomy_filter ) { - if ( $taxonomy_filter->get_filtered_content_type() === $content_type ) { - $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); - - if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { - return $this->get_taxonomy_map( $taxonomy ); - } - } + $taxonomy = $this->taxonomy_filters_repository->get_taxonomy_filter( $content_type ); + if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + return $this->get_taxonomy_map( $taxonomy ); } // As a fallback, we check if the content type has a category taxonomy and we make it the filtering taxonomy if so. diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php index a1e07699c43..70778f40f2b 100644 --- a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php +++ b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php @@ -3,6 +3,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomy_Filters; +use WP_Taxonomy; use Yoast\WP\SEO\General\Domain\Taxonomy_Filters\Taxonomy_Filter_Interface; /** @@ -31,11 +32,21 @@ public function __construct( } /** - * Returns the object of the filtering taxonomy of a content type. + * Returns a taxonomy filter based on a content type. * - * @return Taxonomy_Filter_Interface[] The filtering taxonomy of the content type. + * @param string $content_type The content type. + * + * @return WP_Taxonomy|false The taxonomy filter. */ - public function get_taxonomy_filters(): array { - return $this->taxonomy_filters; + public function get_taxonomy_filter( string $content_type ) { + foreach ( $this->taxonomy_filters as $taxonomy_filter ) { + if ( $taxonomy_filter->get_filtered_content_type() === $content_type ) { + $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); + + return $taxonomy; + } + } + + return false; } } From 47ed233b2695f34477f74d64af99a5860ceb6a73 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 12 Nov 2024 11:41:12 +0200 Subject: [PATCH 009/132] Add taxonomy links in the script data --- src/general/domain/taxonomies/taxonomy.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/general/domain/taxonomies/taxonomy.php b/src/general/domain/taxonomies/taxonomy.php index d17a27d28af..ec1bf5d260e 100644 --- a/src/general/domain/taxonomies/taxonomy.php +++ b/src/general/domain/taxonomies/taxonomy.php @@ -34,6 +34,22 @@ public function map_to_array(): array { return [ 'name' => $this->taxonomy->name, 'label' => $this->taxonomy->label, + 'links' => [ + 'search' => $this->build_rest_url(), + ], ]; } + + /** + * Builds the REST API URL for the taxonomy. + * + * @return string The REST API URL for the taxonomy. + */ + protected function build_rest_url(): string { + $rest_base = ( $this->taxonomy->rest_base ) ? $this->taxonomy->rest_base : $this->taxonomy->name; + + $rest_namespace = ( $this->taxonomy->rest_namespace ) ? $this->taxonomy->rest_namespace : 'wp/v2'; + + return \rest_url( "{$rest_namespace}/{$rest_base}" ); + } } From 912e3a4eb17f9b241d6ea28693a077c8a29f30c3 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:25:07 +0100 Subject: [PATCH 010/132] Add term filter --- packages/js/src/dash/components/dashboard.js | 4 +- packages/js/src/dash/components/seo-scores.js | 18 ++- .../js/src/dash/components/term-filter.js | 129 ++++++++++++++++++ packages/js/src/dash/index.js | 6 + packages/js/src/general/initialize.js | 2 +- .../js/src/shared-admin/constants/index.js | 2 + 6 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 packages/js/src/dash/components/term-filter.js diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dashboard.js index 379f126cce4..fa8b26764d6 100644 --- a/packages/js/src/dash/components/dashboard.js +++ b/packages/js/src/dash/components/dashboard.js @@ -3,8 +3,8 @@ import { PageTitle } from "./page-title"; import { SeoScores } from "./seo-scores"; /** - * @typedef {import("./dashboard").ContentType} ContentType - * @typedef {import("./dashboard").Taxonomy} Taxonomy + * @typedef {import("../index").ContentType} ContentType + * @typedef {import("../index").Taxonomy} Taxonomy */ /** diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index 2f1f994d3b3..fdef7ad4c9d 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -1,8 +1,9 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; -import { AutocompleteField, Paper, Title } from "@yoast/ui-library"; +import { Paper, Title } from "@yoast/ui-library"; import PropTypes from "prop-types"; import { ContentTypeFilter } from "./content-type-filter"; +import { TermFilter } from "./term-filter"; /** * @typedef {import("./dashboard").ContentType} ContentType @@ -10,10 +11,12 @@ import { ContentTypeFilter } from "./content-type-filter"; */ /** + * @param {ContentType[]} contentTypes The content types. May not be empty. * @returns {JSX.Element} The element. */ export const SeoScores = ( { contentTypes } ) => { - const [ selectedContentType, setSelectedContentType ] = useState( () => contentTypes[ 0 ] ); + const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); + const [ selectedTerm, setSelectedTerm ] = useState(); return ( @@ -25,8 +28,13 @@ export const SeoScores = ( { contentTypes } ) => { selected={ selectedContentType } onChange={ setSelectedContentType } /> - { selectedContentType.taxonomy && - + { selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search && + }

    { __( "description", "wordpress-seo" ) }

    @@ -48,7 +56,7 @@ SeoScores.propTypes = { label: PropTypes.string.isRequired, links: PropTypes.shape( { search: PropTypes.string, - } ).isRequired, + } ), } ), } ) ).isRequired, diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js new file mode 100644 index 00000000000..734cbed2dd4 --- /dev/null +++ b/packages/js/src/dash/components/term-filter.js @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; +import { __ } from "@wordpress/i18n"; +import { AutocompleteField } from "@yoast/ui-library"; +import { debounce } from "lodash"; +import PropTypes from "prop-types"; +import { FETCH_DELAY } from "../../shared-admin/constants"; + +/** + * @typedef {import("../index").Taxonomy} Taxonomy + * @typedef {import("../index").Term} Term + */ + +/** + * @param {string} url The URL to fetch from. + * @param {string} query The query to search for. + * @param {AbortSignal} signal The signal to abort the request. + * @returns {Promise} The promise of terms, or an error. + */ +const searchTerms = async( { url, query, signal } ) => { + try { + const urlInstance = new URL( "?" + new URLSearchParams( { + search: query, + _fields: [ "name", "slug" ], + } ), url ); + const response = await fetch( urlInstance, { + headers: { + "Content-Type": "application/json", + }, + signal, + } ); + + if ( ! response.ok ) { + throw new Error( "Failed to search terms" ); + } + return response.json(); + } catch ( e ) { + throw e; + } +}; + +/** + * @param {{name: string, slug: string}} term The term from the response. + * @returns {Term} The transformed term. + */ +const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); + +/** + * @param {string} idSuffix The suffix for the ID. + * @param {Taxonomy} taxonomy The taxonomy. + * @param {Term?} selected The selected term. + * @param {function(ContentType?)} onChange The callback. Expects it changes the `selected` prop. + * @returns {JSX.Element} The element. + */ +export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { + const [ error, setError ] = useState( null ); + const [ query, setQuery ] = useState( "" ); + const [ terms, setTerms ] = useState( [] ); + /** @type {MutableRefObject} */ + const controller = useRef(); + + useEffect( () => { + controller.current?.abort(); + controller.current = new AbortController(); + + searchTerms( { url: taxonomy.links.search, query, signal: controller.current.signal } ) + .then( ( result ) => { + setTerms( result.map( transformTerm ) ); + setError( null ); + } ) + .catch( ( e ) => { + // Ignore abort errors, because they are expected. + if ( e?.name !== "AbortError" ) { + setError( { + variant: "error", + message: __( "Something went wrong", "wordpress-seo" ), + } ); + } + } ); + + return () => { + controller.current?.abort(); + }; + }, [ taxonomy.links.search, query ] ); + + const handleChange = useCallback( ( value ) => { + onChange( terms.find( ( { name } ) => name === value ) ); + }, [ terms ] ); + const handleQueryChange = useCallback( debounce( ( event ) => { + setQuery( event?.target?.value?.trim()?.toLowerCase() || "" ); + }, FETCH_DELAY ), [] ); + + return ( + + { terms + ? terms.map( ( { name, label } ) => ( + + { label } + + ) ) + : __( "None found", "wordpress-seo" ) + } + + ); +}; + +TermFilter.propTypes = { + idSuffix: PropTypes.string.isRequired, + taxonomy: PropTypes.shape( { + label: PropTypes.string.isRequired, + links: PropTypes.shape( { + search: PropTypes.string.isRequired, + } ).isRequired, + } ).isRequired, + selected: PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + } ), + onChange: PropTypes.func.isRequired, +}; diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js index dfd8803248c..6c5a50a4f38 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dash/index.js @@ -14,3 +14,9 @@ export { Dashboard } from "./components/dashboard"; * @property {string} label The user-facing label. * @property {Taxonomy|null} taxonomy The (main) taxonomy or null. */ + +/** + * @typedef {Object} Term A term. + * @property {string} name The unique identifier. + * @property {string} label The user-facing label. + */ diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 071da574c17..6d5ce993840 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -40,7 +40,7 @@ domReady( () => { name: "category", label: "Categories", links: { - search: "http://basic.wordpress.test/wp-json/wp/v2/categories", + search: "https://igor.local/wp-json/wp/v2/categories", }, }, }, diff --git a/packages/js/src/shared-admin/constants/index.js b/packages/js/src/shared-admin/constants/index.js index 2d2d680b7da..c960bf4c063 100644 --- a/packages/js/src/shared-admin/constants/index.js +++ b/packages/js/src/shared-admin/constants/index.js @@ -19,3 +19,5 @@ export const VIDEO_FLOW = { askPermission: "askPermission", isPlaying: "isPlaying", }; + +export const FETCH_DELAY = 200; From 895b9b1b1d2de8fb846c031e0443d1a4aec62ddb Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:04:09 +0100 Subject: [PATCH 011/132] Codescout: use constant Instead of magic number --- .../js/src/settings/components/formik-page-select-field.js | 4 ++-- .../js/src/settings/components/formik-user-select-field.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/js/src/settings/components/formik-page-select-field.js b/packages/js/src/settings/components/formik-page-select-field.js index 4e793221587..a7382dcb8bf 100644 --- a/packages/js/src/settings/components/formik-page-select-field.js +++ b/packages/js/src/settings/components/formik-page-select-field.js @@ -7,7 +7,7 @@ import classNames from "classnames"; import { useField } from "formik"; import { debounce, find, isEmpty, map, values } from "lodash"; import PropTypes from "prop-types"; -import { ASYNC_ACTION_STATUS } from "../../shared-admin/constants"; +import { ASYNC_ACTION_STATUS, FETCH_DELAY } from "../../shared-admin/constants"; import { useDispatchSettings, useSelectSettings } from "../hooks"; /** @@ -63,7 +63,7 @@ const FormikPageSelectField = ( { name, id, ...props } ) => { setQueriedPageIds( [] ); setStatus( ASYNC_ACTION_STATUS.error ); } - }, 200 ), [ setQueriedPageIds, setStatus, fetchPages ] ); + }, FETCH_DELAY ), [ setQueriedPageIds, setStatus, fetchPages ] ); const handleChange = useCallback( newValue => { setTouched( true, false ); diff --git a/packages/js/src/settings/components/formik-user-select-field.js b/packages/js/src/settings/components/formik-user-select-field.js index db84b6ea6f3..38df17a0e85 100644 --- a/packages/js/src/settings/components/formik-user-select-field.js +++ b/packages/js/src/settings/components/formik-user-select-field.js @@ -9,7 +9,7 @@ import classNames from "classnames"; import { useField } from "formik"; import { debounce, find, isEmpty, map, values } from "lodash"; import PropTypes from "prop-types"; -import { ASYNC_ACTION_STATUS } from "../../shared-admin/constants"; +import { ASYNC_ACTION_STATUS, FETCH_DELAY } from "../../shared-admin/constants"; import { useDispatchSettings, useSelectSettings } from "../hooks"; let abortController; @@ -80,7 +80,7 @@ const FormikUserSelectField = ( { name, id, className = "", ...props } ) => { console.error( error.message ); } - }, 200 ), [ setQueriedUserIds, addManyUsers, setStatus ] ); + }, FETCH_DELAY ), [ setQueriedUserIds, addManyUsers, setStatus ] ); const handleChange = useCallback( newValue => { setTouched( true, false ); From a9fbb070641a7e3a61c22778721be2e3e27835fe Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 12 Nov 2024 12:41:01 +0200 Subject: [PATCH 012/132] Clean up --- .../content-types/content-types-repository.php | 6 +++--- .../application/taxonomies/taxonomies-repository.php | 10 +++++----- .../taxonomy-filters/taxonomy-filters-repository.php | 8 +++----- src/general/domain/content-types/content-type.php | 6 +++--- src/general/domain/taxonomies/taxonomy.php | 2 +- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/general/application/content-types/content-types-repository.php b/src/general/application/content-types/content-types-repository.php index 073a1829788..e45a5c35029 100644 --- a/src/general/application/content-types/content-types-repository.php +++ b/src/general/application/content-types/content-types-repository.php @@ -8,7 +8,7 @@ use Yoast\WP\SEO\Helpers\Post_Type_Helper; /** - * The repository to get all content types. + * The repository to get content types. */ class Content_Types_Repository { @@ -43,14 +43,14 @@ public function __construct( /** * Returns the content types object. * - * @return array The content types object. + * @return array>|null> The content types object. */ public function get_content_types(): array { $content_types = []; $post_types = $this->post_type_helper->get_indexable_post_types(); foreach ( $post_types as $post_type ) { - $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. + $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. $content_type_instance = new Content_Type( $post_type_object ); $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $post_type_object->name ); diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 6710191d7f0..ffa857383d4 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -8,7 +8,7 @@ use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; /** - * The repository to get all content types. + * The repository to get taxonomies. */ class Taxonomies_Repository { @@ -35,7 +35,7 @@ public function __construct( * * @param string $content_type The content type the taxonomy filters. * - * @return array The filtering taxonomy of the content type. + * @return array> The filtering taxonomy of the content type. */ public function get_content_type_taxonomy( string $content_type ): array { // First we check if there's a filter that overrides the filtering taxonomy for this content type. @@ -59,13 +59,13 @@ public function get_content_type_taxonomy( string $content_type ): array { ); } - // Then we check if we have made an explicit filter for this content type. + // Then we check if there is a filter explicitly made for this content type. $taxonomy = $this->taxonomy_filters_repository->get_taxonomy_filter( $content_type ); if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { return $this->get_taxonomy_map( $taxonomy ); } - // As a fallback, we check if the content type has a category taxonomy and we make it the filtering taxonomy if so. + // As a fallback, we check if the content type has a category taxonomy. $taxonomy = \get_taxonomy( 'category' ); if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { return $this->get_taxonomy_map( $taxonomy ); @@ -79,7 +79,7 @@ public function get_content_type_taxonomy( string $content_type ): array { * * @param WP_Taxonomy $taxonomy The taxonomy. * - * @return array The filtering taxonomy of the content type. + * @return array> The map of the filtering taxonomy. */ private function get_taxonomy_map( WP_Taxonomy $taxonomy ): array { $taxonomy_instance = new Taxonomy( $taxonomy ); diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php index 70778f40f2b..5121bab5652 100644 --- a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php +++ b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php @@ -7,14 +7,12 @@ use Yoast\WP\SEO\General\Domain\Taxonomy_Filters\Taxonomy_Filter_Interface; /** - * The repository to get all content types. - * - * @makePublic + * The repository to get taxonomy filters. */ class Taxonomy_Filters_Repository { /** - * The taxonomy filter repository. + * All taxonomy filters. * * @var Taxonomy_Filter_Interface[] */ @@ -23,7 +21,7 @@ class Taxonomy_Filters_Repository { /** * The constructor. * - * @param Taxonomy_Filter_Interface ...$taxonomy_filters All taxonomies. + * @param Taxonomy_Filter_Interface ...$taxonomy_filters All taxonomy filters. */ public function __construct( Taxonomy_Filter_Interface ...$taxonomy_filters diff --git a/src/general/domain/content-types/content-type.php b/src/general/domain/content-types/content-type.php index defefa93966..a0d7fa03b40 100644 --- a/src/general/domain/content-types/content-type.php +++ b/src/general/domain/content-types/content-type.php @@ -19,7 +19,7 @@ class Content_Type { /** * The constructor. * - * @param WP_Post_Type $content_type The the content type. + * @param WP_Post_Type $content_type The content type. */ public function __construct( WP_Post_Type $content_type ) { $this->content_type = $content_type; @@ -28,9 +28,9 @@ public function __construct( WP_Post_Type $content_type ) { /** * Maps all content type information to the expected key value representation. * - * @param array $content_type_taxonomy The filtering taxonomy of the content type. + * @param array> $content_type_taxonomy The filtering taxonomy of the content type. * - * @return array The expected key value representation. + * @return array>|null> The expected key value representation. */ public function map_to_array( array $content_type_taxonomy ): array { return [ diff --git a/src/general/domain/taxonomies/taxonomy.php b/src/general/domain/taxonomies/taxonomy.php index ec1bf5d260e..343c599c225 100644 --- a/src/general/domain/taxonomies/taxonomy.php +++ b/src/general/domain/taxonomies/taxonomy.php @@ -28,7 +28,7 @@ public function __construct( WP_Taxonomy $taxonomy ) { /** * Maps all taxonomy information to the expected key value representation. * - * @return array The expected key value representation. + * @return array> The expected key value representation. */ public function map_to_array(): array { return [ From e328d255e2a626ee8ed0b6bf7d09a842c4a39b4d Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 12 Nov 2024 12:46:21 +0200 Subject: [PATCH 013/132] Fix unit tests --- .../General_Page_Integration_Test.php | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php index bfe124bd1e9..bed034aa6cb 100644 --- a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php +++ b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php @@ -8,6 +8,7 @@ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; +use Yoast\WP\SEO\General\Application\Content_Types\Content_Types_Repository; use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Notification_Helper; @@ -35,45 +36,52 @@ final class General_Page_Integration_Test extends TestCase { /** * Holds the Current_Page_Helper. * - * @var Current_Page_Helper + * @var Mockery\MockInterface|Current_Page_Helper */ private $current_page_helper; /** * Holds the Product_Helper. * - * @var Product_Helper + * @var Mockery\MockInterface|Product_Helper */ private $product_helper; /** * Holds the Short_Link_Helper. * - * @var Short_Link_Helper + * @var Mockery\MockInterface|Short_Link_Helper */ private $shortlink_helper; /** * Holds the Notification_Helper. * - * @var Notification_Helper + * @var Mockery\MockInterface|Notification_Helper */ private $notifications_helper; /** * Holds the alert dismissal action. * - * @var Alert_Dismissal_Action + * @var Mockery\MockInterface|Alert_Dismissal_Action */ private $alert_dismissal_action; /** * Holds the promotion manager. * - * @var Promotion_Manager + * @var Mockery\MockInterface|Promotion_Manager */ private $promotion_manager; + /** + * Holds the content types repository. + * + * @var Mockery\MockInterface|Content_Types_Repository + */ + private $content_types_repository; + /** * The class under test. * @@ -89,13 +97,14 @@ final class General_Page_Integration_Test extends TestCase { public function set_up() { $this->stubTranslationFunctions(); - $this->asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); - $this->current_page_helper = Mockery::mock( Current_Page_Helper::class ); - $this->product_helper = Mockery::mock( Product_Helper::class ); - $this->shortlink_helper = Mockery::mock( Short_Link_Helper::class ); - $this->notifications_helper = Mockery::mock( Notification_Helper::class ); - $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); - $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); + $this->asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); + $this->current_page_helper = Mockery::mock( Current_Page_Helper::class ); + $this->product_helper = Mockery::mock( Product_Helper::class ); + $this->shortlink_helper = Mockery::mock( Short_Link_Helper::class ); + $this->notifications_helper = Mockery::mock( Notification_Helper::class ); + $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); + $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); + $this->content_types_repository = Mockery::mock( Content_Types_Repository::class ); $this->instance = new General_Page_Integration( $this->asset_manager, @@ -104,7 +113,8 @@ public function set_up() { $this->shortlink_helper, $this->notifications_helper, $this->alert_dismissal_action, - $this->promotion_manager + $this->promotion_manager, + $this->content_types_repository ); } @@ -125,7 +135,8 @@ public function test_construct() { $this->shortlink_helper, $this->notifications_helper, $this->alert_dismissal_action, - $this->promotion_manager + $this->promotion_manager, + $this->content_types_repository ) ); } @@ -331,6 +342,11 @@ public function expect_get_script_data() { ->once() ->andReturn( [] ); + $this->content_types_repository + ->expects( 'get_content_types' ) + ->once() + ->andReturn( [] ); + return $link_params; } } From 328edcc00d1b2322cdd226cea229180ae7fc99ca Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 12 Nov 2024 17:13:52 +0100 Subject: [PATCH 014/132] Term filter status in the UI * refactor fetch: trying to get to a reusable base -- debounce making it tricky * add spinner * add none found * keep latest query visible as fallback if nothing is selected * clear query if user clears using the interface --- packages/js/src/dash/components/dashboard.js | 4 +- packages/js/src/dash/components/seo-scores.js | 5 +- .../js/src/dash/components/term-filter.js | 114 +++++++++++------- packages/js/src/general/initialize.js | 2 +- 4 files changed, 74 insertions(+), 51 deletions(-) diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dashboard.js index fa8b26764d6..47752fd28cf 100644 --- a/packages/js/src/dash/components/dashboard.js +++ b/packages/js/src/dash/components/dashboard.js @@ -3,8 +3,8 @@ import { PageTitle } from "./page-title"; import { SeoScores } from "./seo-scores"; /** - * @typedef {import("../index").ContentType} ContentType - * @typedef {import("../index").Taxonomy} Taxonomy + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Taxonomy} Taxonomy */ /** diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index fdef7ad4c9d..ba94a4f70cb 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -6,8 +6,9 @@ import { ContentTypeFilter } from "./content-type-filter"; import { TermFilter } from "./term-filter"; /** - * @typedef {import("./dashboard").ContentType} ContentType - * @typedef {import("./dashboard").Taxonomy} Taxonomy + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Taxonomy} Taxonomy + * @type {import("../index").Term} Term */ /** diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js index 734cbed2dd4..d33434692a4 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/components/term-filter.js @@ -1,43 +1,43 @@ import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; -import { AutocompleteField } from "@yoast/ui-library"; +import { AutocompleteField, Spinner } from "@yoast/ui-library"; import { debounce } from "lodash"; import PropTypes from "prop-types"; import { FETCH_DELAY } from "../../shared-admin/constants"; /** - * @typedef {import("../index").Taxonomy} Taxonomy - * @typedef {import("../index").Term} Term + * @type {import("../index").Taxonomy} Taxonomy + * @type {import("../index").Term} Term */ /** - * @param {string} url The URL to fetch from. - * @param {string} query The query to search for. - * @param {AbortSignal} signal The signal to abort the request. - * @returns {Promise} The promise of terms, or an error. + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} requestInit The request options. + * @returns {Promise} The promise of a result, or an error. */ -const searchTerms = async( { url, query, signal } ) => { +const fetchJson = async( url, requestInit ) => { try { - const urlInstance = new URL( "?" + new URLSearchParams( { - search: query, - _fields: [ "name", "slug" ], - } ), url ); - const response = await fetch( urlInstance, { - headers: { - "Content-Type": "application/json", - }, - signal, - } ); - + const response = await fetch( url, requestInit ); if ( ! response.ok ) { - throw new Error( "Failed to search terms" ); + // From the perspective of the results, this is a Promise.reject. + throw new Error( "Not ok" ); } return response.json(); - } catch ( e ) { - throw e; + } catch ( error ) { + return Promise.reject( error ); } }; +/** + * @param {string|URL} baseUrl The URL to fetch from. + * @param {string} query The query to search for. + * @returns {URL} The URL with the search query. + */ +const createSearchTermUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( { + search: query, + _fields: [ "name", "slug" ], +} ), baseUrl ); + /** * @param {{name: string, slug: string}} term The term from the response. * @returns {Term} The transformed term. @@ -52,20 +52,19 @@ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); * @returns {JSX.Element} The element. */ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { - const [ error, setError ] = useState( null ); + const [ isLoading, setIsLoading ] = useState( false ); + const [ error, setError ] = useState(); const [ query, setQuery ] = useState( "" ); const [ terms, setTerms ] = useState( [] ); /** @type {MutableRefObject} */ const controller = useRef(); - useEffect( () => { - controller.current?.abort(); - controller.current = new AbortController(); - - searchTerms( { url: taxonomy.links.search, query, signal: controller.current.signal } ) + // This needs to be wrapped including settings the state, because the debounce return messes with the timing/events. + const handleSearchTerms = useCallback( debounce( ( ...args ) => { + fetchJson( ...args ) .then( ( result ) => { setTerms( result.map( transformTerm ) ); - setError( null ); + setError( undefined ); // eslint-disable-line no-undefined } ) .catch( ( e ) => { // Ignore abort errors, because they are expected. @@ -75,40 +74,63 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { message: __( "Something went wrong", "wordpress-seo" ), } ); } + } ) + .finally( () => { + setIsLoading( false ); } ); - - return () => { - controller.current?.abort(); - }; - }, [ taxonomy.links.search, query ] ); + }, FETCH_DELAY ), [] ); const handleChange = useCallback( ( value ) => { + if ( value === null ) { + // User indicated they want to clear the selection. + setQuery( "" ); + } onChange( terms.find( ( { name } ) => name === value ) ); }, [ terms ] ); - const handleQueryChange = useCallback( debounce( ( event ) => { + + const handleQueryChange = useCallback( ( event ) => { setQuery( event?.target?.value?.trim()?.toLowerCase() || "" ); - }, FETCH_DELAY ), [] ); + }, [] ); + + useEffect( () => { + setIsLoading( true ); + controller.current?.abort(); + controller.current = new AbortController(); + handleSearchTerms( createSearchTermUrl( taxonomy.links.search, query ), { + headers: { "Content-Type": "application/json" }, + signal: controller.current.signal, + } ); + + return () => controller.current?.abort(); + }, [ taxonomy.links.search, query, handleSearchTerms ] ); return ( - { terms - ? terms.map( ( { name, label } ) => ( - - { label } - - ) ) - : __( "None found", "wordpress-seo" ) - } + { isLoading && ( +
    + +
    + ) } + { ! isLoading && terms.length === 0 && ( +
    + { __( "Nothing found", "wordpress-seo" ) } +
    + ) } + { ! isLoading && terms.length > 0 && terms.map( ( { name, label } ) => ( + + { label } + + ) ) }
    ); }; diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 6d5ce993840..91b36f1c372 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -38,7 +38,7 @@ domReady( () => { label: "Posts", taxonomy: { name: "category", - label: "Categories", + label: "Category", links: { search: "https://igor.local/wp-json/wp/v2/categories", }, From a775b85812517f8441e3502194fb0e47eb6956e7 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:34:00 +0100 Subject: [PATCH 015/132] Rename loading to querying * rename search to query * move fetchJson to util folder --- .../js/src/dash/components/term-filter.js | 43 ++++++------------- packages/js/src/dash/util/fetch-json.js | 17 ++++++++ 2 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 packages/js/src/dash/util/fetch-json.js diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js index d33434692a4..4b30fde34f4 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/components/term-filter.js @@ -4,36 +4,19 @@ import { AutocompleteField, Spinner } from "@yoast/ui-library"; import { debounce } from "lodash"; import PropTypes from "prop-types"; import { FETCH_DELAY } from "../../shared-admin/constants"; +import { fetchJson } from "../util/fetch-json"; /** * @type {import("../index").Taxonomy} Taxonomy * @type {import("../index").Term} Term */ -/** - * @param {string|URL} url The URL to fetch from. - * @param {RequestInit} requestInit The request options. - * @returns {Promise} The promise of a result, or an error. - */ -const fetchJson = async( url, requestInit ) => { - try { - const response = await fetch( url, requestInit ); - if ( ! response.ok ) { - // From the perspective of the results, this is a Promise.reject. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } -}; - /** * @param {string|URL} baseUrl The URL to fetch from. - * @param {string} query The query to search for. - * @returns {URL} The URL with the search query. + * @param {string} query The query. + * @returns {URL} The URL with the query. */ -const createSearchTermUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( { +const createQueryUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( { search: query, _fields: [ "name", "slug" ], } ), baseUrl ); @@ -52,7 +35,7 @@ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); * @returns {JSX.Element} The element. */ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { - const [ isLoading, setIsLoading ] = useState( false ); + const [ isQuerying, setIsQuerying ] = useState( false ); const [ error, setError ] = useState(); const [ query, setQuery ] = useState( "" ); const [ terms, setTerms ] = useState( [] ); @@ -60,7 +43,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { const controller = useRef(); // This needs to be wrapped including settings the state, because the debounce return messes with the timing/events. - const handleSearchTerms = useCallback( debounce( ( ...args ) => { + const handleTermQuery = useCallback( debounce( ( ...args ) => { fetchJson( ...args ) .then( ( result ) => { setTerms( result.map( transformTerm ) ); @@ -76,7 +59,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { } } ) .finally( () => { - setIsLoading( false ); + setIsQuerying( false ); } ); }, FETCH_DELAY ), [] ); @@ -93,16 +76,16 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { }, [] ); useEffect( () => { - setIsLoading( true ); + setIsQuerying( true ); controller.current?.abort(); controller.current = new AbortController(); - handleSearchTerms( createSearchTermUrl( taxonomy.links.search, query ), { + handleTermQuery( createQueryUrl( taxonomy.links.search, query ), { headers: { "Content-Type": "application/json" }, signal: controller.current.signal, } ); return () => controller.current?.abort(); - }, [ taxonomy.links.search, query, handleSearchTerms ] ); + }, [ taxonomy.links.search, query, handleTermQuery ] ); return ( { nullable={ true } validation={ error } > - { isLoading && ( + { isQuerying && (
    ) } - { ! isLoading && terms.length === 0 && ( + { ! isQuerying && terms.length === 0 && (
    { __( "Nothing found", "wordpress-seo" ) }
    ) } - { ! isLoading && terms.length > 0 && terms.map( ( { name, label } ) => ( + { ! isQuerying && terms.length > 0 && terms.map( ( { name, label } ) => ( { label } diff --git a/packages/js/src/dash/util/fetch-json.js b/packages/js/src/dash/util/fetch-json.js new file mode 100644 index 00000000000..989c5defa44 --- /dev/null +++ b/packages/js/src/dash/util/fetch-json.js @@ -0,0 +1,17 @@ +/** + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} requestInit The request options. + * @returns {Promise} The promise of a result, or an error. + */ +export const fetchJson = async( url, requestInit ) => { + try { + const response = await fetch( url, requestInit ); + if ( ! response.ok ) { + // From the perspective of the results, we want to reject this as an error. + throw new Error( "Not ok" ); + } + return response.json(); + } catch ( error ) { + return Promise.reject( error ); + } +}; From 9b4af03d4ff7dab367881017dc0cb2b9c5e3a0b4 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 13 Nov 2024 12:53:38 +0200 Subject: [PATCH 016/132] CR feedback: Mostly decouple the domain layer from WP context --- .../content-types-repository.php | 26 +++-- .../taxonomies/taxonomies-repository.php | 81 ++++---------- .../taxonomy-filters-repository.php | 29 +++-- .../domain/content-types/content-type.php | 62 ++++++++--- .../content-types/content-types-list.php | 45 ++++++++ src/general/domain/taxonomies/taxonomy.php | 61 ++++++----- .../infrastructure/taxonomies-collector.php | 101 ++++++++++++++++++ 7 files changed, 285 insertions(+), 120 deletions(-) create mode 100644 src/general/domain/content-types/content-types-list.php create mode 100644 src/general/infrastructure/taxonomies-collector.php diff --git a/src/general/application/content-types/content-types-repository.php b/src/general/application/content-types/content-types-repository.php index e45a5c35029..4a31132278e 100644 --- a/src/general/application/content-types/content-types-repository.php +++ b/src/general/application/content-types/content-types-repository.php @@ -5,6 +5,7 @@ use Yoast\WP\SEO\General\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\General\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\General\Domain\Content_Types\Content_Types_List; use Yoast\WP\SEO\Helpers\Post_Type_Helper; /** @@ -19,6 +20,13 @@ class Content_Types_Repository { */ protected $post_type_helper; + /** + * The content types list. + * + * @var Content_Types_List + */ + protected $content_types_list; + /** * The taxonomies repository. * @@ -30,33 +38,35 @@ class Content_Types_Repository { * The constructor. * * @param Post_Type_Helper $post_type_helper The post type helper. + * @param Content_Types_List $content_types_list The content types list. * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. */ public function __construct( Post_Type_Helper $post_type_helper, + Content_Types_List $content_types_list, Taxonomies_Repository $taxonomies_repository ) { $this->post_type_helper = $post_type_helper; + $this->content_types_list = $content_types_list; $this->taxonomies_repository = $taxonomies_repository; } /** - * Returns the content types object. + * Returns the content types array. * - * @return array>|null> The content types object. + * @return array>>>> The content types array. */ public function get_content_types(): array { - $content_types = []; - $post_types = $this->post_type_helper->get_indexable_post_types(); + $post_types = $this->post_type_helper->get_indexable_post_types(); foreach ( $post_types as $post_type ) { $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. - $content_type_instance = new Content_Type( $post_type_object ); - $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $post_type_object->name ); - $content_types[] = $content_type_instance->map_to_array( $content_type_taxonomy ); + + $content_type = new Content_Type( $post_type_object->name, $post_type_object->label, $content_type_taxonomy ); + $this->content_types_list->add( $content_type ); } - return $content_types; + return $this->content_types_list->to_array(); } } diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index ffa857383d4..580efdbcadf 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -3,15 +3,22 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomies; -use WP_Taxonomy; use Yoast\WP\SEO\General\Application\Taxonomy_Filters\Taxonomy_Filters_Repository; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; +use Yoast\WP\SEO\General\Infrastructure\Taxonomies_Collector; /** * The repository to get taxonomies. */ class Taxonomies_Repository { + /** + * The taxonomies collector. + * + * @var Taxonomies_Collector + */ + private $taxonomies_collector; + /** * The taxonomy filters repository. * @@ -22,82 +29,38 @@ class Taxonomies_Repository { /** * The constructor. * + * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. * @param Taxonomy_Filters_Repository $taxonomy_filters_repository The taxonomy filters repository. */ public function __construct( + Taxonomies_Collector $taxonomies_collector, Taxonomy_Filters_Repository $taxonomy_filters_repository ) { + $this->taxonomies_collector = $taxonomies_collector; $this->taxonomy_filters_repository = $taxonomy_filters_repository; } /** * Returns the object of the filtering taxonomy of a content type. * - * @param string $content_type The content type the taxonomy filters. + * @param string $content_type The content type that the taxonomy filters. * - * @return array> The filtering taxonomy of the content type. + * @return Taxonomy|null The filtering taxonomy of the content type. */ - public function get_content_type_taxonomy( string $content_type ): array { + public function get_content_type_taxonomy( string $content_type ) { // First we check if there's a filter that overrides the filtering taxonomy for this content type. - /** - * Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. - * - * @param string $filtering_taxonomy The taxonomy that filters the content type. - */ - $filtering_taxonomy = \apply_filters( "wpseo_{$content_type}_filtering_taxonomy", '' ); - if ( $filtering_taxonomy !== '' ) { - $taxonomy = \get_taxonomy( $filtering_taxonomy ); - - if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { - return $this->get_taxonomy_map( $taxonomy ); - } - - \_doing_it_wrong( - 'Filter: \'wpseo_{$content_type}_filtering_taxonomy\'', - 'The `wpseo_{$content_type}_filtering_taxonomy` filter should return a public taxonomy, available in REST API, that is associated with that content type.', - 'YoastSEO v24.1' - ); + $taxonomy = $this->taxonomies_collector->get_custom_filtering_taxonomy( $content_type ); + if ( $taxonomy ) { + return $taxonomy; } // Then we check if there is a filter explicitly made for this content type. - $taxonomy = $this->taxonomy_filters_repository->get_taxonomy_filter( $content_type ); - if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { - return $this->get_taxonomy_map( $taxonomy ); - } - - // As a fallback, we check if the content type has a category taxonomy. - $taxonomy = \get_taxonomy( 'category' ); - if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { - return $this->get_taxonomy_map( $taxonomy ); + $taxonomy = $this->taxonomy_filters_repository->get_taxonomy( $content_type ); + if ( $taxonomy ) { + return $taxonomy; } - return []; - } - - /** - * Returns the map of the filtering taxonomy. - * - * @param WP_Taxonomy $taxonomy The taxonomy. - * - * @return array> The map of the filtering taxonomy. - */ - private function get_taxonomy_map( WP_Taxonomy $taxonomy ): array { - $taxonomy_instance = new Taxonomy( $taxonomy ); - return $taxonomy_instance->map_to_array(); - } - - /** - * Returns whether the taxonomy in question is valid and associated with a given content type. - * - * @param WP_Taxonomy|false $taxonomy The taxonomy to check. - * @param string $content_type The name of the content type to check. - * - * @return bool Whether the taxonomy in question is valid. - */ - private function is_taxonomy_valid( $taxonomy, $content_type ): bool { - return \is_a( $taxonomy, 'WP_Taxonomy' ) - && $taxonomy->public - && $taxonomy->show_in_rest - && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); + // If everything else returned empty, we can always try the fallback taxonomy. + return $this->taxonomies_collector->get_fallback_taxonomy( $content_type ); } } diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php index 5121bab5652..d85cc3666ec 100644 --- a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php +++ b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php @@ -3,14 +3,22 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomy_Filters; -use WP_Taxonomy; +use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\General\Domain\Taxonomy_Filters\Taxonomy_Filter_Interface; +use Yoast\WP\SEO\General\Infrastructure\Taxonomies_Collector; /** * The repository to get taxonomy filters. */ class Taxonomy_Filters_Repository { + /** + * The taxonomies collector. + * + * @var Taxonomies_Collector + */ + private $taxonomies_collector; + /** * All taxonomy filters. * @@ -21,30 +29,31 @@ class Taxonomy_Filters_Repository { /** * The constructor. * - * @param Taxonomy_Filter_Interface ...$taxonomy_filters All taxonomy filters. + * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. + * @param Taxonomy_Filter_Interface ...$taxonomy_filters All taxonomy filters. */ public function __construct( + Taxonomies_Collector $taxonomies_collector, Taxonomy_Filter_Interface ...$taxonomy_filters ) { - $this->taxonomy_filters = $taxonomy_filters; + $this->taxonomies_collector = $taxonomies_collector; + $this->taxonomy_filters = $taxonomy_filters; } /** - * Returns a taxonomy filter based on a content type. + * Returns a taxonomy based on a content type, by looking into taxonomy filters. * * @param string $content_type The content type. * - * @return WP_Taxonomy|false The taxonomy filter. + * @return Taxonomy|null The taxonomy filter. */ - public function get_taxonomy_filter( string $content_type ) { + public function get_taxonomy( string $content_type ): ?Taxonomy { foreach ( $this->taxonomy_filters as $taxonomy_filter ) { if ( $taxonomy_filter->get_filtered_content_type() === $content_type ) { - $taxonomy = \get_taxonomy( $taxonomy_filter->get_filtering_taxonomy() ); - - return $taxonomy; + return $this->taxonomies_collector->get_taxonomy( $taxonomy_filter->get_filtering_taxonomy(), $content_type ); } } - return false; + return null; } } diff --git a/src/general/domain/content-types/content-type.php b/src/general/domain/content-types/content-type.php index a0d7fa03b40..b5d0d0c1a25 100644 --- a/src/general/domain/content-types/content-type.php +++ b/src/general/domain/content-types/content-type.php @@ -2,7 +2,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\General\Domain\Content_Types; -use WP_Post_Type; +use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; /** * This class describes a Content Type. @@ -10,33 +10,63 @@ class Content_Type { /** - * The content type. + * The name of the content type. * - * @var WP_Post_Type + * @var string */ - private $content_type; + private $name; + + /** + * The label of the content type. + * + * @var string + */ + private $label; + + /** + * The taxonomy that filters the content type. + * + * @var Taxonomy + */ + private $taxonomy; /** * The constructor. * - * @param WP_Post_Type $content_type The content type. + * @param string $name The name of the content type. + * @param string $label The label of the content type. + * @param Taxonomy $taxonomy The taxonomy that filters the content type. */ - public function __construct( WP_Post_Type $content_type ) { - $this->content_type = $content_type; + public function __construct( string $name, string $label, ?Taxonomy $taxonomy ) { + $this->name = $name; + $this->label = $label; + $this->taxonomy = $taxonomy; } /** - * Maps all content type information to the expected key value representation. + * Gets name of the content type. * - * @param array> $content_type_taxonomy The filtering taxonomy of the content type. + * @return string The name of the content type. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Gets label of the content type. + * + * @return string The label of the content type. + */ + public function get_label(): string { + return $this->label; + } + + /** + * Gets the taxonomy that filters the content type. * - * @return array>|null> The expected key value representation. + * @return string The taxonomy that filters the content type. */ - public function map_to_array( array $content_type_taxonomy ): array { - return [ - 'name' => $this->content_type->name, - 'label' => $this->content_type->label, - 'taxonomy' => ( \count( $content_type_taxonomy ) === 0 ) ? null : $content_type_taxonomy, - ]; + public function get_taxonomy(): ?Taxonomy { + return $this->taxonomy; } } diff --git a/src/general/domain/content-types/content-types-list.php b/src/general/domain/content-types/content-types-list.php new file mode 100644 index 00000000000..afe7dc79db6 --- /dev/null +++ b/src/general/domain/content-types/content-types-list.php @@ -0,0 +1,45 @@ + + */ + private $content_types = []; + + /** + * Adds a content type to the list. + * + * @param Content_Type $content_type The content type to add. + * + * @return void + */ + public function add( Content_Type $content_type ): void { + $this->content_types[] = $content_type; + } + + /** + * Parses the content type list to the expected key value representation. + * + * @return array>>>> The content type list presented as the expected key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->content_types as $content_type ) { + $array[] = [ + 'name' => $content_type->get_name(), + 'label' => $content_type->get_label(), + 'taxonomy' => ( $content_type->get_taxonomy() ) ? $content_type->get_taxonomy()->to_array() : null, + ]; + } + + return $array; + } +} diff --git a/src/general/domain/taxonomies/taxonomy.php b/src/general/domain/taxonomies/taxonomy.php index 343c599c225..6dd11c37d65 100644 --- a/src/general/domain/taxonomies/taxonomy.php +++ b/src/general/domain/taxonomies/taxonomy.php @@ -2,54 +2,61 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\General\Domain\Taxonomies; -use WP_Taxonomy; - /** * This class describes a Taxonomy. */ class Taxonomy { /** - * The taxonomy. + * The name of the taxonomy. + * + * @var string + */ + private $name; + + /** + * The label of the taxonomy. + * + * @var string + */ + private $label; + + /** + * The REST URL of the taxonomy. * - * @var WP_Taxonomy + * @var string */ - private $taxonomy; + private $rest_url; /** * The constructor. * - * @param WP_Taxonomy $taxonomy The taxonomy. + * @param string $name The name of the taxonomy. + * @param string $label The label of the taxonomy. + * @param string $rest_url The REST URL of the taxonomy. */ - public function __construct( WP_Taxonomy $taxonomy ) { - $this->taxonomy = $taxonomy; + public function __construct( + string $name, + string $label, + string $rest_url + ) { + $this->name = $name; + $this->label = $label; + $this->rest_url = $rest_url; } /** - * Maps all taxonomy information to the expected key value representation. + * Parses the taxonomy to the expected key value representation. * - * @return array> The expected key value representation. + * @return array> The taxonomy presented as the expected key value representation. */ - public function map_to_array(): array { + public function to_array(): array { return [ - 'name' => $this->taxonomy->name, - 'label' => $this->taxonomy->label, + 'name' => $this->name, + 'label' => $this->label, 'links' => [ - 'search' => $this->build_rest_url(), + 'search' => $this->rest_url, ], ]; } - - /** - * Builds the REST API URL for the taxonomy. - * - * @return string The REST API URL for the taxonomy. - */ - protected function build_rest_url(): string { - $rest_base = ( $this->taxonomy->rest_base ) ? $this->taxonomy->rest_base : $this->taxonomy->name; - - $rest_namespace = ( $this->taxonomy->rest_namespace ) ? $this->taxonomy->rest_namespace : 'wp/v2'; - - return \rest_url( "{$rest_namespace}/{$rest_base}" ); - } } diff --git a/src/general/infrastructure/taxonomies-collector.php b/src/general/infrastructure/taxonomies-collector.php new file mode 100644 index 00000000000..b4e6581ec01 --- /dev/null +++ b/src/general/infrastructure/taxonomies-collector.php @@ -0,0 +1,101 @@ +get_taxonomy( $filtering_taxonomy, $content_type ); + + if ( $taxonomy ) { + return $taxonomy; + } + + \_doing_it_wrong( + 'Filter: \'wpseo_{$content_type}_filtering_taxonomy\'', + 'The `wpseo_{$content_type}_filtering_taxonomy` filter should return a public taxonomy, available in REST API, that is associated with that content type.', + 'YoastSEO v24.1' + ); + } + + return null; + } + + /** + * Returns the fallback, WP-native category taxonomy, if it's associated with the specific content type. + * + * @param string $content_type The content type. + * + * @return Taxonomy|null The taxonomy object for the category taxonomy. + */ + public function get_fallback_taxonomy( string $content_type ): ?Taxonomy { + return $this->get_taxonomy( 'category', $content_type ); + } + + /** + * Returns the taxonomy object that filters a specific content type. + * + * @param string $taxonomy_name The name of the taxonomy we're going to build the object for. + * @param string $content_type The content type that the taxonomy object is filtering. + * + * @return Taxonomy|null The taxonomy object. + */ + public function get_taxonomy( string $taxonomy_name, string $content_type ): ?Taxonomy { + $taxonomy = \get_taxonomy( $taxonomy_name ); + + if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + return new Taxonomy( $taxonomy->name, $taxonomy->label, $this->get_taxonomy_rest_url( $taxonomy ) ); + } + + return null; + } + + /** + * Builds the REST API URL for the taxonomy. + * + * @param WP_Taxonomy $taxonomy The taxonomy we want to build the REST API URL for. + * + * @return string The REST API URL for the taxonomy. + */ + protected function get_taxonomy_rest_url( WP_Taxonomy $taxonomy ): string { + $rest_base = ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; + + $rest_namespace = ( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2'; + + return \rest_url( "{$rest_namespace}/{$rest_base}" ); + } + + /** + * Returns whether the taxonomy in question is valid and associated with a given content type. + * + * @param WP_Taxonomy|false|null $taxonomy The taxonomy to check. + * @param string $content_type The name of the content type to check. + * + * @return bool Whether the taxonomy in question is valid. + */ + public function is_taxonomy_valid( $taxonomy, $content_type ): bool { + return \is_a( $taxonomy, 'WP_Taxonomy' ) + && $taxonomy->public + && $taxonomy->show_in_rest + && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); + } +} From b96823a5a5b54ff340725a3c5741f57af41954ce Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 13 Nov 2024 13:07:22 +0200 Subject: [PATCH 017/132] Create a separate taxonomy validator in the infrastructure layer --- .../taxonomies/taxonomies-repository.php | 2 +- .../taxonomy-filters-repository.php | 2 +- .../{ => taxonomies}/taxonomies-collector.php | 38 ++++++++++--------- .../taxonomies/taxonomy-validator.php | 28 ++++++++++++++ 4 files changed, 51 insertions(+), 19 deletions(-) rename src/general/infrastructure/{ => taxonomies}/taxonomies-collector.php (80%) create mode 100644 src/general/infrastructure/taxonomies/taxonomy-validator.php diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 580efdbcadf..40ef7738e97 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -5,7 +5,7 @@ use Yoast\WP\SEO\General\Application\Taxonomy_Filters\Taxonomy_Filters_Repository; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\General\Infrastructure\Taxonomies_Collector; +use Yoast\WP\SEO\General\Infrastructure\Taxonomies\Taxonomies_Collector; /** * The repository to get taxonomies. diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php index d85cc3666ec..cdea79392f1 100644 --- a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php +++ b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php @@ -5,7 +5,7 @@ use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\General\Domain\Taxonomy_Filters\Taxonomy_Filter_Interface; -use Yoast\WP\SEO\General\Infrastructure\Taxonomies_Collector; +use Yoast\WP\SEO\General\Infrastructure\Taxonomies\Taxonomies_Collector; /** * The repository to get taxonomy filters. diff --git a/src/general/infrastructure/taxonomies-collector.php b/src/general/infrastructure/taxonomies/taxonomies-collector.php similarity index 80% rename from src/general/infrastructure/taxonomies-collector.php rename to src/general/infrastructure/taxonomies/taxonomies-collector.php index b4e6581ec01..11d51dbb2db 100644 --- a/src/general/infrastructure/taxonomies-collector.php +++ b/src/general/infrastructure/taxonomies/taxonomies-collector.php @@ -1,6 +1,7 @@ taxonomy_validator = $taxonomy_validator; + } + /** * Returns a custom pair of taxonomy/content type, that's been given by users via hooks. * @@ -62,7 +81,7 @@ public function get_fallback_taxonomy( string $content_type ): ?Taxonomy { public function get_taxonomy( string $taxonomy_name, string $content_type ): ?Taxonomy { $taxonomy = \get_taxonomy( $taxonomy_name ); - if ( $this->is_taxonomy_valid( $taxonomy, $content_type ) ) { + if ( $this->taxonomy_validator->is_valid_taxonomy( $taxonomy, $content_type ) ) { return new Taxonomy( $taxonomy->name, $taxonomy->label, $this->get_taxonomy_rest_url( $taxonomy ) ); } @@ -83,19 +102,4 @@ protected function get_taxonomy_rest_url( WP_Taxonomy $taxonomy ): string { return \rest_url( "{$rest_namespace}/{$rest_base}" ); } - - /** - * Returns whether the taxonomy in question is valid and associated with a given content type. - * - * @param WP_Taxonomy|false|null $taxonomy The taxonomy to check. - * @param string $content_type The name of the content type to check. - * - * @return bool Whether the taxonomy in question is valid. - */ - public function is_taxonomy_valid( $taxonomy, $content_type ): bool { - return \is_a( $taxonomy, 'WP_Taxonomy' ) - && $taxonomy->public - && $taxonomy->show_in_rest - && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); - } } diff --git a/src/general/infrastructure/taxonomies/taxonomy-validator.php b/src/general/infrastructure/taxonomies/taxonomy-validator.php new file mode 100644 index 00000000000..0e8d905365d --- /dev/null +++ b/src/general/infrastructure/taxonomies/taxonomy-validator.php @@ -0,0 +1,28 @@ +public + && $taxonomy->show_in_rest + && \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true ); + } +} From 5e4766bac97e42039135312dcc497dd36bbb996a Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 13 Nov 2024 14:47:17 +0200 Subject: [PATCH 018/132] Rename taxonomy filters into filter pairs --- .../filter-pairs/filter-pairs-repository.php | 59 +++++++++++++++++++ .../taxonomies/taxonomies-repository.php | 20 +++---- .../taxonomy-filters-repository.php | 59 ------------------- .../filter-pairs-interface.php} | 6 +- .../product-category-filter-pair.php} | 6 +- 5 files changed, 75 insertions(+), 75 deletions(-) create mode 100644 src/general/application/filter-pairs/filter-pairs-repository.php delete mode 100644 src/general/application/taxonomy-filters/taxonomy-filters-repository.php rename src/general/domain/{taxonomy-filters/taxonomy-filter-interface.php => filter-pairs/filter-pairs-interface.php} (69%) rename src/general/domain/{taxonomy-filters/product-category-filter.php => filter-pairs/product-category-filter-pair.php} (71%) diff --git a/src/general/application/filter-pairs/filter-pairs-repository.php b/src/general/application/filter-pairs/filter-pairs-repository.php new file mode 100644 index 00000000000..61f1739c133 --- /dev/null +++ b/src/general/application/filter-pairs/filter-pairs-repository.php @@ -0,0 +1,59 @@ +taxonomies_collector = $taxonomies_collector; + $this->filter_pairs = $filter_pairs; + } + + /** + * Returns a taxonomy based on a content type, by looking into hardcoded filter pairs. + * + * @param string $content_type The content type. + * + * @return Taxonomy|null The taxonomy filter. + */ + public function get_taxonomy( string $content_type ): ?Taxonomy { + foreach ( $this->filter_pairs as $filter_pair ) { + if ( $filter_pair->get_filtered_content_type() === $content_type ) { + return $this->taxonomies_collector->get_taxonomy( $filter_pair->get_filtering_taxonomy(), $content_type ); + } + } + + return null; + } +} diff --git a/src/general/application/taxonomies/taxonomies-repository.php b/src/general/application/taxonomies/taxonomies-repository.php index 40ef7738e97..cd25d90bed6 100644 --- a/src/general/application/taxonomies/taxonomies-repository.php +++ b/src/general/application/taxonomies/taxonomies-repository.php @@ -3,7 +3,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\General\Application\Taxonomies; -use Yoast\WP\SEO\General\Application\Taxonomy_Filters\Taxonomy_Filters_Repository; +use Yoast\WP\SEO\General\Application\Filter_Pairs\Filter_Pairs_Repository; use Yoast\WP\SEO\General\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\General\Infrastructure\Taxonomies\Taxonomies_Collector; @@ -20,24 +20,24 @@ class Taxonomies_Repository { private $taxonomies_collector; /** - * The taxonomy filters repository. + * The filter pairs repository. * - * @var Taxonomy_Filters_Repository + * @var Filter_Pairs_Repository */ - private $taxonomy_filters_repository; + private $filter_pairs_repository; /** * The constructor. * - * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. - * @param Taxonomy_Filters_Repository $taxonomy_filters_repository The taxonomy filters repository. + * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. + * @param Filter_Pairs_Repository $filter_pairs_repository The filter pairs repository. */ public function __construct( Taxonomies_Collector $taxonomies_collector, - Taxonomy_Filters_Repository $taxonomy_filters_repository + Filter_Pairs_Repository $filter_pairs_repository ) { - $this->taxonomies_collector = $taxonomies_collector; - $this->taxonomy_filters_repository = $taxonomy_filters_repository; + $this->taxonomies_collector = $taxonomies_collector; + $this->filter_pairs_repository = $filter_pairs_repository; } /** @@ -55,7 +55,7 @@ public function get_content_type_taxonomy( string $content_type ) { } // Then we check if there is a filter explicitly made for this content type. - $taxonomy = $this->taxonomy_filters_repository->get_taxonomy( $content_type ); + $taxonomy = $this->filter_pairs_repository->get_taxonomy( $content_type ); if ( $taxonomy ) { return $taxonomy; } diff --git a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php b/src/general/application/taxonomy-filters/taxonomy-filters-repository.php deleted file mode 100644 index cdea79392f1..00000000000 --- a/src/general/application/taxonomy-filters/taxonomy-filters-repository.php +++ /dev/null @@ -1,59 +0,0 @@ -taxonomies_collector = $taxonomies_collector; - $this->taxonomy_filters = $taxonomy_filters; - } - - /** - * Returns a taxonomy based on a content type, by looking into taxonomy filters. - * - * @param string $content_type The content type. - * - * @return Taxonomy|null The taxonomy filter. - */ - public function get_taxonomy( string $content_type ): ?Taxonomy { - foreach ( $this->taxonomy_filters as $taxonomy_filter ) { - if ( $taxonomy_filter->get_filtered_content_type() === $content_type ) { - return $this->taxonomies_collector->get_taxonomy( $taxonomy_filter->get_filtering_taxonomy(), $content_type ); - } - } - - return null; - } -} diff --git a/src/general/domain/taxonomy-filters/taxonomy-filter-interface.php b/src/general/domain/filter-pairs/filter-pairs-interface.php similarity index 69% rename from src/general/domain/taxonomy-filters/taxonomy-filter-interface.php rename to src/general/domain/filter-pairs/filter-pairs-interface.php index 12010c40c66..d8b3cbe0672 100644 --- a/src/general/domain/taxonomy-filters/taxonomy-filter-interface.php +++ b/src/general/domain/filter-pairs/filter-pairs-interface.php @@ -1,11 +1,11 @@ Date: Wed, 13 Nov 2024 16:31:35 +0200 Subject: [PATCH 019/132] Change namespace to Dash --- .../content-types/content-types-repository.php | 8 ++++---- .../application/filter-pairs/filter-pairs-repository.php | 8 ++++---- .../application/taxonomies/taxonomies-repository.php | 8 ++++---- .../domain/content-types/content-type.php | 4 ++-- .../domain/content-types/content-types-list.php | 2 +- .../domain/filter-pairs/filter-pairs-interface.php | 2 +- .../domain/filter-pairs/product-category-filter-pair.php | 2 +- src/{general => dash}/domain/taxonomies/taxonomy.php | 2 +- .../infrastructure/taxonomies/taxonomies-collector.php | 4 ++-- .../infrastructure/taxonomies/taxonomy-validator.php | 4 ++-- src/general/user-interface/general-page-integration.php | 2 +- .../User_Interface/General_Page_Integration_Test.php | 2 +- 12 files changed, 24 insertions(+), 24 deletions(-) rename src/{general => dash}/application/content-types/content-types-repository.php (88%) rename src/{general => dash}/application/filter-pairs/filter-pairs-repository.php (83%) rename src/{general => dash}/application/taxonomies/taxonomies-repository.php (87%) rename src/{general => dash}/domain/content-types/content-type.php (92%) rename src/{general => dash}/domain/content-types/content-types-list.php (95%) rename src/{general => dash}/domain/filter-pairs/filter-pairs-interface.php (89%) rename src/{general => dash}/domain/filter-pairs/product-category-filter-pair.php (91%) rename src/{general => dash}/domain/taxonomies/taxonomy.php (95%) rename src/{general => dash}/infrastructure/taxonomies/taxonomies-collector.php (96%) rename src/{general => dash}/infrastructure/taxonomies/taxonomy-validator.php (87%) diff --git a/src/general/application/content-types/content-types-repository.php b/src/dash/application/content-types/content-types-repository.php similarity index 88% rename from src/general/application/content-types/content-types-repository.php rename to src/dash/application/content-types/content-types-repository.php index 4a31132278e..f8ed7224cb2 100644 --- a/src/general/application/content-types/content-types-repository.php +++ b/src/dash/application/content-types/content-types-repository.php @@ -1,11 +1,11 @@ Date: Thu, 14 Nov 2024 08:25:24 +0200 Subject: [PATCH 020/132] Fix style things --- src/dash/domain/content-types/content-type.php | 8 ++++---- src/dash/infrastructure/taxonomies/taxonomy-validator.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dash/domain/content-types/content-type.php b/src/dash/domain/content-types/content-type.php index 6d44b34b9e4..8d0a400fc02 100644 --- a/src/dash/domain/content-types/content-type.php +++ b/src/dash/domain/content-types/content-type.php @@ -33,9 +33,9 @@ class Content_Type { /** * The constructor. * - * @param string $name The name of the content type. - * @param string $label The label of the content type. - * @param Taxonomy $taxonomy The taxonomy that filters the content type. + * @param string $name The name of the content type. + * @param string $label The label of the content type. + * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. */ public function __construct( string $name, string $label, ?Taxonomy $taxonomy ) { $this->name = $name; @@ -64,7 +64,7 @@ public function get_label(): string { /** * Gets the taxonomy that filters the content type. * - * @return string The taxonomy that filters the content type. + * @return Taxonomy|null The taxonomy that filters the content type. */ public function get_taxonomy(): ?Taxonomy { return $this->taxonomy; diff --git a/src/dash/infrastructure/taxonomies/taxonomy-validator.php b/src/dash/infrastructure/taxonomies/taxonomy-validator.php index 86588d2eee5..67620292d24 100644 --- a/src/dash/infrastructure/taxonomies/taxonomy-validator.php +++ b/src/dash/infrastructure/taxonomies/taxonomy-validator.php @@ -19,7 +19,7 @@ class Taxonomy_Validator { * * @return bool Whether the taxonomy in question is valid. */ - public function is_valid_taxonomy( $taxonomy, $content_type ): bool { + public function is_valid_taxonomy( $taxonomy, string $content_type ): bool { return \is_a( $taxonomy, 'WP_Taxonomy' ) && $taxonomy->public && $taxonomy->show_in_rest From 5e45c716a793dfe38eeadc2e429a33ae0c47698d Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Thu, 14 Nov 2024 09:30:35 +0100 Subject: [PATCH 021/132] use backend contentTypes --- packages/js/src/general/initialize.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 91b36f1c372..2e574cf45b8 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -32,24 +32,7 @@ domReady( () => { } ); const isRtl = select( STORE_NAME ).selectPreference( "isRtl", false ); - const contentTypes = get( window, "wpseoScriptData.dash.contentTypes", [ - { - name: "post", - label: "Posts", - taxonomy: { - name: "category", - label: "Category", - links: { - search: "https://igor.local/wp-json/wp/v2/categories", - }, - }, - }, - { - name: "page", - label: "Pages", - taxonomy: null, - }, - ] ); + const contentTypes = get( window, "wpseoScriptData.contentTypes", [] ); const userName = get( window, "wpseoScriptData.dash.userName", "User" ); const router = createHashRouter( From e294975b7e0718354c7b0f7cc74bfd51555172ab Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Fri, 15 Nov 2024 09:19:51 +0100 Subject: [PATCH 022/132] Move from Dash to Dashboard --- .../content-types/content-types-repository.php | 8 ++++---- .../application/filter-pairs/filter-pairs-repository.php | 8 ++++---- .../application/taxonomies/taxonomies-repository.php | 8 ++++---- .../domain/content-types/content-type.php | 4 ++-- .../domain/content-types/content-types-list.php | 2 +- .../domain/filter-pairs/filter-pairs-interface.php | 2 +- .../domain/filter-pairs/product-category-filter-pair.php | 2 +- src/{dash => dashboard}/domain/taxonomies/taxonomy.php | 2 +- .../infrastructure/taxonomies/taxonomies-collector.php | 4 ++-- .../infrastructure/taxonomies/taxonomy-validator.php | 4 ++-- src/general/user-interface/general-page-integration.php | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) rename src/{dash => dashboard}/application/content-types/content-types-repository.php (88%) rename src/{dash => dashboard}/application/filter-pairs/filter-pairs-repository.php (83%) rename src/{dash => dashboard}/application/taxonomies/taxonomies-repository.php (86%) rename src/{dash => dashboard}/domain/content-types/content-type.php (92%) rename src/{dash => dashboard}/domain/content-types/content-types-list.php (95%) rename src/{dash => dashboard}/domain/filter-pairs/filter-pairs-interface.php (89%) rename src/{dash => dashboard}/domain/filter-pairs/product-category-filter-pair.php (91%) rename src/{dash => dashboard}/domain/taxonomies/taxonomy.php (95%) rename src/{dash => dashboard}/infrastructure/taxonomies/taxonomies-collector.php (96%) rename src/{dash => dashboard}/infrastructure/taxonomies/taxonomy-validator.php (86%) diff --git a/src/dash/application/content-types/content-types-repository.php b/src/dashboard/application/content-types/content-types-repository.php similarity index 88% rename from src/dash/application/content-types/content-types-repository.php rename to src/dashboard/application/content-types/content-types-repository.php index f8ed7224cb2..dd77cafd1dd 100644 --- a/src/dash/application/content-types/content-types-repository.php +++ b/src/dashboard/application/content-types/content-types-repository.php @@ -1,11 +1,11 @@ Date: Fri, 15 Nov 2024 09:36:49 +0100 Subject: [PATCH 023/132] Update use statement in General_Page_Integration_Test.php --- .../General/User_Interface/General_Page_Integration_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php index dbab4be7a20..1c21f9435d8 100644 --- a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php +++ b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php @@ -8,7 +8,7 @@ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; -use Yoast\WP\SEO\Dash\Application\Content_Types\Content_Types_Repository; +use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Notification_Helper; From b7ba3da1e2e8c3e2dca89d75b5e27e6eddef1c10 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Fri, 15 Nov 2024 14:52:42 +0100 Subject: [PATCH 024/132] Add dashboard configuration object. --- packages/js/src/general/initialize.js | 4 +- .../configuration/dashboard-configuration.php | 85 +++++++++++++++++++ .../enabled-analysis-features-repository.php | 38 ++++++--- .../analysis-features-list.php | 15 ++++ src/editors/framework/cornerstone-content.php | 4 +- .../framework/inclusive-language-analysis.php | 4 +- src/editors/framework/keyphrase-analysis.php | 4 +- .../framework/previously-used-keyphrase.php | 4 +- .../framework/readability-analysis.php | 3 +- .../framework/word-form-recognition.php | 4 +- .../general-page-integration.php | 52 ++++++------ src/helpers/user-helper.php | 14 +++ .../Analysis_Features_List_Test.php | 40 ++++++--- .../General_Page_Integration_Test.php | 32 +++---- tests/Unit/Helpers/User_Helper_Test.php | 39 +++++++++ 15 files changed, 269 insertions(+), 73 deletions(-) create mode 100644 src/dashboard/application/configuration/dashboard-configuration.php diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 2e574cf45b8..510e44426e5 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -32,8 +32,8 @@ domReady( () => { } ); const isRtl = select( STORE_NAME ).selectPreference( "isRtl", false ); - const contentTypes = get( window, "wpseoScriptData.contentTypes", [] ); - const userName = get( window, "wpseoScriptData.dash.userName", "User" ); + const contentTypes = get( window, "wpseoScriptData.dashboard.contentTypes", [] ); + const userName = get( window, "wpseoScriptData.dashboard.displayName", "User" ); const router = createHashRouter( createRoutesFromElements( diff --git a/src/dashboard/application/configuration/dashboard-configuration.php b/src/dashboard/application/configuration/dashboard-configuration.php new file mode 100644 index 00000000000..2a578848775 --- /dev/null +++ b/src/dashboard/application/configuration/dashboard-configuration.php @@ -0,0 +1,85 @@ +content_types_repository = $content_types_repository; + $this->indexable_helper = $indexable_helper; + $this->user_helper = $user_helper; + $this->analysis_features_repository = $analysis_features_repository; + } + + /** + * Returns a configuration + * + * @return array> + */ + public function get_configuration(): array { + return [ + 'contentTypes' => $this->content_types_repository->get_content_types(), + 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), + 'displayName' => $this->user_helper->get_current_user_display_name(), + 'enabledAnalysisFeatures' => $this->analysis_features_repository->get_enabled_features_by_keys( + [ + Readability_Analysis::NAME, + Keyphrase_Analysis::NAME, + ] + )->to_array(), + ]; + } +} diff --git a/src/editors/application/analysis-features/enabled-analysis-features-repository.php b/src/editors/application/analysis-features/enabled-analysis-features-repository.php index 2cec11db2ac..9261d22aba3 100644 --- a/src/editors/application/analysis-features/enabled-analysis-features-repository.php +++ b/src/editors/application/analysis-features/enabled-analysis-features-repository.php @@ -21,21 +21,13 @@ class Enabled_Analysis_Features_Repository { */ private $plugin_features; - /** - * The list of analysis features. - * - * @var Analysis_Features_List - */ - private $enabled_analysis_features; - /** * The constructor. * * @param Analysis_Feature_Interface ...$plugin_features All analysis objects. */ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { - $this->enabled_analysis_features = new Analysis_Features_List(); - $this->plugin_features = $plugin_features; + $this->plugin_features = $plugin_features; } /** @@ -44,12 +36,32 @@ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { * @return Analysis_Features_List The analysis list. */ public function get_enabled_features(): Analysis_Features_List { - if ( \count( $this->enabled_analysis_features->parse_to_legacy_array() ) === 0 ) { - foreach ( $this->plugin_features as $plugin_feature ) { + $enabled_analysis_features = new Analysis_Features_List(); + foreach ( $this->plugin_features as $plugin_feature ) { + $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); + $enabled_analysis_features->add_feature( $analysis_feature ); + } + + return $enabled_analysis_features; + } + + /** + * Returns the analysis list for the given names. + * + * @param array $feature_names The feature names to include. + * + * @return Analysis_Features_List The analysis list. + */ + public function get_enabled_features_by_keys( array $feature_names ): Analysis_Features_List { + $enabled_analysis_features = new Analysis_Features_List(); + + foreach ( $this->plugin_features as $plugin_feature ) { + if ( \in_array( $plugin_feature->get_name(), $feature_names, true ) ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $this->enabled_analysis_features->add_feature( $analysis_feature ); + $enabled_analysis_features->add_feature( $analysis_feature ); } } - return $this->enabled_analysis_features; + + return $enabled_analysis_features; } } diff --git a/src/editors/domain/analysis-features/analysis-features-list.php b/src/editors/domain/analysis-features/analysis-features-list.php index 3fb19d04fec..bc918d25d03 100644 --- a/src/editors/domain/analysis-features/analysis-features-list.php +++ b/src/editors/domain/analysis-features/analysis-features-list.php @@ -35,6 +35,21 @@ public function parse_to_legacy_array(): array { foreach ( $this->features as $feature ) { $array = \array_merge( $array, $feature->to_legacy_array() ); } + + return $array; + } + + /** + * Parses the feature list to an array representation. + * + * @return array The list presented as a key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->features as $feature ) { + $array = \array_merge( $array, $feature->to_array() ); + } + return $array; } } diff --git a/src/editors/framework/cornerstone-content.php b/src/editors/framework/cornerstone-content.php index 8dff716c8c9..e75f3bdbf50 100644 --- a/src/editors/framework/cornerstone-content.php +++ b/src/editors/framework/cornerstone-content.php @@ -10,6 +10,8 @@ */ class Cornerstone_Content implements Analysis_Feature_Interface { + public const NAME = 'cornerstoneContent'; + /** * The options helper. * @@ -41,7 +43,7 @@ public function is_enabled(): bool { * @return string The name. */ public function get_name(): string { - return 'cornerstoneContent'; + return self::NAME; } /** diff --git a/src/editors/framework/inclusive-language-analysis.php b/src/editors/framework/inclusive-language-analysis.php index 4cfbe0254dc..8a8328b58e1 100644 --- a/src/editors/framework/inclusive-language-analysis.php +++ b/src/editors/framework/inclusive-language-analysis.php @@ -12,6 +12,8 @@ */ class Inclusive_Language_Analysis implements Analysis_Feature_Interface { + public const NAME = 'inclusiveLanguageAnalysis'; + /** * The options helper. * @@ -102,7 +104,7 @@ private function is_current_version_supported(): bool { * @return string The name. */ public function get_name(): string { - return 'inclusiveLanguageAnalysis'; + return self::NAME; } /** diff --git a/src/editors/framework/keyphrase-analysis.php b/src/editors/framework/keyphrase-analysis.php index 648639b2e55..287477ec38a 100644 --- a/src/editors/framework/keyphrase-analysis.php +++ b/src/editors/framework/keyphrase-analysis.php @@ -10,6 +10,8 @@ */ class Keyphrase_Analysis implements Analysis_Feature_Interface { + public const NAME = 'keyphraseAnalysis'; + /** * The options helper. * @@ -59,7 +61,7 @@ public function is_globally_enabled(): bool { * @return string The name. */ public function get_name(): string { - return 'keyphraseAnalysis'; + return self::NAME; } /** diff --git a/src/editors/framework/previously-used-keyphrase.php b/src/editors/framework/previously-used-keyphrase.php index 3400f561c6d..e4f3da7288f 100644 --- a/src/editors/framework/previously-used-keyphrase.php +++ b/src/editors/framework/previously-used-keyphrase.php @@ -9,6 +9,8 @@ */ class Previously_Used_Keyphrase implements Analysis_Feature_Interface { + public const NAME = 'previouslyUsedKeyphrase'; + /** * If this analysis is enabled. * @@ -29,7 +31,7 @@ public function is_enabled(): bool { * @return string */ public function get_name(): string { - return 'previouslyUsedKeyphrase'; + return self::NAME; } /** diff --git a/src/editors/framework/readability-analysis.php b/src/editors/framework/readability-analysis.php index 03accffe776..9e3b4b9cf9a 100644 --- a/src/editors/framework/readability-analysis.php +++ b/src/editors/framework/readability-analysis.php @@ -9,6 +9,7 @@ * This class describes the Readability analysis feature. */ class Readability_Analysis implements Analysis_Feature_Interface { + public const NAME = 'readabilityAnalysis'; /** * The options helper. @@ -59,7 +60,7 @@ private function is_globally_enabled(): bool { * @return string The name. */ public function get_name(): string { - return 'readabilityAnalysis'; + return self::NAME; } /** diff --git a/src/editors/framework/word-form-recognition.php b/src/editors/framework/word-form-recognition.php index da63caf798e..e54dbf828ee 100644 --- a/src/editors/framework/word-form-recognition.php +++ b/src/editors/framework/word-form-recognition.php @@ -10,6 +10,8 @@ */ class Word_Form_Recognition implements Analysis_Feature_Interface { + public const NAME = 'wordFormRecognition'; + /** * The language helper. * @@ -41,7 +43,7 @@ public function is_enabled(): bool { * @return string */ public function get_name(): string { - return 'wordFormRecognition'; + return self::NAME; } /** diff --git a/src/general/user-interface/general-page-integration.php b/src/general/user-interface/general-page-integration.php index 3346da75324..0a119835477 100644 --- a/src/general/user-interface/general-page-integration.php +++ b/src/general/user-interface/general-page-integration.php @@ -6,7 +6,7 @@ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; -use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; +use Yoast\WP\SEO\Dashboard\Application\Configuration\Dashboard_Configuration; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Notification_Helper; use Yoast\WP\SEO\Helpers\Product_Helper; @@ -31,6 +31,13 @@ class General_Page_Integration implements Integration_Interface { */ protected $notification_helper; + /** + * The dashboard configuration. + * + * @var Dashboard_Configuration + */ + private $dashboard_configuration; + /** * Holds the WPSEO_Admin_Asset_Manager. * @@ -73,24 +80,17 @@ class General_Page_Integration implements Integration_Interface { */ private $alert_dismissal_action; - /** - * The content types repository. - * - * @var Content_Types_Repository $content_types_repository - */ - private $content_types_repository; - /** * Constructs Academy_Integration. * - * @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager. - * @param Current_Page_Helper $current_page_helper The Current_Page_Helper. - * @param Product_Helper $product_helper The Product_Helper. - * @param Short_Link_Helper $shortlink_helper The Short_Link_Helper. - * @param Notification_Helper $notification_helper The Notification_Helper. - * @param Alert_Dismissal_Action $alert_dismissal_action The alert dismissal action. - * @param Promotion_Manager $promotion_manager The promotion manager. - * @param Content_Types_Repository $content_types_repository The content types repository. + * @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager. + * @param Current_Page_Helper $current_page_helper The Current_Page_Helper. + * @param Product_Helper $product_helper The Product_Helper. + * @param Short_Link_Helper $shortlink_helper The Short_Link_Helper. + * @param Notification_Helper $notification_helper The Notification_Helper. + * @param Alert_Dismissal_Action $alert_dismissal_action The alert dismissal action. + * @param Promotion_Manager $promotion_manager The promotion manager. + * @param Dashboard_Configuration $dashboard_configuration The dashboard configuration. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, @@ -100,16 +100,16 @@ public function __construct( Notification_Helper $notification_helper, Alert_Dismissal_Action $alert_dismissal_action, Promotion_Manager $promotion_manager, - Content_Types_Repository $content_types_repository + Dashboard_Configuration $dashboard_configuration ) { - $this->asset_manager = $asset_manager; - $this->current_page_helper = $current_page_helper; - $this->product_helper = $product_helper; - $this->shortlink_helper = $shortlink_helper; - $this->notification_helper = $notification_helper; - $this->alert_dismissal_action = $alert_dismissal_action; - $this->promotion_manager = $promotion_manager; - $this->content_types_repository = $content_types_repository; + $this->asset_manager = $asset_manager; + $this->current_page_helper = $current_page_helper; + $this->product_helper = $product_helper; + $this->shortlink_helper = $shortlink_helper; + $this->notification_helper = $notification_helper; + $this->alert_dismissal_action = $alert_dismissal_action; + $this->promotion_manager = $promotion_manager; + $this->dashboard_configuration = $dashboard_configuration; } /** @@ -213,7 +213,7 @@ private function get_script_data() { 'alerts' => $this->notification_helper->get_alerts(), 'currentPromotions' => $this->promotion_manager->get_current_promotions(), 'dismissedAlerts' => $this->alert_dismissal_action->all_dismissed(), - 'contentTypes' => $this->content_types_repository->get_content_types(), + 'dashboard' => $this->dashboard_configuration->get_configuration(), ]; } } diff --git a/src/helpers/user-helper.php b/src/helpers/user-helper.php index d319a8c2ad9..a34dbf0d529 100644 --- a/src/helpers/user-helper.php +++ b/src/helpers/user-helper.php @@ -65,6 +65,20 @@ public function get_current_user_id() { return \get_current_user_id(); } + /** + * Returns the current users display_name. + * + * @return string + */ + public function get_current_user_display_name(): string { + $user = \wp_get_current_user(); + if ( $user && $user->display_name ) { + return $user->display_name; + } + + return ''; + } + /** * Updates user meta field for a user. * diff --git a/tests/Unit/Editors/Domain/Analysis_Features/Analysis_Features_List_Test.php b/tests/Unit/Editors/Domain/Analysis_Features/Analysis_Features_List_Test.php index 3792992aba0..435076d252e 100644 --- a/tests/Unit/Editors/Domain/Analysis_Features/Analysis_Features_List_Test.php +++ b/tests/Unit/Editors/Domain/Analysis_Features/Analysis_Features_List_Test.php @@ -22,16 +22,6 @@ final class Analysis_Features_List_Test extends TestCase { */ private $instance; - /** - * Set up the test. - * - * @return void - */ - protected function set_up(): void { - parent::set_up(); - $this->instance = new Analysis_Features_List(); - } - /** * Tests the getters. * @@ -51,4 +41,34 @@ public function test_parse_to_legacy_array(): void { $this->instance->parse_to_legacy_array() ); } + + /** + * Tests the to array. + * + * @covers ::add_feature + * @covers ::parse_to_array + * + * @return void + */ + public function test_parse_to_array(): void { + $this->instance->add_feature( new Analysis_Feature( false, 'name-false', 'legacy-key-false' ) ); + $this->instance->add_feature( new Analysis_Feature( true, 'name-true', 'legacy-key-true' ) ); + $this->assertSame( + [ + 'name-false' => false, + 'name-true' => true, + ], + $this->instance->to_array() + ); + } + + /** + * Set up the test. + * + * @return void + */ + protected function set_up(): void { + parent::set_up(); + $this->instance = new Analysis_Features_List(); + } } diff --git a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php index 1c21f9435d8..e40003f3148 100644 --- a/tests/Unit/General/User_Interface/General_Page_Integration_Test.php +++ b/tests/Unit/General/User_Interface/General_Page_Integration_Test.php @@ -8,7 +8,7 @@ use Yoast\WP\SEO\Actions\Alert_Dismissal_Action; use Yoast\WP\SEO\Conditionals\Admin\Non_Network_Admin_Conditional; use Yoast\WP\SEO\Conditionals\Admin_Conditional; -use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; +use Yoast\WP\SEO\Dashboard\Application\Configuration\Dashboard_Configuration; use Yoast\WP\SEO\General\User_Interface\General_Page_Integration; use Yoast\WP\SEO\Helpers\Current_Page_Helper; use Yoast\WP\SEO\Helpers\Notification_Helper; @@ -76,11 +76,11 @@ final class General_Page_Integration_Test extends TestCase { private $promotion_manager; /** - * Holds the content types repository. + * Holds the dashboard configuration. * - * @var Mockery\MockInterface|Content_Types_Repository + * @var Mockery\MockInterface|Dashboard_Configuration */ - private $content_types_repository; + private $dashboard_configuration; /** * The class under test. @@ -97,14 +97,14 @@ final class General_Page_Integration_Test extends TestCase { public function set_up() { $this->stubTranslationFunctions(); - $this->asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); - $this->current_page_helper = Mockery::mock( Current_Page_Helper::class ); - $this->product_helper = Mockery::mock( Product_Helper::class ); - $this->shortlink_helper = Mockery::mock( Short_Link_Helper::class ); - $this->notifications_helper = Mockery::mock( Notification_Helper::class ); - $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); - $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); - $this->content_types_repository = Mockery::mock( Content_Types_Repository::class ); + $this->asset_manager = Mockery::mock( WPSEO_Admin_Asset_Manager::class ); + $this->current_page_helper = Mockery::mock( Current_Page_Helper::class ); + $this->product_helper = Mockery::mock( Product_Helper::class ); + $this->shortlink_helper = Mockery::mock( Short_Link_Helper::class ); + $this->notifications_helper = Mockery::mock( Notification_Helper::class ); + $this->alert_dismissal_action = Mockery::mock( Alert_Dismissal_Action::class ); + $this->promotion_manager = Mockery::mock( Promotion_Manager::class ); + $this->dashboard_configuration = Mockery::mock( Dashboard_Configuration::class ); $this->instance = new General_Page_Integration( $this->asset_manager, @@ -114,7 +114,7 @@ public function set_up() { $this->notifications_helper, $this->alert_dismissal_action, $this->promotion_manager, - $this->content_types_repository + $this->dashboard_configuration ); } @@ -136,7 +136,7 @@ public function test_construct() { $this->notifications_helper, $this->alert_dismissal_action, $this->promotion_manager, - $this->content_types_repository + $this->dashboard_configuration ) ); } @@ -342,8 +342,8 @@ public function expect_get_script_data() { ->once() ->andReturn( [] ); - $this->content_types_repository - ->expects( 'get_content_types' ) + $this->dashboard_configuration + ->expects( 'get_configuration' ) ->once() ->andReturn( [] ); diff --git a/tests/Unit/Helpers/User_Helper_Test.php b/tests/Unit/Helpers/User_Helper_Test.php index f461ad81c52..48632ec35b5 100644 --- a/tests/Unit/Helpers/User_Helper_Test.php +++ b/tests/Unit/Helpers/User_Helper_Test.php @@ -3,6 +3,7 @@ namespace Yoast\WP\SEO\Tests\Unit\Helpers; use Brain\Monkey\Functions; +use WP_User; use Yoast\WP\SEO\Helpers\User_Helper; use Yoast\WP\SEO\Tests\Unit\TestCase; @@ -139,4 +140,42 @@ public function test_delete_meta() { $this->assertTrue( $this->instance->delete_meta( 1, 'key', 'value' ) ); } + + /** + * Tests that get_current_user_display_name return the display_name if its there or an empty string. + * + * @covers ::get_current_user_display_name + * + * @dataProvider current_user_display_name_provider + * + * @param WP_User|null $user The user. + * @param string $expected_display_name The expected display name. + * + * @return void + */ + public function test_get_current_user_display_name( $user, $expected_display_name ) { + Functions\expect( 'wp_get_current_user' ) + ->once() + ->andReturn( $user ); + + $this->assertSame( $expected_display_name, $this->instance->get_current_user_display_name() ); + } + + /** + * Data provider for current_user_display_name_provider test. + * + * @return array> + */ + public static function current_user_display_name_provider() { + $user1 = new WP_User(); + $user1->display_name = 'admin'; + $user2 = new WP_User(); + $user2->display_name = 'First_name'; + + return [ + [ $user1, 'admin' ], + [ $user2, 'First_name' ], + [ null, '' ], + ]; + } } From ef64321ee00383028819664fac6836980060bedb Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 15 Nov 2024 16:47:41 +0200 Subject: [PATCH 025/132] Create API endpoint for SEO scores --- .../content-types-repository.php | 30 +- .../domain/content-types/content-type.php | 13 +- .../content-types/content-types-collector.php | 48 ++++ .../user-interface/seo-scores-route.php | 263 ++++++++++++++++++ 4 files changed, 337 insertions(+), 17 deletions(-) create mode 100644 src/dashboard/infrastructure/content-types/content-types-collector.php create mode 100644 src/dashboard/user-interface/seo-scores-route.php diff --git a/src/dashboard/application/content-types/content-types-repository.php b/src/dashboard/application/content-types/content-types-repository.php index dd77cafd1dd..0424194a50a 100644 --- a/src/dashboard/application/content-types/content-types-repository.php +++ b/src/dashboard/application/content-types/content-types-repository.php @@ -4,9 +4,8 @@ namespace Yoast\WP\SEO\Dashboard\Application\Content_Types; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Types_List; -use Yoast\WP\SEO\Helpers\Post_Type_Helper; +use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; /** * The repository to get content types. @@ -16,9 +15,9 @@ class Content_Types_Repository { /** * The post type helper. * - * @var Post_Type_Helper + * @var Content_Types_Collector */ - protected $post_type_helper; + protected $content_types_collector; /** * The content types list. @@ -37,18 +36,18 @@ class Content_Types_Repository { /** * The constructor. * - * @param Post_Type_Helper $post_type_helper The post type helper. - * @param Content_Types_List $content_types_list The content types list. - * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. + * @param Content_Types_Collector $content_types_collector The post type helper. + * @param Content_Types_List $content_types_list The content types list. + * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. */ public function __construct( - Post_Type_Helper $post_type_helper, + Content_Types_Collector $content_types_collector, Content_Types_List $content_types_list, Taxonomies_Repository $taxonomies_repository ) { - $this->post_type_helper = $post_type_helper; - $this->content_types_list = $content_types_list; - $this->taxonomies_repository = $taxonomies_repository; + $this->content_types_collector = $content_types_collector; + $this->content_types_list = $content_types_list; + $this->taxonomies_repository = $taxonomies_repository; } /** @@ -57,13 +56,12 @@ public function __construct( * @return array>>>> The content types array. */ public function get_content_types(): array { - $post_types = $this->post_type_helper->get_indexable_post_types(); + $content_types = $this->content_types_collector->get_content_types(); - foreach ( $post_types as $post_type ) { - $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. - $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $post_type_object->name ); + foreach ( $content_types as $content_type ) { + $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + $content_type->set_taxonomy( $content_type_taxonomy ); - $content_type = new Content_Type( $post_type_object->name, $post_type_object->label, $content_type_taxonomy ); $this->content_types_list->add( $content_type ); } diff --git a/src/dashboard/domain/content-types/content-type.php b/src/dashboard/domain/content-types/content-type.php index c1797868df1..275e40ef5c4 100644 --- a/src/dashboard/domain/content-types/content-type.php +++ b/src/dashboard/domain/content-types/content-type.php @@ -37,7 +37,7 @@ class Content_Type { * @param string $label The label of the content type. * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. */ - public function __construct( string $name, string $label, ?Taxonomy $taxonomy ) { + public function __construct( string $name, string $label, ?Taxonomy $taxonomy = null ) { $this->name = $name; $this->label = $label; $this->taxonomy = $taxonomy; @@ -69,4 +69,15 @@ public function get_label(): string { public function get_taxonomy(): ?Taxonomy { return $this->taxonomy; } + + /** + * Sets the taxonomy that filters the content type. + * + * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. + * + * @return void + */ + public function set_taxonomy( ?Taxonomy $taxonomy ): void { + $this->taxonomy = $taxonomy; + } } diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php new file mode 100644 index 00000000000..99244c405c2 --- /dev/null +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -0,0 +1,48 @@ +post_type_helper = $post_type_helper; + } + + /** + * Returns the content types array. + * + * @return array>>>> The content types array. + */ + public function get_content_types(): array { + $content_types = []; + $post_types = $this->post_type_helper->get_indexable_post_types(); + + foreach ( $post_types as $post_type ) { + $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. + + $content_types[ $post_type_object->name ] = new Content_Type( $post_type_object->name, $post_type_object->label ); + } + + return $content_types; + } +} diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/seo-scores-route.php new file mode 100644 index 00000000000..202845a884e --- /dev/null +++ b/src/dashboard/user-interface/seo-scores-route.php @@ -0,0 +1,263 @@ +content_types_collector = $content_types_collector; + $this->indexable_repository = $indexable_repository; + $this->wpdb = $wpdb; + } + + /** + * Registers routes with WordPress. + * + * @return void + */ + public function register_routes() { + \register_rest_route( + Main::API_V1_NAMESPACE, + self::ROUTE_PREFIX, + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_seo_scores' ], + 'permission_callback' => [ $this, 'permission_manage_options' ], + 'args' => [ + 'contentType' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => [ $this, 'validate_content_type' ], + ], + 'term' => [ + 'required' => false, + 'type' => 'integer', + 'default' => 0, + 'sanitize_callback' => static function ( $param ) { + return \intval( $param ); + }, + 'validate_callback' => [ $this, 'validate_term' ], + ], + 'taxonomy' => [ + 'required' => false, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => [ $this, 'validate_taxonomy' ], + ], + ], + ], + ] + ); + } + + /** + * Sets the value of the wistia embed permission. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response|WP_Error The success or failure response. + */ + public function get_seo_scores( WP_REST_Request $request ) { + $content_type = $request['contentType']; + + $selects = [ + 'needs_improvement' => 'COUNT(CASE WHEN primary_focus_keyword_score < 41 THEN 1 END)', + 'ok' => 'COUNT(CASE WHEN primary_focus_keyword_score >= 41 AND primary_focus_keyword_score < 70 THEN 1 END)', + 'good' => 'COUNT(CASE WHEN primary_focus_keyword_score >= 71 THEN 1 END)', + 'not_analyzed' => 'COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END)', + ]; + + if ( $request['term'] === 0 || $request['taxonomy'] === '' ) { + // Without taxonomy filtering. + $counts = $this->indexable_repository->query() + ->select_many_expr( $selects ) + ->where_raw( '( post_status = \'publish\' OR post_status IS NULL )' ) + ->where_in( 'object_type', [ 'post' ] ) + ->where_in( 'object_sub_type', [ $content_type ] ) + ->find_one(); + + // This results in: + // SELECT + // COUNT(CASE WHEN primary_focus_keyword_score < 41 THEN 1 END) AS `needs_improvement`, + // COUNT(CASE WHEN primary_focus_keyword_score >= 41 AND primary_focus_keyword_score < 70 THEN 1 END) AS `ok`, + // COUNT(CASE WHEN primary_focus_keyword_score >= 71 THEN 1 END) AS `good`, + // COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END) AS `not_analyzed` + // FROM `wp_yoast_indexable` + // WHERE ( post_status = 'publish' OR post_status IS NULL ) + // AND `object_type` IN ('post') + // AND `object_sub_type` IN ('post') + // LIMIT 1 + + } + else { + // With taxonomy filtering. + $query = $this->wpdb->prepare( + " + SELECT + COUNT(CASE WHEN I.primary_focus_keyword_score < 41 THEN 1 END) AS `needs_improvement`, + COUNT(CASE WHEN I.primary_focus_keyword_score >= 41 AND I.primary_focus_keyword_score < 70 THEN 1 END) AS `ok`, + COUNT(CASE WHEN I.primary_focus_keyword_score >= 70 THEN 1 END) AS `good`, + COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS `not_analyzed` + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s) + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id IN ( + SELECT term_taxonomy_id + FROM + %i + WHERE + term_id = %d + AND taxonomy = %s + ) + )", + Model::get_table_name( 'Indexable' ), + $content_type, + $this->wpdb->term_relationships, + $this->wpdb->term_taxonomy, + $request['term'], + $request['taxonomy'] + ); + + $counts = $this->wpdb->get_row( $query ); + } + + return new WP_REST_Response( + [ + 'json' => (object) [ + 'good' => $counts->good, + 'ok' => $counts->ok, + 'needs_improvement' => $counts->needs_improvement, + 'not_analyzed' => $counts->not_analyzed, + ], + ], + 200 + ); + } + + /** + * Validates the content type against the content types collector. + * + * @param string $content_type The content type. + * + * @return bool Whether the content type passed validation. + */ + public function validate_content_type( $content_type ) { + // @TODO: Is it necessary to go through all the indexable content types again and validate against those? If so, it can look like this. + $content_types = $this->content_types_collector->get_content_types(); + + if ( isset( $content_types[ $content_type ] ) ) { + return true; + } + + return false; + } + + /** + * Validates the taxonomy against the given content type. + * + * @param string $taxonomy The taxonomy. + * @param WP_REST_Request $request The request object. + * + * @return bool Whether the taxonomy passed validation. + */ + public function validate_taxonomy( $taxonomy, $request ) { + // @TODO: Is it necessary to validate against content types? If so, it can take inspiration from validate_content_type(). + return true; + } + + /** + * Validates the term against the given content type. + * + * @param int $term_id The term ID. + * @param WP_REST_Request $request The request object. + * + * @return bool Whether the term passed validation. + */ + public function validate_term( $term_id, $request ) { + // @TODO: Is it necessary to validate against content types? If so, it can look like this. + if ( $request['term'] === 0 ) { + return true; + } + + $term = \get_term( $term_id ); + if ( ! $term || \is_wp_error( $term ) ) { + return false; + } + + $post_type = $request['contentType']; + + // Check if the term's taxonomy is associated with the post type. + return \in_array( $term->taxonomy, \get_object_taxonomies( $post_type ), true ); + } + + /** + * Permission callback. + * + * @return bool True when user has the 'wpseo_manage_options' capability. + */ + public function permission_manage_options() { + return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + } +} From 0e6d6818367eda63ca0fbf96e03bc189b0dd3744 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Wed, 13 Nov 2024 13:09:59 +0100 Subject: [PATCH 026/132] Adds score list component. This is now based on two fake score objects. --- .../components/content-status-description.js | 15 +++ packages/js/src/dash/components/page-title.js | 2 +- packages/js/src/dash/components/score-list.js | 20 ++++ packages/js/src/dash/components/scores.js | 96 +++++++++++++++++++ packages/js/src/dash/components/seo-scores.js | 8 +- packages/js/src/dash/index.js | 16 ++++ packages/js/src/dash/util/scores.js | 23 +++++ packages/tailwindcss-preset/index.js | 6 ++ 8 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 packages/js/src/dash/components/content-status-description.js create mode 100644 packages/js/src/dash/components/score-list.js create mode 100644 packages/js/src/dash/components/scores.js create mode 100644 packages/js/src/dash/util/scores.js diff --git a/packages/js/src/dash/components/content-status-description.js b/packages/js/src/dash/components/content-status-description.js new file mode 100644 index 00000000000..7b444c91342 --- /dev/null +++ b/packages/js/src/dash/components/content-status-description.js @@ -0,0 +1,15 @@ +import { __ } from "@wordpress/i18n"; +/** + * @type {import("../index").Scores} Scores + */ + +/** + * @param {Scores|null} scores The SEO scores. + * @returns {JSX.Element} + */ +export const ContentStatusDescription = ( { scores } ) => { + if ( ! scores ) { + return

    { __( "No scores could be retrieved Or maybe loading??", "wordpress-seo" ) }

    ; + } + return

    { __( "description placeholder", "wordpress-seo" ) }

    ; +}; diff --git a/packages/js/src/dash/components/page-title.js b/packages/js/src/dash/components/page-title.js index bcbad90630d..81bcf0ad6ef 100644 --- a/packages/js/src/dash/components/page-title.js +++ b/packages/js/src/dash/components/page-title.js @@ -16,7 +16,7 @@ export const PageTitle = ( { userName } ) => ( ) }

    - { __( "Welcome to your SEO dashboard! Don't forget to check it regularly to see how your site is performing and if there are any important tasks waiting for you.", "wordpress-seo" ) } + { __( "Welcome to your SEO dash!", "wordpress-seo" ) }

    diff --git a/packages/js/src/dash/components/score-list.js b/packages/js/src/dash/components/score-list.js new file mode 100644 index 00000000000..c12a300d17b --- /dev/null +++ b/packages/js/src/dash/components/score-list.js @@ -0,0 +1,20 @@ +import { SCORES } from "../util/scores"; +import { Badge, Button } from "@yoast/ui-library"; +/** + * @type {import("../index").Scores} Scores + */ +/** + * @param {Scores} scores The scores. + * @returns {JSX.Element} The element. + */ +export const ScoreList = ( { scores } ) => { + return
      + { Object.values( scores ).map( ( score ) =>{ + return
    • + + { SCORES[ score.name ].label } { score.amount } + { score.links.view && } +
    • ; + } ) } +
    ; +}; diff --git a/packages/js/src/dash/components/scores.js b/packages/js/src/dash/components/scores.js new file mode 100644 index 00000000000..fcb6208ebbd --- /dev/null +++ b/packages/js/src/dash/components/scores.js @@ -0,0 +1,96 @@ +import { useEffect, useState } from "@wordpress/element"; +import { ContentStatusDescription } from "./content-status-description"; +import { ScoreList } from "./score-list"; + +/** + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Term} Term + * @type {import("../index").Scores} Scores + */ + +/** @type {Scores} **/ +const fakeScores = { + ok: { + name: "ok", + amount: 6, + links: { + view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", + }, + }, + good: { + name: "good", + amount: 6, + links: { + view: null, + }, + }, + bad: { + name: "bad", + amount: 6, + links: { + view: null, + }, + }, + notAnalyzed: { + name: "notAnalyzed", + amount: 6, + links: { + view: null, + }, + }, +}; +const fakeScores2 = { + ok: { + name: "ok", + amount: 7, + links: { + view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", + }, + }, + good: { + name: "good", + amount: 12, + links: { + view: null, + }, + }, + bad: { + name: "bad", + amount: 0, + links: { + view: null, + }, + }, + notAnalyzed: { + name: "notAnalyzed", + amount: 0, + links: { + view: null, + }, + }, +}; + +/** + * @param {ContentType} contentType The selected contentType. + * @param {Term?} [term] The selected term. + * @returns {JSX.Element} The element. + */ +export const Scores = ( { contentType, term } ) => { + const [ scores, setScores ] = useState(); + useEffect( () => { + const rand = Math.random(); + if ( rand < 0.5 ) { + setScores( fakeScores ); + } else { + setScores( fakeScores2 ); + } + }, [ contentType.name, term?.name ] ); + + return <> + +
    + { scores && } +
    chart
    +
    + ; +}; diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index ba94a4f70cb..cc23fd0d9a9 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -3,6 +3,7 @@ import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; import PropTypes from "prop-types"; import { ContentTypeFilter } from "./content-type-filter"; +import { Scores } from "./scores"; import { TermFilter } from "./term-filter"; /** @@ -11,6 +12,7 @@ import { TermFilter } from "./term-filter"; * @type {import("../index").Term} Term */ + /** * @param {ContentType[]} contentTypes The content types. May not be empty. * @returns {JSX.Element} The element. @@ -38,11 +40,7 @@ export const SeoScores = ( { contentTypes } ) => { /> }
    -

    { __( "description", "wordpress-seo" ) }

    -
    -
    Scores
    -
    chart
    -
    + ); }; diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js index 6c5a50a4f38..ee978ca03a2 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dash/index.js @@ -20,3 +20,19 @@ export { Dashboard } from "./components/dashboard"; * @property {string} name The unique identifier. * @property {string} label The user-facing label. */ + +/** + * @typedef {Object} Scores A set of Score objects. + * @property {Score} ok Ok score. + * @property {Score} good Good score. + * @property {Score} bad Bad score. + * @property {Score} notAnalyzed Not analyzed score. + */ + +/** + * @typedef {Object} Score A set of Score objects. + * @property {string} name The name of the score. + * @property {number} amount The amount of content for this score. + * @property {Object} links The links. + * @property {string} [links.view] The view link, might not exist. + */ diff --git a/packages/js/src/dash/util/scores.js b/packages/js/src/dash/util/scores.js new file mode 100644 index 00000000000..83fc77105ae --- /dev/null +++ b/packages/js/src/dash/util/scores.js @@ -0,0 +1,23 @@ +import { __ } from "@wordpress/i18n"; +export const SCORES = { + good: { + label: __( "Good", "wordpress-seo" ), + color: "yst-bg-analysis-good", + hex: "#7ad03a", + }, + ok: { + label: __( "OK", "wordpress-seo" ), + color: "yst-bg-analysis-ok", + hex: "#ee7c1b", + }, + bad: { + label: __( "Needs improvement", "wordpress-seo" ), + color: "yst-bg-analysis-bad", + hex: "#dc3232", + }, + notAnalyzed: { + label: __( "Not analyzed", "wordpress-seo" ), + color: "yst-bg-analysis-na", + hex: "#cbd5e1", + }, +}; diff --git a/packages/tailwindcss-preset/index.js b/packages/tailwindcss-preset/index.js index 5d8cbf8729b..f1bcab23622 100644 --- a/packages/tailwindcss-preset/index.js +++ b/packages/tailwindcss-preset/index.js @@ -31,6 +31,12 @@ module.exports = { 800: "#83084e", 900: "#770045", }, + analysis: { + good: "#7ad03a", + ok: "#ee7c1b", + bad: "#dc3232", + na: "#cbd5e1", + }, }, strokeWidth: { 3: "3px", From 8e002b81769544baef70a53b69aef7f9258ac7e8 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Wed, 13 Nov 2024 14:35:10 +0100 Subject: [PATCH 027/132] Add graph components and readability scores. --- packages/js/src/dash/components/dashboard.js | 3 +- .../components/readability-score-content.js | 97 +++++++++++++++++++ .../src/dash/components/readability-scores.js | 62 ++++++++++++ .../js/src/dash/components/score-chart.js | 60 ++++++++++++ packages/js/src/dash/components/score-list.js | 2 +- .../{scores.js => seo-score-content.js} | 34 +++---- packages/js/src/dash/components/seo-scores.js | 4 +- packages/js/src/dash/util/scores.js | 12 +++ 8 files changed, 253 insertions(+), 21 deletions(-) create mode 100644 packages/js/src/dash/components/readability-score-content.js create mode 100644 packages/js/src/dash/components/readability-scores.js create mode 100644 packages/js/src/dash/components/score-chart.js rename packages/js/src/dash/components/{scores.js => seo-score-content.js} (85%) diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dashboard.js index 47752fd28cf..a5ae96927f2 100644 --- a/packages/js/src/dash/components/dashboard.js +++ b/packages/js/src/dash/components/dashboard.js @@ -1,5 +1,6 @@ import PropTypes from "prop-types"; import { PageTitle } from "./page-title"; +import { ReadabilityScores } from "./readability-scores"; import { SeoScores } from "./seo-scores"; /** @@ -18,7 +19,7 @@ export const Dashboard = ( { contentTypes, userName } ) => {
    - +
); diff --git a/packages/js/src/dash/components/readability-score-content.js b/packages/js/src/dash/components/readability-score-content.js new file mode 100644 index 00000000000..2b0013e0d0a --- /dev/null +++ b/packages/js/src/dash/components/readability-score-content.js @@ -0,0 +1,97 @@ +import { useEffect, useState } from "@wordpress/element"; +import { ScoreChart } from "./score-chart"; +import { ContentStatusDescription } from "./content-status-description"; +import { ScoreList } from "./score-list"; + +/** + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Term} Term + * @type {import("../index").Scores} Scores + */ + +/** @type {Scores} **/ +const fakeScores = [ + { + name: "good", + amount: 6, + links: { + view: null, + }, + }, + { + name: "ok", + amount: 6, + links: { + view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", + }, + }, + { + name: "bad", + amount: 6, + links: { + view: null, + }, + }, + { + name: "notAnalyzed", + amount: 6, + links: { + view: null, + }, + }, +]; +const fakeScores2 = [ + { + name: "ok", + amount: 7, + links: { + view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", + }, + }, + { + name: "good", + amount: 12, + links: { + view: null, + }, + }, + { + name: "bad", + amount: 1, + links: { + view: null, + }, + }, + { + name: "notAnalyzed", + amount: 2, + links: { + view: null, + }, + }, +]; + +/** + * @param {ContentType} contentType The selected contentType. + * @param {Term?} [term] The selected term. + * @returns {JSX.Element} The element. + */ +export const ReadabilityScoreContent = ( { contentType, term } ) => { + const [ scores, setScores ] = useState(); + useEffect( () => { + const rand = Math.random(); + if ( rand < 0.5 ) { + setScores( fakeScores ); + } else { + setScores( fakeScores2 ); + } + }, [ contentType.name, term?.name ] ); + + return <> + +
+ { scores && } + { scores && } +
+ ; +}; diff --git a/packages/js/src/dash/components/readability-scores.js b/packages/js/src/dash/components/readability-scores.js new file mode 100644 index 00000000000..d8591e9d9f0 --- /dev/null +++ b/packages/js/src/dash/components/readability-scores.js @@ -0,0 +1,62 @@ +import { useState } from "@wordpress/element"; +import { __ } from "@wordpress/i18n"; +import { Paper, Title } from "@yoast/ui-library"; +import PropTypes from "prop-types"; +import { ContentTypeFilter } from "./content-type-filter"; +import { ReadabilityScoreContent } from "./readability-score-content"; +import { TermFilter } from "./term-filter"; + +/** + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Taxonomy} Taxonomy + * @type {import("../index").Term} Term + */ + + +/** + * @param {ContentType[]} contentTypes The content types. May not be empty. + * @returns {JSX.Element} The element. + */ +export const ReadabilityScores = ( { contentTypes } ) => { + const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); + const [ selectedTerm, setSelectedTerm ] = useState(); + + return ( + + { __( "Readability scores", "wordpress-seo" ) } +
+ + { selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search && + + } +
+ +
+ ); +}; + +ReadabilityScores.propTypes = { + contentTypes: PropTypes.arrayOf( + PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + taxonomy: PropTypes.shape( { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + links: PropTypes.shape( { + search: PropTypes.string, + } ), + } ), + } ) + ).isRequired, +}; diff --git a/packages/js/src/dash/components/score-chart.js b/packages/js/src/dash/components/score-chart.js new file mode 100644 index 00000000000..066cf47e5b6 --- /dev/null +++ b/packages/js/src/dash/components/score-chart.js @@ -0,0 +1,60 @@ +import { ArcElement, Chart, Tooltip } from "chart.js"; +import { Doughnut } from "react-chartjs-2"; +import { getHex, getLabels } from "../util/scores"; + +Chart.register( ArcElement, Tooltip ); +/** + * @type {import("../index").Scores} Scores + */ + +/** + * @param {Scores} scores The scores. + * @returns {Object} Parsed chart data. + */ +const transformScoresToGraphData = ( scores ) => ( { + labels: getLabels(), + datasets: [ + { + cutout: "82%", + data: Object.entries( scores ).map( ( [ , { amount } ] ) => amount ), + backgroundColor: getHex(), + borderColor: getHex(), + borderWidth: 1, + offset: 1, + hoverOffset: 5, + spacing: 1, + weight: 1, + animation: { + animateRotate: true, + }, + }, + ], +} ); + +const chartOptions = { + plugins: { + responsive: true, + legend: false, + tooltip: { + displayColors: false, + callbacks: { + title: () => "", + label: context => `${ context.label }: ${ context?.formattedValue }`, + }, + }, + }, +}; +/** + * + * @param {Scores} scores The scores. + * @returns {JSX.Element} The element. + */ +export const ScoreChart = ( { scores } )=> { + return
+ +
+ ; +}; diff --git a/packages/js/src/dash/components/score-list.js b/packages/js/src/dash/components/score-list.js index c12a300d17b..d1e0427abca 100644 --- a/packages/js/src/dash/components/score-list.js +++ b/packages/js/src/dash/components/score-list.js @@ -12,7 +12,7 @@ export const ScoreList = ( { scores } ) => { { Object.values( scores ).map( ( score ) =>{ return
  • - { SCORES[ score.name ].label } { score.amount } + { SCORES[ score.name ].label } { score.amount } { score.links.view && }
  • ; } ) } diff --git a/packages/js/src/dash/components/scores.js b/packages/js/src/dash/components/seo-score-content.js similarity index 85% rename from packages/js/src/dash/components/scores.js rename to packages/js/src/dash/components/seo-score-content.js index fcb6208ebbd..9f3eba5a0db 100644 --- a/packages/js/src/dash/components/scores.js +++ b/packages/js/src/dash/components/seo-score-content.js @@ -1,4 +1,5 @@ import { useEffect, useState } from "@wordpress/element"; +import { ScoreChart } from "./score-chart"; import { ContentStatusDescription } from "./content-status-description"; import { ScoreList } from "./score-list"; @@ -9,73 +10,72 @@ import { ScoreList } from "./score-list"; */ /** @type {Scores} **/ -const fakeScores = { - ok: { +const fakeScores = [ + { name: "ok", amount: 6, links: { view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", }, }, - good: { + { name: "good", amount: 6, links: { view: null, }, }, - bad: { + { name: "bad", amount: 6, links: { view: null, }, }, - notAnalyzed: { + { name: "notAnalyzed", amount: 6, links: { view: null, }, }, -}; -const fakeScores2 = { - ok: { +]; +const fakeScores2 = [ + { name: "ok", amount: 7, links: { view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", }, }, - good: { + { name: "good", amount: 12, links: { view: null, }, }, - bad: { + { name: "bad", - amount: 0, + amount: 1, links: { view: null, }, }, - notAnalyzed: { + { name: "notAnalyzed", - amount: 0, + amount: 2, links: { view: null, }, }, -}; - +]; /** * @param {ContentType} contentType The selected contentType. * @param {Term?} [term] The selected term. * @returns {JSX.Element} The element. */ -export const Scores = ( { contentType, term } ) => { +export const SeoScoreContent = ( { contentType, term } ) => { const [ scores, setScores ] = useState(); useEffect( () => { const rand = Math.random(); @@ -90,7 +90,7 @@ export const Scores = ( { contentType, term } ) => {
    { scores && } -
    chart
    + { scores && }
    ; }; diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index cc23fd0d9a9..1d869efcd55 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -3,7 +3,7 @@ import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; import PropTypes from "prop-types"; import { ContentTypeFilter } from "./content-type-filter"; -import { Scores } from "./scores"; +import { SeoScoreContent } from "./seo-score-content"; import { TermFilter } from "./term-filter"; /** @@ -40,7 +40,7 @@ export const SeoScores = ( { contentTypes } ) => { /> } - + ); }; diff --git a/packages/js/src/dash/util/scores.js b/packages/js/src/dash/util/scores.js index 83fc77105ae..67377b3bde8 100644 --- a/packages/js/src/dash/util/scores.js +++ b/packages/js/src/dash/util/scores.js @@ -21,3 +21,15 @@ export const SCORES = { hex: "#cbd5e1", }, }; + +export const getLabels = () => { + return Object.values( SCORES ).map( ( value ) =>{ + return value.label; + } ); +}; + +export const getHex = () => { + return Object.values( SCORES ).map( ( value ) =>{ + return value.hex; + } ); +}; From bd65d5b5ef1854171c8c538fbcfeff272c2c0b0b Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 07:57:10 +0100 Subject: [PATCH 028/132] Rename to dash --- packages/js/src/dash/components/{dashboard.js => dash.js} | 4 ++-- packages/js/src/dash/index.js | 2 +- packages/js/src/general/initialize.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename packages/js/src/dash/components/{dashboard.js => dash.js} (92%) diff --git a/packages/js/src/dash/components/dashboard.js b/packages/js/src/dash/components/dash.js similarity index 92% rename from packages/js/src/dash/components/dashboard.js rename to packages/js/src/dash/components/dash.js index a5ae96927f2..03a21555137 100644 --- a/packages/js/src/dash/components/dashboard.js +++ b/packages/js/src/dash/components/dash.js @@ -13,7 +13,7 @@ import { SeoScores } from "./seo-scores"; * @param {string} userName The user name. * @returns {JSX.Element} The element. */ -export const Dashboard = ( { contentTypes, userName } ) => { +export const Dash = ( { contentTypes, userName } ) => { return (
    @@ -25,7 +25,7 @@ export const Dashboard = ( { contentTypes, userName } ) => { ); }; -Dashboard.propTypes = { +Dash.propTypes = { contentTypes: PropTypes.arrayOf( PropTypes.shape( { name: PropTypes.string.isRequired, diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js index ee978ca03a2..3ad1743758d 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dash/index.js @@ -1,4 +1,4 @@ -export { Dashboard } from "./components/dashboard"; +export { Dash } from "./components/dash"; /** * @typedef {Object} Taxonomy A taxonomy. diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 2e574cf45b8..f08aeef01d2 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -5,7 +5,7 @@ import { render } from "@wordpress/element"; import { Root } from "@yoast/ui-library"; import { get } from "lodash"; import { createHashRouter, createRoutesFromElements, Navigate, Route, RouterProvider } from "react-router-dom"; -import { Dashboard } from "../dash"; +import { Dash } from "../dash"; import { LINK_PARAMS_NAME } from "../shared-admin/store"; import App from "./app"; import { RouteErrorFallback } from "./components"; @@ -40,7 +40,7 @@ domReady( () => { } errorElement={ }> } errorElement={ } + element={ } errorElement={ } /> } errorElement={ } /> } errorElement={ } /> From 2bd0257a062d6114fd8a680938f5a1130740f48e Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:33:47 +0100 Subject: [PATCH 029/132] Refactor score to array * the scores is an array so the backend can control the order and the frontend does not need to sort * honor the order in the chart labels and colors --- .../components/content-status-description.js | 7 +- .../components/readability-score-content.js | 12 ++-- .../js/src/dash/components/score-chart.js | 69 ++++++++++--------- packages/js/src/dash/components/score-list.js | 31 +++++---- .../src/dash/components/seo-score-content.js | 13 ++-- packages/js/src/dash/index.js | 12 +--- .../dash/util/{scores.js => score-meta.js} | 15 +--- 7 files changed, 77 insertions(+), 82 deletions(-) rename packages/js/src/dash/util/{scores.js => score-meta.js} (65%) diff --git a/packages/js/src/dash/components/content-status-description.js b/packages/js/src/dash/components/content-status-description.js index 7b444c91342..4ef7504a937 100644 --- a/packages/js/src/dash/components/content-status-description.js +++ b/packages/js/src/dash/components/content-status-description.js @@ -1,11 +1,12 @@ import { __ } from "@wordpress/i18n"; + /** - * @type {import("../index").Scores} Scores + * @type {import("../index").Score} Score */ /** - * @param {Scores|null} scores The SEO scores. - * @returns {JSX.Element} + * @param {Score[]|null} scores The SEO scores. + * @returns {JSX.Element} The element. */ export const ContentStatusDescription = ( { scores } ) => { if ( ! scores ) { diff --git a/packages/js/src/dash/components/readability-score-content.js b/packages/js/src/dash/components/readability-score-content.js index 2b0013e0d0a..373ff16b6f0 100644 --- a/packages/js/src/dash/components/readability-score-content.js +++ b/packages/js/src/dash/components/readability-score-content.js @@ -1,26 +1,26 @@ import { useEffect, useState } from "@wordpress/element"; -import { ScoreChart } from "./score-chart"; import { ContentStatusDescription } from "./content-status-description"; +import { ScoreChart } from "./score-chart"; import { ScoreList } from "./score-list"; /** * @type {import("../index").ContentType} ContentType * @type {import("../index").Term} Term - * @type {import("../index").Scores} Scores + * @type {import("../index").Score} Score */ -/** @type {Scores} **/ +/** @type {Score[]} **/ const fakeScores = [ { name: "good", - amount: 6, + amount: 4, links: { view: null, }, }, { name: "ok", - amount: 6, + amount: 5, links: { view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", }, @@ -34,7 +34,7 @@ const fakeScores = [ }, { name: "notAnalyzed", - amount: 6, + amount: 7, links: { view: null, }, diff --git a/packages/js/src/dash/components/score-chart.js b/packages/js/src/dash/components/score-chart.js index 066cf47e5b6..077cd3e34b7 100644 --- a/packages/js/src/dash/components/score-chart.js +++ b/packages/js/src/dash/components/score-chart.js @@ -1,35 +1,40 @@ import { ArcElement, Chart, Tooltip } from "chart.js"; import { Doughnut } from "react-chartjs-2"; -import { getHex, getLabels } from "../util/scores"; +import { SCORE_META } from "../util/score-meta"; -Chart.register( ArcElement, Tooltip ); /** - * @type {import("../index").Scores} Scores + * @type {import("../index").Score} Score */ +Chart.register( ArcElement, Tooltip ); + /** - * @param {Scores} scores The scores. + * @param {Score[]} scores The scores. * @returns {Object} Parsed chart data. */ -const transformScoresToGraphData = ( scores ) => ( { - labels: getLabels(), - datasets: [ - { - cutout: "82%", - data: Object.entries( scores ).map( ( [ , { amount } ] ) => amount ), - backgroundColor: getHex(), - borderColor: getHex(), - borderWidth: 1, - offset: 1, - hoverOffset: 5, - spacing: 1, - weight: 1, - animation: { - animateRotate: true, +const transformScoresToGraphData = ( scores ) => { + const hexes = scores.map( ( { name } ) => SCORE_META[ name ].hex ); + + return { + labels: scores.map( ( { name } ) => SCORE_META[ name ].label ), + datasets: [ + { + cutout: "82%", + data: scores.map( ( { amount } ) => amount ), + backgroundColor: hexes, + borderColor: hexes, + borderWidth: 1, + offset: 1, + hoverOffset: 5, + spacing: 1, + weight: 1, + animation: { + animateRotate: true, + }, }, - }, - ], -} ); + ], + }; +}; const chartOptions = { plugins: { @@ -44,17 +49,19 @@ const chartOptions = { }, }, }; + /** * - * @param {Scores} scores The scores. + * @param {Score[]} scores The scores. * @returns {JSX.Element} The element. */ -export const ScoreChart = ( { scores } )=> { - return
    - -
    - ; +export const ScoreChart = ( { scores } ) => { + return ( +
    + +
    + ); }; diff --git a/packages/js/src/dash/components/score-list.js b/packages/js/src/dash/components/score-list.js index d1e0427abca..d2da6d1f3ab 100644 --- a/packages/js/src/dash/components/score-list.js +++ b/packages/js/src/dash/components/score-list.js @@ -1,20 +1,25 @@ -import { SCORES } from "../util/scores"; import { Badge, Button } from "@yoast/ui-library"; +import { SCORE_META } from "../util/score-meta"; + /** - * @type {import("../index").Scores} Scores + * @type {import("../index").Score} Score */ + /** - * @param {Scores} scores The scores. + * @param {Score[]} scores The scores. * @returns {JSX.Element} The element. */ -export const ScoreList = ( { scores } ) => { - return
      - { Object.values( scores ).map( ( score ) =>{ - return
    • - - { SCORES[ score.name ].label } { score.amount } +export const ScoreList = ( { scores } ) => ( +
        + { scores.map( ( score ) => ( +
      • + + { SCORE_META[ score.name ].label } { score.amount } { score.links.view && } -
      • ; - } ) } -
      ; -}; +
    • + ) ) } +
    +); diff --git a/packages/js/src/dash/components/seo-score-content.js b/packages/js/src/dash/components/seo-score-content.js index 9f3eba5a0db..b501aac2cdf 100644 --- a/packages/js/src/dash/components/seo-score-content.js +++ b/packages/js/src/dash/components/seo-score-content.js @@ -1,26 +1,26 @@ import { useEffect, useState } from "@wordpress/element"; -import { ScoreChart } from "./score-chart"; import { ContentStatusDescription } from "./content-status-description"; +import { ScoreChart } from "./score-chart"; import { ScoreList } from "./score-list"; /** * @type {import("../index").ContentType} ContentType * @type {import("../index").Term} Term - * @type {import("../index").Scores} Scores + * @type {import("../index").Score} Score */ -/** @type {Scores} **/ +/** @type {Score[]} **/ const fakeScores = [ { name: "ok", - amount: 6, + amount: 4, links: { view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", }, }, { name: "good", - amount: 6, + amount: 5, links: { view: null, }, @@ -34,7 +34,7 @@ const fakeScores = [ }, { name: "notAnalyzed", - amount: 6, + amount: 7, links: { view: null, }, @@ -70,6 +70,7 @@ const fakeScores2 = [ }, }, ]; + /** * @param {ContentType} contentType The selected contentType. * @param {Term?} [term] The selected term. diff --git a/packages/js/src/dash/index.js b/packages/js/src/dash/index.js index 3ad1743758d..2f35b62a52b 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dash/index.js @@ -22,16 +22,8 @@ export { Dash } from "./components/dash"; */ /** - * @typedef {Object} Scores A set of Score objects. - * @property {Score} ok Ok score. - * @property {Score} good Good score. - * @property {Score} bad Bad score. - * @property {Score} notAnalyzed Not analyzed score. - */ - -/** - * @typedef {Object} Score A set of Score objects. - * @property {string} name The name of the score. + * @typedef {Object} Score A score. + * @property {string} name The name of the score. Can be "ok", "good", "bad" or "notAnalyzed". * @property {number} amount The amount of content for this score. * @property {Object} links The links. * @property {string} [links.view] The view link, might not exist. diff --git a/packages/js/src/dash/util/scores.js b/packages/js/src/dash/util/score-meta.js similarity index 65% rename from packages/js/src/dash/util/scores.js rename to packages/js/src/dash/util/score-meta.js index 67377b3bde8..ab247b0ad9b 100644 --- a/packages/js/src/dash/util/scores.js +++ b/packages/js/src/dash/util/score-meta.js @@ -1,5 +1,6 @@ import { __ } from "@wordpress/i18n"; -export const SCORES = { + +export const SCORE_META = { good: { label: __( "Good", "wordpress-seo" ), color: "yst-bg-analysis-good", @@ -21,15 +22,3 @@ export const SCORES = { hex: "#cbd5e1", }, }; - -export const getLabels = () => { - return Object.values( SCORES ).map( ( value ) =>{ - return value.label; - } ); -}; - -export const getHex = () => { - return Object.values( SCORES ).map( ( value ) =>{ - return value.hex; - } ); -}; From 460b364a37d711dbaa5b4f40b09406610ae335d5 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:34:31 +0100 Subject: [PATCH 030/132] Rename querying to pending --- packages/js/src/dash/components/term-filter.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js index 4b30fde34f4..86fbb0982de 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/components/term-filter.js @@ -35,7 +35,7 @@ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); * @returns {JSX.Element} The element. */ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { - const [ isQuerying, setIsQuerying ] = useState( false ); + const [ isPending, setIsPending ] = useState( false ); const [ error, setError ] = useState(); const [ query, setQuery ] = useState( "" ); const [ terms, setTerms ] = useState( [] ); @@ -59,7 +59,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { } } ) .finally( () => { - setIsQuerying( false ); + setIsPending( false ); } ); }, FETCH_DELAY ), [] ); @@ -76,7 +76,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { }, [] ); useEffect( () => { - setIsQuerying( true ); + setIsPending( true ); controller.current?.abort(); controller.current = new AbortController(); handleTermQuery( createQueryUrl( taxonomy.links.search, query ), { @@ -99,17 +99,17 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { nullable={ true } validation={ error } > - { isQuerying && ( + { isPending && (
    ) } - { ! isQuerying && terms.length === 0 && ( + { ! isPending && terms.length === 0 && (
    { __( "Nothing found", "wordpress-seo" ) }
    ) } - { ! isQuerying && terms.length > 0 && terms.map( ( { name, label } ) => ( + { ! isPending && terms.length > 0 && terms.map( ( { name, label } ) => ( { label } From 35976bdf199dac5f9bcd463ca0bcf5016f5632e5 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:34:54 +0100 Subject: [PATCH 031/132] Cleanup --- packages/js/src/dash/components/dash.js | 1 - packages/js/src/dash/components/readability-scores.js | 2 -- packages/js/src/dash/components/seo-scores.js | 1 - 3 files changed, 4 deletions(-) diff --git a/packages/js/src/dash/components/dash.js b/packages/js/src/dash/components/dash.js index 03a21555137..6a34fd60b06 100644 --- a/packages/js/src/dash/components/dash.js +++ b/packages/js/src/dash/components/dash.js @@ -5,7 +5,6 @@ import { SeoScores } from "./seo-scores"; /** * @type {import("../index").ContentType} ContentType - * @type {import("../index").Taxonomy} Taxonomy */ /** diff --git a/packages/js/src/dash/components/readability-scores.js b/packages/js/src/dash/components/readability-scores.js index d8591e9d9f0..ebb65969cf9 100644 --- a/packages/js/src/dash/components/readability-scores.js +++ b/packages/js/src/dash/components/readability-scores.js @@ -8,11 +8,9 @@ import { TermFilter } from "./term-filter"; /** * @type {import("../index").ContentType} ContentType - * @type {import("../index").Taxonomy} Taxonomy * @type {import("../index").Term} Term */ - /** * @param {ContentType[]} contentTypes The content types. May not be empty. * @returns {JSX.Element} The element. diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index 1d869efcd55..e4a8e0bd72f 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -12,7 +12,6 @@ import { TermFilter } from "./term-filter"; * @type {import("../index").Term} Term */ - /** * @param {ContentType[]} contentTypes The content types. May not be empty. * @returns {JSX.Element} The element. From 63c2e4c20fbfa5e50e75c563da2829ee96ff671f Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 08:37:30 +0100 Subject: [PATCH 032/132] Remove prop-types from dash We are defining our types in JSDoc, let's not duplicate our types in React PropTypes. Prop-types will be removed in the future. But TS scope is a bit too big for now. --- packages/js/.eslintrc.js | 7 +++++++ .../src/dash/components/content-type-filter.js | 16 ---------------- packages/js/src/dash/components/dash.js | 18 ------------------ packages/js/src/dash/components/page-title.js | 5 ----- .../src/dash/components/readability-scores.js | 17 ----------------- packages/js/src/dash/components/seo-scores.js | 17 ----------------- packages/js/src/dash/components/term-filter.js | 16 ---------------- 7 files changed, 7 insertions(+), 89 deletions(-) diff --git a/packages/js/.eslintrc.js b/packages/js/.eslintrc.js index 212c1cfe589..b7b83602d24 100644 --- a/packages/js/.eslintrc.js +++ b/packages/js/.eslintrc.js @@ -103,5 +103,12 @@ module.exports = { "react/display-name": 0, }, }, + // Ignore Proptypes in dash. + { + files: [ "src/dash/**/*.js" ], + rules: { + "react/prop-types": 0, + }, + }, ], }; diff --git a/packages/js/src/dash/components/content-type-filter.js b/packages/js/src/dash/components/content-type-filter.js index 940edd47719..f4cc7633ab9 100644 --- a/packages/js/src/dash/components/content-type-filter.js +++ b/packages/js/src/dash/components/content-type-filter.js @@ -1,7 +1,6 @@ import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField } from "@yoast/ui-library"; -import PropTypes from "prop-types"; /** * @typedef {import("./dashboard").ContentType} ContentType @@ -44,18 +43,3 @@ export const ContentTypeFilter = ( { idSuffix, contentTypes, selected, onChange ); }; - -ContentTypeFilter.propTypes = { - idSuffix: PropTypes.string.isRequired, - contentTypes: PropTypes.arrayOf( - PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - } ) - ).isRequired, - selected: PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - } ), - onChange: PropTypes.func.isRequired, -}; diff --git a/packages/js/src/dash/components/dash.js b/packages/js/src/dash/components/dash.js index 6a34fd60b06..c0c97505458 100644 --- a/packages/js/src/dash/components/dash.js +++ b/packages/js/src/dash/components/dash.js @@ -1,4 +1,3 @@ -import PropTypes from "prop-types"; import { PageTitle } from "./page-title"; import { ReadabilityScores } from "./readability-scores"; import { SeoScores } from "./seo-scores"; @@ -23,20 +22,3 @@ export const Dash = ( { contentTypes, userName } ) => {
    ); }; - -Dash.propTypes = { - contentTypes: PropTypes.arrayOf( - PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - taxonomy: PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - links: PropTypes.shape( { - search: PropTypes.string, - } ).isRequired, - } ), - } ) - ).isRequired, - userName: PropTypes.string.isRequired, -}; diff --git a/packages/js/src/dash/components/page-title.js b/packages/js/src/dash/components/page-title.js index 81bcf0ad6ef..78dd29e616c 100644 --- a/packages/js/src/dash/components/page-title.js +++ b/packages/js/src/dash/components/page-title.js @@ -1,6 +1,5 @@ import { __, sprintf } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import PropTypes from "prop-types"; /** * @param {string} userName The user name. @@ -21,7 +20,3 @@ export const PageTitle = ( { userName } ) => ( ); - -PageTitle.propTypes = { - userName: PropTypes.string.isRequired, -}; diff --git a/packages/js/src/dash/components/readability-scores.js b/packages/js/src/dash/components/readability-scores.js index ebb65969cf9..03e839c31a0 100644 --- a/packages/js/src/dash/components/readability-scores.js +++ b/packages/js/src/dash/components/readability-scores.js @@ -1,7 +1,6 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import PropTypes from "prop-types"; import { ContentTypeFilter } from "./content-type-filter"; import { ReadabilityScoreContent } from "./readability-score-content"; import { TermFilter } from "./term-filter"; @@ -42,19 +41,3 @@ export const ReadabilityScores = ( { contentTypes } ) => { ); }; - -ReadabilityScores.propTypes = { - contentTypes: PropTypes.arrayOf( - PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - taxonomy: PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - links: PropTypes.shape( { - search: PropTypes.string, - } ), - } ), - } ) - ).isRequired, -}; diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/components/seo-scores.js index e4a8e0bd72f..b063d0351bf 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/components/seo-scores.js @@ -1,7 +1,6 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import PropTypes from "prop-types"; import { ContentTypeFilter } from "./content-type-filter"; import { SeoScoreContent } from "./seo-score-content"; import { TermFilter } from "./term-filter"; @@ -43,19 +42,3 @@ export const SeoScores = ( { contentTypes } ) => { ); }; - -SeoScores.propTypes = { - contentTypes: PropTypes.arrayOf( - PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - taxonomy: PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - links: PropTypes.shape( { - search: PropTypes.string, - } ), - } ), - } ) - ).isRequired, -}; diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js index 86fbb0982de..159d47dbedd 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/components/term-filter.js @@ -2,7 +2,6 @@ import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Spinner } from "@yoast/ui-library"; import { debounce } from "lodash"; -import PropTypes from "prop-types"; import { FETCH_DELAY } from "../../shared-admin/constants"; import { fetchJson } from "../util/fetch-json"; @@ -117,18 +116,3 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { ); }; - -TermFilter.propTypes = { - idSuffix: PropTypes.string.isRequired, - taxonomy: PropTypes.shape( { - label: PropTypes.string.isRequired, - links: PropTypes.shape( { - search: PropTypes.string.isRequired, - } ).isRequired, - } ).isRequired, - selected: PropTypes.shape( { - name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - } ), - onChange: PropTypes.func.isRequired, -}; From 251c787807dfb22a8b9dc3de36585ac83fea0763 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 09:53:32 +0100 Subject: [PATCH 033/132] Abstract fetch to hook --- .../js/src/dash/components/term-filter.js | 55 ++++----------- packages/js/src/dash/util/fetch-json.js | 6 +- packages/js/src/dash/util/use-fetch.js | 68 +++++++++++++++++++ 3 files changed, 83 insertions(+), 46 deletions(-) create mode 100644 packages/js/src/dash/util/use-fetch.js diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/components/term-filter.js index 159d47dbedd..0ed63894bc1 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/components/term-filter.js @@ -1,9 +1,7 @@ -import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; +import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Spinner } from "@yoast/ui-library"; -import { debounce } from "lodash"; -import { FETCH_DELAY } from "../../shared-admin/constants"; -import { fetchJson } from "../util/fetch-json"; +import { useFetch } from "../util/use-fetch"; /** * @type {import("../index").Taxonomy} Taxonomy @@ -34,33 +32,13 @@ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); * @returns {JSX.Element} The element. */ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { - const [ isPending, setIsPending ] = useState( false ); - const [ error, setError ] = useState(); const [ query, setQuery ] = useState( "" ); - const [ terms, setTerms ] = useState( [] ); - /** @type {MutableRefObject} */ - const controller = useRef(); - - // This needs to be wrapped including settings the state, because the debounce return messes with the timing/events. - const handleTermQuery = useCallback( debounce( ( ...args ) => { - fetchJson( ...args ) - .then( ( result ) => { - setTerms( result.map( transformTerm ) ); - setError( undefined ); // eslint-disable-line no-undefined - } ) - .catch( ( e ) => { - // Ignore abort errors, because they are expected. - if ( e?.name !== "AbortError" ) { - setError( { - variant: "error", - message: __( "Something went wrong", "wordpress-seo" ), - } ); - } - } ) - .finally( () => { - setIsPending( false ); - } ); - }, FETCH_DELAY ), [] ); + const { data: terms = [], error, isPending } = useFetch( { + dependencies: [ taxonomy.links.search, query ], + url: createQueryUrl( taxonomy.links.search, query ), + options: { headers: { "Content-Type": "application/json" } }, + prepareData: ( result ) => result.map( transformTerm ), + } ); const handleChange = useCallback( ( value ) => { if ( value === null ) { @@ -74,18 +52,6 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { setQuery( event?.target?.value?.trim()?.toLowerCase() || "" ); }, [] ); - useEffect( () => { - setIsPending( true ); - controller.current?.abort(); - controller.current = new AbortController(); - handleTermQuery( createQueryUrl( taxonomy.links.search, query ), { - headers: { "Content-Type": "application/json" }, - signal: controller.current.signal, - } ); - - return () => controller.current?.abort(); - }, [ taxonomy.links.search, query, handleTermQuery ] ); - return ( { onQueryChange={ handleQueryChange } placeholder={ __( "All", "wordpress-seo" ) } nullable={ true } - validation={ error } + validation={ error && { + variant: "error", + message: __( "Something went wrong", "wordpress-seo" ), + } } > { isPending && (
    diff --git a/packages/js/src/dash/util/fetch-json.js b/packages/js/src/dash/util/fetch-json.js index 989c5defa44..b952721cadc 100644 --- a/packages/js/src/dash/util/fetch-json.js +++ b/packages/js/src/dash/util/fetch-json.js @@ -1,11 +1,11 @@ /** * @param {string|URL} url The URL to fetch from. - * @param {RequestInit} requestInit The request options. + * @param {RequestInit} options The request options. * @returns {Promise} The promise of a result, or an error. */ -export const fetchJson = async( url, requestInit ) => { +export const fetchJson = async( url, options ) => { try { - const response = await fetch( url, requestInit ); + const response = await fetch( url, options ); if ( ! response.ok ) { // From the perspective of the results, we want to reject this as an error. throw new Error( "Not ok" ); diff --git a/packages/js/src/dash/util/use-fetch.js b/packages/js/src/dash/util/use-fetch.js new file mode 100644 index 00000000000..cd70edd7b74 --- /dev/null +++ b/packages/js/src/dash/util/use-fetch.js @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; +import { debounce, identity } from "lodash"; +import { FETCH_DELAY } from "../../shared-admin/constants"; +import { fetchJson } from "./fetch-json"; + +/** + * @typedef {Object} FetchResult + * @property {boolean} isPending Whether the fetch is pending. + * @property {Error?} error The error, if any. + * @property {any?} data The data, if any. + */ + +/** + * @typedef {function} FetchFunction + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} options The request options. + * @returns {Promise} The promise of a result, or an error. + */ + +/** + * @param {any[]} dependencies The dependencies for the fetch. + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} options The request options. + * @param {function(any): any} [prepareData] Transforms the data before "storage". + * @param {FetchFunction} [doFetch] Fetches the data. Defaults to `fetchJson`. + * @param {number} [fetchDelay] Debounce delay for fetching. Defaults to `FETCH_DELAY`. + * @returns {FetchResult} The fetch result. + */ +export const useFetch = ( { dependencies, url, options, prepareData = identity, doFetch = fetchJson, fetchDelay = FETCH_DELAY } ) => { + const [ isPending, setIsPending ] = useState( false ); + const [ error, setError ] = useState(); + const [ data, setData ] = useState(); + /** @type {MutableRefObject} */ + const controller = useRef(); + + // This needs to be wrapped including setting the state, because the debounce return messes with the timing/events. + const handleFetch = useCallback( debounce( ( ...args ) => { + doFetch( ...args ) + .then( ( result ) => { + setData( prepareData( result ) ); + setError( undefined ); // eslint-disable-line no-undefined + } ) + .catch( ( e ) => { + // Ignore abort errors, because they are expected. + if ( e?.name !== "AbortError" ) { + setError( e ); + } + } ) + .finally( () => { + setIsPending( false ); + } ); + }, fetchDelay ), [] ); + + useEffect( () => { + setIsPending( true ); + controller.current?.abort(); + controller.current = new AbortController(); + handleFetch( url, { signal: controller.current.signal, ...options } ); + + return () => controller.current?.abort(); + }, dependencies ); + + return { + data, + error, + isPending, + }; +}; From 23fc5429be809129746e44b4252cd12feb256b70 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:48:19 +0100 Subject: [PATCH 034/132] Restructure Otherwise the components folder will get unruly --- packages/js/src/dash/components/dash.js | 4 ++-- .../js/src/dash/{util => hooks}/use-fetch.js | 19 ++++++++++++++++++- .../components/content-status-description.js | 0 .../components/content-type-filter.js | 0 .../{ => scores}/components/score-chart.js | 2 +- .../{ => scores}/components/score-list.js | 2 +- .../{ => scores}/components/term-filter.js | 2 +- .../readability}/readability-score-content.js | 6 +++--- .../readability}/readability-scores.js | 4 ++-- .../src/dash/{util => scores}/score-meta.js | 0 .../seo}/seo-score-content.js | 6 +++--- .../{components => scores/seo}/seo-scores.js | 4 ++-- packages/js/src/dash/util/fetch-json.js | 17 ----------------- 13 files changed, 33 insertions(+), 33 deletions(-) rename packages/js/src/dash/{util => hooks}/use-fetch.js (81%) rename packages/js/src/dash/{ => scores}/components/content-status-description.js (100%) rename packages/js/src/dash/{ => scores}/components/content-type-filter.js (100%) rename packages/js/src/dash/{ => scores}/components/score-chart.js (96%) rename packages/js/src/dash/{ => scores}/components/score-list.js (94%) rename packages/js/src/dash/{ => scores}/components/term-filter.js (98%) rename packages/js/src/dash/{components => scores/readability}/readability-score-content.js (89%) rename packages/js/src/dash/{components => scores/readability}/readability-scores.js (91%) rename packages/js/src/dash/{util => scores}/score-meta.js (100%) rename packages/js/src/dash/{components => scores/seo}/seo-score-content.js (89%) rename packages/js/src/dash/{components => scores/seo}/seo-scores.js (91%) delete mode 100644 packages/js/src/dash/util/fetch-json.js diff --git a/packages/js/src/dash/components/dash.js b/packages/js/src/dash/components/dash.js index c0c97505458..c552767132d 100644 --- a/packages/js/src/dash/components/dash.js +++ b/packages/js/src/dash/components/dash.js @@ -1,6 +1,6 @@ +import { ReadabilityScores } from "../scores/readability/readability-scores"; +import { SeoScores } from "../scores/seo/seo-scores"; import { PageTitle } from "./page-title"; -import { ReadabilityScores } from "./readability-scores"; -import { SeoScores } from "./seo-scores"; /** * @type {import("../index").ContentType} ContentType diff --git a/packages/js/src/dash/util/use-fetch.js b/packages/js/src/dash/hooks/use-fetch.js similarity index 81% rename from packages/js/src/dash/util/use-fetch.js rename to packages/js/src/dash/hooks/use-fetch.js index cd70edd7b74..f84ced169e8 100644 --- a/packages/js/src/dash/util/use-fetch.js +++ b/packages/js/src/dash/hooks/use-fetch.js @@ -1,7 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; import { debounce, identity } from "lodash"; import { FETCH_DELAY } from "../../shared-admin/constants"; -import { fetchJson } from "./fetch-json"; /** * @typedef {Object} FetchResult @@ -17,6 +16,24 @@ import { fetchJson } from "./fetch-json"; * @returns {Promise} The promise of a result, or an error. */ +/** + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} options The request options. + * @returns {Promise} The promise of a result, or an error. + */ +const fetchJson = async( url, options ) => { + try { + const response = await fetch( url, options ); + if ( ! response.ok ) { + // From the perspective of the results, we want to reject this as an error. + throw new Error( "Not ok" ); + } + return response.json(); + } catch ( error ) { + return Promise.reject( error ); + } +}; + /** * @param {any[]} dependencies The dependencies for the fetch. * @param {string|URL} url The URL to fetch from. diff --git a/packages/js/src/dash/components/content-status-description.js b/packages/js/src/dash/scores/components/content-status-description.js similarity index 100% rename from packages/js/src/dash/components/content-status-description.js rename to packages/js/src/dash/scores/components/content-status-description.js diff --git a/packages/js/src/dash/components/content-type-filter.js b/packages/js/src/dash/scores/components/content-type-filter.js similarity index 100% rename from packages/js/src/dash/components/content-type-filter.js rename to packages/js/src/dash/scores/components/content-type-filter.js diff --git a/packages/js/src/dash/components/score-chart.js b/packages/js/src/dash/scores/components/score-chart.js similarity index 96% rename from packages/js/src/dash/components/score-chart.js rename to packages/js/src/dash/scores/components/score-chart.js index 077cd3e34b7..17440b4374e 100644 --- a/packages/js/src/dash/components/score-chart.js +++ b/packages/js/src/dash/scores/components/score-chart.js @@ -1,6 +1,6 @@ import { ArcElement, Chart, Tooltip } from "chart.js"; import { Doughnut } from "react-chartjs-2"; -import { SCORE_META } from "../util/score-meta"; +import { SCORE_META } from "../score-meta"; /** * @type {import("../index").Score} Score diff --git a/packages/js/src/dash/components/score-list.js b/packages/js/src/dash/scores/components/score-list.js similarity index 94% rename from packages/js/src/dash/components/score-list.js rename to packages/js/src/dash/scores/components/score-list.js index d2da6d1f3ab..0e143dbfee5 100644 --- a/packages/js/src/dash/components/score-list.js +++ b/packages/js/src/dash/scores/components/score-list.js @@ -1,5 +1,5 @@ import { Badge, Button } from "@yoast/ui-library"; -import { SCORE_META } from "../util/score-meta"; +import { SCORE_META } from "../score-meta"; /** * @type {import("../index").Score} Score diff --git a/packages/js/src/dash/components/term-filter.js b/packages/js/src/dash/scores/components/term-filter.js similarity index 98% rename from packages/js/src/dash/components/term-filter.js rename to packages/js/src/dash/scores/components/term-filter.js index 0ed63894bc1..7bbd69aedc8 100644 --- a/packages/js/src/dash/components/term-filter.js +++ b/packages/js/src/dash/scores/components/term-filter.js @@ -1,7 +1,7 @@ import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Spinner } from "@yoast/ui-library"; -import { useFetch } from "../util/use-fetch"; +import { useFetch } from "../../hooks/use-fetch"; /** * @type {import("../index").Taxonomy} Taxonomy diff --git a/packages/js/src/dash/components/readability-score-content.js b/packages/js/src/dash/scores/readability/readability-score-content.js similarity index 89% rename from packages/js/src/dash/components/readability-score-content.js rename to packages/js/src/dash/scores/readability/readability-score-content.js index 373ff16b6f0..4ed3a275fcd 100644 --- a/packages/js/src/dash/components/readability-score-content.js +++ b/packages/js/src/dash/scores/readability/readability-score-content.js @@ -1,7 +1,7 @@ import { useEffect, useState } from "@wordpress/element"; -import { ContentStatusDescription } from "./content-status-description"; -import { ScoreChart } from "./score-chart"; -import { ScoreList } from "./score-list"; +import { ContentStatusDescription } from "../components/content-status-description"; +import { ScoreChart } from "../components/score-chart"; +import { ScoreList } from "../components/score-list"; /** * @type {import("../index").ContentType} ContentType diff --git a/packages/js/src/dash/components/readability-scores.js b/packages/js/src/dash/scores/readability/readability-scores.js similarity index 91% rename from packages/js/src/dash/components/readability-scores.js rename to packages/js/src/dash/scores/readability/readability-scores.js index 03e839c31a0..4aacaa5990e 100644 --- a/packages/js/src/dash/components/readability-scores.js +++ b/packages/js/src/dash/scores/readability/readability-scores.js @@ -1,9 +1,9 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import { ContentTypeFilter } from "./content-type-filter"; +import { ContentTypeFilter } from "../components/content-type-filter"; +import { TermFilter } from "../components/term-filter"; import { ReadabilityScoreContent } from "./readability-score-content"; -import { TermFilter } from "./term-filter"; /** * @type {import("../index").ContentType} ContentType diff --git a/packages/js/src/dash/util/score-meta.js b/packages/js/src/dash/scores/score-meta.js similarity index 100% rename from packages/js/src/dash/util/score-meta.js rename to packages/js/src/dash/scores/score-meta.js diff --git a/packages/js/src/dash/components/seo-score-content.js b/packages/js/src/dash/scores/seo/seo-score-content.js similarity index 89% rename from packages/js/src/dash/components/seo-score-content.js rename to packages/js/src/dash/scores/seo/seo-score-content.js index b501aac2cdf..3be93829c64 100644 --- a/packages/js/src/dash/components/seo-score-content.js +++ b/packages/js/src/dash/scores/seo/seo-score-content.js @@ -1,7 +1,7 @@ import { useEffect, useState } from "@wordpress/element"; -import { ContentStatusDescription } from "./content-status-description"; -import { ScoreChart } from "./score-chart"; -import { ScoreList } from "./score-list"; +import { ContentStatusDescription } from "../components/content-status-description"; +import { ScoreChart } from "../components/score-chart"; +import { ScoreList } from "../components/score-list"; /** * @type {import("../index").ContentType} ContentType diff --git a/packages/js/src/dash/components/seo-scores.js b/packages/js/src/dash/scores/seo/seo-scores.js similarity index 91% rename from packages/js/src/dash/components/seo-scores.js rename to packages/js/src/dash/scores/seo/seo-scores.js index b063d0351bf..24d522907db 100644 --- a/packages/js/src/dash/components/seo-scores.js +++ b/packages/js/src/dash/scores/seo/seo-scores.js @@ -1,9 +1,9 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import { ContentTypeFilter } from "./content-type-filter"; +import { ContentTypeFilter } from "../components/content-type-filter"; +import { TermFilter } from "../components/term-filter"; import { SeoScoreContent } from "./seo-score-content"; -import { TermFilter } from "./term-filter"; /** * @type {import("../index").ContentType} ContentType diff --git a/packages/js/src/dash/util/fetch-json.js b/packages/js/src/dash/util/fetch-json.js deleted file mode 100644 index b952721cadc..00000000000 --- a/packages/js/src/dash/util/fetch-json.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @param {string|URL} url The URL to fetch from. - * @param {RequestInit} options The request options. - * @returns {Promise} The promise of a result, or an error. - */ -export const fetchJson = async( url, options ) => { - try { - const response = await fetch( url, options ); - if ( ! response.ok ) { - // From the perspective of the results, we want to reject this as an error. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } -}; From 483eb0029f9342dce7bb43a3d672af5dc5669e6a Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:50:43 +0100 Subject: [PATCH 035/132] Rename Dash back to Dashboard --- packages/js/.eslintrc.js | 4 ++-- .../components/dash.js => dashboard/components/dashboard.js} | 2 +- packages/js/src/{dash => dashboard}/components/page-title.js | 2 +- packages/js/src/{dash => dashboard}/hooks/use-fetch.js | 0 packages/js/src/{dash => dashboard}/index.js | 2 +- .../scores/components/content-status-description.js | 0 .../scores/components/content-type-filter.js | 0 .../src/{dash => dashboard}/scores/components/score-chart.js | 0 .../src/{dash => dashboard}/scores/components/score-list.js | 0 .../src/{dash => dashboard}/scores/components/term-filter.js | 0 .../scores/readability/readability-score-content.js | 0 .../scores/readability/readability-scores.js | 0 packages/js/src/{dash => dashboard}/scores/score-meta.js | 0 .../src/{dash => dashboard}/scores/seo/seo-score-content.js | 0 packages/js/src/{dash => dashboard}/scores/seo/seo-scores.js | 0 packages/js/src/general/initialize.js | 4 ++-- 16 files changed, 7 insertions(+), 7 deletions(-) rename packages/js/src/{dash/components/dash.js => dashboard/components/dashboard.js} (91%) rename packages/js/src/{dash => dashboard}/components/page-title.js (87%) rename packages/js/src/{dash => dashboard}/hooks/use-fetch.js (100%) rename packages/js/src/{dash => dashboard}/index.js (94%) rename packages/js/src/{dash => dashboard}/scores/components/content-status-description.js (100%) rename packages/js/src/{dash => dashboard}/scores/components/content-type-filter.js (100%) rename packages/js/src/{dash => dashboard}/scores/components/score-chart.js (100%) rename packages/js/src/{dash => dashboard}/scores/components/score-list.js (100%) rename packages/js/src/{dash => dashboard}/scores/components/term-filter.js (100%) rename packages/js/src/{dash => dashboard}/scores/readability/readability-score-content.js (100%) rename packages/js/src/{dash => dashboard}/scores/readability/readability-scores.js (100%) rename packages/js/src/{dash => dashboard}/scores/score-meta.js (100%) rename packages/js/src/{dash => dashboard}/scores/seo/seo-score-content.js (100%) rename packages/js/src/{dash => dashboard}/scores/seo/seo-scores.js (100%) diff --git a/packages/js/.eslintrc.js b/packages/js/.eslintrc.js index b7b83602d24..ed4fe8ecd26 100644 --- a/packages/js/.eslintrc.js +++ b/packages/js/.eslintrc.js @@ -103,9 +103,9 @@ module.exports = { "react/display-name": 0, }, }, - // Ignore Proptypes in dash. + // Ignore Proptypes in the dashboard. { - files: [ "src/dash/**/*.js" ], + files: [ "src/dashboard/**/*.js" ], rules: { "react/prop-types": 0, }, diff --git a/packages/js/src/dash/components/dash.js b/packages/js/src/dashboard/components/dashboard.js similarity index 91% rename from packages/js/src/dash/components/dash.js rename to packages/js/src/dashboard/components/dashboard.js index c552767132d..1ca3a30987c 100644 --- a/packages/js/src/dash/components/dash.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -11,7 +11,7 @@ import { PageTitle } from "./page-title"; * @param {string} userName The user name. * @returns {JSX.Element} The element. */ -export const Dash = ( { contentTypes, userName } ) => { +export const Dashboard = ( { contentTypes, userName } ) => { return (
    diff --git a/packages/js/src/dash/components/page-title.js b/packages/js/src/dashboard/components/page-title.js similarity index 87% rename from packages/js/src/dash/components/page-title.js rename to packages/js/src/dashboard/components/page-title.js index 78dd29e616c..c15b7008603 100644 --- a/packages/js/src/dash/components/page-title.js +++ b/packages/js/src/dashboard/components/page-title.js @@ -15,7 +15,7 @@ export const PageTitle = ( { userName } ) => ( ) }

    - { __( "Welcome to your SEO dash!", "wordpress-seo" ) } + { __( "Welcome to your SEO dashboard!", "wordpress-seo" ) }

    diff --git a/packages/js/src/dash/hooks/use-fetch.js b/packages/js/src/dashboard/hooks/use-fetch.js similarity index 100% rename from packages/js/src/dash/hooks/use-fetch.js rename to packages/js/src/dashboard/hooks/use-fetch.js diff --git a/packages/js/src/dash/index.js b/packages/js/src/dashboard/index.js similarity index 94% rename from packages/js/src/dash/index.js rename to packages/js/src/dashboard/index.js index 2f35b62a52b..8708ef50873 100644 --- a/packages/js/src/dash/index.js +++ b/packages/js/src/dashboard/index.js @@ -1,4 +1,4 @@ -export { Dash } from "./components/dash"; +export { Dashboard } from "./components/dashboard"; /** * @typedef {Object} Taxonomy A taxonomy. diff --git a/packages/js/src/dash/scores/components/content-status-description.js b/packages/js/src/dashboard/scores/components/content-status-description.js similarity index 100% rename from packages/js/src/dash/scores/components/content-status-description.js rename to packages/js/src/dashboard/scores/components/content-status-description.js diff --git a/packages/js/src/dash/scores/components/content-type-filter.js b/packages/js/src/dashboard/scores/components/content-type-filter.js similarity index 100% rename from packages/js/src/dash/scores/components/content-type-filter.js rename to packages/js/src/dashboard/scores/components/content-type-filter.js diff --git a/packages/js/src/dash/scores/components/score-chart.js b/packages/js/src/dashboard/scores/components/score-chart.js similarity index 100% rename from packages/js/src/dash/scores/components/score-chart.js rename to packages/js/src/dashboard/scores/components/score-chart.js diff --git a/packages/js/src/dash/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js similarity index 100% rename from packages/js/src/dash/scores/components/score-list.js rename to packages/js/src/dashboard/scores/components/score-list.js diff --git a/packages/js/src/dash/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js similarity index 100% rename from packages/js/src/dash/scores/components/term-filter.js rename to packages/js/src/dashboard/scores/components/term-filter.js diff --git a/packages/js/src/dash/scores/readability/readability-score-content.js b/packages/js/src/dashboard/scores/readability/readability-score-content.js similarity index 100% rename from packages/js/src/dash/scores/readability/readability-score-content.js rename to packages/js/src/dashboard/scores/readability/readability-score-content.js diff --git a/packages/js/src/dash/scores/readability/readability-scores.js b/packages/js/src/dashboard/scores/readability/readability-scores.js similarity index 100% rename from packages/js/src/dash/scores/readability/readability-scores.js rename to packages/js/src/dashboard/scores/readability/readability-scores.js diff --git a/packages/js/src/dash/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js similarity index 100% rename from packages/js/src/dash/scores/score-meta.js rename to packages/js/src/dashboard/scores/score-meta.js diff --git a/packages/js/src/dash/scores/seo/seo-score-content.js b/packages/js/src/dashboard/scores/seo/seo-score-content.js similarity index 100% rename from packages/js/src/dash/scores/seo/seo-score-content.js rename to packages/js/src/dashboard/scores/seo/seo-score-content.js diff --git a/packages/js/src/dash/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js similarity index 100% rename from packages/js/src/dash/scores/seo/seo-scores.js rename to packages/js/src/dashboard/scores/seo/seo-scores.js diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index f08aeef01d2..62d2156f339 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -5,7 +5,7 @@ import { render } from "@wordpress/element"; import { Root } from "@yoast/ui-library"; import { get } from "lodash"; import { createHashRouter, createRoutesFromElements, Navigate, Route, RouterProvider } from "react-router-dom"; -import { Dash } from "../dash"; +import { Dashboard } from "../dashboard"; import { LINK_PARAMS_NAME } from "../shared-admin/store"; import App from "./app"; import { RouteErrorFallback } from "./components"; @@ -40,7 +40,7 @@ domReady( () => { } errorElement={ }> } errorElement={ } + element={ } errorElement={ } /> } errorElement={ } /> } errorElement={ } /> From 52b70b02cd9169e72c3492b08cc14d5650abd40a Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:13:08 +0100 Subject: [PATCH 036/132] Add skeleton loader for the seo scores --- .../scores/components/score-chart.js | 3 +- .../dashboard/scores/components/score-list.js | 9 +- .../readability/readability-score-content.js | 2 +- .../js/src/dashboard/scores/seo/scores.json | 30 ++++ .../dashboard/scores/seo/seo-score-content.js | 138 ++++++++---------- 5 files changed, 96 insertions(+), 86 deletions(-) create mode 100644 packages/js/src/dashboard/scores/seo/scores.json diff --git a/packages/js/src/dashboard/scores/components/score-chart.js b/packages/js/src/dashboard/scores/components/score-chart.js index 17440b4374e..fd2874993bb 100644 --- a/packages/js/src/dashboard/scores/components/score-chart.js +++ b/packages/js/src/dashboard/scores/components/score-chart.js @@ -38,7 +38,6 @@ const transformScoresToGraphData = ( scores ) => { const chartOptions = { plugins: { - responsive: true, legend: false, tooltip: { displayColors: false, @@ -57,7 +56,7 @@ const chartOptions = { */ export const ScoreChart = ( { scores } ) => { return ( -
    +
    ( -
      +
        { scores.map( ( score ) => (
      • - { SCORE_META[ score.name ].label } { score.amount } - { score.links.view && } + { SCORE_META[ score.name ].label } + { score.amount } + { score.links.view && }
      • ) ) }
      diff --git a/packages/js/src/dashboard/scores/readability/readability-score-content.js b/packages/js/src/dashboard/scores/readability/readability-score-content.js index 4ed3a275fcd..dd93652187c 100644 --- a/packages/js/src/dashboard/scores/readability/readability-score-content.js +++ b/packages/js/src/dashboard/scores/readability/readability-score-content.js @@ -89,7 +89,7 @@ export const ReadabilityScoreContent = ( { contentType, term } ) => { return <> -
      +
      { scores && } { scores && }
      diff --git a/packages/js/src/dashboard/scores/seo/scores.json b/packages/js/src/dashboard/scores/seo/scores.json new file mode 100644 index 00000000000..14fb01d8b03 --- /dev/null +++ b/packages/js/src/dashboard/scores/seo/scores.json @@ -0,0 +1,30 @@ +[ + { + "name": "good", + "amount": 5, + "links": { + "view": null + } + }, + { + "name": "ok", + "amount": 4, + "links": { + "view": "https://basic.wordpress.test/wp-admin/edit.php?category=22" + } + }, + { + "name": "bad", + "amount": 6, + "links": { + "view": null + } + }, + { + "name": "notAnalyzed", + "amount": 7, + "links": { + "view": null + } + } +] diff --git a/packages/js/src/dashboard/scores/seo/seo-score-content.js b/packages/js/src/dashboard/scores/seo/seo-score-content.js index 3be93829c64..7a759eb0680 100644 --- a/packages/js/src/dashboard/scores/seo/seo-score-content.js +++ b/packages/js/src/dashboard/scores/seo/seo-score-content.js @@ -1,7 +1,9 @@ -import { useEffect, useState } from "@wordpress/element"; +import { SkeletonLoader } from "@yoast/ui-library"; +import { useFetch } from "../../hooks/use-fetch"; import { ContentStatusDescription } from "../components/content-status-description"; import { ScoreChart } from "../components/score-chart"; import { ScoreList } from "../components/score-list"; +import { SCORE_META } from "../score-meta"; /** * @type {import("../index").ContentType} ContentType @@ -9,89 +11,67 @@ import { ScoreList } from "../components/score-list"; * @type {import("../index").Score} Score */ -/** @type {Score[]} **/ -const fakeScores = [ - { - name: "ok", - amount: 4, - links: { - view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", - }, - }, - { - name: "good", - amount: 5, - links: { - view: null, - }, - }, - { - name: "bad", - amount: 6, - links: { - view: null, - }, - }, - { - name: "notAnalyzed", - amount: 7, - links: { - view: null, - }, - }, -]; -const fakeScores2 = [ - { - name: "ok", - amount: 7, - links: { - view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", - }, - }, - { - name: "good", - amount: 12, - links: { - view: null, - }, - }, - { - name: "bad", - amount: 1, - links: { - view: null, - }, - }, - { - name: "notAnalyzed", - amount: 2, - links: { - view: null, - }, - }, -]; - /** * @param {ContentType} contentType The selected contentType. * @param {Term?} [term] The selected term. * @returns {JSX.Element} The element. */ export const SeoScoreContent = ( { contentType, term } ) => { - const [ scores, setScores ] = useState(); - useEffect( () => { - const rand = Math.random(); - if ( rand < 0.5 ) { - setScores( fakeScores ); - } else { - setScores( fakeScores2 ); - } - }, [ contentType.name, term?.name ] ); + const { data: scores = [], isPending } = useFetch( { + dependencies: [ contentType.name, term?.name ], + url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/seo/scores.json", + // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, + options: { headers: { "Content-Type": "application/json" } }, + fetchDelay: 0, + doFetch: async( url, options ) => { + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + try { + const response = await fetch( url, options ); + if ( ! response.ok ) { + // From the perspective of the results, we want to reject this as an error. + throw new Error( "Not ok" ); + } + return response.json(); + } catch ( error ) { + return Promise.reject( error ); + } + }, + } ); + + if ( isPending ) { + return ( + <> +   +
      +
        + { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => ( +
      • + + { label } + 1 + View +
      • + ) ) } +
      +
      + +
      +
      +
      + + ); + } - return <> - -
      - { scores && } - { scores && } -
      - ; + return ( + <> + +
      + { scores && } + { scores && } +
      + + ); }; From d23eb6f1641e7c6cb4edd28a57e8b10deb421ce7 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:37:18 +0100 Subject: [PATCH 037/132] Use score content in readability --- .../scores/components/score-content.js | 58 +++++++++++ .../scores/components/term-filter.js | 2 +- .../readability/readability-score-content.js | 97 ------------------- .../scores/readability/readability-scores.js | 26 ++++- .../dashboard/scores/readability/scores.json | 30 ++++++ .../dashboard/scores/seo/seo-score-content.js | 77 --------------- .../js/src/dashboard/scores/seo/seo-scores.js | 26 ++++- 7 files changed, 137 insertions(+), 179 deletions(-) create mode 100644 packages/js/src/dashboard/scores/components/score-content.js delete mode 100644 packages/js/src/dashboard/scores/readability/readability-score-content.js create mode 100644 packages/js/src/dashboard/scores/readability/scores.json delete mode 100644 packages/js/src/dashboard/scores/seo/seo-score-content.js diff --git a/packages/js/src/dashboard/scores/components/score-content.js b/packages/js/src/dashboard/scores/components/score-content.js new file mode 100644 index 00000000000..d5acfe7dcf4 --- /dev/null +++ b/packages/js/src/dashboard/scores/components/score-content.js @@ -0,0 +1,58 @@ +import { SkeletonLoader } from "@yoast/ui-library"; +import { SCORE_META } from "../score-meta"; +import { ContentStatusDescription } from "./content-status-description"; +import { ScoreChart } from "./score-chart"; +import { ScoreList } from "./score-list"; + +/** + * @type {import("../index").Score} Score + */ + +/** + * @returns {JSX.Element} The element. + */ +const ScoreContentSkeletonLoader = () => ( + <> +   +
      +
        + { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => ( +
      • + + { label } + 1 + View +
      • + ) ) } +
      +
      + +
      +
      +
      + +); + +/** + * @param {Score[]} [scores=[]] The scores. + * @param {boolean} isLoading Whether the scores are still loading. + * @returns {JSX.Element} The element. + */ +export const ScoreContent = ( { scores = [], isLoading } ) => { + if ( isLoading ) { + return ; + } + + return ( + <> + +
      + { scores && } + { scores && } +
      + + ); +}; diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 7bbd69aedc8..5942ec993e6 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -28,7 +28,7 @@ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); * @param {string} idSuffix The suffix for the ID. * @param {Taxonomy} taxonomy The taxonomy. * @param {Term?} selected The selected term. - * @param {function(ContentType?)} onChange The callback. Expects it changes the `selected` prop. + * @param {function(Term?)} onChange The callback. Expects it changes the `selected` prop. * @returns {JSX.Element} The element. */ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { diff --git a/packages/js/src/dashboard/scores/readability/readability-score-content.js b/packages/js/src/dashboard/scores/readability/readability-score-content.js deleted file mode 100644 index dd93652187c..00000000000 --- a/packages/js/src/dashboard/scores/readability/readability-score-content.js +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from "@wordpress/element"; -import { ContentStatusDescription } from "../components/content-status-description"; -import { ScoreChart } from "../components/score-chart"; -import { ScoreList } from "../components/score-list"; - -/** - * @type {import("../index").ContentType} ContentType - * @type {import("../index").Term} Term - * @type {import("../index").Score} Score - */ - -/** @type {Score[]} **/ -const fakeScores = [ - { - name: "good", - amount: 4, - links: { - view: null, - }, - }, - { - name: "ok", - amount: 5, - links: { - view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", - }, - }, - { - name: "bad", - amount: 6, - links: { - view: null, - }, - }, - { - name: "notAnalyzed", - amount: 7, - links: { - view: null, - }, - }, -]; -const fakeScores2 = [ - { - name: "ok", - amount: 7, - links: { - view: "https://basic.wordpress.test/wp-admin/edit.php?category=22", - }, - }, - { - name: "good", - amount: 12, - links: { - view: null, - }, - }, - { - name: "bad", - amount: 1, - links: { - view: null, - }, - }, - { - name: "notAnalyzed", - amount: 2, - links: { - view: null, - }, - }, -]; - -/** - * @param {ContentType} contentType The selected contentType. - * @param {Term?} [term] The selected term. - * @returns {JSX.Element} The element. - */ -export const ReadabilityScoreContent = ( { contentType, term } ) => { - const [ scores, setScores ] = useState(); - useEffect( () => { - const rand = Math.random(); - if ( rand < 0.5 ) { - setScores( fakeScores ); - } else { - setScores( fakeScores2 ); - } - }, [ contentType.name, term?.name ] ); - - return <> - -
      - { scores && } - { scores && } -
      - ; -}; diff --git a/packages/js/src/dashboard/scores/readability/readability-scores.js b/packages/js/src/dashboard/scores/readability/readability-scores.js index 4aacaa5990e..c959d1175f1 100644 --- a/packages/js/src/dashboard/scores/readability/readability-scores.js +++ b/packages/js/src/dashboard/scores/readability/readability-scores.js @@ -1,9 +1,10 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; +import { useFetch } from "../../hooks/use-fetch"; import { ContentTypeFilter } from "../components/content-type-filter"; +import { ScoreContent } from "../components/score-content"; import { TermFilter } from "../components/term-filter"; -import { ReadabilityScoreContent } from "./readability-score-content"; /** * @type {import("../index").ContentType} ContentType @@ -18,6 +19,27 @@ export const ReadabilityScores = ( { contentTypes } ) => { const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); const [ selectedTerm, setSelectedTerm ] = useState(); + const { data: scores, isPending } = useFetch( { + dependencies: [ selectedContentType.name, selectedTerm?.name ], + url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/readability/scores.json", + // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, + options: { headers: { "Content-Type": "application/json" } }, + fetchDelay: 0, + doFetch: async( url, options ) => { + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + try { + const response = await fetch( url, options ); + if ( ! response.ok ) { + // From the perspective of the results, we want to reject this as an error. + throw new Error( "Not ok" ); + } + return response.json(); + } catch ( error ) { + return Promise.reject( error ); + } + }, + } ); + return ( { __( "Readability scores", "wordpress-seo" ) } @@ -37,7 +59,7 @@ export const ReadabilityScores = ( { contentTypes } ) => { /> }
      - + ); }; diff --git a/packages/js/src/dashboard/scores/readability/scores.json b/packages/js/src/dashboard/scores/readability/scores.json new file mode 100644 index 00000000000..ab6b9692e1d --- /dev/null +++ b/packages/js/src/dashboard/scores/readability/scores.json @@ -0,0 +1,30 @@ +[ + { + "name": "good", + "amount": 8, + "links": { + "view": "https://basic.wordpress.test/wp-admin/edit.php?category=22" + } + }, + { + "name": "ok", + "amount": 9, + "links": { + "view": null + } + }, + { + "name": "bad", + "amount": 10, + "links": { + "view": null + } + }, + { + "name": "notAnalyzed", + "amount": 11, + "links": { + "view": null + } + } +] diff --git a/packages/js/src/dashboard/scores/seo/seo-score-content.js b/packages/js/src/dashboard/scores/seo/seo-score-content.js deleted file mode 100644 index 7a759eb0680..00000000000 --- a/packages/js/src/dashboard/scores/seo/seo-score-content.js +++ /dev/null @@ -1,77 +0,0 @@ -import { SkeletonLoader } from "@yoast/ui-library"; -import { useFetch } from "../../hooks/use-fetch"; -import { ContentStatusDescription } from "../components/content-status-description"; -import { ScoreChart } from "../components/score-chart"; -import { ScoreList } from "../components/score-list"; -import { SCORE_META } from "../score-meta"; - -/** - * @type {import("../index").ContentType} ContentType - * @type {import("../index").Term} Term - * @type {import("../index").Score} Score - */ - -/** - * @param {ContentType} contentType The selected contentType. - * @param {Term?} [term] The selected term. - * @returns {JSX.Element} The element. - */ -export const SeoScoreContent = ( { contentType, term } ) => { - const { data: scores = [], isPending } = useFetch( { - dependencies: [ contentType.name, term?.name ], - url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/seo/scores.json", - // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, - options: { headers: { "Content-Type": "application/json" } }, - fetchDelay: 0, - doFetch: async( url, options ) => { - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - try { - const response = await fetch( url, options ); - if ( ! response.ok ) { - // From the perspective of the results, we want to reject this as an error. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } - }, - } ); - - if ( isPending ) { - return ( - <> -   -
      -
        - { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => ( -
      • - - { label } - 1 - View -
      • - ) ) } -
      -
      - -
      -
      -
      - - ); - } - - return ( - <> - -
      - { scores && } - { scores && } -
      - - ); -}; diff --git a/packages/js/src/dashboard/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js index 24d522907db..fb30520789e 100644 --- a/packages/js/src/dashboard/scores/seo/seo-scores.js +++ b/packages/js/src/dashboard/scores/seo/seo-scores.js @@ -1,9 +1,10 @@ import { useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; +import { useFetch } from "../../hooks/use-fetch"; import { ContentTypeFilter } from "../components/content-type-filter"; +import { ScoreContent } from "../components/score-content"; import { TermFilter } from "../components/term-filter"; -import { SeoScoreContent } from "./seo-score-content"; /** * @type {import("../index").ContentType} ContentType @@ -19,6 +20,27 @@ export const SeoScores = ( { contentTypes } ) => { const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); const [ selectedTerm, setSelectedTerm ] = useState(); + const { data: scores, isPending } = useFetch( { + dependencies: [ selectedContentType.name, selectedTerm?.name ], + url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/seo/scores.json", + // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, + options: { headers: { "Content-Type": "application/json" } }, + fetchDelay: 0, + doFetch: async( url, options ) => { + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + try { + const response = await fetch( url, options ); + if ( ! response.ok ) { + // From the perspective of the results, we want to reject this as an error. + throw new Error( "Not ok" ); + } + return response.json(); + } catch ( error ) { + return Promise.reject( error ); + } + }, + } ); + return ( { __( "SEO scores", "wordpress-seo" ) } @@ -38,7 +60,7 @@ export const SeoScores = ( { contentTypes } ) => { /> }
      - + ); }; From 4c232b3228470d1c29bcc11ca8d55923bc803f45 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:08:20 +0100 Subject: [PATCH 038/132] Add sidebar to dashboard --- .../js/src/dashboard/components/dashboard.js | 4 +- .../src/general/components/sidebar-layout.js | 44 +++++++++++++++++++ packages/js/src/general/initialize.js | 10 ++++- .../js/src/general/routes/alert-center.js | 36 +++++---------- 4 files changed, 66 insertions(+), 28 deletions(-) create mode 100644 packages/js/src/general/components/sidebar-layout.js diff --git a/packages/js/src/dashboard/components/dashboard.js b/packages/js/src/dashboard/components/dashboard.js index 1ca3a30987c..a5a9a3b9503 100644 --- a/packages/js/src/dashboard/components/dashboard.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -13,12 +13,12 @@ import { PageTitle } from "./page-title"; */ export const Dashboard = ( { contentTypes, userName } ) => { return ( -
      + <>
      -
      + ); }; diff --git a/packages/js/src/general/components/sidebar-layout.js b/packages/js/src/general/components/sidebar-layout.js new file mode 100644 index 00000000000..854ef4b3cce --- /dev/null +++ b/packages/js/src/general/components/sidebar-layout.js @@ -0,0 +1,44 @@ +import { useSelect } from "@wordpress/data"; +import classNames from "classnames"; +import PropTypes from "prop-types"; +import { SidebarRecommendations } from "../../shared-admin/components"; +import { STORE_NAME } from "../constants"; +import { useSelectGeneralPage } from "../hooks"; + +/** + * @param {string} [contentClassName] Extra class name for the children container. + * @param {JSX.node} children The children. + * @returns {JSX.Element} The element. + */ +export const SidebarLayout = ( { contentClassName, children } ) => { + const isPremium = useSelectGeneralPage( "selectPreference", [], "isPremium" ); + const premiumLinkSidebar = useSelectGeneralPage( "selectLink", [], "https://yoa.st/jj" ); + const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); + const academyLink = useSelectGeneralPage( "selectLink", [], "https://yoa.st/3t6" ); + const { isPromotionActive } = useSelect( STORE_NAME ); + + return ( +
      +
      + { children } +
      + { ! isPremium && +
      +
      + +
      +
      + } +
      + ); +}; + +SidebarLayout.propTypes = { + contentClassName: PropTypes.string, + children: PropTypes.node, +}; diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 62d2156f339..510b67d6215 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -9,6 +9,7 @@ import { Dashboard } from "../dashboard"; import { LINK_PARAMS_NAME } from "../shared-admin/store"; import App from "./app"; import { RouteErrorFallback } from "./components"; +import { SidebarLayout } from "./components/sidebar-layout"; import { STORE_NAME } from "./constants"; import { AlertCenter, FirstTimeConfiguration, ROUTES } from "./routes"; import registerStore from "./store"; @@ -40,9 +41,14 @@ domReady( () => { } errorElement={ }> } errorElement={ } + element={ } + errorElement={ } + /> + } + errorElement={ } /> - } errorElement={ } /> } errorElement={ } /> { /** diff --git a/packages/js/src/general/routes/alert-center.js b/packages/js/src/general/routes/alert-center.js index 6dc12e5aa21..550397d9e47 100644 --- a/packages/js/src/general/routes/alert-center.js +++ b/packages/js/src/general/routes/alert-center.js @@ -1,7 +1,7 @@ import { useSelect } from "@wordpress/data"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import { PremiumUpsellList, SidebarRecommendations } from "../../shared-admin/components"; +import { PremiumUpsellList } from "../../shared-admin/components"; import { Notifications, Problems } from "../components"; import { STORE_NAME } from "../constants"; import { useSelectGeneralPage } from "../hooks"; @@ -12,13 +12,11 @@ import { useSelectGeneralPage } from "../hooks"; export const AlertCenter = () => { const isPremium = useSelectGeneralPage( "selectPreference", [], "isPremium" ); const premiumLinkList = useSelectGeneralPage( "selectLink", [], "https://yoa.st/17h" ); - const premiumLinkSidebar = useSelectGeneralPage( "selectLink", [], "https://yoa.st/jj" ); const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); - const academyLink = useSelectGeneralPage( "selectLink", [], "https://yoa.st/3t6" ); const { isPromotionActive } = useSelect( STORE_NAME ); - return
      -
      + return ( + <>
      { __( "Alert center", "wordpress-seo" ) } @@ -31,23 +29,13 @@ export const AlertCenter = () => {
      - { ! isPremium && } -
      - { ! isPremium && -
      -
      - -
      -
      - } -
      ; + { isPremium ? null : ( + + ) } + + ); }; From 042d654d925876f63ca2112c8a5d8108bb0b971e Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 18 Nov 2024 13:40:31 +0100 Subject: [PATCH 039/132] Small refactor + feedback. --- admin/formatter/class-metabox-formatter.php | 21 +++++++++--------- .../configuration/dashboard-configuration.php | 22 +++++++++---------- ...y.php => analysis-features-repository.php} | 18 +++++++-------- 3 files changed, 31 insertions(+), 30 deletions(-) rename src/editors/application/analysis-features/{enabled-analysis-features-repository.php => analysis-features-repository.php} (74%) diff --git a/admin/formatter/class-metabox-formatter.php b/admin/formatter/class-metabox-formatter.php index 02deb3c8eb4..aeabaacf4c4 100644 --- a/admin/formatter/class-metabox-formatter.php +++ b/admin/formatter/class-metabox-formatter.php @@ -6,7 +6,7 @@ */ use Yoast\WP\SEO\Config\Schema_Types; -use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; +use Yoast\WP\SEO\Editors\Application\Analysis_Features\Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository; /** @@ -51,31 +51,32 @@ private function get_defaults() { $schema_types = new Schema_Types(); $defaults = [ - 'author_name' => get_the_author_meta( 'display_name' ), - 'keyword_usage' => [], - 'title_template' => '', - 'metadesc_template' => '', - 'schema' => [ + 'author_name' => get_the_author_meta( 'display_name' ), + 'keyword_usage' => [], + 'title_template' => '', + 'metadesc_template' => '', + 'schema' => [ 'displayFooter' => WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ), 'pageTypeOptions' => $schema_types->get_page_type_options(), 'articleTypeOptions' => $schema_types->get_article_type_options(), ], - 'twitterCardType' => 'summary_large_image', + 'twitterCardType' => 'summary_large_image', /** * Filter to determine if the markers should be enabled or not. * * @param bool $showMarkers Should the markers being enabled. Default = true. */ - 'show_markers' => apply_filters( 'wpseo_enable_assessment_markers', true ), + 'show_markers' => apply_filters( 'wpseo_enable_assessment_markers', true ), ]; $integration_information_repo = YoastSEO()->classes->get( Integration_Information_Repository::class ); $enabled_integrations = $integration_information_repo->get_integration_information(); $defaults = array_merge( $defaults, $enabled_integrations ); - $enabled_features_repo = YoastSEO()->classes->get( Enabled_Analysis_Features_Repository::class ); + $enabled_features_repo = YoastSEO()->classes->get( Analysis_Features_Repository::class ); + + $enabled_features = $enabled_features_repo->get_analysis_features()->parse_to_legacy_array(); - $enabled_features = $enabled_features_repo->get_enabled_features()->parse_to_legacy_array(); return array_merge( $defaults, $enabled_features ); } } diff --git a/src/dashboard/application/configuration/dashboard-configuration.php b/src/dashboard/application/configuration/dashboard-configuration.php index 2a578848775..f55073169b9 100644 --- a/src/dashboard/application/configuration/dashboard-configuration.php +++ b/src/dashboard/application/configuration/dashboard-configuration.php @@ -5,7 +5,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Configuration; use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; -use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; +use Yoast\WP\SEO\Editors\Application\Analysis_Features\Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis; use Yoast\WP\SEO\Editors\Framework\Readability_Analysis; use Yoast\WP\SEO\Helpers\Indexable_Helper; @@ -40,23 +40,23 @@ class Dashboard_Configuration { /** * The repository. * - * @var Enabled_Analysis_Features_Repository + * @var Analysis_Features_Repository */ private $analysis_features_repository; /** * The constructor. * - * @param Content_Types_Repository $content_types_repository The content types repository. - * @param Indexable_Helper $indexable_helper The indexable helper repository. - * @param User_Helper $user_helper The user helper. - * @param Enabled_Analysis_Features_Repository $analysis_features_repository The analysis feature repository. + * @param Content_Types_Repository $content_types_repository The content types repository. + * @param Indexable_Helper $indexable_helper The indexable helper repository. + * @param User_Helper $user_helper The user helper. + * @param Analysis_Features_Repository $analysis_features_repository The analysis feature repository. */ public function __construct( Content_Types_Repository $content_types_repository, Indexable_Helper $indexable_helper, User_Helper $user_helper, - Enabled_Analysis_Features_Repository $analysis_features_repository + Analysis_Features_Repository $analysis_features_repository ) { $this->content_types_repository = $content_types_repository; $this->indexable_helper = $indexable_helper; @@ -71,10 +71,10 @@ public function __construct( */ public function get_configuration(): array { return [ - 'contentTypes' => $this->content_types_repository->get_content_types(), - 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), - 'displayName' => $this->user_helper->get_current_user_display_name(), - 'enabledAnalysisFeatures' => $this->analysis_features_repository->get_enabled_features_by_keys( + 'contentTypes' => $this->content_types_repository->get_content_types(), + 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), + 'displayName' => $this->user_helper->get_current_user_display_name(), + 'analysisFeatures' => $this->analysis_features_repository->get_analysis_features_by_keys( [ Readability_Analysis::NAME, Keyphrase_Analysis::NAME, diff --git a/src/editors/application/analysis-features/enabled-analysis-features-repository.php b/src/editors/application/analysis-features/analysis-features-repository.php similarity index 74% rename from src/editors/application/analysis-features/enabled-analysis-features-repository.php rename to src/editors/application/analysis-features/analysis-features-repository.php index 9261d22aba3..d70aa57e4c9 100644 --- a/src/editors/application/analysis-features/enabled-analysis-features-repository.php +++ b/src/editors/application/analysis-features/analysis-features-repository.php @@ -12,7 +12,7 @@ * * @makePublic */ -class Enabled_Analysis_Features_Repository { +class Analysis_Features_Repository { /** * All plugin features. @@ -35,14 +35,14 @@ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { * * @return Analysis_Features_List The analysis list. */ - public function get_enabled_features(): Analysis_Features_List { - $enabled_analysis_features = new Analysis_Features_List(); + public function get_analysis_features(): Analysis_Features_List { + $analysis_features_list = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $enabled_analysis_features->add_feature( $analysis_feature ); + $analysis_features_list->add_feature( $analysis_feature ); } - return $enabled_analysis_features; + return $analysis_features_list; } /** @@ -52,16 +52,16 @@ public function get_enabled_features(): Analysis_Features_List { * * @return Analysis_Features_List The analysis list. */ - public function get_enabled_features_by_keys( array $feature_names ): Analysis_Features_List { - $enabled_analysis_features = new Analysis_Features_List(); + public function get_analysis_features_by_keys( array $feature_names ): Analysis_Features_List { + $analysis_features_list = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { if ( \in_array( $plugin_feature->get_name(), $feature_names, true ) ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $enabled_analysis_features->add_feature( $analysis_feature ); + $analysis_features_list->add_feature( $analysis_feature ); } } - return $enabled_analysis_features; + return $analysis_features_list; } } From 56fbe7f4a3de866edfa6baf3525e129272b0511e Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 18 Nov 2024 13:45:39 +0100 Subject: [PATCH 040/132] Add a dot --- packages/js/src/dashboard/scores/components/term-filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 5942ec993e6..2cb2fc0553a 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -64,7 +64,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { nullable={ true } validation={ error && { variant: "error", - message: __( "Something went wrong", "wordpress-seo" ), + message: __( "Something went wrong.", "wordpress-seo" ), } } > { isPending && ( From 5e277021236845be8c3fcff77c547cc3868a3eb1 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 18 Nov 2024 15:13:52 +0100 Subject: [PATCH 041/132] Adds ability to also select Readability: No Focus Keyphrase as option. --- admin/class-meta-columns.php | 3 +++ inc/class-wpseo-rank.php | 7 ++++++- tests/WP/Admin/Meta_Columns_Test.php | 15 +++++++++++++++ tests/WP/Inc/Rank_Test.php | 1 + 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 7b2108fb750..1dd19e9d6b0 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -355,6 +355,9 @@ protected function determine_seo_filters( $seo_filter ) { * @return array The Readability score filter. */ protected function determine_readability_filters( $readability_filter ) { + if ( $readability_filter === WPSEO_Rank::NO_FOCUS ) { + return $this->create_no_focus_keyword_filter(); + } $rank = new WPSEO_Rank( $readability_filter ); return $this->create_readability_score_filter( $rank->get_starting_score(), $rank->get_end_score() ); diff --git a/inc/class-wpseo-rank.php b/inc/class-wpseo-rank.php index e44c1e3c9ad..a8b9c101519 100644 --- a/inc/class-wpseo-rank.php +++ b/inc/class-wpseo-rank.php @@ -217,6 +217,11 @@ public function get_drop_down_readability_labels() { __( 'Readability: %s', 'wordpress-seo' ), __( 'Good', 'wordpress-seo' ) ), + self::NO_FOCUS => sprintf( + /* translators: %s expands to the readability score */ + __( 'Readability: %s', 'wordpress-seo' ), + __( 'Not analyzed', 'wordpress-seo' ) + ), ]; return $labels[ $this->rank ]; @@ -313,7 +318,7 @@ public static function get_all_ranks() { * @return WPSEO_Rank[] */ public static function get_all_readability_ranks() { - return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD ] ); + return array_map( [ 'WPSEO_Rank', 'create_rank' ], [ self::BAD, self::OK, self::GOOD, self::NO_FOCUS ] ); } /** diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 24e25460c3e..63c067388f6 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -161,6 +161,21 @@ public static function determine_readability_filters_dataprovider() { ], ], ], + [ + 'na', + [ + [ + 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ], + ], ]; } diff --git a/tests/WP/Inc/Rank_Test.php b/tests/WP/Inc/Rank_Test.php index 21d92c47a9f..c0b4e23d66a 100644 --- a/tests/WP/Inc/Rank_Test.php +++ b/tests/WP/Inc/Rank_Test.php @@ -189,6 +189,7 @@ public static function provider_get_drop_down_readability_labels() { [ WPSEO_Rank::BAD, 'Readability: Needs improvement' ], [ WPSEO_Rank::OK, 'Readability: OK' ], [ WPSEO_Rank::GOOD, 'Readability: Good' ], + [ WPSEO_Rank::NO_FOCUS, 'Readability: Not analyzed' ], ]; } From a374a9799f8ad2da23104a86142d7788080d664d Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 18 Nov 2024 16:55:26 +0200 Subject: [PATCH 042/132] Onionize the SEO score feature --- .../seo-scores/seo-scores-repository.php | 63 ++++++++ .../domain/seo-scores/abstract-seo-score.php | 81 ++++++++++ .../domain/seo-scores/bad-seo-score.php | 36 +++++ .../domain/seo-scores/good-seo-score.php | 36 +++++ .../domain/seo-scores/no-seo-score.php | 36 +++++ .../domain/seo-scores/ok-seo-score.php | 36 +++++ .../seo-scores/seo-scores-interface.php | 55 +++++++ .../seo-scores/seo-scores-collector.php | 149 ++++++++++++++++++ .../user-interface/seo-scores-route.php | 88 ++--------- 9 files changed, 506 insertions(+), 74 deletions(-) create mode 100644 src/dashboard/application/seo-scores/seo-scores-repository.php create mode 100644 src/dashboard/domain/seo-scores/abstract-seo-score.php create mode 100644 src/dashboard/domain/seo-scores/bad-seo-score.php create mode 100644 src/dashboard/domain/seo-scores/good-seo-score.php create mode 100644 src/dashboard/domain/seo-scores/no-seo-score.php create mode 100644 src/dashboard/domain/seo-scores/ok-seo-score.php create mode 100644 src/dashboard/domain/seo-scores/seo-scores-interface.php create mode 100644 src/dashboard/infrastructure/seo-scores/seo-scores-collector.php diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php new file mode 100644 index 00000000000..a5a88319e19 --- /dev/null +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -0,0 +1,63 @@ +seo_scores_collector = $seo_scores_collector; + $this->seo_scores = $seo_scores; + } + + /** + * Returns the SEO Scores of a content type. + * + * @param string $content_type The content type. + * @param string $taxonomy The taxonomy of the term we're filtering for. + * @param int $term_id The ID of the term we're filtering for. + * + * @return array>> The SEO scores. + */ + public function get_seo_scores( string $content_type, ?string $taxonomy, ?int $term_id ): array { + $seo_scores = []; + + $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $taxonomy, $term_id ); + foreach ( $this->seo_scores as $seo_score ) { + $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); + $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score->get_name(), $content_type, $term_id ) ); + + $seo_scores[] = $seo_score->to_array(); + } + + return $seo_scores; + } +} diff --git a/src/dashboard/domain/seo-scores/abstract-seo-score.php b/src/dashboard/domain/seo-scores/abstract-seo-score.php new file mode 100644 index 00000000000..ec923480727 --- /dev/null +++ b/src/dashboard/domain/seo-scores/abstract-seo-score.php @@ -0,0 +1,81 @@ +amount = $amount; + } + + /** + * Sets the view link of the SEO score. + * + * @param string $view_link The view link of the SEO score. + * + * @return void + */ + public function set_view_link( string $view_link ): void { + $this->view_link = $view_link; + } + + /** + * Parses the SEO score to the expected key value representation. + * + * @return array> The SEO score presented as the expected key value representation. + */ + public function to_array(): array { + return [ + 'name' => $this->get_name(), + 'amount' => $this->amount, + 'links' => [ + 'view' => $this->view_link, + ], + ]; + } +} diff --git a/src/dashboard/domain/seo-scores/bad-seo-score.php b/src/dashboard/domain/seo-scores/bad-seo-score.php new file mode 100644 index 00000000000..cf42366a043 --- /dev/null +++ b/src/dashboard/domain/seo-scores/bad-seo-score.php @@ -0,0 +1,36 @@ +> + */ + public function to_array(): array; +} diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php new file mode 100644 index 00000000000..671776e612e --- /dev/null +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -0,0 +1,149 @@ +wpdb = $wpdb; + } + + /** + * Retrieves the current SEO scores for a content type. + * + * @param SEO_Scores_Interface[] $seo_scores All SEO scores. + * @param string $content_type The content type. + * @param string $taxonomy The taxonomy of the term we're filtering for. + * @param int $term_id The ID of the term we're filtering for. + * + * @return array The SEO scores for a content type. + */ + public function get_seo_scores( array $seo_scores, string $content_type, string $taxonomy, int $term_id ) { + $select = $this->build_select( $seo_scores ); + + $replacements = \array_merge( + \array_values( $select['replacements'] ), + [ + Model::get_table_name( 'Indexable' ), + $content_type, + ] + ); + + if ( $term_id === 0 || $taxonomy === '' ) { + $query = $this->wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s)", + $replacements + ); + + $scores = $this->wpdb->get_row( $query, \ARRAY_A ); + return $scores; + + } + + $replacements[] = $this->wpdb->term_relationships; + $replacements[] = $this->wpdb->term_taxonomy; + $replacements[] = $term_id; + $replacements[] = $taxonomy; + + $query = $this->wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s) + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id IN ( + SELECT term_taxonomy_id + FROM + %i + WHERE + term_id = %d + AND taxonomy = %s + ) + )", + $replacements + ); + + $scores = $this->wpdb->get_row( $query, \ARRAY_A ); + return $scores; + } + + /** + * Builds the select statement for the SEO scores query. + * + * @param SEO_Scores_Interface[] $seo_scores All SEO scores. + * + * @return array The select statement for the SEO scores query. + */ + private function build_select( array $seo_scores ): array { + $select_fields = []; + $select_replacements = []; + + foreach ( $seo_scores as $seo_score ) { + $min = $seo_score->get_min_score(); + $max = $seo_score->get_max_score(); + $name = $seo_score->get_name(); + + if ( $min === null || $max === null ) { + $select_fields[] = 'COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; + $select_replacements[] = $name; + } + else { + $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score >= %d AND I.primary_focus_keyword_score <= %d THEN 1 END) AS %i'; + $select_replacements[] = $min; + $select_replacements[] = $max; + $select_replacements[] = $name; + } + } + + $select_fields = \implode( ', ', $select_fields ); + + return [ + 'fields' => $select_fields, + 'replacements' => $select_replacements, + ]; + } + + /** + * Builds the view link of the SEO score. + * + * @param string $seo_score_name The name of the SEO score. + * @param string $content_type The content type. + * @param int|null $term_id The ID of the term we might be filtering. + * + * @return string The view link of the SEO score. + */ + public function get_view_link( string $seo_score_name, string $content_type, ?int $term_id ): string { + // @TODO. + return '#'; + } +} diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/seo-scores-route.php index 202845a884e..f4cd80e1036 100644 --- a/src/dashboard/user-interface/seo-scores-route.php +++ b/src/dashboard/user-interface/seo-scores-route.php @@ -6,8 +6,8 @@ use WP_REST_Response; use wpdb; use WPSEO_Capability_Utils; -use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Conditionals\No_Conditionals; +use Yoast\WP\SEO\Dashboard\Application\SEO_Scores\SEO_Scores_Repository; use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; use Yoast\WP\SEO\Main; use Yoast\WP\SEO\Repositories\Indexable_Repository; @@ -34,6 +34,13 @@ class SEO_Scores_Route implements Route_Interface { */ private $content_types_collector; + /** + * The SEO Scores repository. + * + * @var SEO_Scores_Repository + */ + private $seo_scores_repository; + /** * The indexable repository. * @@ -52,15 +59,18 @@ class SEO_Scores_Route implements Route_Interface { * Constructs the class. * * @param Content_Types_Collector $content_types_collector The content type collector. + * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. * @param Indexable_Repository $indexable_repository The indexable repository. * @param wpdb $wpdb The WordPress database object. */ public function __construct( Content_Types_Collector $content_types_collector, + SEO_Scores_Repository $seo_scores_repository, Indexable_Repository $indexable_repository, wpdb $wpdb ) { $this->content_types_collector = $content_types_collector; + $this->seo_scores_repository = $seo_scores_repository; $this->indexable_repository = $indexable_repository; $this->wpdb = $wpdb; } @@ -109,88 +119,18 @@ public function register_routes() { } /** - * Sets the value of the wistia embed permission. + * Gets the SEO scores of a specific content type. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function get_seo_scores( WP_REST_Request $request ) { - $content_type = $request['contentType']; - - $selects = [ - 'needs_improvement' => 'COUNT(CASE WHEN primary_focus_keyword_score < 41 THEN 1 END)', - 'ok' => 'COUNT(CASE WHEN primary_focus_keyword_score >= 41 AND primary_focus_keyword_score < 70 THEN 1 END)', - 'good' => 'COUNT(CASE WHEN primary_focus_keyword_score >= 71 THEN 1 END)', - 'not_analyzed' => 'COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END)', - ]; - - if ( $request['term'] === 0 || $request['taxonomy'] === '' ) { - // Without taxonomy filtering. - $counts = $this->indexable_repository->query() - ->select_many_expr( $selects ) - ->where_raw( '( post_status = \'publish\' OR post_status IS NULL )' ) - ->where_in( 'object_type', [ 'post' ] ) - ->where_in( 'object_sub_type', [ $content_type ] ) - ->find_one(); - - // This results in: - // SELECT - // COUNT(CASE WHEN primary_focus_keyword_score < 41 THEN 1 END) AS `needs_improvement`, - // COUNT(CASE WHEN primary_focus_keyword_score >= 41 AND primary_focus_keyword_score < 70 THEN 1 END) AS `ok`, - // COUNT(CASE WHEN primary_focus_keyword_score >= 71 THEN 1 END) AS `good`, - // COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END) AS `not_analyzed` - // FROM `wp_yoast_indexable` - // WHERE ( post_status = 'publish' OR post_status IS NULL ) - // AND `object_type` IN ('post') - // AND `object_sub_type` IN ('post') - // LIMIT 1 - - } - else { - // With taxonomy filtering. - $query = $this->wpdb->prepare( - " - SELECT - COUNT(CASE WHEN I.primary_focus_keyword_score < 41 THEN 1 END) AS `needs_improvement`, - COUNT(CASE WHEN I.primary_focus_keyword_score >= 41 AND I.primary_focus_keyword_score < 70 THEN 1 END) AS `ok`, - COUNT(CASE WHEN I.primary_focus_keyword_score >= 70 THEN 1 END) AS `good`, - COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS `not_analyzed` - FROM %i AS I - WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s) - AND I.object_id IN ( - SELECT object_id - FROM %i - WHERE term_taxonomy_id IN ( - SELECT term_taxonomy_id - FROM - %i - WHERE - term_id = %d - AND taxonomy = %s - ) - )", - Model::get_table_name( 'Indexable' ), - $content_type, - $this->wpdb->term_relationships, - $this->wpdb->term_taxonomy, - $request['term'], - $request['taxonomy'] - ); - - $counts = $this->wpdb->get_row( $query ); - } + $result = $this->seo_scores_repository->get_seo_scores( $request['contentType'], $request['taxonomy'], $request['term'] ); return new WP_REST_Response( [ - 'json' => (object) [ - 'good' => $counts->good, - 'ok' => $counts->ok, - 'needs_improvement' => $counts->needs_improvement, - 'not_analyzed' => $counts->not_analyzed, - ], + 'scores' => $result, ], 200 ); From ae7b25649da6cd722ef6d7fd40022beb3441b92a Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 18 Nov 2024 16:19:22 +0100 Subject: [PATCH 043/132] Revert "Small refactor + feedback." This reverts commit 042d654d925876f63ca2112c8a5d8108bb0b971e. --- admin/formatter/class-metabox-formatter.php | 21 +++++++++--------- .../configuration/dashboard-configuration.php | 22 +++++++++---------- ... enabled-analysis-features-repository.php} | 18 +++++++-------- 3 files changed, 30 insertions(+), 31 deletions(-) rename src/editors/application/analysis-features/{analysis-features-repository.php => enabled-analysis-features-repository.php} (74%) diff --git a/admin/formatter/class-metabox-formatter.php b/admin/formatter/class-metabox-formatter.php index aeabaacf4c4..02deb3c8eb4 100644 --- a/admin/formatter/class-metabox-formatter.php +++ b/admin/formatter/class-metabox-formatter.php @@ -6,7 +6,7 @@ */ use Yoast\WP\SEO\Config\Schema_Types; -use Yoast\WP\SEO\Editors\Application\Analysis_Features\Analysis_Features_Repository; +use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Application\Integrations\Integration_Information_Repository; /** @@ -51,32 +51,31 @@ private function get_defaults() { $schema_types = new Schema_Types(); $defaults = [ - 'author_name' => get_the_author_meta( 'display_name' ), - 'keyword_usage' => [], - 'title_template' => '', - 'metadesc_template' => '', - 'schema' => [ + 'author_name' => get_the_author_meta( 'display_name' ), + 'keyword_usage' => [], + 'title_template' => '', + 'metadesc_template' => '', + 'schema' => [ 'displayFooter' => WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ), 'pageTypeOptions' => $schema_types->get_page_type_options(), 'articleTypeOptions' => $schema_types->get_article_type_options(), ], - 'twitterCardType' => 'summary_large_image', + 'twitterCardType' => 'summary_large_image', /** * Filter to determine if the markers should be enabled or not. * * @param bool $showMarkers Should the markers being enabled. Default = true. */ - 'show_markers' => apply_filters( 'wpseo_enable_assessment_markers', true ), + 'show_markers' => apply_filters( 'wpseo_enable_assessment_markers', true ), ]; $integration_information_repo = YoastSEO()->classes->get( Integration_Information_Repository::class ); $enabled_integrations = $integration_information_repo->get_integration_information(); $defaults = array_merge( $defaults, $enabled_integrations ); - $enabled_features_repo = YoastSEO()->classes->get( Analysis_Features_Repository::class ); - - $enabled_features = $enabled_features_repo->get_analysis_features()->parse_to_legacy_array(); + $enabled_features_repo = YoastSEO()->classes->get( Enabled_Analysis_Features_Repository::class ); + $enabled_features = $enabled_features_repo->get_enabled_features()->parse_to_legacy_array(); return array_merge( $defaults, $enabled_features ); } } diff --git a/src/dashboard/application/configuration/dashboard-configuration.php b/src/dashboard/application/configuration/dashboard-configuration.php index f55073169b9..2a578848775 100644 --- a/src/dashboard/application/configuration/dashboard-configuration.php +++ b/src/dashboard/application/configuration/dashboard-configuration.php @@ -5,7 +5,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Configuration; use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; -use Yoast\WP\SEO\Editors\Application\Analysis_Features\Analysis_Features_Repository; +use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis; use Yoast\WP\SEO\Editors\Framework\Readability_Analysis; use Yoast\WP\SEO\Helpers\Indexable_Helper; @@ -40,23 +40,23 @@ class Dashboard_Configuration { /** * The repository. * - * @var Analysis_Features_Repository + * @var Enabled_Analysis_Features_Repository */ private $analysis_features_repository; /** * The constructor. * - * @param Content_Types_Repository $content_types_repository The content types repository. - * @param Indexable_Helper $indexable_helper The indexable helper repository. - * @param User_Helper $user_helper The user helper. - * @param Analysis_Features_Repository $analysis_features_repository The analysis feature repository. + * @param Content_Types_Repository $content_types_repository The content types repository. + * @param Indexable_Helper $indexable_helper The indexable helper repository. + * @param User_Helper $user_helper The user helper. + * @param Enabled_Analysis_Features_Repository $analysis_features_repository The analysis feature repository. */ public function __construct( Content_Types_Repository $content_types_repository, Indexable_Helper $indexable_helper, User_Helper $user_helper, - Analysis_Features_Repository $analysis_features_repository + Enabled_Analysis_Features_Repository $analysis_features_repository ) { $this->content_types_repository = $content_types_repository; $this->indexable_helper = $indexable_helper; @@ -71,10 +71,10 @@ public function __construct( */ public function get_configuration(): array { return [ - 'contentTypes' => $this->content_types_repository->get_content_types(), - 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), - 'displayName' => $this->user_helper->get_current_user_display_name(), - 'analysisFeatures' => $this->analysis_features_repository->get_analysis_features_by_keys( + 'contentTypes' => $this->content_types_repository->get_content_types(), + 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), + 'displayName' => $this->user_helper->get_current_user_display_name(), + 'enabledAnalysisFeatures' => $this->analysis_features_repository->get_enabled_features_by_keys( [ Readability_Analysis::NAME, Keyphrase_Analysis::NAME, diff --git a/src/editors/application/analysis-features/analysis-features-repository.php b/src/editors/application/analysis-features/enabled-analysis-features-repository.php similarity index 74% rename from src/editors/application/analysis-features/analysis-features-repository.php rename to src/editors/application/analysis-features/enabled-analysis-features-repository.php index d70aa57e4c9..9261d22aba3 100644 --- a/src/editors/application/analysis-features/analysis-features-repository.php +++ b/src/editors/application/analysis-features/enabled-analysis-features-repository.php @@ -12,7 +12,7 @@ * * @makePublic */ -class Analysis_Features_Repository { +class Enabled_Analysis_Features_Repository { /** * All plugin features. @@ -35,14 +35,14 @@ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { * * @return Analysis_Features_List The analysis list. */ - public function get_analysis_features(): Analysis_Features_List { - $analysis_features_list = new Analysis_Features_List(); + public function get_enabled_features(): Analysis_Features_List { + $enabled_analysis_features = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $analysis_features_list->add_feature( $analysis_feature ); + $enabled_analysis_features->add_feature( $analysis_feature ); } - return $analysis_features_list; + return $enabled_analysis_features; } /** @@ -52,16 +52,16 @@ public function get_analysis_features(): Analysis_Features_List { * * @return Analysis_Features_List The analysis list. */ - public function get_analysis_features_by_keys( array $feature_names ): Analysis_Features_List { - $analysis_features_list = new Analysis_Features_List(); + public function get_enabled_features_by_keys( array $feature_names ): Analysis_Features_List { + $enabled_analysis_features = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { if ( \in_array( $plugin_feature->get_name(), $feature_names, true ) ) { $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $analysis_features_list->add_feature( $analysis_feature ); + $enabled_analysis_features->add_feature( $analysis_feature ); } } - return $analysis_features_list; + return $enabled_analysis_features; } } From c154ab86dd3374fb9a18a021059ff59984e1777f Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 18 Nov 2024 16:21:54 +0100 Subject: [PATCH 044/132] Revert refactors. --- .../configuration/dashboard-configuration.php | 24 ++++++++++--------- .../enabled-analysis-features-repository.php | 23 ++++++++++++------ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/dashboard/application/configuration/dashboard-configuration.php b/src/dashboard/application/configuration/dashboard-configuration.php index 2a578848775..c9eb9fef674 100644 --- a/src/dashboard/application/configuration/dashboard-configuration.php +++ b/src/dashboard/application/configuration/dashboard-configuration.php @@ -42,26 +42,28 @@ class Dashboard_Configuration { * * @var Enabled_Analysis_Features_Repository */ - private $analysis_features_repository; + private $enabled_analysis_features_repository; /** * The constructor. * - * @param Content_Types_Repository $content_types_repository The content types repository. - * @param Indexable_Helper $indexable_helper The indexable helper repository. - * @param User_Helper $user_helper The user helper. - * @param Enabled_Analysis_Features_Repository $analysis_features_repository The analysis feature repository. + * @param Content_Types_Repository $content_types_repository The content types repository. + * @param Indexable_Helper $indexable_helper The indexable helper + * repository. + * @param User_Helper $user_helper The user helper. + * @param Enabled_Analysis_Features_Repository $enabled_analysis_features_repository The analysis feature + * repository. */ public function __construct( Content_Types_Repository $content_types_repository, Indexable_Helper $indexable_helper, User_Helper $user_helper, - Enabled_Analysis_Features_Repository $analysis_features_repository + Enabled_Analysis_Features_Repository $enabled_analysis_features_repository ) { - $this->content_types_repository = $content_types_repository; - $this->indexable_helper = $indexable_helper; - $this->user_helper = $user_helper; - $this->analysis_features_repository = $analysis_features_repository; + $this->content_types_repository = $content_types_repository; + $this->indexable_helper = $indexable_helper; + $this->user_helper = $user_helper; + $this->enabled_analysis_features_repository = $enabled_analysis_features_repository; } /** @@ -74,7 +76,7 @@ public function get_configuration(): array { 'contentTypes' => $this->content_types_repository->get_content_types(), 'indexablesEnabled' => $this->indexable_helper->should_index_indexables(), 'displayName' => $this->user_helper->get_current_user_display_name(), - 'enabledAnalysisFeatures' => $this->analysis_features_repository->get_enabled_features_by_keys( + 'enabledAnalysisFeatures' => $this->enabled_analysis_features_repository->get_features_by_keys( [ Readability_Analysis::NAME, Keyphrase_Analysis::NAME, diff --git a/src/editors/application/analysis-features/enabled-analysis-features-repository.php b/src/editors/application/analysis-features/enabled-analysis-features-repository.php index 9261d22aba3..66312a610a7 100644 --- a/src/editors/application/analysis-features/enabled-analysis-features-repository.php +++ b/src/editors/application/analysis-features/enabled-analysis-features-repository.php @@ -21,13 +21,21 @@ class Enabled_Analysis_Features_Repository { */ private $plugin_features; + /** + * The list of analysis features. + * + * @var Analysis_Features_List + */ + private $enabled_analysis_features; + /** * The constructor. * * @param Analysis_Feature_Interface ...$plugin_features All analysis objects. */ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { - $this->plugin_features = $plugin_features; + $this->enabled_analysis_features = new Analysis_Features_List(); + $this->plugin_features = $plugin_features; } /** @@ -36,13 +44,14 @@ public function __construct( Analysis_Feature_Interface ...$plugin_features ) { * @return Analysis_Features_List The analysis list. */ public function get_enabled_features(): Analysis_Features_List { - $enabled_analysis_features = new Analysis_Features_List(); - foreach ( $this->plugin_features as $plugin_feature ) { - $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); - $enabled_analysis_features->add_feature( $analysis_feature ); + if ( \count( $this->enabled_analysis_features->parse_to_legacy_array() ) === 0 ) { + foreach ( $this->plugin_features as $plugin_feature ) { + $analysis_feature = new Analysis_Feature( $plugin_feature->is_enabled(), $plugin_feature->get_name(), $plugin_feature->get_legacy_key() ); + $this->enabled_analysis_features->add_feature( $analysis_feature ); + } } - return $enabled_analysis_features; + return $this->enabled_analysis_features; } /** @@ -52,7 +61,7 @@ public function get_enabled_features(): Analysis_Features_List { * * @return Analysis_Features_List The analysis list. */ - public function get_enabled_features_by_keys( array $feature_names ): Analysis_Features_List { + public function get_features_by_keys( array $feature_names ): Analysis_Features_List { $enabled_analysis_features = new Analysis_Features_List(); foreach ( $this->plugin_features as $plugin_feature ) { From 0a88fc47ff6489be13afc4f948cf43357fda84ab Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 19 Nov 2024 11:47:16 +0200 Subject: [PATCH 045/132] Add link to API response --- .../seo-scores/seo-scores-repository.php | 4 +-- .../domain/seo-scores/abstract-seo-score.php | 21 +++++++++--- .../domain/seo-scores/bad-seo-score.php | 9 +++++ .../domain/seo-scores/good-seo-score.php | 9 +++++ .../domain/seo-scores/no-seo-score.php | 9 +++++ .../domain/seo-scores/ok-seo-score.php | 9 +++++ .../seo-scores/seo-scores-interface.php | 9 ++++- .../seo-scores/seo-scores-collector.php | 34 +++++++++++++++---- 8 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php index a5a88319e19..89e882b9227 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -47,13 +47,13 @@ public function __construct( * * @return array>> The SEO scores. */ - public function get_seo_scores( string $content_type, ?string $taxonomy, ?int $term_id ): array { + public function get_seo_scores( string $content_type, string $taxonomy, int $term_id ): array { $seo_scores = []; $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $taxonomy, $term_id ); foreach ( $this->seo_scores as $seo_score ) { $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); - $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score->get_name(), $content_type, $term_id ) ); + $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score->get_filter_name(), $content_type, $taxonomy, $term_id ) ); $seo_scores[] = $seo_score->to_array(); } diff --git a/src/dashboard/domain/seo-scores/abstract-seo-score.php b/src/dashboard/domain/seo-scores/abstract-seo-score.php index ec923480727..ccab858949d 100644 --- a/src/dashboard/domain/seo-scores/abstract-seo-score.php +++ b/src/dashboard/domain/seo-scores/abstract-seo-score.php @@ -14,6 +14,13 @@ abstract class Abstract_SEO_Score implements SEO_Scores_Interface { */ private $name; + /** + * The name of the SEO score that is used when filtering on the posts page. + * + * @var string + */ + private $filter_name; + /** * The amount of the SEO score. * @@ -60,7 +67,7 @@ public function set_amount( int $amount ): void { * * @return void */ - public function set_view_link( string $view_link ): void { + public function set_view_link( ?string $view_link ): void { $this->view_link = $view_link; } @@ -70,12 +77,16 @@ public function set_view_link( string $view_link ): void { * @return array> The SEO score presented as the expected key value representation. */ public function to_array(): array { - return [ + $array = [ 'name' => $this->get_name(), 'amount' => $this->amount, - 'links' => [ - 'view' => $this->view_link, - ], + 'links' => [], ]; + + if ( $this->view_link !== null ) { + $array['links']['view'] = $this->view_link; + } + + return $array; } } diff --git a/src/dashboard/domain/seo-scores/bad-seo-score.php b/src/dashboard/domain/seo-scores/bad-seo-score.php index cf42366a043..d40b41d102d 100644 --- a/src/dashboard/domain/seo-scores/bad-seo-score.php +++ b/src/dashboard/domain/seo-scores/bad-seo-score.php @@ -16,6 +16,15 @@ public function get_name(): string { return 'bad'; } + /** + * Gets the name of the SEO score that is used when filtering on the posts page. + * + * @return string The name of the SEO score that is used when filtering on the posts page. + */ + public function get_filter_name(): string { + return 'bad'; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/good-seo-score.php b/src/dashboard/domain/seo-scores/good-seo-score.php index fa22422a78d..3143bc9e162 100644 --- a/src/dashboard/domain/seo-scores/good-seo-score.php +++ b/src/dashboard/domain/seo-scores/good-seo-score.php @@ -16,6 +16,15 @@ public function get_name(): string { return 'good'; } + /** + * Gets the name of the SEO score that is used when filtering on the posts page. + * + * @return string The name of the SEO score that is used when filtering on the posts page. + */ + public function get_filter_name(): string { + return 'good'; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/no-seo-score.php b/src/dashboard/domain/seo-scores/no-seo-score.php index 88bf1c25632..af62da975f1 100644 --- a/src/dashboard/domain/seo-scores/no-seo-score.php +++ b/src/dashboard/domain/seo-scores/no-seo-score.php @@ -16,6 +16,15 @@ public function get_name(): string { return 'notAnalyzed'; } + /** + * Gets the name of the SEO score that is used when filtering on the posts page. + * + * @return string The name of the SEO score that is used when filtering on the posts page. + */ + public function get_filter_name(): string { + return 'na'; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/ok-seo-score.php b/src/dashboard/domain/seo-scores/ok-seo-score.php index d04484df9ba..4149c32da3e 100644 --- a/src/dashboard/domain/seo-scores/ok-seo-score.php +++ b/src/dashboard/domain/seo-scores/ok-seo-score.php @@ -16,6 +16,15 @@ public function get_name(): string { return 'ok'; } + /** + * Gets the name of the SEO score that is used when filtering on the posts page. + * + * @return string The name of the SEO score that is used when filtering on the posts page. + */ + public function get_filter_name(): string { + return 'ok'; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/seo-scores-interface.php b/src/dashboard/domain/seo-scores/seo-scores-interface.php index f19cd6aaa7b..48cbb7448af 100644 --- a/src/dashboard/domain/seo-scores/seo-scores-interface.php +++ b/src/dashboard/domain/seo-scores/seo-scores-interface.php @@ -14,6 +14,13 @@ interface SEO_Scores_Interface { */ public function get_name(): string; + /** + * Gets the name of the SEO score that is used when filtering on the posts page. + * + * @return string + */ + public function get_filter_name(): string; + /** * Gets the minimum score of the SEO score. * @@ -44,7 +51,7 @@ public function set_amount( int $amount ): void; * * @return void */ - public function set_view_link( string $view_link ): void; + public function set_view_link( ?string $view_link ): void; /** * Parses the SEO score to the expected key value representation. diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php index 671776e612e..878cebeec70 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -136,14 +136,36 @@ private function build_select( array $seo_scores ): array { /** * Builds the view link of the SEO score. * - * @param string $seo_score_name The name of the SEO score. - * @param string $content_type The content type. - * @param int|null $term_id The ID of the term we might be filtering. + * @param string $seo_score_name The name of the SEO score. + * @param string $content_type The content type. + * @param string $taxonomy The taxonomy of the term we might be filtering. + * @param int $term_id The ID of the term we might be filtering. * * @return string The view link of the SEO score. */ - public function get_view_link( string $seo_score_name, string $content_type, ?int $term_id ): string { - // @TODO. - return '#'; + public function get_view_link( string $seo_score_name, string $content_type, string $taxonomy, int $term_id ): ?string { + // @TODO: Refactor by Single Source of Truthing this with the `WPSEO_Meta_Columns` class. Until then, we build this manually. + $posts_page = \admin_url( 'edit.php' ); + $args = [ + 'post_status' => 'publish', + 'post_type' => $content_type, + 'seo_filter' => $seo_score_name, + ]; + + if ( $taxonomy === '' || $term_id === 0 ) { + return \add_query_arg( $args, $posts_page ); + } + + $taxonomy_object = \get_taxonomy( $taxonomy ); + $query_var = $taxonomy_object->query_var; + + if ( $query_var === false ) { + return null; + } + + $term = \get_term( $term_id ); + $args[ $query_var ] = $term->slug; + + return \add_query_arg( $args, $posts_page ); } } From 4fb8959a03e3deac9fbcba90c216f944e835972b Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:55:41 +0100 Subject: [PATCH 046/132] Add features and links data to the Dashboard * using prop drilling instead of context/store: trying to keep it simple * inventing features structure, not convinced this makes sense (for one, it is platform dependent) * presuming the base url is the same for the site features link --- .../js/src/dashboard/components/dashboard.js | 10 +++-- .../js/src/dashboard/components/page-title.js | 37 ++++++++++++++++--- packages/js/src/dashboard/index.js | 7 ++++ .../scores/components/content-type-filter.js | 2 +- packages/js/src/general/initialize.js | 20 +++++++++- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/js/src/dashboard/components/dashboard.js b/packages/js/src/dashboard/components/dashboard.js index a5a9a3b9503..193ef04d044 100644 --- a/packages/js/src/dashboard/components/dashboard.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -4,20 +4,22 @@ import { PageTitle } from "./page-title"; /** * @type {import("../index").ContentType} ContentType + * @type {import("../index").Features} Features */ /** * @param {ContentType[]} contentTypes The content types. * @param {string} userName The user name. + * @param {Features} features Whether features are enabled. * @returns {JSX.Element} The element. */ -export const Dashboard = ( { contentTypes, userName } ) => { +export const Dashboard = ( { contentTypes, userName, features } ) => { return ( <> - +
      - - + { features.indexables && features.seoAnalysis && } + { features.indexables && features.readabilityAnalysis && }
      ); diff --git a/packages/js/src/dashboard/components/page-title.js b/packages/js/src/dashboard/components/page-title.js index c15b7008603..c8d518cd7ce 100644 --- a/packages/js/src/dashboard/components/page-title.js +++ b/packages/js/src/dashboard/components/page-title.js @@ -1,22 +1,47 @@ +import { createInterpolateElement } from "@wordpress/element"; import { __, sprintf } from "@wordpress/i18n"; -import { Paper, Title } from "@yoast/ui-library"; +import { Alert, Link, Paper, Title } from "@yoast/ui-library"; + +/** + * @type {import("../index").Features} Features + */ /** * @param {string} userName The user name. + * @param {Features} features Whether features are enabled. * @returns {JSX.Element} The element. */ -export const PageTitle = ( { userName } ) => ( +export const PageTitle = ( { userName, features } ) => ( - + { sprintf( - __( "Hi %s!", "wordpress-seo" ), + __( "Hi %s,", "wordpress-seo" ), userName ) } -

      - { __( "Welcome to your SEO dashboard!", "wordpress-seo" ) } +

      + { features.indexables && ! features.seoAnalysis && ! features.readabilityAnalysis + ? createInterpolateElement( + sprintf( + /* translators: %1$s and %2$s expand to an opening and closing anchor tag. */ + __( "It seems that the SEO analysis and the Readability analysis are currently disabled in your %1$sSite features%2$s. Once you enable these features, you'll be able to see the insights you need right here!", "wordpress-seo" ), + "", + "" + ), + { + // Added dummy space as content to prevent children prop warnings in the console. + link: , + } + ) + : __( "Welcome to our SEO dashboard!", "wordpress-seo" ) + }

      + { ! features.indexables && ( + + { __( "The overview of your SEO scores and Readability scores is not available because you're on a non-production environment.", "wordpress-seo" ) } + + ) }
      ); diff --git a/packages/js/src/dashboard/index.js b/packages/js/src/dashboard/index.js index 8708ef50873..e238875c015 100644 --- a/packages/js/src/dashboard/index.js +++ b/packages/js/src/dashboard/index.js @@ -28,3 +28,10 @@ export { Dashboard } from "./components/dashboard"; * @property {Object} links The links. * @property {string} [links.view] The view link, might not exist. */ + +/** + * @typedef {Object} Features Whether features are enabled. + * @property {boolean} indexables Whether indexables are enabled. + * @property {boolean} seoAnalysis Whether SEO analysis is enabled. + * @property {boolean} readabilityAnalysis Whether readability analysis is enabled. + */ diff --git a/packages/js/src/dashboard/scores/components/content-type-filter.js b/packages/js/src/dashboard/scores/components/content-type-filter.js index f4cc7633ab9..2bce8f44624 100644 --- a/packages/js/src/dashboard/scores/components/content-type-filter.js +++ b/packages/js/src/dashboard/scores/components/content-type-filter.js @@ -3,7 +3,7 @@ import { __ } from "@wordpress/i18n"; import { AutocompleteField } from "@yoast/ui-library"; /** - * @typedef {import("./dashboard").ContentType} ContentType + * @type {import("../index").ContentType} ContentType */ /** diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 782ed289d05..49990a4a174 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -16,6 +16,12 @@ import registerStore from "./store"; import { ALERT_CENTER_NAME } from "./store/alert-center"; import { FTC_NAME } from "./store/first-time-configuration"; +/** + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Features} Features + * @type {import("../index").Links} Links + */ + domReady( () => { const root = document.getElementById( "yoast-seo-general" ); if ( ! root ) { @@ -33,15 +39,27 @@ domReady( () => { } ); const isRtl = select( STORE_NAME ).selectPreference( "isRtl", false ); + /** @type {ContentType[]} */ const contentTypes = get( window, "wpseoScriptData.dashboard.contentTypes", [] ); + /** @type {string} */ const userName = get( window, "wpseoScriptData.dashboard.displayName", "User" ); + /** @type {Features} */ + const features = { + indexables: get( window, "wpseoScriptData.dashboard.indexablesEnabled", false ), + seoAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.keyphraseAnalysis", false ), + readabilityAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.readabilityAnalysis", false ), + }; const router = createHashRouter( createRoutesFromElements( } errorElement={ }> } + element={ + + + + } errorElement={ } /> Date: Tue, 19 Nov 2024 13:24:16 +0200 Subject: [PATCH 047/132] Use domain context for validating REST requests --- .../seo-scores/seo-scores-repository.php | 12 ++- src/dashboard/domain/taxonomies/taxonomy.php | 9 ++ .../content-types/content-types-collector.php | 2 +- .../seo-scores/seo-scores-collector.php | 34 ++++--- .../user-interface/seo-scores-route.php | 97 +++++++++++++------ 5 files changed, 102 insertions(+), 52 deletions(-) diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php index 89e882b9227..45c69d22ea1 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -2,7 +2,9 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\Dashboard\Application\SEO_Scores; +use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\SEO_Scores\SEO_Scores_Collector; /** @@ -41,19 +43,19 @@ public function __construct( /** * Returns the SEO Scores of a content type. * - * @param string $content_type The content type. - * @param string $taxonomy The taxonomy of the term we're filtering for. - * @param int $term_id The ID of the term we're filtering for. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. * * @return array>> The SEO scores. */ - public function get_seo_scores( string $content_type, string $taxonomy, int $term_id ): array { + public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $seo_scores = []; $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $taxonomy, $term_id ); foreach ( $this->seo_scores as $seo_score ) { $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); - $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score->get_filter_name(), $content_type, $taxonomy, $term_id ) ); + $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); $seo_scores[] = $seo_score->to_array(); } diff --git a/src/dashboard/domain/taxonomies/taxonomy.php b/src/dashboard/domain/taxonomies/taxonomy.php index 0765c1289ab..df213f5939e 100644 --- a/src/dashboard/domain/taxonomies/taxonomy.php +++ b/src/dashboard/domain/taxonomies/taxonomy.php @@ -45,6 +45,15 @@ public function __construct( $this->rest_url = $rest_url; } + /** + * Returns the name of the taxonomy. + * + * @return string The name of the taxonomy. + */ + public function get_name(): string { + return $this->name; + } + /** * Parses the taxonomy to the expected key value representation. * diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php index 99244c405c2..46a5c9bf709 100644 --- a/src/dashboard/infrastructure/content-types/content-types-collector.php +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -31,7 +31,7 @@ public function __construct( /** * Returns the content types array. * - * @return array>>>> The content types array. + * @return Content_Type[] The content types array. */ public function get_content_types(): array { $content_types = []; diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php index 878cebeec70..394f5779433 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -4,7 +4,9 @@ use wpdb; use Yoast\WP\Lib\Model; +use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; /** * Getting SEO scores from the indexable database table. @@ -33,24 +35,24 @@ public function __construct( * Retrieves the current SEO scores for a content type. * * @param SEO_Scores_Interface[] $seo_scores All SEO scores. - * @param string $content_type The content type. - * @param string $taxonomy The taxonomy of the term we're filtering for. - * @param int $term_id The ID of the term we're filtering for. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. * * @return array The SEO scores for a content type. */ - public function get_seo_scores( array $seo_scores, string $content_type, string $taxonomy, int $term_id ) { + public function get_seo_scores( array $seo_scores, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { $select = $this->build_select( $seo_scores ); $replacements = \array_merge( \array_values( $select['replacements'] ), [ Model::get_table_name( 'Indexable' ), - $content_type, + $content_type->get_name(), ] ); - if ( $term_id === 0 || $taxonomy === '' ) { + if ( $term_id === null || $taxonomy === null ) { $query = $this->wpdb->prepare( " SELECT {$select['fields']} @@ -69,7 +71,7 @@ public function get_seo_scores( array $seo_scores, string $content_type, string $replacements[] = $this->wpdb->term_relationships; $replacements[] = $this->wpdb->term_taxonomy; $replacements[] = $term_id; - $replacements[] = $taxonomy; + $replacements[] = $taxonomy->get_name(); $query = $this->wpdb->prepare( " @@ -136,27 +138,27 @@ private function build_select( array $seo_scores ): array { /** * Builds the view link of the SEO score. * - * @param string $seo_score_name The name of the SEO score. - * @param string $content_type The content type. - * @param string $taxonomy The taxonomy of the term we might be filtering. - * @param int $term_id The ID of the term we might be filtering. + * @param SEO_Scores_Interface $seo_score_name The name of the SEO score. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. + * @param int|null $term_id The ID of the term we might be filtering. * * @return string The view link of the SEO score. */ - public function get_view_link( string $seo_score_name, string $content_type, string $taxonomy, int $term_id ): ?string { + public function get_view_link( SEO_Scores_Interface $seo_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { // @TODO: Refactor by Single Source of Truthing this with the `WPSEO_Meta_Columns` class. Until then, we build this manually. $posts_page = \admin_url( 'edit.php' ); $args = [ 'post_status' => 'publish', - 'post_type' => $content_type, - 'seo_filter' => $seo_score_name, + 'post_type' => $content_type->get_name(), + 'seo_filter' => $seo_score_name->get_filter_name(), ]; - if ( $taxonomy === '' || $term_id === 0 ) { + if ( $taxonomy === null || $term_id === null ) { return \add_query_arg( $args, $posts_page ); } - $taxonomy_object = \get_taxonomy( $taxonomy ); + $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); $query_var = $taxonomy_object->query_var; if ( $query_var === false ) { diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/seo-scores-route.php index f4cd80e1036..df04d4741db 100644 --- a/src/dashboard/user-interface/seo-scores-route.php +++ b/src/dashboard/user-interface/seo-scores-route.php @@ -8,7 +8,10 @@ use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\SEO_Scores\SEO_Scores_Repository; +use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; +use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector; use Yoast\WP\SEO\Main; use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Routes\Route_Interface; @@ -34,6 +37,13 @@ class SEO_Scores_Route implements Route_Interface { */ private $content_types_collector; + /** + * The taxonomies collector. + * + * @var Taxonomies_Collector + */ + private $taxonomies_collector; + /** * The SEO Scores repository. * @@ -59,17 +69,20 @@ class SEO_Scores_Route implements Route_Interface { * Constructs the class. * * @param Content_Types_Collector $content_types_collector The content type collector. + * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. * @param Indexable_Repository $indexable_repository The indexable repository. * @param wpdb $wpdb The WordPress database object. */ public function __construct( Content_Types_Collector $content_types_collector, + Taxonomies_Collector $taxonomies_collector, SEO_Scores_Repository $seo_scores_repository, Indexable_Repository $indexable_repository, wpdb $wpdb ) { $this->content_types_collector = $content_types_collector; + $this->taxonomies_collector = $taxonomies_collector; $this->seo_scores_repository = $seo_scores_repository; $this->indexable_repository = $indexable_repository; $this->wpdb = $wpdb; @@ -94,23 +107,20 @@ public function register_routes() { 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => [ $this, 'validate_content_type' ], ], 'term' => [ 'required' => false, 'type' => 'integer', - 'default' => 0, + 'default' => null, 'sanitize_callback' => static function ( $param ) { return \intval( $param ); }, - 'validate_callback' => [ $this, 'validate_term' ], ], 'taxonomy' => [ 'required' => false, 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => [ $this, 'validate_taxonomy' ], ], ], ], @@ -126,7 +136,36 @@ public function register_routes() { * @return WP_REST_Response|WP_Error The success or failure response. */ public function get_seo_scores( WP_REST_Request $request ) { - $result = $this->seo_scores_repository->get_seo_scores( $request['contentType'], $request['taxonomy'], $request['term'] ); + $content_type = $this->get_content_type( $request['contentType'] ); + if ( $content_type === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid content type.', + ], + 400 + ); + } + + $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); + if ( $request['taxonomy'] !== '' && $taxonomy === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid taxonomy.', + ], + 400 + ); + } + + if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid term.', + ], + 400 + ); + } + + $result = $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $request['term'] ); return new WP_REST_Response( [ @@ -137,48 +176,48 @@ public function get_seo_scores( WP_REST_Request $request ) { } /** - * Validates the content type against the content types collector. + * Gets the content type object. * * @param string $content_type The content type. * - * @return bool Whether the content type passed validation. + * @return Content_Type|null The content type object. */ - public function validate_content_type( $content_type ) { - // @TODO: Is it necessary to go through all the indexable content types again and validate against those? If so, it can look like this. + protected function get_content_type( string $content_type ): ?Content_Type { $content_types = $this->content_types_collector->get_content_types(); - if ( isset( $content_types[ $content_type ] ) ) { - return true; + if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { + return $content_types[ $content_type ]; } - return false; + return null; } /** - * Validates the taxonomy against the given content type. + * Gets the taxonomy object. * - * @param string $taxonomy The taxonomy. - * @param WP_REST_Request $request The request object. + * @param string $taxonomy The taxonomy. + * @param Content_Type $content_type The content type that the taxonomy is filtering. * - * @return bool Whether the taxonomy passed validation. + * @return Taxonomy|null The taxonomy object. */ - public function validate_taxonomy( $taxonomy, $request ) { - // @TODO: Is it necessary to validate against content types? If so, it can take inspiration from validate_content_type(). - return true; + protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { + if ( $taxonomy === '' ) { + return null; + } + return $this->taxonomies_collector->get_taxonomy( $taxonomy, $content_type->get_name() ); } /** - * Validates the term against the given content type. + * Validates the term against the given taxonomy. * - * @param int $term_id The term ID. - * @param WP_REST_Request $request The request object. + * @param int $term_id The ID of the term. + * @param Taxonomy|null $taxonomy The taxonomy. * * @return bool Whether the term passed validation. */ - public function validate_term( $term_id, $request ) { - // @TODO: Is it necessary to validate against content types? If so, it can look like this. - if ( $request['term'] === 0 ) { - return true; + protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { + if ( $term_id === null ) { + return ( $taxonomy === null ); } $term = \get_term( $term_id ); @@ -186,10 +225,8 @@ public function validate_term( $term_id, $request ) { return false; } - $post_type = $request['contentType']; - - // Check if the term's taxonomy is associated with the post type. - return \in_array( $term->taxonomy, \get_object_taxonomies( $post_type ), true ); + $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); + return $term->taxonomy === $taxonomy_name; } /** From 933ff1048143c9e8515370f631a99c12ec022bb4 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:26:11 +0100 Subject: [PATCH 048/132] Implement descriptions Descriptions are based on the analysis type and the score "type" --- packages/js/src/dashboard/index.js | 10 ++++++- .../components/content-status-description.js | 15 ++++++----- .../scores/components/score-content.js | 6 +++-- .../scores/readability/readability-scores.js | 3 ++- .../js/src/dashboard/scores/score-meta.js | 26 +++++++++++++++++++ .../js/src/dashboard/scores/seo/seo-scores.js | 3 ++- 6 files changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/js/src/dashboard/index.js b/packages/js/src/dashboard/index.js index e238875c015..ce58d85df75 100644 --- a/packages/js/src/dashboard/index.js +++ b/packages/js/src/dashboard/index.js @@ -21,9 +21,17 @@ export { Dashboard } from "./components/dashboard"; * @property {string} label The user-facing label. */ +/** + * @typedef {"seo"|"readability"} AnalysisType The analysis type. + */ + +/** + * @typedef {"ok"|"good"|"bad"|"notAnalyzed"} ScoreType The score type. + */ + /** * @typedef {Object} Score A score. - * @property {string} name The name of the score. Can be "ok", "good", "bad" or "notAnalyzed". + * @property {ScoreType} name The name of the score. * @property {number} amount The amount of content for this score. * @property {Object} links The links. * @property {string} [links.view] The view link, might not exist. diff --git a/packages/js/src/dashboard/scores/components/content-status-description.js b/packages/js/src/dashboard/scores/components/content-status-description.js index 4ef7504a937..fbd5e269234 100644 --- a/packages/js/src/dashboard/scores/components/content-status-description.js +++ b/packages/js/src/dashboard/scores/components/content-status-description.js @@ -1,16 +1,17 @@ -import { __ } from "@wordpress/i18n"; +import { maxBy } from "lodash"; /** * @type {import("../index").Score} Score + * @type {import("../index").ScoreType} ScoreType */ /** - * @param {Score[]|null} scores The SEO scores. + * @param {Score[]} scores The SEO scores. + * @param {Object.} descriptions The descriptions. * @returns {JSX.Element} The element. */ -export const ContentStatusDescription = ( { scores } ) => { - if ( ! scores ) { - return

      { __( "No scores could be retrieved Or maybe loading??", "wordpress-seo" ) }

      ; - } - return

      { __( "description placeholder", "wordpress-seo" ) }

      ; +export const ContentStatusDescription = ( { scores, descriptions } ) => { + const maxScore = maxBy( scores, "amount" ); + + return

      { descriptions[ maxScore.name ] || "" }

      ; }; diff --git a/packages/js/src/dashboard/scores/components/score-content.js b/packages/js/src/dashboard/scores/components/score-content.js index d5acfe7dcf4..468433a1e42 100644 --- a/packages/js/src/dashboard/scores/components/score-content.js +++ b/packages/js/src/dashboard/scores/components/score-content.js @@ -6,6 +6,7 @@ import { ScoreList } from "./score-list"; /** * @type {import("../index").Score} Score + * @type {import("../index").ScoreType} ScoreType */ /** @@ -39,16 +40,17 @@ const ScoreContentSkeletonLoader = () => ( /** * @param {Score[]} [scores=[]] The scores. * @param {boolean} isLoading Whether the scores are still loading. + * @param {Object.} descriptions The descriptions. * @returns {JSX.Element} The element. */ -export const ScoreContent = ( { scores = [], isLoading } ) => { +export const ScoreContent = ( { scores = [], isLoading, descriptions } ) => { if ( isLoading ) { return ; } return ( <> - +
      { scores && } { scores && } diff --git a/packages/js/src/dashboard/scores/readability/readability-scores.js b/packages/js/src/dashboard/scores/readability/readability-scores.js index c959d1175f1..3a420aab01b 100644 --- a/packages/js/src/dashboard/scores/readability/readability-scores.js +++ b/packages/js/src/dashboard/scores/readability/readability-scores.js @@ -5,6 +5,7 @@ import { useFetch } from "../../hooks/use-fetch"; import { ContentTypeFilter } from "../components/content-type-filter"; import { ScoreContent } from "../components/score-content"; import { TermFilter } from "../components/term-filter"; +import { SCORE_DESCRIPTIONS } from "../score-meta"; /** * @type {import("../index").ContentType} ContentType @@ -59,7 +60,7 @@ export const ReadabilityScores = ( { contentTypes } ) => { /> }
      - + ); }; diff --git a/packages/js/src/dashboard/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js index ab247b0ad9b..ef4cd27a96e 100644 --- a/packages/js/src/dashboard/scores/score-meta.js +++ b/packages/js/src/dashboard/scores/score-meta.js @@ -1,5 +1,13 @@ import { __ } from "@wordpress/i18n"; +/** + * @type {import("../index").AnalysisType} AnalysisType + * @type {import("../index").ScoreType} ScoreType + */ + +/** + * @type {Object.} The meta data. + */ export const SCORE_META = { good: { label: __( "Good", "wordpress-seo" ), @@ -22,3 +30,21 @@ export const SCORE_META = { hex: "#cbd5e1", }, }; + +/** + * @type {Object.>} The descriptions. + */ +export const SCORE_DESCRIPTIONS = { + seo: { + good: __( "Most of your content has a good SEO score. Well done!", "wordpress-seo" ), + ok: __( "Your content has an average SEO score. Find opportunities for enhancement.", "wordpress-seo" ), + bad: __( "Some of your content needs attention. Identify and address areas for improvement.", "wordpress-seo" ), + notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it in your editor so we can analyze it.", "wordpress-seo" ), + }, + readability: { + good: __( "Most of your content has a good Readability score. Well done!", "wordpress-seo" ), + ok: __( "Your content has an average Readability score. Find opportunities for enhancement.", "wordpress-seo" ), + bad: __( "Some of your content needs attention. Identify and address areas for improvement.", "wordpress-seo" ), + notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it in your editor so we can analyze it.", "wordpress-seo" ), + }, +}; diff --git a/packages/js/src/dashboard/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js index fb30520789e..520b4b2541e 100644 --- a/packages/js/src/dashboard/scores/seo/seo-scores.js +++ b/packages/js/src/dashboard/scores/seo/seo-scores.js @@ -5,6 +5,7 @@ import { useFetch } from "../../hooks/use-fetch"; import { ContentTypeFilter } from "../components/content-type-filter"; import { ScoreContent } from "../components/score-content"; import { TermFilter } from "../components/term-filter"; +import { SCORE_DESCRIPTIONS } from "../score-meta"; /** * @type {import("../index").ContentType} ContentType @@ -60,7 +61,7 @@ export const SeoScores = ( { contentTypes } ) => { /> }
      - + ); }; From 231c22862fd491852a7c292ec5f9e2d273e2f4c6 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:27:26 +0100 Subject: [PATCH 049/132] Change the initial isPending to true This prevents the initial rendering of the descriptions without any score As the useFetch has a useEffect which should trigger on rending anyway, this makes sense --- packages/js/src/dashboard/hooks/use-fetch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/hooks/use-fetch.js b/packages/js/src/dashboard/hooks/use-fetch.js index f84ced169e8..7b41afc6df7 100644 --- a/packages/js/src/dashboard/hooks/use-fetch.js +++ b/packages/js/src/dashboard/hooks/use-fetch.js @@ -44,7 +44,7 @@ const fetchJson = async( url, options ) => { * @returns {FetchResult} The fetch result. */ export const useFetch = ( { dependencies, url, options, prepareData = identity, doFetch = fetchJson, fetchDelay = FETCH_DELAY } ) => { - const [ isPending, setIsPending ] = useState( false ); + const [ isPending, setIsPending ] = useState( true ); const [ error, setError ] = useState(); const [ data, setData ] = useState(); /** @type {MutableRefObject} */ From d67fdfd0ba4e1f1730ad7b840863b66261770cbc Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:28:29 +0100 Subject: [PATCH 050/132] For testing Add random amounts (and links) for now --- .../src/dashboard/scores/readability/readability-scores.js | 6 ++++++ packages/js/src/dashboard/scores/seo/seo-scores.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/js/src/dashboard/scores/readability/readability-scores.js b/packages/js/src/dashboard/scores/readability/readability-scores.js index 3a420aab01b..b471cc7a97d 100644 --- a/packages/js/src/dashboard/scores/readability/readability-scores.js +++ b/packages/js/src/dashboard/scores/readability/readability-scores.js @@ -28,6 +28,12 @@ export const ReadabilityScores = ( { contentTypes } ) => { fetchDelay: 0, doFetch: async( url, options ) => { await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + return [ "good", "ok", "bad", "notAnalyzed" ].map( ( name ) => ( { + name, + amount: Math.ceil( Math.random() * 10 ), + links: Math.random() > 0.5 ? {} : { view: `edit.php?readability_filter=${ name }` }, + } ) ); + // eslint-disable-next-line no-unreachable try { const response = await fetch( url, options ); if ( ! response.ok ) { diff --git a/packages/js/src/dashboard/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js index 520b4b2541e..0bcc1860fcc 100644 --- a/packages/js/src/dashboard/scores/seo/seo-scores.js +++ b/packages/js/src/dashboard/scores/seo/seo-scores.js @@ -29,6 +29,12 @@ export const SeoScores = ( { contentTypes } ) => { fetchDelay: 0, doFetch: async( url, options ) => { await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + return [ "good", "ok", "bad", "notAnalyzed" ].map( ( name ) => ( { + name, + amount: Math.ceil( Math.random() * 10 ), + links: Math.random() > 0.5 ? {} : { view: `edit.php?seo_filter=${ name }` }, + } ) ); + // eslint-disable-next-line no-unreachable try { const response = await fetch( url, options ); if ( ! response.ok ) { From 3c57da307512b19d3eadf9fa52f32cc3575240ee Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:28:39 +0100 Subject: [PATCH 051/132] Line too long --- packages/js/src/dashboard/scores/components/score-list.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index 66ffd12628c..ab4a4d29f36 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -19,7 +19,9 @@ export const ScoreList = ( { scores } ) => ( { SCORE_META[ score.name ].label } { score.amount } - { score.links.view && } + { score.links.view && ( + + ) } ) ) }
    From 0040ea6817bf9771f848cc95140870da613c6a92 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Tue, 19 Nov 2024 14:36:02 +0100 Subject: [PATCH 052/132] Extract upsell to sidebar-layout.js --- .../src/general/components/sidebar-layout.js | 10 +++++++++- packages/js/src/general/initialize.js | 1 - packages/js/src/general/routes/alert-center.js | 18 +----------------- .../components/premium-upsell-list.js | 6 +++--- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/packages/js/src/general/components/sidebar-layout.js b/packages/js/src/general/components/sidebar-layout.js index 854ef4b3cce..d7b612069a8 100644 --- a/packages/js/src/general/components/sidebar-layout.js +++ b/packages/js/src/general/components/sidebar-layout.js @@ -1,7 +1,7 @@ import { useSelect } from "@wordpress/data"; import classNames from "classnames"; import PropTypes from "prop-types"; -import { SidebarRecommendations } from "../../shared-admin/components"; +import { PremiumUpsellList, SidebarRecommendations } from "../../shared-admin/components"; import { STORE_NAME } from "../constants"; import { useSelectGeneralPage } from "../hooks"; @@ -15,12 +15,20 @@ export const SidebarLayout = ( { contentClassName, children } ) => { const premiumLinkSidebar = useSelectGeneralPage( "selectLink", [], "https://yoa.st/jj" ); const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); const academyLink = useSelectGeneralPage( "selectLink", [], "https://yoa.st/3t6" ); + const premiumLinkList = useSelectGeneralPage( "selectLink", [], "https://yoa.st/17h" ); const { isPromotionActive } = useSelect( STORE_NAME ); return (
    { children } + { isPremium ? null : ( + + ) }
    { ! isPremium &&
    diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 49990a4a174..021d99f9987 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -49,7 +49,6 @@ domReady( () => { seoAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.keyphraseAnalysis", false ), readabilityAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.readabilityAnalysis", false ), }; - const router = createHashRouter( createRoutesFromElements( } errorElement={ }> diff --git a/packages/js/src/general/routes/alert-center.js b/packages/js/src/general/routes/alert-center.js index 550397d9e47..dd123016550 100644 --- a/packages/js/src/general/routes/alert-center.js +++ b/packages/js/src/general/routes/alert-center.js @@ -1,20 +1,11 @@ -import { useSelect } from "@wordpress/data"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; -import { PremiumUpsellList } from "../../shared-admin/components"; import { Notifications, Problems } from "../components"; -import { STORE_NAME } from "../constants"; -import { useSelectGeneralPage } from "../hooks"; /** * @returns {JSX.Element} The general page content placeholder. */ export const AlertCenter = () => { - const isPremium = useSelectGeneralPage( "selectPreference", [], "isPremium" ); - const premiumLinkList = useSelectGeneralPage( "selectLink", [], "https://yoa.st/17h" ); - const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); - const { isPromotionActive } = useSelect( STORE_NAME ); - return ( <> @@ -25,17 +16,10 @@ export const AlertCenter = () => {

    -
    +
    - { isPremium ? null : ( - - ) } ); }; diff --git a/packages/js/src/shared-admin/components/premium-upsell-list.js b/packages/js/src/shared-admin/components/premium-upsell-list.js index 42f19a9eaa3..67f8f9625c9 100644 --- a/packages/js/src/shared-admin/components/premium-upsell-list.js +++ b/packages/js/src/shared-admin/components/premium-upsell-list.js @@ -1,8 +1,8 @@ -import { noop } from "lodash"; import { ArrowNarrowRightIcon } from "@heroicons/react/outline"; import { createInterpolateElement } from "@wordpress/element"; import { __, sprintf } from "@wordpress/i18n"; -import { Button, Title, Paper } from "@yoast/ui-library"; +import { Button, Paper, Title } from "@yoast/ui-library"; +import { noop } from "lodash"; import PropTypes from "prop-types"; import { getPremiumBenefits } from "../../helpers/get-premium-benefits"; @@ -16,7 +16,7 @@ export const PremiumUpsellList = ( { premiumLink, premiumUpsellConfig, isPromoti const isBlackFriday = isPromotionActive( "black-friday-2024-promotion" ); return ( - + { isBlackFriday &&
    From 705d71d5652c83a814c91d5f77acc1e86c07e5ad Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 19 Nov 2024 15:52:08 +0200 Subject: [PATCH 053/132] Optimize query for getting SEO scores --- .../seo-scores/seo-scores-repository.php | 2 +- .../seo-scores/seo-scores-collector.php | 100 ++++++++---------- 2 files changed, 43 insertions(+), 59 deletions(-) diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php index 45c69d22ea1..c39c0c6c77e 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -52,7 +52,7 @@ public function __construct( public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $seo_scores = []; - $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $taxonomy, $term_id ); + $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $term_id ); foreach ( $this->seo_scores as $seo_score ) { $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php index 394f5779433..97c36957dd0 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -2,7 +2,6 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong namespace Yoast\WP\SEO\Dashboard\Infrastructure\SEO_Scores; -use wpdb; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface; @@ -13,35 +12,17 @@ */ class SEO_Scores_Collector { - /** - * The WordPress database instance. - * - * @var wpdb - */ - protected $wpdb; - - /** - * Constructs the class. - * - * @param wpdb $wpdb The WordPress database instance. - */ - public function __construct( - wpdb $wpdb - ) { - $this->wpdb = $wpdb; - } - /** * Retrieves the current SEO scores for a content type. * * @param SEO_Scores_Interface[] $seo_scores All SEO scores. * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. * @param int|null $term_id The ID of the term we're filtering for. * * @return array The SEO scores for a content type. */ - public function get_seo_scores( array $seo_scores, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { + public function get_seo_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { + global $wpdb; $select = $this->build_select( $seo_scores ); $replacements = \array_merge( @@ -52,50 +33,53 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? ] ); - if ( $term_id === null || $taxonomy === null ) { - $query = $this->wpdb->prepare( - " - SELECT {$select['fields']} - FROM %i AS I - WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s)", - $replacements + if ( $term_id === null ) { + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s)", + $replacements + ), + \ARRAY_A ); - - $scores = $this->wpdb->get_row( $query, \ARRAY_A ); + //phpcs:enable return $scores; } - $replacements[] = $this->wpdb->term_relationships; - $replacements[] = $this->wpdb->term_taxonomy; + $replacements[] = $wpdb->term_relationships; $replacements[] = $term_id; - $replacements[] = $taxonomy->get_name(); - - $query = $this->wpdb->prepare( - " - SELECT {$select['fields']} - FROM %i AS I - WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s) - AND I.object_id IN ( - SELECT object_id - FROM %i - WHERE term_taxonomy_id IN ( - SELECT term_taxonomy_id - FROM - %i - WHERE - term_id = %d - AND taxonomy = %s - ) - )", - $replacements - ); - $scores = $this->wpdb->get_row( $query, \ARRAY_A ); + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s) + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id = %d + )", + $replacements + ), + \ARRAY_A + ); + //phpcs:enable return $scores; } From c8a0fb53a3c4eaf1675e9c4e01e9e1b82edfd7ca Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:38:36 +0100 Subject: [PATCH 054/132] Reset the selected term on content type change --- packages/js/src/dashboard/scores/seo/seo-scores.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js index fb30520789e..2e107528ebe 100644 --- a/packages/js/src/dashboard/scores/seo/seo-scores.js +++ b/packages/js/src/dashboard/scores/seo/seo-scores.js @@ -1,4 +1,4 @@ -import { useState } from "@wordpress/element"; +import { useEffect, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { Paper, Title } from "@yoast/ui-library"; import { useFetch } from "../../hooks/use-fetch"; @@ -41,6 +41,11 @@ export const SeoScores = ( { contentTypes } ) => { }, } ); + useEffect( () => { + // Reset the selected term when the selected content type changes. + setSelectedTerm( undefined ); // eslint-disable-line no-undefined + }, [ selectedContentType.name ] ); + return ( { __( "SEO scores", "wordpress-seo" ) } From e2bb4ace5cb86b3c0516be97a406a0b19822f679 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 19 Nov 2024 15:39:03 +0100 Subject: [PATCH 055/132] Codescout Decreases the complexity by offloading to Content component --- .../scores/components/term-filter.js | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 2cb2fc0553a..5cffaa8ec60 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -24,6 +24,24 @@ const createQueryUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( */ const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); +/** + * Renders either a list of terms or a message that nothing was found. + * @param {Term[]} terms The terms. + * @returns {JSX.Element} The element. + */ +const Content = ( { terms } ) => terms.length === 0 + ? ( +
    + { __( "Nothing found", "wordpress-seo" ) } +
    + ) + : terms.map( ( { name, label } ) => ( + + { label } + + ) ) +; + /** * @param {string} idSuffix The suffix for the ID. * @param {Taxonomy} taxonomy The taxonomy. @@ -72,16 +90,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => {
    ) } - { ! isPending && terms.length === 0 && ( -
    - { __( "Nothing found", "wordpress-seo" ) } -
    - ) } - { ! isPending && terms.length > 0 && terms.map( ( { name, label } ) => ( - - { label } - - ) ) } + { ! isPending && } ); }; From 6487339989af5ef72ee82e3f4287eea58faa7c42 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 19 Nov 2024 16:48:57 +0200 Subject: [PATCH 056/132] Return SEO scores sorted --- .../seo-scores/seo-scores-repository.php | 26 ++++++---- .../domain/seo-scores/abstract-seo-score.php | 44 +++++++++-------- .../domain/seo-scores/bad-seo-score.php | 9 ++++ .../domain/seo-scores/good-seo-score.php | 9 ++++ .../domain/seo-scores/no-seo-score.php | 9 ++++ .../domain/seo-scores/ok-seo-score.php | 9 ++++ .../seo-scores/seo-scores-interface.php | 28 ++++++++--- .../domain/seo-scores/seo-scores-list.php | 47 +++++++++++++++++++ 8 files changed, 147 insertions(+), 34 deletions(-) create mode 100644 src/dashboard/domain/seo-scores/seo-scores-list.php diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php index c39c0c6c77e..e7028468328 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -4,6 +4,7 @@ use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\SEO_Scores\SEO_Scores_Collector; @@ -13,14 +14,21 @@ class SEO_Scores_Repository { /** - * The SEO Scores collector. + * The SEO cores collector. * * @var SEO_Scores_Collector */ private $seo_scores_collector; /** - * All SEO Scores. + * The SEO scores list. + * + * @var SEO_Scores_List + */ + protected $seo_scores_list; + + /** + * All SEO scores. * * @var SEO_Scores_Interface[] */ @@ -29,14 +37,17 @@ class SEO_Scores_Repository { /** * The constructor. * - * @param SEO_Scores_Collector $seo_scores_collector The SEO Scores collector. - * @param SEO_Scores_Interface ...$seo_scores All SEO Scores. + * @param SEO_Scores_Collector $seo_scores_collector The SEO scores collector. + * @param SEO_Scores_List $seo_scores_list The SEO scores list. + * @param SEO_Scores_Interface ...$seo_scores All SEO scores. */ public function __construct( SEO_Scores_Collector $seo_scores_collector, + SEO_Scores_List $seo_scores_list, SEO_Scores_Interface ...$seo_scores ) { $this->seo_scores_collector = $seo_scores_collector; + $this->seo_scores_list = $seo_scores_list; $this->seo_scores = $seo_scores; } @@ -50,16 +61,15 @@ public function __construct( * @return array>> The SEO scores. */ public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { - $seo_scores = []; - $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $term_id ); + foreach ( $this->seo_scores as $seo_score ) { $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); - $seo_scores[] = $seo_score->to_array(); + $this->seo_scores_list->add( $seo_score ); } - return $seo_scores; + return $this->seo_scores_list->to_array(); } } diff --git a/src/dashboard/domain/seo-scores/abstract-seo-score.php b/src/dashboard/domain/seo-scores/abstract-seo-score.php index ccab858949d..459e177fa77 100644 --- a/src/dashboard/domain/seo-scores/abstract-seo-score.php +++ b/src/dashboard/domain/seo-scores/abstract-seo-score.php @@ -49,6 +49,22 @@ abstract class Abstract_SEO_Score implements SEO_Scores_Interface { */ private $view_link; + /** + * The position of the score. + * + * @var int + */ + private $position; + + /** + * Gets the amount of the SEO score. + * + * @return int The amount of the SEO score. + */ + public function get_amount(): int { + return $this->amount; + } + /** * Sets the amount of the SEO score. * @@ -60,6 +76,15 @@ public function set_amount( int $amount ): void { $this->amount = $amount; } + /** + * Gets the view link of the SEO score. + * + * @return string|null The view link of the SEO score. + */ + public function get_view_link(): ?string { + return $this->view_link; + } + /** * Sets the view link of the SEO score. * @@ -70,23 +95,4 @@ public function set_amount( int $amount ): void { public function set_view_link( ?string $view_link ): void { $this->view_link = $view_link; } - - /** - * Parses the SEO score to the expected key value representation. - * - * @return array> The SEO score presented as the expected key value representation. - */ - public function to_array(): array { - $array = [ - 'name' => $this->get_name(), - 'amount' => $this->amount, - 'links' => [], - ]; - - if ( $this->view_link !== null ) { - $array['links']['view'] = $this->view_link; - } - - return $array; - } } diff --git a/src/dashboard/domain/seo-scores/bad-seo-score.php b/src/dashboard/domain/seo-scores/bad-seo-score.php index d40b41d102d..97c1eb0c920 100644 --- a/src/dashboard/domain/seo-scores/bad-seo-score.php +++ b/src/dashboard/domain/seo-scores/bad-seo-score.php @@ -25,6 +25,15 @@ public function get_filter_name(): string { return 'bad'; } + /** + * Gets the position of the SEO score. + * + * @return int The position of the SEO score. + */ + public function get_position(): int { + return 2; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/good-seo-score.php b/src/dashboard/domain/seo-scores/good-seo-score.php index 3143bc9e162..9e12fba8ed7 100644 --- a/src/dashboard/domain/seo-scores/good-seo-score.php +++ b/src/dashboard/domain/seo-scores/good-seo-score.php @@ -25,6 +25,15 @@ public function get_filter_name(): string { return 'good'; } + /** + * Gets the position of the SEO score. + * + * @return int The position of the SEO score. + */ + public function get_position(): int { + return 0; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/no-seo-score.php b/src/dashboard/domain/seo-scores/no-seo-score.php index af62da975f1..e01e68ea619 100644 --- a/src/dashboard/domain/seo-scores/no-seo-score.php +++ b/src/dashboard/domain/seo-scores/no-seo-score.php @@ -25,6 +25,15 @@ public function get_filter_name(): string { return 'na'; } + /** + * Gets the position of the SEO score. + * + * @return int The position of the SEO score. + */ + public function get_position(): int { + return 3; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/ok-seo-score.php b/src/dashboard/domain/seo-scores/ok-seo-score.php index 4149c32da3e..80abc6ee0b1 100644 --- a/src/dashboard/domain/seo-scores/ok-seo-score.php +++ b/src/dashboard/domain/seo-scores/ok-seo-score.php @@ -25,6 +25,15 @@ public function get_filter_name(): string { return 'ok'; } + /** + * Gets the position of the SEO score. + * + * @return int The position of the SEO score. + */ + public function get_position(): int { + return 1; + } + /** * Gets the minimum score of the SEO score. * diff --git a/src/dashboard/domain/seo-scores/seo-scores-interface.php b/src/dashboard/domain/seo-scores/seo-scores-interface.php index 48cbb7448af..9f4c9c52552 100644 --- a/src/dashboard/domain/seo-scores/seo-scores-interface.php +++ b/src/dashboard/domain/seo-scores/seo-scores-interface.php @@ -35,6 +35,20 @@ public function get_min_score(): ?int; */ public function get_max_score(): ?int; + /** + * Gets the position of the SEO score. + * + * @return int + */ + public function get_position(): int; + + /** + * Gets the amount of the SEO score. + * + * @return int + */ + public function get_amount(): int; + /** * Sets the amount of the SEO score. * @@ -44,6 +58,13 @@ public function get_max_score(): ?int; */ public function set_amount( int $amount ): void; + /** + * Gets the view link of the SEO score. + * + * @return string|null + */ + public function get_view_link(): ?string; + /** * Sets the view link of the SEO score. * @@ -52,11 +73,4 @@ public function set_amount( int $amount ): void; * @return void */ public function set_view_link( ?string $view_link ): void; - - /** - * Parses the SEO score to the expected key value representation. - * - * @return array> - */ - public function to_array(): array; } diff --git a/src/dashboard/domain/seo-scores/seo-scores-list.php b/src/dashboard/domain/seo-scores/seo-scores-list.php new file mode 100644 index 00000000000..7d1a5a7fbf4 --- /dev/null +++ b/src/dashboard/domain/seo-scores/seo-scores-list.php @@ -0,0 +1,47 @@ + + */ + private $seo_scores = []; + + /** + * Adds an SEO score to the list. + * + * @param SEO_Scores_Interface $seo_score The SEO score to add. + * + * @return void + */ + public function add( SEO_Scores_Interface $seo_score ): void { + $this->seo_scores[] = $seo_score; + } + + /** + * Parses the SEO score list to the expected key value representation. + * + * @return array>>>> The SEO score list presented as the expected key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->seo_scores as $seo_score ) { + $array[ $seo_score->get_position() ] = [ + 'name' => $seo_score->get_name(), + 'amount' => $seo_score->get_amount(), + 'links' => ( $seo_score->get_view_link() === null ) ? [] : [ 'view' => $seo_score->get_view_link() ], + ]; + } + + \ksort( $array ); + + return $array; + } +} From f40fa003b7297366343ef174e167558c6133246d Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Tue, 19 Nov 2024 17:20:39 +0200 Subject: [PATCH 057/132] Validate taxonomy parameter also based on whether it's an active filter of the given content type --- .../user-interface/seo-scores-route.php | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/seo-scores-route.php index df04d4741db..7f34fcf76f1 100644 --- a/src/dashboard/user-interface/seo-scores-route.php +++ b/src/dashboard/user-interface/seo-scores-route.php @@ -8,10 +8,10 @@ use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\SEO_Scores\SEO_Scores_Repository; +use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; -use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector; use Yoast\WP\SEO\Main; use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Routes\Route_Interface; @@ -38,11 +38,11 @@ class SEO_Scores_Route implements Route_Interface { private $content_types_collector; /** - * The taxonomies collector. + * The taxonomies repository. * - * @var Taxonomies_Collector + * @var Taxonomies_Repository */ - private $taxonomies_collector; + private $taxonomies_repository; /** * The SEO Scores repository. @@ -69,20 +69,20 @@ class SEO_Scores_Route implements Route_Interface { * Constructs the class. * * @param Content_Types_Collector $content_types_collector The content type collector. - * @param Taxonomies_Collector $taxonomies_collector The taxonomies collector. + * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. * @param Indexable_Repository $indexable_repository The indexable repository. * @param wpdb $wpdb The WordPress database object. */ public function __construct( Content_Types_Collector $content_types_collector, - Taxonomies_Collector $taxonomies_collector, + Taxonomies_Repository $taxonomies_repository, SEO_Scores_Repository $seo_scores_repository, Indexable_Repository $indexable_repository, wpdb $wpdb ) { $this->content_types_collector = $content_types_collector; - $this->taxonomies_collector = $taxonomies_collector; + $this->taxonomies_repository = $taxonomies_repository; $this->seo_scores_repository = $seo_scores_repository; $this->indexable_repository = $indexable_repository; $this->wpdb = $wpdb; @@ -204,7 +204,14 @@ protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): if ( $taxonomy === '' ) { return null; } - return $this->taxonomies_collector->get_taxonomy( $taxonomy, $content_type->get_name() ); + + $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + + if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { + return $valid_taxonomy; + } + + return null; } /** From e8a7da263d17005abd76c11ad20f2f389f43a589 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 11:30:41 +0200 Subject: [PATCH 058/132] Clean up --- .../seo-scores/seo-scores-repository.php | 4 ++-- .../domain/seo-scores/abstract-seo-score.php | 4 ++-- .../domain/seo-scores/seo-scores-list.php | 2 +- .../content-types/content-types-collector.php | 2 +- .../seo-scores/seo-scores-collector.php | 1 - .../user-interface/seo-scores-route.php | 17 ++--------------- 6 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/seo-scores/seo-scores-repository.php index e7028468328..5f15113fbcc 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/seo-scores/seo-scores-repository.php @@ -14,7 +14,7 @@ class SEO_Scores_Repository { /** - * The SEO cores collector. + * The SEO scores collector. * * @var SEO_Scores_Collector */ @@ -58,7 +58,7 @@ public function __construct( * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array>> The SEO scores. + * @return array>> The SEO scores. */ public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $term_id ); diff --git a/src/dashboard/domain/seo-scores/abstract-seo-score.php b/src/dashboard/domain/seo-scores/abstract-seo-score.php index 459e177fa77..f618522e37a 100644 --- a/src/dashboard/domain/seo-scores/abstract-seo-score.php +++ b/src/dashboard/domain/seo-scores/abstract-seo-score.php @@ -22,7 +22,7 @@ abstract class Abstract_SEO_Score implements SEO_Scores_Interface { private $filter_name; /** - * The amount of the SEO score. + * The min score of the SEO score. * * @var int */ @@ -36,7 +36,7 @@ abstract class Abstract_SEO_Score implements SEO_Scores_Interface { private $max_score; /** - * The min score of the SEO score. + * The amount of the SEO score. * * @var int */ diff --git a/src/dashboard/domain/seo-scores/seo-scores-list.php b/src/dashboard/domain/seo-scores/seo-scores-list.php index 7d1a5a7fbf4..98e8a59f0bf 100644 --- a/src/dashboard/domain/seo-scores/seo-scores-list.php +++ b/src/dashboard/domain/seo-scores/seo-scores-list.php @@ -28,7 +28,7 @@ public function add( SEO_Scores_Interface $seo_score ): void { /** * Parses the SEO score list to the expected key value representation. * - * @return array>>>> The SEO score list presented as the expected key value representation. + * @return array>> The SEO score list presented as the expected key value representation. */ public function to_array(): array { $array = []; diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php index 46a5c9bf709..f4927b51bd3 100644 --- a/src/dashboard/infrastructure/content-types/content-types-collector.php +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -31,7 +31,7 @@ public function __construct( /** * Returns the content types array. * - * @return Content_Type[] The content types array. + * @return array The content types array. */ public function get_content_types(): array { $content_types = []; diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php index 97c36957dd0..959d406d767 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -130,7 +130,6 @@ private function build_select( array $seo_scores ): array { * @return string The view link of the SEO score. */ public function get_view_link( SEO_Scores_Interface $seo_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { - // @TODO: Refactor by Single Source of Truthing this with the `WPSEO_Meta_Columns` class. Until then, we build this manually. $posts_page = \admin_url( 'edit.php' ); $args = [ 'post_status' => 'publish', diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/seo-scores-route.php index 7f34fcf76f1..6b22bc2c4db 100644 --- a/src/dashboard/user-interface/seo-scores-route.php +++ b/src/dashboard/user-interface/seo-scores-route.php @@ -4,7 +4,6 @@ use WP_REST_Request; use WP_REST_Response; -use wpdb; use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\SEO_Scores\SEO_Scores_Repository; @@ -58,13 +57,6 @@ class SEO_Scores_Route implements Route_Interface { */ private $indexable_repository; - /** - * The WordPress database instance. - * - * @var wpdb - */ - protected $wpdb; - /** * Constructs the class. * @@ -72,20 +64,17 @@ class SEO_Scores_Route implements Route_Interface { * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. * @param Indexable_Repository $indexable_repository The indexable repository. - * @param wpdb $wpdb The WordPress database object. */ public function __construct( Content_Types_Collector $content_types_collector, Taxonomies_Repository $taxonomies_repository, SEO_Scores_Repository $seo_scores_repository, - Indexable_Repository $indexable_repository, - wpdb $wpdb + Indexable_Repository $indexable_repository ) { $this->content_types_collector = $content_types_collector; $this->taxonomies_repository = $taxonomies_repository; $this->seo_scores_repository = $seo_scores_repository; $this->indexable_repository = $indexable_repository; - $this->wpdb = $wpdb; } /** @@ -165,11 +154,9 @@ public function get_seo_scores( WP_REST_Request $request ) { ); } - $result = $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $request['term'] ); - return new WP_REST_Response( [ - 'scores' => $result, + 'scores' => $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $request['term'] ), ], 200 ); From 1535c214f118c9e6801d6587bcfa6c3b8b2aa237 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Wed, 20 Nov 2024 11:28:31 +0100 Subject: [PATCH 059/132] Feedback Igor. --- .../js/src/dashboard/components/dashboard.js | 2 +- .../connected-premium-upsell-list.js | 24 +++++++++++++++++++ .../src/general/components/sidebar-layout.js | 10 +------- packages/js/src/general/initialize.js | 4 +++- .../js/src/general/routes/alert-center.js | 2 +- .../components/premium-upsell-list.js | 2 +- 6 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 packages/js/src/general/components/connected-premium-upsell-list.js diff --git a/packages/js/src/dashboard/components/dashboard.js b/packages/js/src/dashboard/components/dashboard.js index 193ef04d044..d0c4cfbf61c 100644 --- a/packages/js/src/dashboard/components/dashboard.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -17,7 +17,7 @@ export const Dashboard = ( { contentTypes, userName, features } ) => { return ( <> -
    +
    { features.indexables && features.seoAnalysis && } { features.indexables && features.readabilityAnalysis && }
    diff --git a/packages/js/src/general/components/connected-premium-upsell-list.js b/packages/js/src/general/components/connected-premium-upsell-list.js new file mode 100644 index 00000000000..cce55eb5bd4 --- /dev/null +++ b/packages/js/src/general/components/connected-premium-upsell-list.js @@ -0,0 +1,24 @@ +import { useSelect } from "@wordpress/data"; +import { PremiumUpsellList } from "../../shared-admin/components"; +import { STORE_NAME } from "../constants"; +import { useSelectGeneralPage } from "../hooks"; + +/** + * @returns {JSX.Element|null} The premium upsell list or null if not applicable. + */ +export const ConnectedPremiumUpsellList = () => { + const isPremium = useSelectGeneralPage( "selectPreference", [], "isPremium" ); + const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); + const { isPromotionActive } = useSelect( STORE_NAME ); + const premiumLinkList = useSelectGeneralPage( "selectLink", [], "https://yoa.st/17h" ); + + if ( isPremium ) { + return null; + } + return ; +}; + diff --git a/packages/js/src/general/components/sidebar-layout.js b/packages/js/src/general/components/sidebar-layout.js index d7b612069a8..854ef4b3cce 100644 --- a/packages/js/src/general/components/sidebar-layout.js +++ b/packages/js/src/general/components/sidebar-layout.js @@ -1,7 +1,7 @@ import { useSelect } from "@wordpress/data"; import classNames from "classnames"; import PropTypes from "prop-types"; -import { PremiumUpsellList, SidebarRecommendations } from "../../shared-admin/components"; +import { SidebarRecommendations } from "../../shared-admin/components"; import { STORE_NAME } from "../constants"; import { useSelectGeneralPage } from "../hooks"; @@ -15,20 +15,12 @@ export const SidebarLayout = ( { contentClassName, children } ) => { const premiumLinkSidebar = useSelectGeneralPage( "selectLink", [], "https://yoa.st/jj" ); const premiumUpsellConfig = useSelectGeneralPage( "selectUpsellSettingsAsProps" ); const academyLink = useSelectGeneralPage( "selectLink", [], "https://yoa.st/3t6" ); - const premiumLinkList = useSelectGeneralPage( "selectLink", [], "https://yoa.st/17h" ); const { isPromotionActive } = useSelect( STORE_NAME ); return (
    { children } - { isPremium ? null : ( - - ) }
    { ! isPremium &&
    diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 021d99f9987..9bba09cded4 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -9,6 +9,7 @@ import { Dashboard } from "../dashboard"; import { LINK_PARAMS_NAME } from "../shared-admin/store"; import App from "./app"; import { RouteErrorFallback } from "./components"; +import { ConnectedPremiumUpsellList } from "./components/connected-premium-upsell-list"; import { SidebarLayout } from "./components/sidebar-layout"; import { STORE_NAME } from "./constants"; import { AlertCenter, FirstTimeConfiguration, ROUTES } from "./routes"; @@ -57,13 +58,14 @@ domReady( () => { element={ + } errorElement={ } /> } + element={ } errorElement={ } /> } errorElement={ } /> diff --git a/packages/js/src/general/routes/alert-center.js b/packages/js/src/general/routes/alert-center.js index dd123016550..f1631af5a55 100644 --- a/packages/js/src/general/routes/alert-center.js +++ b/packages/js/src/general/routes/alert-center.js @@ -16,7 +16,7 @@ export const AlertCenter = () => {

    -
    +
    diff --git a/packages/js/src/shared-admin/components/premium-upsell-list.js b/packages/js/src/shared-admin/components/premium-upsell-list.js index 67f8f9625c9..ecc231bfa3e 100644 --- a/packages/js/src/shared-admin/components/premium-upsell-list.js +++ b/packages/js/src/shared-admin/components/premium-upsell-list.js @@ -16,7 +16,7 @@ export const PremiumUpsellList = ( { premiumLink, premiumUpsellConfig, isPromoti const isBlackFriday = isPromotionActive( "black-friday-2024-promotion" ); return ( - + { isBlackFriday &&
    From e0744ea9de447d7e0ad4c7453f86e760221635a6 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 13:32:48 +0200 Subject: [PATCH 060/132] Don't include explicitly noindexed posts in the SEO scores --- .../infrastructure/seo-scores/seo-scores-collector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php index 959d406d767..8834ca6bac4 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php @@ -45,7 +45,8 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type IN ('post') - AND I.object_sub_type IN (%s)", + AND I.object_sub_type IN (%s) + AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", $replacements ), \ARRAY_A @@ -70,6 +71,7 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type IN ('post') AND I.object_sub_type IN (%s) + AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 ) AND I.object_id IN ( SELECT object_id FROM %i From 851649c201a1385e49761cda1410956d642dccb7 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 15:27:23 +0200 Subject: [PATCH 061/132] Add API endpoint for readability scores --- .../readability-scores-repository.php | 76 ++++++ .../seo-scores/seo-scores-repository.php | 25 +- .../abstract-score.php} | 34 +-- .../abstract-readability-score.php | 11 + .../bad-readability-score.php | 55 ++++ .../good-readability-score.php | 55 ++++ .../no-readability-score.php | 55 ++++ .../ok-readability-score.php | 55 ++++ .../readability-scores-interface.php | 10 + .../scores-interface.php} | 28 +-- src/dashboard/domain/scores/scores-list.php | 47 ++++ .../scores/seo-scores/abstract-seo-score.php | 11 + .../{ => scores}/seo-scores/bad-seo-score.php | 5 +- .../seo-scores/good-seo-score.php | 3 +- .../{ => scores}/seo-scores/no-seo-score.php | 3 +- .../{ => scores}/seo-scores/ok-seo-score.php | 3 +- .../seo-scores/seo-scores-interface.php | 10 + .../domain/seo-scores/seo-scores-list.php | 47 ---- .../readability-scores-collector.php | 157 ++++++++++++ .../seo-scores/seo-scores-collector.php | 15 +- .../scores/readability-scores-route.php | 234 ++++++++++++++++++ .../{ => scores}/seo-scores-route.php | 6 +- 22 files changed, 840 insertions(+), 105 deletions(-) create mode 100644 src/dashboard/application/scores/readability-scores/readability-scores-repository.php rename src/dashboard/application/{ => scores}/seo-scores/seo-scores-repository.php (73%) rename src/dashboard/domain/{seo-scores/abstract-seo-score.php => scores/abstract-score.php} (53%) create mode 100644 src/dashboard/domain/scores/readability-scores/abstract-readability-score.php create mode 100644 src/dashboard/domain/scores/readability-scores/bad-readability-score.php create mode 100644 src/dashboard/domain/scores/readability-scores/good-readability-score.php create mode 100644 src/dashboard/domain/scores/readability-scores/no-readability-score.php create mode 100644 src/dashboard/domain/scores/readability-scores/ok-readability-score.php create mode 100644 src/dashboard/domain/scores/readability-scores/readability-scores-interface.php rename src/dashboard/domain/{seo-scores/seo-scores-interface.php => scores/scores-interface.php} (54%) create mode 100644 src/dashboard/domain/scores/scores-list.php create mode 100644 src/dashboard/domain/scores/seo-scores/abstract-seo-score.php rename src/dashboard/domain/{ => scores}/seo-scores/bad-seo-score.php (88%) rename src/dashboard/domain/{ => scores}/seo-scores/good-seo-score.php (89%) rename src/dashboard/domain/{ => scores}/seo-scores/no-seo-score.php (89%) rename src/dashboard/domain/{ => scores}/seo-scores/ok-seo-score.php (89%) create mode 100644 src/dashboard/domain/scores/seo-scores/seo-scores-interface.php delete mode 100644 src/dashboard/domain/seo-scores/seo-scores-list.php create mode 100644 src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php rename src/dashboard/infrastructure/{ => scores}/seo-scores/seo-scores-collector.php (91%) create mode 100644 src/dashboard/user-interface/scores/readability-scores-route.php rename src/dashboard/user-interface/{ => scores}/seo-scores-route.php (96%) diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php new file mode 100644 index 00000000000..eedb30ac93c --- /dev/null +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -0,0 +1,76 @@ +readability_scores_collector = $readability_scores_collector; + $this->scores_list = $scores_list; + $this->readability_scores = $readability_scores; + } + + /** + * Returns the readability Scores of a content type. + * + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. + * + * @return array>> The readability scores. + */ + public function get_readability_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + $current_scores = $this->readability_scores_collector->get_readability_scores( $this->readability_scores, $content_type, $term_id ); + + foreach ( $this->readability_scores as $readability_score ) { + $readability_score->set_amount( (int) $current_scores[ $readability_score->get_name() ] ); + $readability_score->set_view_link( $this->readability_scores_collector->get_view_link( $readability_score, $content_type, $taxonomy, $term_id ) ); + + $this->scores_list->add( $readability_score ); + } + + return $this->scores_list->to_array(); + } +} diff --git a/src/dashboard/application/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php similarity index 73% rename from src/dashboard/application/seo-scores/seo-scores-repository.php rename to src/dashboard/application/scores/seo-scores/seo-scores-repository.php index 5f15113fbcc..c4d727d74fe 100644 --- a/src/dashboard/application/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php @@ -1,12 +1,13 @@ seo_scores_collector = $seo_scores_collector; - $this->seo_scores_list = $seo_scores_list; + $this->scores_list = $scores_list; $this->seo_scores = $seo_scores; } @@ -67,9 +68,9 @@ public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); - $this->seo_scores_list->add( $seo_score ); + $this->scores_list->add( $seo_score ); } - return $this->seo_scores_list->to_array(); + return $this->scores_list->to_array(); } } diff --git a/src/dashboard/domain/seo-scores/abstract-seo-score.php b/src/dashboard/domain/scores/abstract-score.php similarity index 53% rename from src/dashboard/domain/seo-scores/abstract-seo-score.php rename to src/dashboard/domain/scores/abstract-score.php index f618522e37a..e37a7a4fac1 100644 --- a/src/dashboard/domain/seo-scores/abstract-seo-score.php +++ b/src/dashboard/domain/scores/abstract-score.php @@ -1,49 +1,49 @@ amount; } /** - * Sets the amount of the SEO score. + * Sets the amount of the score. * - * @param int $amount The amount of the SEO score. + * @param int $amount The amount of the score. * * @return void */ @@ -77,18 +77,18 @@ public function set_amount( int $amount ): void { } /** - * Gets the view link of the SEO score. + * Gets the view link of the score. * - * @return string|null The view link of the SEO score. + * @return string|null The view link of the score. */ public function get_view_link(): ?string { return $this->view_link; } /** - * Sets the view link of the SEO score. + * Sets the view link of the score. * - * @param string $view_link The view link of the SEO score. + * @param string $view_link The view link of the score. * * @return void */ diff --git a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php new file mode 100644 index 00000000000..2c7685aebf4 --- /dev/null +++ b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php @@ -0,0 +1,11 @@ + + */ + private $scores = []; + + /** + * Adds a score to the list. + * + * @param Scores_Interface $score The score to add. + * + * @return void + */ + public function add( Scores_Interface $score ): void { + $this->scores[] = $score; + } + + /** + * Parses the score list to the expected key value representation. + * + * @return array>> The score list presented as the expected key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->scores as $score ) { + $array[ $score->get_position() ] = [ + 'name' => $score->get_name(), + 'amount' => $score->get_amount(), + 'links' => ( $score->get_view_link() === null ) ? [] : [ 'view' => $score->get_view_link() ], + ]; + } + + \ksort( $array ); + + return $array; + } +} diff --git a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php new file mode 100644 index 00000000000..acf40cd4813 --- /dev/null +++ b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php @@ -0,0 +1,11 @@ + - */ - private $seo_scores = []; - - /** - * Adds an SEO score to the list. - * - * @param SEO_Scores_Interface $seo_score The SEO score to add. - * - * @return void - */ - public function add( SEO_Scores_Interface $seo_score ): void { - $this->seo_scores[] = $seo_score; - } - - /** - * Parses the SEO score list to the expected key value representation. - * - * @return array>> The SEO score list presented as the expected key value representation. - */ - public function to_array(): array { - $array = []; - foreach ( $this->seo_scores as $seo_score ) { - $array[ $seo_score->get_position() ] = [ - 'name' => $seo_score->get_name(), - 'amount' => $seo_score->get_amount(), - 'links' => ( $seo_score->get_view_link() === null ) ? [] : [ 'view' => $seo_score->get_view_link() ], - ]; - } - - \ksort( $array ); - - return $array; - } -} diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php new file mode 100644 index 00000000000..874cf3aaf86 --- /dev/null +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -0,0 +1,157 @@ + The readability scores for a content type. + */ + public function get_readability_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { + global $wpdb; + $select = $this->build_select( $readability_scores ); + + $replacements = \array_merge( + \array_values( $select['replacements'] ), + [ + Model::get_table_name( 'Indexable' ), + $content_type->get_name(), + ] + ); + + if ( $term_id === null ) { + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s)", + $replacements + ), + \ARRAY_A + ); + //phpcs:enable + return $current_scores; + + } + + $replacements[] = $wpdb->term_relationships; + $replacements[] = $term_id; + + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type IN (%s) + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id = %d + )", + $replacements + ), + \ARRAY_A + ); + //phpcs:enable + return $current_scores; + } + + /** + * Builds the select statement for the readability scores query. + * + * @param Readability_Scores_Interface[] $readability_scores All readability scores. + * + * @return array The select statement for the readability scores query. + */ + private function build_select( array $readability_scores ): array { + $select_fields = []; + $select_replacements = []; + + foreach ( $readability_scores as $readability_score ) { + $min = $readability_score->get_min_score(); + $max = $readability_score->get_max_score(); + $name = $readability_score->get_name(); + + if ( $min === null || $max === null ) { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; + $select_replacements[] = $name; + } + else { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score >= %d AND I.readability_score <= %d AND I.estimated_reading_time_minutes IS NOT NULL THEN 1 END) AS %i'; + $select_replacements[] = $min; + $select_replacements[] = $max; + $select_replacements[] = $name; + } + } + + $select_fields = \implode( ', ', $select_fields ); + + return [ + 'fields' => $select_fields, + 'replacements' => $select_replacements, + ]; + } + + /** + * Builds the view link of the readability_scores score. + * + * @param Readability_Scores_Interface $readability_score_name The name of the readability_scores score. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. + * @param int|null $term_id The ID of the term we might be filtering. + * + * @return string The view link of the readability_scores score. + */ + public function get_view_link( Readability_Scores_Interface $readability_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { + $posts_page = \admin_url( 'edit.php' ); + $args = [ + 'post_status' => 'publish', + 'post_type' => $content_type->get_name(), + 'readability_filter' => $readability_score_name->get_filter_name(), + ]; + + if ( $taxonomy === null || $term_id === null ) { + return \add_query_arg( $args, $posts_page ); + } + + $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); + $query_var = $taxonomy_object->query_var; + + if ( $query_var === false ) { + return null; + } + + $term = \get_term( $term_id ); + $args[ $query_var ] = $term->slug; + + return \add_query_arg( $args, $posts_page ); + } +} diff --git a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php similarity index 91% rename from src/dashboard/infrastructure/seo-scores/seo-scores-collector.php rename to src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index 8834ca6bac4..ba583fd14bd 100644 --- a/src/dashboard/infrastructure/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -1,10 +1,11 @@ get_row( + $current_scores = $wpdb->get_row( $wpdb->prepare( " SELECT {$select['fields']} @@ -52,7 +53,7 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? \ARRAY_A ); //phpcs:enable - return $scores; + return $current_scores; } @@ -63,7 +64,7 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $scores = $wpdb->get_row( + $current_scores = $wpdb->get_row( $wpdb->prepare( " SELECT {$select['fields']} @@ -82,7 +83,7 @@ public function get_seo_scores( array $seo_scores, Content_Type $content_type, ? \ARRAY_A ); //phpcs:enable - return $scores; + return $current_scores; } /** @@ -102,7 +103,7 @@ private function build_select( array $seo_scores ): array { $name = $seo_score->get_name(); if ( $min === null || $max === null ) { - $select_fields[] = 'COUNT(CASE WHEN primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; + $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; $select_replacements[] = $name; } else { diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php new file mode 100644 index 00000000000..3e759b14b23 --- /dev/null +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -0,0 +1,234 @@ +content_types_collector = $content_types_collector; + $this->taxonomies_repository = $taxonomies_repository; + $this->readability_scores_repository = $readability_scores_repository; + $this->indexable_repository = $indexable_repository; + } + + /** + * Registers routes with WordPress. + * + * @return void + */ + public function register_routes() { + \register_rest_route( + Main::API_V1_NAMESPACE, + self::ROUTE_PREFIX, + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_readability_scores' ], + 'permission_callback' => [ $this, 'permission_manage_options' ], + 'args' => [ + 'contentType' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'term' => [ + 'required' => false, + 'type' => 'integer', + 'default' => null, + 'sanitize_callback' => static function ( $param ) { + return \intval( $param ); + }, + ], + 'taxonomy' => [ + 'required' => false, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ], + ] + ); + } + + /** + * Gets the readability scores of a specific content type. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response|WP_Error The success or failure response. + */ + public function get_readability_scores( WP_REST_Request $request ) { + $content_type = $this->get_content_type( $request['contentType'] ); + if ( $content_type === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid content type.', + ], + 400 + ); + } + + $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); + if ( $request['taxonomy'] !== '' && $taxonomy === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid taxonomy.', + ], + 400 + ); + } + + if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid term.', + ], + 400 + ); + } + + return new WP_REST_Response( + [ + 'scores' => $this->readability_scores_repository->get_readability_scores( $content_type, $taxonomy, $request['term'] ), + ], + 200 + ); + } + + /** + * Gets the content type object. + * + * @param string $content_type The content type. + * + * @return Content_Type|null The content type object. + */ + protected function get_content_type( string $content_type ): ?Content_Type { + $content_types = $this->content_types_collector->get_content_types(); + + if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { + return $content_types[ $content_type ]; + } + + return null; + } + + /** + * Gets the taxonomy object. + * + * @param string $taxonomy The taxonomy. + * @param Content_Type $content_type The content type that the taxonomy is filtering. + * + * @return Taxonomy|null The taxonomy object. + */ + protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { + if ( $taxonomy === '' ) { + return null; + } + + $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + + if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { + return $valid_taxonomy; + } + + return null; + } + + /** + * Validates the term against the given taxonomy. + * + * @param int $term_id The ID of the term. + * @param Taxonomy|null $taxonomy The taxonomy. + * + * @return bool Whether the term passed validation. + */ + protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { + if ( $term_id === null ) { + return ( $taxonomy === null ); + } + + $term = \get_term( $term_id ); + if ( ! $term || \is_wp_error( $term ) ) { + return false; + } + + $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); + return $term->taxonomy === $taxonomy_name; + } + + /** + * Permission callback. + * + * @return bool True when user has the 'wpseo_manage_options' capability. + */ + public function permission_manage_options() { + return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + } +} diff --git a/src/dashboard/user-interface/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php similarity index 96% rename from src/dashboard/user-interface/seo-scores-route.php rename to src/dashboard/user-interface/scores/seo-scores-route.php index 6b22bc2c4db..7f252e5049b 100644 --- a/src/dashboard/user-interface/seo-scores-route.php +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -1,12 +1,12 @@ Date: Wed, 20 Nov 2024 15:57:04 +0200 Subject: [PATCH 062/132] Abstract getting the view link of a score into a unified collector --- .../readability-scores-repository.php | 13 ++++- .../seo-scores/seo-scores-repository.php | 13 ++++- .../domain/scores/abstract-score.php | 11 ++++- .../abstract-readability-score.php | 12 ++++- .../bad-readability-score.php | 4 +- .../good-readability-score.php | 4 +- .../no-readability-score.php | 4 +- .../ok-readability-score.php | 4 +- .../domain/scores/scores-interface.php | 11 ++++- .../scores/seo-scores/abstract-seo-score.php | 12 ++++- .../scores/seo-scores/bad-seo-score.php | 4 +- .../scores/seo-scores/good-seo-score.php | 4 +- .../domain/scores/seo-scores/no-seo-score.php | 4 +- .../domain/scores/seo-scores/ok-seo-score.php | 4 +- .../readability-scores-collector.php | 36 -------------- .../scores/score-link-collector.php | 49 +++++++++++++++++++ .../seo-scores/seo-scores-collector.php | 36 -------------- 17 files changed, 129 insertions(+), 96 deletions(-) create mode 100644 src/dashboard/infrastructure/scores/score-link-collector.php diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php index eedb30ac93c..08f1d2cece8 100644 --- a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -8,6 +8,7 @@ use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Readability_Scores\Readability_Scores_Collector; +use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; /** * The repository to get readability Scores. @@ -21,6 +22,13 @@ class Readability_Scores_Repository { */ private $readability_scores_collector; + /** + * The score link collector. + * + * @var Score_Link_Collector + */ + private $score_link_collector; + /** * The scores list. * @@ -39,15 +47,18 @@ class Readability_Scores_Repository { * The constructor. * * @param Readability_Scores_Collector $readability_scores_collector The readability scores collector. + * @param Score_Link_Collector $score_link_collector The score link collector. * @param Scores_List $scores_list The scores list. * @param Readability_Scores_Interface ...$readability_scores All readability scores. */ public function __construct( Readability_Scores_Collector $readability_scores_collector, + Score_Link_Collector $score_link_collector, Scores_List $scores_list, Readability_Scores_Interface ...$readability_scores ) { $this->readability_scores_collector = $readability_scores_collector; + $this->score_link_collector = $score_link_collector; $this->scores_list = $scores_list; $this->readability_scores = $readability_scores; } @@ -66,7 +77,7 @@ public function get_readability_scores( Content_Type $content_type, ?Taxonomy $t foreach ( $this->readability_scores as $readability_score ) { $readability_score->set_amount( (int) $current_scores[ $readability_score->get_name() ] ); - $readability_score->set_view_link( $this->readability_scores_collector->get_view_link( $readability_score, $content_type, $taxonomy, $term_id ) ); + $readability_score->set_view_link( $this->score_link_collector->get_view_link( $readability_score, $content_type, $taxonomy, $term_id ) ); $this->scores_list->add( $readability_score ); } diff --git a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php index c4d727d74fe..d79b24f129d 100644 --- a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php @@ -7,6 +7,7 @@ use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; +use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\SEO_Scores\SEO_Scores_Collector; /** @@ -21,6 +22,13 @@ class SEO_Scores_Repository { */ private $seo_scores_collector; + /** + * The score link collector. + * + * @var Score_Link_Collector + */ + private $score_link_collector; + /** * The scores list. * @@ -39,15 +47,18 @@ class SEO_Scores_Repository { * The constructor. * * @param SEO_Scores_Collector $seo_scores_collector The SEO scores collector. + * @param Score_Link_Collector $score_link_collector The score link collector. * @param Scores_List $scores_list The scores list. * @param SEO_Scores_Interface ...$seo_scores All SEO scores. */ public function __construct( SEO_Scores_Collector $seo_scores_collector, + Score_Link_Collector $score_link_collector, Scores_List $scores_list, SEO_Scores_Interface ...$seo_scores ) { $this->seo_scores_collector = $seo_scores_collector; + $this->score_link_collector = $score_link_collector; $this->scores_list = $scores_list; $this->seo_scores = $seo_scores; } @@ -66,7 +77,7 @@ public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, foreach ( $this->seo_scores as $seo_score ) { $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); - $seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); + $seo_score->set_view_link( $this->score_link_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); $this->scores_list->add( $seo_score ); } diff --git a/src/dashboard/domain/scores/abstract-score.php b/src/dashboard/domain/scores/abstract-score.php index e37a7a4fac1..f30f13ac885 100644 --- a/src/dashboard/domain/scores/abstract-score.php +++ b/src/dashboard/domain/scores/abstract-score.php @@ -15,11 +15,18 @@ abstract class Abstract_Score implements Scores_Interface { private $name; /** - * The name of the score that is used when filtering on the posts page. + * The key of the score that is used when filtering on the posts page. * * @var string */ - private $filter_name; + private $filter_key; + + /** + * The value of the score that is used when filtering on the posts page. + * + * @var string + */ + private $filter_value; /** * The min score of the score. diff --git a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php index 2c7685aebf4..133623f9173 100644 --- a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php +++ b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php @@ -8,4 +8,14 @@ /** * Abstract class for a readability score. */ -abstract class Abstract_Readability_Score extends Abstract_Score implements Readability_Scores_Interface {} +abstract class Abstract_Readability_Score extends Abstract_Score implements Readability_Scores_Interface { + + /** + * Gets the key of the readability score that is used when filtering on the posts page. + * + * @return string The name of the readability score that is used when filtering on the posts page. + */ + public function get_filter_key(): string { + return 'readability_filter'; + } +} diff --git a/src/dashboard/domain/scores/readability-scores/bad-readability-score.php b/src/dashboard/domain/scores/readability-scores/bad-readability-score.php index 3a2b4cdcb2e..8c46f512061 100644 --- a/src/dashboard/domain/scores/readability-scores/bad-readability-score.php +++ b/src/dashboard/domain/scores/readability-scores/bad-readability-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the readability score that is used when filtering on the posts page. + * Gets the value of the readability score that is used when filtering on the posts page. * * @return string The name of the readability score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'bad'; } diff --git a/src/dashboard/domain/scores/readability-scores/good-readability-score.php b/src/dashboard/domain/scores/readability-scores/good-readability-score.php index 34663b69084..fdad0d13712 100644 --- a/src/dashboard/domain/scores/readability-scores/good-readability-score.php +++ b/src/dashboard/domain/scores/readability-scores/good-readability-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the readability score that is used when filtering on the posts page. + * Gets the value of the readability score that is used when filtering on the posts page. * * @return string The name of the readability score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'good'; } diff --git a/src/dashboard/domain/scores/readability-scores/no-readability-score.php b/src/dashboard/domain/scores/readability-scores/no-readability-score.php index 2110dcdba51..a8139fb21f3 100644 --- a/src/dashboard/domain/scores/readability-scores/no-readability-score.php +++ b/src/dashboard/domain/scores/readability-scores/no-readability-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the readability score that is used when filtering on the posts page. + * Gets the value of the readability score that is used when filtering on the posts page. * * @return string The name of the readability score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'na'; } diff --git a/src/dashboard/domain/scores/readability-scores/ok-readability-score.php b/src/dashboard/domain/scores/readability-scores/ok-readability-score.php index 2577df86938..45bb8e45126 100644 --- a/src/dashboard/domain/scores/readability-scores/ok-readability-score.php +++ b/src/dashboard/domain/scores/readability-scores/ok-readability-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the readability score that is used when filtering on the posts page. + * Gets the value of the readability score that is used when filtering on the posts page. * * @return string The name of the readability score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'ok'; } diff --git a/src/dashboard/domain/scores/scores-interface.php b/src/dashboard/domain/scores/scores-interface.php index 029000c5e93..a228b6595a9 100644 --- a/src/dashboard/domain/scores/scores-interface.php +++ b/src/dashboard/domain/scores/scores-interface.php @@ -15,11 +15,18 @@ interface Scores_Interface { public function get_name(): string; /** - * Gets the name of the score that is used when filtering on the posts page. + * Gets the key of the score that is used when filtering on the posts page. * * @return string */ - public function get_filter_name(): string; + public function get_filter_key(): string; + + /** + * Gets the value of the score that is used when filtering on the posts page. + * + * @return string + */ + public function get_filter_value(): string; /** * Gets the minimum score of the score. diff --git a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php index acf40cd4813..52f5b3f0d0f 100644 --- a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php +++ b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php @@ -8,4 +8,14 @@ /** * Abstract class for an SEO score. */ -abstract class Abstract_SEO_Score extends Abstract_Score implements SEO_Scores_Interface {} +abstract class Abstract_SEO_Score extends Abstract_Score implements SEO_Scores_Interface { + + /** + * Gets the key of the SEO score that is used when filtering on the posts page. + * + * @return string The name of the SEO score that is used when filtering on the posts page. + */ + public function get_filter_key(): string { + return 'seo_filter'; + } +} diff --git a/src/dashboard/domain/scores/seo-scores/bad-seo-score.php b/src/dashboard/domain/scores/seo-scores/bad-seo-score.php index 8aa45c74349..9ffbb67fe9a 100644 --- a/src/dashboard/domain/scores/seo-scores/bad-seo-score.php +++ b/src/dashboard/domain/scores/seo-scores/bad-seo-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the SEO score that is used when filtering on the posts page. + * Gets the value of the SEO score that is used when filtering on the posts page. * * @return string The name of the SEO score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'bad'; } diff --git a/src/dashboard/domain/scores/seo-scores/good-seo-score.php b/src/dashboard/domain/scores/seo-scores/good-seo-score.php index 087b72e79d1..2cddb4c2a2d 100644 --- a/src/dashboard/domain/scores/seo-scores/good-seo-score.php +++ b/src/dashboard/domain/scores/seo-scores/good-seo-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the SEO score that is used when filtering on the posts page. + * Gets the value of the SEO score that is used when filtering on the posts page. * * @return string The name of the SEO score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'good'; } diff --git a/src/dashboard/domain/scores/seo-scores/no-seo-score.php b/src/dashboard/domain/scores/seo-scores/no-seo-score.php index 681fc97e394..11d7b99bb13 100644 --- a/src/dashboard/domain/scores/seo-scores/no-seo-score.php +++ b/src/dashboard/domain/scores/seo-scores/no-seo-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the SEO score that is used when filtering on the posts page. + * Gets the value of the SEO score that is used when filtering on the posts page. * * @return string The name of the SEO score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'na'; } diff --git a/src/dashboard/domain/scores/seo-scores/ok-seo-score.php b/src/dashboard/domain/scores/seo-scores/ok-seo-score.php index c044c2a722d..692a40d696b 100644 --- a/src/dashboard/domain/scores/seo-scores/ok-seo-score.php +++ b/src/dashboard/domain/scores/seo-scores/ok-seo-score.php @@ -18,11 +18,11 @@ public function get_name(): string { } /** - * Gets the name of the SEO score that is used when filtering on the posts page. + * Gets the value of the SEO score that is used when filtering on the posts page. * * @return string The name of the SEO score that is used when filtering on the posts page. */ - public function get_filter_name(): string { + public function get_filter_value(): string { return 'ok'; } diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 874cf3aaf86..4b59ac85800 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -6,7 +6,6 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; /** * Getting readability scores from the indexable database table. @@ -119,39 +118,4 @@ private function build_select( array $readability_scores ): array { 'replacements' => $select_replacements, ]; } - - /** - * Builds the view link of the readability_scores score. - * - * @param Readability_Scores_Interface $readability_score_name The name of the readability_scores score. - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. - * @param int|null $term_id The ID of the term we might be filtering. - * - * @return string The view link of the readability_scores score. - */ - public function get_view_link( Readability_Scores_Interface $readability_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { - $posts_page = \admin_url( 'edit.php' ); - $args = [ - 'post_status' => 'publish', - 'post_type' => $content_type->get_name(), - 'readability_filter' => $readability_score_name->get_filter_name(), - ]; - - if ( $taxonomy === null || $term_id === null ) { - return \add_query_arg( $args, $posts_page ); - } - - $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); - $query_var = $taxonomy_object->query_var; - - if ( $query_var === false ) { - return null; - } - - $term = \get_term( $term_id ); - $args[ $query_var ] = $term->slug; - - return \add_query_arg( $args, $posts_page ); - } } diff --git a/src/dashboard/infrastructure/scores/score-link-collector.php b/src/dashboard/infrastructure/scores/score-link-collector.php new file mode 100644 index 00000000000..0576811346e --- /dev/null +++ b/src/dashboard/infrastructure/scores/score-link-collector.php @@ -0,0 +1,49 @@ + 'publish', + 'post_type' => $content_type->get_name(), + $score_name->get_filter_key() => $score_name->get_filter_value(), + ]; + + if ( $taxonomy === null || $term_id === null ) { + return \add_query_arg( $args, $posts_page ); + } + + $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); + $query_var = $taxonomy_object->query_var; + + if ( $query_var === false ) { + return null; + } + + $term = \get_term( $term_id ); + $args[ $query_var ] = $term->slug; + + return \add_query_arg( $args, $posts_page ); + } +} diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index ba583fd14bd..c924ea061af 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -6,7 +6,6 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; /** * Getting SEO scores from the indexable database table. @@ -121,39 +120,4 @@ private function build_select( array $seo_scores ): array { 'replacements' => $select_replacements, ]; } - - /** - * Builds the view link of the SEO score. - * - * @param SEO_Scores_Interface $seo_score_name The name of the SEO score. - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. - * @param int|null $term_id The ID of the term we might be filtering. - * - * @return string The view link of the SEO score. - */ - public function get_view_link( SEO_Scores_Interface $seo_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { - $posts_page = \admin_url( 'edit.php' ); - $args = [ - 'post_status' => 'publish', - 'post_type' => $content_type->get_name(), - 'seo_filter' => $seo_score_name->get_filter_name(), - ]; - - if ( $taxonomy === null || $term_id === null ) { - return \add_query_arg( $args, $posts_page ); - } - - $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); - $query_var = $taxonomy_object->query_var; - - if ( $query_var === false ) { - return null; - } - - $term = \get_term( $term_id ); - $args[ $query_var ] = $term->slug; - - return \add_query_arg( $args, $posts_page ); - } } From 7b85e584b817278dfdcc7f095c84630c481485e1 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 16:42:41 +0200 Subject: [PATCH 063/132] Use trait for score routes --- .../scores/readability-scores-route.php | 196 +---------------- .../scores/scores-route-trait.php | 207 ++++++++++++++++++ .../scores/seo-scores-route.php | 200 +---------------- 3 files changed, 227 insertions(+), 376 deletions(-) create mode 100644 src/dashboard/user-interface/scores/scores-route-trait.php diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php index 3e759b14b23..a1d6b0760a3 100644 --- a/src/dashboard/user-interface/scores/readability-scores-route.php +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -2,17 +2,10 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use WP_REST_Request; -use WP_REST_Response; -use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores\Readability_Scores_Repository; -use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; -use Yoast\WP\SEO\Main; -use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Routes\Route_Interface; /** @@ -20,6 +13,7 @@ */ class Readability_Scores_Route implements Route_Interface { + use Scores_Route_Trait; use No_Conditionals; /** @@ -29,20 +23,6 @@ class Readability_Scores_Route implements Route_Interface { */ public const ROUTE_PREFIX = '/readability_scores'; - /** - * The content type collector. - * - * @var Content_Types_Collector - */ - private $content_types_collector; - - /** - * The taxonomies repository. - * - * @var Taxonomies_Repository - */ - private $taxonomies_repository; - /** * The readability Scores repository. * @@ -50,185 +30,27 @@ class Readability_Scores_Route implements Route_Interface { */ private $readability_scores_repository; - /** - * The indexable repository. - * - * @var Indexable_Repository - */ - private $indexable_repository; - /** * Constructs the class. * - * @param Content_Types_Collector $content_types_collector The content type collector. - * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. * @param Readability_Scores_Repository $readability_scores_repository The readability Scores repository. - * @param Indexable_Repository $indexable_repository The indexable repository. */ public function __construct( - Content_Types_Collector $content_types_collector, - Taxonomies_Repository $taxonomies_repository, - Readability_Scores_Repository $readability_scores_repository, - Indexable_Repository $indexable_repository + Readability_Scores_Repository $readability_scores_repository ) { - $this->content_types_collector = $content_types_collector; - $this->taxonomies_repository = $taxonomies_repository; $this->readability_scores_repository = $readability_scores_repository; - $this->indexable_repository = $indexable_repository; - } - - /** - * Registers routes with WordPress. - * - * @return void - */ - public function register_routes() { - \register_rest_route( - Main::API_V1_NAMESPACE, - self::ROUTE_PREFIX, - [ - [ - 'methods' => 'GET', - 'callback' => [ $this, 'get_readability_scores' ], - 'permission_callback' => [ $this, 'permission_manage_options' ], - 'args' => [ - 'contentType' => [ - 'required' => true, - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - ], - 'term' => [ - 'required' => false, - 'type' => 'integer', - 'default' => null, - 'sanitize_callback' => static function ( $param ) { - return \intval( $param ); - }, - ], - 'taxonomy' => [ - 'required' => false, - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ], - ] - ); - } - - /** - * Gets the readability scores of a specific content type. - * - * @param WP_REST_Request $request The request object. - * - * @return WP_REST_Response|WP_Error The success or failure response. - */ - public function get_readability_scores( WP_REST_Request $request ) { - $content_type = $this->get_content_type( $request['contentType'] ); - if ( $content_type === null ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid content type.', - ], - 400 - ); - } - - $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); - if ( $request['taxonomy'] !== '' && $taxonomy === null ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid taxonomy.', - ], - 400 - ); - } - - if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid term.', - ], - 400 - ); - } - - return new WP_REST_Response( - [ - 'scores' => $this->readability_scores_repository->get_readability_scores( $content_type, $taxonomy, $request['term'] ), - ], - 200 - ); } /** - * Gets the content type object. - * - * @param string $content_type The content type. + * Returns the readability scores of a content type. * - * @return Content_Type|null The content type object. - */ - protected function get_content_type( string $content_type ): ?Content_Type { - $content_types = $this->content_types_collector->get_content_types(); - - if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { - return $content_types[ $content_type ]; - } - - return null; - } - - /** - * Gets the taxonomy object. - * - * @param string $taxonomy The taxonomy. - * @param Content_Type $content_type The content type that the taxonomy is filtering. - * - * @return Taxonomy|null The taxonomy object. - */ - protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { - if ( $taxonomy === '' ) { - return null; - } - - $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); - - if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { - return $valid_taxonomy; - } - - return null; - } - - /** - * Validates the term against the given taxonomy. - * - * @param int $term_id The ID of the term. - * @param Taxonomy|null $taxonomy The taxonomy. - * - * @return bool Whether the term passed validation. - */ - protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { - if ( $term_id === null ) { - return ( $taxonomy === null ); - } - - $term = \get_term( $term_id ); - if ( ! $term || \is_wp_error( $term ) ) { - return false; - } - - $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); - return $term->taxonomy === $taxonomy_name; - } - - /** - * Permission callback. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. * - * @return bool True when user has the 'wpseo_manage_options' capability. + * @return array>> The readability scores. */ - public function permission_manage_options() { - return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + public function calculate_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { + return $this->readability_scores_repository->get_readability_scores( $content_type, $taxonomy, $term_id ); } } diff --git a/src/dashboard/user-interface/scores/scores-route-trait.php b/src/dashboard/user-interface/scores/scores-route-trait.php new file mode 100644 index 00000000000..9603ff1859c --- /dev/null +++ b/src/dashboard/user-interface/scores/scores-route-trait.php @@ -0,0 +1,207 @@ +content_types_collector = $content_types_collector; + } + + /** + * Sets the collectors for the trait. + * + * @required + * + * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. + * @param Indexable_Repository $indexable_repository The indexable repository. + * + * @return void + */ + public function set_repositories( + Taxonomies_Repository $taxonomies_repository, + Indexable_Repository $indexable_repository + ) { + $this->taxonomies_repository = $taxonomies_repository; + $this->indexable_repository = $indexable_repository; + } + + /** + * Registers routes for scores. + * + * @return void + */ + public function register_routes() { + \register_rest_route( + Main::API_V1_NAMESPACE, + self::ROUTE_PREFIX, + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_scores' ], + 'permission_callback' => [ $this, 'permission_manage_options' ], + 'args' => [ + 'contentType' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'term' => [ + 'required' => false, + 'type' => 'integer', + 'default' => null, + 'sanitize_callback' => static function ( $param ) { + return \intval( $param ); + }, + ], + 'taxonomy' => [ + 'required' => false, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ], + ] + ); + } + + /** + * Gets the SEO scores of a specific content type. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response The success or failure response. + */ + public function get_scores( WP_REST_Request $request ) { + $content_type = $this->get_content_type( $request['contentType'] ); + if ( $content_type === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid content type.', + ], + 400 + ); + } + + $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); + if ( $request['taxonomy'] !== '' && $taxonomy === null ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid taxonomy.', + ], + 400 + ); + } + + if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { + return new WP_REST_Response( + [ + 'error' => 'Invalid term.', + ], + 400 + ); + } + + return new WP_REST_Response( + [ + 'scores' => $this->calculate_scores( $content_type, $taxonomy, $request['term'] ), + ], + 200 + ); + } + + /** + * Gets the content type object. + * + * @param string $content_type The content type. + * + * @return Content_Type|null The content type object. + */ + protected function get_content_type( string $content_type ): ?Content_Type { + $content_types = $this->content_types_collector->get_content_types(); + + if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { + return $content_types[ $content_type ]; + } + + return null; + } + + /** + * Gets the taxonomy object. + * + * @param string $taxonomy The taxonomy. + * @param Content_Type $content_type The content type that the taxonomy is filtering. + * + * @return Taxonomy|null The taxonomy object. + */ + protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { + if ( $taxonomy === '' ) { + return null; + } + + $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + + if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { + return $valid_taxonomy; + } + + return null; + } + + /** + * Validates the term against the given taxonomy. + * + * @param int $term_id The ID of the term. + * @param Taxonomy|null $taxonomy The taxonomy. + * + * @return bool Whether the term passed validation. + */ + protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { + if ( $term_id === null ) { + return ( $taxonomy === null ); + } + + $term = \get_term( $term_id ); + if ( ! $term || \is_wp_error( $term ) ) { + return false; + } + + $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); + return $term->taxonomy === $taxonomy_name; + } + + /** + * Permission callback. + * + * @return bool True when user has the 'wpseo_manage_options' capability. + */ + public function permission_manage_options() { + return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + } +} diff --git a/src/dashboard/user-interface/scores/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php index 7f252e5049b..5488504214b 100644 --- a/src/dashboard/user-interface/scores/seo-scores-route.php +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -2,17 +2,10 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use WP_REST_Request; -use WP_REST_Response; -use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores\SEO_Scores_Repository; -use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; -use Yoast\WP\SEO\Main; -use Yoast\WP\SEO\Repositories\Indexable_Repository; use Yoast\WP\SEO\Routes\Route_Interface; /** @@ -20,6 +13,7 @@ */ class SEO_Scores_Route implements Route_Interface { + use Scores_Route_Trait; use No_Conditionals; /** @@ -29,20 +23,6 @@ class SEO_Scores_Route implements Route_Interface { */ public const ROUTE_PREFIX = '/seo_scores'; - /** - * The content type collector. - * - * @var Content_Types_Collector - */ - private $content_types_collector; - - /** - * The taxonomies repository. - * - * @var Taxonomies_Repository - */ - private $taxonomies_repository; - /** * The SEO Scores repository. * @@ -50,185 +30,27 @@ class SEO_Scores_Route implements Route_Interface { */ private $seo_scores_repository; - /** - * The indexable repository. - * - * @var Indexable_Repository - */ - private $indexable_repository; - /** * Constructs the class. * - * @param Content_Types_Collector $content_types_collector The content type collector. - * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. - * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. - * @param Indexable_Repository $indexable_repository The indexable repository. + * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. */ public function __construct( - Content_Types_Collector $content_types_collector, - Taxonomies_Repository $taxonomies_repository, - SEO_Scores_Repository $seo_scores_repository, - Indexable_Repository $indexable_repository + SEO_Scores_Repository $seo_scores_repository ) { - $this->content_types_collector = $content_types_collector; - $this->taxonomies_repository = $taxonomies_repository; - $this->seo_scores_repository = $seo_scores_repository; - $this->indexable_repository = $indexable_repository; - } - - /** - * Registers routes with WordPress. - * - * @return void - */ - public function register_routes() { - \register_rest_route( - Main::API_V1_NAMESPACE, - self::ROUTE_PREFIX, - [ - [ - 'methods' => 'GET', - 'callback' => [ $this, 'get_seo_scores' ], - 'permission_callback' => [ $this, 'permission_manage_options' ], - 'args' => [ - 'contentType' => [ - 'required' => true, - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - ], - 'term' => [ - 'required' => false, - 'type' => 'integer', - 'default' => null, - 'sanitize_callback' => static function ( $param ) { - return \intval( $param ); - }, - ], - 'taxonomy' => [ - 'required' => false, - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ], - ] - ); - } - - /** - * Gets the SEO scores of a specific content type. - * - * @param WP_REST_Request $request The request object. - * - * @return WP_REST_Response|WP_Error The success or failure response. - */ - public function get_seo_scores( WP_REST_Request $request ) { - $content_type = $this->get_content_type( $request['contentType'] ); - if ( $content_type === null ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid content type.', - ], - 400 - ); - } - - $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); - if ( $request['taxonomy'] !== '' && $taxonomy === null ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid taxonomy.', - ], - 400 - ); - } - - if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid term.', - ], - 400 - ); - } - - return new WP_REST_Response( - [ - 'scores' => $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $request['term'] ), - ], - 200 - ); + $this->seo_scores_repository = $seo_scores_repository; } /** - * Gets the content type object. - * - * @param string $content_type The content type. + * Returns the SEO scores of a content type. * - * @return Content_Type|null The content type object. - */ - protected function get_content_type( string $content_type ): ?Content_Type { - $content_types = $this->content_types_collector->get_content_types(); - - if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { - return $content_types[ $content_type ]; - } - - return null; - } - - /** - * Gets the taxonomy object. - * - * @param string $taxonomy The taxonomy. - * @param Content_Type $content_type The content type that the taxonomy is filtering. - * - * @return Taxonomy|null The taxonomy object. - */ - protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { - if ( $taxonomy === '' ) { - return null; - } - - $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); - - if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { - return $valid_taxonomy; - } - - return null; - } - - /** - * Validates the term against the given taxonomy. - * - * @param int $term_id The ID of the term. - * @param Taxonomy|null $taxonomy The taxonomy. - * - * @return bool Whether the term passed validation. - */ - protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { - if ( $term_id === null ) { - return ( $taxonomy === null ); - } - - $term = \get_term( $term_id ); - if ( ! $term || \is_wp_error( $term ) ) { - return false; - } - - $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); - return $term->taxonomy === $taxonomy_name; - } - - /** - * Permission callback. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. * - * @return bool True when user has the 'wpseo_manage_options' capability. + * @return array>> The SEO scores. */ - public function permission_manage_options() { - return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + public function calculate_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { + return $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $term_id ); } } From f66d7f20430dd17156059713b36196e497bb9fda Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 16:53:17 +0200 Subject: [PATCH 064/132] Fix deprecation of creation of dynamic properties for the trait --- .../scores/scores-route-trait.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/dashboard/user-interface/scores/scores-route-trait.php b/src/dashboard/user-interface/scores/scores-route-trait.php index 9603ff1859c..c1b158ba110 100644 --- a/src/dashboard/user-interface/scores/scores-route-trait.php +++ b/src/dashboard/user-interface/scores/scores-route-trait.php @@ -17,6 +17,27 @@ */ trait Scores_Route_Trait { + /** + * The content types collector. + * + * @var Content_Types_Collector + */ + protected $content_types_collector; + + /** + * The taxonomies repository. + * + * @var Taxonomies_Repository + */ + protected $taxonomies_repository; + + /** + * The indexable repository. + * + * @var Indexable_Repository + */ + protected $indexable_repository; + /** * Sets the collectors for the trait. * From 4c43d5479a8dd376a1728e1a217dc0e5880badaa Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Wed, 20 Nov 2024 17:03:34 +0200 Subject: [PATCH 065/132] Fix the return type of the endpoint --- src/dashboard/user-interface/scores/scores-route-trait.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dashboard/user-interface/scores/scores-route-trait.php b/src/dashboard/user-interface/scores/scores-route-trait.php index c1b158ba110..8cd4ed0f3ab 100644 --- a/src/dashboard/user-interface/scores/scores-route-trait.php +++ b/src/dashboard/user-interface/scores/scores-route-trait.php @@ -149,9 +149,7 @@ public function get_scores( WP_REST_Request $request ) { } return new WP_REST_Response( - [ - 'scores' => $this->calculate_scores( $content_type, $taxonomy, $request['term'] ), - ], + $this->calculate_scores( $content_type, $taxonomy, $request['term'] ), 200 ); } From 3ffb5484bf971c9965a9142fb8c087cb570ab492 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Thu, 21 Nov 2024 08:53:28 +0200 Subject: [PATCH 066/132] DRY up --- .../readability-scores-repository.php | 68 ++----------- .../scores/scores-repository-interface.php | 24 +++++ .../scores/scores-repository-trait.php | 96 +++++++++++++++++++ .../seo-scores/seo-scores-repository.php | 68 ++----------- .../readability-scores-collector.php | 7 +- .../scores/scores-collector-interface.php | 24 +++++ .../seo-scores/seo-scores-collector.php | 7 +- .../scores/readability-scores-route.php | 26 +---- .../scores/scores-route-trait.php | 24 +++-- .../scores/seo-scores-route.php | 26 +---- 10 files changed, 184 insertions(+), 186 deletions(-) create mode 100644 src/dashboard/application/scores/scores-repository-interface.php create mode 100644 src/dashboard/application/scores/scores-repository-trait.php create mode 100644 src/dashboard/infrastructure/scores/scores-collector-interface.php diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php index 08f1d2cece8..d4f23f4a022 100644 --- a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -3,85 +3,29 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; +use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Trait; use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_List; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Readability_Scores\Readability_Scores_Collector; -use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; /** * The repository to get readability Scores. */ -class Readability_Scores_Repository { +class Readability_Scores_Repository implements Scores_Repository_Interface { - /** - * The readability scores collector. - * - * @var Readability_Scores_Collector - */ - private $readability_scores_collector; - - /** - * The score link collector. - * - * @var Score_Link_Collector - */ - private $score_link_collector; - - /** - * The scores list. - * - * @var Scores_List - */ - protected $scores_list; - - /** - * All readability scores. - * - * @var Readability_Scores_Interface[] - */ - private $readability_scores; + use Scores_Repository_Trait; /** * The constructor. * * @param Readability_Scores_Collector $readability_scores_collector The readability scores collector. - * @param Score_Link_Collector $score_link_collector The score link collector. - * @param Scores_List $scores_list The scores list. * @param Readability_Scores_Interface ...$readability_scores All readability scores. */ public function __construct( Readability_Scores_Collector $readability_scores_collector, - Score_Link_Collector $score_link_collector, - Scores_List $scores_list, Readability_Scores_Interface ...$readability_scores ) { - $this->readability_scores_collector = $readability_scores_collector; - $this->score_link_collector = $score_link_collector; - $this->scores_list = $scores_list; - $this->readability_scores = $readability_scores; - } - - /** - * Returns the readability Scores of a content type. - * - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. - * @param int|null $term_id The ID of the term we're filtering for. - * - * @return array>> The readability scores. - */ - public function get_readability_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { - $current_scores = $this->readability_scores_collector->get_readability_scores( $this->readability_scores, $content_type, $term_id ); - - foreach ( $this->readability_scores as $readability_score ) { - $readability_score->set_amount( (int) $current_scores[ $readability_score->get_name() ] ); - $readability_score->set_view_link( $this->score_link_collector->get_view_link( $readability_score, $content_type, $taxonomy, $term_id ) ); - - $this->scores_list->add( $readability_score ); - } - - return $this->scores_list->to_array(); + $this->scores_collector = $readability_scores_collector; + $this->scores = $readability_scores; } } diff --git a/src/dashboard/application/scores/scores-repository-interface.php b/src/dashboard/application/scores/scores-repository-interface.php new file mode 100644 index 00000000000..3dd84acab7c --- /dev/null +++ b/src/dashboard/application/scores/scores-repository-interface.php @@ -0,0 +1,24 @@ +>> The scores. + */ + public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array; +} diff --git a/src/dashboard/application/scores/scores-repository-trait.php b/src/dashboard/application/scores/scores-repository-trait.php new file mode 100644 index 00000000000..84559158698 --- /dev/null +++ b/src/dashboard/application/scores/scores-repository-trait.php @@ -0,0 +1,96 @@ +scores_list = $scores_list; + } + + /** + * Sets the score link collector for the trait. + * + * @required + * + * @param Score_Link_Collector $score_link_collector The score link collector. + * + * @return void + */ + public function set_score_link_collector( + Score_Link_Collector $score_link_collector + ) { + $this->score_link_collector = $score_link_collector; + } + + /** + * Returns the scores of a content type. + * + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. + * + * @return array>> The scores. + */ + public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + $current_scores = $this->scores_collector->get_current_scores( $this->scores, $content_type, $term_id ); + + foreach ( $this->scores as $score ) { + $score->set_amount( (int) $current_scores[ $score->get_name() ] ); + $score->set_view_link( $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ) ); + + $this->scores_list->add( $score ); + } + + return $this->scores_list->to_array(); + } +} diff --git a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php index d79b24f129d..3aced6d7fda 100644 --- a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php @@ -3,85 +3,29 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_List; +use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; +use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Trait; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\SEO_Scores\SEO_Scores_Collector; /** * The repository to get SEO Scores. */ -class SEO_Scores_Repository { +class SEO_Scores_Repository implements Scores_Repository_Interface { - /** - * The SEO scores collector. - * - * @var SEO_Scores_Collector - */ - private $seo_scores_collector; - - /** - * The score link collector. - * - * @var Score_Link_Collector - */ - private $score_link_collector; - - /** - * The scores list. - * - * @var Scores_List - */ - protected $scores_list; - - /** - * All SEO scores. - * - * @var SEO_Scores_Interface[] - */ - private $seo_scores; + use Scores_Repository_Trait; /** * The constructor. * * @param SEO_Scores_Collector $seo_scores_collector The SEO scores collector. - * @param Score_Link_Collector $score_link_collector The score link collector. - * @param Scores_List $scores_list The scores list. * @param SEO_Scores_Interface ...$seo_scores All SEO scores. */ public function __construct( SEO_Scores_Collector $seo_scores_collector, - Score_Link_Collector $score_link_collector, - Scores_List $scores_list, SEO_Scores_Interface ...$seo_scores ) { - $this->seo_scores_collector = $seo_scores_collector; - $this->score_link_collector = $score_link_collector; - $this->scores_list = $scores_list; - $this->seo_scores = $seo_scores; - } - - /** - * Returns the SEO Scores of a content type. - * - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. - * @param int|null $term_id The ID of the term we're filtering for. - * - * @return array>> The SEO scores. - */ - public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { - $current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $term_id ); - - foreach ( $this->seo_scores as $seo_score ) { - $seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] ); - $seo_score->set_view_link( $this->score_link_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) ); - - $this->scores_list->add( $seo_score ); - } - - return $this->scores_list->to_array(); + $this->scores_collector = $seo_scores_collector; + $this->scores = $seo_scores; } } diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 4b59ac85800..4ba6d68a561 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -6,11 +6,12 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Scores_Collector_Interface; /** * Getting readability scores from the indexable database table. */ -class Readability_Scores_Collector { +class Readability_Scores_Collector implements Scores_Collector_Interface { /** * Retrieves the current readability scores for a content type. @@ -19,9 +20,9 @@ class Readability_Scores_Collector { * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array The readability scores for a content type. + * @return array The current readability scores for a content type. */ - public function get_readability_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { + public function get_current_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; $select = $this->build_select( $readability_scores ); diff --git a/src/dashboard/infrastructure/scores/scores-collector-interface.php b/src/dashboard/infrastructure/scores/scores-collector-interface.php new file mode 100644 index 00000000000..06d398e84a7 --- /dev/null +++ b/src/dashboard/infrastructure/scores/scores-collector-interface.php @@ -0,0 +1,24 @@ + The current scores for a content type. + */ + public function get_current_scores( array $scores, Content_Type $content_type, ?int $term_id ); +} diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index c924ea061af..4fb3e1dbb7e 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -6,11 +6,12 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Scores_Collector_Interface; /** * Getting SEO scores from the indexable database table. */ -class SEO_Scores_Collector { +class SEO_Scores_Collector implements Scores_Collector_Interface { /** * Retrieves the current SEO scores for a content type. @@ -19,9 +20,9 @@ class SEO_Scores_Collector { * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array The SEO scores for a content type. + * @return array The current SEO scores for a content type. */ - public function get_seo_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { + public function get_current_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; $select = $this->build_select( $seo_scores ); diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php index a1d6b0760a3..896be0b4ee4 100644 --- a/src/dashboard/user-interface/scores/readability-scores-route.php +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -4,8 +4,6 @@ use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores\Readability_Scores_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Routes\Route_Interface; /** @@ -23,34 +21,14 @@ class Readability_Scores_Route implements Route_Interface { */ public const ROUTE_PREFIX = '/readability_scores'; - /** - * The readability Scores repository. - * - * @var Readability_Scores_Repository - */ - private $readability_scores_repository; - /** * Constructs the class. * - * @param Readability_Scores_Repository $readability_scores_repository The readability Scores repository. + * @param Readability_Scores_Repository $readability_scores_repository The readability scores repository. */ public function __construct( Readability_Scores_Repository $readability_scores_repository ) { - $this->readability_scores_repository = $readability_scores_repository; - } - - /** - * Returns the readability scores of a content type. - * - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. - * @param int|null $term_id The ID of the term we're filtering for. - * - * @return array>> The readability scores. - */ - public function calculate_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { - return $this->readability_scores_repository->get_readability_scores( $content_type, $taxonomy, $term_id ); + $this->scores_repository = $readability_scores_repository; } } diff --git a/src/dashboard/user-interface/scores/scores-route-trait.php b/src/dashboard/user-interface/scores/scores-route-trait.php index 8cd4ed0f3ab..02247ab4ee3 100644 --- a/src/dashboard/user-interface/scores/scores-route-trait.php +++ b/src/dashboard/user-interface/scores/scores-route-trait.php @@ -5,6 +5,7 @@ use WP_REST_Request; use WP_REST_Response; use WPSEO_Capability_Utils; +use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; @@ -38,6 +39,13 @@ trait Scores_Route_Trait { */ protected $indexable_repository; + /** + * The scores repository. + * + * @var Scores_Repository_Interface + */ + private $scores_repository; + /** * Sets the collectors for the trait. * @@ -91,6 +99,12 @@ public function register_routes() { 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], + 'taxonomy' => [ + 'required' => false, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ], 'term' => [ 'required' => false, 'type' => 'integer', @@ -99,12 +113,6 @@ public function register_routes() { return \intval( $param ); }, ], - 'taxonomy' => [ - 'required' => false, - 'type' => 'string', - 'default' => '', - 'sanitize_callback' => 'sanitize_text_field', - ], ], ], ] @@ -112,7 +120,7 @@ public function register_routes() { } /** - * Gets the SEO scores of a specific content type. + * Gets the scores of a specific content type. * * @param WP_REST_Request $request The request object. * @@ -149,7 +157,7 @@ public function get_scores( WP_REST_Request $request ) { } return new WP_REST_Response( - $this->calculate_scores( $content_type, $taxonomy, $request['term'] ), + $this->scores_repository->get_scores( $content_type, $taxonomy, $request['term'] ), 200 ); } diff --git a/src/dashboard/user-interface/scores/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php index 5488504214b..d1ea7fab623 100644 --- a/src/dashboard/user-interface/scores/seo-scores-route.php +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -4,8 +4,6 @@ use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores\SEO_Scores_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Routes\Route_Interface; /** @@ -23,34 +21,14 @@ class SEO_Scores_Route implements Route_Interface { */ public const ROUTE_PREFIX = '/seo_scores'; - /** - * The SEO Scores repository. - * - * @var SEO_Scores_Repository - */ - private $seo_scores_repository; - /** * Constructs the class. * - * @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository. + * @param SEO_Scores_Repository $seo_scores_repository The SEO scores repository. */ public function __construct( SEO_Scores_Repository $seo_scores_repository ) { - $this->seo_scores_repository = $seo_scores_repository; - } - - /** - * Returns the SEO scores of a content type. - * - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. - * @param int|null $term_id The ID of the term we're filtering for. - * - * @return array>> The SEO scores. - */ - public function calculate_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) { - return $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $term_id ); + $this->scores_repository = $seo_scores_repository; } } From b6c54283e7bc2ab24f1cfcf247892a2309b735c9 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Thu, 21 Nov 2024 09:35:58 +0200 Subject: [PATCH 067/132] Clean up --- .../scores/readability-scores/readability-scores-repository.php | 2 +- .../scores/readability-scores/readability-scores-interface.php | 2 +- src/dashboard/domain/scores/scores-list.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php index d4f23f4a022..2fca55e58af 100644 --- a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -9,7 +9,7 @@ use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Readability_Scores\Readability_Scores_Collector; /** - * The repository to get readability Scores. + * The repository to get readability scores. */ class Readability_Scores_Repository implements Scores_Repository_Interface { diff --git a/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php b/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php index c57a623d045..6b985cfa2b5 100644 --- a/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php +++ b/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php @@ -5,6 +5,6 @@ use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; /** - * This interface describes a score implementation. + * This interface describes a readability score implementation. */ interface Readability_Scores_Interface extends Scores_Interface {} diff --git a/src/dashboard/domain/scores/scores-list.php b/src/dashboard/domain/scores/scores-list.php index f27d8de6ad6..dd0d2d10641 100644 --- a/src/dashboard/domain/scores/scores-list.php +++ b/src/dashboard/domain/scores/scores-list.php @@ -10,7 +10,7 @@ class Scores_List { /** * The scores. * - * @var array + * @var Scores_Interface[] */ private $scores = []; From 76b5ccac0924243ee4e9ee73f1f578ecc7048f0c Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Thu, 21 Nov 2024 15:01:44 +0200 Subject: [PATCH 068/132] Don't pass domain object through DI --- .../content-types/content-types-repository.php | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/dashboard/application/content-types/content-types-repository.php b/src/dashboard/application/content-types/content-types-repository.php index 0424194a50a..152e5b1e2ec 100644 --- a/src/dashboard/application/content-types/content-types-repository.php +++ b/src/dashboard/application/content-types/content-types-repository.php @@ -19,13 +19,6 @@ class Content_Types_Repository { */ protected $content_types_collector; - /** - * The content types list. - * - * @var Content_Types_List - */ - protected $content_types_list; - /** * The taxonomies repository. * @@ -37,16 +30,13 @@ class Content_Types_Repository { * The constructor. * * @param Content_Types_Collector $content_types_collector The post type helper. - * @param Content_Types_List $content_types_list The content types list. * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. */ public function __construct( Content_Types_Collector $content_types_collector, - Content_Types_List $content_types_list, Taxonomies_Repository $taxonomies_repository ) { $this->content_types_collector = $content_types_collector; - $this->content_types_list = $content_types_list; $this->taxonomies_repository = $taxonomies_repository; } @@ -56,15 +46,16 @@ public function __construct( * @return array>>>> The content types array. */ public function get_content_types(): array { - $content_types = $this->content_types_collector->get_content_types(); + $content_types_list = new Content_Types_List(); + $content_types = $this->content_types_collector->get_content_types(); foreach ( $content_types as $content_type ) { $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); $content_type->set_taxonomy( $content_type_taxonomy ); - $this->content_types_list->add( $content_type ); + $content_types_list->add( $content_type ); } - return $this->content_types_list->to_array(); + return $content_types_list->to_array(); } } From 8aa5fec268996cca768e5168a2535604e3284b5e Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Thu, 21 Nov 2024 15:39:17 +0200 Subject: [PATCH 069/132] Import Scores_Interface --- src/dashboard/application/scores/scores-repository-trait.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dashboard/application/scores/scores-repository-trait.php b/src/dashboard/application/scores/scores-repository-trait.php index 84559158698..532ac3a6c2e 100644 --- a/src/dashboard/application/scores/scores-repository-trait.php +++ b/src/dashboard/application/scores/scores-repository-trait.php @@ -4,6 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Scores; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; From 4c786a5b2d862051e2e46fabdf6cdaf6c80b337b Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Thu, 21 Nov 2024 15:47:46 +0200 Subject: [PATCH 070/132] Don't pass scores list domain object through DI --- .../scores/scores-repository-trait.php | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/dashboard/application/scores/scores-repository-trait.php b/src/dashboard/application/scores/scores-repository-trait.php index 532ac3a6c2e..856aa79dfe1 100644 --- a/src/dashboard/application/scores/scores-repository-trait.php +++ b/src/dashboard/application/scores/scores-repository-trait.php @@ -29,13 +29,6 @@ trait Scores_Repository_Trait { */ private $score_link_collector; - /** - * The scores list. - * - * @var Scores_List - */ - protected $scores_list; - /** * All scores. * @@ -43,21 +36,6 @@ trait Scores_Repository_Trait { */ private $scores; - /** - * Sets the scores list for the trait. - * - * @required - * - * @param Scores_List $scores_list The scores list. - * - * @return void - */ - public function set_scores_list( - Scores_List $scores_list - ) { - $this->scores_list = $scores_list; - } - /** * Sets the score link collector for the trait. * @@ -83,15 +61,16 @@ public function set_score_link_collector( * @return array>> The scores. */ public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + $scores_list = new Scores_List(); $current_scores = $this->scores_collector->get_current_scores( $this->scores, $content_type, $term_id ); foreach ( $this->scores as $score ) { $score->set_amount( (int) $current_scores[ $score->get_name() ] ); $score->set_view_link( $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ) ); - $this->scores_list->add( $score ); + $scores_list->add( $score ); } - return $this->scores_list->to_array(); + return $scores_list->to_array(); } } From 9084ce6c3833aa6c48c017c5c25e35ab44b98006 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 09:37:49 +0200 Subject: [PATCH 071/132] Use abstract classes instead of traits --- ...ait.php => abstract-scores-repository.php} | 12 +++--- .../readability-scores-repository.php | 7 +--- .../seo-scores/seo-scores-repository.php | 7 +--- ...te-trait.php => abstract-scores-route.php} | 42 ++++++++++++++++--- .../scores/readability-scores-route.php | 9 +--- .../scores/seo-scores-route.php | 9 +--- 6 files changed, 50 insertions(+), 36 deletions(-) rename src/dashboard/application/scores/{scores-repository-trait.php => abstract-scores-repository.php} (89%) rename src/dashboard/user-interface/scores/{scores-route-trait.php => abstract-scores-route.php} (87%) diff --git a/src/dashboard/application/scores/scores-repository-trait.php b/src/dashboard/application/scores/abstract-scores-repository.php similarity index 89% rename from src/dashboard/application/scores/scores-repository-trait.php rename to src/dashboard/application/scores/abstract-scores-repository.php index 856aa79dfe1..d9e3ac988a3 100644 --- a/src/dashboard/application/scores/scores-repository-trait.php +++ b/src/dashboard/application/scores/abstract-scores-repository.php @@ -11,33 +11,33 @@ use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Scores_Collector_Interface; /** - * The trait to scores repositories. + * The abstract scores repository. */ -trait Scores_Repository_Trait { +abstract class Abstract_Scores_Repository implements Scores_Repository_Interface { /** * The scores collector. * * @var Scores_Collector_Interface */ - private $scores_collector; + protected $scores_collector; /** * The score link collector. * * @var Score_Link_Collector */ - private $score_link_collector; + protected $score_link_collector; /** * All scores. * * @var Scores_Interface[] */ - private $scores; + protected $scores; /** - * Sets the score link collector for the trait. + * Sets the score link collector. * * @required * diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php index 2fca55e58af..cbd51810975 100644 --- a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -3,17 +3,14 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores; -use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; -use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Trait; +use Yoast\WP\SEO\Dashboard\Application\Scores\Abstract_Scores_Repository; use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Readability_Scores\Readability_Scores_Collector; /** * The repository to get readability scores. */ -class Readability_Scores_Repository implements Scores_Repository_Interface { - - use Scores_Repository_Trait; +class Readability_Scores_Repository extends Abstract_Scores_Repository { /** * The constructor. diff --git a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php index 3aced6d7fda..7fee6024d7c 100644 --- a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php +++ b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php @@ -3,17 +3,14 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores; -use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; -use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Trait; +use Yoast\WP\SEO\Dashboard\Application\Scores\Abstract_Scores_Repository; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\SEO_Scores\SEO_Scores_Collector; /** * The repository to get SEO Scores. */ -class SEO_Scores_Repository implements Scores_Repository_Interface { - - use Scores_Repository_Trait; +class SEO_Scores_Repository extends Abstract_Scores_Repository { /** * The constructor. diff --git a/src/dashboard/user-interface/scores/scores-route-trait.php b/src/dashboard/user-interface/scores/abstract-scores-route.php similarity index 87% rename from src/dashboard/user-interface/scores/scores-route-trait.php rename to src/dashboard/user-interface/scores/abstract-scores-route.php index 02247ab4ee3..b51d93ad946 100644 --- a/src/dashboard/user-interface/scores/scores-route-trait.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -2,9 +2,11 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; +use Exception; use WP_REST_Request; use WP_REST_Response; use WPSEO_Capability_Utils; +use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; @@ -12,11 +14,21 @@ use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; use Yoast\WP\SEO\Main; use Yoast\WP\SEO\Repositories\Indexable_Repository; +use Yoast\WP\SEO\Routes\Route_Interface; /** - * Trait for routes of scores. + * Abstract scores route. */ -trait Scores_Route_Trait { +abstract class Abstract_Scores_Route implements Route_Interface { + + use No_Conditionals; + + /** + * The prefix of the rout. + * + * @var string + */ + public const ROUTE_PREFIX = null; /** * The content types collector. @@ -44,10 +56,10 @@ trait Scores_Route_Trait { * * @var Scores_Repository_Interface */ - private $scores_repository; + protected $scores_repository; /** - * Sets the collectors for the trait. + * Sets the collectors. * * @required * @@ -62,7 +74,7 @@ public function set_collectors( } /** - * Sets the collectors for the trait. + * Sets the repositories. * * @required * @@ -79,6 +91,24 @@ public function set_repositories( $this->indexable_repository = $indexable_repository; } + /** + * Returns the route prefix. + * + * @return string The route prefix. + * + * @throws Exception If the ROUTE_PREFIX constant is not set in the child class. + */ + public function get_route_prefix() { + $class = static::class; + $prefix = $class::ROUTE_PREFIX; + + if ( $prefix === null ) { + throw new Exception( 'Score route without explicit prefix' ); + } + + return $prefix; + } + /** * Registers routes for scores. * @@ -87,7 +117,7 @@ public function set_repositories( public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, - self::ROUTE_PREFIX, + $this->get_route_prefix(), [ [ 'methods' => 'GET', diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php index 896be0b4ee4..680d03b265e 100644 --- a/src/dashboard/user-interface/scores/readability-scores-route.php +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -2,20 +2,15 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores\Readability_Scores_Repository; -use Yoast\WP\SEO\Routes\Route_Interface; /** * Registers a route to get readability scores. */ -class Readability_Scores_Route implements Route_Interface { - - use Scores_Route_Trait; - use No_Conditionals; +class Readability_Scores_Route extends Abstract_Scores_Route { /** - * Represents the prefix. + * The prefix of the route. * * @var string */ diff --git a/src/dashboard/user-interface/scores/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php index d1ea7fab623..8fd7c79647d 100644 --- a/src/dashboard/user-interface/scores/seo-scores-route.php +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -2,20 +2,15 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use Yoast\WP\SEO\Conditionals\No_Conditionals; use Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores\SEO_Scores_Repository; -use Yoast\WP\SEO\Routes\Route_Interface; /** * Registers a route to get SEO scores. */ -class SEO_Scores_Route implements Route_Interface { - - use Scores_Route_Trait; - use No_Conditionals; +class SEO_Scores_Route extends Abstract_Scores_Route { /** - * Represents the prefix. + * The prefix of the route. * * @var string */ From 24be8d37935be379b59df47eb9f50f5cacb4ff9a Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 09:41:54 +0200 Subject: [PATCH 072/132] Fix parameter type --- src/dashboard/user-interface/scores/abstract-scores-route.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index b51d93ad946..f9c5e8b39e0 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -234,7 +234,7 @@ protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): /** * Validates the term against the given taxonomy. * - * @param int $term_id The ID of the term. + * @param int|null $term_id The ID of the term. * @param Taxonomy|null $taxonomy The taxonomy. * * @return bool Whether the term passed validation. From 19c8839603efc560f2c4876986c058a1a6e9fe8b Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 11:26:13 +0200 Subject: [PATCH 073/132] Use exceptions when validating API parameters --- .../scores/abstract-scores-route.php | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index f9c5e8b39e0..7491a9ba9b8 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -157,37 +157,21 @@ public function register_routes() { * @return WP_REST_Response The success or failure response. */ public function get_scores( WP_REST_Request $request ) { - $content_type = $this->get_content_type( $request['contentType'] ); - if ( $content_type === null ) { + try { + $content_type = $this->get_content_type( $request['contentType'] ); + $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); + $term_id = $this->get_validated_term_id( $request['term'], $taxonomy ); + } catch ( Exception $exception ) { return new WP_REST_Response( [ - 'error' => 'Invalid content type.', + 'error' => $exception->getMessage(), ], - 400 - ); - } - - $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); - if ( $request['taxonomy'] !== '' && $taxonomy === null ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid taxonomy.', - ], - 400 - ); - } - - if ( ! $this->validate_term( $request['term'], $taxonomy ) ) { - return new WP_REST_Response( - [ - 'error' => 'Invalid term.', - ], - 400 + $exception->getCode() ); } return new WP_REST_Response( - $this->scores_repository->get_scores( $content_type, $taxonomy, $request['term'] ), + $this->scores_repository->get_scores( $content_type, $taxonomy, $term_id ), 200 ); } @@ -198,6 +182,8 @@ public function get_scores( WP_REST_Request $request ) { * @param string $content_type The content type. * * @return Content_Type|null The content type object. + * + * @throws Exception When the content type is invalid. */ protected function get_content_type( string $content_type ): ?Content_Type { $content_types = $this->content_types_collector->get_content_types(); @@ -206,7 +192,7 @@ protected function get_content_type( string $content_type ): ?Content_Type { return $content_types[ $content_type ]; } - return null; + throw new Exception( 'Invalid content type.', 400 ); } /** @@ -216,6 +202,8 @@ protected function get_content_type( string $content_type ): ?Content_Type { * @param Content_Type $content_type The content type that the taxonomy is filtering. * * @return Taxonomy|null The taxonomy object. + * + * @throws Exception When the taxonomy is invalid. */ protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { if ( $taxonomy === '' ) { @@ -228,29 +216,40 @@ protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): return $valid_taxonomy; } - return null; + throw new Exception( 'Invalid taxonomy.', 400 ); } /** - * Validates the term against the given taxonomy. + * Gets the term ID validated against the given taxonomy. * - * @param int|null $term_id The ID of the term. + * @param int|null $term_id The term ID to be validated. * @param Taxonomy|null $taxonomy The taxonomy. * - * @return bool Whether the term passed validation. + * @return bool The validated term ID. + * + * @throws Exception When the term id is invalidated. */ - protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool { - if ( $term_id === null ) { - return ( $taxonomy === null ); + protected function get_validated_term_id( ?int $term_id, ?Taxonomy $taxonomy ): ?int { + if ( $term_id !== null && $taxonomy === null ) { + throw new Exception( 'Term needs a provided taxonomy.', 400 ); + } + + if ( $term_id === null && $taxonomy !== null ) { + throw new Exception( 'Taxonomy needs a provided term.', 400 ); } - $term = \get_term( $term_id ); - if ( ! $term || \is_wp_error( $term ) ) { - return false; + if ( $term_id !== null ) { + $term = \get_term( $term_id ); + if ( ! $term || \is_wp_error( $term ) ) { + throw new Exception( 'Invalid term.', 400 ); + } + + if ( $taxonomy !== null && $term->taxonomy !== $taxonomy->get_name() ) { + throw new Exception( 'Invalid term.', 400 ); + } } - $taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name(); - return $term->taxonomy === $taxonomy_name; + return $term_id; } /** From 188b4e49bdd169e4785f7024035d9ef8b918c5fd Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 12:01:04 +0200 Subject: [PATCH 074/132] Use list instead of array in the collector of the content types --- .../content-types/content-types-repository.php | 8 ++------ .../domain/content-types/content-types-list.php | 11 ++++++++++- .../content-types/content-types-collector.php | 16 +++++++++------- .../scores/abstract-scores-route.php | 2 +- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/dashboard/application/content-types/content-types-repository.php b/src/dashboard/application/content-types/content-types-repository.php index 152e5b1e2ec..5c9d443a5cb 100644 --- a/src/dashboard/application/content-types/content-types-repository.php +++ b/src/dashboard/application/content-types/content-types-repository.php @@ -4,7 +4,6 @@ namespace Yoast\WP\SEO\Dashboard\Application\Content_Types; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Types_List; use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; /** @@ -46,14 +45,11 @@ public function __construct( * @return array>>>> The content types array. */ public function get_content_types(): array { - $content_types_list = new Content_Types_List(); - $content_types = $this->content_types_collector->get_content_types(); + $content_types_list = $this->content_types_collector->get_content_types(); - foreach ( $content_types as $content_type ) { + foreach ( $content_types_list->get() as $content_type ) { $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); $content_type->set_taxonomy( $content_type_taxonomy ); - - $content_types_list->add( $content_type ); } return $content_types_list->to_array(); diff --git a/src/dashboard/domain/content-types/content-types-list.php b/src/dashboard/domain/content-types/content-types-list.php index 520161e2a54..ad77c889ab8 100644 --- a/src/dashboard/domain/content-types/content-types-list.php +++ b/src/dashboard/domain/content-types/content-types-list.php @@ -22,7 +22,16 @@ class Content_Types_List { * @return void */ public function add( Content_Type $content_type ): void { - $this->content_types[] = $content_type; + $this->content_types[ $content_type->get_name() ] = $content_type; + } + + /** + * Returns the content types in the list. + * + * @return array The content types in the list. + */ + public function get(): array { + return $this->content_types; } /** diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php index f4927b51bd3..02b5af03139 100644 --- a/src/dashboard/infrastructure/content-types/content-types-collector.php +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -4,6 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Types_List; use Yoast\WP\SEO\Helpers\Post_Type_Helper; /** * Class that collects post types and relevant information. @@ -29,20 +30,21 @@ public function __construct( } /** - * Returns the content types array. + * Returns the content types in a list. * - * @return array The content types array. + * @return Content_Types_List The content types in a list. */ - public function get_content_types(): array { - $content_types = []; - $post_types = $this->post_type_helper->get_indexable_post_types(); + public function get_content_types(): Content_Types_List { + $content_types_list = new Content_Types_List(); + $post_types = $this->post_type_helper->get_indexable_post_types(); foreach ( $post_types as $post_type ) { $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. - $content_types[ $post_type_object->name ] = new Content_Type( $post_type_object->name, $post_type_object->label ); + $content_type = new Content_Type( $post_type_object->name, $post_type_object->label ); + $content_types_list->add( $content_type ); } - return $content_types; + return $content_types_list; } } diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index 7491a9ba9b8..72f69930f21 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -186,7 +186,7 @@ public function get_scores( WP_REST_Request $request ) { * @throws Exception When the content type is invalid. */ protected function get_content_type( string $content_type ): ?Content_Type { - $content_types = $this->content_types_collector->get_content_types(); + $content_types = $this->content_types_collector->get_content_types()->get(); if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { return $content_types[ $content_type ]; From 85d9dd5a4b23cb66626d2da53062fda458927b03 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 15:10:11 +0200 Subject: [PATCH 075/132] Have the queries return objects --- .../application/scores/abstract-scores-repository.php | 3 ++- .../readability-scores/readability-scores-collector.php | 6 ++---- .../scores/seo-scores/seo-scores-collector.php | 6 ++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/dashboard/application/scores/abstract-scores-repository.php b/src/dashboard/application/scores/abstract-scores-repository.php index d9e3ac988a3..fb46e9d80d6 100644 --- a/src/dashboard/application/scores/abstract-scores-repository.php +++ b/src/dashboard/application/scores/abstract-scores-repository.php @@ -65,7 +65,8 @@ public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?in $current_scores = $this->scores_collector->get_current_scores( $this->scores, $content_type, $term_id ); foreach ( $this->scores as $score ) { - $score->set_amount( (int) $current_scores[ $score->get_name() ] ); + $score_name = $score->get_name(); + $score->set_amount( (int) $current_scores->$score_name ); $score->set_view_link( $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ) ); $scores_list->add( $score ); diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 4ba6d68a561..1e3c78455aa 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -48,8 +48,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con AND I.object_type IN ('post') AND I.object_sub_type IN (%s)", $replacements - ), - \ARRAY_A + ) ); //phpcs:enable return $current_scores; @@ -77,8 +76,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con WHERE term_taxonomy_id = %d )", $replacements - ), - \ARRAY_A + ) ); //phpcs:enable return $current_scores; diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index 4fb3e1dbb7e..a982e7476c6 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -49,8 +49,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ AND I.object_sub_type IN (%s) AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", $replacements - ), - \ARRAY_A + ) ); //phpcs:enable return $current_scores; @@ -79,8 +78,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ WHERE term_taxonomy_id = %d )", $replacements - ), - \ARRAY_A + ) ); //phpcs:enable return $current_scores; From 610c0d8c4f2368aeb9bc466ff55a0ced0b1f3ed5 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 15:19:40 +0200 Subject: [PATCH 076/132] Minor optimization in the collector queries --- .../readability-scores/readability-scores-collector.php | 8 ++++---- .../scores/seo-scores/seo-scores-collector.php | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 1e3c78455aa..8edb76b1ee0 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -45,8 +45,8 @@ public function get_current_scores( array $readability_scores, Content_Type $con SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s)", + AND I.object_type = 'post' + AND I.object_sub_type = %s", $replacements ) ); @@ -68,8 +68,8 @@ public function get_current_scores( array $readability_scores, Content_Type $con SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s) + AND I.object_type = 'post' + AND I.object_sub_type = %s AND I.object_id IN ( SELECT object_id FROM %i diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index a982e7476c6..0a8f4503465 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -45,8 +45,8 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ SELECT {$select['fields']} FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type IN ('post') - AND I.object_sub_type IN (%s) + AND I.object_type = 'post' + AND I.object_sub_type = %s AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", $replacements ) @@ -70,7 +70,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ FROM %i AS I WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) AND I.object_type IN ('post') - AND I.object_sub_type IN (%s) + AND I.object_sub_type = %s AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 ) AND I.object_id IN ( SELECT object_id From 9ad483b49be2ad7757b6fb2a3d1045f848037e16 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Fri, 22 Nov 2024 16:25:13 +0200 Subject: [PATCH 077/132] Add microcaching around the score queries --- .../readability-scores-collector.php | 16 +++++++++++++++- .../scores/seo-scores/seo-scores-collector.php | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 8edb76b1ee0..582a8b2be2d 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -3,6 +3,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Readability_Scores; +use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; @@ -13,6 +14,8 @@ */ class Readability_Scores_Collector implements Scores_Collector_Interface { + public const READABILITY_SCORES_TRANSIENT = 'wpseo_readability_scores'; + /** * Retrieves the current readability scores for a content type. * @@ -24,13 +27,22 @@ class Readability_Scores_Collector implements Scores_Collector_Interface { */ public function get_current_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; + + $content_type_name = $content_type->get_name(); + $transient_name = self::READABILITY_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); + + $transient = \get_transient( $transient_name ); + if ( $transient !== false ) { + return \json_decode( $transient, false ); + } + $select = $this->build_select( $readability_scores ); $replacements = \array_merge( \array_values( $select['replacements'] ), [ Model::get_table_name( 'Indexable' ), - $content_type->get_name(), + $content_type_name, ] ); @@ -51,6 +63,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con ) ); //phpcs:enable + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); return $current_scores; } @@ -79,6 +92,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con ) ); //phpcs:enable + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); return $current_scores; } diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index 0a8f4503465..44232fa6974 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -3,6 +3,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Infrastructure\Scores\SEO_Scores; +use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; @@ -13,6 +14,8 @@ */ class SEO_Scores_Collector implements Scores_Collector_Interface { + public const SEO_SCORES_TRANSIENT = 'wpseo_seo_scores'; + /** * Retrieves the current SEO scores for a content type. * @@ -24,13 +27,22 @@ class SEO_Scores_Collector implements Scores_Collector_Interface { */ public function get_current_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; + + $content_type_name = $content_type->get_name(); + $transient_name = self::SEO_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); + + $transient = \get_transient( $transient_name ); + if ( $transient !== false ) { + return \json_decode( $transient, false ); + } + $select = $this->build_select( $seo_scores ); $replacements = \array_merge( \array_values( $select['replacements'] ), [ Model::get_table_name( 'Indexable' ), - $content_type->get_name(), + $content_type_name, ] ); @@ -52,6 +64,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ ) ); //phpcs:enable + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); return $current_scores; } @@ -81,6 +94,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ ) ); //phpcs:enable + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); return $current_scores; } From bba6bd807a41c9a9792b13dfb887cdc3d1f78c3c Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 25 Nov 2024 10:07:18 +0200 Subject: [PATCH 078/132] Make filtering taxonomy filter internal --- .../infrastructure/taxonomies/taxonomies-collector.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php b/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php index a89f2b11f66..d6fdc4529b3 100644 --- a/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php +++ b/src/dashboard/infrastructure/taxonomies/taxonomies-collector.php @@ -39,6 +39,8 @@ public function get_custom_filtering_taxonomy( string $content_type ) { /** * Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type. * + * @internal + * * @param string $filtering_taxonomy The taxonomy that filters the content type. */ $filtering_taxonomy = \apply_filters( "wpseo_{$content_type}_filtering_taxonomy", '' ); From d5b1a13dc150de12ece94f269d5d29988b708961 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:07:21 +0100 Subject: [PATCH 079/132] Implement score endpoints and errors * refactored seo & readability scores to one component with an "AnalysisType" * refactored the useFetch and spread to some more files * by default parse the 408 status to a TimeoutError so it matches with the signal' timeout * created a wrapper fro the margin, saves adding it in each layer inside * fix our wrong assumption of term request using the name (string) and use the id (number) instead * use prop drilling to keep the API visible for this iteration --- .../js/src/dashboard/components/dashboard.js | 16 ++- packages/js/src/dashboard/fetch/fetch-json.js | 18 +++ .../src/dashboard/fetch/get-response-error.js | 14 ++ .../js/src/dashboard/fetch/timeout-error.js | 12 ++ .../dashboard/{hooks => fetch}/use-fetch.js | 19 +-- packages/js/src/dashboard/index.js | 6 + .../components/content-status-description.js | 2 +- .../scores/components/score-content.js | 6 +- .../src/dashboard/scores/components/scores.js | 136 ++++++++++++++++++ .../scores/components/term-filter.js | 24 ++-- .../scores/readability/readability-scores.js | 72 ---------- .../dashboard/scores/readability/scores.json | 30 ---- .../js/src/dashboard/scores/seo/scores.json | 30 ---- .../js/src/dashboard/scores/seo/seo-scores.js | 78 ---------- packages/js/src/general/initialize.js | 20 ++- 15 files changed, 235 insertions(+), 248 deletions(-) create mode 100644 packages/js/src/dashboard/fetch/fetch-json.js create mode 100644 packages/js/src/dashboard/fetch/get-response-error.js create mode 100644 packages/js/src/dashboard/fetch/timeout-error.js rename packages/js/src/dashboard/{hooks => fetch}/use-fetch.js (81%) create mode 100644 packages/js/src/dashboard/scores/components/scores.js delete mode 100644 packages/js/src/dashboard/scores/readability/readability-scores.js delete mode 100644 packages/js/src/dashboard/scores/readability/scores.json delete mode 100644 packages/js/src/dashboard/scores/seo/scores.json delete mode 100644 packages/js/src/dashboard/scores/seo/seo-scores.js diff --git a/packages/js/src/dashboard/components/dashboard.js b/packages/js/src/dashboard/components/dashboard.js index d0c4cfbf61c..387556946a3 100644 --- a/packages/js/src/dashboard/components/dashboard.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -1,25 +1,31 @@ -import { ReadabilityScores } from "../scores/readability/readability-scores"; -import { SeoScores } from "../scores/seo/seo-scores"; +import { Scores } from "../scores/components/scores"; import { PageTitle } from "./page-title"; /** * @type {import("../index").ContentType} ContentType * @type {import("../index").Features} Features + * @type {import("../index").Endpoints} Endpoints */ /** * @param {ContentType[]} contentTypes The content types. * @param {string} userName The user name. * @param {Features} features Whether features are enabled. + * @param {Endpoints} endpoints The endpoints. + * @param {Object} headers The headers for the score requests. * @returns {JSX.Element} The element. */ -export const Dashboard = ( { contentTypes, userName, features } ) => { +export const Dashboard = ( { contentTypes, userName, features, endpoints, headers } ) => { return ( <>
    - { features.indexables && features.seoAnalysis && } - { features.indexables && features.readabilityAnalysis && } + { features.indexables && features.seoAnalysis && ( + + ) } + { features.indexables && features.readabilityAnalysis && ( + + ) }
    ); diff --git a/packages/js/src/dashboard/fetch/fetch-json.js b/packages/js/src/dashboard/fetch/fetch-json.js new file mode 100644 index 00000000000..62f95c6a9fb --- /dev/null +++ b/packages/js/src/dashboard/fetch/fetch-json.js @@ -0,0 +1,18 @@ +import { getResponseError } from "./get-response-error"; + +/** + * @param {string|URL} url The URL to fetch from. + * @param {RequestInit} options The request options. + * @returns {Promise} The promise of a result, or an error. + */ +export const fetchJson = async( url, options ) => { + try { + const response = await fetch( url, options ); + if ( ! response.ok ) { + throw getResponseError( response ); + } + return response.json(); + } catch ( e ) { + return Promise.reject( e ); + } +}; diff --git a/packages/js/src/dashboard/fetch/get-response-error.js b/packages/js/src/dashboard/fetch/get-response-error.js new file mode 100644 index 00000000000..c526d2b5ffa --- /dev/null +++ b/packages/js/src/dashboard/fetch/get-response-error.js @@ -0,0 +1,14 @@ +import { TimeoutError } from "./timeout-error"; + +/** + * @param {Response} response The response. + * @returns {Error} The error that corresponds to the response. + */ +export const getResponseError = ( response ) => { + switch ( response.status ) { + case 408: + return new TimeoutError( "request timed out" ); + default: + return new Error( "not ok" ); + } +}; diff --git a/packages/js/src/dashboard/fetch/timeout-error.js b/packages/js/src/dashboard/fetch/timeout-error.js new file mode 100644 index 00000000000..4d6230e160e --- /dev/null +++ b/packages/js/src/dashboard/fetch/timeout-error.js @@ -0,0 +1,12 @@ +/** + * Represents a timeout error. + */ +export class TimeoutError extends Error { + /** + * @param {string} message The error message. + */ + constructor( message ) { + super( message ); + this.name = "TimeoutError"; + } +} diff --git a/packages/js/src/dashboard/hooks/use-fetch.js b/packages/js/src/dashboard/fetch/use-fetch.js similarity index 81% rename from packages/js/src/dashboard/hooks/use-fetch.js rename to packages/js/src/dashboard/fetch/use-fetch.js index 7b41afc6df7..8c78ae34ce1 100644 --- a/packages/js/src/dashboard/hooks/use-fetch.js +++ b/packages/js/src/dashboard/fetch/use-fetch.js @@ -1,6 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "@wordpress/element"; import { debounce, identity } from "lodash"; import { FETCH_DELAY } from "../../shared-admin/constants"; +import { fetchJson } from "./fetch-json"; /** * @typedef {Object} FetchResult @@ -16,24 +17,6 @@ import { FETCH_DELAY } from "../../shared-admin/constants"; * @returns {Promise} The promise of a result, or an error. */ -/** - * @param {string|URL} url The URL to fetch from. - * @param {RequestInit} options The request options. - * @returns {Promise} The promise of a result, or an error. - */ -const fetchJson = async( url, options ) => { - try { - const response = await fetch( url, options ); - if ( ! response.ok ) { - // From the perspective of the results, we want to reject this as an error. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } -}; - /** * @param {any[]} dependencies The dependencies for the fetch. * @param {string|URL} url The URL to fetch from. diff --git a/packages/js/src/dashboard/index.js b/packages/js/src/dashboard/index.js index ce58d85df75..4cff5bdbb74 100644 --- a/packages/js/src/dashboard/index.js +++ b/packages/js/src/dashboard/index.js @@ -43,3 +43,9 @@ export { Dashboard } from "./components/dashboard"; * @property {boolean} seoAnalysis Whether SEO analysis is enabled. * @property {boolean} readabilityAnalysis Whether readability analysis is enabled. */ + +/** + * @typedef {Object} Endpoints The endpoints. + * @property {string} seoScores The endpoint for SEO scores. + * @property {string} readabilityScores The endpoint for readability scores. + */ diff --git a/packages/js/src/dashboard/scores/components/content-status-description.js b/packages/js/src/dashboard/scores/components/content-status-description.js index fbd5e269234..8f0c5000a37 100644 --- a/packages/js/src/dashboard/scores/components/content-status-description.js +++ b/packages/js/src/dashboard/scores/components/content-status-description.js @@ -13,5 +13,5 @@ import { maxBy } from "lodash"; export const ContentStatusDescription = ( { scores, descriptions } ) => { const maxScore = maxBy( scores, "amount" ); - return

    { descriptions[ maxScore.name ] || "" }

    ; + return

    { descriptions[ maxScore?.name ] || "" }

    ; }; diff --git a/packages/js/src/dashboard/scores/components/score-content.js b/packages/js/src/dashboard/scores/components/score-content.js index 468433a1e42..956b79a3d5d 100644 --- a/packages/js/src/dashboard/scores/components/score-content.js +++ b/packages/js/src/dashboard/scores/components/score-content.js @@ -14,8 +14,8 @@ import { ScoreList } from "./score-list"; */ const ScoreContentSkeletonLoader = () => ( <> -   -
    +   +
      { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => (
    • { return ( <> -
      +
      { scores && } { scores && }
      diff --git a/packages/js/src/dashboard/scores/components/scores.js b/packages/js/src/dashboard/scores/components/scores.js new file mode 100644 index 00000000000..fb4d99157d6 --- /dev/null +++ b/packages/js/src/dashboard/scores/components/scores.js @@ -0,0 +1,136 @@ +import { createInterpolateElement, useEffect, useState } from "@wordpress/element"; +import { __, sprintf } from "@wordpress/i18n"; +import { Alert, Link, Paper, Title } from "@yoast/ui-library"; +import { useFetch } from "../../fetch/use-fetch"; +import { SCORE_DESCRIPTIONS } from "../score-meta"; +import { ContentTypeFilter } from "./content-type-filter"; +import { ScoreContent } from "./score-content"; +import { TermFilter } from "./term-filter"; + +/** + * @type {import("../index").ContentType} ContentType + * @type {import("../index").Taxonomy} Taxonomy + * @type {import("../index").Term} Term + * @type {import("../index").AnalysisType} AnalysisType + */ + +/** + * @param {string|URL} endpoint The endpoint. + * @param {ContentType} contentType The content type. + * @param {Term?} [term] The term. + * @returns {URL} The URL to get scores. + */ +const createScoresUrl = ( endpoint, contentType, term ) => { + const searchParams = new URLSearchParams( { contentType: contentType.name } ); + if ( contentType.taxonomy?.name && term?.name ) { + searchParams.set( "taxonomy", contentType.taxonomy.name ); + searchParams.set( "term", term.name ); + } + return new URL( "?" + searchParams, endpoint ); +}; + +// Added dummy space as content to prevent children prop warnings in the console. +const supportLink = ; + +const TimeoutErrorMessage = createInterpolateElement( + sprintf( + /* translators: %1$s and %2$s expand to an opening/closing tag for a link to the support page. */ + __( "A timeout occurred, possibly due to a large number of posts or terms. In case you need further help, please take a look at our %1$sSupport page%2$s.", "wordpress-seo" ), + "", + "" + ), + { + supportLink, + } +); +const OtherErrorMessage = createInterpolateElement( + sprintf( + /* translators: %1$s and %2$s expand to an opening/closing tag for a link to the support page. */ + __( "Something went wrong. In case you need further help, please take a look at our %1$sSupport page%2$s.", "wordpress-seo" ), + "", + "" + ), + { + supportLink, + } +); + +/** + * @param {Error?} [error] The error. + * @returns {JSX.Element} The element. + */ +const ErrorAlert = ( { error } ) => { + if ( ! error ) { + return null; + } + return ( + + { error?.name === "TimeoutError" + ? TimeoutErrorMessage + : OtherErrorMessage + } + + ); +}; + +/** + * @param {AnalysisType} analysisType The analysis type. Either "seo" or "readability". + * @param {ContentType[]} contentTypes The content types. May not be empty. + * @param {string} endpoint The endpoint or base URL. + * @param {Object} headers The headers to send with the request. + * @returns {JSX.Element} The element. + */ +export const Scores = ( { analysisType, contentTypes, endpoint, headers } ) => { + const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); + const [ selectedTerm, setSelectedTerm ] = useState(); + + const { data: scores, error, isPending } = useFetch( { + dependencies: [ selectedContentType.name, selectedContentType?.taxonomy, selectedTerm?.name ], + url: createScoresUrl( endpoint, selectedContentType, selectedTerm ), + options: { + headers: { + "Content-Type": "application/json", + ...headers, + }, + }, + fetchDelay: 0, + } ); + + useEffect( () => { + // Reset the selected term when the selected content type changes. + setSelectedTerm( undefined ); // eslint-disable-line no-undefined + }, [ selectedContentType.name ] ); + + return ( + + + { analysisType === "readability" + ? __( "Readability scores", "wordpress-seo" ) + : __( "SEO scores", "wordpress-seo" ) + } + +
      + + { selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search && + + } +
      +
      + + { ! error && ( + + ) } +
      +
      + ); +}; diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 5cffaa8ec60..5a6703378fc 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -1,7 +1,7 @@ import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Spinner } from "@yoast/ui-library"; -import { useFetch } from "../../hooks/use-fetch"; +import { useFetch } from "../../fetch/use-fetch"; /** * @type {import("../index").Taxonomy} Taxonomy @@ -9,20 +9,20 @@ import { useFetch } from "../../hooks/use-fetch"; */ /** - * @param {string|URL} baseUrl The URL to fetch from. + * @param {string|URL} endpoint The URL to fetch from. * @param {string} query The query. - * @returns {URL} The URL with the query. + * @returns {URL} The URL to query for the terms. */ -const createQueryUrl = ( baseUrl, query ) => new URL( "?" + new URLSearchParams( { +const createQueryUrl = ( endpoint, query ) => new URL( "?" + new URLSearchParams( { search: query, - _fields: [ "name", "slug" ], -} ), baseUrl ); + _fields: [ "id", "name" ], +} ), endpoint ); /** - * @param {{name: string, slug: string}} term The term from the response. - * @returns {Term} The transformed term. + * @param {{id: number, name: string}} term The term from the response. + * @returns {Term} The transformed term for internal usage. */ -const transformTerm = ( term ) => ( { name: term.slug, label: term.name } ); +const transformTerm = ( term ) => ( { name: String( term.id ), label: term.name } ); /** * Renders either a list of terms or a message that nothing was found. @@ -54,7 +54,11 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { const { data: terms = [], error, isPending } = useFetch( { dependencies: [ taxonomy.links.search, query ], url: createQueryUrl( taxonomy.links.search, query ), - options: { headers: { "Content-Type": "application/json" } }, + options: { + headers: { + "Content-Type": "application/json", + }, + }, prepareData: ( result ) => result.map( transformTerm ), } ); diff --git a/packages/js/src/dashboard/scores/readability/readability-scores.js b/packages/js/src/dashboard/scores/readability/readability-scores.js deleted file mode 100644 index b471cc7a97d..00000000000 --- a/packages/js/src/dashboard/scores/readability/readability-scores.js +++ /dev/null @@ -1,72 +0,0 @@ -import { useState } from "@wordpress/element"; -import { __ } from "@wordpress/i18n"; -import { Paper, Title } from "@yoast/ui-library"; -import { useFetch } from "../../hooks/use-fetch"; -import { ContentTypeFilter } from "../components/content-type-filter"; -import { ScoreContent } from "../components/score-content"; -import { TermFilter } from "../components/term-filter"; -import { SCORE_DESCRIPTIONS } from "../score-meta"; - -/** - * @type {import("../index").ContentType} ContentType - * @type {import("../index").Term} Term - */ - -/** - * @param {ContentType[]} contentTypes The content types. May not be empty. - * @returns {JSX.Element} The element. - */ -export const ReadabilityScores = ( { contentTypes } ) => { - const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); - const [ selectedTerm, setSelectedTerm ] = useState(); - - const { data: scores, isPending } = useFetch( { - dependencies: [ selectedContentType.name, selectedTerm?.name ], - url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/readability/scores.json", - // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, - options: { headers: { "Content-Type": "application/json" } }, - fetchDelay: 0, - doFetch: async( url, options ) => { - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - return [ "good", "ok", "bad", "notAnalyzed" ].map( ( name ) => ( { - name, - amount: Math.ceil( Math.random() * 10 ), - links: Math.random() > 0.5 ? {} : { view: `edit.php?readability_filter=${ name }` }, - } ) ); - // eslint-disable-next-line no-unreachable - try { - const response = await fetch( url, options ); - if ( ! response.ok ) { - // From the perspective of the results, we want to reject this as an error. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } - }, - } ); - - return ( - - { __( "Readability scores", "wordpress-seo" ) } -
      - - { selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search && - - } -
      - -
      - ); -}; diff --git a/packages/js/src/dashboard/scores/readability/scores.json b/packages/js/src/dashboard/scores/readability/scores.json deleted file mode 100644 index ab6b9692e1d..00000000000 --- a/packages/js/src/dashboard/scores/readability/scores.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "name": "good", - "amount": 8, - "links": { - "view": "https://basic.wordpress.test/wp-admin/edit.php?category=22" - } - }, - { - "name": "ok", - "amount": 9, - "links": { - "view": null - } - }, - { - "name": "bad", - "amount": 10, - "links": { - "view": null - } - }, - { - "name": "notAnalyzed", - "amount": 11, - "links": { - "view": null - } - } -] diff --git a/packages/js/src/dashboard/scores/seo/scores.json b/packages/js/src/dashboard/scores/seo/scores.json deleted file mode 100644 index 14fb01d8b03..00000000000 --- a/packages/js/src/dashboard/scores/seo/scores.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "name": "good", - "amount": 5, - "links": { - "view": null - } - }, - { - "name": "ok", - "amount": 4, - "links": { - "view": "https://basic.wordpress.test/wp-admin/edit.php?category=22" - } - }, - { - "name": "bad", - "amount": 6, - "links": { - "view": null - } - }, - { - "name": "notAnalyzed", - "amount": 7, - "links": { - "view": null - } - } -] diff --git a/packages/js/src/dashboard/scores/seo/seo-scores.js b/packages/js/src/dashboard/scores/seo/seo-scores.js deleted file mode 100644 index 7de639c7b7b..00000000000 --- a/packages/js/src/dashboard/scores/seo/seo-scores.js +++ /dev/null @@ -1,78 +0,0 @@ -import { useEffect, useState } from "@wordpress/element"; -import { __ } from "@wordpress/i18n"; -import { Paper, Title } from "@yoast/ui-library"; -import { useFetch } from "../../hooks/use-fetch"; -import { ContentTypeFilter } from "../components/content-type-filter"; -import { ScoreContent } from "../components/score-content"; -import { TermFilter } from "../components/term-filter"; -import { SCORE_DESCRIPTIONS } from "../score-meta"; - -/** - * @type {import("../index").ContentType} ContentType - * @type {import("../index").Taxonomy} Taxonomy - * @type {import("../index").Term} Term - */ - -/** - * @param {ContentType[]} contentTypes The content types. May not be empty. - * @returns {JSX.Element} The element. - */ -export const SeoScores = ( { contentTypes } ) => { - const [ selectedContentType, setSelectedContentType ] = useState( contentTypes[ 0 ] ); - const [ selectedTerm, setSelectedTerm ] = useState(); - - const { data: scores, isPending } = useFetch( { - dependencies: [ selectedContentType.name, selectedTerm?.name ], - url: "/wp-content/plugins/wordpress-seo/packages/js/src/dashboard/scores/seo/scores.json", - // url: `/wp-json/yoast/v1/scores/${ contentType.name }/${ term?.name }`, - options: { headers: { "Content-Type": "application/json" } }, - fetchDelay: 0, - doFetch: async( url, options ) => { - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - return [ "good", "ok", "bad", "notAnalyzed" ].map( ( name ) => ( { - name, - amount: Math.ceil( Math.random() * 10 ), - links: Math.random() > 0.5 ? {} : { view: `edit.php?seo_filter=${ name }` }, - } ) ); - // eslint-disable-next-line no-unreachable - try { - const response = await fetch( url, options ); - if ( ! response.ok ) { - // From the perspective of the results, we want to reject this as an error. - throw new Error( "Not ok" ); - } - return response.json(); - } catch ( error ) { - return Promise.reject( error ); - } - }, - } ); - - useEffect( () => { - // Reset the selected term when the selected content type changes. - setSelectedTerm( undefined ); // eslint-disable-line no-undefined - }, [ selectedContentType.name ] ); - - return ( - - { __( "SEO scores", "wordpress-seo" ) } -
      - - { selectedContentType.taxonomy && selectedContentType.taxonomy?.links?.search && - - } -
      - -
      - ); -}; diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index 9bba09cded4..e464531591f 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -21,6 +21,7 @@ import { FTC_NAME } from "./store/first-time-configuration"; * @type {import("../index").ContentType} ContentType * @type {import("../index").Features} Features * @type {import("../index").Links} Links + * @type {import("../index").Endpoints} Endpoints */ domReady( () => { @@ -50,6 +51,17 @@ domReady( () => { seoAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.keyphraseAnalysis", false ), readabilityAnalysis: get( window, "wpseoScriptData.dashboard.enabledAnalysisFeatures.readabilityAnalysis", false ), }; + + /** @type {Endpoints} */ + const endpoints = { + seoScores: get( window, "wpseoScriptData.dashboard.endpoints.seoScores", "" ), + readabilityScores: get( window, "wpseoScriptData.dashboard.endpoints.readabilityScores", "" ), + }; + /** @type {Object} */ + const headers = { + "X-Wp-Nonce": get( window, "wpseoScriptData.dashboard.nonce", "" ), + }; + const router = createHashRouter( createRoutesFromElements( } errorElement={ }> @@ -57,7 +69,13 @@ domReady( () => { path={ ROUTES.dashboard } element={ - + } From c5f6c46ec12e7dce93e4abb28b925912862f27d8 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:09:16 +0100 Subject: [PATCH 080/132] Supply frontend with endpoints and nonce * the UI needs to fetch the scores for SEO and readability * those routes are protected by a nonce we need to provide * introduce endpoint interface -- apply that to the seo scores and readability scores * refactor the routes slightly to add the namespace * lazier approach with the nonce # Conflicts: # src/dashboard/user-interface/scores/abstract-scores-route.php # src/dashboard/user-interface/scores/readability-scores-route.php # src/dashboard/user-interface/scores/seo-scores-route.php --- .../configuration/dashboard-configuration.php | 26 +++++++++- .../endpoints/endpoints-repository.php | 42 ++++++++++++++++ .../domain/endpoint/endpoint-interface.php | 34 +++++++++++++ .../domain/endpoint/endpoint-list.php | 41 ++++++++++++++++ .../endpoints/readability-scores-endpoint.php | 48 +++++++++++++++++++ .../endpoints/seo-scores-endpoint.php | 48 +++++++++++++++++++ .../nonces/nonce-repository.php | 18 +++++++ .../scores/abstract-scores-route.php | 9 +++- 8 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/dashboard/application/endpoints/endpoints-repository.php create mode 100644 src/dashboard/domain/endpoint/endpoint-interface.php create mode 100644 src/dashboard/domain/endpoint/endpoint-list.php create mode 100644 src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php create mode 100644 src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php create mode 100644 src/dashboard/infrastructure/nonces/nonce-repository.php diff --git a/src/dashboard/application/configuration/dashboard-configuration.php b/src/dashboard/application/configuration/dashboard-configuration.php index c9eb9fef674..eae738da4a0 100644 --- a/src/dashboard/application/configuration/dashboard-configuration.php +++ b/src/dashboard/application/configuration/dashboard-configuration.php @@ -5,6 +5,8 @@ namespace Yoast\WP\SEO\Dashboard\Application\Configuration; use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository; +use Yoast\WP\SEO\Dashboard\Application\Endpoints\Endpoints_Repository; +use Yoast\WP\SEO\Dashboard\Infrastructure\Nonces\Nonce_Repository; use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository; use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis; use Yoast\WP\SEO\Editors\Framework\Readability_Analysis; @@ -44,6 +46,20 @@ class Dashboard_Configuration { */ private $enabled_analysis_features_repository; + /** + * The endpoints repository. + * + * @var Endpoints_Repository + */ + private $endpoints_repository; + + /** + * The nonce repository. + * + * @var Nonce_Repository + */ + private $nonce_repository; + /** * The constructor. * @@ -53,17 +69,23 @@ class Dashboard_Configuration { * @param User_Helper $user_helper The user helper. * @param Enabled_Analysis_Features_Repository $enabled_analysis_features_repository The analysis feature * repository. + * @param Endpoints_Repository $endpoints_repository The endpoints repository. + * @param Nonce_Repository $nonce_repository The nonce repository. */ public function __construct( Content_Types_Repository $content_types_repository, Indexable_Helper $indexable_helper, User_Helper $user_helper, - Enabled_Analysis_Features_Repository $enabled_analysis_features_repository + Enabled_Analysis_Features_Repository $enabled_analysis_features_repository, + Endpoints_Repository $endpoints_repository, + Nonce_Repository $nonce_repository ) { $this->content_types_repository = $content_types_repository; $this->indexable_helper = $indexable_helper; $this->user_helper = $user_helper; $this->enabled_analysis_features_repository = $enabled_analysis_features_repository; + $this->endpoints_repository = $endpoints_repository; + $this->nonce_repository = $nonce_repository; } /** @@ -82,6 +104,8 @@ public function get_configuration(): array { Keyphrase_Analysis::NAME, ] )->to_array(), + 'endpoints' => $this->endpoints_repository->get_all_endpoints()->to_array(), + 'nonce' => $this->nonce_repository->get_rest_nonce(), ]; } } diff --git a/src/dashboard/application/endpoints/endpoints-repository.php b/src/dashboard/application/endpoints/endpoints-repository.php new file mode 100644 index 00000000000..d293197a559 --- /dev/null +++ b/src/dashboard/application/endpoints/endpoints-repository.php @@ -0,0 +1,42 @@ + + */ + private $endpoints; + + /** + * Constructs the repository. + * + * @param Endpoint_Interface ...$endpoints The endpoints to add to the repository. + */ + public function __construct( Endpoint_Interface ...$endpoints ) { + $this->endpoints = $endpoints; + } + + /** + * Creates a list with all endpoints. + * + * @return Endpoint_List The list with all endpoints. + */ + public function get_all_endpoints(): Endpoint_List { + $list = new Endpoint_List(); + foreach ( $this->endpoints as $endpoint ) { + $list->add_endpoint( $endpoint ); + } + + return $list; + } +} diff --git a/src/dashboard/domain/endpoint/endpoint-interface.php b/src/dashboard/domain/endpoint/endpoint-interface.php new file mode 100644 index 00000000000..ae99c2667e9 --- /dev/null +++ b/src/dashboard/domain/endpoint/endpoint-interface.php @@ -0,0 +1,34 @@ + $endpoints + */ + private $endpoints = []; + + /** + * Adds an endpoint to the list. + * + * @param Endpoint_Interface $endpoint An endpoint. + * + * @return void + */ + public function add_endpoint( Endpoint_Interface $endpoint ): void { + $this->endpoints[] = $endpoint; + } + + /** + * Converts the list to an array. + * + * @return array The array of endpoints. + */ + public function to_array(): array { + $result = []; + foreach ( $this->endpoints as $endpoint ) { + $result[ $endpoint->get_name() ] = $endpoint->get_url(); + } + + return $result; + } +} diff --git a/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php b/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php new file mode 100644 index 00000000000..387162ea3d0 --- /dev/null +++ b/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php @@ -0,0 +1,48 @@ +get_namespace() . $this->get_route() ); + } +} diff --git a/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php b/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php new file mode 100644 index 00000000000..ae129ab562b --- /dev/null +++ b/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php @@ -0,0 +1,48 @@ +get_namespace() . $this->get_route() ); + } +} diff --git a/src/dashboard/infrastructure/nonces/nonce-repository.php b/src/dashboard/infrastructure/nonces/nonce-repository.php new file mode 100644 index 00000000000..162fe086e3a --- /dev/null +++ b/src/dashboard/infrastructure/nonces/nonce-repository.php @@ -0,0 +1,18 @@ +get_route_prefix(), [ [ From a82bdb1d9369e2904d058fc8d192d2b78f3e8eb0 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:52:00 +0100 Subject: [PATCH 081/132] UX: refactor score container * refactor so the skeleton loaders are next to the components themselves -- easier for sharing of classnames * optimize for default English case: hardcode chart size to match the seemingly default height -- change the grid to flex * fix score disc collapsing in width * fix minimum gap between the badge and the link --- .../scores/components/score-chart.js | 19 +++++++-- .../scores/components/score-content.js | 42 ++++++++----------- .../dashboard/scores/components/score-list.js | 42 ++++++++++++++++--- 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-chart.js b/packages/js/src/dashboard/scores/components/score-chart.js index fd2874993bb..96e49d6e1c6 100644 --- a/packages/js/src/dashboard/scores/components/score-chart.js +++ b/packages/js/src/dashboard/scores/components/score-chart.js @@ -1,4 +1,6 @@ +import { SkeletonLoader } from "@yoast/ui-library"; import { ArcElement, Chart, Tooltip } from "chart.js"; +import classNames from "classnames"; import { Doughnut } from "react-chartjs-2"; import { SCORE_META } from "../score-meta"; @@ -50,13 +52,24 @@ const chartOptions = { }; /** - * + * @param {string} [className] The class name. + * @returns {JSX.Element} The element. + */ +export const ScoreChartSkeletonLoader = ( { className } ) => ( +
      + +
      +
      +); + +/** + * @param {string} [className] The class name. * @param {Score[]} scores The scores. * @returns {JSX.Element} The element. */ -export const ScoreChart = ( { scores } ) => { +export const ScoreChart = ( { className, scores } ) => { return ( -
      +
      ( <>   -
      -
        - { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => ( -
      • - - { label } - 1 - View -
      • - ) ) } -
      -
      - -
      -
      +
      + +
      ); @@ -51,9 +45,9 @@ export const ScoreContent = ( { scores = [], isLoading, descriptions } ) => { return ( <> -
      - { scores && } - { scores && } +
      + { scores && } + { scores && }
      ); diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index ab4a4d29f36..12ba429ceae 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,4 +1,5 @@ -import { Badge, Button } from "@yoast/ui-library"; +import { Badge, Button, SkeletonLoader } from "@yoast/ui-library"; +import classNames from "classnames"; import { SCORE_META } from "../score-meta"; /** @@ -6,19 +7,48 @@ import { SCORE_META } from "../score-meta"; */ /** + * @type {{listItem: string, score: string}} + */ +const CLASSNAMES = { + listItem: "yst-flex yst-items-center yst-py-3 first:yst-pt-0 last:yst-pb-0 yst-border-b last:yst-border-b-0", + score: "yst-shrink-0 yst-w-3 yst-aspect-square yst-rounded-full", +}; + +/** + * @param {string} [className] The class name for the UL. + * @returns {JSX.Element} The element. + */ +export const ScoreListSkeletonLoader = ( { className } ) => ( +
        + { Object.entries( SCORE_META ).map( ( [ name, { label } ] ) => ( +
      • + + { label } + 1 + View +
      • + ) ) } +
      +); + +/** + * @param {string} [className] The class name for the UL. * @param {Score[]} scores The scores. * @returns {JSX.Element} The element. */ -export const ScoreList = ( { scores } ) => ( -
        +export const ScoreList = ( { className, scores } ) => ( +
          { scores.map( ( score ) => (
        • - + { SCORE_META[ score.name ].label } - { score.amount } + { score.amount } { score.links.view && ( ) } From 8eb046dd46d929e36db36f82d99241d87278d236 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:58:47 +0100 Subject: [PATCH 082/132] UX: fix score labels * using UI lib labels for styling --- packages/js/src/dashboard/scores/components/score-list.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index 12ba429ceae..02edf077354 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,4 +1,4 @@ -import { Badge, Button, SkeletonLoader } from "@yoast/ui-library"; +import { Badge, Button, Label, SkeletonLoader } from "@yoast/ui-library"; import classNames from "classnames"; import { SCORE_META } from "../score-meta"; @@ -12,6 +12,7 @@ import { SCORE_META } from "../score-meta"; const CLASSNAMES = { listItem: "yst-flex yst-items-center yst-py-3 first:yst-pt-0 last:yst-pb-0 yst-border-b last:yst-border-b-0", score: "yst-shrink-0 yst-w-3 yst-aspect-square yst-rounded-full", + label: "yst-ml-3 yst-mr-2", }; /** @@ -26,7 +27,7 @@ export const ScoreListSkeletonLoader = ( { className } ) => ( className={ CLASSNAMES.listItem } > - { label } + { label } 1 View
        • @@ -47,7 +48,7 @@ export const ScoreList = ( { className, scores } ) => ( className={ CLASSNAMES.listItem } > - { SCORE_META[ score.name ].label } + { score.amount } { score.links.view && ( From 6132ebb8dad852d4edfcf286be9105ea71f4bfd4 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:14:07 +0100 Subject: [PATCH 083/132] UX: add more spacing between menu items * removing the wrapper div - does not seem to be needed --- packages/js/src/general/app.js | 60 ++++++++++++++++------------------ 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/packages/js/src/general/app.js b/packages/js/src/general/app.js index ce178815884..387ec046419 100644 --- a/packages/js/src/general/app.js +++ b/packages/js/src/general/app.js @@ -37,37 +37,35 @@ const Menu = ( { idSuffix = "" } ) => { -
          -
            - - - { __( "Dashboard", "wordpress-seo" ) } - } - idSuffix={ idSuffix } - className="yst-gap-3" - /> - - - { __( "Alert center", "wordpress-seo" ) } - } - idSuffix={ idSuffix } - className="yst-gap-3" - /> - - - { __( "First-time configuration", "wordpress-seo" ) } - } - idSuffix={ idSuffix } - className="yst-gap-3" - /> -
          -
          +
            + + + { __( "Dashboard", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> + + + { __( "Alert center", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> + + + { __( "First-time configuration", "wordpress-seo" ) } + } + idSuffix={ idSuffix } + className="yst-gap-3" + /> +
          ; }; Menu.propTypes = { From 51428cf2b3c3add73dad3a90adf6ececac6f4f6a Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:13:02 +0100 Subject: [PATCH 084/132] UX: improve chart distances * fixes an inconsistent gap -- by removing the border (we had it the same as the background anyway) * fixes the hovered piece from being cut off -- by adding internal padding the same as the (hover) offset --- .../scores/components/score-chart.js | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-chart.js b/packages/js/src/dashboard/scores/components/score-chart.js index 96e49d6e1c6..c786f84b8ae 100644 --- a/packages/js/src/dashboard/scores/components/score-chart.js +++ b/packages/js/src/dashboard/scores/components/score-chart.js @@ -14,29 +14,24 @@ Chart.register( ArcElement, Tooltip ); * @param {Score[]} scores The scores. * @returns {Object} Parsed chart data. */ -const transformScoresToGraphData = ( scores ) => { - const hexes = scores.map( ( { name } ) => SCORE_META[ name ].hex ); - - return { - labels: scores.map( ( { name } ) => SCORE_META[ name ].label ), - datasets: [ - { - cutout: "82%", - data: scores.map( ( { amount } ) => amount ), - backgroundColor: hexes, - borderColor: hexes, - borderWidth: 1, - offset: 1, - hoverOffset: 5, - spacing: 1, - weight: 1, - animation: { - animateRotate: true, - }, +const transformScoresToGraphData = ( scores ) => ( { + labels: scores.map( ( { name } ) => SCORE_META[ name ].label ), + datasets: [ + { + cutout: "82%", + data: scores.map( ( { amount } ) => amount ), + backgroundColor: scores.map( ( { name } ) => SCORE_META[ name ].hex ), + borderWidth: 0, + offset: 0, + hoverOffset: 5, + spacing: 1, + weight: 1, + animation: { + animateRotate: true, }, - ], - }; -}; + }, + ], +} ); const chartOptions = { plugins: { @@ -49,6 +44,9 @@ const chartOptions = { }, }, }, + layout: { + padding: 5, + }, }; /** From 7da1cda38e15824bb81d174961ee47c1300fbdf1 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 25 Nov 2024 15:25:24 +0200 Subject: [PATCH 085/132] Add metadata in the results and refactor score classes accordingly --- .../abstract-score-results-repository.php | 83 +++++++++++++++++++ .../readability-score-results-repository.php | 28 +++++++ .../seo-score-results-repository.php | 28 +++++++ .../scores/abstract-scores-repository.php | 77 ----------------- .../readability-scores-repository.php | 28 ------- .../scores/scores-repository-interface.php | 24 ------ .../seo-scores/seo-scores-repository.php | 28 ------- .../domain/score-results/current-score.php | 79 ++++++++++++++++++ .../score-results/current-scores-list.php | 45 ++++++++++ .../domain/score-results/score-result.php | 56 +++++++++++++ .../domain/scores/abstract-score.php | 47 ----------- .../domain/scores/scores-interface.php | 32 ------- src/dashboard/domain/scores/scores-list.php | 47 ----------- .../readability-score-results-collector.php} | 30 +++++-- .../score-results-collector-interface.php} | 6 +- .../seo-score-results-collector.php} | 30 +++++-- .../scores/abstract-scores-route.php | 8 +- .../scores/readability-scores-route.php | 8 +- .../scores/seo-scores-route.php | 8 +- 19 files changed, 380 insertions(+), 312 deletions(-) create mode 100644 src/dashboard/application/score-results/abstract-score-results-repository.php create mode 100644 src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php create mode 100644 src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php delete mode 100644 src/dashboard/application/scores/abstract-scores-repository.php delete mode 100644 src/dashboard/application/scores/readability-scores/readability-scores-repository.php delete mode 100644 src/dashboard/application/scores/scores-repository-interface.php delete mode 100644 src/dashboard/application/scores/seo-scores/seo-scores-repository.php create mode 100644 src/dashboard/domain/score-results/current-score.php create mode 100644 src/dashboard/domain/score-results/current-scores-list.php create mode 100644 src/dashboard/domain/score-results/score-result.php delete mode 100644 src/dashboard/domain/scores/scores-list.php rename src/dashboard/infrastructure/{scores/readability-scores/readability-scores-collector.php => score-results/readability-score-results/readability-score-results-collector.php} (83%) rename src/dashboard/infrastructure/{scores/scores-collector-interface.php => score-results/score-results-collector-interface.php} (80%) rename src/dashboard/infrastructure/{scores/seo-scores/seo-scores-collector.php => score-results/seo-score-results/seo-score-results-collector.php} (83%) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php new file mode 100644 index 00000000000..e8c49f79006 --- /dev/null +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -0,0 +1,83 @@ +score_link_collector = $score_link_collector; + } + + /** + * Returns the score results for a content type. + * + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. + * + * @return array>> The scores. + */ + public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + $current_scores_list = new Current_Scores_List(); + $current_scores = $this->score_results_collector->get_current_scores( $this->scores, $content_type, $term_id ); + + foreach ( $this->scores as $score ) { + $score_name = $score->get_name(); + $current_score_links = [ + 'view' => $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ), + ]; + + $current_score = new Current_Score( $score_name, (int) $current_scores->scores->$score_name, $current_score_links ); + $current_scores_list->add( $current_score ); + } + + $score_result = new Score_Result( $current_scores_list, $current_scores->query_time, $current_scores->cache_used ); + + return $score_result->to_array(); + } +} diff --git a/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php b/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php new file mode 100644 index 00000000000..197a37c5d2a --- /dev/null +++ b/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php @@ -0,0 +1,28 @@ +score_results_collector = $readability_score_results_collector; + $this->scores = $readability_scores; + } +} diff --git a/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php b/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php new file mode 100644 index 00000000000..7774e46d050 --- /dev/null +++ b/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php @@ -0,0 +1,28 @@ +score_results_collector = $seo_score_results_collector; + $this->scores = $seo_scores; + } +} diff --git a/src/dashboard/application/scores/abstract-scores-repository.php b/src/dashboard/application/scores/abstract-scores-repository.php deleted file mode 100644 index fb46e9d80d6..00000000000 --- a/src/dashboard/application/scores/abstract-scores-repository.php +++ /dev/null @@ -1,77 +0,0 @@ -score_link_collector = $score_link_collector; - } - - /** - * Returns the scores of a content type. - * - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. - * @param int|null $term_id The ID of the term we're filtering for. - * - * @return array>> The scores. - */ - public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { - $scores_list = new Scores_List(); - $current_scores = $this->scores_collector->get_current_scores( $this->scores, $content_type, $term_id ); - - foreach ( $this->scores as $score ) { - $score_name = $score->get_name(); - $score->set_amount( (int) $current_scores->$score_name ); - $score->set_view_link( $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ) ); - - $scores_list->add( $score ); - } - - return $scores_list->to_array(); - } -} diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php deleted file mode 100644 index cbd51810975..00000000000 --- a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php +++ /dev/null @@ -1,28 +0,0 @@ -scores_collector = $readability_scores_collector; - $this->scores = $readability_scores; - } -} diff --git a/src/dashboard/application/scores/scores-repository-interface.php b/src/dashboard/application/scores/scores-repository-interface.php deleted file mode 100644 index 3dd84acab7c..00000000000 --- a/src/dashboard/application/scores/scores-repository-interface.php +++ /dev/null @@ -1,24 +0,0 @@ ->> The scores. - */ - public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array; -} diff --git a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php deleted file mode 100644 index 7fee6024d7c..00000000000 --- a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php +++ /dev/null @@ -1,28 +0,0 @@ -scores_collector = $seo_scores_collector; - $this->scores = $seo_scores; - } -} diff --git a/src/dashboard/domain/score-results/current-score.php b/src/dashboard/domain/score-results/current-score.php new file mode 100644 index 00000000000..47a207805fc --- /dev/null +++ b/src/dashboard/domain/score-results/current-score.php @@ -0,0 +1,79 @@ + + */ + private $links; + + /** + * The constructor. + * + * @param string $name The name of the current score. + * @param int $amount The amount of the current score. + * @param array $links The links of the current score. + */ + public function __construct( string $name, int $amount, ?array $links = null ) { + $this->name = $name; + $this->amount = $amount; + $this->links = $links; + } + + /** + * Gets name of the current score. + * + * @return string The name of the current score. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Gets the amount of the current score. + * + * @return string The amount of the current score. + */ + public function get_amount(): int { + return $this->amount; + } + + /** + * Gets the links of the current score in the expected key value representation. + * + * @return array The links of the current score in the expected key value representation. + */ + public function get_links_to_array(): ?array { + $links = []; + + if ( $this->links === null ) { + return $links; + } + + foreach ( $this->links as $key => $link ) { + $links[ $key ] = $link; + } + return $links; + } +} diff --git a/src/dashboard/domain/score-results/current-scores-list.php b/src/dashboard/domain/score-results/current-scores-list.php new file mode 100644 index 00000000000..46b4d64c0d2 --- /dev/null +++ b/src/dashboard/domain/score-results/current-scores-list.php @@ -0,0 +1,45 @@ +current_scores[] = $current_score; + } + + /** + * Parses the score list to the expected key value representation. + * + * @return array>> The score list presented as the expected key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->current_scores as $current_score ) { + $array[] = [ + 'name' => $current_score->get_name(), + 'amount' => $current_score->get_amount(), + 'links' => $current_score->get_links_to_array(), + ]; + } + + return $array; + } +} diff --git a/src/dashboard/domain/score-results/score-result.php b/src/dashboard/domain/score-results/score-result.php new file mode 100644 index 00000000000..d6f97e8dd36 --- /dev/null +++ b/src/dashboard/domain/score-results/score-result.php @@ -0,0 +1,56 @@ +current_scores_list = $current_scores_list; + $this->query_time = $query_time; + $this->is_cached_used = $is_cached_used; + } + + /** + * Return this object represented by a key value array. + * + * @return array>>|float|bool> Returns the name and if the feature is enabled. + */ + public function to_array(): array { + return [ + 'scores' => $this->current_scores_list->to_array(), + 'queryTime' => $this->query_time, + 'cacheUsed' => $this->is_cached_used, + ]; + } +} diff --git a/src/dashboard/domain/scores/abstract-score.php b/src/dashboard/domain/scores/abstract-score.php index f30f13ac885..3ff07f5b03a 100644 --- a/src/dashboard/domain/scores/abstract-score.php +++ b/src/dashboard/domain/scores/abstract-score.php @@ -42,13 +42,6 @@ abstract class Abstract_Score implements Scores_Interface { */ private $max_score; - /** - * The amount of the score. - * - * @var int - */ - private $amount; - /** * The view link of the score. * @@ -62,44 +55,4 @@ abstract class Abstract_Score implements Scores_Interface { * @var int */ private $position; - - /** - * Gets the amount of the score. - * - * @return int The amount of the score. - */ - public function get_amount(): int { - return $this->amount; - } - - /** - * Sets the amount of the score. - * - * @param int $amount The amount of the score. - * - * @return void - */ - public function set_amount( int $amount ): void { - $this->amount = $amount; - } - - /** - * Gets the view link of the score. - * - * @return string|null The view link of the score. - */ - public function get_view_link(): ?string { - return $this->view_link; - } - - /** - * Sets the view link of the score. - * - * @param string $view_link The view link of the score. - * - * @return void - */ - public function set_view_link( ?string $view_link ): void { - $this->view_link = $view_link; - } } diff --git a/src/dashboard/domain/scores/scores-interface.php b/src/dashboard/domain/scores/scores-interface.php index a228b6595a9..c3b9e4879ee 100644 --- a/src/dashboard/domain/scores/scores-interface.php +++ b/src/dashboard/domain/scores/scores-interface.php @@ -48,36 +48,4 @@ public function get_max_score(): ?int; * @return int */ public function get_position(): int; - - /** - * Gets the amount of the score. - * - * @return int - */ - public function get_amount(): int; - - /** - * Sets the amount of the score. - * - * @param int $amount The amount of the score. - * - * @return void - */ - public function set_amount( int $amount ): void; - - /** - * Gets the view link of the score. - * - * @return string|null - */ - public function get_view_link(): ?string; - - /** - * Sets the view link of the score. - * - * @param string $view_link The view link of the score. - * - * @return void - */ - public function set_view_link( ?string $view_link ): void; } diff --git a/src/dashboard/domain/scores/scores-list.php b/src/dashboard/domain/scores/scores-list.php deleted file mode 100644 index dd0d2d10641..00000000000 --- a/src/dashboard/domain/scores/scores-list.php +++ /dev/null @@ -1,47 +0,0 @@ -scores[] = $score; - } - - /** - * Parses the score list to the expected key value representation. - * - * @return array>> The score list presented as the expected key value representation. - */ - public function to_array(): array { - $array = []; - foreach ( $this->scores as $score ) { - $array[ $score->get_position() ] = [ - 'name' => $score->get_name(), - 'amount' => $score->get_amount(), - 'links' => ( $score->get_view_link() === null ) ? [] : [ 'view' => $score->get_view_link() ], - ]; - } - - \ksort( $array ); - - return $array; - } -} diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php similarity index 83% rename from src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php rename to src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index 582a8b2be2d..4712d94592f 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -1,18 +1,19 @@ get_name(); $transient_name = self::READABILITY_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $transient = \get_transient( $transient_name ); if ( $transient !== false ) { - return \json_decode( $transient, false ); + $results->scores = \json_decode( $transient, false ); + $results->cache_used = true; + $results->query_time = 0; + + return $results; } $select = $this->build_select( $readability_scores ); @@ -51,6 +57,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -62,9 +69,13 @@ public function get_current_scores( array $readability_scores, Content_Type $con $replacements ) ); + $end_time = microtime(true); //phpcs:enable \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - return $current_scores; + $results->scores = $current_scores; + $results->cache_used = false; + $results->query_time = $end_time - $start_time; + return $results; } @@ -75,6 +86,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -91,9 +103,13 @@ public function get_current_scores( array $readability_scores, Content_Type $con $replacements ) ); + $end_time = microtime(true); //phpcs:enable \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - return $current_scores; + $results->scores = $current_scores; + $results->cache_used = false; + $results->query_time = $end_time - $start_time; + return $results; } /** diff --git a/src/dashboard/infrastructure/scores/scores-collector-interface.php b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php similarity index 80% rename from src/dashboard/infrastructure/scores/scores-collector-interface.php rename to src/dashboard/infrastructure/score-results/score-results-collector-interface.php index 06d398e84a7..9cf76c9bcb1 100644 --- a/src/dashboard/infrastructure/scores/scores-collector-interface.php +++ b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php @@ -1,7 +1,7 @@ get_name(); $transient_name = self::SEO_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $transient = \get_transient( $transient_name ); if ( $transient !== false ) { - return \json_decode( $transient, false ); + $results->scores = \json_decode( $transient, false ); + $results->cache_used = true; + $results->query_time = 0; + + return $results; } $select = $this->build_select( $seo_scores ); @@ -51,6 +57,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -63,9 +70,13 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ $replacements ) ); + $end_time = microtime(true); //phpcs:enable \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - return $current_scores; + $results->scores = $current_scores; + $results->cache_used = false; + $results->query_time = $end_time - $start_time; + return $results; } @@ -76,6 +87,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -93,9 +105,13 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ $replacements ) ); + $end_time = microtime(true); //phpcs:enable \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - return $current_scores; + $results->scores = $current_scores; + $results->cache_used = false; + $results->query_time = $end_time - $start_time; + return $results; } /** diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index 72f69930f21..687ab22b142 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -7,7 +7,7 @@ use WP_REST_Response; use WPSEO_Capability_Utils; use Yoast\WP\SEO\Conditionals\No_Conditionals; -use Yoast\WP\SEO\Dashboard\Application\Scores\Scores_Repository_Interface; +use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; @@ -54,9 +54,9 @@ abstract class Abstract_Scores_Route implements Route_Interface { /** * The scores repository. * - * @var Scores_Repository_Interface + * @var Abstract_Score_Results_Repository */ - protected $scores_repository; + protected $score_results_repository; /** * Sets the collectors. @@ -171,7 +171,7 @@ public function get_scores( WP_REST_Request $request ) { } return new WP_REST_Response( - $this->scores_repository->get_scores( $content_type, $taxonomy, $term_id ), + $this->score_results_repository->get_score_results( $content_type, $taxonomy, $term_id ), 200 ); } diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php index 680d03b265e..7fb174500d8 100644 --- a/src/dashboard/user-interface/scores/readability-scores-route.php +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -2,7 +2,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use Yoast\WP\SEO\Dashboard\Application\Scores\Readability_Scores\Readability_Scores_Repository; +use Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results\Readability_Score_Results_Repository; /** * Registers a route to get readability scores. @@ -19,11 +19,11 @@ class Readability_Scores_Route extends Abstract_Scores_Route { /** * Constructs the class. * - * @param Readability_Scores_Repository $readability_scores_repository The readability scores repository. + * @param Readability_Score_Results_Repository $readability_score_results_repository The readability score results repository. */ public function __construct( - Readability_Scores_Repository $readability_scores_repository + Readability_Score_Results_Repository $readability_score_results_repository ) { - $this->scores_repository = $readability_scores_repository; + $this->score_results_repository = $readability_score_results_repository; } } diff --git a/src/dashboard/user-interface/scores/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php index 8fd7c79647d..75d41e0cbac 100644 --- a/src/dashboard/user-interface/scores/seo-scores-route.php +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -2,7 +2,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores; -use Yoast\WP\SEO\Dashboard\Application\Scores\SEO_Scores\SEO_Scores_Repository; +use Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results\SEO_Score_Results_Repository; /** * Registers a route to get SEO scores. @@ -19,11 +19,11 @@ class SEO_Scores_Route extends Abstract_Scores_Route { /** * Constructs the class. * - * @param SEO_Scores_Repository $seo_scores_repository The SEO scores repository. + * @param SEO_Score_Results_Repository $seo_score_results_repository The SEO score results repository. */ public function __construct( - SEO_Scores_Repository $seo_scores_repository + SEO_Score_Results_Repository $seo_score_results_repository ) { - $this->scores_repository = $seo_scores_repository; + $this->score_results_repository = $seo_score_results_repository; } } From b930e736abddbefedf7a0de766596af83527a711 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 25 Nov 2024 14:47:00 +0100 Subject: [PATCH 086/132] Use new get_route_prefix --- .../endpoints/readability-scores-endpoint.php | 7 +++++-- .../infrastructure/endpoints/seo-scores-endpoint.php | 9 ++++++--- .../user-interface/scores/abstract-scores-route.php | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php b/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php index 387162ea3d0..35822aef6c4 100644 --- a/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php +++ b/src/dashboard/infrastructure/endpoints/readability-scores-endpoint.php @@ -2,7 +2,9 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints; +use Exception; use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface; +use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route; use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route; /** @@ -25,16 +27,17 @@ public function get_name(): string { * @return string */ public function get_namespace(): string { - return Readability_Scores_Route::ROUTE_NAMESPACE; + return Abstract_Scores_Route::ROUTE_NAMESPACE; } /** * Gets the route. * + * @throws Exception If the route prefix is not overwritten this throws. * @return string */ public function get_route(): string { - return Readability_Scores_Route::ROUTE_PREFIX; + return Readability_Scores_Route::get_route_prefix(); } /** diff --git a/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php b/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php index ae129ab562b..e726507fb4e 100644 --- a/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php +++ b/src/dashboard/infrastructure/endpoints/seo-scores-endpoint.php @@ -2,13 +2,15 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints; +use Exception; use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface; +use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route; use Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route; /** * Represents the SEO scores endpoint. */ -class Seo_Scores_Endpoint implements Endpoint_Interface { +class SEO_Scores_Endpoint implements Endpoint_Interface { /** * Gets the name. @@ -25,16 +27,17 @@ public function get_name(): string { * @return string */ public function get_namespace(): string { - return Seo_Scores_Route::ROUTE_NAMESPACE; + return Abstract_Scores_Route::ROUTE_NAMESPACE; } /** * Gets the route. * + * @throws Exception If the route prefix is not overwritten this throws. * @return string */ public function get_route(): string { - return Seo_Scores_Route::ROUTE_PREFIX; + return SEO_Scores_Route::get_route_prefix(); } /** diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index 57b6b686447..ee57c273c12 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -105,7 +105,7 @@ public function set_repositories( * * @throws Exception If the ROUTE_PREFIX constant is not set in the child class. */ - public function get_route_prefix() { + public static function get_route_prefix() { $class = static::class; $prefix = $class::ROUTE_PREFIX; From 0ba229a3eac461b962d990171a3b55a945de1cf6 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 25 Nov 2024 16:09:24 +0200 Subject: [PATCH 087/132] Return array of results from the collector --- .../abstract-score-results-repository.php | 6 ++-- .../readability-score-results-collector.php | 35 ++++++++++--------- .../seo-score-results-collector.php | 35 ++++++++++--------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index e8c49f79006..b059bc32fe9 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -4,10 +4,10 @@ namespace Yoast\WP\SEO\Dashboard\Application\Score_Results; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Score; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Result; +use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; @@ -72,11 +72,11 @@ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxono 'view' => $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ), ]; - $current_score = new Current_Score( $score_name, (int) $current_scores->scores->$score_name, $current_score_links ); + $current_score = new Current_Score( $score_name, (int) $current_scores['scores']->$score_name, $current_score_links ); $current_scores_list->add( $current_score ); } - $score_result = new Score_Result( $current_scores_list, $current_scores->query_time, $current_scores->cache_used ); + $score_result = new Score_Result( $current_scores_list, $current_scores['query_time'], $current_scores['cache_used'] ); return $score_result->to_array(); } diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index 4712d94592f..541f2f7912a 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -3,7 +3,6 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results; -use stdClass; use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; @@ -24,20 +23,20 @@ class Readability_Score_Results_Collector implements Score_Results_Collector_Int * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array The current readability scores for a content type. + * @return array The current SEO scores for a content type. */ public function get_current_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; - $results = new stdClass(); + $results = []; $content_type_name = $content_type->get_name(); $transient_name = self::READABILITY_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $transient = \get_transient( $transient_name ); if ( $transient !== false ) { - $results->scores = \json_decode( $transient, false ); - $results->cache_used = true; - $results->query_time = 0; + $results['scores'] = \json_decode( $transient, false ); + $results['cache_used'] = true; + $results['query_time'] = 0; return $results; } @@ -53,11 +52,11 @@ public function get_current_scores( array $readability_scores, Content_Type $con ); if ( $term_id === null ) { + $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -69,12 +68,14 @@ public function get_current_scores( array $readability_scores, Content_Type $con $replacements ) ); - $end_time = microtime(true); //phpcs:enable + $end_time = \microtime( true ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - $results->scores = $current_scores; - $results->cache_used = false; - $results->query_time = $end_time - $start_time; + + $results['scores'] = $current_scores; + $results['cache_used'] = false; + $results['query_time'] = ( $end_time - $start_time ); return $results; } @@ -82,11 +83,11 @@ public function get_current_scores( array $readability_scores, Content_Type $con $replacements[] = $wpdb->term_relationships; $replacements[] = $term_id; + $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -103,12 +104,14 @@ public function get_current_scores( array $readability_scores, Content_Type $con $replacements ) ); - $end_time = microtime(true); //phpcs:enable + $end_time = \microtime( true ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - $results->scores = $current_scores; - $results->cache_used = false; - $results->query_time = $end_time - $start_time; + + $results['scores'] = $current_scores; + $results['cache_used'] = false; + $results['query_time'] = ( $end_time - $start_time ); return $results; } diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index ebb9f2ca34f..452c105bc6f 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -3,7 +3,6 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results; -use stdClass; use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; @@ -24,20 +23,20 @@ class SEO_Score_Results_Collector implements Score_Results_Collector_Interface { * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array The current SEO scores for a content type. + * @return array The current SEO scores for a content type. */ public function get_current_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { global $wpdb; - $results = new stdClass(); + $results = []; $content_type_name = $content_type->get_name(); $transient_name = self::SEO_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id ); $transient = \get_transient( $transient_name ); if ( $transient !== false ) { - $results->scores = \json_decode( $transient, false ); - $results->cache_used = true; - $results->query_time = 0; + $results['scores'] = \json_decode( $transient, false ); + $results['cache_used'] = true; + $results['query_time'] = 0; return $results; } @@ -53,11 +52,11 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ ); if ( $term_id === null ) { + $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -70,12 +69,14 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ $replacements ) ); - $end_time = microtime(true); //phpcs:enable + $end_time = \microtime( true ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - $results->scores = $current_scores; - $results->cache_used = false; - $results->query_time = $end_time - $start_time; + + $results['scores'] = $current_scores; + $results['cache_used'] = false; + $results['query_time'] = ( $end_time - $start_time ); return $results; } @@ -83,11 +84,11 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ $replacements[] = $wpdb->term_relationships; $replacements[] = $term_id; + $start_time = \microtime( true ); //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $start_time = microtime(true); $current_scores = $wpdb->get_row( $wpdb->prepare( " @@ -105,12 +106,14 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ $replacements ) ); - $end_time = microtime(true); //phpcs:enable + $end_time = \microtime( true ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); - $results->scores = $current_scores; - $results->cache_used = false; - $results->query_time = $end_time - $start_time; + + $results['scores'] = $current_scores; + $results['cache_used'] = false; + $results['query_time'] = ( $end_time - $start_time ); return $results; } From e363c4012ff54af187601eedb2efbb9a8f98384c Mon Sep 17 00:00:00 2001 From: Thijs van der heijden Date: Mon, 25 Nov 2024 15:20:06 +0100 Subject: [PATCH 088/132] Removes unneeded () --- .../readability-scores/readability-scores-collector.php | 4 ++-- .../infrastructure/scores/seo-scores/seo-scores-collector.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php index 582a8b2be2d..3314d847b44 100644 --- a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -63,7 +63,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con ) ); //phpcs:enable - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); return $current_scores; } @@ -92,7 +92,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con ) ); //phpcs:enable - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); return $current_scores; } diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php index 44232fa6974..b6920bc3a56 100644 --- a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -64,7 +64,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ ) ); //phpcs:enable - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); return $current_scores; } @@ -94,7 +94,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ ) ); //phpcs:enable - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), ( \MINUTE_IN_SECONDS ) ); + \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); return $current_scores; } From 5826db437ce435f92a45df856f4ef5da7a61918a Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 15:23:05 +0100 Subject: [PATCH 089/132] Descriptions copy --- packages/js/src/dashboard/scores/score-meta.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/js/src/dashboard/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js index ef4cd27a96e..9ab0009a561 100644 --- a/packages/js/src/dashboard/scores/score-meta.js +++ b/packages/js/src/dashboard/scores/score-meta.js @@ -37,14 +37,14 @@ export const SCORE_META = { export const SCORE_DESCRIPTIONS = { seo: { good: __( "Most of your content has a good SEO score. Well done!", "wordpress-seo" ), - ok: __( "Your content has an average SEO score. Find opportunities for enhancement.", "wordpress-seo" ), - bad: __( "Some of your content needs attention. Identify and address areas for improvement.", "wordpress-seo" ), - notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it in your editor so we can analyze it.", "wordpress-seo" ), + ok: __( "Your content has an average SEO score. Time to find opportunities for improvement!", "wordpress-seo" ), + bad: __( "Some of your content could use a little extra care. Take a look and start improving!", "wordpress-seo" ), + notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it and save it in your editor so we can start the analysis.", "wordpress-seo" ), }, readability: { - good: __( "Most of your content has a good Readability score. Well done!", "wordpress-seo" ), - ok: __( "Your content has an average Readability score. Find opportunities for enhancement.", "wordpress-seo" ), - bad: __( "Some of your content needs attention. Identify and address areas for improvement.", "wordpress-seo" ), - notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it in your editor so we can analyze it.", "wordpress-seo" ), + good: __( "Most of your content has a good readability score. Well done!", "wordpress-seo" ), + ok: __( "Your content has an average readability score. Time to find opportunities for improvement!", "wordpress-seo" ), + bad: __( "Some of your content could use a little extra care. Take a look and start improving!", "wordpress-seo" ), + notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it and save it in your editor so we can start the analysis.", "wordpress-seo" ), }, }; From 79d15c010491403e6b5dddab028eb933886ee0b7 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis Date: Mon, 25 Nov 2024 16:24:45 +0200 Subject: [PATCH 090/132] Return current scores in the expected order --- .../abstract-score-results-repository.php | 2 +- .../domain/score-results/current-scores-list.php | 12 ++++++++---- src/dashboard/domain/score-results/score-result.php | 6 +++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index b059bc32fe9..d465e48969a 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -73,7 +73,7 @@ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxono ]; $current_score = new Current_Score( $score_name, (int) $current_scores['scores']->$score_name, $current_score_links ); - $current_scores_list->add( $current_score ); + $current_scores_list->add( $current_score, $score->get_position() ); } $score_result = new Score_Result( $current_scores_list, $current_scores['query_time'], $current_scores['cache_used'] ); diff --git a/src/dashboard/domain/score-results/current-scores-list.php b/src/dashboard/domain/score-results/current-scores-list.php index 46b4d64c0d2..8f9b7801d1f 100644 --- a/src/dashboard/domain/score-results/current-scores-list.php +++ b/src/dashboard/domain/score-results/current-scores-list.php @@ -3,12 +3,12 @@ namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results; /** - * This class describes a list of score results. + * This class describes a list of current scores. */ class Current_Scores_List { /** - * The scores. + * The current scores. * * @var Current_Score[] */ @@ -18,11 +18,12 @@ class Current_Scores_List { * Adds a current score to the list. * * @param Current_Score $current_score The current score to add. + * @param int $position The position to add the current score. * * @return void */ - public function add( Current_Score $current_score ): void { - $this->current_scores[] = $current_score; + public function add( Current_Score $current_score, int $position ): void { + $this->current_scores[ $position ] = $current_score; } /** @@ -32,6 +33,9 @@ public function add( Current_Score $current_score ): void { */ public function to_array(): array { $array = []; + + \ksort( $this->current_scores ); + foreach ( $this->current_scores as $current_score ) { $array[] = [ 'name' => $current_score->get_name(), diff --git a/src/dashboard/domain/score-results/score-result.php b/src/dashboard/domain/score-results/score-result.php index d6f97e8dd36..1a87c6348c7 100644 --- a/src/dashboard/domain/score-results/score-result.php +++ b/src/dashboard/domain/score-results/score-result.php @@ -31,9 +31,9 @@ class Score_Result { /** * The constructor. * - * @param Current_Score[] $current_scores_list The list of the current scores of the score result. - * @param float $query_time The time the query took to get the score results. - * @param bool $is_cached_used Whether cache was used to get the score results. + * @param Current_Scores_List $current_scores_list The list of the current scores of the score result. + * @param float $query_time The time the query took to get the score results. + * @param bool $is_cached_used Whether cache was used to get the score results. */ public function __construct( Current_Scores_List $current_scores_list, float $query_time, bool $is_cached_used ) { $this->current_scores_list = $current_scores_list; From 66a57fa3aac0f23a86412b0b73561fbc9d40f6ee Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:12:04 +0100 Subject: [PATCH 091/132] Page descriptions copy * default copy: with link to external content-analysis * both features disabled but still indexables: with link to internal site features * no indexables: default copy but with info alert --- .../js/src/dashboard/components/dashboard.js | 6 +++-- .../js/src/dashboard/components/page-title.js | 22 +++++++++++++++---- packages/js/src/dashboard/index.js | 5 +++++ packages/js/src/general/initialize.js | 6 +++++ 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/js/src/dashboard/components/dashboard.js b/packages/js/src/dashboard/components/dashboard.js index 387556946a3..62f58e02edc 100644 --- a/packages/js/src/dashboard/components/dashboard.js +++ b/packages/js/src/dashboard/components/dashboard.js @@ -5,6 +5,7 @@ import { PageTitle } from "./page-title"; * @type {import("../index").ContentType} ContentType * @type {import("../index").Features} Features * @type {import("../index").Endpoints} Endpoints + * @type {import("../index").Links} Links */ /** @@ -13,12 +14,13 @@ import { PageTitle } from "./page-title"; * @param {Features} features Whether features are enabled. * @param {Endpoints} endpoints The endpoints. * @param {Object} headers The headers for the score requests. + * @param {Links} links The links. * @returns {JSX.Element} The element. */ -export const Dashboard = ( { contentTypes, userName, features, endpoints, headers } ) => { +export const Dashboard = ( { contentTypes, userName, features, endpoints, headers, links } ) => { return ( <> - +
          { features.indexables && features.seoAnalysis && ( diff --git a/packages/js/src/dashboard/components/page-title.js b/packages/js/src/dashboard/components/page-title.js index c8d518cd7ce..f2d085b735f 100644 --- a/packages/js/src/dashboard/components/page-title.js +++ b/packages/js/src/dashboard/components/page-title.js @@ -1,17 +1,20 @@ import { createInterpolateElement } from "@wordpress/element"; import { __, sprintf } from "@wordpress/i18n"; import { Alert, Link, Paper, Title } from "@yoast/ui-library"; +import { OutboundLink } from "../../shared-admin/components"; /** * @type {import("../index").Features} Features + * @type {import("../index").Links} Links */ /** * @param {string} userName The user name. * @param {Features} features Whether features are enabled. + * @param {Links} links The links. * @returns {JSX.Element} The element. */ -export const PageTitle = ( { userName, features } ) => ( +export const PageTitle = ( { userName, features, links } ) => ( @@ -25,7 +28,7 @@ export const PageTitle = ( { userName, features } ) => ( ? createInterpolateElement( sprintf( /* translators: %1$s and %2$s expand to an opening and closing anchor tag. */ - __( "It seems that the SEO analysis and the Readability analysis are currently disabled in your %1$sSite features%2$s. Once you enable these features, you'll be able to see the insights you need right here!", "wordpress-seo" ), + __( "It looks like the ‘SEO analysis’ and the ‘Readability analysis’ are currently turned off in your %1$sSite features%2$s. Enable these features to start seeing all the insights you need right here!", "wordpress-seo" ), "<link>", "</link>" ), @@ -34,12 +37,23 @@ export const PageTitle = ( { userName, features } ) => ( link: <Link href="admin.php?page=wpseo_page_settings#/site-features"> </Link>, } ) - : __( "Welcome to our SEO dashboard!", "wordpress-seo" ) + : createInterpolateElement( + sprintf( + /* translators: %1$s and %2$s expand to an opening and closing anchor tag. */ + __( "Welcome to your dashboard! Check your content's SEO performance, readability, and overall strengths and opportunities. %1$sLearn more on how to improve your content with our content analysis tool%2$s.", "wordpress-seo" ), + "<link>", + "</link>" + ), + { + // Added dummy space as content to prevent children prop warnings in the console. + link: <OutboundLink href={ links.contentAnalysis }> </OutboundLink>, + } + ) } </p> { ! features.indexables && ( <Alert type="info"> - { __( "The overview of your SEO scores and Readability scores is not available because you're on a non-production environment.", "wordpress-seo" ) } + { __( "Oops! You can’t see the overview of your SEO scores and readability scores right now because you’re in a non-production environment.", "wordpress-seo" ) } </Alert> ) } </Paper.Content> diff --git a/packages/js/src/dashboard/index.js b/packages/js/src/dashboard/index.js index 4cff5bdbb74..5b23f115a4b 100644 --- a/packages/js/src/dashboard/index.js +++ b/packages/js/src/dashboard/index.js @@ -49,3 +49,8 @@ export { Dashboard } from "./components/dashboard"; * @property {string} seoScores The endpoint for SEO scores. * @property {string} readabilityScores The endpoint for readability scores. */ + +/** + * @typedef {Object} Links The links. + * @property {string} contentAnalysis The content analysis information link. + */ diff --git a/packages/js/src/general/initialize.js b/packages/js/src/general/initialize.js index e464531591f..c8f93a57511 100644 --- a/packages/js/src/general/initialize.js +++ b/packages/js/src/general/initialize.js @@ -62,6 +62,11 @@ domReady( () => { "X-Wp-Nonce": get( window, "wpseoScriptData.dashboard.nonce", "" ), }; + /** @type {{contentAnalysis: string}} */ + const links = { + contentAnalysis: select( STORE_NAME ).selectLink( "https://yoa.st/content-analysis-tool" ), + }; + const router = createHashRouter( createRoutesFromElements( <Route path="/" element={ <App /> } errorElement={ <RouteErrorFallback className="yst-m-8" /> }> @@ -75,6 +80,7 @@ domReady( () => { features={ features } endpoints={ endpoints } headers={ headers } + links={ links } /> <ConnectedPremiumUpsellList /> </SidebarLayout> From ae2ac97656fcc098b1f401aaa6bb4c4bd217d9e8 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 10:37:05 +0200 Subject: [PATCH 092/132] Rename scores to score groups for better readability --- .../abstract-score-results-repository.php | 34 +++++------ .../readability-score-results-repository.php | 8 +-- .../seo-score-results-repository.php | 8 +-- .../score-groups/abstract-score-group.php | 58 +++++++++++++++++++ .../abstract-readability-score-group.php | 21 +++++++ .../bad-readability-score-group.php | 55 ++++++++++++++++++ .../good-readability-score-group.php | 55 ++++++++++++++++++ .../no-readability-score-group.php | 55 ++++++++++++++++++ .../ok-readability-score-group.php | 55 ++++++++++++++++++ .../readability-score-groups-interface.php | 10 ++++ .../score-groups-interface.php} | 18 +++--- .../abstract-seo-score-group.php | 21 +++++++ .../seo-score-groups/bad-seo-score-group.php | 55 ++++++++++++++++++ .../seo-score-groups/good-seo-score-group.php | 55 ++++++++++++++++++ .../seo-score-groups/no-seo-score-group.php | 55 ++++++++++++++++++ .../seo-score-groups/ok-seo-score-group.php | 55 ++++++++++++++++++ .../seo-score-groups-interface.php | 10 ++++ .../score-results/current-scores-list.php | 2 +- .../domain/scores/abstract-score.php | 58 ------------------- .../abstract-readability-score.php | 21 ------- .../bad-readability-score.php | 55 ------------------ .../good-readability-score.php | 55 ------------------ .../no-readability-score.php | 55 ------------------ .../ok-readability-score.php | 55 ------------------ .../readability-scores-interface.php | 10 ---- .../scores/seo-scores/abstract-seo-score.php | 21 ------- .../scores/seo-scores/bad-seo-score.php | 55 ------------------ .../scores/seo-scores/good-seo-score.php | 55 ------------------ .../domain/scores/seo-scores/no-seo-score.php | 55 ------------------ .../domain/scores/seo-scores/ok-seo-score.php | 55 ------------------ .../seo-scores/seo-scores-interface.php | 10 ---- .../score-group-link-collector.php} | 22 +++---- .../readability-score-results-collector.php | 26 ++++----- .../score-results-collector-interface.php | 10 ++-- .../seo-score-results-collector.php | 24 ++++---- 35 files changed, 636 insertions(+), 636 deletions(-) create mode 100644 src/dashboard/domain/score-groups/abstract-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php create mode 100644 src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php rename src/dashboard/domain/{scores/scores-interface.php => score-groups/score-groups-interface.php} (53%) create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php create mode 100644 src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php delete mode 100644 src/dashboard/domain/scores/abstract-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/abstract-readability-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/bad-readability-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/good-readability-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/no-readability-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/ok-readability-score.php delete mode 100644 src/dashboard/domain/scores/readability-scores/readability-scores-interface.php delete mode 100644 src/dashboard/domain/scores/seo-scores/abstract-seo-score.php delete mode 100644 src/dashboard/domain/scores/seo-scores/bad-seo-score.php delete mode 100644 src/dashboard/domain/scores/seo-scores/good-seo-score.php delete mode 100644 src/dashboard/domain/scores/seo-scores/no-seo-score.php delete mode 100644 src/dashboard/domain/scores/seo-scores/ok-seo-score.php delete mode 100644 src/dashboard/domain/scores/seo-scores/seo-scores-interface.php rename src/dashboard/infrastructure/{scores/score-link-collector.php => score-groups/score-group-link-collector.php} (55%) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index d465e48969a..8567e44940f 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -4,13 +4,13 @@ namespace Yoast\WP\SEO\Dashboard\Application\Score_Results; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Score; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Result; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; +use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups\Score_Group_Link_Collector; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; -use Yoast\WP\SEO\Dashboard\Infrastructure\Scores\Score_Link_Collector; /** * The abstract score results repository. @@ -25,32 +25,32 @@ abstract class Abstract_Score_Results_Repository { protected $score_results_collector; /** - * The score link collector. + * The score group link collector. * - * @var Score_Link_Collector + * @var Score_Group_Link_Collector */ - protected $score_link_collector; + protected $score_group_link_collector; /** - * All scores. + * All score groups. * - * @var Scores_Interface[] + * @var Score_Groups_Interface[] */ - protected $scores; + protected $score_groups; /** * Sets the score link collector. * * @required * - * @param Score_Link_Collector $score_link_collector The score link collector. + * @param Score_Group_Link_Collector $score_group_link_collector The score group link collector. * * @return void */ - public function set_score_link_collector( - Score_Link_Collector $score_link_collector + public function set_score_group_link_collector( + Score_Group_Link_Collector $score_group_link_collector ) { - $this->score_link_collector = $score_link_collector; + $this->score_group_link_collector = $score_group_link_collector; } /** @@ -64,16 +64,16 @@ public function set_score_link_collector( */ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $current_scores_list = new Current_Scores_List(); - $current_scores = $this->score_results_collector->get_current_scores( $this->scores, $content_type, $term_id ); + $current_scores = $this->score_results_collector->get_current_scores( $this->score_groups, $content_type, $term_id ); - foreach ( $this->scores as $score ) { - $score_name = $score->get_name(); + foreach ( $this->score_groups as $score_group ) { + $score_name = $score_group->get_name(); $current_score_links = [ - 'view' => $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ), + 'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ), ]; $current_score = new Current_Score( $score_name, (int) $current_scores['scores']->$score_name, $current_score_links ); - $current_scores_list->add( $current_score, $score->get_position() ); + $current_scores_list->add( $current_score, $score_group->get_position() ); } $score_result = new Score_Result( $current_scores_list, $current_scores['query_time'], $current_scores['cache_used'] ); diff --git a/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php b/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php index 197a37c5d2a..7b1d2f586ed 100644 --- a/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php +++ b/src/dashboard/application/score-results/readability-score-results/readability-score-results-repository.php @@ -4,7 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results; use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results\Readability_Score_Results_Collector; /** @@ -16,13 +16,13 @@ class Readability_Score_Results_Repository extends Abstract_Score_Results_Reposi * The constructor. * * @param Readability_Score_Results_Collector $readability_score_results_collector The readability score results collector. - * @param Readability_Scores_Interface ...$readability_scores All readability scores. + * @param Readability_Score_Groups_Interface ...$readability_score_groups All readability score groups. */ public function __construct( Readability_Score_Results_Collector $readability_score_results_collector, - Readability_Scores_Interface ...$readability_scores + Readability_Score_Groups_Interface ...$readability_score_groups ) { $this->score_results_collector = $readability_score_results_collector; - $this->scores = $readability_scores; + $this->score_groups = $readability_score_groups; } } diff --git a/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php b/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php index 7774e46d050..851c5ab59de 100644 --- a/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php +++ b/src/dashboard/application/score-results/seo-score-results/seo-score-results-repository.php @@ -4,7 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results; use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results\SEO_Score_Results_Collector; /** @@ -16,13 +16,13 @@ class SEO_Score_Results_Repository extends Abstract_Score_Results_Repository { * The constructor. * * @param SEO_Score_Results_Collector $seo_score_results_collector The SEO score results collector. - * @param SEO_Scores_Interface ...$seo_scores All SEO scores. + * @param SEO_Score_Groups_Interface ...$seo_score_groups All SEO score groups. */ public function __construct( SEO_Score_Results_Collector $seo_score_results_collector, - SEO_Scores_Interface ...$seo_scores + SEO_Score_Groups_Interface ...$seo_score_groups ) { $this->score_results_collector = $seo_score_results_collector; - $this->scores = $seo_scores; + $this->score_groups = $seo_score_groups; } } diff --git a/src/dashboard/domain/score-groups/abstract-score-group.php b/src/dashboard/domain/score-groups/abstract-score-group.php new file mode 100644 index 00000000000..f961b0c1955 --- /dev/null +++ b/src/dashboard/domain/score-groups/abstract-score-group.php @@ -0,0 +1,58 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups; + +/** + * Abstract class for a score group. + */ +abstract class Abstract_Score_Group implements Score_Groups_Interface { + + /** + * The name of the score group. + * + * @var string + */ + private $name; + + /** + * The key of the score group that is used when filtering on the posts page. + * + * @var string + */ + private $filter_key; + + /** + * The value of the score group that is used when filtering on the posts page. + * + * @var string + */ + private $filter_value; + + /** + * The min score of the score group. + * + * @var int + */ + private $min_score; + + /** + * The max score of the score group. + * + * @var int + */ + private $max_score; + + /** + * The view link of the score group. + * + * @var string + */ + private $view_link; + + /** + * The position of the score group. + * + * @var int + */ + private $position; +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php new file mode 100644 index 00000000000..fedfb850b56 --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php @@ -0,0 +1,21 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group; + +/** + * Abstract class for a readability score group. + */ +abstract class Abstract_Readability_Score_Group extends Abstract_Score_Group implements Readability_Score_Groups_Interface { + + /** + * Gets the key of the readability score group that is used when filtering on the posts page. + * + * @return string The name of the readability score group that is used when filtering on the posts page. + */ + public function get_filter_key(): string { + return 'readability_filter'; + } +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php new file mode 100644 index 00000000000..57eac8485d4 --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +/** + * This class describes a bad readability score group. + */ +class Bad_Readability_Score_Group extends Abstract_Readability_Score_Group { + + /** + * Gets the name of the readability score group. + * + * @return string The name of the readability score group. + */ + public function get_name(): string { + return 'bad'; + } + + /** + * Gets the value of the readability score group that is used when filtering on the posts page. + * + * @return string The name of the readability score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'bad'; + } + + /** + * Gets the position of the readability score group. + * + * @return int The position of the readability score group. + */ + public function get_position(): int { + return 2; + } + + /** + * Gets the minimum score of the readability score group. + * + * @return int The minimum score of the readability score group. + */ + public function get_min_score(): ?int { + return 0; + } + + /** + * Gets the maximum score of the readability score group. + * + * @return int The maximum score of the readability score group. + */ + public function get_max_score(): ?int { + return 40; + } +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php new file mode 100644 index 00000000000..425f4bfe7c8 --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +/** + * This class describes a good readability score group. + */ +class Good_Readability_Score_Group extends Abstract_Readability_Score_Group { + + /** + * Gets the name of the readability score group. + * + * @return string The name of the readability score group. + */ + public function get_name(): string { + return 'good'; + } + + /** + * Gets the value of the readability score group that is used when filtering on the posts page. + * + * @return string The name of the readability score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'good'; + } + + /** + * Gets the position of the readability score group. + * + * @return int The position of the readability score group. + */ + public function get_position(): int { + return 0; + } + + /** + * Gets the minimum score of the readability score group. + * + * @return int The minimum score of the readability score group. + */ + public function get_min_score(): ?int { + return 71; + } + + /** + * Gets the maximum score of the readability score group. + * + * @return int The maximum score of the readability score group. + */ + public function get_max_score(): ?int { + return 100; + } +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php new file mode 100644 index 00000000000..0466dcee3e1 --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +/** + * This class describes a missing readability score group. + */ +class No_Readability_Score_Group extends Abstract_Readability_Score_Group { + + /** + * Gets the name of the readability score group. + * + * @return string The name of the readability score group. + */ + public function get_name(): string { + return 'notAnalyzed'; + } + + /** + * Gets the value of the readability score group that is used when filtering on the posts page. + * + * @return string The name of the readability score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'na'; + } + + /** + * Gets the position of the readability score group. + * + * @return int The position of the readability score group. + */ + public function get_position(): int { + return 3; + } + + /** + * Gets the minimum score of the readability score group. + * + * @return null The minimum score of the readability score group. + */ + public function get_min_score(): ?int { + return null; + } + + /** + * Gets the maximum score of the readability score group. + * + * @return null The maximum score of the readability score group. + */ + public function get_max_score(): ?int { + return null; + } +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php new file mode 100644 index 00000000000..a6218261c2e --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +/** + * This class describes an OK readability score group. + */ +class Ok_Readability_Score_Group extends Abstract_Readability_Score_Group { + + /** + * Gets the name of the readability score group. + * + * @return string The the name of the readability score group. + */ + public function get_name(): string { + return 'ok'; + } + + /** + * Gets the value of the readability score group that is used when filtering on the posts page. + * + * @return string The name of the readability score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'ok'; + } + + /** + * Gets the position of the readability score group. + * + * @return int The position of the readability score group. + */ + public function get_position(): int { + return 1; + } + + /** + * Gets the minimum score of the readability score group. + * + * @return int The minimum score of the readability score group. + */ + public function get_min_score(): ?int { + return 41; + } + + /** + * Gets the maximum score of the readability score group. + * + * @return int The maximum score of the readability score group. + */ + public function get_max_score(): ?int { + return 70; + } +} diff --git a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php new file mode 100644 index 00000000000..2d7596a79e8 --- /dev/null +++ b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php @@ -0,0 +1,10 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups; + +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; + +/** + * This interface describes a readability score group implementation. + */ +interface Readability_Score_Groups_Interface extends Score_Groups_Interface {} diff --git a/src/dashboard/domain/scores/scores-interface.php b/src/dashboard/domain/score-groups/score-groups-interface.php similarity index 53% rename from src/dashboard/domain/scores/scores-interface.php rename to src/dashboard/domain/score-groups/score-groups-interface.php index c3b9e4879ee..a2becbd3a5a 100644 --- a/src/dashboard/domain/scores/scores-interface.php +++ b/src/dashboard/domain/score-groups/score-groups-interface.php @@ -1,49 +1,49 @@ <?php // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -namespace Yoast\WP\SEO\Dashboard\Domain\Scores; +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups; /** - * This interface describes a score implementation. + * This interface describes a score group implementation. */ -interface Scores_Interface { +interface Score_Groups_Interface { /** - * Gets the name of the score. + * Gets the name of the score group. * * @return string */ public function get_name(): string; /** - * Gets the key of the score that is used when filtering on the posts page. + * Gets the key of the score group that is used when filtering on the posts page. * * @return string */ public function get_filter_key(): string; /** - * Gets the value of the score that is used when filtering on the posts page. + * Gets the value of the score group that is used when filtering on the posts page. * * @return string */ public function get_filter_value(): string; /** - * Gets the minimum score of the score. + * Gets the minimum score of the score group. * * @return int */ public function get_min_score(): ?int; /** - * Gets the maximum score of the score. + * Gets the maximum score of the score group. * * @return int */ public function get_max_score(): ?int; /** - * Gets the position of the score. + * Gets the position of the score group. * * @return int */ diff --git a/src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php new file mode 100644 index 00000000000..bd47f2f02cc --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/abstract-seo-score-group.php @@ -0,0 +1,21 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group; + +/** + * Abstract class for an SEO score group. + */ +abstract class Abstract_SEO_Score_Group extends Abstract_Score_Group implements SEO_Score_Groups_Interface { + + /** + * Gets the key of the SEO score group that is used when filtering on the posts page. + * + * @return string The name of the SEO score group that is used when filtering on the posts page. + */ + public function get_filter_key(): string { + return 'seo_filter'; + } +} diff --git a/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php new file mode 100644 index 00000000000..a0772795bfe --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +/** + * This class describes a bad SEO score group. + */ +class Bad_SEO_Score_Group extends Abstract_SEO_Score_Group { + + /** + * Gets the name of the SEO score group. + * + * @return string The name of the SEO score group. + */ + public function get_name(): string { + return 'bad'; + } + + /** + * Gets the value of the SEO score group that is used when filtering on the posts page. + * + * @return string The name of the SEO score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'bad'; + } + + /** + * Gets the position of the SEO score group. + * + * @return int The position of the SEO score group. + */ + public function get_position(): int { + return 2; + } + + /** + * Gets the minimum score of the SEO score group. + * + * @return int The minimum score of the SEO score group. + */ + public function get_min_score(): ?int { + return 0; + } + + /** + * Gets the maximum score of the SEO score group. + * + * @return int The maximum score of the SEO score group. + */ + public function get_max_score(): ?int { + return 40; + } +} diff --git a/src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php new file mode 100644 index 00000000000..f9db08ad52a --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/good-seo-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +/** + * This class describes a good SEO score group. + */ +class Good_SEO_Score_Group extends Abstract_SEO_Score_Group { + + /** + * Gets the name of the SEO score group. + * + * @return string The name of the SEO score group. + */ + public function get_name(): string { + return 'good'; + } + + /** + * Gets the value of the SEO score group that is used when filtering on the posts page. + * + * @return string The name of the SEO score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'good'; + } + + /** + * Gets the position of the SEO score group. + * + * @return int The position of the SEO score group. + */ + public function get_position(): int { + return 0; + } + + /** + * Gets the minimum score of the SEO score group. + * + * @return int The minimum score of the SEO score group. + */ + public function get_min_score(): ?int { + return 71; + } + + /** + * Gets the maximum score of the SEO score group. + * + * @return int The maximum score of the SEO score group. + */ + public function get_max_score(): ?int { + return 100; + } +} diff --git a/src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php new file mode 100644 index 00000000000..e73d961d557 --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/no-seo-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +/** + * This class describes a missing SEO score group. + */ +class No_SEO_Score_Group extends Abstract_SEO_Score_Group { + + /** + * Gets the name of the SEO score group. + * + * @return string The name of the SEO score group. + */ + public function get_name(): string { + return 'notAnalyzed'; + } + + /** + * Gets the value of the SEO score group that is used when filtering on the posts page. + * + * @return string The name of the SEO score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'na'; + } + + /** + * Gets the position of the SEO score group. + * + * @return int The position of the SEO score group. + */ + public function get_position(): int { + return 3; + } + + /** + * Gets the minimum score of the SEO score group. + * + * @return null The minimum score of the SEO score group. + */ + public function get_min_score(): ?int { + return null; + } + + /** + * Gets the maximum score of the SEO score group. + * + * @return null The maximum score of the SEO score group. + */ + public function get_max_score(): ?int { + return null; + } +} diff --git a/src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php new file mode 100644 index 00000000000..92c7cff2e47 --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/ok-seo-score-group.php @@ -0,0 +1,55 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +/** + * This class describes an OK SEO score group. + */ +class Ok_SEO_Score_Group extends Abstract_SEO_Score_Group { + + /** + * Gets the name of the SEO score group. + * + * @return string The the name of the SEO score group. + */ + public function get_name(): string { + return 'ok'; + } + + /** + * Gets the value of the SEO score group that is used when filtering on the posts page. + * + * @return string The name of the SEO score group that is used when filtering on the posts page. + */ + public function get_filter_value(): string { + return 'ok'; + } + + /** + * Gets the position of the SEO score group. + * + * @return int The position of the SEO score group. + */ + public function get_position(): int { + return 1; + } + + /** + * Gets the minimum score of the SEO score group. + * + * @return int The minimum score of the SEO score group. + */ + public function get_min_score(): ?int { + return 41; + } + + /** + * Gets the maximum score of the SEO score group. + * + * @return int The maximum score of the SEO score group. + */ + public function get_max_score(): ?int { + return 70; + } +} diff --git a/src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php b/src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php new file mode 100644 index 00000000000..5999e4a5829 --- /dev/null +++ b/src/dashboard/domain/score-groups/seo-score-groups/seo-score-groups-interface.php @@ -0,0 +1,10 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups; + +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; + +/** + * This interface describes an SEO score group implementation. + */ +interface SEO_Score_Groups_Interface extends Score_Groups_Interface {} diff --git a/src/dashboard/domain/score-results/current-scores-list.php b/src/dashboard/domain/score-results/current-scores-list.php index 8f9b7801d1f..a4fee60a515 100644 --- a/src/dashboard/domain/score-results/current-scores-list.php +++ b/src/dashboard/domain/score-results/current-scores-list.php @@ -27,7 +27,7 @@ public function add( Current_Score $current_score, int $position ): void { } /** - * Parses the score list to the expected key value representation. + * Parses the current score list to the expected key value representation. * * @return array<array<string, string|int|array<string, string>>> The score list presented as the expected key value representation. */ diff --git a/src/dashboard/domain/scores/abstract-score.php b/src/dashboard/domain/scores/abstract-score.php deleted file mode 100644 index 3ff07f5b03a..00000000000 --- a/src/dashboard/domain/scores/abstract-score.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -namespace Yoast\WP\SEO\Dashboard\Domain\Scores; - -/** - * Abstract class for a score. - */ -abstract class Abstract_Score implements Scores_Interface { - - /** - * The name of the score. - * - * @var string - */ - private $name; - - /** - * The key of the score that is used when filtering on the posts page. - * - * @var string - */ - private $filter_key; - - /** - * The value of the score that is used when filtering on the posts page. - * - * @var string - */ - private $filter_value; - - /** - * The min score of the score. - * - * @var int - */ - private $min_score; - - /** - * The max score of the score. - * - * @var int - */ - private $max_score; - - /** - * The view link of the score. - * - * @var string - */ - private $view_link; - - /** - * The position of the score. - * - * @var int - */ - private $position; -} diff --git a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php deleted file mode 100644 index 133623f9173..00000000000 --- a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -use Yoast\WP\SEO\Dashboard\Domain\Scores\Abstract_Score; - -/** - * Abstract class for a readability score. - */ -abstract class Abstract_Readability_Score extends Abstract_Score implements Readability_Scores_Interface { - - /** - * Gets the key of the readability score that is used when filtering on the posts page. - * - * @return string The name of the readability score that is used when filtering on the posts page. - */ - public function get_filter_key(): string { - return 'readability_filter'; - } -} diff --git a/src/dashboard/domain/scores/readability-scores/bad-readability-score.php b/src/dashboard/domain/scores/readability-scores/bad-readability-score.php deleted file mode 100644 index 8c46f512061..00000000000 --- a/src/dashboard/domain/scores/readability-scores/bad-readability-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -/** - * This class describes a bad readability score. - */ -class Bad_Readability_Score extends Abstract_Readability_Score { - - /** - * Gets the name of the readability score. - * - * @return string The name of the readability score. - */ - public function get_name(): string { - return 'bad'; - } - - /** - * Gets the value of the readability score that is used when filtering on the posts page. - * - * @return string The name of the readability score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'bad'; - } - - /** - * Gets the position of the readability score. - * - * @return int The position of the readability score. - */ - public function get_position(): int { - return 2; - } - - /** - * Gets the minimum score of the readability score. - * - * @return int The minimum score of the readability score. - */ - public function get_min_score(): ?int { - return 0; - } - - /** - * Gets the maximum score of the readability score. - * - * @return int The maximum score of the readability score. - */ - public function get_max_score(): ?int { - return 40; - } -} diff --git a/src/dashboard/domain/scores/readability-scores/good-readability-score.php b/src/dashboard/domain/scores/readability-scores/good-readability-score.php deleted file mode 100644 index fdad0d13712..00000000000 --- a/src/dashboard/domain/scores/readability-scores/good-readability-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -/** - * This class describes a Good readability score. - */ -class Good_Readability_Score extends Abstract_Readability_Score { - - /** - * Gets the name of the readability score. - * - * @return string The name of the readability score. - */ - public function get_name(): string { - return 'good'; - } - - /** - * Gets the value of the readability score that is used when filtering on the posts page. - * - * @return string The name of the readability score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'good'; - } - - /** - * Gets the position of the readability score. - * - * @return int The position of the readability score. - */ - public function get_position(): int { - return 0; - } - - /** - * Gets the minimum score of the readability score. - * - * @return int The minimum score of the readability score. - */ - public function get_min_score(): ?int { - return 71; - } - - /** - * Gets the maximum score of the readability score. - * - * @return int The maximum score of the readability score. - */ - public function get_max_score(): ?int { - return 100; - } -} diff --git a/src/dashboard/domain/scores/readability-scores/no-readability-score.php b/src/dashboard/domain/scores/readability-scores/no-readability-score.php deleted file mode 100644 index a8139fb21f3..00000000000 --- a/src/dashboard/domain/scores/readability-scores/no-readability-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -/** - * This class describes a missing readability score. - */ -class No_Readability_Score extends Abstract_Readability_Score { - - /** - * Gets the name of the readability score. - * - * @return string The name of the readability score. - */ - public function get_name(): string { - return 'notAnalyzed'; - } - - /** - * Gets the value of the readability score that is used when filtering on the posts page. - * - * @return string The name of the readability score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'na'; - } - - /** - * Gets the position of the readability score. - * - * @return int The position of the readability score. - */ - public function get_position(): int { - return 3; - } - - /** - * Gets the minimum score of the readability score. - * - * @return null The minimum score of the readability score. - */ - public function get_min_score(): ?int { - return null; - } - - /** - * Gets the maximum score of the readability score. - * - * @return null The maximum score of the readability score. - */ - public function get_max_score(): ?int { - return null; - } -} diff --git a/src/dashboard/domain/scores/readability-scores/ok-readability-score.php b/src/dashboard/domain/scores/readability-scores/ok-readability-score.php deleted file mode 100644 index 45bb8e45126..00000000000 --- a/src/dashboard/domain/scores/readability-scores/ok-readability-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -/** - * This class describes an OK readability score. - */ -class Ok_Readability_Score extends Abstract_Readability_Score { - - /** - * Gets the name of the readability score. - * - * @return string The the name of the readability score. - */ - public function get_name(): string { - return 'ok'; - } - - /** - * Gets the value of the readability score that is used when filtering on the posts page. - * - * @return string The name of the readability score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'ok'; - } - - /** - * Gets the position of the readability score. - * - * @return int The position of the readability score. - */ - public function get_position(): int { - return 1; - } - - /** - * Gets the minimum score of the readability score. - * - * @return int The minimum score of the readability score. - */ - public function get_min_score(): ?int { - return 41; - } - - /** - * Gets the maximum score of the readability score. - * - * @return int The maximum score of the readability score. - */ - public function get_max_score(): ?int { - return 70; - } -} diff --git a/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php b/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php deleted file mode 100644 index 6b985cfa2b5..00000000000 --- a/src/dashboard/domain/scores/readability-scores/readability-scores-interface.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores; - -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; - -/** - * This interface describes a readability score implementation. - */ -interface Readability_Scores_Interface extends Scores_Interface {} diff --git a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php deleted file mode 100644 index 52f5b3f0d0f..00000000000 --- a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -use Yoast\WP\SEO\Dashboard\Domain\Scores\Abstract_Score; - -/** - * Abstract class for an SEO score. - */ -abstract class Abstract_SEO_Score extends Abstract_Score implements SEO_Scores_Interface { - - /** - * Gets the key of the SEO score that is used when filtering on the posts page. - * - * @return string The name of the SEO score that is used when filtering on the posts page. - */ - public function get_filter_key(): string { - return 'seo_filter'; - } -} diff --git a/src/dashboard/domain/scores/seo-scores/bad-seo-score.php b/src/dashboard/domain/scores/seo-scores/bad-seo-score.php deleted file mode 100644 index 9ffbb67fe9a..00000000000 --- a/src/dashboard/domain/scores/seo-scores/bad-seo-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -/** - * This class describes a bad SEO score. - */ -class Bad_SEO_Score extends Abstract_SEO_Score { - - /** - * Gets the name of the SEO score. - * - * @return string The name of the SEO score. - */ - public function get_name(): string { - return 'bad'; - } - - /** - * Gets the value of the SEO score that is used when filtering on the posts page. - * - * @return string The name of the SEO score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'bad'; - } - - /** - * Gets the position of the SEO score. - * - * @return int The position of the SEO score. - */ - public function get_position(): int { - return 2; - } - - /** - * Gets the minimum score of the SEO score. - * - * @return int The minimum score of the SEO score. - */ - public function get_min_score(): ?int { - return 0; - } - - /** - * Gets the maximum score of the SEO score. - * - * @return int The maximum score of the SEO score. - */ - public function get_max_score(): ?int { - return 40; - } -} diff --git a/src/dashboard/domain/scores/seo-scores/good-seo-score.php b/src/dashboard/domain/scores/seo-scores/good-seo-score.php deleted file mode 100644 index 2cddb4c2a2d..00000000000 --- a/src/dashboard/domain/scores/seo-scores/good-seo-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -/** - * This class describes a Good SEO score. - */ -class Good_SEO_Score extends Abstract_SEO_Score { - - /** - * Gets the name of the SEO score. - * - * @return string The name of the SEO score. - */ - public function get_name(): string { - return 'good'; - } - - /** - * Gets the value of the SEO score that is used when filtering on the posts page. - * - * @return string The name of the SEO score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'good'; - } - - /** - * Gets the position of the SEO score. - * - * @return int The position of the SEO score. - */ - public function get_position(): int { - return 0; - } - - /** - * Gets the minimum score of the SEO score. - * - * @return int The minimum score of the SEO score. - */ - public function get_min_score(): ?int { - return 71; - } - - /** - * Gets the maximum score of the SEO score. - * - * @return int The maximum score of the SEO score. - */ - public function get_max_score(): ?int { - return 100; - } -} diff --git a/src/dashboard/domain/scores/seo-scores/no-seo-score.php b/src/dashboard/domain/scores/seo-scores/no-seo-score.php deleted file mode 100644 index 11d7b99bb13..00000000000 --- a/src/dashboard/domain/scores/seo-scores/no-seo-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -/** - * This class describes a missing SEO score. - */ -class No_SEO_Score extends Abstract_SEO_Score { - - /** - * Gets the name of the SEO score. - * - * @return string The name of the SEO score. - */ - public function get_name(): string { - return 'notAnalyzed'; - } - - /** - * Gets the value of the SEO score that is used when filtering on the posts page. - * - * @return string The name of the SEO score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'na'; - } - - /** - * Gets the position of the SEO score. - * - * @return int The position of the SEO score. - */ - public function get_position(): int { - return 3; - } - - /** - * Gets the minimum score of the SEO score. - * - * @return null The minimum score of the SEO score. - */ - public function get_min_score(): ?int { - return null; - } - - /** - * Gets the maximum score of the SEO score. - * - * @return null The maximum score of the SEO score. - */ - public function get_max_score(): ?int { - return null; - } -} diff --git a/src/dashboard/domain/scores/seo-scores/ok-seo-score.php b/src/dashboard/domain/scores/seo-scores/ok-seo-score.php deleted file mode 100644 index 692a40d696b..00000000000 --- a/src/dashboard/domain/scores/seo-scores/ok-seo-score.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. -// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -/** - * This class describes an OK SEO score. - */ -class Ok_SEO_Score extends Abstract_SEO_Score { - - /** - * Gets the name of the SEO score. - * - * @return string The the name of the SEO score. - */ - public function get_name(): string { - return 'ok'; - } - - /** - * Gets the value of the SEO score that is used when filtering on the posts page. - * - * @return string The name of the SEO score that is used when filtering on the posts page. - */ - public function get_filter_value(): string { - return 'ok'; - } - - /** - * Gets the position of the SEO score. - * - * @return int The position of the SEO score. - */ - public function get_position(): int { - return 1; - } - - /** - * Gets the minimum score of the SEO score. - * - * @return int The minimum score of the SEO score. - */ - public function get_min_score(): ?int { - return 41; - } - - /** - * Gets the maximum score of the SEO score. - * - * @return int The maximum score of the SEO score. - */ - public function get_max_score(): ?int { - return 70; - } -} diff --git a/src/dashboard/domain/scores/seo-scores/seo-scores-interface.php b/src/dashboard/domain/scores/seo-scores/seo-scores-interface.php deleted file mode 100644 index 91ec8e096af..00000000000 --- a/src/dashboard/domain/scores/seo-scores/seo-scores-interface.php +++ /dev/null @@ -1,10 +0,0 @@ -<?php -// phpcs:disable Yoast.NamingConventions.NamespaceName -namespace Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores; - -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; - -/** - * This interface describes an SEO score implementation. - */ -interface SEO_Scores_Interface extends Scores_Interface {} diff --git a/src/dashboard/infrastructure/scores/score-link-collector.php b/src/dashboard/infrastructure/score-groups/score-group-link-collector.php similarity index 55% rename from src/dashboard/infrastructure/scores/score-link-collector.php rename to src/dashboard/infrastructure/score-groups/score-group-link-collector.php index 0576811346e..8d4199e1050 100644 --- a/src/dashboard/infrastructure/scores/score-link-collector.php +++ b/src/dashboard/infrastructure/score-groups/score-group-link-collector.php @@ -1,33 +1,33 @@ <?php // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded -namespace Yoast\WP\SEO\Dashboard\Infrastructure\Scores; +namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; /** - * Getting links for scores. + * Getting links for score groups. */ -class Score_Link_Collector { +class Score_Group_Link_Collector { /** - * Builds the view link of the score. + * Builds the view link of the score group. * - * @param Scores_Interface $score_name The name of the score. - * @param Content_Type $content_type The content type. - * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. - * @param int|null $term_id The ID of the term we might be filtering. + * @param Score_Groups_Interface $score_group The score group. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering. + * @param int|null $term_id The ID of the term we might be filtering. * * @return string The view link of the score. */ - public function get_view_link( Scores_Interface $score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { + public function get_view_link( Score_Groups_Interface $score_group, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string { $posts_page = \admin_url( 'edit.php' ); $args = [ 'post_status' => 'publish', 'post_type' => $content_type->get_name(), - $score_name->get_filter_key() => $score_name->get_filter_value(), + $score_group->get_filter_key() => $score_group->get_filter_value(), ]; if ( $taxonomy === null || $term_id === null ) { diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index 4c1ee1e17b9..d8670798ceb 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -6,7 +6,7 @@ use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Readability_Scores\Readability_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; /** @@ -19,13 +19,13 @@ class Readability_Score_Results_Collector implements Score_Results_Collector_Int /** * Retrieves the current readability scores for a content type. * - * @param Readability_Scores_Interface[] $readability_scores All readability scores. - * @param Content_Type $content_type The content type. - * @param int|null $term_id The ID of the term we're filtering for. + * @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups. + * @param Content_Type $content_type The content type. + * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float> The current SEO scores for a content type. + * @return array<string, object|bool|float> The current readability scores for a content type. */ - public function get_current_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { + public function get_current_scores( array $readability_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; $results = []; @@ -41,7 +41,7 @@ public function get_current_scores( array $readability_scores, Content_Type $con return $results; } - $select = $this->build_select( $readability_scores ); + $select = $this->build_select( $readability_score_groups ); $replacements = \array_merge( \array_values( $select['replacements'] ), @@ -118,18 +118,18 @@ public function get_current_scores( array $readability_scores, Content_Type $con /** * Builds the select statement for the readability scores query. * - * @param Readability_Scores_Interface[] $readability_scores All readability scores. + * @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups. * * @return array<string, string> The select statement for the readability scores query. */ - private function build_select( array $readability_scores ): array { + private function build_select( array $readability_score_groups ): array { $select_fields = []; $select_replacements = []; - foreach ( $readability_scores as $readability_score ) { - $min = $readability_score->get_min_score(); - $max = $readability_score->get_max_score(); - $name = $readability_score->get_name(); + foreach ( $readability_score_groups as $readability_score_group ) { + $min = $readability_score_group->get_min_score(); + $max = $readability_score_group->get_max_score(); + $name = $readability_score_group->get_name(); if ( $min === null || $max === null ) { $select_fields[] = 'COUNT(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; diff --git a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php index 9cf76c9bcb1..07834c100be 100644 --- a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php +++ b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php @@ -4,7 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; /** * The interface of scores collectors. @@ -14,11 +14,11 @@ interface Score_Results_Collector_Interface { /** * Retrieves the current score results for a content type. * - * @param Scores_Interface[] $scores All scores. - * @param Content_Type $content_type The content type. - * @param int|null $term_id The ID of the term we're filtering for. + * @param Score_Groups_Interface[] $score_groups All score groups. + * @param Content_Type $content_type The content type. + * @param int|null $term_id The ID of the term we're filtering for. * * @return array<string, string> The current scores for a content type. */ - public function get_current_scores( array $scores, Content_Type $content_type, ?int $term_id ); + public function get_current_scores( array $score_groups, Content_Type $content_type, ?int $term_id ); } diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index 595f681b61c..c607f8268ac 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -6,7 +6,7 @@ use WPSEO_Utils; use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Scores\SEO_Scores\SEO_Scores_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; /** @@ -19,13 +19,13 @@ class SEO_Score_Results_Collector implements Score_Results_Collector_Interface { /** * Retrieves the current SEO scores for a content type. * - * @param SEO_Scores_Interface[] $seo_scores All SEO scores. - * @param Content_Type $content_type The content type. - * @param int|null $term_id The ID of the term we're filtering for. + * @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups. + * @param Content_Type $content_type The content type. + * @param int|null $term_id The ID of the term we're filtering for. * * @return array<string, object|bool|float> The current SEO scores for a content type. */ - public function get_current_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { + public function get_current_scores( array $seo_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; $results = []; @@ -41,7 +41,7 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ return $results; } - $select = $this->build_select( $seo_scores ); + $select = $this->build_select( $seo_score_groups ); $replacements = \array_merge( \array_values( $select['replacements'] ), @@ -120,18 +120,18 @@ public function get_current_scores( array $seo_scores, Content_Type $content_typ /** * Builds the select statement for the SEO scores query. * - * @param SEO_Scores_Interface[] $seo_scores All SEO scores. + * @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups. * * @return array<string, string> The select statement for the SEO scores query. */ - private function build_select( array $seo_scores ): array { + private function build_select( array $seo_score_groups ): array { $select_fields = []; $select_replacements = []; - foreach ( $seo_scores as $seo_score ) { - $min = $seo_score->get_min_score(); - $max = $seo_score->get_max_score(); - $name = $seo_score->get_name(); + foreach ( $seo_score_groups as $seo_score_group ) { + $min = $seo_score_group->get_min_score(); + $max = $seo_score_group->get_max_score(); + $name = $seo_score_group->get_name(); if ( $min === null || $max === null ) { $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; From 03b699f5b85800836e93edba1886b8f292a3056a Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 10:44:52 +0200 Subject: [PATCH 093/132] Rename collector method to reflect getting score results --- .../score-results/abstract-score-results-repository.php | 6 +++--- .../readability-score-results-collector.php | 6 +++--- .../score-results/score-results-collector-interface.php | 6 +++--- .../seo-score-results/seo-score-results-collector.php | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index 8567e44940f..eed4cb53fc2 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -64,7 +64,7 @@ public function set_score_group_link_collector( */ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $current_scores_list = new Current_Scores_List(); - $current_scores = $this->score_results_collector->get_current_scores( $this->score_groups, $content_type, $term_id ); + $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id ); foreach ( $this->score_groups as $score_group ) { $score_name = $score_group->get_name(); @@ -72,11 +72,11 @@ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxono 'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ), ]; - $current_score = new Current_Score( $score_name, (int) $current_scores['scores']->$score_name, $current_score_links ); + $current_score = new Current_Score( $score_name, (int) $score_results['scores']->$score_name, $current_score_links ); $current_scores_list->add( $current_score, $score_group->get_position() ); } - $score_result = new Score_Result( $current_scores_list, $current_scores['query_time'], $current_scores['cache_used'] ); + $score_result = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); return $score_result->to_array(); } diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index d8670798ceb..b950e44cfc5 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -17,15 +17,15 @@ class Readability_Score_Results_Collector implements Score_Results_Collector_Int public const READABILITY_SCORES_TRANSIENT = 'wpseo_readability_scores'; /** - * Retrieves the current readability scores for a content type. + * Retrieves readability score results for a content type. * * @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups. * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float> The current readability scores for a content type. + * @return array<string, object|bool|float> The readability score results for a content type. */ - public function get_current_scores( array $readability_score_groups, Content_Type $content_type, ?int $term_id ) { + public function get_score_results( array $readability_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; $results = []; diff --git a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php index 07834c100be..33d2d732ff3 100644 --- a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php +++ b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php @@ -12,13 +12,13 @@ interface Score_Results_Collector_Interface { /** - * Retrieves the current score results for a content type. + * Retrieves the score results for a content type. * * @param Score_Groups_Interface[] $score_groups All score groups. * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, string> The current scores for a content type. + * @return array<string, string> The score results for a content type. */ - public function get_current_scores( array $score_groups, Content_Type $content_type, ?int $term_id ); + public function get_score_results( array $score_groups, Content_Type $content_type, ?int $term_id ); } diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index c607f8268ac..34411e44d10 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -17,15 +17,15 @@ class SEO_Score_Results_Collector implements Score_Results_Collector_Interface { public const SEO_SCORES_TRANSIENT = 'wpseo_seo_scores'; /** - * Retrieves the current SEO scores for a content type. + * Retrieves the SEO score results for a content type. * * @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups. * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float> The current SEO scores for a content type. + * @return array<string, object|bool|float> The SEO score results for a content type. */ - public function get_current_scores( array $seo_score_groups, Content_Type $content_type, ?int $term_id ) { + public function get_score_results( array $seo_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; $results = []; From 01c04e3c3da748ea9203a8a89740a9820264f0e0 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 11:26:42 +0200 Subject: [PATCH 094/132] Dont add link if it's null --- src/dashboard/domain/score-results/current-score.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dashboard/domain/score-results/current-score.php b/src/dashboard/domain/score-results/current-score.php index 47a207805fc..169ca1daa9a 100644 --- a/src/dashboard/domain/score-results/current-score.php +++ b/src/dashboard/domain/score-results/current-score.php @@ -72,6 +72,9 @@ public function get_links_to_array(): ?array { } foreach ( $this->links as $key => $link ) { + if ( $link === null ) { + continue; + } $links[ $key ] = $link; } return $links; From 4aadc27e059c5f34438326fdc9b582d3e1175197 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 12:06:38 +0200 Subject: [PATCH 095/132] Update the frontend with the new response structure --- packages/js/src/dashboard/scores/components/scores.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/src/dashboard/scores/components/scores.js b/packages/js/src/dashboard/scores/components/scores.js index fb4d99157d6..683f8a8200f 100644 --- a/packages/js/src/dashboard/scores/components/scores.js +++ b/packages/js/src/dashboard/scores/components/scores.js @@ -94,6 +94,7 @@ export const Scores = ( { analysisType, contentTypes, endpoint, headers } ) => { }, }, fetchDelay: 0, + prepareData: ( data ) => data?.scores, } ); useEffect( () => { From 3d06b3fe2b8a41c16e2de5d7513f7989776dfb68 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 12:07:01 +0200 Subject: [PATCH 096/132] Clean up --- src/dashboard/domain/score-groups/abstract-score-group.php | 7 ------- .../score-results/score-results-collector-interface.php | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/dashboard/domain/score-groups/abstract-score-group.php b/src/dashboard/domain/score-groups/abstract-score-group.php index f961b0c1955..98e7dddac17 100644 --- a/src/dashboard/domain/score-groups/abstract-score-group.php +++ b/src/dashboard/domain/score-groups/abstract-score-group.php @@ -42,13 +42,6 @@ abstract class Abstract_Score_Group implements Score_Groups_Interface { */ private $max_score; - /** - * The view link of the score group. - * - * @var string - */ - private $view_link; - /** * The position of the score group. * diff --git a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php index 33d2d732ff3..8ac594168fc 100644 --- a/src/dashboard/infrastructure/score-results/score-results-collector-interface.php +++ b/src/dashboard/infrastructure/score-results/score-results-collector-interface.php @@ -7,7 +7,7 @@ use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; /** - * The interface of scores collectors. + * The interface of score result collectors. */ interface Score_Results_Collector_Interface { From 71329f78cef490c33563866e337dbc38b6c6a934 Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Tue, 26 Nov 2024 11:40:59 +0100 Subject: [PATCH 097/132] Update filters. --- admin/class-meta-columns.php | 47 +++++++++++++++++++++++++++- tests/WP/Admin/Meta_Columns_Test.php | 29 ++++++++++------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 1dd19e9d6b0..988bebcb10c 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -356,7 +356,10 @@ protected function determine_seo_filters( $seo_filter ) { */ protected function determine_readability_filters( $readability_filter ) { if ( $readability_filter === WPSEO_Rank::NO_FOCUS ) { - return $this->create_no_focus_keyword_filter(); + return $this->create_no_readability_scores_filter(); + } + if ( $readability_filter === WPSEO_Rank::BAD ) { + return $this->create_bad_readability_scores_filter(); } $rank = new WPSEO_Rank( $readability_filter ); @@ -594,6 +597,7 @@ protected function build_filter_query( $vars, $filters ) { $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] ); } + return array_merge( $vars, $result ); } @@ -669,6 +673,47 @@ protected function create_no_focus_keyword_filter() { ], ]; } + /** + * Creates a filter to retrieve posts that have no keyword set. + * + * @return array Array containing the no focus keyword filter. + */ + protected function create_no_readability_scores_filter() { + return [ + [ + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ]; + } + /** + * Creates a filter to retrieve posts that have no keyword set. + * + * @return array Array containing the no focus keyword filter. + */ + protected function create_bad_readability_scores_filter() { + return [ + [ + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', + 'compare' => 'EXISTS', + ], + [ + 'relation' => 'OR', + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 41, + 'type' => 'numeric', + 'compare' => '<', + ],[ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ] + ] + + ]; + } /** * Determines whether a particular post_id is of an indexable post type. diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 63c067388f6..9d1c99b642b 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -132,12 +132,24 @@ public static function determine_readability_filters_dataprovider() { 'bad', [ [ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => [ 1, 40 ], - 'type' => 'numeric', - 'compare' => 'BETWEEN', + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', + 'compare' => 'EXISTS', ], - ], + [ + 'relation' => 'OR', + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 41, + 'type' => 'numeric', + 'compare' => '<', + ],[ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ] + ] + + ] ], [ 'ok', @@ -165,12 +177,7 @@ public static function determine_readability_filters_dataprovider() { 'na', [ [ - 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', - 'value' => 'needs-a-value-anyway', - 'compare' => 'NOT EXISTS', - ], - [ - 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], From ed4033a1b66a47f642ccc8ebcdf5172d9ed5a0ce Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Tue, 26 Nov 2024 11:45:03 +0100 Subject: [PATCH 098/132] cs --- admin/class-meta-columns.php | 38 +++++++++++++++------------- composer.json | 2 +- tests/WP/Admin/Meta_Columns_Test.php | 15 ++++++----- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 988bebcb10c..2af5e29619b 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -597,7 +597,6 @@ protected function build_filter_query( $vars, $filters ) { $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] ); } - return array_merge( $vars, $result ); } @@ -607,7 +606,7 @@ protected function build_filter_query( $vars, $filters ) { * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * - * @return array The Readability Score filter. + * @return array<array<string>> The Readability Score filter. */ protected function create_readability_score_filter( $low, $high ) { return [ @@ -626,7 +625,7 @@ protected function create_readability_score_filter( $low, $high ) { * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * - * @return array The SEO score filter. + * @return array<array<string>> The SEO score filter. */ protected function create_seo_score_filter( $low, $high ) { return [ @@ -642,7 +641,7 @@ protected function create_seo_score_filter( $low, $high ) { /** * Creates a filter to retrieve posts that were set to no-index. * - * @return array Array containin the no-index filter. + * @return array<array<string>> Array containin the no-index filter. */ protected function create_no_index_filter() { return [ @@ -657,7 +656,7 @@ protected function create_no_index_filter() { /** * Creates a filter to retrieve posts that have no keyword set. * - * @return array Array containing the no focus keyword filter. + * @return array<array<string>> Array containing the no focus keyword filter. */ protected function create_no_focus_keyword_filter() { return [ @@ -673,10 +672,11 @@ protected function create_no_focus_keyword_filter() { ], ]; } + /** * Creates a filter to retrieve posts that have no keyword set. * - * @return array Array containing the no focus keyword filter. + * @return array<array<string>> Array containing the no focus keyword filter. */ protected function create_no_readability_scores_filter() { return [ @@ -687,10 +687,11 @@ protected function create_no_readability_scores_filter() { ], ]; } + /** * Creates a filter to retrieve posts that have no keyword set. * - * @return array Array containing the no focus keyword filter. + * @return array<array<string>> Array containing the no focus keyword filter. */ protected function create_bad_readability_scores_filter() { return [ @@ -701,16 +702,17 @@ protected function create_bad_readability_scores_filter() { [ 'relation' => 'OR', [ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 41, - 'type' => 'numeric', - 'compare' => '<', - ],[ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 'needs-a-value-anyway', - 'compare' => 'NOT EXISTS', - ] - ] + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 41, + 'type' => 'numeric', + 'compare' => '<', + ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ], ]; } @@ -753,7 +755,7 @@ protected function uses_default_indexing( $post_id ) { * * @param string $order_by The ID of the column by which to order the posts. * - * @return array Array containing the order filters. + * @return array<string> Array containing the order filters. */ private function filter_order_by( $order_by ) { switch ( $order_by ) { diff --git a/composer.json b/composer.json index bddc56eb708..e6ded9d63d6 100644 --- a/composer.json +++ b/composer.json @@ -91,7 +91,7 @@ "Yoast\\WP\\SEO\\Composer\\Actions::check_coding_standards" ], "check-cs-thresholds": [ - "@putenv YOASTCS_THRESHOLD_ERRORS=2479", + "@putenv YOASTCS_THRESHOLD_ERRORS=2474", "@putenv YOASTCS_THRESHOLD_WARNINGS=252", "Yoast\\WP\\SEO\\Composer\\Actions::check_cs_thresholds" ], diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 9d1c99b642b..4d233d68d06 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -142,14 +142,15 @@ public static function determine_readability_filters_dataprovider() { 'value' => 41, 'type' => 'numeric', 'compare' => '<', - ],[ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 'needs-a-value-anyway', - 'compare' => 'NOT EXISTS', - ] - ] + ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ], - ] + ], ], [ 'ok', From 6ad6444829ce3cc126ac58752f843c8b08b62d44 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 12:45:19 +0200 Subject: [PATCH 099/132] Refactor by creating a separate repository for current scores --- .../abstract-score-results-repository.php | 38 +++------- .../current-scores-repository.php | 76 +++++++++++++++++++ 2 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 src/dashboard/application/score-results/current-scores-repository.php diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index eed4cb53fc2..b241f94a33d 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -5,11 +5,8 @@ use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; -use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Score; -use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Scores_List; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Result; use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; -use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups\Score_Group_Link_Collector; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; /** @@ -24,13 +21,6 @@ abstract class Abstract_Score_Results_Repository { */ protected $score_results_collector; - /** - * The score group link collector. - * - * @var Score_Group_Link_Collector - */ - protected $score_group_link_collector; - /** * All score groups. * @@ -39,18 +29,18 @@ abstract class Abstract_Score_Results_Repository { protected $score_groups; /** - * Sets the score link collector. + * Sets the repositories. * * @required * - * @param Score_Group_Link_Collector $score_group_link_collector The score group link collector. + * @param Current_Scores_Repository $current_scores_repository The current scores repository. * * @return void */ - public function set_score_group_link_collector( - Score_Group_Link_Collector $score_group_link_collector + public function set_repositories( + Current_Scores_Repository $current_scores_repository ) { - $this->score_group_link_collector = $score_group_link_collector; + $this->current_scores_repository = $current_scores_repository; } /** @@ -63,21 +53,11 @@ public function set_score_group_link_collector( * @return array<array<string, string|int|array<string, string>>> The scores. */ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { - $current_scores_list = new Current_Scores_List(); - $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id ); - - foreach ( $this->score_groups as $score_group ) { - $score_name = $score_group->get_name(); - $current_score_links = [ - 'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ), - ]; - - $current_score = new Current_Score( $score_name, (int) $score_results['scores']->$score_name, $current_score_links ); - $current_scores_list->add( $current_score, $score_group->get_position() ); - } + $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id ); - $score_result = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); + $current_scores_list = $this->current_scores_repository->get_current_scores( $this->score_groups, $score_results, $content_type, $taxonomy, $term_id ); + $score_result_object = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); - return $score_result->to_array(); + return $score_result_object->to_array(); } } diff --git a/src/dashboard/application/score-results/current-scores-repository.php b/src/dashboard/application/score-results/current-scores-repository.php new file mode 100644 index 00000000000..8475023ba42 --- /dev/null +++ b/src/dashboard/application/score-results/current-scores-repository.php @@ -0,0 +1,76 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong +// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded +namespace Yoast\WP\SEO\Dashboard\Application\Score_Results; + +use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; +use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Score; +use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Scores_List; +use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy; +use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups\Score_Group_Link_Collector; + +/** + * The current scores repository. + */ +class Current_Scores_Repository { + + /** + * The score group link collector. + * + * @var Score_Group_Link_Collector + */ + protected $score_group_link_collector; + + /** + * The constructor. + * + * @param Score_Group_Link_Collector $score_group_link_collector The score group link collector. + */ + public function __construct( + Score_Group_Link_Collector $score_group_link_collector + ) { + $this->score_group_link_collector = $score_group_link_collector; + } + + /** + * Returns the current results. + * + * @param Score_Groups_Interface[] $score_groups The score groups. + * @param array<string, string> $score_results The score results. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. + * + * @return array<array<string, string|int|array<string, string>>> The current results. + */ + public function get_current_scores( array $score_groups, array $score_results, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): Current_Scores_List { + $current_scores_list = new Current_Scores_List(); + + foreach ( $score_groups as $score_group ) { + $score_name = $score_group->get_name(); + $current_score_links = $this->get_current_score_links( $score_group, $content_type, $taxonomy, $term_id ); + + $current_score = new Current_Score( $score_name, (int) $score_results['scores']->$score_name, $current_score_links ); + $current_scores_list->add( $current_score, $score_group->get_position() ); + } + + return $current_scores_list; + } + + /** + * Returns the links for the current scores of a score group. + * + * @param Score_Groups_Interface $score_group The scoure group. + * @param Content_Type $content_type The content type. + * @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for. + * @param int|null $term_id The ID of the term we're filtering for. + * + * @return array<string,string> The current score links. + */ + protected function get_current_score_links( Score_Groups_Interface $score_group, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + return [ + 'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ), + ]; + } +} From a94b9db5b8d81ed63d0238df88de81999954ab99 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 12:48:55 +0200 Subject: [PATCH 100/132] Fix for PHP 8.2 --- .../score-results/abstract-score-results-repository.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index b241f94a33d..ecd7055408a 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -21,6 +21,13 @@ abstract class Abstract_Score_Results_Repository { */ protected $score_results_collector; + /** + * The current scores repository. + * + * @var Current_Scores_Repository + */ + protected $current_scores_repository; + /** * All score groups. * From 259ca96b794d575fbab629b3235c1f2e1a33cc72 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 15:41:32 +0200 Subject: [PATCH 101/132] Fix request URL when there's already query parameters in the initial endpoint --- .../src/dashboard/scores/components/scores.js | 7 +++++++ .../dashboard/scores/components/term-filter.js | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/scores.js b/packages/js/src/dashboard/scores/components/scores.js index fb4d99157d6..344976c5f57 100644 --- a/packages/js/src/dashboard/scores/components/scores.js +++ b/packages/js/src/dashboard/scores/components/scores.js @@ -21,11 +21,18 @@ import { TermFilter } from "./term-filter"; * @returns {URL} The URL to get scores. */ const createScoresUrl = ( endpoint, contentType, term ) => { + const existingParams = new URL( endpoint ).searchParams; + const searchParams = new URLSearchParams( { contentType: contentType.name } ); if ( contentType.taxonomy?.name && term?.name ) { searchParams.set( "taxonomy", contentType.taxonomy.name ); searchParams.set( "term", term.name ); } + + if ( existingParams.size !== 0 ) { + return new URL( "?" + existingParams + "&" + searchParams, endpoint ); + } + return new URL( "?" + searchParams, endpoint ); }; diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 5a6703378fc..c3cc9934d02 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -13,10 +13,20 @@ import { useFetch } from "../../fetch/use-fetch"; * @param {string} query The query. * @returns {URL} The URL to query for the terms. */ -const createQueryUrl = ( endpoint, query ) => new URL( "?" + new URLSearchParams( { - search: query, - _fields: [ "id", "name" ], -} ), endpoint ); +const createQueryUrl = ( endpoint, query ) => { + const existingParams = new URL( endpoint ).searchParams; + if ( existingParams.size !== 0 ) { + return new URL( "?" + existingParams + "&" + new URLSearchParams( { + search: query, + _fields: [ "id", "name" ], + } ), endpoint ); + } + + return new URL( "?" + new URLSearchParams( { + search: query, + _fields: [ "id", "name" ], + } ), endpoint ); +}; /** * @param {{id: number, name: string}} term The term from the response. From aa7eb8b1a20bd011fb29c2615dfe816e4ef403d7 Mon Sep 17 00:00:00 2001 From: Thijs van der Heijden <thijsoo@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:50:27 +0100 Subject: [PATCH 102/132] Apply suggestions from code review Co-authored-by: Leonidas Milosis <leonidas.milossis@gmail.com> --- admin/class-meta-columns.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 2af5e29619b..ed2fd946f67 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -674,9 +674,9 @@ protected function create_no_focus_keyword_filter() { } /** - * Creates a filter to retrieve posts that have no keyword set. + * Creates a filter to retrieve posts that have not been analyzed for readability yet.. * - * @return array<array<string>> Array containing the no focus keyword filter. + * @return array<array<string>> Array containing the no readability filter. */ protected function create_no_readability_scores_filter() { return [ @@ -689,9 +689,9 @@ protected function create_no_readability_scores_filter() { } /** - * Creates a filter to retrieve posts that have no keyword set. + * Creates a filter to retrieve posts that have bad readability scores, including those that have not enough content to have one. * - * @return array<array<string>> Array containing the no focus keyword filter. + * @return array<array<string>> Array containing the bad readability filter. */ protected function create_bad_readability_scores_filter() { return [ From 34f946ef5d6c4458804e6ea9f5e330be3bbce51c Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Tue, 26 Nov 2024 15:06:26 +0100 Subject: [PATCH 103/132] Use Rank class to make sure there is no magic number. --- admin/class-meta-columns.php | 5 +++-- tests/WP/Admin/Meta_Columns_Test.php | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index ed2fd946f67..4119b5c13bf 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -694,6 +694,7 @@ protected function create_no_readability_scores_filter() { * @return array<array<string>> Array containing the bad readability filter. */ protected function create_bad_readability_scores_filter() { + $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', @@ -703,9 +704,9 @@ protected function create_bad_readability_scores_filter() { 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 41, + 'value' => $rank->get_end_score(), 'type' => 'numeric', - 'compare' => '<', + 'compare' => '<=', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 4d233d68d06..a2a14e8a78b 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -139,9 +139,9 @@ public static function determine_readability_filters_dataprovider() { 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 41, + 'value' => 40, 'type' => 'numeric', - 'compare' => '<', + 'compare' => '<=', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', From edefefe6ea34e66ddb2ead48ae10717490e1376f Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 26 Nov 2024 16:43:29 +0200 Subject: [PATCH 104/132] Simplify solution --- .../js/src/dashboard/scores/components/scores.js | 15 ++++++--------- .../dashboard/scores/components/term-filter.js | 16 +++++----------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/scores.js b/packages/js/src/dashboard/scores/components/scores.js index 344976c5f57..e4717d42454 100644 --- a/packages/js/src/dashboard/scores/components/scores.js +++ b/packages/js/src/dashboard/scores/components/scores.js @@ -21,19 +21,16 @@ import { TermFilter } from "./term-filter"; * @returns {URL} The URL to get scores. */ const createScoresUrl = ( endpoint, contentType, term ) => { - const existingParams = new URL( endpoint ).searchParams; + const url = new URL( endpoint ); - const searchParams = new URLSearchParams( { contentType: contentType.name } ); - if ( contentType.taxonomy?.name && term?.name ) { - searchParams.set( "taxonomy", contentType.taxonomy.name ); - searchParams.set( "term", term.name ); - } + url.searchParams.set( "contentType", contentType.name ); - if ( existingParams.size !== 0 ) { - return new URL( "?" + existingParams + "&" + searchParams, endpoint ); + if ( contentType.taxonomy?.name && term?.name ) { + url.searchParams.set( "taxonomy", contentType.taxonomy.name ); + url.searchParams.set( "term", term.name ); } - return new URL( "?" + searchParams, endpoint ); + return url; }; // Added dummy space as content to prevent children prop warnings in the console. diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index c3cc9934d02..1fda3dff9e3 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -14,18 +14,12 @@ import { useFetch } from "../../fetch/use-fetch"; * @returns {URL} The URL to query for the terms. */ const createQueryUrl = ( endpoint, query ) => { - const existingParams = new URL( endpoint ).searchParams; - if ( existingParams.size !== 0 ) { - return new URL( "?" + existingParams + "&" + new URLSearchParams( { - search: query, - _fields: [ "id", "name" ], - } ), endpoint ); - } + const url = new URL( endpoint ); - return new URL( "?" + new URLSearchParams( { - search: query, - _fields: [ "id", "name" ], - } ), endpoint ); + url.searchParams.set( "search", query ); + url.searchParams.set( "_fields", [ "id", "name" ] ); + + return url; }; /** From 99cee6c5987e3b06ff5dddc0e5f24b7b596bea33 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Wed, 27 Nov 2024 10:26:35 +0200 Subject: [PATCH 105/132] Add comment explaining our decision to use the ERT in the readability filter --- admin/class-meta-columns.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 4119b5c13bf..b5cfc4c0941 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -674,11 +674,13 @@ protected function create_no_focus_keyword_filter() { } /** - * Creates a filter to retrieve posts that have not been analyzed for readability yet.. + * Creates a filter to retrieve posts that have not been analyzed for readability yet. * * @return array<array<string>> Array containing the no readability filter. */ protected function create_no_readability_scores_filter() { + // We check the existence of the Estimated Reading Time, because readability scores of posts that haven't been manually saved while Yoast SEO is 0, which is the same score as for posts with not enough content. + // Meanwhile, the ERT is a solid indicator of whether a post has ever been saved (aka, analyzed), so we're using that. return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', From 08a76147e3a6b530725411dcd833a855df38eba0 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:37:40 +0100 Subject: [PATCH 106/132] Add tooltip to not analyzed --- .../dashboard/scores/components/score-list.js | 44 +++++++++++++------ .../js/src/dashboard/scores/score-meta.js | 3 +- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index 02edf077354..efdb2680fa2 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,4 +1,4 @@ -import { Badge, Button, Label, SkeletonLoader } from "@yoast/ui-library"; +import { Badge, Button, Label, SkeletonLoader, Tooltip, useToggleState } from "@yoast/ui-library"; import classNames from "classnames"; import { SCORE_META } from "../score-meta"; @@ -35,6 +35,34 @@ export const ScoreListSkeletonLoader = ( { className } ) => ( </ul> ); +/** + * @param {Score} score The score. + * @returns {JSX.Element} The element. + */ +const ScoreListItem = ( { score } ) => { + const [ isVisible, , , show, hide ] = useToggleState( false ); + // eslint-disable-next-line no-undefined + const tooltipId = SCORE_META[ score.name ].tooltip ? `tooltip__${ score.name }` : undefined; + + return ( + <li className={ CLASSNAMES.listItem }> + <span className="yst-relative yst-flex yst-items-center" onMouseEnter={ show } onMouseLeave={ hide } aria-describedby={ tooltipId }> + <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> + <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }> + { SCORE_META[ score.name ].label } + </Label> + <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> + { SCORE_META[ score.name ].tooltip && isVisible && ( + <Tooltip id={ tooltipId }>{ SCORE_META[ score.name ].tooltip }</Tooltip> + ) } + </span> + { score.links.view && ( + <Button as="a" variant="secondary" size="small" href={ score.links.view } className="yst-ml-auto">View</Button> + ) } + </li> + ); +}; + /** * @param {string} [className] The class name for the UL. * @param {Score[]} scores The scores. @@ -42,18 +70,6 @@ export const ScoreListSkeletonLoader = ( { className } ) => ( */ export const ScoreList = ( { className, scores } ) => ( <ul className={ className }> - { scores.map( ( score ) => ( - <li - key={ score.name } - className={ CLASSNAMES.listItem } - > - <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> - <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }>{ SCORE_META[ score.name ].label }</Label> - <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> - { score.links.view && ( - <Button as="a" variant="secondary" size="small" href={ score.links.view } className="yst-ml-auto">View</Button> - ) } - </li> - ) ) } + { scores.map( ( score ) => <ScoreListItem key={ score.name } score={ score } /> ) } </ul> ); diff --git a/packages/js/src/dashboard/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js index 9ab0009a561..c3a8d8583d4 100644 --- a/packages/js/src/dashboard/scores/score-meta.js +++ b/packages/js/src/dashboard/scores/score-meta.js @@ -6,7 +6,7 @@ import { __ } from "@wordpress/i18n"; */ /** - * @type {Object.<ScoreType,{label: string, color: string, hex: string}>} The meta data. + * @type {Object.<ScoreType,{label: string, color: string, hex: string, tooltip?: string}>} The meta data. */ export const SCORE_META = { good: { @@ -28,6 +28,7 @@ export const SCORE_META = { label: __( "Not analyzed", "wordpress-seo" ), color: "yst-bg-analysis-na", hex: "#cbd5e1", + tooltip: __( "We haven’t analyzed this content yet. Please open it and save it in your editor so we can start the analysis.", "wordpress-seo" ), }, }; From f946277b0ecb8abe54fb3d9d1a9fcd11a7b9ac05 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:41:47 +0100 Subject: [PATCH 107/132] Improve a11y of the tooltip See: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/tooltip_role * add aria-describedby that needs an ID * use button as a focusable element, this is a hacky solution * add aria-disabled to the button to indicate that it does nothing * show the tooltip on focus and hover * hide the tooltip on Escape press * move the tooltip out of the button so it does not influence the content (an alternative, less ideal would be to add aria-hidden) * add a wrapper to still get the tooltip positioning with relative * use focus-visible so the keyboard user still sees the focus Missing: adjustment of the tooltip display styling. As that is inside the UI library, more commits incoming to try to move this solution there. --- .../scores/components/score-content.js | 5 +- .../dashboard/scores/components/score-list.js | 70 +++++++++++++++---- .../src/dashboard/scores/components/scores.js | 7 +- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-content.js b/packages/js/src/dashboard/scores/components/score-content.js index 02a3c125425..d6ee49381e4 100644 --- a/packages/js/src/dashboard/scores/components/score-content.js +++ b/packages/js/src/dashboard/scores/components/score-content.js @@ -35,9 +35,10 @@ const ScoreContentSkeletonLoader = () => ( * @param {Score[]} [scores=[]] The scores. * @param {boolean} isLoading Whether the scores are still loading. * @param {Object.<ScoreType,string>} descriptions The descriptions. + * @param {string} idSuffix The suffix for the IDs. * @returns {JSX.Element} The element. */ -export const ScoreContent = ( { scores = [], isLoading, descriptions } ) => { +export const ScoreContent = ( { scores = [], isLoading, descriptions, idSuffix } ) => { if ( isLoading ) { return <ScoreContentSkeletonLoader />; } @@ -46,7 +47,7 @@ export const ScoreContent = ( { scores = [], isLoading, descriptions } ) => { <> <ContentStatusDescription scores={ scores } descriptions={ descriptions } /> <div className={ CLASSNAMES.container }> - { scores && <ScoreList className={ CLASSNAMES.list } scores={ scores } /> } + { scores && <ScoreList className={ CLASSNAMES.list } scores={ scores } idSuffix={ idSuffix } /> } { scores && <ScoreChart className={ CLASSNAMES.chart } scores={ scores } /> } </div> </> diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index efdb2680fa2..94723857a05 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,3 +1,4 @@ +import { useEffect, useMemo, useRef } from "@wordpress/element"; import { Badge, Button, Label, SkeletonLoader, Tooltip, useToggleState } from "@yoast/ui-library"; import classNames from "classnames"; import { SCORE_META } from "../score-meta"; @@ -35,27 +36,67 @@ export const ScoreListSkeletonLoader = ( { className } ) => ( </ul> ); +/** + * @param {EventListener} onKeydown The keydown event listener. + * @returns {void} + */ +const useKeydown = ( onKeydown ) => { + useEffect( () => { + document.addEventListener( "keydown", onKeydown ); + return () => { + document.removeEventListener( "keydown", onKeydown ); + }; + }, [ onKeydown ] ); +}; + /** * @param {Score} score The score. + * @param {string} idSuffix The suffix for the IDs. * @returns {JSX.Element} The element. */ -const ScoreListItem = ( { score } ) => { +const ScoreListItem = ( { score, idSuffix } ) => { const [ isVisible, , , show, hide ] = useToggleState( false ); - // eslint-disable-next-line no-undefined - const tooltipId = SCORE_META[ score.name ].tooltip ? `tooltip__${ score.name }` : undefined; + const ref = useRef(); + + const TooltipTrigger = SCORE_META[ score.name ].tooltip ? "button" : "span"; + const tooltipTriggerProps = useMemo( () => SCORE_META[ score.name ].tooltip ? { + onFocus: show, + onMouseEnter: show, + "aria-describedby": `tooltip--${ idSuffix }__${ score.name }`, + "aria-disabled": true, + className: "yst-rounded-md yst-cursor-default focus:yst-outline-none focus-visible:yst-ring-primary-500 focus-visible:yst-border-primary-500 focus-visible:yst-ring-2 focus-visible:yst-ring-offset-2 focus-visible:yst-bg-white focus-visible:yst-border-opacity-0", + } : {}, [ show ] ); + + useKeydown( ( event ) => { + if ( event.key === "Escape" ) { + hide(); + ref.current?.blur(); + } + } ); return ( <li className={ CLASSNAMES.listItem }> - <span className="yst-relative yst-flex yst-items-center" onMouseEnter={ show } onMouseLeave={ hide } aria-describedby={ tooltipId }> - <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> - <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }> - { SCORE_META[ score.name ].label } - </Label> - <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> - { SCORE_META[ score.name ].tooltip && isVisible && ( - <Tooltip id={ tooltipId }>{ SCORE_META[ score.name ].tooltip }</Tooltip> + <div className="yst-tooltip-container"> + <TooltipTrigger + ref={ ref } + { ...tooltipTriggerProps } + className={ classNames( tooltipTriggerProps.className, "yst-flex yst-items-center" ) } + > + <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> + <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }> + { SCORE_META[ score.name ].label } + </Label> + <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> + </TooltipTrigger> + { SCORE_META[ score.name ].tooltip && ( + <Tooltip + id={ tooltipTriggerProps[ "aria-describedby" ] } + className={ classNames( ! isVisible && "yst-hidden" ) } + > + { SCORE_META[ score.name ].tooltip } + </Tooltip> ) } - </span> + </div> { score.links.view && ( <Button as="a" variant="secondary" size="small" href={ score.links.view } className="yst-ml-auto">View</Button> ) } @@ -66,10 +107,11 @@ const ScoreListItem = ( { score } ) => { /** * @param {string} [className] The class name for the UL. * @param {Score[]} scores The scores. + * @param {string} idSuffix The suffix for the IDs. * @returns {JSX.Element} The element. */ -export const ScoreList = ( { className, scores } ) => ( +export const ScoreList = ( { className, scores, idSuffix } ) => ( <ul className={ className }> - { scores.map( ( score ) => <ScoreListItem key={ score.name } score={ score } /> ) } + { scores.map( ( score ) => <ScoreListItem key={ score.name } score={ score } idSuffix={ idSuffix } /> ) } </ul> ); diff --git a/packages/js/src/dashboard/scores/components/scores.js b/packages/js/src/dashboard/scores/components/scores.js index f05cc017773..0fe90097617 100644 --- a/packages/js/src/dashboard/scores/components/scores.js +++ b/packages/js/src/dashboard/scores/components/scores.js @@ -133,7 +133,12 @@ export const Scores = ( { analysisType, contentTypes, endpoint, headers } ) => { <div className="yst-mt-6"> <ErrorAlert error={ error } /> { ! error && ( - <ScoreContent scores={ scores } isLoading={ isPending } descriptions={ SCORE_DESCRIPTIONS[ analysisType ] } /> + <ScoreContent + scores={ scores } + isLoading={ isPending } + descriptions={ SCORE_DESCRIPTIONS[ analysisType ] } + idSuffix={ analysisType } + /> ) } </div> </Paper> From d48f398a90e8998664bcce5ac6011c0b34e20fb1 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Wed, 27 Nov 2024 12:02:14 +0200 Subject: [PATCH 108/132] Return 500 error code when query fails --- .../abstract-score-results-repository.php | 7 ++ .../readability-score-results-collector.php | 67 +++++++++--------- .../seo-score-results-collector.php | 69 +++++++++---------- .../scores/abstract-scores-route.php | 4 +- 4 files changed, 73 insertions(+), 74 deletions(-) diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index ecd7055408a..0a09cc74025 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -3,6 +3,7 @@ // phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded namespace Yoast\WP\SEO\Dashboard\Application\Score_Results; +use Exception; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface; use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Result; @@ -58,10 +59,16 @@ public function set_repositories( * @param int|null $term_id The ID of the term we're filtering for. * * @return array<array<string, string|int|array<string, string>>> The scores. + * + * @throws Exception When getting score results from the infrastructure fails. */ public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id ); + if ( $score_results === null ) { + throw new Exception( 'Error getting score results', 500 ); + } + $current_scores_list = $this->current_scores_repository->get_current_scores( $this->score_groups, $score_results, $content_type, $taxonomy, $term_id ); $score_result_object = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index b950e44cfc5..23695b59cb4 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -23,7 +23,7 @@ class Readability_Score_Results_Collector implements Score_Results_Collector_Int * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float> The readability score results for a content type. + * @return array<string, object|bool|float>|null The readability score results for a content type, null for failure. */ public function get_score_results( array $readability_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; @@ -52,44 +52,26 @@ public function get_score_results( array $readability_score_groups, Content_Type ); if ( $term_id === null ) { - $start_time = \microtime( true ); - //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $current_scores = $wpdb->get_row( - $wpdb->prepare( - " - SELECT {$select['fields']} - FROM %i AS I - WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type = 'post' - AND I.object_sub_type = %s", - $replacements - ) + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + $query = $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type = 'post' + AND I.object_sub_type = %s", + $replacements ); //phpcs:enable - $end_time = \microtime( true ); - - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); - - $results['scores'] = $current_scores; - $results['cache_used'] = false; - $results['query_time'] = ( $end_time - $start_time ); - return $results; - } + else { + $replacements[] = $wpdb->term_relationships; + $replacements[] = $term_id; - $replacements[] = $wpdb->term_relationships; - $replacements[] = $term_id; - - $start_time = \microtime( true ); - //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. - //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $current_scores = $wpdb->get_row( - $wpdb->prepare( + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I @@ -102,9 +84,22 @@ public function get_score_results( array $readability_score_groups, Content_Type WHERE term_taxonomy_id = %d )", $replacements - ) - ); + ); + //phpcs:enable + } + + $start_time = \microtime( true ); + + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( $query ); //phpcs:enable + + if ( $current_scores === null ) { + return null; + } + $end_time = \microtime( true ); \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index 34411e44d10..51aaee73b3f 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -23,7 +23,7 @@ class SEO_Score_Results_Collector implements Score_Results_Collector_Interface { * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float> The SEO score results for a content type. + * @return array<string, object|bool|float>|null The SEO score results for a content type, null for failure. */ public function get_score_results( array $seo_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; @@ -52,45 +52,27 @@ public function get_score_results( array $seo_score_groups, Content_Type $conten ); if ( $term_id === null ) { - $start_time = \microtime( true ); - //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $current_scores = $wpdb->get_row( - $wpdb->prepare( - " - SELECT {$select['fields']} - FROM %i AS I - WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) - AND I.object_type = 'post' - AND I.object_sub_type = %s - AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", - $replacements - ) + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + $query = $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type = 'post' + AND I.object_sub_type = %s + AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", + $replacements ); //phpcs:enable - $end_time = \microtime( true ); - - \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); - - $results['scores'] = $current_scores; - $results['cache_used'] = false; - $results['query_time'] = ( $end_time - $start_time ); - return $results; - } + else { + $replacements[] = $wpdb->term_relationships; + $replacements[] = $term_id; - $replacements[] = $wpdb->term_relationships; - $replacements[] = $term_id; - - $start_time = \microtime( true ); - //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. - //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. - //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. - $current_scores = $wpdb->get_row( - $wpdb->prepare( + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + $query = $wpdb->prepare( " SELECT {$select['fields']} FROM %i AS I @@ -104,9 +86,22 @@ public function get_score_results( array $seo_score_groups, Content_Type $conten WHERE term_taxonomy_id = %d )", $replacements - ) - ); + ); + //phpcs:enable + } + + $start_time = \microtime( true ); + + //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( $query ); //phpcs:enable + + if ( $current_scores === null ) { + return null; + } + $end_time = \microtime( true ); \set_transient( $transient_name, WPSEO_Utils::format_json_encode( $current_scores ), \MINUTE_IN_SECONDS ); diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php index e711003bc93..dd981357a2b 100644 --- a/src/dashboard/user-interface/scores/abstract-scores-route.php +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -168,6 +168,8 @@ public function get_scores( WP_REST_Request $request ) { $content_type = $this->get_content_type( $request['contentType'] ); $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); $term_id = $this->get_validated_term_id( $request['term'], $taxonomy ); + + $results = $this->score_results_repository->get_score_results( $content_type, $taxonomy, $term_id ); } catch ( Exception $exception ) { return new WP_REST_Response( [ @@ -178,7 +180,7 @@ public function get_scores( WP_REST_Request $request ) { } return new WP_REST_Response( - $this->score_results_repository->get_score_results( $content_type, $taxonomy, $term_id ), + $results, 200 ); } From 1dd4451ed6c40ebc2de41429fbbeb72d8aa0d93e Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Wed, 27 Nov 2024 14:21:04 +0100 Subject: [PATCH 109/132] Use `get_indexable_post_type_objects` instead of `get_indexable_post_types` --- .../content-types/content-types-collector.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php index 02b5af03139..1f3bd64fc35 100644 --- a/src/dashboard/infrastructure/content-types/content-types-collector.php +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -6,6 +6,7 @@ use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Types_List; use Yoast\WP\SEO\Helpers\Post_Type_Helper; + /** * Class that collects post types and relevant information. */ @@ -36,11 +37,9 @@ public function __construct( */ public function get_content_types(): Content_Types_List { $content_types_list = new Content_Types_List(); - $post_types = $this->post_type_helper->get_indexable_post_types(); - - foreach ( $post_types as $post_type ) { - $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. + $post_types = $this->post_type_helper->get_indexable_post_type_objects(); + foreach ( $post_types as $post_type_object ) { $content_type = new Content_Type( $post_type_object->name, $post_type_object->label ); $content_types_list->add( $content_type ); } From 700663df83a660828d7f7d98526161579123b48d Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:56:02 +0100 Subject: [PATCH 110/132] UI library: add useKeydown hook --- packages/ui-library/src/hooks/index.js | 1 + packages/ui-library/src/hooks/readme.md | 24 ++++++++++++++++++++ packages/ui-library/src/hooks/use-keydown.js | 15 ++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 packages/ui-library/src/hooks/use-keydown.js diff --git a/packages/ui-library/src/hooks/index.js b/packages/ui-library/src/hooks/index.js index 8add304dbb9..a43e67dcc53 100644 --- a/packages/ui-library/src/hooks/index.js +++ b/packages/ui-library/src/hooks/index.js @@ -1,5 +1,6 @@ export { default as useBeforeUnload } from "./use-before-unload"; export { default as useDescribedBy } from "./use-described-by"; +export { useKeydown } from "./use-keydown"; export { default as usePrevious } from "./use-previous"; export { default as useRootContext } from "./use-root-context"; export { default as useSvgAria } from "./use-svg-aria"; diff --git a/packages/ui-library/src/hooks/readme.md b/packages/ui-library/src/hooks/readme.md index ae4358d9dca..8a78c96d76c 100644 --- a/packages/ui-library/src/hooks/readme.md +++ b/packages/ui-library/src/hooks/readme.md @@ -39,6 +39,30 @@ const Component = () => { return <div />; }; +~~~ + +## useKeydown +The `useKeydown` hook adds and removes a listener to the document' `keydown` event. +The hook accepts a callback function that is called when the event is triggered. +The callback function receives the KeyboardEvent as an argument. + +### Related +* https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event +* https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent + +### Usage/Examples +~~~javascript +import { useKeydown } from from "@yoast/ui-library"; + +const Component = () => { + useKeydown( ( event ) => { + if ( event.key === "Escape" ) { + console.log( "Escape key pressed" ); + } + }, document ); + + return <div/>; +}; ~~~ diff --git a/packages/ui-library/src/hooks/use-keydown.js b/packages/ui-library/src/hooks/use-keydown.js new file mode 100644 index 00000000000..8e666772597 --- /dev/null +++ b/packages/ui-library/src/hooks/use-keydown.js @@ -0,0 +1,15 @@ +import { useEffect } from "react"; + +/** + * @param {EventListener} onKeydown The keydown event listener. + * @param {EventTarget} eventTarget The target to listen on. E.g. document or window or an element. + * @returns {void} + */ +export const useKeydown = ( onKeydown, eventTarget ) => { + useEffect( () => { + eventTarget.addEventListener( "keydown", onKeydown ); + return () => { + eventTarget.removeEventListener( "keydown", onKeydown ); + }; + }, [ onKeydown ] ); +}; From ee308a9756f7c50118c441a0a5e5fdab1ecd5ca5 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Wed, 27 Nov 2024 15:57:12 +0100 Subject: [PATCH 111/132] UI library: add TooltipContainer Components to use the Tooltip element in a a11y friendly manner --- packages/ui-library/.storybook/style.css | 2 +- .../tooltip-container/docs/component.md | 24 ++++ .../tooltip-container/docs/index.js | 1 + .../src/components/tooltip-container/index.js | 124 ++++++++++++++++++ .../components/tooltip-container/stories.js | 83 ++++++++++++ .../components/tooltip-container/style.css | 25 ++++ .../ui-library/src/elements/tooltip/style.css | 4 +- packages/ui-library/src/index.js | 1 + 8 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 packages/ui-library/src/components/tooltip-container/docs/component.md create mode 100644 packages/ui-library/src/components/tooltip-container/docs/index.js create mode 100644 packages/ui-library/src/components/tooltip-container/index.js create mode 100644 packages/ui-library/src/components/tooltip-container/stories.js create mode 100644 packages/ui-library/src/components/tooltip-container/style.css diff --git a/packages/ui-library/.storybook/style.css b/packages/ui-library/.storybook/style.css index 07724fc8976..498e30b3a16 100644 --- a/packages/ui-library/.storybook/style.css +++ b/packages/ui-library/.storybook/style.css @@ -38,7 +38,7 @@ @import "../src/components/text-field/style.css"; @import "../src/components/textarea-field/style.css"; @import "../src/components/toggle-field/style.css"; - +@import "../src/components/tooltip-container/style.css"; @tailwind base; @tailwind components; diff --git a/packages/ui-library/src/components/tooltip-container/docs/component.md b/packages/ui-library/src/components/tooltip-container/docs/component.md new file mode 100644 index 00000000000..aa00cd70cd1 --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/docs/component.md @@ -0,0 +1,24 @@ +Tooltips provide contextual information about an element when that owning element receives focus or is hovered over, but are otherwise not visible. They are displayed as small boxes containing a brief label or message. See the Tooltip elements. + +However, to get a fully functioning experience, with regards to accessibility, the Tooltip needs more. The following are the requirements for the Tooltip: +* The Tooltip should be visible when the element that triggers the Tooltip is focused. And it should be hidden when the element is blurred. +* The Tooltip should be visible when the element that triggers the Tooltip is hovered over. And it should be hidden when the element is no longer hovered over. +* The Tooltip should be hidden when the user presses the `Escape` key, regardless of focus or hover. + +That is what this **TooltipContainer**, the **TooltipTrigger** and the **TooltipComponent** components are for. + +The **TooltipContainer** is the parent component that wraps the TooltipTrigger and the Tooltip components. It manages the visibility of the Tooltip. +* It provides the `isVisible` boolean and `show` and `hide` functions. +* It adds a `keydown` event listener to hide the tooltip when the user presses `Escape`. +* It contains the styling to center and control the tooltip visibility. + +The **TooltipTrigger** wraps the content that should trigger the Tooltip in a focusable element. +* It ensures that the tooltip is shown on focus and mouse enter. +* It adds the `aria-describedby` attribute to associate the tooltip with the trigger. +* It adds the `aria-disabled` attribute to indicate the trigger itself is not actually doing anything. +* It has styling for keyboard focus (`focus-visible`) and none for hover. + +The **TooltipComponent** wraps the Tooltip element. +* It gets the `isVisible` from the context. +* It hides the Tooltip via the `yst-hidden` className when `isVisible` is false. +* It forwards any props to the Tooltip element. diff --git a/packages/ui-library/src/components/tooltip-container/docs/index.js b/packages/ui-library/src/components/tooltip-container/docs/index.js new file mode 100644 index 00000000000..a01392a1542 --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/docs/index.js @@ -0,0 +1 @@ +export { default as component } from "./component.md"; diff --git a/packages/ui-library/src/components/tooltip-container/index.js b/packages/ui-library/src/components/tooltip-container/index.js new file mode 100644 index 00000000000..4bf88a7f90c --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/index.js @@ -0,0 +1,124 @@ +/* eslint-disable react/require-default-props */ +import classNames from "classnames"; +import { noop } from "lodash"; +import PropTypes from "prop-types"; +import React, { createContext, useContext } from "react"; +import Tooltip from "../../elements/tooltip"; +import { useKeydown, useToggleState } from "../../hooks"; + +/** + * @typedef {Object} TooltipContextValue + * @property {boolean} isVisible Whether the tooltip is visible. + * @property {function} show Show the tooltip. + * @property {function} hide Hide the tooltip. + */ + +/** + * @type {React.Context<TooltipContextValue>} + */ +const TooltipContext = createContext( { + isVisible: false, + show: noop, + hide: noop, +} ); + +/** + * @returns {TooltipContextValue} The value of the Tooltip context. + */ +export const useTooltipContext = () => useContext( TooltipContext ); + +/** + * Manages the visibility of the tooltip. + * - It provides the isVisible boolean and show and hide functions. + * - It adds a keydown event listener to hide the tooltip when the user presses Escape. + * - It contains the styling to center and control the tooltip visibility. + * @param {JSX.ElementClass} [as="span"] Base component. + * @param {string} [className] CSS class. + * @param {JSX.node} [children] The tooltip trigger and tooltip. + * @returns {JSX.Element} The element. + */ +export const TooltipContainer = ( { as: Component = "span", className, children } ) => { + const [ isVisible, , , show, hide ] = useToggleState( false ); + + useKeydown( ( event ) => { + if ( event.key === "Escape" ) { + hide(); + } + }, document ); + + return ( + <TooltipContext.Provider value={ { isVisible, show, hide } }> + <Component className={ classNames( "yst-tooltip-container", className ) }> + { children } + </Component> + </TooltipContext.Provider> + ); +}; +TooltipContainer.propTypes = { + as: PropTypes.elementType, + children: PropTypes.node, + className: PropTypes.string, +}; + +/** + * Wraps the content that should trigger the tooltip in a focusable element. + * - It ensures that the tooltip is shown on focus and mouse enter. + * - It adds the aria-describedby attribute to associate the tooltip with the trigger. + * - It adds the aria-disabled attribute to indicate the trigger is not actually doing anything. + * - It has styling for keyboard focus and none for hover. + * @param {string|JSX.node} [as="button"] Base component. Needs to be focusable. + * @param {string} [className] CSS class. + * @param {JSX.node} [children] What the tooltip should center on. + * @param {string} [ariaDescribedby] The ID of the tooltip, so that screen readers can associate the tooltip with the trigger. + * @param {Object} [props] Additional props. + * @returns {JSX.Element} The element. + */ +export const TooltipTrigger = ( { as: Component = "button", className, children, ariaDescribedby, ...props } ) => { + const { show } = useTooltipContext(); + + return ( + <Component + className={ classNames( "yst-tooltip-trigger", className ) } + onFocus={ show } + onMouseEnter={ show } + aria-describedby={ ariaDescribedby } + aria-disabled={ true } + { ...props } + > + { children } + </Component> + ); +}; +TooltipTrigger.propTypes = { + as: PropTypes.elementType, + children: PropTypes.node, + className: PropTypes.string, + ariaDescribedby: PropTypes.string, +}; + +/** + * Wraps the Tooltip element. + * - It gets the `isVisible` from the context. + * - It hides the Tooltip via the `yst-hidden` className when `isVisible` is false. + * - It forwards any props to the Tooltip element. + * @param {string} [className] CSS class. + * @param {JSX.node} [children] What the tooltip should center on. + * @param {Object} [props] Additional props. + * @returns {JSX.Element} The element. + */ +export const TooltipComponent = ( { className, children, ...props } ) => { + const { isVisible } = useTooltipContext(); + + return ( + <Tooltip + className={ classNames( className, { "yst-hidden": ! isVisible } ) } + { ...props } + > + { children } + </Tooltip> + ); +}; +TooltipComponent.propTypes = { + className: PropTypes.string, + children: PropTypes.node, +}; diff --git a/packages/ui-library/src/components/tooltip-container/stories.js b/packages/ui-library/src/components/tooltip-container/stories.js new file mode 100644 index 00000000000..51d62552589 --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/stories.js @@ -0,0 +1,83 @@ +import React from "react"; +import { InteractiveDocsPage } from "../../../.storybook/interactive-docs-page"; +import { component } from "./docs"; +import { TooltipComponent, TooltipContainer, TooltipTrigger } from "./index"; + +export const Factory = { + parameters: { + controls: { disable: false }, + }, + args: { + children: <> + <TooltipTrigger ariaDescribedby="tooltip-factory">Element containing a tooltip.</TooltipTrigger> + <TooltipComponent id="tooltip-factory">I'm a tooltip</TooltipComponent> + </>, + }, +}; + +export const Trigger = { + name: "TooltipTrigger", + render: ( args ) => <TooltipTrigger { ...args } />, + parameters: { + controls: { disable: false }, + }, + args: { + children: "Element containing a tooltip.", + ariaDescribedby: "tooltip-trigger", + }, + decorators: [ + ( Story, args ) => ( + <TooltipContainer> + <Story /> + <TooltipComponent id={ args.ariaDescribedby }>I'm a tooltip</TooltipComponent> + </TooltipContainer> + ), + ], +}; + +export const Tooltip = { + name: "TooltipComponent", + render: ( args ) => <TooltipComponent { ...args } />, + parameters: { + controls: { disable: false }, + }, + args: { + id: "tooltip", + children: "I'm a tooltip", + }, + decorators: [ + ( Story, args ) => ( + <TooltipContainer> + <TooltipTrigger ariaDescribedby={ args.id }>Element containing a tooltip.</TooltipTrigger> + <Story /> + </TooltipContainer> + ), + ], +}; + +export default { + title: "2) Components/Tooltip Container", + component: TooltipContainer, + argTypes: { + as: { + control: { type: "select" }, + options: [ "span", "div" ], + table: { type: { summary: "span | div" }, defaultValue: { summary: "span" } }, + }, + }, + parameters: { + docs: { + description: { + component, + }, + page: () => <InteractiveDocsPage stories={ [ Trigger, Tooltip ] } />, + }, + }, + decorators: [ + ( Story ) => ( + <div className="yst-m-20"> + <Story /> + </div> + ), + ], +}; diff --git a/packages/ui-library/src/components/tooltip-container/style.css b/packages/ui-library/src/components/tooltip-container/style.css new file mode 100644 index 00000000000..b14a46b753b --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/style.css @@ -0,0 +1,25 @@ +@layer components { + .yst-root { + .yst-tooltip-container { + @apply yst-relative; + + --yst-display-tooltip: none; + + &:hover, &:focus-within { + --yst-display-tooltip: inline-block; + } + } + + .yst-tooltip-trigger { + @apply yst-cursor-default + yst-rounded-md + focus:yst-outline-none + focus-visible:yst-ring-primary-500 + focus-visible:yst-border-primary-500 + focus-visible:yst-ring-2 + focus-visible:yst-ring-offset-2 + focus-visible:yst-bg-white + focus-visible:yst-border-opacity-0; + } + } +} diff --git a/packages/ui-library/src/elements/tooltip/style.css b/packages/ui-library/src/elements/tooltip/style.css index ef46ede5e29..b4bed5d5f38 100644 --- a/packages/ui-library/src/elements/tooltip/style.css +++ b/packages/ui-library/src/elements/tooltip/style.css @@ -1,8 +1,8 @@ @layer components { .yst-root { .yst-tooltip { + display: var(--yst-display-tooltip, inline-block); @apply yst-absolute - yst-inline-block yst-z-10 yst-px-2.5 yst-py-2 @@ -59,7 +59,7 @@ } .yst-tooltip--right { - @apply yst--translate-x-0 + @apply yst--translate-x-0 yst--translate-y-1/2 yst-left-full yst-top-1/2 diff --git a/packages/ui-library/src/index.js b/packages/ui-library/src/index.js index b22a5f0aaa0..070b145e505 100644 --- a/packages/ui-library/src/index.js +++ b/packages/ui-library/src/index.js @@ -40,6 +40,7 @@ export { default as TagField } from "./components/tag-field"; export { default as TextField } from "./components/text-field"; export { default as TextareaField } from "./components/textarea-field"; export { default as ToggleField } from "./components/toggle-field"; +export { TooltipContainer, TooltipTrigger, TooltipComponent, useTooltipContext } from "./components/tooltip-container"; export * from "./hooks"; export * from "./constants"; From 68a9c95c14d2b230d13a39a3297b54e9994c15b8 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:17:59 +0100 Subject: [PATCH 112/132] Implement tooltip from UI library --- .../dashboard/scores/components/score-list.js | 83 ++++++++----------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index 94723857a05..2a270c5b70e 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,5 +1,4 @@ -import { useEffect, useMemo, useRef } from "@wordpress/element"; -import { Badge, Button, Label, SkeletonLoader, Tooltip, useToggleState } from "@yoast/ui-library"; +import { Badge, Button, Label, SkeletonLoader, TooltipComponent, TooltipContainer, TooltipTrigger } from "@yoast/ui-library"; import classNames from "classnames"; import { SCORE_META } from "../score-meta"; @@ -37,66 +36,50 @@ export const ScoreListSkeletonLoader = ( { className } ) => ( ); /** - * @param {EventListener} onKeydown The keydown event listener. - * @returns {void} + * @param {Score} score The score. + * @returns {JSX.Element} The element. */ -const useKeydown = ( onKeydown ) => { - useEffect( () => { - document.addEventListener( "keydown", onKeydown ); - return () => { - document.removeEventListener( "keydown", onKeydown ); - }; - }, [ onKeydown ] ); -}; +const ScoreListItemContent = ( { score } ) => ( + <> + <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> + <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }> + { SCORE_META[ score.name ].label } + </Label> + <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> + </> +); /** * @param {Score} score The score. * @param {string} idSuffix The suffix for the IDs. * @returns {JSX.Element} The element. */ -const ScoreListItem = ( { score, idSuffix } ) => { - const [ isVisible, , , show, hide ] = useToggleState( false ); - const ref = useRef(); +const ScoreListItemContentWithTooltip = ( { score, idSuffix } ) => { + const id = `tooltip--${ idSuffix }__${ score.name }`; - const TooltipTrigger = SCORE_META[ score.name ].tooltip ? "button" : "span"; - const tooltipTriggerProps = useMemo( () => SCORE_META[ score.name ].tooltip ? { - onFocus: show, - onMouseEnter: show, - "aria-describedby": `tooltip--${ idSuffix }__${ score.name }`, - "aria-disabled": true, - className: "yst-rounded-md yst-cursor-default focus:yst-outline-none focus-visible:yst-ring-primary-500 focus-visible:yst-border-primary-500 focus-visible:yst-ring-2 focus-visible:yst-ring-offset-2 focus-visible:yst-bg-white focus-visible:yst-border-opacity-0", - } : {}, [ show ] ); + return ( + <TooltipContainer> + <TooltipTrigger className="yst-flex yst-items-center" ariaDescribedby={ id }> + <ScoreListItemContent score={ score } /> + </TooltipTrigger> + <TooltipComponent id={ id }> + { SCORE_META[ score.name ].tooltip } + </TooltipComponent> + </TooltipContainer> + ); +}; - useKeydown( ( event ) => { - if ( event.key === "Escape" ) { - hide(); - ref.current?.blur(); - } - } ); +/** + * @param {Score} score The score. + * @param {string} idSuffix The suffix for the IDs. + * @returns {JSX.Element} The element. + */ +const ScoreListItem = ( { score, idSuffix } ) => { + const Content = SCORE_META[ score.name ].tooltip ? ScoreListItemContentWithTooltip : ScoreListItemContent; return ( <li className={ CLASSNAMES.listItem }> - <div className="yst-tooltip-container"> - <TooltipTrigger - ref={ ref } - { ...tooltipTriggerProps } - className={ classNames( tooltipTriggerProps.className, "yst-flex yst-items-center" ) } - > - <span className={ classNames( CLASSNAMES.score, SCORE_META[ score.name ].color ) } /> - <Label as="span" className={ classNames( CLASSNAMES.label, "yst-leading-4 yst-py-1.5" ) }> - { SCORE_META[ score.name ].label } - </Label> - <Badge variant="plain" className={ classNames( score.links.view && "yst-mr-3" ) }>{ score.amount }</Badge> - </TooltipTrigger> - { SCORE_META[ score.name ].tooltip && ( - <Tooltip - id={ tooltipTriggerProps[ "aria-describedby" ] } - className={ classNames( ! isVisible && "yst-hidden" ) } - > - { SCORE_META[ score.name ].tooltip } - </Tooltip> - ) } - </div> + <Content score={ score } idSuffix={ idSuffix } /> { score.links.view && ( <Button as="a" variant="secondary" size="small" href={ score.links.view } className="yst-ml-auto">View</Button> ) } From f5b202812d466a467d08de467b36d6fa9b37c711 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Thu, 28 Nov 2024 10:31:58 +0200 Subject: [PATCH 113/132] Have the collectors throw the exceptions instead of the repos --- .../abstract-score-results-repository.php | 4 ---- .../score-results-not-found-exception.php | 18 ++++++++++++++++++ .../readability-score-results-collector.php | 7 +++++-- .../seo-score-results-collector.php | 7 +++++-- 4 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/dashboard/domain/score-results/score-results-not-found-exception.php diff --git a/src/dashboard/application/score-results/abstract-score-results-repository.php b/src/dashboard/application/score-results/abstract-score-results-repository.php index 0a09cc74025..9d322645f77 100644 --- a/src/dashboard/application/score-results/abstract-score-results-repository.php +++ b/src/dashboard/application/score-results/abstract-score-results-repository.php @@ -65,10 +65,6 @@ public function set_repositories( public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { $score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id ); - if ( $score_results === null ) { - throw new Exception( 'Error getting score results', 500 ); - } - $current_scores_list = $this->current_scores_repository->get_current_scores( $this->score_groups, $score_results, $content_type, $taxonomy, $term_id ); $score_result_object = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] ); diff --git a/src/dashboard/domain/score-results/score-results-not-found-exception.php b/src/dashboard/domain/score-results/score-results-not-found-exception.php new file mode 100644 index 00000000000..594fdb397b8 --- /dev/null +++ b/src/dashboard/domain/score-results/score-results-not-found-exception.php @@ -0,0 +1,18 @@ +<?php +// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure. +namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results; + +use Exception; + +/** + * Exception for when score results are not found. + */ +class Score_Results_Not_Found_Exception extends Exception { + + /** + * Constructor of the exception. + */ + public function __construct() { + parent::__construct( 'Score results not found', 500 ); + } +} diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index 23695b59cb4..7a5f46dcdea 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -7,6 +7,7 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; /** @@ -23,7 +24,9 @@ class Readability_Score_Results_Collector implements Score_Results_Collector_Int * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float>|null The readability score results for a content type, null for failure. + * @return array<string, object|bool|float> The readability score results for a content type. + * + * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $readability_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; @@ -97,7 +100,7 @@ public function get_score_results( array $readability_score_groups, Content_Type //phpcs:enable if ( $current_scores === null ) { - return null; + throw new Score_Results_Not_Found_Exception(); } $end_time = \microtime( true ); diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index 51aaee73b3f..85f8eeecdc6 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -7,6 +7,7 @@ use Yoast\WP\Lib\Model; use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface; +use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception; use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface; /** @@ -23,7 +24,9 @@ class SEO_Score_Results_Collector implements Score_Results_Collector_Interface { * @param Content_Type $content_type The content type. * @param int|null $term_id The ID of the term we're filtering for. * - * @return array<string, object|bool|float>|null The SEO score results for a content type, null for failure. + * @return array<string, object|bool|float> The SEO score results for a content type. + * + * @throws Score_Results_Not_Found_Exception When the query of getting score results fails. */ public function get_score_results( array $seo_score_groups, Content_Type $content_type, ?int $term_id ) { global $wpdb; @@ -99,7 +102,7 @@ public function get_score_results( array $seo_score_groups, Content_Type $conten //phpcs:enable if ( $current_scores === null ) { - return null; + throw new Score_Results_Not_Found_Exception(); } $end_time = \microtime( true ); From c4de185d199c23476583b09aeb57a1596d92988e Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 28 Nov 2024 10:19:20 +0100 Subject: [PATCH 114/132] Make clear button screen reader text translated --- packages/js/src/dashboard/scores/components/term-filter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 1fda3dff9e3..6b74ac9c0c3 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -88,6 +88,7 @@ export const TermFilter = ( { idSuffix, taxonomy, selected, onChange } ) => { onQueryChange={ handleQueryChange } placeholder={ __( "All", "wordpress-seo" ) } nullable={ true } + clearButtonScreenReaderText={ __( "Clear filter", "wordpress-seo" ) } validation={ error && { variant: "error", message: __( "Something went wrong.", "wordpress-seo" ), From 94a06e7ef44f5e3cb117ae9d1a6f108aeb988d14 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:10:57 +0100 Subject: [PATCH 115/132] Rename TooltipComponent to TooltipWithContext --- .../src/dashboard/scores/components/score-list.js | 6 +++--- .../components/tooltip-container/docs/component.md | 4 ++-- .../src/components/tooltip-container/index.js | 4 ++-- .../src/components/tooltip-container/stories.js | 14 +++++++------- packages/ui-library/src/index.js | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index 2a270c5b70e..ddf8313dc30 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -1,4 +1,4 @@ -import { Badge, Button, Label, SkeletonLoader, TooltipComponent, TooltipContainer, TooltipTrigger } from "@yoast/ui-library"; +import { Badge, Button, Label, SkeletonLoader, TooltipContainer, TooltipTrigger, TooltipWithContext } from "@yoast/ui-library"; import classNames from "classnames"; import { SCORE_META } from "../score-meta"; @@ -62,9 +62,9 @@ const ScoreListItemContentWithTooltip = ( { score, idSuffix } ) => { <TooltipTrigger className="yst-flex yst-items-center" ariaDescribedby={ id }> <ScoreListItemContent score={ score } /> </TooltipTrigger> - <TooltipComponent id={ id }> + <TooltipWithContext id={ id }> { SCORE_META[ score.name ].tooltip } - </TooltipComponent> + </TooltipWithContext> </TooltipContainer> ); }; diff --git a/packages/ui-library/src/components/tooltip-container/docs/component.md b/packages/ui-library/src/components/tooltip-container/docs/component.md index aa00cd70cd1..b4796a26436 100644 --- a/packages/ui-library/src/components/tooltip-container/docs/component.md +++ b/packages/ui-library/src/components/tooltip-container/docs/component.md @@ -5,7 +5,7 @@ However, to get a fully functioning experience, with regards to accessibility, t * The Tooltip should be visible when the element that triggers the Tooltip is hovered over. And it should be hidden when the element is no longer hovered over. * The Tooltip should be hidden when the user presses the `Escape` key, regardless of focus or hover. -That is what this **TooltipContainer**, the **TooltipTrigger** and the **TooltipComponent** components are for. +That is what this **TooltipContainer**, the **TooltipTrigger** and the **TooltipWithContext** components are for. The **TooltipContainer** is the parent component that wraps the TooltipTrigger and the Tooltip components. It manages the visibility of the Tooltip. * It provides the `isVisible` boolean and `show` and `hide` functions. @@ -18,7 +18,7 @@ The **TooltipTrigger** wraps the content that should trigger the Tooltip in a fo * It adds the `aria-disabled` attribute to indicate the trigger itself is not actually doing anything. * It has styling for keyboard focus (`focus-visible`) and none for hover. -The **TooltipComponent** wraps the Tooltip element. +The **TooltipWithContext** wraps the Tooltip element. * It gets the `isVisible` from the context. * It hides the Tooltip via the `yst-hidden` className when `isVisible` is false. * It forwards any props to the Tooltip element. diff --git a/packages/ui-library/src/components/tooltip-container/index.js b/packages/ui-library/src/components/tooltip-container/index.js index 4bf88a7f90c..e56a884cda6 100644 --- a/packages/ui-library/src/components/tooltip-container/index.js +++ b/packages/ui-library/src/components/tooltip-container/index.js @@ -106,7 +106,7 @@ TooltipTrigger.propTypes = { * @param {Object} [props] Additional props. * @returns {JSX.Element} The element. */ -export const TooltipComponent = ( { className, children, ...props } ) => { +export const TooltipWithContext = ( { className, children, ...props } ) => { const { isVisible } = useTooltipContext(); return ( @@ -118,7 +118,7 @@ export const TooltipComponent = ( { className, children, ...props } ) => { </Tooltip> ); }; -TooltipComponent.propTypes = { +TooltipWithContext.propTypes = { className: PropTypes.string, children: PropTypes.node, }; diff --git a/packages/ui-library/src/components/tooltip-container/stories.js b/packages/ui-library/src/components/tooltip-container/stories.js index 51d62552589..9764dfb3a8f 100644 --- a/packages/ui-library/src/components/tooltip-container/stories.js +++ b/packages/ui-library/src/components/tooltip-container/stories.js @@ -1,7 +1,7 @@ import React from "react"; import { InteractiveDocsPage } from "../../../.storybook/interactive-docs-page"; import { component } from "./docs"; -import { TooltipComponent, TooltipContainer, TooltipTrigger } from "./index"; +import { TooltipContainer, TooltipTrigger, TooltipWithContext } from "./index"; export const Factory = { parameters: { @@ -10,7 +10,7 @@ export const Factory = { args: { children: <> <TooltipTrigger ariaDescribedby="tooltip-factory">Element containing a tooltip.</TooltipTrigger> - <TooltipComponent id="tooltip-factory">I'm a tooltip</TooltipComponent> + <TooltipWithContext id="tooltip-factory">I'm a tooltip</TooltipWithContext> </>, }, }; @@ -29,15 +29,15 @@ export const Trigger = { ( Story, args ) => ( <TooltipContainer> <Story /> - <TooltipComponent id={ args.ariaDescribedby }>I'm a tooltip</TooltipComponent> + <TooltipWithContext id={ args.ariaDescribedby }>I'm a tooltip</TooltipWithContext> </TooltipContainer> ), ], }; -export const Tooltip = { - name: "TooltipComponent", - render: ( args ) => <TooltipComponent { ...args } />, +export const WithContext = { + name: "TooltipWithContext", + render: ( args ) => <TooltipWithContext { ...args } />, parameters: { controls: { disable: false }, }, @@ -70,7 +70,7 @@ export default { description: { component, }, - page: () => <InteractiveDocsPage stories={ [ Trigger, Tooltip ] } />, + page: () => <InteractiveDocsPage stories={ [ Trigger, WithContext ] } />, }, }, decorators: [ diff --git a/packages/ui-library/src/index.js b/packages/ui-library/src/index.js index 070b145e505..830ac850945 100644 --- a/packages/ui-library/src/index.js +++ b/packages/ui-library/src/index.js @@ -40,7 +40,7 @@ export { default as TagField } from "./components/tag-field"; export { default as TextField } from "./components/text-field"; export { default as TextareaField } from "./components/textarea-field"; export { default as ToggleField } from "./components/toggle-field"; -export { TooltipContainer, TooltipTrigger, TooltipComponent, useTooltipContext } from "./components/tooltip-container"; +export { TooltipContainer, TooltipTrigger, TooltipWithContext, useTooltipContext } from "./components/tooltip-container"; export * from "./hooks"; export * from "./constants"; From aebf8f216dadd527edf1ed9e2d7507d666af88ef Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:50:35 +0100 Subject: [PATCH 116/132] Update copy --- packages/js/src/dashboard/scores/score-meta.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js index c3a8d8583d4..8baac3cc582 100644 --- a/packages/js/src/dashboard/scores/score-meta.js +++ b/packages/js/src/dashboard/scores/score-meta.js @@ -28,7 +28,7 @@ export const SCORE_META = { label: __( "Not analyzed", "wordpress-seo" ), color: "yst-bg-analysis-na", hex: "#cbd5e1", - tooltip: __( "We haven’t analyzed this content yet. Please open it and save it in your editor so we can start the analysis.", "wordpress-seo" ), + tooltip: __( "We haven’t analyzed this content yet. Please open it in your editor, ensure a focus keyphrase is entered, and save it so we can start the analysis.", "wordpress-seo" ), }, }; From 58423ad4b51bb0074f7035d5dbddcc435f4764cd Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:12:17 +0100 Subject: [PATCH 117/132] Shrink width when mobile menu appears bit of a hack, but better than outside the screen --- packages/js/src/dashboard/scores/components/score-list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/components/score-list.js b/packages/js/src/dashboard/scores/components/score-list.js index ddf8313dc30..4c7f6b94b15 100644 --- a/packages/js/src/dashboard/scores/components/score-list.js +++ b/packages/js/src/dashboard/scores/components/score-list.js @@ -62,7 +62,7 @@ const ScoreListItemContentWithTooltip = ( { score, idSuffix } ) => { <TooltipTrigger className="yst-flex yst-items-center" ariaDescribedby={ id }> <ScoreListItemContent score={ score } /> </TooltipTrigger> - <TooltipWithContext id={ id }> + <TooltipWithContext id={ id } className="max-[784px]:yst-max-w-full"> { SCORE_META[ score.name ].tooltip } </TooltipWithContext> </TooltipContainer> From 66b69f4e7c907180e9d613ee96d338383346beff Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Mon, 2 Dec 2024 09:51:05 +0100 Subject: [PATCH 118/132] Copy changes. --- packages/js/src/dashboard/components/page-title.js | 9 ++++++--- packages/js/src/dashboard/scores/score-meta.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/js/src/dashboard/components/page-title.js b/packages/js/src/dashboard/components/page-title.js index f2d085b735f..102a63f3d83 100644 --- a/packages/js/src/dashboard/components/page-title.js +++ b/packages/js/src/dashboard/components/page-title.js @@ -27,14 +27,17 @@ export const PageTitle = ( { userName, features, links } ) => ( { features.indexables && ! features.seoAnalysis && ! features.readabilityAnalysis ? createInterpolateElement( sprintf( - /* translators: %1$s and %2$s expand to an opening and closing anchor tag. */ - __( "It looks like the ‘SEO analysis’ and the ‘Readability analysis’ are currently turned off in your %1$sSite features%2$s. Enable these features to start seeing all the insights you need right here!", "wordpress-seo" ), + /* translators: %1$s, %2$s, %3$s and %4$s expand to an opening and closing anchor tag. */ + __( "It looks like the ‘SEO analysis’ and the ‘Readability analysis’ are currently disabled in your %1$sSite features%2$s or your %3$suser profile settings%4$s. Enable these features to start seeing all the insights you need right here!", "wordpress-seo" ), "<link>", - "</link>" + "</link>", + "<profilelink>", + "</profilelink>" ), { // Added dummy space as content to prevent children prop warnings in the console. link: <Link href="admin.php?page=wpseo_page_settings#/site-features"> </Link>, + profilelink: <Link href="profile.php"> </Link>, } ) : createInterpolateElement( diff --git a/packages/js/src/dashboard/scores/score-meta.js b/packages/js/src/dashboard/scores/score-meta.js index 9ab0009a561..f320d755679 100644 --- a/packages/js/src/dashboard/scores/score-meta.js +++ b/packages/js/src/dashboard/scores/score-meta.js @@ -39,7 +39,7 @@ export const SCORE_DESCRIPTIONS = { good: __( "Most of your content has a good SEO score. Well done!", "wordpress-seo" ), ok: __( "Your content has an average SEO score. Time to find opportunities for improvement!", "wordpress-seo" ), bad: __( "Some of your content could use a little extra care. Take a look and start improving!", "wordpress-seo" ), - notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it and save it in your editor so we can start the analysis.", "wordpress-seo" ), + notAnalyzed: __( "Some of your content hasn't been analyzed yet. Please open it in your editor, ensure a focus keyphrase is entered, and save it so we can start the analysis.", "wordpress-seo" ), }, readability: { good: __( "Most of your content has a good readability score. Well done!", "wordpress-seo" ), From 639bf04addaf83d20c71b055a00fef85858e521c Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:43:51 +0100 Subject: [PATCH 119/132] Fix RTL for tooltip --- packages/ui-library/src/elements/tooltip/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/ui-library/src/elements/tooltip/style.css b/packages/ui-library/src/elements/tooltip/style.css index b4bed5d5f38..71c5f7af228 100644 --- a/packages/ui-library/src/elements/tooltip/style.css +++ b/packages/ui-library/src/elements/tooltip/style.css @@ -17,6 +17,7 @@ .yst-tooltip--top { @apply yst--translate-x-1/2 + rtl:yst-translate-x-1/2 yst--translate-y-full yst-left-1/2 yst-top-0 @@ -29,6 +30,7 @@ yst-left-1/2 yst-top-full /* Arrow positioned at the bottom of the tooltip */ yst--translate-x-1/2 + rtl:yst-translate-x-1/2 yst-translate-y-0 yst-border-8 yst-border-x-transparent @@ -39,6 +41,7 @@ .yst-tooltip--bottom { @apply yst--translate-x-1/2 + rtl:yst-translate-x-1/2 yst--translate-y-0 yst-left-1/2 yst-top-full @@ -51,6 +54,7 @@ yst-left-1/2 yst-bottom-full /* Arrow positioned at the top of the tooltip */ yst--translate-x-1/2 + rtl:yst-translate-x-1/2 yst-border-8 yst-border-x-transparent yst-border-t-transparent From a7709c743f2b7c9a4b550aae09155204af362685 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 11:44:58 +0200 Subject: [PATCH 120/132] Fix edge case where keyword is saved but SEO score is 0 --- .../score-groups/seo-score-groups/bad-seo-score-group.php | 2 +- .../seo-score-results/seo-score-results-collector.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php b/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php index a0772795bfe..7e3803ef65b 100644 --- a/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php +++ b/src/dashboard/domain/score-groups/seo-score-groups/bad-seo-score-group.php @@ -41,7 +41,7 @@ public function get_position(): int { * @return int The minimum score of the SEO score group. */ public function get_min_score(): ?int { - return 0; + return 1; } /** diff --git a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php index 85f8eeecdc6..3e866cd4a1e 100644 --- a/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/seo-score-results/seo-score-results-collector.php @@ -132,7 +132,7 @@ private function build_select( array $seo_score_groups ): array { $name = $seo_score_group->get_name(); if ( $min === null || $max === null ) { - $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; + $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score = 0 OR I.primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; $select_replacements[] = $name; } else { From e561c9c0515811be872af0930d06496f3d3e8ee5 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:58:41 +0100 Subject: [PATCH 121/132] Expand storybook documentation on style override --- .../tooltip-container/docs/component.md | 2 ++ .../tooltip-container/docs/index.js | 1 + .../tooltip-container/docs/with-flex.md | 5 +++ .../components/tooltip-container/stories.js | 33 +++++++++++++++++-- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 packages/ui-library/src/components/tooltip-container/docs/with-flex.md diff --git a/packages/ui-library/src/components/tooltip-container/docs/component.md b/packages/ui-library/src/components/tooltip-container/docs/component.md index b4796a26436..b52340a8690 100644 --- a/packages/ui-library/src/components/tooltip-container/docs/component.md +++ b/packages/ui-library/src/components/tooltip-container/docs/component.md @@ -22,3 +22,5 @@ The **TooltipWithContext** wraps the Tooltip element. * It gets the `isVisible` from the context. * It hides the Tooltip via the `yst-hidden` className when `isVisible` is false. * It forwards any props to the Tooltip element. + +**Note**: The tooltip is the same element, so if you want to override styling like `display`. You should add a container inside. diff --git a/packages/ui-library/src/components/tooltip-container/docs/index.js b/packages/ui-library/src/components/tooltip-container/docs/index.js index a01392a1542..b838fe39bec 100644 --- a/packages/ui-library/src/components/tooltip-container/docs/index.js +++ b/packages/ui-library/src/components/tooltip-container/docs/index.js @@ -1 +1,2 @@ export { default as component } from "./component.md"; +export { default as withFlex } from "./with-flex.md"; diff --git a/packages/ui-library/src/components/tooltip-container/docs/with-flex.md b/packages/ui-library/src/components/tooltip-container/docs/with-flex.md new file mode 100644 index 00000000000..5d16f795f73 --- /dev/null +++ b/packages/ui-library/src/components/tooltip-container/docs/with-flex.md @@ -0,0 +1,5 @@ +The TooltipWithContext renders the Tooltip element directly. +Meaning that adding style could mean overriding the Tooltip style. + +For example the `display` if you want to use `diplay: flex`. +Here is an example, that solves this by adding a container inside. diff --git a/packages/ui-library/src/components/tooltip-container/stories.js b/packages/ui-library/src/components/tooltip-container/stories.js index 9764dfb3a8f..b3b1ad85830 100644 --- a/packages/ui-library/src/components/tooltip-container/stories.js +++ b/packages/ui-library/src/components/tooltip-container/stories.js @@ -1,6 +1,6 @@ import React from "react"; import { InteractiveDocsPage } from "../../../.storybook/interactive-docs-page"; -import { component } from "./docs"; +import { component, withFlex } from "./docs"; import { TooltipContainer, TooltipTrigger, TooltipWithContext } from "./index"; export const Factory = { @@ -55,6 +55,35 @@ export const WithContext = { ], }; +export const WithFlex = { + name: "With display flex", + render: ( args ) => <TooltipWithContext { ...args } />, + parameters: { + controls: { disable: false }, + docs: { + description: { + story: withFlex, + }, + }, + }, + args: { + id: "tooltip", + children: <div className="yst-flex yst-flex-col"> + <span>Row one</span> + <span>Row two</span> + </div>, + }, + decorators: [ + ( Story, args ) => ( + <TooltipContainer> + <TooltipTrigger ariaDescribedby={ args.id }>Element containing a tooltip.</TooltipTrigger> + <Story /> + </TooltipContainer> + ), + ], +}; + + export default { title: "2) Components/Tooltip Container", component: TooltipContainer, @@ -70,7 +99,7 @@ export default { description: { component, }, - page: () => <InteractiveDocsPage stories={ [ Trigger, WithContext ] } />, + page: () => <InteractiveDocsPage stories={ [ Trigger, WithContext, WithFlex ] } />, }, }, decorators: [ From 57192507822295e027f405acc1454b1cfe300bfa Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:55:24 +0100 Subject: [PATCH 122/132] Improve translators comment By mentioning the pairs separately. As they should be treated as pairs, or we get errors. --- packages/js/src/dashboard/components/page-title.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/components/page-title.js b/packages/js/src/dashboard/components/page-title.js index 102a63f3d83..a62fd0a05e5 100644 --- a/packages/js/src/dashboard/components/page-title.js +++ b/packages/js/src/dashboard/components/page-title.js @@ -27,7 +27,10 @@ export const PageTitle = ( { userName, features, links } ) => ( { features.indexables && ! features.seoAnalysis && ! features.readabilityAnalysis ? createInterpolateElement( sprintf( - /* translators: %1$s, %2$s, %3$s and %4$s expand to an opening and closing anchor tag. */ + /** + * translators: %1$s and %2$s expand to an opening and closing anchor tag, to the site features page. + * %3$s and %4$s expand to an opening and closing anchor tag, to the user profile page. + **/ __( "It looks like the ‘SEO analysis’ and the ‘Readability analysis’ are currently disabled in your %1$sSite features%2$s or your %3$suser profile settings%4$s. Enable these features to start seeing all the insights you need right here!", "wordpress-seo" ), "<link>", "</link>", From aa8b9c140848c3903d26836c776ce70e151ff888 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 14:05:06 +0200 Subject: [PATCH 123/132] Fix edge case where readability scores have been saved but ERT has not --- admin/class-meta-columns.php | 16 +++++++++++++++- .../abstract-readability-score-group.php | 7 +++++++ .../bad-readability-score-group.php | 9 +++++++++ .../good-readability-score-group.php | 9 +++++++++ .../no-readability-score-group.php | 11 ++++++++++- .../ok-readability-score-group.php | 9 +++++++++ .../readability-score-groups-interface.php | 10 +++++++++- .../readability-score-results-collector.php | 15 +++++++++------ 8 files changed, 77 insertions(+), 9 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index 607b7fe94e5..c164f7c4249 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -681,12 +681,27 @@ protected function create_no_focus_keyword_filter() { protected function create_no_readability_scores_filter() { // We check the existence of the Estimated Reading Time, because readability scores of posts that haven't been manually saved while Yoast SEO is active, don't exist, which is also the case for posts with not enough content. // Meanwhile, the ERT is a solid indicator of whether a post has ever been saved (aka, analyzed), so we're using that. + $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], + [ + 'relation' => 'OR', + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => $rank->get_end_score(), + 'type' => 'numeric', + 'compare' => '<=', + ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ], ]; } @@ -716,7 +731,6 @@ protected function create_bad_readability_scores_filter() { 'compare' => 'NOT EXISTS', ], ], - ]; } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php index fedfb850b56..461099e2fe1 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php @@ -10,6 +10,13 @@ */ abstract class Abstract_Readability_Score_Group extends Abstract_Score_Group implements Readability_Score_Groups_Interface { + /** + * Whether the score group is ambiguous. + * + * @var bool + */ + private $is_ambiguous; + /** * Gets the key of the readability score group that is used when filtering on the posts page. * diff --git a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php index 57eac8485d4..361a88d6503 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php @@ -52,4 +52,13 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 40; } + + /** + * Gets whether the score group is abiguous. + * + * @return string + */ + public function get_is_ambiguous(): string { + return true; + } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php index 425f4bfe7c8..03495a41817 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php @@ -52,4 +52,13 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 100; } + + /** + * Gets whether the score group is abiguous. + * + * @return string + */ + public function get_is_ambiguous(): string { + return false; + } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php index 0466dcee3e1..4509ff1935c 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php @@ -50,6 +50,15 @@ public function get_min_score(): ?int { * @return null The maximum score of the readability score group. */ public function get_max_score(): ?int { - return null; + return 40; + } + + /** + * Gets whether the score group is abiguous. + * + * @return string + */ + public function get_is_ambiguous(): string { + return false; } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php index a6218261c2e..59e1b721ab3 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php @@ -52,4 +52,13 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 70; } + + /** + * Gets whether the score group is abiguous. + * + * @return string + */ + public function get_is_ambiguous(): string { + return false; + } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php index 2d7596a79e8..e0251496630 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php @@ -7,4 +7,12 @@ /** * This interface describes a readability score group implementation. */ -interface Readability_Score_Groups_Interface extends Score_Groups_Interface {} +interface Readability_Score_Groups_Interface extends Score_Groups_Interface { + + /** + * Gets whether the score group is abiguous. + * + * @return string + */ + public function get_is_ambiguous(): string; +} diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index 7a5f46dcdea..e498e7ef1a3 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -125,16 +125,19 @@ private function build_select( array $readability_score_groups ): array { $select_replacements = []; foreach ( $readability_score_groups as $readability_score_group ) { - $min = $readability_score_group->get_min_score(); - $max = $readability_score_group->get_max_score(); - $name = $readability_score_group->get_name(); + $min = $readability_score_group->get_min_score(); + $max = $readability_score_group->get_max_score(); + $name = $readability_score_group->get_name(); + $is_ambiguous = $readability_score_group->get_is_ambiguous(); - if ( $min === null || $max === null ) { - $select_fields[] = 'COUNT(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; + if ( $min === null ) { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score <= %d AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; + $select_replacements[] = $max; $select_replacements[] = $name; } else { - $select_fields[] = 'COUNT(CASE WHEN I.readability_score >= %d AND I.readability_score <= %d AND I.estimated_reading_time_minutes IS NOT NULL THEN 1 END) AS %i'; + $check_for_ert = ( $is_ambiguous ) ? ' AND I.estimated_reading_time_minutes IS NOT NULL' : ''; + $select_fields[] = "COUNT(CASE WHEN I.readability_score >= %d AND I.readability_score <= %d{$check_for_ert} THEN 1 END) AS %i"; $select_replacements[] = $min; $select_replacements[] = $max; $select_replacements[] = $name; From 26946f6a7d3fc63712f2beafc33a2dc2a966380c Mon Sep 17 00:00:00 2001 From: Thijs van der heijden <thijsvanderheijden2@gmail.com> Date: Mon, 2 Dec 2024 13:42:31 +0100 Subject: [PATCH 124/132] Use proper character decoding --- .../scores/components/content-type-filter.js | 30 +++++++++++++++---- .../scores/components/term-filter.js | 3 +- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/js/src/dashboard/scores/components/content-type-filter.js b/packages/js/src/dashboard/scores/components/content-type-filter.js index 2bce8f44624..50cc3f3bac2 100644 --- a/packages/js/src/dashboard/scores/components/content-type-filter.js +++ b/packages/js/src/dashboard/scores/components/content-type-filter.js @@ -1,11 +1,28 @@ import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField } from "@yoast/ui-library"; +import { replace, unescape } from "lodash"; /** * @type {import("../index").ContentType} ContentType */ + +/** + * The regex to find a single quote. + * @type {RegExp} + */ +const findSingleQuoteRegex = new RegExp( "'", "g" ); + +/** + * Decodes the label to remove HTML entities + * @param {string} encodedString The string to decode. + * @returns {string} The decoded string. + */ +function decodeString( encodedString ) { + return replace( unescape( encodedString ), findSingleQuoteRegex, "'" ); +} + /** * @param {string} idSuffix The suffix for the ID. * @param {ContentType[]} contentTypes The content types. @@ -31,15 +48,16 @@ export const ContentTypeFilter = ( { idSuffix, contentTypes, selected, onChange id={ `content-type--${ idSuffix }` } label={ __( "Content type", "wordpress-seo" ) } value={ selected?.name } - selectedLabel={ selected?.label || "" } + selectedLabel={ decodeString( selected?.label ) || "" } onChange={ handleChange } onQueryChange={ handleQueryChange } > - { filtered.map( ( { name, label } ) => ( - <AutocompleteField.Option key={ name } value={ name }> - { label } - </AutocompleteField.Option> - ) ) } + { filtered.map( ( { name, label } ) => { + const decodedLabel = decodeString( label ); + return <AutocompleteField.Option key={ name } value={ name }> + { decodedLabel } + </AutocompleteField.Option>; + } ) } </AutocompleteField> ); }; diff --git a/packages/js/src/dashboard/scores/components/term-filter.js b/packages/js/src/dashboard/scores/components/term-filter.js index 6b74ac9c0c3..6833ff2787b 100644 --- a/packages/js/src/dashboard/scores/components/term-filter.js +++ b/packages/js/src/dashboard/scores/components/term-filter.js @@ -1,6 +1,7 @@ import { useCallback, useState } from "@wordpress/element"; import { __ } from "@wordpress/i18n"; import { AutocompleteField, Spinner } from "@yoast/ui-library"; +import { unescape } from "lodash"; import { useFetch } from "../../fetch/use-fetch"; /** @@ -26,7 +27,7 @@ const createQueryUrl = ( endpoint, query ) => { * @param {{id: number, name: string}} term The term from the response. * @returns {Term} The transformed term for internal usage. */ -const transformTerm = ( term ) => ( { name: String( term.id ), label: term.name } ); +const transformTerm = ( term ) => ( { name: String( term.id ), label: unescape( term.name ) } ); /** * Renders either a list of terms or a message that nothing was found. From 53bef1b757b696c397145116de9d0118268cb1fb Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 14:44:45 +0200 Subject: [PATCH 125/132] Fix integration tests --- tests/WP/Admin/Meta_Columns_Test.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index a2a14e8a78b..754294b26b5 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -182,6 +182,20 @@ public static function determine_readability_filters_dataprovider() { 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], + [ + 'relation' => 'OR', + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 40, + 'type' => 'numeric', + 'compare' => '<=', + ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => 'needs-a-value-anyway', + 'compare' => 'NOT EXISTS', + ], + ], ], ], ]; From a798e32e536ff9fe18b068da3c23ebec736e1972 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 15:55:30 +0200 Subject: [PATCH 126/132] Start showing explicitly not-noindexed posts in the no focus keeword filter --- admin/class-meta-columns.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index c164f7c4249..a63a41406e5 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -593,7 +593,7 @@ protected function build_filter_query( $vars, $filters ) { $current_seo_filter = $this->get_current_seo_filter(); // This only applies for the SEO score filter because it can because the SEO score can be altered by the no-index option. - if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, [ WPSEO_Rank::NO_INDEX, WPSEO_Rank::NO_FOCUS ], true ) ) { + if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, [ WPSEO_Rank::NO_INDEX ], true ) ) { $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] ); } @@ -660,11 +660,6 @@ protected function create_no_index_filter() { */ protected function create_no_focus_keyword_filter() { return [ - [ - 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', - 'value' => 'needs-a-value-anyway', - 'compare' => 'NOT EXISTS', - ], [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => 'needs-a-value-anyway', From 2d85e4df1a0c57bc078b82eb943f37f92eb47f18 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 16:17:58 +0200 Subject: [PATCH 127/132] Add integration tests --- tests/WP/Admin/Meta_Columns_Test.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 754294b26b5..553aa4f5e53 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -85,11 +85,6 @@ public static function determine_seo_filters_dataprovider() { [ 'na', [ - [ - 'key' => '_yoast_wpseo_meta-robots-noindex', - 'value' => 'needs-a-value-anyway', - 'compare' => 'NOT EXISTS', - ], [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => 'needs-a-value-anyway', @@ -473,6 +468,9 @@ public function test_is_invalid_filter() { public function test_determine_seo_filters( $filter, $expected ) { $result = self::$class_instance->determine_seo_filters( $filter ); + error_log(print_r($expected, true)); + error_log(print_r($result, true)); + $this->assertEquals( $expected, $result ); } From 52cb47650b2e7d636a6da063dc9e16b2e8109372 Mon Sep 17 00:00:00 2001 From: Igor <35524806+igorschoester@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:21:40 +0100 Subject: [PATCH 128/132] Optional zero padding The CPTUI version encodes the single quote without zero padding. --- .../js/src/dashboard/scores/components/content-type-filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/dashboard/scores/components/content-type-filter.js b/packages/js/src/dashboard/scores/components/content-type-filter.js index 50cc3f3bac2..5e03d00a39c 100644 --- a/packages/js/src/dashboard/scores/components/content-type-filter.js +++ b/packages/js/src/dashboard/scores/components/content-type-filter.js @@ -12,7 +12,7 @@ import { replace, unescape } from "lodash"; * The regex to find a single quote. * @type {RegExp} */ -const findSingleQuoteRegex = new RegExp( "'", "g" ); +const findSingleQuoteRegex = new RegExp( "�?39;", "g" ); /** * Decodes the label to remove HTML entities From 3f8824b0756173673cc9ffc1df9d51ff04c85263 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 16:23:34 +0200 Subject: [PATCH 129/132] Remove temporary logging code --- tests/WP/Admin/Meta_Columns_Test.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 553aa4f5e53..3f3a29cf245 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -468,9 +468,6 @@ public function test_is_invalid_filter() { public function test_determine_seo_filters( $filter, $expected ) { $result = self::$class_instance->determine_seo_filters( $filter ); - error_log(print_r($expected, true)); - error_log(print_r($result, true)); - $this->assertEquals( $expected, $result ); } From c31c854ed27d76afcabe1dfd302fff61bb0c92f0 Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Mon, 2 Dec 2024 16:37:39 +0200 Subject: [PATCH 130/132] Fix typo in docs --- .../readability-score-groups/bad-readability-score-group.php | 2 +- .../readability-score-groups/good-readability-score-group.php | 2 +- .../readability-score-groups/no-readability-score-group.php | 2 +- .../readability-score-groups/ok-readability-score-group.php | 2 +- .../readability-score-groups-interface.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php index 361a88d6503..05ea93dc8b4 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php @@ -54,7 +54,7 @@ public function get_max_score(): ?int { } /** - * Gets whether the score group is abiguous. + * Gets whether the score group is ambiguous. * * @return string */ diff --git a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php index 03495a41817..01bf94890f3 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php @@ -54,7 +54,7 @@ public function get_max_score(): ?int { } /** - * Gets whether the score group is abiguous. + * Gets whether the score group is ambiguous. * * @return string */ diff --git a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php index 4509ff1935c..1e2d9f2396d 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php @@ -54,7 +54,7 @@ public function get_max_score(): ?int { } /** - * Gets whether the score group is abiguous. + * Gets whether the score group is ambiguous. * * @return string */ diff --git a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php index 59e1b721ab3..1534cd4e95e 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php @@ -54,7 +54,7 @@ public function get_max_score(): ?int { } /** - * Gets whether the score group is abiguous. + * Gets whether the score group is ambiguous. * * @return string */ diff --git a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php index e0251496630..250330a7397 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php @@ -10,7 +10,7 @@ interface Readability_Score_Groups_Interface extends Score_Groups_Interface { /** - * Gets whether the score group is abiguous. + * Gets whether the score group is ambiguous. * * @return string */ From 7c6a5689d2da276a6aacb697972c59d3fb1fb67b Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 3 Dec 2024 10:04:04 +0200 Subject: [PATCH 131/132] Show posts with low readability and no ERT into the right filter --- admin/class-meta-columns.php | 22 +++++++++--------- .../abstract-readability-score-group.php | 7 ------ .../bad-readability-score-group.php | 11 +-------- .../good-readability-score-group.php | 9 -------- .../no-readability-score-group.php | 11 +-------- .../ok-readability-score-group.php | 9 -------- .../readability-score-groups-interface.php | 7 ------ .../readability-score-results-collector.php | 16 ++++++------- tests/WP/Admin/Meta_Columns_Test.php | 23 +++++++++---------- 9 files changed, 31 insertions(+), 84 deletions(-) diff --git a/admin/class-meta-columns.php b/admin/class-meta-columns.php index a63a41406e5..900b4360031 100644 --- a/admin/class-meta-columns.php +++ b/admin/class-meta-columns.php @@ -687,9 +687,9 @@ protected function create_no_readability_scores_filter() { 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => $rank->get_end_score(), + 'value' => $rank->get_starting_score(), 'type' => 'numeric', - 'compare' => '<=', + 'compare' => '<', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', @@ -708,23 +708,23 @@ protected function create_no_readability_scores_filter() { protected function create_bad_readability_scores_filter() { $rank = new WPSEO_Rank( WPSEO_Rank::BAD ); return [ + 'relation' => 'OR', [ - 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', - 'compare' => 'EXISTS', + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => [ $rank->get_starting_score(), $rank->get_end_score() ], + 'type' => 'numeric', + 'compare' => 'BETWEEN', ], [ - 'relation' => 'OR', - [ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => $rank->get_end_score(), - 'type' => 'numeric', - 'compare' => '<=', - ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', + 'compare' => 'EXISTS', + ], ], ]; } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php index 461099e2fe1..fedfb850b56 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/abstract-readability-score-group.php @@ -10,13 +10,6 @@ */ abstract class Abstract_Readability_Score_Group extends Abstract_Score_Group implements Readability_Score_Groups_Interface { - /** - * Whether the score group is ambiguous. - * - * @var bool - */ - private $is_ambiguous; - /** * Gets the key of the readability score group that is used when filtering on the posts page. * diff --git a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php index 05ea93dc8b4..99f7ee61a69 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/bad-readability-score-group.php @@ -41,7 +41,7 @@ public function get_position(): int { * @return int The minimum score of the readability score group. */ public function get_min_score(): ?int { - return 0; + return 1; } /** @@ -52,13 +52,4 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 40; } - - /** - * Gets whether the score group is ambiguous. - * - * @return string - */ - public function get_is_ambiguous(): string { - return true; - } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php index 01bf94890f3..425f4bfe7c8 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/good-readability-score-group.php @@ -52,13 +52,4 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 100; } - - /** - * Gets whether the score group is ambiguous. - * - * @return string - */ - public function get_is_ambiguous(): string { - return false; - } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php index 1e2d9f2396d..0466dcee3e1 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/no-readability-score-group.php @@ -50,15 +50,6 @@ public function get_min_score(): ?int { * @return null The maximum score of the readability score group. */ public function get_max_score(): ?int { - return 40; - } - - /** - * Gets whether the score group is ambiguous. - * - * @return string - */ - public function get_is_ambiguous(): string { - return false; + return null; } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php index 1534cd4e95e..a6218261c2e 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/ok-readability-score-group.php @@ -52,13 +52,4 @@ public function get_min_score(): ?int { public function get_max_score(): ?int { return 70; } - - /** - * Gets whether the score group is ambiguous. - * - * @return string - */ - public function get_is_ambiguous(): string { - return false; - } } diff --git a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php index 250330a7397..2efebb4be77 100644 --- a/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php +++ b/src/dashboard/domain/score-groups/readability-score-groups/readability-score-groups-interface.php @@ -8,11 +8,4 @@ * This interface describes a readability score group implementation. */ interface Readability_Score_Groups_Interface extends Score_Groups_Interface { - - /** - * Gets whether the score group is ambiguous. - * - * @return string - */ - public function get_is_ambiguous(): string; } diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index e498e7ef1a3..d04dae1617d 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -125,19 +125,17 @@ private function build_select( array $readability_score_groups ): array { $select_replacements = []; foreach ( $readability_score_groups as $readability_score_group ) { - $min = $readability_score_group->get_min_score(); - $max = $readability_score_group->get_max_score(); - $name = $readability_score_group->get_name(); - $is_ambiguous = $readability_score_group->get_is_ambiguous(); + $min = $readability_score_group->get_min_score(); + $max = $readability_score_group->get_max_score(); + $name = $readability_score_group->get_name(); - if ( $min === null ) { - $select_fields[] = 'COUNT(CASE WHEN I.readability_score <= %d AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; - $select_replacements[] = $max; + if ( $min === null && $max === null ) { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; $select_replacements[] = $name; } else { - $check_for_ert = ( $is_ambiguous ) ? ' AND I.estimated_reading_time_minutes IS NOT NULL' : ''; - $select_fields[] = "COUNT(CASE WHEN I.readability_score >= %d AND I.readability_score <= %d{$check_for_ert} THEN 1 END) AS %i"; + $needs_ert = ( $min === 1 ) ? ' OR (I.readability_score = 0 AND I.estimated_reading_time_minutes IS NOT NULL)' : ''; + $select_fields[] = "COUNT(CASE WHEN ( I.readability_score >= %d AND I.readability_score <= %d{$needs_ert} ) THEN 1 END) AS %i"; $select_replacements[] = $min; $select_replacements[] = $max; $select_replacements[] = $name; diff --git a/tests/WP/Admin/Meta_Columns_Test.php b/tests/WP/Admin/Meta_Columns_Test.php index 3f3a29cf245..24e44e3f095 100644 --- a/tests/WP/Admin/Meta_Columns_Test.php +++ b/tests/WP/Admin/Meta_Columns_Test.php @@ -126,25 +126,24 @@ public static function determine_readability_filters_dataprovider() { [ 'bad', [ + 'relation' => 'OR', [ - 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', - 'compare' => 'EXISTS', + 'key' => WPSEO_Meta::$meta_prefix . 'content_score', + 'value' => [ 1, 40 ], + 'type' => 'numeric', + 'compare' => 'BETWEEN', ], [ - 'relation' => 'OR', - [ - 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 40, - 'type' => 'numeric', - 'compare' => '<=', - ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], + [ + 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes', + 'compare' => 'EXISTS', + ], ], - ], ], [ @@ -181,9 +180,9 @@ public static function determine_readability_filters_dataprovider() { 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', - 'value' => 40, + 'value' => 1, 'type' => 'numeric', - 'compare' => '<=', + 'compare' => '<', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', From 159341c870ce0819c59e5bb3ce70b912455afbdd Mon Sep 17 00:00:00 2001 From: Leonidas Milosis <leonidas.milossis@gmail.com> Date: Tue, 3 Dec 2024 10:50:55 +0200 Subject: [PATCH 132/132] Move parenthesis to right spot in readability score query --- .../readability-score-results-collector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php index d04dae1617d..dd9920b7908 100644 --- a/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php +++ b/src/dashboard/infrastructure/score-results/readability-score-results/readability-score-results-collector.php @@ -135,7 +135,7 @@ private function build_select( array $readability_score_groups ): array { } else { $needs_ert = ( $min === 1 ) ? ' OR (I.readability_score = 0 AND I.estimated_reading_time_minutes IS NOT NULL)' : ''; - $select_fields[] = "COUNT(CASE WHEN ( I.readability_score >= %d AND I.readability_score <= %d{$needs_ert} ) THEN 1 END) AS %i"; + $select_fields[] = "COUNT(CASE WHEN ( I.readability_score >= %d AND I.readability_score <= %d ){$needs_ert} THEN 1 END) AS %i"; $select_replacements[] = $min; $select_replacements[] = $max; $select_replacements[] = $name;