diff --git a/admin/class-admin-asset-manager.php b/admin/class-admin-asset-manager.php index d9c16f15333..5904b2d592d 100644 --- a/admin/class-admin-asset-manager.php +++ b/admin/class-admin-asset-manager.php @@ -482,6 +482,7 @@ protected function load_renamed_scripts() { 'feature-flag' => 'feature-flag-package', 'helpers' => 'helpers-package', 'jed' => 'jed-package', + 'chart.js' => 'chart.js-package', 'legacy-components' => 'components-package', 'network-admin-script' => 'network-admin', 'redux' => 'redux-package', diff --git a/config/webpack/externals.js b/config/webpack/externals.js index 03f564ae81e..2a26a0926d2 100644 --- a/config/webpack/externals.js +++ b/config/webpack/externals.js @@ -1,11 +1,13 @@ -const { camelCaseDash } = require( "@wordpress/dependency-extraction-webpack-plugin/lib/util" ); +const { + camelCaseDash, +} = require("@wordpress/dependency-extraction-webpack-plugin/lib/util"); /** * Yoast dependencies, declared as such in the package.json. */ -const { dependencies } = require( "../../packages/js/package" ); -const legacyYoastPackages = [ "yoast-components", "yoastseo" ]; -const additionalPackages = [ +const { dependencies } = require("../../packages/js/package"); +const legacyYoastPackages = ["yoast-components", "yoastseo"]; +const additionalPackages = [ "draft-js", "styled-components", "jed", @@ -13,26 +15,26 @@ const additionalPackages = [ "redux", "@reduxjs/toolkit", "react-helmet", + "chart.js", ]; const YOAST_PACKAGE_NAMESPACE = "@yoast/"; // Fetch all packages from the dependencies list. -const yoastPackages = Object.keys( dependencies ) - .filter( - ( packageName ) => - packageName.startsWith( YOAST_PACKAGE_NAMESPACE ) || - legacyYoastPackages.includes( packageName ) || - additionalPackages.includes( packageName ) - ); +const yoastPackages = Object.keys(dependencies).filter( + (packageName) => + packageName.startsWith(YOAST_PACKAGE_NAMESPACE) || + legacyYoastPackages.includes(packageName) || + additionalPackages.includes(packageName) +); /** * Convert Yoast packages to externals configuration. */ -const yoastExternals = yoastPackages.reduce( ( memo, packageName ) => { - let useablePackageName = packageName.replace( YOAST_PACKAGE_NAMESPACE, "" ); +const yoastExternals = yoastPackages.reduce((memo, packageName) => { + let useablePackageName = packageName.replace(YOAST_PACKAGE_NAMESPACE, ""); - switch ( useablePackageName ) { + switch (useablePackageName) { case "components": useablePackageName = "components-new"; break; @@ -47,9 +49,9 @@ const yoastExternals = yoastPackages.reduce( ( memo, packageName ) => { break; } - memo[ packageName ] = camelCaseDash( useablePackageName ); + memo[packageName] = camelCaseDash(useablePackageName); return memo; -}, {} ); +}, {}); module.exports = { YOAST_PACKAGE_NAMESPACE, diff --git a/config/webpack/webpack.config.base.js b/config/webpack/webpack.config.base.js index b31bf148d59..0b661d108dc 100644 --- a/config/webpack/webpack.config.base.js +++ b/config/webpack/webpack.config.base.js @@ -10,7 +10,7 @@ const { yoastExternals } = require( "./externals" ); let analyzerPort = 8888; module.exports = function( { entry, output, combinedOutputFile, cssExtractFileName } ) { - const exclude = /node_modules[/\\](?!(yoast-components|gutenberg|yoastseo|@wordpress|@yoast|parse5)[/\\]).*/; + const exclude = /node_modules[/\\](?!(yoast-components|gutenberg|yoastseo|@wordpress|@yoast|parse5|chart.js)[/\\]).*/; // The index of the babel-loader rule. let ruleIndex = 0; if ( process.env.NODE_ENV !== "production" ) { diff --git a/packages/babel-preset/index.js b/packages/babel-preset/index.js index b9b6601be29..51798047fda 100644 --- a/packages/babel-preset/index.js +++ b/packages/babel-preset/index.js @@ -3,7 +3,11 @@ module.exports = ( api ) => { return { presets: [ "@wordpress/babel-preset-default" ], - plugins: [ "@babel/plugin-proposal-optional-chaining", "@babel/plugin-transform-runtime" ], + plugins: [ + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-transform-runtime", + "@babel/plugin-transform-class-properties", + ], sourceType: "unambiguous", }; }; diff --git a/packages/babel-preset/package.json b/packages/babel-preset/package.json index cf427199114..df81cbc8084 100644 --- a/packages/babel-preset/package.json +++ b/packages/babel-preset/package.json @@ -13,6 +13,7 @@ "private": false, "dependencies": { "@babel/plugin-proposal-optional-chaining": "^7.17.12", + "@babel/plugin-transform-class-properties": "^7.22.5", "@babel/plugin-transform-runtime": "^7.17.12", "@wordpress/babel-preset-default": "^6.13.0" }, diff --git a/packages/js/package.json b/packages/js/package.json index ea70d872a91..f130756f2d6 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -11,8 +11,9 @@ "@draft-js-plugins/mention": "^5.0.0", "@headlessui/react": "^1.7.8", "@heroicons/react": "^1.0.6", - "@wordpress/api-fetch": "^6.13.0", + "@reduxjs/toolkit": "^1.8.3", "@wordpress/a11y": "^2.15.1", + "@wordpress/api-fetch": "^6.13.0", "@wordpress/block-editor": "^5.3.1", "@wordpress/blocks": "^11.1.2", "@wordpress/components": "^13.0.3", @@ -40,9 +41,13 @@ "@yoast/ui-library": "^3.2.1", "a11y-speak": "git+https://github.com/Yoast/a11y-speak.git#master", "babel-polyfill": "^6.26.0", + "bowser": "^2.11.0", + "chart.js": "^4.2.1", + "chartjs-adapter-moment": "^1.0.1", "classnames": "^2.3.2", "draft-js": "^0.11.7", "find-with-regex": "~1.0.2", + "formik": "^2.2.9", "interpolate-components": "^1.1.0", "jed": "^1.1.1", "lodash": "^4.17.21", @@ -51,22 +56,20 @@ "moment-duration-format": "^2.2.2", "prop-types": "^15.5.10", "react-animate-height": "^2.0.23", + "react-aria-live": "^2.0.5", + "react-chartjs-2": "^5.2.0", "react-helmet": "^6.1.0", + "react-hotkeys-hook": "^4.0.5", "react-intl": "^2.4.0", "react-redux": "^5.0.6", + "react-router-dom": "^6.3.0", "react-select": "^3.1.0", "redux": "^3.7.2", "redux-thunk": "^2.2.0", "styled-components": "^5.3.6", "yoast-components": "^5.24.0", "yoastseo": "^1.91.1", - "formik": "^2.2.9", - "@reduxjs/toolkit": "^1.8.3", - "react-router-dom": "^6.3.0", - "yup": "^0.32.11", - "bowser": "^2.11.0", - "react-hotkeys-hook": "^4.0.5", - "react-aria-live": "^2.0.5" + "yup": "^0.32.11" }, "devDependencies": { "@babel/core": "^7.17.9", diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 89c38852488..be88912e053 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import { Fragment, useRef, useState, useEffect, useCallback, useMemo } from "@wordpress/element"; import { __, sprintf } from "@wordpress/i18n"; -import { isEmpty, filter, debounce, without, difference } from "lodash"; +import { isEmpty, filter, debounce, without, difference, orderBy } from "lodash"; import styled from "styled-components"; /* Yoast dependencies */ @@ -19,6 +19,7 @@ import { } from "../helpers/wincherEndpoints"; import { handleAPIResponse } from "../helpers/api"; +import { Checkbox } from "@yoast/components"; const GetMoreInsightsLink = makeOutboundLink(); @@ -33,15 +34,24 @@ const FocusKeyphraseFootnote = styled.span` } `; -const ViewColumn = styled.th` - min-width: 60px; -`; - const TableWrapper = styled.div` width: 100%; overflow-y: auto; `; +const SelectKeyphraseCheckboxWrapper = styled.th` + pointer-events: ${ props => props.isDisabled ? "none" : "initial" }; + padding-right: 0 !important; + + & > div { + margin: 0px; + } +`; + +const KeyphraseThWrapper = styled.th` + padding-left: 2px !important; +`; + /** * Hook that returns the previous value. * @@ -87,6 +97,9 @@ const WincherKeyphrasesTable = ( props ) => { websiteId, focusKeyphrase, newRequest, + startAt, + selectedKeyphrases, + onSelectKeyphrases, } = props; const interval = useRef(); @@ -124,7 +137,7 @@ const WincherKeyphrasesTable = ( props ) => { abortController.current.abort(); } abortController.current = typeof AbortController === "undefined" ? null : new AbortController(); - return debouncedGetKeyphrases( keyphrases, permalink, abortController.current.signal ); + return debouncedGetKeyphrases( keyphrases, startAt, permalink, abortController.current.signal ); }, ( response ) => { setRequestSucceeded( response ); @@ -140,6 +153,7 @@ const WincherKeyphrasesTable = ( props ) => { setTrackedKeyphrases, keyphrases, permalink, + startAt, ] ); /** @@ -227,8 +241,12 @@ const WincherKeyphrasesTable = ( props ) => { // Fetch initial data and re-fetch if the permalink or keyphrases change. const prevPermalink = usePrevious( permalink ); const prevKeyphrases = usePrevious( keyphrases ); + const prevStartAt = usePrevious( startAt ); + const hasParams = permalink && startAt; + useEffect( () => { - if ( isLoggedIn && permalink && ( permalink !== prevPermalink || difference( keyphrases, prevKeyphrases ).length ) ) { + if ( isLoggedIn && hasParams && + ( permalink !== prevPermalink || difference( keyphrases, prevKeyphrases ).length || startAt !== prevStartAt ) ) { getTrackedKeyphrases(); } }, [ @@ -238,6 +256,9 @@ const WincherKeyphrasesTable = ( props ) => { keyphrases, prevKeyphrases, getTrackedKeyphrases, + hasParams, + startAt, + prevStartAt, ] ); // Tracks remaining keyphrases if trackAll is set and we have data. @@ -298,24 +319,48 @@ const WincherKeyphrasesTable = ( props ) => { const isDataLoading = isLoggedIn && trackedKeyphrases === null; + const trackedKeywordsWithHistory = useMemo( () => isEmpty( trackedKeyphrases ) ? [] : Object.values( trackedKeyphrases ) + .filter( keyword => ! isEmpty( keyword?.position?.history ) ) + .map( keyword => keyword.keyword ), [ trackedKeyphrases ] ); + + const areAllSelected = useMemo( () => selectedKeyphrases.length > 0 && trackedKeywordsWithHistory.length > 0 && + trackedKeywordsWithHistory.every( selected => selectedKeyphrases.includes( selected ) ), + [ selectedKeyphrases, trackedKeywordsWithHistory ] ); + + /** + * Select or deselect all keyphrases. + * + * @returns {void} + */ + const onSelectAllKeyphrases = useCallback( () => { + onSelectKeyphrases( areAllSelected ? [] : trackedKeywordsWithHistory ); + }, [ onSelectKeyphrases, areAllSelected, trackedKeywordsWithHistory ] ); + + const sortedKeyphrases = useMemo( () => orderBy( keyphrases, [ + ( keyphrase ) => Object.values( trackedKeyphrases || {} ) + .map( trackedKeyphrase => trackedKeyphrase.keyword ).includes( keyphrase ), + ], [ "desc" ] ), [ keyphrases, trackedKeyphrases ] ); + return ( keyphrases && ! isEmpty( keyphrases ) && - - + - + + { - keyphrases.map( ( keyphrase, index ) => { + sortedKeyphrases.map( ( keyphrase, index ) => { return ( { websiteId={ websiteId } isDisabled={ ! isLoggedIn } isLoading={ isDataLoading || loadingKeyphrases.indexOf( keyphrase.toLowerCase() ) >= 0 } + isSelected={ selectedKeyphrases.includes( keyphrase ) } + onSelectKeyphrases={ onSelectKeyphrases } /> ); } ) } @@ -385,6 +443,9 @@ WincherKeyphrasesTable.propTypes = { websiteId: PropTypes.string, permalink: PropTypes.string.isRequired, focusKeyphrase: PropTypes.string, + startAt: PropTypes.string, + selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, + onSelectKeyphrases: PropTypes.func.isRequired, }; WincherKeyphrasesTable.defaultProps = { @@ -392,7 +453,6 @@ WincherKeyphrasesTable.defaultProps = { isNewlyAuthenticated: false, keyphrases: [], trackAll: false, - trackedKeyphrases: null, websiteId: "", focusKeyphrase: "", }; diff --git a/packages/js/src/components/WincherPerformanceReport.js b/packages/js/src/components/WincherPerformanceReport.js index 9d0eb6dadf5..c7a464b3f57 100644 --- a/packages/js/src/components/WincherPerformanceReport.js +++ b/packages/js/src/components/WincherPerformanceReport.js @@ -16,7 +16,7 @@ import { Alert, NewButton } from "@yoast/components"; import WincherNoTrackedKeyphrasesAlert from "./modals/WincherNoTrackedKeyphrasesAlert"; import { getKeyphrasePosition, PositionOverTimeChart } from "./WincherTableRow"; import WincherReconnectAlert from "./modals/WincherReconnectAlert"; -import WincherUpgradeCallout from "./modals/WincherUpgradeCallout"; +import WincherUpgradeCallout, { useTrackingInfo } from "./modals/WincherUpgradeCallout"; const ViewLink = makeOutboundLink(); const GetMoreInsightsLink = makeOutboundLink(); @@ -465,12 +465,13 @@ const WincherPerformanceReport = ( props ) => { const data = isLoggedIn ? props.data : fakeWincherPerformanceData; const isBlurred = ! isLoggedIn; const hasResults = checkHasResults( data ); + const trackingInfo = useTrackingInfo( isLoggedIn ); return ( - { isLoggedIn && } + { isLoggedIn && } diff --git a/packages/js/src/components/WincherRankingHistoryChart.js b/packages/js/src/components/WincherRankingHistoryChart.js new file mode 100644 index 00000000000..904303061e2 --- /dev/null +++ b/packages/js/src/components/WincherRankingHistoryChart.js @@ -0,0 +1,140 @@ +/* External dependencies */ +import { Line } from "react-chartjs-2"; +import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement, TimeScale, Legend, Tooltip } from "chart.js"; +import "chartjs-adapter-moment"; +import PropTypes from "prop-types"; +import { noop } from "lodash"; +import moment from "moment"; + +Chart.register( CategoryScale, LineController, LineElement, PointElement, LinearScale, TimeScale, Legend, Tooltip ); + +const CHART_COLORS = [ + "#ff983b", + "#ffa3f7", + "#3798ff", + "#ff3b3b", + "#acce81", + "#b51751", + "#3949ab", + "#26c6da", + "#ccb800", + "#de66ff", + "#4db6ac", + "#ffab91", + "#45f5f1", + "#77f210", + "#90a4ae", + "#ffd54f", + "#006b5e", + "#8ec7d2", + "#b1887c", + "#cc9300", +]; + +/** + * Renders the Wincher ranking history chart. + * + * @param {Object} props The ranking history props. + * + * @returns {null|wp.Element} The Wincher ranking history chart. + */ +export default function WincherRankingHistoryChart( { datasets, isChartShown } ) { + if ( ! isChartShown ) { + return null; + } + + const data = datasets.map( ( dataset, index ) => ( { + ...dataset, + data: dataset.data.map( ( { datetime, value } ) => ( { + x: datetime, + y: value, + } ) ), + lineTension: 0, + pointRadius: 1, + pointHoverRadius: 4, + borderWidth: 2, + pointHitRadius: 6, + backgroundColor: CHART_COLORS[ index % CHART_COLORS.length ], + } ) ).filter( dataset => dataset.selected !== false ); + + return ( + moment( x[ 0 ].raw.x ).utc().format( "YYYY-MM-DD" ), + }, + titleAlign: "center", + intersect: false, + mode: "point", + position: "nearest", + usePointStyle: true, + boxHeight: 7, + boxWidth: 7, + boxPadding: 2, + }, + }, + scales: { + x: { + bounds: "ticks", + type: "time", + time: { + unit: "day", + minUnit: "day", + }, + grid: { + display: false, + }, + ticks: { + autoSkipPadding: 50, + maxRotation: 0, + color: "black", + }, + }, + y: { + bounds: "ticks", + offset: true, + reverse: true, + ticks: { + precision: 0, + color: "black", + }, + }, + }, + } } + /> + ); +} + +WincherRankingHistoryChart.propTypes = { + datasets: PropTypes.arrayOf( + PropTypes.shape( { + label: PropTypes.string.isRequired, + data: PropTypes.arrayOf( + PropTypes.shape( { + datetime: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + } ) + ).isRequired, + selected: PropTypes.bool, + } ) + ).isRequired, + isChartShown: PropTypes.bool.isRequired, +}; diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index 6328cc64bae..c808b3af28c 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -1,11 +1,13 @@ /* global wpseoAdminL10n */ /* External dependencies */ -import { useCallback } from "@wordpress/element"; +import { useCallback, useEffect, useMemo, useState } from "@wordpress/element"; +import { usePrevious } from "@wordpress/compose"; import { __, sprintf } from "@wordpress/i18n"; import PropTypes from "prop-types"; -import { isEmpty } from "lodash"; +import { isEmpty, orderBy } from "lodash"; import styled from "styled-components"; +import moment from "moment"; /* Yoast dependencies */ import { NewButton, HelpIcon } from "@yoast/components"; @@ -13,7 +15,7 @@ import { NewButton, HelpIcon } from "@yoast/components"; /* Internal dependencies */ import WincherLimitReached from "./modals/WincherLimitReached"; import WincherRequestFailed from "./modals/WincherRequestFailed"; -import WincherUpgradeCallout from "./modals/WincherUpgradeCallout"; +import WincherUpgradeCallout, { useTrackingInfo } from "./modals/WincherUpgradeCallout"; import WincherConnectedAlert from "./modals/WincherConnectedAlert"; import WincherCurrentlyTrackingAlert from "./modals/WincherCurrentlyTrackingAlert"; import WincherKeyphrasesTable from "../containers/WincherKeyphrasesTable"; @@ -25,6 +27,7 @@ import { authenticate, getAuthorizationUrl, trackKeyphrases } from "../helpers/w import { handleAPIResponse } from "../helpers/api"; import WincherReconnectAlert from "./modals/WincherReconnectAlert"; import WincherNoPermalinkAlert from "./modals/WincherNoPermalinkAlert"; +import WincherRankingHistoryChart from "./WincherRankingHistoryChart"; /** * Gets the proper error message component. @@ -228,6 +231,90 @@ const Title = styled.div` font-size: var(--yoast-font-size-default); `; +const WincherChartSettings = styled.div.attrs( { className: "yoast-field-group" } )` + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 14px; +`; + +const ChartWrapper = styled.div` + margin: 8px 0; +`; + +const START_OF_TODAY = moment.utc().startOf( "day" ); + +const WINCHER_PERIOD_OPTIONS = [ + { + name: __( "Last day", "wordpress-seo" ), + value: moment( START_OF_TODAY ).subtract( 1, "days" ).format(), + defaultIndex: 1, + }, + { + name: __( "Last week", "wordpress-seo" ), + value: moment( START_OF_TODAY ).subtract( 1, "week" ).format(), + defaultIndex: 2, + }, + { + name: __( "Last month", "wordpress-seo" ), + value: moment( START_OF_TODAY ).subtract( 1, "month" ).format(), + defaultIndex: 3, + }, + { + name: __( "Last year", "wordpress-seo" ), + value: moment( START_OF_TODAY ).subtract( 1, "year" ).format(), + defaultIndex: 0, + }, +]; + +/** + * Displays the Wincher period picker. + * + * @param {Object} props The component props. + * + * @returns {null|wp.Element} The Wincher period picker. + */ +const WincherPeriodPicker = ( props ) => { + const { onSelect, selected, options, isLoggedIn } = props; + + if ( ! isLoggedIn ) { + return null; + } + + if ( options.length < 1 ) { + return null; + } + + return ( + + ); +}; + +WincherPeriodPicker.propTypes = { + onSelect: PropTypes.func.isRequired, + selected: PropTypes.object, + options: PropTypes.array.isRequired, + isLoggedIn: PropTypes.bool.isRequired, +}; + /** * Creates the table content. * @@ -237,10 +324,12 @@ const Title = styled.div` */ const TableContent = ( props ) => { const { + trackedKeyphrases, isLoggedIn, keyphrases, shouldTrackAll, permalink, + historyDaysLimit, } = props; if ( ! permalink && isLoggedIn ) { @@ -251,20 +340,93 @@ const TableContent = ( props ) => { return ; } + const historyLimitDate = moment( START_OF_TODAY ).subtract( historyDaysLimit, "days" ); + + const periodOptions = WINCHER_PERIOD_OPTIONS.filter( + opt => moment( opt.value ).isSameOrAfter( historyLimitDate ) + ); + + const defaultPeriod = orderBy( periodOptions, opt => opt.defaultIndex, "desc" )[ 0 ]; + + const [ period, setPeriod ] = useState( defaultPeriod ); + + const [ selectedKeyphrases, setSelectedKeyphrases ] = useState( [] ); + + const isChartShown = selectedKeyphrases.length > 0; + + const trackedKeyphrasesPrev = usePrevious( trackedKeyphrases ); + + useEffect( () => { + if ( ! isEmpty( trackedKeyphrases ) && Object.values( trackedKeyphrases ).length !== ( trackedKeyphrasesPrev || [] ).length ) { + const keywords = Object.values( trackedKeyphrases ).map( keyphrase => keyphrase.keyword ); + setSelectedKeyphrases( keywords ); + } + }, [ trackedKeyphrases, trackedKeyphrasesPrev ] ); + + useEffect( () => { + setPeriod( defaultPeriod ); + }, [ defaultPeriod?.name ] ); + + const onSelectPeriod = useCallback( ( event ) => { + const option = WINCHER_PERIOD_OPTIONS.find( opt => opt.value === event.target.value ); + if ( option ) { + setPeriod( option ); + } + }, [ setPeriod ] ); + + const chartData = useMemo( () => { + if ( isEmpty( selectedKeyphrases ) ) { + return []; + } + if ( isEmpty( trackedKeyphrases ) ) { + return []; + } + return Object.values( trackedKeyphrases ) + .filter( keyphrase => !! keyphrase?.position?.history ) + .map( keyphrase => ( { + label: keyphrase.keyword, + data: keyphrase.position.history, + selected: selectedKeyphrases.includes( keyphrase.keyword ) && ! isEmpty( keyphrase.position?.history ), + } ) ); + }, [ selectedKeyphrases, trackedKeyphrases ] ); + return

{ __( "You can enable / disable tracking the SEO performance for each keyphrase below.", "wordpress-seo" ) }

{ isLoggedIn && shouldTrackAll && } - + + + + + + + + +
; }; TableContent.propTypes = { + trackedKeyphrases: PropTypes.object, keyphrases: PropTypes.array.isRequired, isLoggedIn: PropTypes.bool.isRequired, shouldTrackAll: PropTypes.bool.isRequired, permalink: PropTypes.string.isRequired, + historyDaysLimit: PropTypes.number, }; /** @@ -283,11 +445,12 @@ export default function WincherSEOPerformance( props ) { const onLoginCallback = useCallback( () => { onLoginOpen( props ); }, [ onLoginOpen, props ] ); + const trackingInfo = useTrackingInfo( isLoggedIn ); return ( { isNewlyAuthenticated && } - { isLoggedIn && } + { isLoggedIn && } { __( "SEO performance", "wordpress-seo" ) } @@ -302,12 +465,13 @@ export default function WincherSEOPerformance( props ) { <ConnectToWincher isLoggedIn={ isLoggedIn } onLogin={ onLoginCallback } /> <GetUserMessage { ...props } onLogin={ onLoginCallback } /> - <TableContent { ...props } /> + <TableContent { ...props } historyDaysLimit={ trackingInfo?.historyDays || 0 } /> </Wrapper> ); } WincherSEOPerformance.propTypes = { + trackedKeyphrases: PropTypes.object, addTrackedKeyphrase: PropTypes.func.isRequired, isLoggedIn: PropTypes.bool, isNewlyAuthenticated: PropTypes.bool, @@ -315,13 +479,16 @@ WincherSEOPerformance.propTypes = { response: PropTypes.object, shouldTrackAll: PropTypes.bool, permalink: PropTypes.string, + historyDaysLimit: PropTypes.number, }; WincherSEOPerformance.defaultProps = { + trackedKeyphrases: null, isLoggedIn: false, isNewlyAuthenticated: false, keyphrases: [], response: {}, shouldTrackAll: false, permalink: "", + historyDaysLimit: 0, }; diff --git a/packages/js/src/components/WincherSEOPerformanceModal.js b/packages/js/src/components/WincherSEOPerformanceModal.js index 07b1f3cc8a5..29922bbbd71 100644 --- a/packages/js/src/components/WincherSEOPerformanceModal.js +++ b/packages/js/src/components/WincherSEOPerformanceModal.js @@ -6,6 +6,10 @@ import { useSvgAria } from "@yoast/ui-library/src"; import PropTypes from "prop-types"; import styled from "styled-components"; +/* Yoast dependencies */ +import { colors } from "@yoast/style-guide"; +import { Collapsible } from "@yoast/components"; + /* Internal dependencies */ import { ModalContainer } from "./modals/Container"; import Modal from "./modals/Modal"; @@ -21,6 +25,17 @@ const StyledHeroIcon = styled( ChartBarIcon )` margin: 3px; `; +const MetaboxModalButton = styled( Collapsible )` + h2 > button { + padding-left: 24px; + padding-top: 16px; + + &:hover { + background-color: #f0f0f0; + } + } +`; + /** * Handles the click event on the "Track SEO performance" button. * @@ -33,7 +48,13 @@ export function openModal( props ) { if ( ! keyphrases.length ) { // This is fragile, should replace with a real React ref. - document.querySelector( "#focus-keyword-input-sidebar" ).focus(); + let input = document.querySelector( "#focus-keyword-input-metabox" ); + + // In elementor we use input-sidebar + if ( ! input ) { + input = document.querySelector( "#focus-keyword-input-sidebar" ); + } + input.focus(); onNoKeyphraseSet(); return; @@ -106,6 +127,19 @@ export default function WincherSEOPerformanceModal( props ) { onClick={ onModalOpen } /> } + + { location === "metabox" && <MetaboxModalButton + hasPadding={ false } + hasSeparator={ true } + suffixIconCollapsed={ { + icon: "pencil-square", + color: colors.$black, + size: "20px", + } } + id={ `wincher-open-button-${location}` } + title={ title } + onToggle={ onModalOpen } + /> } </Fragment> ); } diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index dee87fc0e69..5bee5aa2298 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -6,14 +6,66 @@ import { isEmpty } from "lodash"; import moment from "moment"; /* Yoast dependencies */ -import { SvgIcon, Toggle } from "@yoast/components"; -import { makeOutboundLink } from "@yoast/helpers"; +import { Checkbox, SvgIcon, Toggle, ButtonStyledLink } from "@yoast/components"; /* Internal dependencies */ import AreaChart from "./AreaChart"; import WincherSEOPerformanceLoading from "./modals/WincherSEOPerformanceLoading"; +import styled from "styled-components"; -const ViewLink = makeOutboundLink(); +export const CaretIcon = styled( SvgIcon )` + margin-left: 2px; + flex-shrink: 0; + rotate: ${ props => props.isImproving ? "-90deg" : "90deg" }; +`; + +export const PositionChangeValue = styled.span` + color: ${ props => props.isImproving ? "#69AB56" : "#DC3332" }; + font-size: 13px; + font-weight: 600; + line-height: 20px; + margin-right: 2px; + margin-left: 12px; +`; + +export const SelectKeyphraseCheckboxWrapper = styled.td` + padding-right: 0 !important; + + & > div { + margin: 0px; + } +`; + +export const KeyphraseTdWrapper = styled.td` + padding-left: 2px !important; +`; + +export const TrackingTdWrapper = styled.td.attrs( { className: "yoast-table--nopadding" } )` + & > div { + justify-content: center; + } +`; + +const PositionAndViewLinkWrapper = styled.div` + display: flex; + align-items: center; +`; + +const PositionOverTimeButton = styled.button` + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + display: flex; + align-items: center; +`; + +const WincherTableRowElement = styled.tr` + background-color: ${ props => props.isEnabled ? "#FFFFFF" : "#F9F9F9" } !important; +`; /** * Transforms the Wincher Position data to x/y points for the SVG area chart. @@ -74,7 +126,6 @@ export function PositionOverTimeChart( { chartData } ) { strokeWidth={ 1.8 } strokeColor="#498afc" fillColor="#ade3fc" - className="yoast-related-keyphrases-modal__chart" mapChartDataToTableData={ mapAreaChartDataToTableData } dataTableCaption={ __( "Keyphrase position in the last 90 days on a scale from 0 to 100.", "wordpress-seo" ) @@ -132,6 +183,45 @@ export function getKeyphrasePosition( keyphrase ) { return keyphrase.position.value; } +/** + * Humanize the last updated date string + * + * @param {string} dateString The date string to format. + * + * @returns {string} The formatted last updated date. + */ +const formatLastUpdated = ( dateString ) => moment( dateString ).fromNow(); + +/** + * Displays the position over time cell. + * + * @param {object} rowData The position over time data. + * + * @returns {wp.Element} The position over time table cell. + */ +export const PositionOverTimeCell = ( { rowData } ) => { + if ( ! rowData?.position?.change ) { + return <PositionOverTimeChart chartData={ rowData } />; + } + + const isImproving = rowData.position.change < 0; + return ( + <Fragment> + <PositionOverTimeChart chartData={ rowData } /> + <PositionChangeValue isImproving={ isImproving }>{ Math.abs( rowData.position.change ) }</PositionChangeValue> + <CaretIcon + icon={ "caret-right" } + color={ isImproving ? "#69AB56" : "#DC3332" } + size={ "14px" } isImproving={ isImproving } + /> + </Fragment> + ); +}; + +PositionOverTimeCell.propTypes = { + rowData: PropTypes.object, +}; + /** * Gets the positional data based on the current UI state and returns the appropiate UI element. * @@ -140,7 +230,16 @@ export function getKeyphrasePosition( keyphrase ) { * @returns {wp.Element} The rendered element. */ export function getPositionalDataByState( props ) { - const { rowData, websiteId } = props; + const { rowData, websiteId, keyphrase, onSelectKeyphrases } = props; + + /** + * Fires when click on position over time + * + * @returns {void} + */ + const onPositionOverTimeClick = useCallback( () => { + onSelectKeyphrases( [ keyphrase ] ); + }, [ onSelectKeyphrases, keyphrase ] ); const isEnabled = ! isEmpty( rowData ); const hasFreshData = rowData && rowData.updated_at && moment( rowData.updated_at ) >= moment().subtract( 7, "days" ); @@ -152,34 +251,35 @@ export function getPositionalDataByState( props ) { if ( ! isEnabled ) { return ( - <Fragment> - <td>?</td> - <td className="yoast-table--nopadding">?</td> - <td className="yoast-table--nobreak" /> - </Fragment> + <td className="yoast-table--nopadding" colSpan="3"> + <i>{ __( "Activate tracking to show the ranking position", "wordpress-seo" ) }</i> + </td> ); } if ( ! hasFreshData ) { return ( - <Fragment> - <td className="yoast-table--nopadding" colSpan="3"> - <WincherSEOPerformanceLoading /> - </td> - </Fragment> + <td className="yoast-table--nopadding" colSpan="3"> + <WincherSEOPerformanceLoading /> + </td> ); } return ( <Fragment> - <td>{ getKeyphrasePosition( rowData ) }</td> - <td className="yoast-table--nopadding">{ <PositionOverTimeChart chartData={ rowData } /> }</td> - <td className="yoast-table--nobreak"> - { - <ViewLink href={ viewLinkURL }> + <td> + <PositionAndViewLinkWrapper> + { getKeyphrasePosition( rowData ) } + <ButtonStyledLink variant="secondary" href={ viewLinkURL } style={ { height: 28, marginLeft: 12 } }> { __( "View", "wordpress-seo" ) } - </ViewLink> - } + </ButtonStyledLink> + </PositionAndViewLinkWrapper> + </td> + <td className="yoast-table--nopadding"> + <PositionOverTimeButton onClick={ onPositionOverTimeClick }> + <PositionOverTimeCell rowData={ rowData } /> + </PositionOverTimeButton> </td> + <td>{ formatLastUpdated( rowData.updated_at ) }</td> </Fragment> ); } @@ -201,10 +301,14 @@ export default function WincherTableRow( props ) { isFocusKeyphrase, isDisabled, isLoading, + isSelected, + onSelectKeyphrases, } = props; const isEnabled = ! isEmpty( rowData ); + const hasHistory = ! isEmpty( rowData?.position?.history ); + const toggleAction = useCallback( () => { if ( isDisabled ) { @@ -220,14 +324,35 @@ export default function WincherTableRow( props ) { [ keyphrase, onTrackKeyphrase, onUntrackKeyphrase, isEnabled, rowData, isDisabled ] ); - return <tr> - <td className="yoast-table--nopadding"> - { renderToggleState( { keyphrase, isEnabled, toggleAction, isLoading } ) } - </td> - <td>{ keyphrase }{ isFocusKeyphrase && <span>*</span> }</td> + /** + * Fires when checkbox value changes + * + * @returns {void} + */ + const onChange = useCallback( () => { + onSelectKeyphrases( prev => isSelected ? prev.filter( e => e !== keyphrase ) : prev.concat( keyphrase ) ); + }, [ onSelectKeyphrases, isSelected, keyphrase ] ); + + return <WincherTableRowElement isEnabled={ isEnabled }> + <SelectKeyphraseCheckboxWrapper> + { hasHistory && <Checkbox + id={ "select-" + keyphrase } + onChange={ onChange } + checked={ isSelected } + label="" + /> } + </SelectKeyphraseCheckboxWrapper> + + <KeyphraseTdWrapper> + { keyphrase }{ isFocusKeyphrase && <span>*</span> } + </KeyphraseTdWrapper> { getPositionalDataByState( props ) } - </tr>; + + <TrackingTdWrapper> + { renderToggleState( { keyphrase, isEnabled, toggleAction, isLoading } ) } + </TrackingTdWrapper> + </WincherTableRowElement>; } WincherTableRow.propTypes = { @@ -240,6 +365,8 @@ WincherTableRow.propTypes = { isLoading: PropTypes.bool, // eslint-disable-next-line react/no-unused-prop-types websiteId: PropTypes.string, + isSelected: PropTypes.bool.isRequired, + onSelectKeyphrases: PropTypes.func.isRequired, }; WincherTableRow.defaultProps = { diff --git a/packages/js/src/components/fills/MetaboxFill.js b/packages/js/src/components/fills/MetaboxFill.js index fc4a0d405a5..954ea2ff90b 100644 --- a/packages/js/src/components/fills/MetaboxFill.js +++ b/packages/js/src/components/fills/MetaboxFill.js @@ -1,12 +1,12 @@ /* External dependencies */ import { useSelect } from "@wordpress/data"; -import { Fragment, useCallback } from "@wordpress/element"; +import { Fragment } from "@wordpress/element"; import { Fill } from "@wordpress/components"; import { __ } from "@wordpress/i18n"; import PropTypes from "prop-types"; -import { colors } from "@yoast/style-guide"; /* Internal dependencies */ +import WincherSEOPerformanceModal from "../../containers/WincherSEOPerformanceModal"; import CollapsibleCornerstone from "../../containers/CollapsibleCornerstone"; import SnippetEditor from "../../containers/SnippetEditor"; import Warning from "../../containers/Warning"; @@ -19,7 +19,6 @@ import AdvancedSettings from "../../containers/AdvancedSettings"; import SocialMetadataPortal from "../portals/SocialMetadataPortal"; import SchemaTabContainer from "../../containers/SchemaTab"; import SEMrushRelatedKeyphrases from "../../containers/SEMrushRelatedKeyphrases"; -import WincherSEOPerformance from "../../containers/WincherSEOPerformance"; import { isWordProofIntegrationActive } from "../../helpers/wordproof"; import WordProofAuthenticationModals from "../../components/modals/WordProofAuthenticationModals"; import PremiumSEOAnalysisModal from "../modals/PremiumSEOAnalysisModal"; @@ -37,23 +36,10 @@ const BlackFridayPromotionWithMetaboxWarningsCheck = withMetaboxWarningsCheck( B * Creates the Metabox component. * * @param {Object} settings The feature toggles. - * @param {Object} store The Redux store. - * @param {Object} theme The theme to use. - * @param {Array} wincherKeyphrases The Wincher trackable keyphrases. - * @param {Function} setWincherNoKeyphrase Sets wincher no keyphrases in the store. * * @returns {wp.Element} The Metabox component. */ -export default function MetaboxFill( { settings, wincherKeyphrases, setWincherNoKeyphrase } ) { - const onToggleWincher = useCallback( () => { - if ( ! wincherKeyphrases.length ) { - setWincherNoKeyphrase( true ); - // This is fragile, should replace with a real React ref. - document.querySelector( "#focus-keyword-input-metabox" ).focus(); - return false; - } - }, [ wincherKeyphrases, setWincherNoKeyphrase ] ); - +export default function MetaboxFill( { settings } ) { const isTerm = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsTerm(), [] ); const isProduct = useSelect( ( select ) => select( "yoast-seo/editor" ).getIsProduct(), [] ); @@ -117,17 +103,9 @@ export default function MetaboxFill( { settings, wincherKeyphrases, setWincherNo </SidebarItem> } { settings.isKeywordAnalysisActive && settings.isWincherIntegrationActive && <SidebarItem key="wincher-seo-performance" renderPriority={ 25 }> - <MetaboxCollapsible - id={ "yoast-wincher-seo-performance-metabox" } - title={ __( "Track SEO performance", "wordpress-seo" ) } - initialIsOpen={ false } - prefixIcon={ { icon: "chart-square-bar", color: colors.$color_grey_medium_dark } } - prefixIconCollapsed={ { icon: "chart-square-bar", color: colors.$color_grey_medium_dark } } - onToggle={ onToggleWincher } - > - <WincherSEOPerformance /> - </MetaboxCollapsible> - </SidebarItem> } + <WincherSEOPerformanceModal location="metabox" /> + </SidebarItem> + } { settings.isCornerstoneActive && <SidebarItem key="cornerstone" renderPriority={ 30 }> <CollapsibleCornerstone /> </SidebarItem> } @@ -155,8 +133,6 @@ export default function MetaboxFill( { settings, wincherKeyphrases, setWincherNo MetaboxFill.propTypes = { settings: PropTypes.object.isRequired, - wincherKeyphrases: PropTypes.array.isRequired, - setWincherNoKeyphrase: PropTypes.func.isRequired, }; /* eslint-enable complexity */ diff --git a/packages/js/src/components/modals/WincherUpgradeCallout.js b/packages/js/src/components/modals/WincherUpgradeCallout.js index e74f41b4f24..88a6b4f1a76 100644 --- a/packages/js/src/components/modals/WincherUpgradeCallout.js +++ b/packages/js/src/components/modals/WincherUpgradeCallout.js @@ -56,13 +56,15 @@ const CalloutContainer = styled.div` /** * Hook to fetch the account tracking info. * + * @param {boolean} isLoggedIn Whether the use is logged in. + * * @returns {object} The Wincher account tracking info. */ -const useTrackingInfo = () => { +export const useTrackingInfo = ( isLoggedIn ) => { const [ trackingInfo, setTrackingInfo ] = useState( null ); useEffect( ()=>{ - if ( ! trackingInfo ) { + if ( isLoggedIn && ! trackingInfo ) { checkLimit().then( data => setTrackingInfo( data ) ); } }, [ trackingInfo ] ); @@ -70,6 +72,10 @@ const useTrackingInfo = () => { return trackingInfo; }; +useTrackingInfo.propTypes = { + limit: PropTypes.bool.isRequired, +}; + /** * Hook to fetch the upgrade campaign. * @@ -221,8 +227,7 @@ WincherUpgradeCalloutDescription.propTypes = { * * @returns {wp.Element | null} The Wincher upgrade callout. */ -const WincherUpgradeCallout = ( { onClose, isTitleShortened } ) => { - const trackingInfo = useTrackingInfo(); +const WincherUpgradeCallout = ( { onClose, isTitleShortened, trackingInfo } ) => { const upgradeCampaign = useUpgradeCampaign(); if ( trackingInfo === null ) { @@ -257,6 +262,7 @@ const WincherUpgradeCallout = ( { onClose, isTitleShortened } ) => { WincherUpgradeCallout.propTypes = { onClose: PropTypes.func, isTitleShortened: PropTypes.bool, + trackingInfo: PropTypes.object, }; export default WincherUpgradeCallout; diff --git a/packages/js/src/containers/MetaboxFill.js b/packages/js/src/containers/MetaboxFill.js index 518650b4a7f..7ef90c3056b 100644 --- a/packages/js/src/containers/MetaboxFill.js +++ b/packages/js/src/containers/MetaboxFill.js @@ -1,4 +1,4 @@ -import { withSelect, withDispatch } from "@wordpress/data"; +import { withSelect } from "@wordpress/data"; import { compose } from "@wordpress/compose"; import MetaboxFill from "../components/fills/MetaboxFill"; @@ -6,19 +6,11 @@ export default compose( [ withSelect( ( select, ownProps ) => { const { getPreferences, - getWincherTrackableKeyphrases, } = select( "yoast-seo/editor" ); return { settings: getPreferences(), store: ownProps.store, - wincherKeyphrases: getWincherTrackableKeyphrases(), - }; - } ), - withDispatch( ( dispatch ) => { - const { setWincherNoKeyphrase } = dispatch( "yoast-seo/editor" ); - return { - setWincherNoKeyphrase, }; } ), ] )( MetaboxFill ); diff --git a/packages/js/src/containers/WincherKeyphrasesTable.js b/packages/js/src/containers/WincherKeyphrasesTable.js index d762cdb5456..33ed6d2d6b6 100644 --- a/packages/js/src/containers/WincherKeyphrasesTable.js +++ b/packages/js/src/containers/WincherKeyphrasesTable.js @@ -9,7 +9,6 @@ export default compose( [ withSelect( ( select ) => { const { getWincherWebsiteId, - getWincherTrackedKeyphrases, getWincherTrackableKeyphrases, getWincherLoginStatus, getWincherPermalink, @@ -21,7 +20,6 @@ export default compose( [ return { focusKeyphrase: getFocusKeyphrase(), keyphrases: getWincherTrackableKeyphrases(), - trackedKeyphrases: getWincherTrackedKeyphrases(), isLoggedIn: getWincherLoginStatus(), trackAll: shouldWincherTrackAll(), websiteId: getWincherWebsiteId(), diff --git a/packages/js/src/containers/WincherSEOPerformance.js b/packages/js/src/containers/WincherSEOPerformance.js index 67e0f6e69ad..f94d7826817 100644 --- a/packages/js/src/containers/WincherSEOPerformance.js +++ b/packages/js/src/containers/WincherSEOPerformance.js @@ -11,10 +11,12 @@ export default compose( [ isWincherNewlyAuthenticated, getWincherKeyphraseLimitReached, getWincherLimit, + getWincherHistoryDaysLimit, getWincherLoginStatus, getWincherRequestIsSuccess, getWincherRequestResponse, getWincherTrackableKeyphrases, + getWincherTrackedKeyphrases, getWincherAllKeyphrasesMissRanking, getWincherPermalink, shouldWincherAutomaticallyTrackAll, @@ -22,6 +24,7 @@ export default compose( [ return { keyphrases: getWincherTrackableKeyphrases(), + trackedKeyphrases: getWincherTrackedKeyphrases(), allKeyphrasesMissRanking: getWincherAllKeyphrasesMissRanking(), isLoggedIn: getWincherLoginStatus(), isNewlyAuthenticated: isWincherNewlyAuthenticated(), @@ -31,6 +34,7 @@ export default compose( [ response: getWincherRequestResponse(), shouldTrackAll: shouldWincherAutomaticallyTrackAll(), permalink: getWincherPermalink(), + historyDaysLimit: getWincherHistoryDaysLimit(), }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/js/src/helpers/wincherEndpoints.js b/packages/js/src/helpers/wincherEndpoints.js index f913a7c80f6..b05a87f00c1 100644 --- a/packages/js/src/helpers/wincherEndpoints.js +++ b/packages/js/src/helpers/wincherEndpoints.js @@ -58,18 +58,20 @@ export async function authenticate( responseData ) { * Gets the tracked keyphrases data via POST. * * @param {Array} keyphrases The keyphrases to get the data for. + * @param {string} startAt The keyphrases to get the data for. * @param {String} permalink The post's/page's permalink. Optional. * @param {AbortSignal} signal (optional) Abort signal. * * @returns {Promise} The API response promise. */ -export async function getKeyphrases( keyphrases = null, permalink = null, signal ) { +export async function getKeyphrases( keyphrases = null, startAt = null, permalink = null, signal ) { return await callEndpoint( { path: "yoast/v1/wincher/keyphrases", method: "POST", data: { keyphrases, permalink, + startAt, }, signal, } ); diff --git a/packages/js/src/redux/reducers/WincherRequest.js b/packages/js/src/redux/reducers/WincherRequest.js index c8b4622d213..d16c63952b1 100644 --- a/packages/js/src/redux/reducers/WincherRequest.js +++ b/packages/js/src/redux/reducers/WincherRequest.js @@ -19,6 +19,7 @@ const INITIAL_STATE = { limit: 10, trackAll: false, automaticallyTrack: false, + historyDaysLimit: 0, }; /** * A reducer for the Wincher request. diff --git a/packages/js/src/redux/selectors/WincherRequest.js b/packages/js/src/redux/selectors/WincherRequest.js index c802aa84bff..8f064721f8e 100644 --- a/packages/js/src/redux/selectors/WincherRequest.js +++ b/packages/js/src/redux/selectors/WincherRequest.js @@ -66,6 +66,17 @@ export function getWincherLimit( state ) { return state.WincherRequest.limit; } +/** + * Gets the history days limit. + * + * @param {Object} state The state. + * + * @returns {int} The history days limit assigned to the user account. + */ +export function getWincherHistoryDaysLimit( state ) { + return state.WincherRequest.historyDays; +} + /** * Determines whether all keyphrases should be tracked. * diff --git a/packages/js/src/redux/selectors/WincherSEOPerformance.js b/packages/js/src/redux/selectors/WincherSEOPerformance.js index a39eff63566..26cb970788c 100644 --- a/packages/js/src/redux/selectors/WincherSEOPerformance.js +++ b/packages/js/src/redux/selectors/WincherSEOPerformance.js @@ -48,8 +48,7 @@ export function hasWincherTrackedKeyphrases( state ) { export function getWincherTrackableKeyphrases( state ) { const isPremium = getL10nObject().isPremium; const premiumStore = window.wp.data.select( "yoast-seo-premium/editor" ); - const tracked = Object.keys( getWincherTrackedKeyphrases( state ) || {} ).map( k => k.trim() ); - const keyphrases = [ state.focusKeyword.trim(), ...tracked ]; + const keyphrases = [ state.focusKeyword.trim() ]; if ( isPremium && premiumStore ) { // eslint-disable-next-line no-undefined diff --git a/packages/js/tests/components/WincherKeyphrasesTable.test.js b/packages/js/tests/components/WincherKeyphrasesTable.test.js index 24c6418a16f..ac0d3f8cc77 100644 --- a/packages/js/tests/components/WincherKeyphrasesTable.test.js +++ b/packages/js/tests/components/WincherKeyphrasesTable.test.js @@ -58,6 +58,8 @@ describe( "WincherKeyphrasesTable", () => { removeTrackedKeyphrase={ noop } setHasTrackedAll={ noop } permalink="" + selectedKeyphrases={ [] } + onSelectKeyphrases={ noop } /> ); expect( component.find( "tbody" ).getElement().props.children.length ).toEqual( 1 ); @@ -78,6 +80,8 @@ describe( "WincherKeyphrasesTable", () => { removeTrackedKeyphrase={ noop } setHasTrackedAll={ noop } permalink="" + selectedKeyphrases={ [] } + onSelectKeyphrases={ noop } /> ); const rows = component.find( WincherTableRow ); @@ -103,6 +107,8 @@ describe( "WincherKeyphrasesTable", () => { isLoggedIn={ true } trackAll={ true } permalink="" + selectedKeyphrases={ [] } + onSelectKeyphrases={ noop } /> ); } ); @@ -125,6 +131,8 @@ describe( "WincherKeyphrasesTable", () => { setHasTrackedAll={ noop } permalink="" focusKeyphrase={ "Yoast SEO" } + selectedKeyphrases={ [] } + onSelectKeyphrases={ noop } /> ); const rows = component.find( WincherTableRow ); diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index c3ee6dee972..c92111d11a2 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -1,8 +1,17 @@ import { shallow } from "enzyme"; -import WincherTableRow, { PositionOverTimeChart } from "../../src/components/WincherTableRow"; +import WincherTableRow, { + PositionOverTimeChart, + PositionOverTimeCell, + CaretIcon, + PositionChangeValue, + SelectKeyphraseCheckboxWrapper, + KeyphraseTdWrapper, + TrackingTdWrapper, +} from "../../src/components/WincherTableRow"; import { Toggle } from "@yoast/components"; import WincherSEOPerformanceLoading from "../../src/components/modals/WincherSEOPerformanceLoading"; +import { noop } from "lodash"; const keyphrasesData = { "yoast seo": { @@ -20,6 +29,7 @@ const keyphrasesData = { value: 38, }, ], + change: -2, }, // eslint-disable-next-line camelcase updated_at: new Date(), @@ -38,45 +48,128 @@ describe( "WincherTableRow", () => { const component = shallow( <WincherTableRow rowData={ keyphrasesData[ "woocommerce seo" ] } keyphrase="woocommerce seo" + isSelected={ false } + onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 3 ); - expect( component.find( "td" ).at( 1 ).text() ).toEqual( "woocommerce seo" ); - expect( component.find( "td" ).at( 2 ).getElement().props.children ).toEqual( <WincherSEOPerformanceLoading /> ); + expect( component.find( "td" ).length ).toEqual( 1 ); + expect( component.find( "td" ).at( 0 ).getElement().props.children ).toEqual( <WincherSEOPerformanceLoading /> ); + expect( component.find( SelectKeyphraseCheckboxWrapper ).length ).toEqual( 1 ); + expect( component.find( KeyphraseTdWrapper ).length ).toEqual( 1 ); + expect( component.find( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "woocommerce seo" ); + expect( component.find( TrackingTdWrapper ).length ).toEqual( 1 ); } ); it( "should render a row with the available data and with chart data", () => { const component = shallow( <WincherTableRow rowData={ keyphrasesData[ "yoast seo" ] } keyphrase="yoast seo" + isSelected={ false } + onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 5 ); + expect( component.find( "td" ).length ).toEqual( 3 ); expect( component.find( Toggle ).length ).toEqual( 1 ); - expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( PositionOverTimeCell ).length ).toEqual( 1 ); + expect( component.find( SelectKeyphraseCheckboxWrapper ).length ).toEqual( 1 ); + expect( component.find( KeyphraseTdWrapper ).length ).toEqual( 1 ); + expect( component.find( TrackingTdWrapper ).length ).toEqual( 1 ); expect( component.find( Toggle ).getElement().props.id ).toBe( "toggle-keyphrase-tracking-yoast seo" ); expect( component.find( Toggle ).getElement().props.isEnabled ).toBe( true ); expect( component.find( Toggle ).getElement().props.showToggleStateLabel ).toBe( false ); - expect( component.find( "td" ).at( 1 ).text() ).toEqual( "yoast seo" ); - expect( component.find( "td" ).at( 2 ).text() ).toEqual( "10" ); + expect( component.find( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "yoast seo" ); + expect( component.find( "td" ).at( 0 ).text() ).toContain( "10" ); + expect( component.find( "td" ).at( 2 ).text() ).toEqual( "a few seconds ago" ); } ); it( "should not render an enabled toggle or any position and chart data when no data is available", () => { const component = shallow( <WincherTableRow rowData={ {} } keyphrase="yoast seo" + isSelected={ false } + onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 5 ); + expect( component.find( "td" ).length ).toEqual( 1 ); expect( component.find( Toggle ).length ).toEqual( 1 ); - expect( component.find( PositionOverTimeChart ).length ).toEqual( 0 ); + expect( component.find( PositionOverTimeCell ).length ).toEqual( 0 ); + expect( component.find( TrackingTdWrapper ).length ).toEqual( 1 ); expect( component.find( Toggle ).getElement().props.id ).toBe( "toggle-keyphrase-tracking-yoast seo" ); expect( component.find( Toggle ).getElement().props.isEnabled ).toBe( false ); - expect( component.find( "td" ).at( 1 ).text() ).toEqual( "yoast seo" ); - expect( component.find( "td" ).at( 2 ).text() ).toEqual( "?" ); - expect( component.find( "td" ).at( 3 ).text() ).toEqual( "?" ); + expect( component.find( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "yoast seo" ); + expect( component.find( "td" ).at( 0 ).text() ).toEqual( "Activate tracking to show the ranking position" ); + } ); +} ); + + +describe( "PositionOverTimeCell", () => { + it( "should render chart but not change if undefined position change", () => { + const component = shallow( <PositionOverTimeCell + rowData={ { + position: { + value: 10, + history: [], + }, + } } + /> ); + + expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( CaretIcon ).length ).toEqual( 0 ); + expect( component.find( PositionChangeValue ).length ).toEqual( 0 ); + } ); + + it( "should render chart but not change if no position change", () => { + const component = shallow( <PositionOverTimeCell + rowData={ { + position: { + value: 10, + history: [], + change: 0, + }, + } } + /> ); + + expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( CaretIcon ).length ).toEqual( 0 ); + expect( component.find( PositionChangeValue ).length ).toEqual( 0 ); + } ); + + it( "should render chart and improving position change", () => { + const component = shallow( <PositionOverTimeCell + rowData={ { + position: { + value: 10, + history: [], + // improving + change: -2, + }, + } } + /> ); + + expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( CaretIcon ).getElement().props.isImproving ).toEqual( true ); + expect( component.find( PositionChangeValue ).getElement().props.isImproving ).toEqual( true ); + expect( component.find( PositionChangeValue ).text() ).toEqual( "2" ); + } ); + + it( "should render chart and declined position change", () => { + const component = shallow( <PositionOverTimeCell + rowData={ { + position: { + value: 10, + history: [], + // declined + change: 2, + }, + } } + /> ); + + expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( CaretIcon ).getElement().props.isImproving ).toEqual( false ); + expect( component.find( PositionChangeValue ).getElement().props.isImproving ).toEqual( false ); + expect( component.find( PositionChangeValue ).text() ).toEqual( "2" ); } ); } ); diff --git a/src/actions/wincher/wincher-account-action.php b/src/actions/wincher/wincher-account-action.php index ea9bb54cfd8..fd616841013 100644 --- a/src/actions/wincher/wincher-account-action.php +++ b/src/actions/wincher/wincher-account-action.php @@ -48,14 +48,16 @@ public function check_limit() { try { $results = $this->client->get( self::ACCOUNT_URL ); - $usage = $results['limits']['keywords']['usage']; - $limit = $results['limits']['keywords']['limit']; + $usage = $results['limits']['keywords']['usage']; + $limit = $results['limits']['keywords']['limit']; + $history = $results['limits']['history_days']; return (object) [ - 'canTrack' => \is_null( $limit ) || $usage < $limit, - 'limit' => $limit, - 'usage' => $usage, - 'status' => 200, + 'canTrack' => \is_null( $limit ) || $usage < $limit, + 'limit' => $limit, + 'usage' => $usage, + 'historyDays' => $history, + 'status' => 200, ]; } catch ( \Exception $e ) { return (object) [ diff --git a/src/actions/wincher/wincher-keyphrases-action.php b/src/actions/wincher/wincher-keyphrases-action.php index 28a5aa0b8a4..1de4f97e741 100644 --- a/src/actions/wincher/wincher-keyphrases-action.php +++ b/src/actions/wincher/wincher-keyphrases-action.php @@ -182,10 +182,11 @@ public function untrack_keyphrase( $keyphrase_id ) { * * @param array|null $used_keyphrases The currently used keyphrases. Optional. * @param string|null $permalink The current permalink. Optional. + * @param string|null $start_at The position start date. Optional. * * @return object The keyphrase chart data. */ - public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null ) { + public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null, $start_at = null ) { try { if ( $used_keyphrases === null ) { $used_keyphrases = $this->collect_all_keyphrases(); @@ -213,6 +214,7 @@ public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = nu [ 'keywords' => $used_keyphrases, 'url' => $permalink, + 'start_at' => $start_at, ] ), [ diff --git a/src/routes/wincher-route.php b/src/routes/wincher-route.php index 9c1c0126031..85c3ad72ccd 100644 --- a/src/routes/wincher-route.php +++ b/src/routes/wincher-route.php @@ -173,6 +173,9 @@ public function register_routes() { 'permalink' => [ 'required' => false, ], + 'startAt' => [ + 'required' => false, + ], ], ]; @@ -256,7 +259,7 @@ public function track_keyphrases( WP_REST_Request $request ) { * @return WP_REST_Response The response. */ public function get_tracked_keyphrases( WP_REST_Request $request ) { - $data = $this->keyphrases_action->get_tracked_keyphrases( $request['keyphrases'], $request['permalink'] ); + $data = $this->keyphrases_action->get_tracked_keyphrases( $request['keyphrases'], $request['permalink'], $request['startAt'] ); return new WP_REST_Response( $data, $data->status ); } diff --git a/tests/unit/actions/wincher/wincher-account-action-test.php b/tests/unit/actions/wincher/wincher-account-action-test.php index 7c6855f404f..c9e98f544f8 100644 --- a/tests/unit/actions/wincher/wincher-account-action-test.php +++ b/tests/unit/actions/wincher/wincher-account-action-test.php @@ -81,10 +81,11 @@ public function test_check_limit() { ->andReturn( [ 'limits' => [ - 'keywords' => [ + 'keywords' => [ 'usage' => 10, 'limit' => 100, ], + 'history_days' => 31, ], 'status' => 200, ] @@ -92,10 +93,11 @@ public function test_check_limit() { $this->assertEquals( (object) [ - 'canTrack' => true, - 'limit' => 100, - 'usage' => 10, - 'status' => 200, + 'canTrack' => true, + 'limit' => 100, + 'usage' => 10, + 'status' => 200, + 'historyDays' => 31, ], $this->instance->check_limit() ); @@ -113,10 +115,11 @@ public function test_invalid_check_limit() { ->andReturn( [ 'limits' => [ - 'keywords' => [ + 'keywords' => [ 'usage' => 100, 'limit' => 100, ], + 'history_days' => 31, ], 'status' => 200, ] @@ -124,10 +127,11 @@ public function test_invalid_check_limit() { $this->assertEquals( (object) [ - 'canTrack' => false, - 'limit' => 100, - 'usage' => 100, - 'status' => 200, + 'canTrack' => false, + 'limit' => 100, + 'usage' => 100, + 'status' => 200, + 'historyDays' => 31, ], $this->instance->check_limit() ); @@ -145,10 +149,11 @@ public function test_unlimited_check_limit() { ->andReturn( [ 'limits' => [ - 'keywords' => [ + 'keywords' => [ 'usage' => 100000, 'limit' => null, ], + 'history_days' => 31, ], 'status' => 200, ] @@ -156,10 +161,11 @@ public function test_unlimited_check_limit() { $this->assertEquals( (object) [ - 'canTrack' => true, - 'limit' => null, - 'usage' => 100000, - 'status' => 200, + 'canTrack' => true, + 'limit' => null, + 'usage' => 100000, + 'status' => 200, + 'historyDays' => 31, ], $this->instance->check_limit() ); diff --git a/tests/unit/actions/wincher/wincher-keyphrases-action-test.php b/tests/unit/actions/wincher/wincher-keyphrases-action-test.php index 03ddc04fcca..812140c2013 100644 --- a/tests/unit/actions/wincher/wincher-keyphrases-action-test.php +++ b/tests/unit/actions/wincher/wincher-keyphrases-action-test.php @@ -264,6 +264,7 @@ public function test_get_tracked_keyphrases() { [ 'keywords' => [ 'yoast seo', 'wincher' ], 'url' => null, + 'start_at' => null, ] ), [ @@ -324,6 +325,7 @@ public function test_get_tracked_keyphrases_no_data_key() { [ 'keywords' => [ 'yoast seo' ], 'url' => null, + 'start_at' => null, ] ), [ @@ -384,6 +386,7 @@ public function test_get_tracked_keyphrases_filtered_by_used_keyphrases() { [ 'keywords' => [ 'yoast seo' ], 'url' => null, + 'start_at' => null, ] ), [ @@ -440,6 +443,7 @@ public function test_get_tracked_keyphrases_with_permalink() { [ 'keywords' => [ 'yoast seo', 'blog seo' ], 'url' => 'https://yoast.com/blog/', + 'start_at' => null, ] ), [ diff --git a/tests/unit/routes/wincher-route-test.php b/tests/unit/routes/wincher-route-test.php index 30cc30cbcf8..d3320d57d43 100644 --- a/tests/unit/routes/wincher-route-test.php +++ b/tests/unit/routes/wincher-route-test.php @@ -168,6 +168,9 @@ public function test_register_routes() { 'permalink' => [ 'required' => false, ], + 'startAt' => [ + 'required' => false, + ], ], ] ); @@ -357,11 +360,17 @@ public function test_get_tracked_keyphrases() { ->with( 'keyphrases' ) ->andReturn( [ 'seo' ] ); + $request + ->expects( 'offsetGet' ) + ->with( 'startAt' ) + ->andReturn( '2023-01-01' ); + $this->keyphrases_action ->expects( 'get_tracked_keyphrases' ) ->with( [ 'seo' ], - 'https://example.com' + 'https://example.com', + '2023-01-01' ) ->andReturn( (object) [ 'status' => '200' ] ); @@ -387,11 +396,17 @@ public function test_get_tracked_keyphrases_without_permalink() { ->with( 'keyphrases' ) ->andReturn( [ 'seo' ] ); + $request + ->expects( 'offsetGet' ) + ->with( 'startAt' ) + ->andReturn( '2023-01-01' ); + $this->keyphrases_action ->expects( 'get_tracked_keyphrases' ) ->with( [ 'seo' ], - '' + '', + '2023-01-01' ) ->andReturn( (object) [ 'status' => '200' ] ); diff --git a/yarn.lock b/yarn.lock index b472ce0274b..b586be195fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,6 +16,14 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + "@axe-core/puppeteer@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@axe-core/puppeteer/-/puppeteer-4.4.0.tgz#7849cd1636d2e82c837ca91d3567e38c852e9957" @@ -67,6 +75,21 @@ dependencies: "@babel/highlight" "^7.18.6" +"@babel/code-frame@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.21.4.tgz#d0fa9e4413aca81f2b23b9442797bda1826edb39" + integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.13.11": version "7.13.15" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4" @@ -92,6 +115,11 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== +"@babel/compat-data@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.21.4.tgz#457ffe647c480dff59c2be092fc3acf71195c87f" + integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== + "@babel/core@7.12.9": version "7.12.9" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.12.9.tgz#fd450c4ec10cdbb980e2928b7aa7a28484593fc8" @@ -199,46 +227,25 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/core@^7.17.9": - version "7.17.9" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.17.9.tgz#6bae81a06d95f4d0dec5bb9d74bbc1f58babdcfe" - integrity sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.9" - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-module-transforms" "^7.17.7" - "@babel/helpers" "^7.17.9" - "@babel/parser" "^7.17.9" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.17.9" - "@babel/types" "^7.17.0" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/core@^7.18.5": - version "7.18.5" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" - integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.18.2" - "@babel/helper-compilation-targets" "^7.18.2" - "@babel/helper-module-transforms" "^7.18.0" - "@babel/helpers" "^7.18.2" - "@babel/parser" "^7.18.5" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.18.5" - "@babel/types" "^7.18.4" +"@babel/core@^7.17.9", "@babel/core@^7.18.5": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.21.4.tgz#c6dc73242507b8e2a27fd13a9c1814f9fa34a659" + integrity sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.4" + "@babel/helper-compilation-targets" "^7.21.4" + "@babel/helper-module-transforms" "^7.21.2" + "@babel/helpers" "^7.21.0" + "@babel/parser" "^7.21.4" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.4" + "@babel/types" "^7.21.4" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" - json5 "^2.2.1" + json5 "^2.2.2" semver "^6.3.0" "@babel/core@^7.20.5": @@ -369,15 +376,6 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/generator@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.2.tgz#33873d6f89b21efe2da63fe554460f3df1c5880d" - integrity sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw== - dependencies: - "@babel/types" "^7.18.2" - "@jridgewell/gen-mapping" "^0.3.0" - jsesc "^2.5.1" - "@babel/generator@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" @@ -387,6 +385,16 @@ "@jridgewell/gen-mapping" "^0.3.2" jsesc "^2.5.1" +"@babel/generator@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" + integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== + dependencies: + "@babel/types" "^7.21.4" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz#0f58e86dfc4bb3b1fcd7db806570e177d439b6ab" @@ -408,6 +416,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz#38d138561ea207f0f69eb1626a418e4f7e6a580b" @@ -426,7 +441,7 @@ browserslist "^4.14.5" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.17.7": +"@babel/helper-compilation-targets@^7.16.7": version "7.17.7" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz#a3c2924f5e5f0379b356d4cfb313d1414dc30e46" integrity sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w== @@ -446,16 +461,6 @@ browserslist "^4.20.2" semver "^6.3.0" -"@babel/helper-compilation-targets@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.2.tgz#67a85a10cbd5fc7f1457fec2e7f45441dc6c754b" - integrity sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ== - dependencies: - "@babel/compat-data" "^7.17.10" - "@babel/helper-validator-option" "^7.16.7" - browserslist "^4.20.2" - semver "^6.3.0" - "@babel/helper-compilation-targets@^7.20.0": version "7.20.0" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" @@ -466,6 +471,17 @@ browserslist "^4.21.3" semver "^6.3.0" +"@babel/helper-compilation-targets@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz#770cd1ce0889097ceacb99418ee6934ef0572656" + integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== + dependencies: + "@babel/compat-data" "^7.21.4" + "@babel/helper-validator-option" "^7.21.0" + browserslist "^4.21.3" + lru-cache "^5.1.1" + semver "^6.3.0" + "@babel/helper-create-class-features-plugin@^7.16.10", "@babel/helper-create-class-features-plugin@^7.16.7", "@babel/helper-create-class-features-plugin@^7.17.1", "@babel/helper-create-class-features-plugin@^7.17.6": version "7.17.6" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz#3778c1ed09a7f3e65e6d6e0f6fbfcc53809d92c9" @@ -492,6 +508,21 @@ "@babel/helper-replace-supers" "^7.19.1" "@babel/helper-split-export-declaration" "^7.18.6" +"@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.12.13": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz#a2ac87e9e319269ac655b8d4415e94d38d663cb7" @@ -543,16 +574,16 @@ dependencies: "@babel/types" "^7.16.7" -"@babel/helper-environment-visitor@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.2.tgz#8a6d2dedb53f6bf248e31b4baf38739ee4a637bd" - integrity sha512-14GQKWkX9oJzPiQQ7/J36FTXcD4kSp8egKjO9nINlSKiHITRA9q/R74qu8S9xlc/b/yjsJItQUeeh3xnGN0voQ== - "@babel/helper-environment-visitor@^7.18.9": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-explode-assignable-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz#12a6d8522fdd834f194e868af6354e8650242b7a" @@ -585,6 +616,22 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" +"@babel/helper-function-name@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" + integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== + dependencies: + "@babel/template" "^7.20.7" + "@babel/types" "^7.21.0" + +"@babel/helper-function-name@^7.22.5": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-get-function-arity@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419" @@ -627,6 +674,13 @@ dependencies: "@babel/types" "^7.18.9" +"@babel/helper-member-expression-to-functions@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz#c6a369a6f3621cb25da014078684da9196b61977" @@ -683,20 +737,6 @@ "@babel/traverse" "^7.13.0" "@babel/types" "^7.13.12" -"@babel/helper-module-transforms@^7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.18.0.tgz#baf05dec7a5875fb9235bd34ca18bad4e21221cd" - integrity sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA== - dependencies: - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-module-imports" "^7.16.7" - "@babel/helper-simple-access" "^7.17.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/helper-validator-identifier" "^7.16.7" - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.18.0" - "@babel/types" "^7.18.0" - "@babel/helper-module-transforms@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" @@ -711,6 +751,20 @@ "@babel/traverse" "^7.20.1" "@babel/types" "^7.20.2" +"@babel/helper-module-transforms@^7.21.2": + version "7.21.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" + integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.2" + "@babel/types" "^7.21.2" + "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz#5c02d171b4c8615b1e7163f888c1c81c30a2aaea" @@ -732,6 +786,13 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-plugin-utils@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz#2f75a831269d4f677de49986dff59927533cf375" @@ -757,6 +818,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== +"@babel/helper-plugin-utils@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + "@babel/helper-remap-async-to-generator@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz#29ffaade68a367e2ed09c90901986918d25e57e3" @@ -798,6 +864,15 @@ "@babel/traverse" "^7.19.1" "@babel/types" "^7.19.0" +"@babel/helper-replace-supers@^7.22.9": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-simple-access@^7.13.12": version "7.13.12" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz#dd6c538afb61819d205a012c31792a39c7a5eaf6" @@ -826,6 +901,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-split-export-declaration@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05" @@ -847,11 +929,23 @@ dependencies: "@babel/types" "^7.18.6" +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + "@babel/helper-string-parser@^7.19.4": version "7.19.4" resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + "@babel/helper-validator-identifier@^7.10.4", "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" @@ -867,6 +961,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" @@ -882,6 +981,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== +"@babel/helper-validator-option@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" + integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== + "@babel/helper-wrap-function@^7.16.8": version "7.16.8" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz#58afda087c4cd235de92f7ceedebca2c41274200" @@ -919,15 +1023,6 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" -"@babel/helpers@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.18.2.tgz#970d74f0deadc3f5a938bfa250738eb4ac889384" - integrity sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg== - dependencies: - "@babel/template" "^7.16.7" - "@babel/traverse" "^7.18.2" - "@babel/types" "^7.18.2" - "@babel/helpers@^7.20.5": version "7.20.6" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" @@ -937,6 +1032,15 @@ "@babel/traverse" "^7.20.5" "@babel/types" "^7.20.5" +"@babel/helpers@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.21.0.tgz#9dd184fb5599862037917cdc9eecb84577dc4e7e" + integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== + dependencies: + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.21.0" + "@babel/types" "^7.21.0" + "@babel/highlight@^7.10.4", "@babel/highlight@^7.12.13": version "7.13.10" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" @@ -964,6 +1068,15 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.0.0": version "7.4.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.5.tgz#04af8d5d5a2b044a2a1bffacc1e5e6673544e872" @@ -999,10 +1112,15 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== -"@babel/parser@^7.18.5": - version "7.18.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.5.tgz#337062363436a893a2d22faa60be5bb37091c83c" - integrity sha512-YZWVaglMiplo7v8f1oMQ5ZPQr0vn7HPeZXxXWsxXJRjGVrzUFn9OxFQl1sb5wzfootjA/yChhW84BV+383FSOw== +"@babel/parser@^7.20.7", "@babel/parser@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" + integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== + +"@babel/parser@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" @@ -1401,6 +1519,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-transform-classes@^7.12.1", "@babel/plugin-transform-classes@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz#8f4b9562850cd973de3b498f1218796eb181ce00" @@ -2029,6 +2155,24 @@ "@babel/parser" "^7.18.10" "@babel/types" "^7.18.10" +"@babel/template@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" + integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.6", "@babel/traverse@^7.12.11", "@babel/traverse@^7.12.9", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.17.3", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.2": version "7.17.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.3.tgz#0ae0f15b27d9a92ba1f2263358ea7c4e7db47b57" @@ -2093,22 +2237,6 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.18.0", "@babel/traverse@^7.18.2", "@babel/traverse@^7.18.5": - version "7.18.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.5.tgz#94a8195ad9642801837988ab77f36e992d9a20cd" - integrity sha512-aKXj1KT66sBj0vVzk6rEeAO6Z9aiiQ68wfDgge3nHhA/my6xMM/7HGQUNumKZaoa2qUPQ5whJG9aAifsxUKfLA== - dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.18.2" - "@babel/helper-environment-visitor" "^7.18.2" - "@babel/helper-function-name" "^7.17.9" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.18.5" - "@babel/types" "^7.18.4" - debug "^4.1.0" - globals "^11.1.0" - "@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" @@ -2125,6 +2253,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" + integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== + dependencies: + "@babel/code-frame" "^7.21.4" + "@babel/generator" "^7.21.4" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.21.4" + "@babel/types" "^7.21.4" + debug "^4.1.0" + globals "^11.1.0" + "@babel/types@^7.0.0", "@babel/types@^7.3.0": version "7.3.4" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.3.4.tgz#bf482eaeaffb367a28abbf9357a94963235d90ed" @@ -2159,14 +2303,6 @@ "@babel/helper-validator-identifier" "^7.16.7" to-fast-properties "^2.0.0" -"@babel/types@^7.18.0", "@babel/types@^7.18.2", "@babel/types@^7.18.4": - version "7.18.4" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.4.tgz#27eae9b9fd18e9dccc3f9d6ad051336f307be354" - integrity sha512-ThN1mBcMq5pG/Vm2IcBmPPfyPXbd8S02rS+OBIDENdufvqC7Z/jHPCv9IcP01277aKtDI8g/2XysBN4hA8niiw== - dependencies: - "@babel/helper-validator-identifier" "^7.16.7" - to-fast-properties "^2.0.0" - "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" @@ -2176,6 +2312,24 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.21.4.tgz#2d5d6bb7908699b3b416409ffd3b5daa25b030d4" + integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" @@ -2949,6 +3103,14 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jridgewell/trace-mapping@^0.3.17": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz#25783b2086daf6ff1dcb53c9249ae480e4dd4cd6" + integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + "@jridgewell/trace-mapping@^0.3.8", "@jridgewell/trace-mapping@^0.3.9": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz#dcfe3e95f224c8fe97a87a5235defec999aa92ea" @@ -2957,6 +3119,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@lerna/add@6.4.1": version "6.4.1" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-6.4.1.tgz#fa20fe9ff875dc5758141262c8cde0d9a6481ec4" @@ -10842,6 +11009,18 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= +chart.js@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.2.1.tgz#d2bd5c98e9a0ae35408975b638f40513b067ba1d" + integrity sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw== + dependencies: + "@kurkle/color" "^0.3.0" + +chartjs-adapter-moment@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz#0f04c30d330b207c14bfb57dfaae9ce332f09102" + integrity sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA== + check-node-version@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/check-node-version/-/check-node-version-4.1.0.tgz#12ff45bfeb8dd591700a0ab848c21b2d8ceeeb94" @@ -26418,6 +26597,11 @@ react-base16-styling@^0.5.1: lodash.flow "^3.3.0" pure-color "^1.2.0" +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + react-colorful@4.4.4: version "4.4.4" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-4.4.4.tgz#38e7c5b7075bbf63d3cce22d8c61a439a58b7561" @@ -28268,6 +28452,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.0.0, semver@^7.3.7: version "7.3.8" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
- { __( "Tracking", "wordpress-seo" ) } - + + + { __( "Keyphrase", "wordpress-seo" ) } - { > { __( "Position over time", "wordpress-seo" ) } + { __( "Last updated", "wordpress-seo" ) } + + { __( "Tracking", "wordpress-seo" ) } +