From 592437a3bf30d68da85cb52e01a86dffe57f1725 Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 2 Oct 2023 14:31:52 +0200 Subject: [PATCH 01/55] add prop type to label in select component --- packages/ui-library/src/elements/select/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui-library/src/elements/select/index.js b/packages/ui-library/src/elements/select/index.js index 433a4cbadd4..e65aa671d16 100644 --- a/packages/ui-library/src/elements/select/index.js +++ b/packages/ui-library/src/elements/select/index.js @@ -14,7 +14,7 @@ const optionPropType = { /** * @param {string|number|boolean} value Value. - * @param {string} label Label. + * @param {string | JSX.Element} label Label. * @returns {JSX.Element} The option. */ const Option = ( { value, label } ) => { @@ -133,7 +133,10 @@ const propTypes = { options: PropTypes.arrayOf( PropTypes.shape( optionPropType ) ), children: PropTypes.node, selectedLabel: PropTypes.string, - label: PropTypes.string, + label: PropTypes.oneOfType( [ + PropTypes.string, + PropTypes.element, + ] ).isRequired, labelProps: PropTypes.object, labelSuffix: PropTypes.node, onChange: PropTypes.func.isRequired, From 96767856ddcbac3fe4a2f5363a213954f775eaf4 Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 2 Oct 2023 14:32:26 +0200 Subject: [PATCH 02/55] Add prop type element to selectField component --- packages/ui-library/src/components/select-field/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ui-library/src/components/select-field/index.js b/packages/ui-library/src/components/select-field/index.js index 1abfc2adda1..89dae6f7b89 100644 --- a/packages/ui-library/src/components/select-field/index.js +++ b/packages/ui-library/src/components/select-field/index.js @@ -9,7 +9,7 @@ import { forwardRef } from "@wordpress/element"; * @param {JSX.Element} error Error node. * @param {string} [className] Optional CSS class. * @param {boolean} [disabled] Disabled state. - * @param {string} label Label. + * @param {string | JSX.Element} label Label. * @param {JSX.node} [description] Optional description. * @param {Object} [props] Any extra props. * @returns {JSX.Element} SelectField component. @@ -54,7 +54,10 @@ const SelectField = forwardRef( ( { const propTypes = { id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, + label: PropTypes.oneOfType( [ + PropTypes.string, + PropTypes.element, + ] ).isRequired, description: PropTypes.node, disabled: PropTypes.bool, validation: PropTypes.shape( { From e450a59cc9460a2e80173f2ebb79b12a1a91765d Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Mon, 2 Oct 2023 14:30:03 -0600 Subject: [PATCH 03/55] Leverage Script Strategy feature to add async to the wordprof script --- src/integrations/third-party/wordproof.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/integrations/third-party/wordproof.php b/src/integrations/third-party/wordproof.php index c9d63ba1d4b..5d7357ff138 100644 --- a/src/integrations/third-party/wordproof.php +++ b/src/integrations/third-party/wordproof.php @@ -92,7 +92,11 @@ public function register_hooks() { /** * Add async to the wordproof scripts. */ - \add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 ); + if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ) ) { + wp_script_add_data( WPSEO_Admin_Asset_Manager::PREFIX . 'wordproof-uikit', 'strategy', 'async' ); + } else { + \add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 ); + } /** * Removes the post meta timestamp key for the old privacy page. From d10b80c0f6708d6fc06fb33264a148a718e187db Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 9 Oct 2023 12:25:49 +0200 Subject: [PATCH 04/55] Add an abstract class for excluding taxonomy --- .../abstract-exclude-taxonomy.php | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/integrations/abstract-exclude-taxonomy.php diff --git a/src/integrations/abstract-exclude-taxonomy.php b/src/integrations/abstract-exclude-taxonomy.php new file mode 100644 index 00000000000..c6f9a7704b1 --- /dev/null +++ b/src/integrations/abstract-exclude-taxonomy.php @@ -0,0 +1,44 @@ +get_taxonomy() ); + } + + /** + * This integration is only active when the child class's conditionals are met. + * + * @return array|string[] The conditionals. + */ + public static function get_conditionals() { + return []; + } + + /** + * Returns the names of the taxonomies to be excluded. + * To be used in the wpseo_indexable_excluded_taxonomies filter. + * + * @return array The names of the taxonomies. + */ + abstract public function get_taxonomy(); +} From 55e0191665d4788f1fb4701af0a4e3bca3ab6b5b Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 9 Oct 2023 12:26:32 +0200 Subject: [PATCH 05/55] add exclude gutenberg taxonomy class --- .../exclude-gutenberg-taxonomy.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/integrations/third-party/exclude-gutenberg-taxonomy.php diff --git a/src/integrations/third-party/exclude-gutenberg-taxonomy.php b/src/integrations/third-party/exclude-gutenberg-taxonomy.php new file mode 100644 index 00000000000..4b6a566421a --- /dev/null +++ b/src/integrations/third-party/exclude-gutenberg-taxonomy.php @@ -0,0 +1,23 @@ + Date: Mon, 9 Oct 2023 13:17:01 +0200 Subject: [PATCH 06/55] Prevents a null-pointer exception when the attribute is not available --- packages/js/src/decorator/helpers/getAnnotationsHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/decorator/helpers/getAnnotationsHelpers.js b/packages/js/src/decorator/helpers/getAnnotationsHelpers.js index bc53672b285..7966d0c6fa0 100644 --- a/packages/js/src/decorator/helpers/getAnnotationsHelpers.js +++ b/packages/js/src/decorator/helpers/getAnnotationsHelpers.js @@ -183,7 +183,7 @@ export const getAnnotationsForFAQ = ( attributeWithAnnotationSupport, block, mar */ export const getAnnotationsForHowTo = ( attributeWithAnnotationSupport, block, marks ) => { const annotatableTextsFromBlock = block.attributes[ attributeWithAnnotationSupport.key ]; - if ( annotatableTextsFromBlock.length === 0 ) { + if ( annotatableTextsFromBlock && annotatableTextsFromBlock.length === 0 ) { return []; } const annotations = []; From 9818457086a2f678b7bdf11926310bb273c4730d Mon Sep 17 00:00:00 2001 From: Martijn van der Klis Date: Mon, 9 Oct 2023 13:29:44 +0200 Subject: [PATCH 07/55] Adds support for
tags: considers them as line breaks and updates the positioning information at relevant points --- .../helpers/positionBasedAnnotationHelper.js | 14 +- .../researches/keywordCountSpec.js | 137 +++++++++++------- .../private/getTextElementPositionsSpec.js | 15 ++ .../spec/parse/build/private/tokenizeSpec.js | 24 +++ .../spec/parse/traverse/innerTextSpec.js | 11 ++ .../seo/KeywordDensityAssessmentSpec.js | 2 +- .../build/private/getTextElementPositions.js | 13 +- .../yoastseo/src/parse/traverse/innerText.js | 2 + 8 files changed, 163 insertions(+), 55 deletions(-) diff --git a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js index 9163801318b..b8dc54bd1f7 100644 --- a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js +++ b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js @@ -83,13 +83,23 @@ const adjustOffsetsForHtmlTags = ( slicedBlockHtmlToStartOffset, slicedBlockHtml let foundHtmlTags = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ]; forEachRight( foundHtmlTags, ( foundHtmlTag ) => { const [ tag ] = foundHtmlTag; - blockStartOffset -= tag.length; + let tagLength = tag.length; + // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. + if ( /<\/?br/.test( tag ) ) { + tagLength -= 1; + } + blockStartOffset -= tagLength; } ); foundHtmlTags = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ]; forEachRight( foundHtmlTags, ( foundHtmlTag ) => { const [ tag ] = foundHtmlTag; - blockEndOffset -= tag.length; + let tagLength = tag.length; + // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. + if ( /<\/?br/.test( tag ) ) { + tagLength -= 1; + } + blockEndOffset -= tagLength; } ); return { blockStartOffset, blockEndOffset }; diff --git a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js index c67344bc7bb..ae90d4f79fd 100644 --- a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js +++ b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js @@ -953,12 +953,47 @@ const testCasesWithSpecialCharacters = [ ], skip: false, }, + { + description: "can match keyphrases in texts that contain
tags", + paper: new Paper( "

This is a test.
This is
another
test.

", { keyword: "test" } ), + keyphraseForms: [ [ "test" ] ], + expectedCount: 2, + expectedMarkings: [ + new Mark( { + original: "This is a test.", + marked: "This is a test.", + position: { + startOffset: 13, + endOffset: 17, + startOffsetBlock: 10, + endOffsetBlock: 14, + attributeId: "", + clientId: "", + isFirstSection: false, + } } + ), + new Mark( { + original: "\nThis is\nanother\ntest.", + marked: "\nThis is\nanother\ntest.", + position: { + startOffset: 46, + endOffset: 50, + startOffsetBlock: 43, + endOffsetBlock: 47, + attributeId: "", + clientId: "", + isFirstSection: false, + } } + ), + ], + skip: false, + }, { description: "can match paragraph gutenberg block", paper: new Paper( - ` -

a string of text with the keyword in it

- `, + "" + + "

a string of text with the keyword in it

" + + "", { keyword: "keyword", wpBlocks: [ @@ -980,8 +1015,8 @@ const testCasesWithSpecialCharacters = [ marked: "a string of text with the keyword in it", original: "a string of text with the keyword in it", position: { - startOffset: 55, - endOffset: 62, + startOffset: 50, + endOffset: 57, startOffsetBlock: 26, endOffsetBlock: 33, attributeId: "", @@ -995,16 +1030,16 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - ` -

Over the years, we’ve written quite a few articles about  - branding. - Branding is about getting people to relate to your company and - products. It’s also about trying to make your brand synonymous with a certain product or service. - This can be a lengthy and hard project. It can potentially cost you all of your revenue. - It’s no wonder that branding is often associated with investing lots of money in marketing and promotion. - However, for a lot of small business owners, the investment in branding will have - to be made with a relatively small budget. 

- `, + "" + + "

Over the years, we’ve written quite a few articles about " + + 'branding. ' + + "Branding is about getting people to relate to your company and products. " + + "It’s also about trying to make your brand synonymous with a certain product or service. " + + "This can be a lengthy and hard project. It can potentially cost you all of your revenue. " + + "It’s no wonder that branding is often associated with investing lots of money in marketing and promotion. " + + "However, for a lot of small business owners, the investment in branding will have " + + "to be made with a relatively small budget. 

" + + "", { keyword: "keyword", wpBlocks: [ @@ -1029,10 +1064,10 @@ const testCasesWithSpecialCharacters = [ marked: " It's also about trying to make your brand synonymous with a certain product or service.", original: " It’s also about trying to make your brand synonymous with a certain product or service.", position: { - startOffset: 295, - endOffset: 305, - startOffsetBlock: 266, - endOffsetBlock: 276, + startOffset: 277, + endOffset: 287, + startOffsetBlock: 253, + endOffsetBlock: 263, attributeId: "", clientId: "", isFirstSection: false, @@ -1044,13 +1079,13 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - ` -

You might be a local bakery with 10 employees, or a local industrial company employing up to 500 people. - These all can be qualified as - ‘small business’. All have the same main goal when they start: the need to establish a name in their field of expertise. There - are multiple ways to do this, without a huge budget. - In this post, I’ll share my thoughts on how to go about your own low-budget branding.

- `, + "" + + "

You might be a local bakery with 10 employees, or a local industrial company employing up to 500 people. " + + "These all can be qualified as ‘small business’. " + + "All have the same main goal when they start: the need to establish a name in their field of expertise. " + + "There are multiple ways to do this, without a huge budget. " + + "In this post, I’ll share my thoughts on how to go about your own low-budget branding.

" + + "", { keyword: "expertise", wpBlocks: [ @@ -1075,10 +1110,10 @@ const testCasesWithSpecialCharacters = [ marked: " All have the same main goal when they start: the need to establish a name in their field of expertise.", original: " All have the same main goal when they start: the need to establish a name in their field of expertise.", position: { - startOffset: 282, - endOffset: 291, - startOffsetBlock: 253, - endOffsetBlock: 262, + startOffset: 269, + endOffset: 278, + startOffsetBlock: 245, + endOffsetBlock: 254, attributeId: "", clientId: "", isFirstSection: false, @@ -1090,9 +1125,9 @@ const testCasesWithSpecialCharacters = [ { description: "can match heading gutenberg block", paper: new Paper( - ` -

Define and communicate brand values

- `, + "" + + '

Define and communicate brand values

' + + "", { keyword: "communicate", wpBlocks: [ @@ -1116,8 +1151,8 @@ const testCasesWithSpecialCharacters = [ marked: "Define and communicate brand values", original: "Define and communicate brand values", position: { - startOffset: 107, - endOffset: 118, + startOffset: 102, + endOffset: 113, startOffsetBlock: 11, endOffsetBlock: 22, attributeId: "", @@ -1131,19 +1166,19 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - ` -
-
-

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

-
- + "" + + '
' + + '
' + + "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

" + + "
" + + "" + - -
-

There are many variations of passages of Lorem Ipsum available

-
-
- `, + "" + + '
' + + "

There are many variations of passages of Lorem Ipsum available

" + + "
" + + "
" + + "", { keyword: "Ipsum", wpBlocks: [ @@ -1195,8 +1230,8 @@ const testCasesWithSpecialCharacters = [ marked: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", original: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", position: { - startOffset: 149, - endOffset: 154, + startOffset: 134, + endOffset: 139, startOffsetBlock: 14, endOffsetBlock: 19, attributeId: "", @@ -1208,8 +1243,8 @@ const testCasesWithSpecialCharacters = [ marked: "There are many variations of passages of Lorem Ipsum available", original: "There are many variations of passages of Lorem Ipsum available", position: { - startOffset: 426, - endOffset: 431, + startOffset: 385, + endOffset: 390, startOffsetBlock: 47, endOffsetBlock: 52, attributeId: "", diff --git a/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js b/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js index 4fb3b9c6a29..c33399e13d1 100644 --- a/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js +++ b/packages/yoastseo/spec/parse/build/private/getTextElementPositionsSpec.js @@ -141,6 +141,21 @@ describe( "A test for getting positions of sentences", () => { expect( getTextElementPositions( node, sentences ) ).toEqual( sentencesWithPositions ); } ); + it( "should determine the correct token positions when the sentence contains a br tag", function() { + // HTML:

Hello
world!

. + const html = "

Hello
world!

"; + const tree = adapt( parseFragment( html, { sourceCodeLocationInfo: true } ) ); + const paragraph = tree.childNodes[ 0 ]; + const tokens = [ "Hello", "\n", "world", "!" ].map( string => new Token( string ) ); + + const [ hello, br, world, bang ] = getTextElementPositions( paragraph, tokens, 3 ); + + expect( hello.sourceCodeRange ).toEqual( { startOffset: 3, endOffset: 8 } ); + expect( br.sourceCodeRange ).toEqual( { startOffset: 13, endOffset: 14 } ); + expect( world.sourceCodeRange ).toEqual( { startOffset: 14, endOffset: 19 } ); + expect( bang.sourceCodeRange ).toEqual( { startOffset: 19, endOffset: 20 } ); + } ); + it( "gets the sentence positions from a node that has a code child node", function() { // HTML:

Hello array.push( something ) code!

const node = new Paragraph( {}, [ diff --git a/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js b/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js index 979f1355f0c..a00d2825961 100644 --- a/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js +++ b/packages/yoastseo/spec/parse/build/private/tokenizeSpec.js @@ -477,6 +477,30 @@ describe( "A test for the tokenize function", name: "#document-fragment", } ); } ); + + it( "should correctly tokenize a paragraph with single br tags", function() { + const mockPaper = new Paper( "

This is a sentence.
This is
another sentence.

" ); + const mockResearcher = new EnglishResearcher( mockPaper ); + const languageProcessor = new LanguageProcessor( mockResearcher ); + buildTreeNoTokenize( mockPaper ); + const result = tokenize( mockPaper.getTree(), languageProcessor ); + const sentences = result.childNodes[ 0 ].sentences; + expect( sentences.length ).toEqual( 2 ); + const firstSentence = sentences[ 0 ]; + expect( firstSentence.text ).toEqual( "This is a sentence." ); + expect( firstSentence.sourceCodeRange ).toEqual( { startOffset: 3, endOffset: 22 } ); + expect( firstSentence.tokens.length ).toEqual( 8 ); + const secondSentence = sentences[ 1 ]; + expect( secondSentence.text ).toEqual( "\nThis is\nanother sentence." ); + expect( secondSentence.sourceCodeRange ).toEqual( { startOffset: 27, endOffset: 56 } ); + expect( secondSentence.tokens.length ).toEqual( 9 ); + const [ br1, this1, , is1, br2, another1, , , ] = secondSentence.tokens; + expect( br1.sourceCodeRange ).toEqual( { startOffset: 27, endOffset: 28 } ); + expect( this1.sourceCodeRange ).toEqual( { startOffset: 28, endOffset: 32 } ); + expect( is1.sourceCodeRange ).toEqual( { startOffset: 33, endOffset: 35 } ); + expect( br2.sourceCodeRange ).toEqual( { startOffset: 38, endOffset: 39 } ); + expect( another1.sourceCodeRange ).toEqual( { startOffset: 39, endOffset: 46 } ); + } ); } ); describe( "A test for tokenizing a Japanese sentence", function() { diff --git a/packages/yoastseo/spec/parse/traverse/innerTextSpec.js b/packages/yoastseo/spec/parse/traverse/innerTextSpec.js index 39e1504f105..4afc8e40d89 100644 --- a/packages/yoastseo/spec/parse/traverse/innerTextSpec.js +++ b/packages/yoastseo/spec/parse/traverse/innerTextSpec.js @@ -46,6 +46,17 @@ describe( "A test for innerText", () => { expect( searchResult ).toEqual( expected ); } ); + it( "should consider break tags to be line breaks", function() { + // Matsuo Bashō's "old pond". + paper._text = "

old pond
frog leaps in
water's sound

"; + const tree = build( paper, languageProcessor ); + + const searchResult = innerText( tree ); + const expected = "old pond\nfrog leaps in\nwater's sound"; + + expect( searchResult ).toEqual( expected ); + } ); + it( "should return an empty string when presented an empty node", function() { const tree = new Node( "" ); diff --git a/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js b/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js index 4b774ada4e2..27e0a372a45 100644 --- a/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js +++ b/packages/yoastseo/spec/scoring/assessments/seo/KeywordDensityAssessmentSpec.js @@ -320,7 +320,7 @@ describe( "A test for marking the keyphrase", function() { const keyphraseDensityAssessment = new KeyphraseDensityAssessment(); const paper = new Paper( "

a different cat with toy " + - "A flamboyant cat with a toy
\n" + + "A flamboyant cat with a toy
" + "

", { keyword: "cat toy" } ); const researcher = new EnglishResearcher( paper ); diff --git a/packages/yoastseo/src/parse/build/private/getTextElementPositions.js b/packages/yoastseo/src/parse/build/private/getTextElementPositions.js index fd13846d4bc..250dfe24a08 100644 --- a/packages/yoastseo/src/parse/build/private/getTextElementPositions.js +++ b/packages/yoastseo/src/parse/build/private/getTextElementPositions.js @@ -26,7 +26,18 @@ function getDescendantPositions( descendantNodes ) { descendantTagPositions.push( node.sourceCodeLocation ); } else { if ( node.sourceCodeLocation.startTag ) { - descendantTagPositions.push( node.sourceCodeLocation.startTag ); + const startRange = { + startOffset: node.sourceCodeLocation.startTag.startOffset, + endOffset: node.sourceCodeLocation.startTag.endOffset, + }; + /* + * Here, we need to account for the fact that earlier (in innerText.js), we treated a
as a newline character. + * Therefore, we need to subtract 1 from the endOffset to not count it twice. + */ + if ( node.name === "br" ) { + startRange.endOffset = startRange.endOffset - 1; + } + descendantTagPositions.push( startRange ); } /* * Check whether node has an end tag before adding it to the array. diff --git a/packages/yoastseo/src/parse/traverse/innerText.js b/packages/yoastseo/src/parse/traverse/innerText.js index ab454196eef..71df6b6dfce 100644 --- a/packages/yoastseo/src/parse/traverse/innerText.js +++ b/packages/yoastseo/src/parse/traverse/innerText.js @@ -14,6 +14,8 @@ export default function innerText( node ) { node.childNodes.forEach( child => { if ( child.name === "#text" ) { text += child.value; + } else if ( child.name === "br" ) { + text += "\n"; } else { text += innerText( child ); } From 9fff2c7cc3fa3f349dcccbc78492700c3698bfe6 Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 9 Oct 2023 14:23:24 +0200 Subject: [PATCH 08/55] add unit test --- .../exclude-gutenberg-taxonomy-test.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php diff --git a/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php b/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php new file mode 100644 index 00000000000..bbc72e21b82 --- /dev/null +++ b/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php @@ -0,0 +1,47 @@ +instance = new Exclude_Gutenberg_Taxonomy(); + } + + /** + * Tests that the correct taxonomies are excluded. + * + * @covers ::exclude_taxonomies + */ + public function test_exclude_gutenberg_taxonomy() { + $excluded_taxonomies = []; + + $expected = [ 'wp_pattern_category' ]; + $actual = $this->instance->exclude_taxonomies( $excluded_taxonomies ); + + self::assertEquals( $expected, $actual ); + } +} \ No newline at end of file From 6a02e4aa98b20bfb62c6679c77d16dc480ad51fe Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 9 Oct 2023 14:27:18 +0200 Subject: [PATCH 09/55] php fix cs --- .../third-party/exclude-gutenberg-taxonomy-test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php b/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php index bbc72e21b82..79ad5883743 100644 --- a/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php +++ b/tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php @@ -44,4 +44,4 @@ public function test_exclude_gutenberg_taxonomy() { self::assertEquals( $expected, $actual ); } -} \ No newline at end of file +} From ac16686f2bf08e1ae9c7434c524e9e13efe57cd1 Mon Sep 17 00:00:00 2001 From: Paolo Luigi Scala Date: Mon, 9 Oct 2023 15:35:00 +0200 Subject: [PATCH 10/55] Use wp_add_script_data when available --- src/integrations/third-party/wordproof.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/integrations/third-party/wordproof.php b/src/integrations/third-party/wordproof.php index 5d7357ff138..c4c47f2cac6 100644 --- a/src/integrations/third-party/wordproof.php +++ b/src/integrations/third-party/wordproof.php @@ -89,12 +89,17 @@ public function register_hooks() { */ \add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 10, 0 ); - /** - * Add async to the wordproof scripts. - */ - if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ) ) { - wp_script_add_data( WPSEO_Admin_Asset_Manager::PREFIX . 'wordproof-uikit', 'strategy', 'async' ); - } else { + if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '<' ) ) { + \add_action( + 'wp_enqueue_scripts', + function() { + \wp_script_add_data( WPSEO_Admin_Asset_Manager::PREFIX . 'wordproof-uikit', 'strategy', 'async' ); + }, + 11, + 0 + ); + } + else { \add_filter( 'script_loader_tag', [ $this, 'add_async_to_script' ], 10, 3 ); } From 97aaf3bbe9ba3b99f11eb3ee9d3021fa5b272953 Mon Sep 17 00:00:00 2001 From: Paolo Luigi Scala Date: Mon, 9 Oct 2023 15:40:28 +0200 Subject: [PATCH 11/55] Revert change made for testing. --- src/integrations/third-party/wordproof.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrations/third-party/wordproof.php b/src/integrations/third-party/wordproof.php index c4c47f2cac6..2928bd1bdca 100644 --- a/src/integrations/third-party/wordproof.php +++ b/src/integrations/third-party/wordproof.php @@ -89,7 +89,7 @@ public function register_hooks() { */ \add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 10, 0 ); - if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '<' ) ) { + if ( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '6.3', '>=' ) ) { \add_action( 'wp_enqueue_scripts', function() { From 49f81cf4c428934d7f8210083f016b7ba5ce7b0a Mon Sep 17 00:00:00 2001 From: Martijn van der Klis Date: Tue, 10 Oct 2023 09:08:06 +0200 Subject: [PATCH 12/55] Extracts duplicate code into a method --- .../helpers/positionBasedAnnotationHelper.js | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js index b8dc54bd1f7..f886675f5af 100644 --- a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js +++ b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js @@ -4,8 +4,8 @@ import { helpers } from "yoastseo"; /** * Regex to detect HTML tags. * Please note that this regex will also detect non-HTML tags that are also wrapped in `<>`. - * For example, in the following sentence, cats rabbit , - * we will match , and . This is an edge case though. + * For example, in the following sentence, `cats rabbit `, + * we will match ``, `` and ``. This is an edge case though. * @type {RegExp} */ const htmlTagsRegex = /(<([a-z]|\/)[^<>]+>)/ig; @@ -53,6 +53,21 @@ const adjustFirstSectionOffsets = ( blockStartOffset, blockEndOffset, blockName return { blockStartOffset, blockEndOffset }; }; +/** + * Retrieves the length for an HTML tag, adjusts the length for `
` tags. + * @param {[string]} htmlTag The HTML tag. + * @returns {number} The length of the given HTML tag. + */ +const getTagLength = ( htmlTag ) => { + const [ tag ] = htmlTag; + let tagLength = tag.length; + // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. + if ( /^<\/?br/.test( tag ) ) { + tagLength -= 1; + } + return tagLength; +}; + /** * Adjusts the block start and end offsets of a given Mark when the block HTML contains HTML tags. * @@ -82,24 +97,12 @@ const adjustOffsetsForHtmlTags = ( slicedBlockHtmlToStartOffset, slicedBlockHtml */ let foundHtmlTags = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ]; forEachRight( foundHtmlTags, ( foundHtmlTag ) => { - const [ tag ] = foundHtmlTag; - let tagLength = tag.length; - // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. - if ( /<\/?br/.test( tag ) ) { - tagLength -= 1; - } - blockStartOffset -= tagLength; + blockStartOffset -= getTagLength( foundHtmlTag ); } ); foundHtmlTags = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ]; forEachRight( foundHtmlTags, ( foundHtmlTag ) => { - const [ tag ] = foundHtmlTag; - let tagLength = tag.length; - // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. - if ( /<\/?br/.test( tag ) ) { - tagLength -= 1; - } - blockEndOffset -= tagLength; + blockEndOffset -= getTagLength( foundHtmlTag ); } ); return { blockStartOffset, blockEndOffset }; From 44b038e1dbb8c42f32c48f6c127c1062d0a59206 Mon Sep 17 00:00:00 2001 From: Martijn van der Klis Date: Tue, 10 Oct 2023 09:18:16 +0200 Subject: [PATCH 13/55] Reverts the changes to interpolation strings --- .../researches/keywordCountSpec.js | 104 +++++++++--------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js index ae90d4f79fd..305f7b6b935 100644 --- a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js +++ b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js @@ -299,7 +299,7 @@ const testCases = [ attributeId: "", clientId: "", isFirstSection: false } } ), - ], + ], skip: false, }, { @@ -991,9 +991,9 @@ const testCasesWithSpecialCharacters = [ { description: "can match paragraph gutenberg block", paper: new Paper( - "" + - "

a string of text with the keyword in it

" + - "", + ` +

a string of text with the keyword in it

+ `, { keyword: "keyword", wpBlocks: [ @@ -1015,8 +1015,8 @@ const testCasesWithSpecialCharacters = [ marked: "a string of text with the keyword in it", original: "a string of text with the keyword in it", position: { - startOffset: 50, - endOffset: 57, + startOffset: 55, + endOffset: 62, startOffsetBlock: 26, endOffsetBlock: 33, attributeId: "", @@ -1030,16 +1030,16 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - "" + - "

Over the years, we’ve written quite a few articles about " + - 'branding. ' + - "Branding is about getting people to relate to your company and products. " + - "It’s also about trying to make your brand synonymous with a certain product or service. " + - "This can be a lengthy and hard project. It can potentially cost you all of your revenue. " + - "It’s no wonder that branding is often associated with investing lots of money in marketing and promotion. " + - "However, for a lot of small business owners, the investment in branding will have " + - "to be made with a relatively small budget. 

" + - "", + ` +

Over the years, we’ve written quite a few articles about  + branding. + Branding is about getting people to relate to your company and + products. It’s also about trying to make your brand synonymous with a certain product or service. + This can be a lengthy and hard project. It can potentially cost you all of your revenue. + It’s no wonder that branding is often associated with investing lots of money in marketing and promotion. + However, for a lot of small business owners, the investment in branding will have + to be made with a relatively small budget. 

+ `, { keyword: "keyword", wpBlocks: [ @@ -1064,10 +1064,10 @@ const testCasesWithSpecialCharacters = [ marked: " It's also about trying to make your brand synonymous with a certain product or service.", original: " It’s also about trying to make your brand synonymous with a certain product or service.", position: { - startOffset: 277, - endOffset: 287, - startOffsetBlock: 253, - endOffsetBlock: 263, + startOffset: 295, + endOffset: 305, + startOffsetBlock: 266, + endOffsetBlock: 276, attributeId: "", clientId: "", isFirstSection: false, @@ -1079,13 +1079,13 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - "" + - "

You might be a local bakery with 10 employees, or a local industrial company employing up to 500 people. " + - "These all can be qualified as ‘small business’. " + - "All have the same main goal when they start: the need to establish a name in their field of expertise. " + - "There are multiple ways to do this, without a huge budget. " + - "In this post, I’ll share my thoughts on how to go about your own low-budget branding.

" + - "", + ` +

You might be a local bakery with 10 employees, or a local industrial company employing up to 500 people. + These all can be qualified as + ‘small business’. All have the same main goal when they start: the need to establish a name in their field of expertise. There + are multiple ways to do this, without a huge budget. + In this post, I’ll share my thoughts on how to go about your own low-budget branding.

+ `, { keyword: "expertise", wpBlocks: [ @@ -1110,10 +1110,10 @@ const testCasesWithSpecialCharacters = [ marked: " All have the same main goal when they start: the need to establish a name in their field of expertise.", original: " All have the same main goal when they start: the need to establish a name in their field of expertise.", position: { - startOffset: 269, - endOffset: 278, - startOffsetBlock: 245, - endOffsetBlock: 254, + startOffset: 282, + endOffset: 291, + startOffsetBlock: 253, + endOffsetBlock: 262, attributeId: "", clientId: "", isFirstSection: false, @@ -1125,9 +1125,9 @@ const testCasesWithSpecialCharacters = [ { description: "can match heading gutenberg block", paper: new Paper( - "" + - '

Define and communicate brand values

' + - "", + ` +

Define and communicate brand values

+ `, { keyword: "communicate", wpBlocks: [ @@ -1151,8 +1151,8 @@ const testCasesWithSpecialCharacters = [ marked: "Define and communicate brand values", original: "Define and communicate brand values", position: { - startOffset: 102, - endOffset: 113, + startOffset: 107, + endOffset: 118, startOffsetBlock: 11, endOffsetBlock: 22, attributeId: "", @@ -1166,19 +1166,19 @@ const testCasesWithSpecialCharacters = [ { description: "can match complex paragraph gutenberg block", paper: new Paper( - "" + - '
' + - '
' + - "

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

" + - "
" + - "" + + ` +
+
+

Lorem Ipsum is simply dummy text of the printing and typesetting industry.

+
+ - "" + - '
' + - "

There are many variations of passages of Lorem Ipsum available

" + - "
" + - "
" + - "", + +
+

There are many variations of passages of Lorem Ipsum available

+
+
+ `, { keyword: "Ipsum", wpBlocks: [ @@ -1230,8 +1230,8 @@ const testCasesWithSpecialCharacters = [ marked: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", original: "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", position: { - startOffset: 134, - endOffset: 139, + startOffset: 149, + endOffset: 154, startOffsetBlock: 14, endOffsetBlock: 19, attributeId: "", @@ -1243,8 +1243,8 @@ const testCasesWithSpecialCharacters = [ marked: "There are many variations of passages of Lorem Ipsum available", original: "There are many variations of passages of Lorem Ipsum available", position: { - startOffset: 385, - endOffset: 390, + startOffset: 426, + endOffset: 431, startOffsetBlock: 47, endOffsetBlock: 52, attributeId: "", From 85498be5d197a4ae2d5b33257b8a72f08b9ceb8a Mon Sep 17 00:00:00 2001 From: Vraja Das <65466507+vraja-pro@users.noreply.github.com> Date: Tue, 10 Oct 2023 10:01:26 +0200 Subject: [PATCH 14/55] Revert "add filter to allow building hierarchy" --- src/builders/indexable-hierarchy-builder.php | 15 --- .../watchers/indexable-ancestor-watcher.php | 26 ----- .../indexable-ancestor-watcher-test.php | 103 +----------------- 3 files changed, 1 insertion(+), 143 deletions(-) diff --git a/src/builders/indexable-hierarchy-builder.php b/src/builders/indexable-hierarchy-builder.php index cdc663bd75c..e2e0f7392d8 100644 --- a/src/builders/indexable-hierarchy-builder.php +++ b/src/builders/indexable-hierarchy-builder.php @@ -132,21 +132,6 @@ public function build( Indexable $indexable ) { * @return bool True when indexable has a built hierarchy. */ protected function hierarchy_is_built( Indexable $indexable ) { - /** - * Filters ignoring checking if the hierarchy is already built. - * - * Used when adding term with `wp_set_object_terms` together with `wp_insert_post`. - * - * @since 21.2 - * - * @param bool $ignore_already_saved If the hierarchy already saved check should be ignored. - * @return bool The filtered value of the `$ignore_already_saved` parameter. - */ - $ignore_already_saved = apply_filters( 'wpseo_hierarchy_ignore_already_saved', false ); - if ( $ignore_already_saved ) { - return false; - } - if ( \in_array( $indexable->id, $this->saved_ancestors, true ) ) { return true; } diff --git a/src/integrations/watchers/indexable-ancestor-watcher.php b/src/integrations/watchers/indexable-ancestor-watcher.php index 9c76a4529b1..fddf0c92d18 100644 --- a/src/integrations/watchers/indexable-ancestor-watcher.php +++ b/src/integrations/watchers/indexable-ancestor-watcher.php @@ -92,9 +92,6 @@ public function __construct( */ public function register_hooks() { \add_action( 'wpseo_save_indexable', [ $this, 'reset_children' ], \PHP_INT_MAX, 2 ); - if ( ! \check_ajax_referer( 'inlineeditnonce', '_inline_edit', false ) ) { - \add_action( 'set_object_terms', [ $this, 'build_post_hierarchy' ], 10, 6 ); - } } /** @@ -106,29 +103,6 @@ public static function get_conditionals() { return [ Migrations_Conditional::class ]; } - /** - * Validates if the current primary category is still present. If not just remove the post meta for it. - * - * @param int $object_id Object ID. - * @param array $terms Unused. An array of object terms. - * @param array $tt_ids An array of term taxonomy IDs. - * @param string $taxonomy Taxonomy slug. - * @param bool $append Whether to append new terms to the old terms. - * @param array $old_tt_ids Old array of term taxonomy IDs. - */ - public function build_post_hierarchy( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { - $post = \get_post( $object_id ); - if ( $this->post_type_helper->is_excluded( $post->post_type ) ) { - return; - } - - $indexable = $this->indexable_repository->find_by_id_and_type( $post->ID, $post->post_type ); - - if ( $indexable instanceof Indexable ) { - $this->indexable_hierarchy_builder->build( $indexable ); - } - } - /** * If an indexable's permalink has changed, updates its children in the hierarchy table and resets the children's permalink. * diff --git a/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php b/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php index 1b55929e38d..04e2eed2e54 100644 --- a/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php +++ b/tests/unit/integrations/watchers/indexable-ancestor-watcher-test.php @@ -168,116 +168,15 @@ public function test_get_conditionals() { ); } - /** - * Data provider for the register_hooks test. - * - * @return array The data. - */ - public function data_provider_register_hooks() { - return [ - 'When ancestor is changes inline edit' => [ - 'is_inline_edit' => true, - 'set_object_terms_action' => false, - ], - 'When ancestor is changes not inline edit' => [ - 'is_inline_edit' => false, - 'set_object_terms_action' => 10, - ], - ]; - } - /** * Tests if the expected hooks are registered. * * @covers ::register_hooks - * - * @dataProvider data_provider_register_hooks - * - * @param bool $is_inline_edit Whether or not the request is an inline edit. - * @param bool|int $set_object_terms_action The set_object_terms action return value. */ - public function test_register_hooks( $is_inline_edit, $set_object_terms_action ) { - - Functions\expect( 'check_ajax_referer' ) - ->once() - ->with( 'inlineeditnonce', '_inline_edit', false ) - ->andReturn( $is_inline_edit ); - + public function test_register_hooks() { $this->instance->register_hooks(); self::assertNotFalse( \has_action( 'wpseo_save_indexable', [ $this->instance, 'reset_children' ] ) ); - self::assertSame( $set_object_terms_action, \has_action( 'set_object_terms', [ $this->instance, 'build_post_hierarchy' ] ) ); - } - - /** - * Data provider for the build_post_hierarchy test. - * - * @return array The data. - */ - public static function data_provider_build_post_hierarchy() { - $indexable = Mockery::mock( Indexable_Mock::class ); - return [ - 'Building hierarchy' => [ - 'is_excluded' => false, - 'find_by_id_and_type_times' => 1, - 'indexable' => $indexable, - 'build_times' => 1, - ], - 'Not building hierarchy because no indexable' => [ - 'is_excluded' => false, - 'find_by_id_and_type_times' => 1, - 'indexable' => (object) [ 'ID' => 5 ], - 'build_times' => 0, - ], - 'Not building because excluded post type' => [ - 'is_excluded' => true, - 'find_by_id_and_type_times' => 0, - 'indexable' => (object) [ 'ID' => 5 ], - 'build_times' => 0, - ], - ]; - } - - /** - * Tests the build_post_hierarchy. - * - * @covers ::build_post_hierarchy - * - * @dataProvider data_provider_build_post_hierarchy - * - * @param bool $is_excluded Whether or not the post type is excluded. - * @param int $find_by_id_and_type_times The number of times the find_by_id_and_type method is called. - * @param Indexable_Mock $indexable The indexable. - * @param int $build_times The number of times the build method is called. - */ - public function test_build_post_hierarchy( $is_excluded, $find_by_id_and_type_times, $indexable, $build_times ) { - $post = Mockery::mock( \WP_Post::class ); - $post->post_type = 'post'; - $post->ID = 5; - - Functions\expect( 'get_post' ) - ->once() - ->with( 5 ) - ->andReturn( $post ); - - $this->post_type_helper - ->expects( 'is_excluded' ) - ->once() - ->with( $post->post_type ) - ->andReturn( $is_excluded ); - - $this->indexable_repository - ->expects( 'find_by_id_and_type' ) - ->times( $find_by_id_and_type_times ) - ->with( $post->ID, $post->post_type ) - ->andReturn( $indexable ); - - $this->indexable_hierarchy_builder - ->expects( 'build' ) - ->times( $build_times ) - ->with( $indexable ); - - $this->instance->build_post_hierarchy( 5, [ 'test_term' ], [ 7 ], 'category', false, [] ); } /** From d65a8c3d05bb64b475d09e2b4f70cecb9c76cf40 Mon Sep 17 00:00:00 2001 From: YoastBot Date: Tue, 10 Oct 2023 08:38:01 +0000 Subject: [PATCH 15/55] Bump version to 21.4-RC5 --- package.json | 2 +- wp-seo-main.php | 2 +- wp-seo.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index baa13713706..92dce53f90f 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "typescript": "^4.2.4" }, "yoast": { - "pluginVersion": "21.4-RC4" + "pluginVersion": "21.4-RC5" }, "version": "0.0.0" } diff --git a/wp-seo-main.php b/wp-seo-main.php index 1f06571a89a..c6c45e7ed72 100644 --- a/wp-seo-main.php +++ b/wp-seo-main.php @@ -15,7 +15,7 @@ * {@internal Nobody should be able to overrule the real version number as this can cause * serious issues with the options, so no if ( ! defined() ).}} */ -define( 'WPSEO_VERSION', '21.4-RC4' ); +define( 'WPSEO_VERSION', '21.4-RC5' ); if ( ! defined( 'WPSEO_PATH' ) ) { diff --git a/wp-seo.php b/wp-seo.php index def76d6adf5..eea5e908bcb 100644 --- a/wp-seo.php +++ b/wp-seo.php @@ -8,7 +8,7 @@ * * @wordpress-plugin * Plugin Name: Yoast SEO - * Version: 21.4-RC4 + * Version: 21.4-RC5 * Plugin URI: https://yoa.st/1uj * Description: The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more. * Author: Team Yoast From 3491029fdd5f88c79eb999d276cbb3e197fd8b8d Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Tue, 10 Oct 2023 11:15:50 +0200 Subject: [PATCH 16/55] Revert abstract exclude taxonomy --- .../abstract-exclude-taxonomy.php | 44 ----------------- .../exclude-gutenberg-taxonomy.php | 23 --------- .../exclude-gutenberg-taxonomy-test.php | 47 ------------------- 3 files changed, 114 deletions(-) delete mode 100644 src/integrations/abstract-exclude-taxonomy.php delete mode 100644 src/integrations/third-party/exclude-gutenberg-taxonomy.php delete mode 100644 tests/unit/integrations/third-party/exclude-gutenberg-taxonomy-test.php diff --git a/src/integrations/abstract-exclude-taxonomy.php b/src/integrations/abstract-exclude-taxonomy.php deleted file mode 100644 index c6f9a7704b1..00000000000 --- a/src/integrations/abstract-exclude-taxonomy.php +++ /dev/null @@ -1,44 +0,0 @@ -get_taxonomy() ); - } - - /** - * This integration is only active when the child class's conditionals are met. - * - * @return array|string[] The conditionals. - */ - public static function get_conditionals() { - return []; - } - - /** - * Returns the names of the taxonomies to be excluded. - * To be used in the wpseo_indexable_excluded_taxonomies filter. - * - * @return array The names of the taxonomies. - */ - abstract public function get_taxonomy(); -} diff --git a/src/integrations/third-party/exclude-gutenberg-taxonomy.php b/src/integrations/third-party/exclude-gutenberg-taxonomy.php deleted file mode 100644 index 4b6a566421a..00000000000 --- a/src/integrations/third-party/exclude-gutenberg-taxonomy.php +++ /dev/null @@ -1,23 +0,0 @@ -instance = new Exclude_Gutenberg_Taxonomy(); - } - - /** - * Tests that the correct taxonomies are excluded. - * - * @covers ::exclude_taxonomies - */ - public function test_exclude_gutenberg_taxonomy() { - $excluded_taxonomies = []; - - $expected = [ 'wp_pattern_category' ]; - $actual = $this->instance->exclude_taxonomies( $excluded_taxonomies ); - - self::assertEquals( $expected, $actual ); - } -} From e5c4e5064e89b8f342402fd82d4d3e63a40132d1 Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Tue, 10 Oct 2023 11:21:35 +0200 Subject: [PATCH 17/55] add default taxonomies to exclude --- .../admin/indexables-exclude-taxonomy-integration.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/integrations/admin/indexables-exclude-taxonomy-integration.php b/src/integrations/admin/indexables-exclude-taxonomy-integration.php index 09e76662e87..4d1592b8d8f 100644 --- a/src/integrations/admin/indexables-exclude-taxonomy-integration.php +++ b/src/integrations/admin/indexables-exclude-taxonomy-integration.php @@ -41,13 +41,15 @@ public function register_hooks() { * * @param array $excluded_taxonomies The excluded taxonomies. * - * @return array The excluded post types, including the specific post type. + * @return array The excluded taxonomies, including specific taxonomies. */ public function exclude_taxonomies_for_indexation( $excluded_taxonomies ) { + $taxonomies_to_exclude = \array_merge( $excluded_taxonomies, [ 'wp_pattern_category' ] ); + if ( $this->options_helper->get( 'disable-post_format', false ) ) { - return \array_merge( $excluded_taxonomies, [ 'post_format' ] ); + return \array_merge( $taxonomies_to_exclude, [ 'post_format' ] ); } - return $excluded_taxonomies; + return $taxonomies_to_exclude; } } From 9f3913e2e6da0d5eaf9415932e54deaffea10313 Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 2 Oct 2023 14:32:26 +0200 Subject: [PATCH 18/55] Revert "Add prop type element to selectField component" This reverts commit 96767856ddcbac3fe4a2f5363a213954f775eaf4. --- packages/ui-library/src/components/select-field/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/ui-library/src/components/select-field/index.js b/packages/ui-library/src/components/select-field/index.js index 89dae6f7b89..1abfc2adda1 100644 --- a/packages/ui-library/src/components/select-field/index.js +++ b/packages/ui-library/src/components/select-field/index.js @@ -9,7 +9,7 @@ import { forwardRef } from "@wordpress/element"; * @param {JSX.Element} error Error node. * @param {string} [className] Optional CSS class. * @param {boolean} [disabled] Disabled state. - * @param {string | JSX.Element} label Label. + * @param {string} label Label. * @param {JSX.node} [description] Optional description. * @param {Object} [props] Any extra props. * @returns {JSX.Element} SelectField component. @@ -54,10 +54,7 @@ const SelectField = forwardRef( ( { const propTypes = { id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, - label: PropTypes.oneOfType( [ - PropTypes.string, - PropTypes.element, - ] ).isRequired, + label: PropTypes.string.isRequired, description: PropTypes.node, disabled: PropTypes.bool, validation: PropTypes.shape( { From 817508ad4fdcf08151b6a8d622d001035daf6fdb Mon Sep 17 00:00:00 2001 From: Vraja Das Date: Mon, 2 Oct 2023 14:31:52 +0200 Subject: [PATCH 19/55] Revert "add prop type to label in select component" This reverts commit 592437a3bf30d68da85cb52e01a86dffe57f1725. --- packages/ui-library/src/elements/select/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/ui-library/src/elements/select/index.js b/packages/ui-library/src/elements/select/index.js index e65aa671d16..433a4cbadd4 100644 --- a/packages/ui-library/src/elements/select/index.js +++ b/packages/ui-library/src/elements/select/index.js @@ -14,7 +14,7 @@ const optionPropType = { /** * @param {string|number|boolean} value Value. - * @param {string | JSX.Element} label Label. + * @param {string} label Label. * @returns {JSX.Element} The option. */ const Option = ( { value, label } ) => { @@ -133,10 +133,7 @@ const propTypes = { options: PropTypes.arrayOf( PropTypes.shape( optionPropType ) ), children: PropTypes.node, selectedLabel: PropTypes.string, - label: PropTypes.oneOfType( [ - PropTypes.string, - PropTypes.element, - ] ).isRequired, + label: PropTypes.string, labelProps: PropTypes.object, labelSuffix: PropTypes.node, onChange: PropTypes.func.isRequired, From 428574da74f2b7b5e2c42fc0a671a83cc731d7c1 Mon Sep 17 00:00:00 2001 From: vraja-pro Date: Tue, 10 Oct 2023 11:54:27 +0200 Subject: [PATCH 20/55] refactor to use labelSuffix prop --- packages/js/src/settings/routes/breadcrumbs.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/js/src/settings/routes/breadcrumbs.js b/packages/js/src/settings/routes/breadcrumbs.js index 3b36fa9c998..6aec54c0fe1 100644 --- a/packages/js/src/settings/routes/breadcrumbs.js +++ b/packages/js/src/settings/routes/breadcrumbs.js @@ -111,10 +111,8 @@ const Breadcrumbs = () => { as={ SelectField } name={ `wpseo_titles.post_types-${ postTypeName }-maintax` } id={ `input-wpseo_titles-post_types-${ postTypeName }-maintax` } - label={ <> - { postType.label } - { postTypeName } - } + label={ postType.label } + labelSuffix={ { postTypeName } } options={ postType.options } className="yst-max-w-sm" /> ) } @@ -131,12 +129,10 @@ const Breadcrumbs = () => { as={ SelectField } name={ `wpseo_titles.taxonomy-${ taxonomy.name }-ptparent` } id={ `input-wpseo_titles-taxonomy-${ taxonomy.name }-ptparent` } - label={ <> - { taxonomy.label } - { taxonomy.name } - } + label={ taxonomy.label } options={ taxonomy.options } className="yst-max-w-sm" + labelSuffix={ { taxonomy.name } } /> ) ) } From f316fe0edf01b0ff13ae48a5abc7bd55a86b5668 Mon Sep 17 00:00:00 2001 From: vraja-pro Date: Tue, 10 Oct 2023 13:01:46 +0200 Subject: [PATCH 21/55] add exclude taxonomy from sitemap --- inc/sitemaps/class-taxonomy-sitemap-provider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/sitemaps/class-taxonomy-sitemap-provider.php b/inc/sitemaps/class-taxonomy-sitemap-provider.php index c8980015e2f..02ebbb5ff9d 100644 --- a/inc/sitemaps/class-taxonomy-sitemap-provider.php +++ b/inc/sitemaps/class-taxonomy-sitemap-provider.php @@ -287,7 +287,7 @@ public function is_valid_taxonomy( $taxonomy_name ) { return false; } - if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu' ], true ) ) { + if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu', 'wp_pattern_category' ], true ) ) { return false; } From e1becd28d36abb5d3945c52de5f3ac2b94a52d81 Mon Sep 17 00:00:00 2001 From: Mykola Shlyakhtun Date: Tue, 10 Oct 2023 14:49:43 +0300 Subject: [PATCH 22/55] Keyphrase density doesn't correctly highlight keyphrases in Yoast Blocks when there is a
tag remove more duplicates. --- .../helpers/positionBasedAnnotationHelper.js | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js index f886675f5af..ac2376338a3 100644 --- a/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js +++ b/packages/js/src/decorator/helpers/positionBasedAnnotationHelper.js @@ -54,18 +54,24 @@ const adjustFirstSectionOffsets = ( blockStartOffset, blockEndOffset, blockName }; /** - * Retrieves the length for an HTML tag, adjusts the length for `
` tags. - * @param {[string]} htmlTag The HTML tag. - * @returns {number} The length of the given HTML tag. + * Retrieves the length for HTML tags, adjusts the length for `
` tags. + * @param {[Object]} htmlTags Array of HTML tags. + * @returns {number} The length of the given HTML tags. */ -const getTagLength = ( htmlTag ) => { - const [ tag ] = htmlTag; - let tagLength = tag.length; - // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. - if ( /^<\/?br/.test( tag ) ) { - tagLength -= 1; - } - return tagLength; +const getTagsLength = ( htmlTags ) => { + let tagsLength = 0; + forEachRight( htmlTags, ( htmlTag ) => { + const [ tag ] = htmlTag; + let tagLength = tag.length; + // Here, we need to account for treating
tags as sentence delimiters, and subtract 1 from the tagLength. + if ( /^<\/?br/.test( tag ) ) { + tagLength -= 1; + } + + tagsLength += tagLength; + } ); + + return tagsLength; }; /** @@ -95,15 +101,11 @@ const adjustOffsetsForHtmlTags = ( slicedBlockHtmlToStartOffset, slicedBlockHtml * - Text: This is a giant panda. * - Range of "panda": 16 -21 */ - let foundHtmlTags = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ]; - forEachRight( foundHtmlTags, ( foundHtmlTag ) => { - blockStartOffset -= getTagLength( foundHtmlTag ); - } ); + const foundHtmlTagsToStartOffset = [ ...slicedBlockHtmlToStartOffset.matchAll( htmlTagsRegex ) ]; + blockStartOffset -= getTagsLength( foundHtmlTagsToStartOffset ); - foundHtmlTags = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ]; - forEachRight( foundHtmlTags, ( foundHtmlTag ) => { - blockEndOffset -= getTagLength( foundHtmlTag ); - } ); + const foundHtmlTagsToEndOffset = [ ...slicedBlockHtmlToEndOffset.matchAll( htmlTagsRegex ) ]; + blockEndOffset -= getTagsLength( foundHtmlTagsToEndOffset ); return { blockStartOffset, blockEndOffset }; }; From 74518e4c72940054d24fc1c0d50c66072837cb25 Mon Sep 17 00:00:00 2001 From: vraja-pro Date: Tue, 10 Oct 2023 14:47:26 +0200 Subject: [PATCH 23/55] add integration tests --- ...-class-wpseo-taxonomy-sitemap-provider.php | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php b/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php index 0f171869e82..8731d7ef87e 100644 --- a/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php +++ b/tests/integration/sitemaps/test-class-wpseo-taxonomy-sitemap-provider.php @@ -9,6 +9,7 @@ * Class WPSEO_Taxonomy_Sitemap_Provider_Test. * * @group sitemaps + * @coversDefaultClass WPSEO_Taxonomy_Sitemap_Provider */ class WPSEO_Taxonomy_Sitemap_Provider_Test extends WPSEO_UnitTestCase { @@ -31,7 +32,7 @@ public function set_up() { /** * Tests the retrieval of the index links. * - * @covers WPSEO_Taxonomy_Sitemap_Provider::get_index_links + * @covers ::get_index_links */ public function test_get_index_links() { @@ -57,7 +58,7 @@ public function test_get_index_links() { /** * Tests retrieval of the sitemap links. * - * @covers WPSEO_Taxonomy_Sitemap_Provider::get_sitemap_links + * @covers ::get_sitemap_links */ public function test_get_sitemap_links() { @@ -71,7 +72,7 @@ public function test_get_sitemap_links() { /** * Makes sure invalid sitemap pages return no contents (404). * - * @covers WPSEO_Taxonomy_Sitemap_Provider::get_index_links + * @covers ::get_index_links */ public function test_get_index_links_empty_sitemap() { // Fetch the global sitemap. @@ -87,4 +88,44 @@ public function test_get_index_links_empty_sitemap() { // Expect an empty page (404) to be returned. $this->expectOutputString( '' ); } + + /** + * Data provider for is_valid_taxonomy test. + * + * @return array + */ + public function data_provider_is_valis_taxonomy() { + return [ + 'Pattern Categories' => [ + 'taxonomy' => 'wp_pattern_category', + 'expected' => false, + ], + 'nav_menu' => [ + 'taxonomy' => 'nav_menu', + 'expected' => false, + ], + 'link_category' => [ + 'taxonomy' => 'link_category', + 'expected' => false, + ], + 'post_format' => [ + 'taxonomy' => 'post_format', + 'expected' => false, + ], + ]; + } + + /** + * Tetst of is_valid_taxonomy. + * + * @covers ::is_valid_taxonomy + * + * @dataProvider data_provider_is_valis_taxonomy + * + * @param string $taxonomy Taxonomy name. + * @param bool $expected Expected result. + */ + public function test_is_valid_taxonomy( $taxonomy, $expected ) { + $this->assertSame( $expected, self::$class_instance->is_valid_taxonomy( $taxonomy ) ); + } } From a8fd86a9f66dbcd19d51b3eb2249090c1d3f04cd Mon Sep 17 00:00:00 2001 From: Colin Stewart <79332690+costdev@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:35:23 +0100 Subject: [PATCH 24/55] Docs: Correct some typos in `class-wpseo-option.php`. --- inc/options/class-wpseo-option.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/inc/options/class-wpseo-option.php b/inc/options/class-wpseo-option.php index d0fe27e870e..98d2ee8b356 100644 --- a/inc/options/class-wpseo-option.php +++ b/inc/options/class-wpseo-option.php @@ -6,7 +6,7 @@ */ /** - * This abstract class and it's concrete classes implement defaults and value validation for + * This abstract class and its concrete classes implement defaults and value validation for * all WPSEO options and subkeys within options. * * Some guidelines: @@ -22,9 +22,9 @@ * * [Updating/Adding options] * - For multisite site_options, please use the WPSEO_Options::update_site_option() method. - * - For normal options, use the normal add/update_option() functions. As long a the classes here + * - For normal options, use the normal add/update_option() functions. As long as the classes here * are instantiated, validation for all options and their subkeys will be automatic. - * - On (succesfull) update of a couple of options, certain related actions will be run automatically. + * - On (successful) update of a couple of options, certain related actions will be run automatically. * Some examples: * - on change of wpseo[yoast_tracking], the cron schedule will be adjusted accordingly * - on change of wpseo and wpseo_title, some caches will be cleared @@ -41,7 +41,7 @@ * translate_defaults() method. * - When you remove an array key from an option: if it's important that the option is really removed, * add the WPSEO_Option::clean_up( $option_name ) method to the upgrade run. - * This will re-save the option and automatically remove the array key no longer in existance. + * This will re-save the option and automatically remove the array key no longer in existence. * - When you rename a sub-option: add it to the clean_option() routine and run that in the upgrade run. * - When you change the default for an option sub-key, make sure you verify that the validation routine will * still work the way it should. @@ -74,8 +74,8 @@ abstract class WPSEO_Option { * Option group name for use in settings forms. * * Will be set automagically if not set in concrete class (i.e. - * if it confirm to the normal pattern 'yoast' . $option_name . 'options', - * only set in conrete class if it doesn't). + * if it conforms to the normal pattern 'yoast' . $option_name . 'options', + * only set in concrete class if it doesn't). * * @var string */ @@ -107,7 +107,7 @@ abstract class WPSEO_Option { protected $defaults; /** - * Array of variable option name patterns for the option - if any -. + * Array of variable option name patterns for the option - if any. * * Set this when the option contains array keys which vary based on post_type * or taxonomy. @@ -230,7 +230,7 @@ protected function __construct() { * ``` * --------------- * - * Concrete classes *may* contain a enrich_defaults method to add additional defaults once + * Concrete classes *may* contain an enrich_defaults method to add additional defaults once * all post_types and taxonomies have been registered. * * ``` @@ -497,7 +497,7 @@ public function get_option( $options = null ) { return $filtered; } - /* *********** METHODS influencing add_uption(), update_option() and saving from admin pages. *********** */ + /* *********** METHODS influencing add_option(), update_option() and saving from admin pages. *********** */ /** * Register (whitelist) the option for the configuration pages. @@ -523,7 +523,7 @@ public function register_setting() { } /** - * Validate the option + * Validate the option. * * @param mixed $option_value The unvalidated new value for the option. * @@ -645,7 +645,7 @@ public function maybe_add_option() { * * @param mixed $value The new value for the option. * - * @return bool Whether the update was succesfull. + * @return bool Whether the update was successful. */ public function update_site_option( $value ) { if ( $this->multisite_only === true && is_multisite() ) { @@ -667,7 +667,7 @@ public function update_site_option( $value ) { * @uses WPSEO_Option::import() * * @param string|null $current_version Optional. Version from which to upgrade, if not set, - * version specific upgrades will be disregarded. + * version-specific upgrades will be disregarded. * * @return void */ @@ -692,7 +692,7 @@ public function clean( $current_version = null ) { * * @param array $option_value Option value to be imported. * @param string|null $current_version Optional. Version from which to upgrade, if not set, - * version specific upgrades will be disregarded. + * version-specific upgrades will be disregarded. * @param array|null $all_old_option_values Optional. Only used when importing old options to * have access to the real old values, in contrast to * the saved ones. From 1c7e644a73445a7889f0c7f2e1eabe85db1ad115 Mon Sep 17 00:00:00 2001 From: aidamarfuaty Date: Wed, 11 Oct 2023 11:13:09 +0200 Subject: [PATCH 25/55] Filters shortcode before getting the sentences for Japanese --- .../researches/keywordCountSpec.js | 29 +++++++++++++++++++ .../researches/keywordCount.js | 7 ++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js index c67344bc7bb..e81d66b9909 100644 --- a/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js +++ b/packages/yoastseo/spec/languageProcessing/researches/keywordCountSpec.js @@ -1856,6 +1856,35 @@ describe( "Test for counting the keyphrase in a text for Japanese", () => { original: "私の猫はかわいいです。" } ) ] ); } ); + it( "counts the keyphrase occurrence inside an image caption.", function() { + const mockPaper = new Paper( "

[caption id=\"attachment_157\" align=\"alignnone\" width=\"225\"]\"\" 一日一冊の本を読むのはできるかどうかやってみます。[/caption]

", { + locale: "ja", + keyphrase: "一冊の本を読む", + shortcodes: [ + "wp_caption", + "caption", + "gallery", + "playlist" ], + } ); + const keyphraseForms = [ + [ "一冊" ], + [ "本" ], + [ "読む", "読み", "読ま", "読め", "読も", "読ん", "読める", "読ませ", "読ませる", "読まれ", "読まれる", "読もう" ], + ]; + const researcher = buildJapaneseMockResearcher( keyphraseForms, wordsCountHelper, matchWordsHelper ); + buildTree( mockPaper, researcher ); + + + expect( getKeyphraseCount( mockPaper, researcher ).count ).toBe( 1 ); + expect( getKeyphraseCount( mockPaper, researcher ).markings ).toEqual( [ + new Mark( { + marked: "一日一冊読むのはできるかどうかやってみます。", + original: "一日一冊の本を読むのはできるかどうかやってみます。" } ) ] ); + } ); + it( "counts/marks a string of text with multiple occurrences of the same keyphrase in it.", function() { const mockPaper = new Paper( "

私の猫はかわいい猫です。

", { locale: "ja", keyphrase: "猫" } ); const researcher = buildJapaneseMockResearcher( [ [ "猫" ] ], wordsCountHelper, matchWordsHelper ); diff --git a/packages/yoastseo/src/languageProcessing/researches/keywordCount.js b/packages/yoastseo/src/languageProcessing/researches/keywordCount.js index e26b819b8ad..315f91fb7c5 100644 --- a/packages/yoastseo/src/languageProcessing/researches/keywordCount.js +++ b/packages/yoastseo/src/languageProcessing/researches/keywordCount.js @@ -6,6 +6,7 @@ import matchWordFormsWithSentence from "../helpers/match/matchWordFormsWithSente import isDoubleQuoted from "../helpers/match/isDoubleQuoted"; import { markWordsInASentence } from "../helpers/word/markWordsInSentences"; import getSentences from "../helpers/sentence/getSentences"; +import { filterShortcodesFromHTML } from "../helpers"; /** * Counts the occurrences of the keyphrase in the text and creates the Mark objects for the matches. @@ -90,8 +91,12 @@ export default function getKeyphraseCount( paper, researcher ) { const matchWordCustomHelper = researcher.getHelper( "matchWordCustomHelper" ); const customSentenceTokenizer = researcher.getHelper( "memoizedTokenizer" ); const locale = paper.getLocale(); + const text = matchWordCustomHelper + ? filterShortcodesFromHTML( paper.getText(), paper._attributes && paper._attributes.shortcodes ) + : paper.getText(); + // When the custom helper is available, we're using the sentences retrieved from the text for the analysis. - const sentences = matchWordCustomHelper ? getSentences( paper.getText(), customSentenceTokenizer ) : getSentencesFromTree( paper ); + const sentences = matchWordCustomHelper ? getSentences( text, customSentenceTokenizer ) : getSentencesFromTree( paper ); // Exact matching is requested when the keyphrase is enclosed in double quotes. const isExactMatchRequested = isDoubleQuoted( paper.getKeyword() ); From c7da10baabefb6c005372641ecc81d9e99596404 Mon Sep 17 00:00:00 2001 From: Martijn van der Klis Date: Wed, 11 Oct 2023 11:16:44 +0200 Subject: [PATCH 26/55] Fixes the locations for implicit paragraphs --- .../helpers/sentence/getSentencesFromTree.js | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js index db19d8189a8..f409563791e 100644 --- a/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js +++ b/packages/yoastseo/src/languageProcessing/helpers/sentence/getSentencesFromTree.js @@ -1,3 +1,23 @@ +/** + * Retrieves the start offset for a given node. + * @param {Node} node The current node. + * @returns {number} The start offset. + */ +function getStartOffset( node ) { + return node.sourceCodeLocation && + ( ( node.sourceCodeLocation.startTag && node.sourceCodeLocation.startTag.endOffset ) || node.sourceCodeLocation.startOffset ) || 0; +} + +/** + * Retrieves the parent node for a given node. + * @param {Paper} paper The current paper. + * @param {Node} node The current node. + * @returns {Node} The parent node. + */ +function getParentNode( paper, node ) { + return paper.getTree().findAll( treeNode => treeNode.childNodes && treeNode.childNodes.includes( node ) )[ 0 ]; +} + /** * Gets all the sentences from paragraph and heading nodes. * These two node types are the nodes that should contain sentences for the analysis. @@ -11,17 +31,23 @@ export default function( paper ) { const tree = paper.getTree().findAll( treeNode => !! treeNode.sentences ); return tree.flatMap( node => node.sentences.map( sentence => { + let parentNode = node; + + // For implicit paragraphs, base the details on the parent of this node. + if ( node.isImplicit ) { + parentNode = getParentNode( paper, node ); + } + return { ...sentence, // The parent node's start offset is the start offset of the parent node if it doesn't have a `startTag` property. - parentStartOffset: node.sourceCodeLocation && ( ( node.sourceCodeLocation.startTag && node.sourceCodeLocation.startTag.endOffset ) || - node.sourceCodeLocation.startOffset ) || 0, + parentStartOffset: getStartOffset( parentNode ), // The block client id of the parent node. - parentClientId: node.clientId || "", + parentClientId: parentNode.clientId || "", // The attribute id of the parent node, if available, otherwise an empty string. - parentAttributeId: node.attributeId || "", + parentAttributeId: parentNode.attributeId || "", // Whether the parent node is the first section of Yoast sub-blocks. - isParentFirstSectionOfBlock: node.isFirstSection || false, + isParentFirstSectionOfBlock: parentNode.isFirstSection || false, }; } ) ); } From fca39ac57b054b9671c3a31630f5708dab9b657f Mon Sep 17 00:00:00 2001 From: YoastBot Date: Wed, 11 Oct 2023 11:44:02 +0000 Subject: [PATCH 27/55] Bump version to 21.4-RC6 --- package.json | 2 +- wp-seo-main.php | 2 +- wp-seo.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 92dce53f90f..d3db5033d78 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "typescript": "^4.2.4" }, "yoast": { - "pluginVersion": "21.4-RC5" + "pluginVersion": "21.4-RC6" }, "version": "0.0.0" } diff --git a/wp-seo-main.php b/wp-seo-main.php index c6c45e7ed72..2f47796809c 100644 --- a/wp-seo-main.php +++ b/wp-seo-main.php @@ -15,7 +15,7 @@ * {@internal Nobody should be able to overrule the real version number as this can cause * serious issues with the options, so no if ( ! defined() ).}} */ -define( 'WPSEO_VERSION', '21.4-RC5' ); +define( 'WPSEO_VERSION', '21.4-RC6' ); if ( ! defined( 'WPSEO_PATH' ) ) { diff --git a/wp-seo.php b/wp-seo.php index eea5e908bcb..358a2b61c0e 100644 --- a/wp-seo.php +++ b/wp-seo.php @@ -8,7 +8,7 @@ * * @wordpress-plugin * Plugin Name: Yoast SEO - * Version: 21.4-RC5 + * Version: 21.4-RC6 * Plugin URI: https://yoa.st/1uj * Description: The first true all-in-one SEO solution for WordPress, including on-page content analysis, XML sitemaps and much more. * Author: Team Yoast @@ -20,7 +20,7 @@ * Requires PHP: 7.2.5 * * WC requires at least: 7.1 - * WC tested up to: 8.1 + * WC tested up to: 8.2 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 3dc89e19cfea11f614401fa54f698ab197c9b7ef Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Sun, 9 Apr 2023 23:23:11 +0100 Subject: [PATCH 28/55] Add updated at to wincher seo performance table --- packages/js/src/components/WincherKeyphrasesTable.js | 6 ++++++ packages/js/src/components/WincherTableRow.js | 11 +++++++++++ packages/js/tests/components/WincherTableRow.test.js | 6 ++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 89c38852488..5bbab192885 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -328,6 +328,12 @@ const WincherKeyphrasesTable = ( props ) => { > { __( "Position over time", "wordpress-seo" ) } + + { __( "Last updated", "wordpress-seo" ) } + diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index dee87fc0e69..08ccb0d5991 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -132,6 +132,15 @@ 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(); + /** * Gets the positional data based on the current UI state and returns the appropiate UI element. * @@ -155,6 +164,7 @@ export function getPositionalDataByState( props ) { ? ? + ? ); @@ -173,6 +183,7 @@ export function getPositionalDataByState( props ) { { getKeyphrasePosition( rowData ) } { } + { formatLastUpdated( rowData.updated_at ) } { diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index c3ee6dee972..906ff753688 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -51,7 +51,7 @@ describe( "WincherTableRow", () => { keyphrase="yoast seo" /> ); - expect( component.find( "td" ).length ).toEqual( 5 ); + expect( component.find( "td" ).length ).toEqual( 6 ); expect( component.find( Toggle ).length ).toEqual( 1 ); expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); @@ -61,6 +61,7 @@ describe( "WincherTableRow", () => { expect( component.find( "td" ).at( 1 ).text() ).toEqual( "yoast seo" ); expect( component.find( "td" ).at( 2 ).text() ).toEqual( "10" ); + expect( component.find( "td" ).at( 4 ).text() ).toEqual( "a few seconds ago" ); } ); it( "should not render an enabled toggle or any position and chart data when no data is available", () => { @@ -69,7 +70,7 @@ describe( "WincherTableRow", () => { keyphrase="yoast seo" /> ); - expect( component.find( "td" ).length ).toEqual( 5 ); + expect( component.find( "td" ).length ).toEqual( 6 ); expect( component.find( Toggle ).length ).toEqual( 1 ); expect( component.find( PositionOverTimeChart ).length ).toEqual( 0 ); @@ -78,5 +79,6 @@ describe( "WincherTableRow", () => { 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( "td" ).at( 4 ).text() ).toEqual( "?" ); } ); } ); From dd61eafe9aedb50377eae91942c3a26e031f9765 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Mon, 10 Apr 2023 18:16:44 +0100 Subject: [PATCH 29/55] Add position change to wincher seo performance table --- css/src/modal.css | 4 + packages/js/src/components/WincherTableRow.js | 59 +++++++++++-- .../tests/components/WincherTableRow.test.js | 82 ++++++++++++++++++- 3 files changed, 135 insertions(+), 10 deletions(-) diff --git a/css/src/modal.css b/css/src/modal.css index 714ac12d7ec..917d24b6753 100644 --- a/css/src/modal.css +++ b/css/src/modal.css @@ -498,6 +498,10 @@ display: block; } +.yoast-seo-performance-modal__mini-chart > * { + vertical-align: middle; +} + .yoast-wordproof-modal { max-width: 380px; text-align: center; diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 08ccb0d5991..577c16e41bd 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -12,9 +12,25 @@ import { makeOutboundLink } from "@yoast/helpers"; /* 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; +`; + /** * Transforms the Wincher Position data to x/y points for the SVG area chart. * @@ -74,7 +90,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" ) @@ -141,6 +156,36 @@ export function getKeyphrasePosition( keyphrase ) { */ 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 ; + } + + const isImproving = rowData.position.change < 0; + return ( + + + { Math.abs( rowData.position.change ) } + + + ); +}; + +PositionOverTimeCell.propTypes = { + rowData: PropTypes.object, +}; + /** * Gets the positional data based on the current UI state and returns the appropiate UI element. * @@ -171,18 +216,18 @@ export function getPositionalDataByState( props ) { } if ( ! hasFreshData ) { return ( - - - - - + + + ); } return ( { getKeyphrasePosition( rowData ) } - { } + + + { formatLastUpdated( rowData.updated_at ) } { diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index 906ff753688..eb4950d17d1 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -1,6 +1,11 @@ import { shallow } from "enzyme"; -import WincherTableRow, { PositionOverTimeChart } from "../../src/components/WincherTableRow"; +import WincherTableRow, { + PositionOverTimeChart, + PositionOverTimeCell, + CaretIcon, + PositionChangeValue, +} from "../../src/components/WincherTableRow"; import { Toggle } from "@yoast/components"; import WincherSEOPerformanceLoading from "../../src/components/modals/WincherSEOPerformanceLoading"; @@ -20,6 +25,7 @@ const keyphrasesData = { value: 38, }, ], + change: -2, }, // eslint-disable-next-line camelcase updated_at: new Date(), @@ -53,7 +59,7 @@ describe( "WincherTableRow", () => { expect( component.find( "td" ).length ).toEqual( 6 ); expect( component.find( Toggle ).length ).toEqual( 1 ); - expect( component.find( PositionOverTimeChart ).length ).toEqual( 1 ); + expect( component.find( PositionOverTimeCell ).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 ); @@ -72,7 +78,7 @@ describe( "WincherTableRow", () => { expect( component.find( "td" ).length ).toEqual( 6 ); expect( component.find( Toggle ).length ).toEqual( 1 ); - expect( component.find( PositionOverTimeChart ).length ).toEqual( 0 ); + expect( component.find( PositionOverTimeCell ).length ).toEqual( 0 ); expect( component.find( Toggle ).getElement().props.id ).toBe( "toggle-keyphrase-tracking-yoast seo" ); expect( component.find( Toggle ).getElement().props.isEnabled ).toBe( false ); @@ -82,3 +88,73 @@ describe( "WincherTableRow", () => { expect( component.find( "td" ).at( 4 ).text() ).toEqual( "?" ); } ); } ); + + +describe( "PositionOverTimeCell", () => { + it( "should render chart but not change if undefined position change", () => { + const component = shallow( ); + + 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( ); + + 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( ); + + 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( ); + + 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" ); + } ); +} ); From 26f8c35fec01c3cd43085bc617be064e6ca2829a Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Tue, 9 May 2023 00:01:46 +0100 Subject: [PATCH 30/55] Add support of start at for keyphrases data --- .../src/components/WincherKeyphrasesTable.js | 8 +- .../components/WincherPerformanceReport.js | 5 +- .../src/components/WincherSEOPerformance.js | 116 +++++++++++++++++- .../modals/WincherUpgradeCallout.js | 6 +- .../src/containers/WincherSEOPerformance.js | 2 + packages/js/src/helpers/wincherEndpoints.js | 3 +- .../js/src/redux/reducers/WincherRequest.js | 1 + .../js/src/redux/selectors/WincherRequest.js | 11 ++ .../wincher/wincher-account-action.php | 14 ++- .../wincher/wincher-keyphrases-action.php | 4 +- src/routes/wincher-route.php | 5 +- 11 files changed, 153 insertions(+), 22 deletions(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 5bbab192885..b646530926d 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -87,6 +87,7 @@ const WincherKeyphrasesTable = ( props ) => { websiteId, focusKeyphrase, newRequest, + startAt, } = props; const interval = useRef(); @@ -124,7 +125,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 ); @@ -227,8 +228,10 @@ 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 ); useEffect( () => { - if ( isLoggedIn && permalink && ( permalink !== prevPermalink || difference( keyphrases, prevKeyphrases ).length ) ) { + if ( isLoggedIn && permalink && + ( permalink !== prevPermalink || difference( keyphrases, prevKeyphrases ).length || startAt !== prevStartAt ) ) { getTrackedKeyphrases(); } }, [ @@ -391,6 +394,7 @@ WincherKeyphrasesTable.propTypes = { websiteId: PropTypes.string, permalink: PropTypes.string.isRequired, focusKeyphrase: PropTypes.string, + startAt: PropTypes.string, }; WincherKeyphrasesTable.defaultProps = { diff --git a/packages/js/src/components/WincherPerformanceReport.js b/packages/js/src/components/WincherPerformanceReport.js index 9d0eb6dadf5..ea1594f2c8d 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(); return ( - { isLoggedIn && } + { isLoggedIn && } diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index 6328cc64bae..27d9154ef59 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -1,11 +1,12 @@ /* global wpseoAdminL10n */ /* External dependencies */ -import { useCallback } from "@wordpress/element"; +import { useCallback, useEffect, useState } from "@wordpress/element"; 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 +14,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"; @@ -228,6 +229,81 @@ const Title = styled.div` font-size: var(--yoast-font-size-default); `; +const START_OF_TODAY = moment().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. * @@ -241,6 +317,7 @@ const TableContent = ( props ) => { keyphrases, shouldTrackAll, permalink, + historyDaysLimit, } = props; if ( ! permalink && isLoggedIn ) { @@ -251,12 +328,35 @@ const TableContent = ( props ) => { return ; } + const historyLimitDate = moment().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 ); + + useEffect( () => { + setPeriod( defaultPeriod ); + }, [ defaultPeriod?.name ] ); + + const onSelect = useCallback( ( event ) => { + const option = WINCHER_PERIOD_OPTIONS.find( opt => opt.value === event.target.value ); + if ( option ) { + setPeriod( option ); + } + }, [ setPeriod ] ); + return

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

{ isLoggedIn && shouldTrackAll && } - + + +
; }; @@ -265,6 +365,7 @@ TableContent.propTypes = { isLoggedIn: PropTypes.bool.isRequired, shouldTrackAll: PropTypes.bool.isRequired, permalink: PropTypes.string.isRequired, + historyDaysLimit: PropTypes.number, }; /** @@ -283,11 +384,12 @@ export default function WincherSEOPerformance( props ) { const onLoginCallback = useCallback( () => { onLoginOpen( props ); }, [ onLoginOpen, props ] ); + const trackingInfo = useTrackingInfo(); return ( { isNewlyAuthenticated && } - { isLoggedIn && } + { isLoggedIn && } { __( "SEO performance", "wordpress-seo" ) } @@ -302,7 +404,7 @@ export default function WincherSEOPerformance( props ) { <ConnectToWincher isLoggedIn={ isLoggedIn } onLogin={ onLoginCallback } /> <GetUserMessage { ...props } onLogin={ onLoginCallback } /> - <TableContent { ...props } /> + <TableContent { ...props } historyDaysLimit={ trackingInfo?.historyDays || 0 } /> </Wrapper> ); } @@ -315,6 +417,7 @@ WincherSEOPerformance.propTypes = { response: PropTypes.object, shouldTrackAll: PropTypes.bool, permalink: PropTypes.string, + historyDaysLimit: PropTypes.number, }; WincherSEOPerformance.defaultProps = { @@ -324,4 +427,5 @@ WincherSEOPerformance.defaultProps = { response: {}, shouldTrackAll: false, permalink: "", + historyDaysLimit: 0, }; diff --git a/packages/js/src/components/modals/WincherUpgradeCallout.js b/packages/js/src/components/modals/WincherUpgradeCallout.js index e74f41b4f24..fa4aa7012e1 100644 --- a/packages/js/src/components/modals/WincherUpgradeCallout.js +++ b/packages/js/src/components/modals/WincherUpgradeCallout.js @@ -58,7 +58,7 @@ const CalloutContainer = styled.div` * * @returns {object} The Wincher account tracking info. */ -const useTrackingInfo = () => { +export const useTrackingInfo = () => { const [ trackingInfo, setTrackingInfo ] = useState( null ); useEffect( ()=>{ @@ -221,8 +221,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 +256,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/WincherSEOPerformance.js b/packages/js/src/containers/WincherSEOPerformance.js index 67e0f6e69ad..330ff416540 100644 --- a/packages/js/src/containers/WincherSEOPerformance.js +++ b/packages/js/src/containers/WincherSEOPerformance.js @@ -11,6 +11,7 @@ export default compose( [ isWincherNewlyAuthenticated, getWincherKeyphraseLimitReached, getWincherLimit, + getWincherHistoryDaysLimit, getWincherLoginStatus, getWincherRequestIsSuccess, getWincherRequestResponse, @@ -31,6 +32,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..0af8f07742a 100644 --- a/packages/js/src/helpers/wincherEndpoints.js +++ b/packages/js/src/helpers/wincherEndpoints.js @@ -63,13 +63,14 @@ export async function authenticate( responseData ) { * * @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/src/actions/wincher/wincher-account-action.php b/src/actions/wincher/wincher-account-action.php index ea9bb54cfd8..96103bdf281 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 ); } From 2e42d76a1d04a7cc84764e930ee5e8b9f4f6fc60 Mon Sep 17 00:00:00 2001 From: Kais Zaouali <kais.zaouali@wincher.com> Date: Fri, 16 Jun 2023 14:53:32 +0100 Subject: [PATCH 31/55] Add chart --- admin/class-admin-asset-manager.php | 1 + config/webpack/externals.js | 34 +- config/webpack/webpack.config.base.js | 2 +- packages/babel-preset/index.js | 6 +- packages/babel-preset/package.json | 1 + packages/js/package.json | 18 +- .../src/components/WincherRankingHistory.js | 68 ++++ .../src/components/WincherSEOPerformance.js | 14 + yarn.lock | 320 +++++++++++------- 9 files changed, 324 insertions(+), 140 deletions(-) create mode 100644 packages/js/src/components/WincherRankingHistory.js 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..f86678c29d0 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-proposal-class-properties", + ], sourceType: "unambiguous", }; }; diff --git a/packages/babel-preset/package.json b/packages/babel-preset/package.json index cf427199114..268536e4080 100644 --- a/packages/babel-preset/package.json +++ b/packages/babel-preset/package.json @@ -12,6 +12,7 @@ "license": "GPL-3.0", "private": false, "dependencies": { + "@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-optional-chaining": "^7.17.12", "@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..0d5f3b3cdc0 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,12 @@ "@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", "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 +55,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/WincherRankingHistory.js b/packages/js/src/components/WincherRankingHistory.js new file mode 100644 index 00000000000..bbd51a14de1 --- /dev/null +++ b/packages/js/src/components/WincherRankingHistory.js @@ -0,0 +1,68 @@ +/* External dependencies */ +import { Line } from "react-chartjs-2"; +import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement } from "chart.js"; +import PropTypes from "prop-types"; + +Chart.register( CategoryScale, LineController, LineElement, PointElement, LinearScale ); + +/** + * Renders the Wincher SEO Performance modal content. + * + * @param {Object} props The props to use within the content. + * + * @returns {wp.Element} The Wincher SEO Performance modal content. + */ +export default function LineChart( { data } ) { + return ( + <Line + height={ 75 } + data={ { + datasets: [ { + data: data.map( ( { datetime, value } ) => ( { + x: datetime, + y: value, + } ) ), + borderColor: "#7CB5EC", + backgroundColor: "#E5F0FB", + lineTension: 0, + pointRadius: 0, + pointHoverRadius: 0, + } ], + } } + options={ { + tooltips: { + enabled: false, + }, + legend: { + display: false, + }, + scales: { + xAxes: [ { + gridLines: { + color: "rgba(0, 0, 0, 0)", + }, + ticks: { + display: false, + }, + type: "time", + time: { + unit: "day", + }, + } ], + yAxes: [ { + ticks: { + display: false, + }, + gridLines: { + color: "rgba(0, 0, 0, 0)", + }, + } ], + }, + } } + /> + ); +} + +LineChart.propTypes = { + data: PropTypes.array.isRequired, +}; diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index 27d9154ef59..a603677d696 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -26,6 +26,7 @@ import { authenticate, getAuthorizationUrl, trackKeyphrases } from "../helpers/w import { handleAPIResponse } from "../helpers/api"; import WincherReconnectAlert from "./modals/WincherReconnectAlert"; import WincherNoPermalinkAlert from "./modals/WincherNoPermalinkAlert"; +import LineChart from "./WincherRankingHistory"; /** * Gets the proper error message component. @@ -400,6 +401,19 @@ export default function WincherSEOPerformance( props ) { /> + + diff --git a/yarn.lock b/yarn.lock index b472ce0274b..36493f45c96 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,13 @@ 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/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 +107,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 +219,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 +368,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 +377,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" @@ -426,7 +426,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 +446,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 +456,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" @@ -479,6 +480,20 @@ "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" +"@babel/helper-create-class-features-plugin@^7.18.6": + version "7.21.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" + integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.21.0" + "@babel/helper-member-expression-to-functions" "^7.21.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.20.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-create-class-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz#327154eedfb12e977baa4ecc72e5806720a85a06" @@ -543,11 +558,6 @@ 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" @@ -585,6 +595,14 @@ "@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-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 +645,13 @@ dependencies: "@babel/types" "^7.18.9" +"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" + integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== + dependencies: + "@babel/types" "^7.21.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 +708,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 +722,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" @@ -752,7 +777,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== -"@babel/helper-plugin-utils@^7.20.2": +"@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2": version "7.20.2" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== @@ -798,6 +823,18 @@ "@babel/traverse" "^7.19.1" "@babel/types" "^7.19.0" +"@babel/helper-replace-supers@^7.20.7": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" + integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.20.7" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/template" "^7.20.7" + "@babel/traverse" "^7.20.7" + "@babel/types" "^7.20.7" + "@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 +863,13 @@ dependencies: "@babel/types" "^7.16.0" +"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" + integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== + dependencies: + "@babel/types" "^7.20.0" + "@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" @@ -882,6 +926,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 +968,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 +977,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" @@ -999,10 +1048,10 @@ 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/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" @@ -1037,6 +1086,14 @@ "@babel/helper-create-class-features-plugin" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" +"@babel/plugin-proposal-class-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-proposal-class-static-block@^7.16.7": version "7.17.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" @@ -2029,6 +2086,15 @@ "@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/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 +2159,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 +2175,22 @@ debug "^4.1.0" globals "^11.1.0" +"@babel/traverse@^7.20.7", "@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 +2225,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 +2234,15 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" +"@babel/types@^7.20.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" + "@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 +3016,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 +3032,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 +10922,13 @@ 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" + 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 +26505,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" From 33b0f114c82047553ea855e4f3b39c0f69da508a Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Fri, 30 Jun 2023 16:59:06 +0100 Subject: [PATCH 32/55] Add wincher ranking history --- packages/js/package.json | 1 + .../src/components/WincherKeyphrasesTable.js | 41 ++++- .../src/components/WincherRankingHistory.js | 68 -------- .../components/WincherRankingHistoryChart.js | 99 +++++++++++ .../src/components/WincherSEOPerformance.js | 160 +++++++++++++----- packages/js/src/components/WincherTableRow.js | 34 +++- .../src/containers/WincherKeyphrasesTable.js | 2 - .../src/containers/WincherSEOPerformance.js | 2 + yarn.lock | 5 + 9 files changed, 301 insertions(+), 111 deletions(-) delete mode 100644 packages/js/src/components/WincherRankingHistory.js create mode 100644 packages/js/src/components/WincherRankingHistoryChart.js diff --git a/packages/js/package.json b/packages/js/package.json index 0d5f3b3cdc0..f130756f2d6 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -43,6 +43,7 @@ "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", diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index b646530926d..98e5d25f619 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -19,6 +19,7 @@ import { } from "../helpers/wincherEndpoints"; import { handleAPIResponse } from "../helpers/api"; +import { Checkbox } from "@yoast/components"; const GetMoreInsightsLink = makeOutboundLink(); @@ -42,6 +43,14 @@ const TableWrapper = styled.div` overflow-y: auto; `; +const SelectKeyphraseCheckboxWrapper = styled.th` + pointer-events: ${ props => props.isDisabled ? "none" : "initial" }; + + & > div { + margin: 0px; + } +`; + /** * Hook that returns the previous value. * @@ -88,6 +97,8 @@ const WincherKeyphrasesTable = ( props ) => { focusKeyphrase, newRequest, startAt, + selectedKeyphrases, + onSelectKeyphrases, } = props; const interval = useRef(); @@ -301,12 +312,37 @@ 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 ] ); + return ( keyphrases && ! isEmpty( keyphrases ) && + + + + + { hasHistory && } + + @@ -296,6 +326,8 @@ WincherTableRow.propTypes = { isLoading: PropTypes.bool, // eslint-disable-next-line react/no-unused-prop-types websiteId: PropTypes.string, + selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, + onSelectKeyphrases: PropTypes.func.isRequired, }; WincherTableRow.defaultProps = { 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 330ff416540..f94d7826817 100644 --- a/packages/js/src/containers/WincherSEOPerformance.js +++ b/packages/js/src/containers/WincherSEOPerformance.js @@ -16,6 +16,7 @@ export default compose( [ getWincherRequestIsSuccess, getWincherRequestResponse, getWincherTrackableKeyphrases, + getWincherTrackedKeyphrases, getWincherAllKeyphrasesMissRanking, getWincherPermalink, shouldWincherAutomaticallyTrackAll, @@ -23,6 +24,7 @@ export default compose( [ return { keyphrases: getWincherTrackableKeyphrases(), + trackedKeyphrases: getWincherTrackedKeyphrases(), allKeyphrasesMissRanking: getWincherAllKeyphrasesMissRanking(), isLoggedIn: getWincherLoginStatus(), isNewlyAuthenticated: isWincherNewlyAuthenticated(), diff --git a/yarn.lock b/yarn.lock index 36493f45c96..c42ff8f662a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10929,6 +10929,11 @@ chart.js@^4.2.1: 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" From 0578fe1010ff573b8476aae554adeb9684f23571 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Mon, 3 Jul 2023 18:49:04 +0100 Subject: [PATCH 33/55] Fix test and linting --- packages/js/src/helpers/wincherEndpoints.js | 1 + .../components/WincherKeyphrasesTable.test.js | 8 +++++ .../tests/components/WincherTableRow.test.js | 7 ++++ .../wincher/wincher-account-action.php | 10 +++--- .../wincher/wincher-account-action-test.php | 36 +++++++++++-------- .../wincher-keyphrases-action-test.php | 4 +++ tests/unit/routes/wincher-route-test.php | 19 ++++++++-- 7 files changed, 63 insertions(+), 22 deletions(-) diff --git a/packages/js/src/helpers/wincherEndpoints.js b/packages/js/src/helpers/wincherEndpoints.js index 0af8f07742a..b05a87f00c1 100644 --- a/packages/js/src/helpers/wincherEndpoints.js +++ b/packages/js/src/helpers/wincherEndpoints.js @@ -58,6 +58,7 @@ 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. * 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 eb4950d17d1..9b182b51bf4 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -8,6 +8,7 @@ import WincherTableRow, { } from "../../src/components/WincherTableRow"; import { Toggle } from "@yoast/components"; import WincherSEOPerformanceLoading from "../../src/components/modals/WincherSEOPerformanceLoading"; +import { noop } from "lodash"; const keyphrasesData = { "yoast seo": { @@ -44,6 +45,8 @@ describe( "WincherTableRow", () => { const component = shallow( ); expect( component.find( "td" ).length ).toEqual( 3 ); @@ -55,6 +58,8 @@ describe( "WincherTableRow", () => { const component = shallow( ); expect( component.find( "td" ).length ).toEqual( 6 ); @@ -74,6 +79,8 @@ describe( "WincherTableRow", () => { const component = shallow( ); expect( component.find( "td" ).length ).toEqual( 6 ); diff --git a/src/actions/wincher/wincher-account-action.php b/src/actions/wincher/wincher-account-action.php index 96103bdf281..fd616841013 100644 --- a/src/actions/wincher/wincher-account-action.php +++ b/src/actions/wincher/wincher-account-action.php @@ -53,11 +53,11 @@ public function check_limit() { $history = $results['limits']['history_days']; return (object) [ - 'canTrack' => \is_null( $limit ) || $usage < $limit, - 'limit' => $limit, - 'usage' => $usage, - 'historyDays' => $history, - 'status' => 200, + 'canTrack' => \is_null( $limit ) || $usage < $limit, + 'limit' => $limit, + 'usage' => $usage, + 'historyDays' => $history, + 'status' => 200, ]; } catch ( \Exception $e ) { return (object) [ 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' ] ); From 30b1e84862f77bc7bb8a8901e3c349c7f040c510 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Tue, 18 Jul 2023 15:18:59 +0100 Subject: [PATCH 34/55] Fix extra Wincher keywords api call --- packages/js/src/components/WincherKeyphrasesTable.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 98e5d25f619..4c4ad35e868 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -152,6 +152,7 @@ const WincherKeyphrasesTable = ( props ) => { setTrackedKeyphrases, keyphrases, permalink, + startAt, ] ); /** @@ -240,8 +241,10 @@ const WincherKeyphrasesTable = ( props ) => { const prevPermalink = usePrevious( permalink ); const prevKeyphrases = usePrevious( keyphrases ); const prevStartAt = usePrevious( startAt ); + const hasParams = permalink && startAt; + useEffect( () => { - if ( isLoggedIn && permalink && + if ( isLoggedIn && hasParams && ( permalink !== prevPermalink || difference( keyphrases, prevKeyphrases ).length || startAt !== prevStartAt ) ) { getTrackedKeyphrases(); } @@ -252,6 +255,9 @@ const WincherKeyphrasesTable = ( props ) => { keyphrases, prevKeyphrases, getTrackedKeyphrases, + hasParams, + startAt, + prevStartAt, ] ); // Tracks remaining keyphrases if trackAll is set and we have data. From f95f8d2dde838a2714ef99653c1fc122c984a6d2 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 20 Jul 2023 00:57:16 +0100 Subject: [PATCH 35/55] Add mini chart click action --- css/src/modal.css | 4 --- .../src/components/WincherKeyphrasesTable.js | 3 ++ .../src/components/WincherSEOPerformance.js | 1 + packages/js/src/components/WincherTableRow.js | 30 +++++++++++++++++-- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/css/src/modal.css b/css/src/modal.css index 917d24b6753..714ac12d7ec 100644 --- a/css/src/modal.css +++ b/css/src/modal.css @@ -498,10 +498,6 @@ display: block; } -.yoast-seo-performance-modal__mini-chart > * { - vertical-align: middle; -} - .yoast-wordproof-modal { max-width: 380px; text-align: center; diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 4c4ad35e868..41d1d7e542b 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -99,6 +99,7 @@ const WincherKeyphrasesTable = ( props ) => { startAt, selectedKeyphrases, onSelectKeyphrases, + setIsChartShown, } = props; const interval = useRef(); @@ -397,6 +398,7 @@ const WincherKeyphrasesTable = ( props ) => { isLoading={ isDataLoading || loadingKeyphrases.indexOf( keyphrase.toLowerCase() ) >= 0 } selectedKeyphrases={ selectedKeyphrases } onSelectKeyphrases={ onSelectKeyphrases } + setIsChartShown={ setIsChartShown } /> ); } ) } @@ -441,6 +443,7 @@ WincherKeyphrasesTable.propTypes = { startAt: PropTypes.string, selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, onSelectKeyphrases: PropTypes.func.isRequired, + setIsChartShown: PropTypes.func.isRequired, }; WincherKeyphrasesTable.defaultProps = { diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index d6f56fd1139..ce7f29b5ac9 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -449,6 +449,7 @@ const TableContent = ( props ) => { selectedKeyphrases={ selectedKeyphrases } onSelectKeyphrases={ setSelectedKeyphrases } trackedKeyphrases={ trackedKeyphrases } + setIsChartShown={ setIsChartShown } /> ; }; diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 768c7128ce7..1bb8c1c7399 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -37,6 +37,18 @@ const SelectKeyphraseCheckboxWrapper = styled.td` } `; +const PositionOverTimeButton = styled.button` + background: none; + color: inherit; + border: none; + padding: 0; + font: inherit; + cursor: pointer; + outline: inherit; + display: flex; + align-items: center; +`; + /** * Transforms the Wincher Position data to x/y points for the SVG area chart. * @@ -200,7 +212,17 @@ PositionOverTimeCell.propTypes = { * @returns {wp.Element} The rendered element. */ export function getPositionalDataByState( props ) { - const { rowData, websiteId } = props; + const { rowData, websiteId, keyphrase, onSelectKeyphrases, setIsChartShown } = props; + + /** + * Fires when click on position over time + * + * @returns {void} + */ + const onPositionOverTimeClick = useCallback( () => { + onSelectKeyphrases( [ keyphrase ] ); + setIsChartShown( true ); + }, [ onSelectKeyphrases, keyphrase, setIsChartShown ] ); const isEnabled = ! isEmpty( rowData ); const hasFreshData = rowData && rowData.updated_at && moment( rowData.updated_at ) >= moment().subtract( 7, "days" ); @@ -231,8 +253,10 @@ export function getPositionalDataByState( props ) { return ( - - + - + diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 1bb8c1c7399..e0761cb7ecd 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -6,16 +6,13 @@ import { isEmpty } from "lodash"; import moment from "moment"; /* Yoast dependencies */ -import { Checkbox, 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; @@ -31,12 +28,23 @@ export const PositionChangeValue = styled.span` margin-left: 12px; `; -const SelectKeyphraseCheckboxWrapper = styled.td` +export const SelectKeyphraseCheckboxWrapper = styled.td` + padding-right: 0 !important; + & > div { margin: 0px; } `; +export const KeyphraseTdWrapper = styled.td` + padding-left: 2px !important; +`; + +const PositionAndViewLinkWrapper = styled.div` + display: flex; + align-items: center; +`; + const PositionOverTimeButton = styled.button` background: none; color: inherit; @@ -49,6 +57,10 @@ const PositionOverTimeButton = styled.button` 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. * @@ -238,7 +250,6 @@ export function getPositionalDataByState( props ) { - + - ); } @@ -321,7 +332,7 @@ export default function WincherTableRow( props ) { onSelectKeyphrases( prev => isSelected ? prev.filter( e => e !== keyphrase ) : prev.concat( keyphrase ) ); }, [ onSelectKeyphrases, isSelected, keyphrase ] ); - return + return { hasHistory && } + + { keyphrase }{ isFocusKeyphrase && * } + + + { getPositionalDataByState( props ) } + - - - { getPositionalDataByState( props ) } - ; + ; } WincherTableRow.propTypes = { diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index 9b182b51bf4..c0f5ec6683f 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -5,6 +5,8 @@ import WincherTableRow, { PositionOverTimeCell, CaretIcon, PositionChangeValue, + SelectKeyphraseCheckboxWrapper, + KeyphraseTdWrapper, } from "../../src/components/WincherTableRow"; import { Toggle } from "@yoast/components"; import WincherSEOPerformanceLoading from "../../src/components/modals/WincherSEOPerformanceLoading"; @@ -49,9 +51,11 @@ describe( "WincherTableRow", () => { 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( ); + expect( component.find( "td" ).length ).toEqual( 2 ); + expect( component.find( "td" ).at( 0 ).getElement().props.children ).toEqual( ); + expect( component.find( SelectKeyphraseCheckboxWrapper ).length ).toEqual( 1 ); + expect( component.find( KeyphraseTdWrapper ).length ).toEqual( 1 ); + expect( component.find( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "woocommerce seo" ); } ); it( "should render a row with the available data and with chart data", () => { @@ -62,17 +66,19 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 6 ); + expect( component.find( "td" ).length ).toEqual( 4 ); expect( component.find( Toggle ).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( 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( "td" ).at( 4 ).text() ).toEqual( "a few seconds ago" ); + 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", () => { @@ -83,16 +89,15 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 6 ); + expect( component.find( "td" ).length ).toEqual( 4 ); expect( component.find( Toggle ).length ).toEqual( 1 ); expect( component.find( PositionOverTimeCell ).length ).toEqual( 0 ); 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( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "yoast seo" ); + expect( component.find( "td" ).at( 1 ).text() ).toEqual( "?" ); expect( component.find( "td" ).at( 2 ).text() ).toEqual( "?" ); - expect( component.find( "td" ).at( 3 ).text() ).toEqual( "?" ); - expect( component.find( "td" ).at( 4 ).text() ).toEqual( "?" ); } ); } ); From f25a30cc07c0f0760a7cac252e5f1ef59b3bb4ed Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 19:03:10 +0100 Subject: [PATCH 39/55] Remove show/hide wincher chart button --- .../src/components/WincherKeyphrasesTable.js | 3 -- .../src/components/WincherSEOPerformance.js | 47 +------------------ packages/js/src/components/WincherTableRow.js | 5 +- 3 files changed, 3 insertions(+), 52 deletions(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 10d2a87b8c7..5cd64146aae 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -100,7 +100,6 @@ const WincherKeyphrasesTable = ( props ) => { startAt, selectedKeyphrases, onSelectKeyphrases, - setIsChartShown, } = props; const interval = useRef(); @@ -398,7 +397,6 @@ const WincherKeyphrasesTable = ( props ) => { isLoading={ isDataLoading || loadingKeyphrases.indexOf( keyphrase.toLowerCase() ) >= 0 } selectedKeyphrases={ selectedKeyphrases } onSelectKeyphrases={ onSelectKeyphrases } - setIsChartShown={ setIsChartShown } /> ); } ) } @@ -443,7 +441,6 @@ WincherKeyphrasesTable.propTypes = { startAt: PropTypes.string, selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, onSelectKeyphrases: PropTypes.func.isRequired, - setIsChartShown: PropTypes.func.isRequired, }; WincherKeyphrasesTable.defaultProps = { diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index 7483bbec2d7..a034431d31a 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -314,38 +314,6 @@ WincherPeriodPicker.propTypes = { isLoggedIn: PropTypes.bool.isRequired, }; -/** - * Displays the Wincher show ranking history button. - * - * @param {Object} props The component props. - * - * @returns {null|wp.Element} The Wincher show ranking history button. - */ -const WincherShowRankingHistory = ( props ) => { - const { selectedKeyphrases, isLoggedIn, isChartShown, setIsChartShown } = props; - - if ( ! isLoggedIn ) { - return null; - } - - const title = isChartShown ? __( "Hide ranking history", "wordpress-seo" ) : __( "Show ranking history", "wordpress-seo" ); - - const onClick = useCallback( () => { - setIsChartShown( prev => ! prev ); - }, [ setIsChartShown ] ); - - return ( - { title } - ); -}; - -WincherShowRankingHistory.propTypes = { - selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, - isLoggedIn: PropTypes.bool.isRequired, - isChartShown: PropTypes.bool.isRequired, - setIsChartShown: PropTypes.func.isRequired, -}; - /** * Creates the table content. * @@ -383,18 +351,12 @@ const TableContent = ( props ) => { const [ selectedKeyphrases, setSelectedKeyphrases ] = useState( [] ); - const [ isChartShown, setIsChartShown ] = useState( false ); + const isChartShown = selectedKeyphrases.length > 0; useEffect( () => { setPeriod( defaultPeriod ); }, [ defaultPeriod?.name ] ); - useEffect( () => { - if ( isChartShown && selectedKeyphrases.length === 0 ) { - setIsChartShown( false ); - } - }, [ isChartShown, setIsChartShown, selectedKeyphrases ] ); - const onSelectPeriod = useCallback( ( event ) => { const option = WINCHER_PERIOD_OPTIONS.find( opt => opt.value === event.target.value ); if ( option ) { @@ -429,12 +391,6 @@ const TableContent = ( props ) => { options={ periodOptions } isLoggedIn={ isLoggedIn } /> - @@ -449,7 +405,6 @@ const TableContent = ( props ) => { selectedKeyphrases={ selectedKeyphrases } onSelectKeyphrases={ setSelectedKeyphrases } trackedKeyphrases={ trackedKeyphrases } - setIsChartShown={ setIsChartShown } /> ; }; diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index e0761cb7ecd..3a5d1a9dfa4 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -224,7 +224,7 @@ PositionOverTimeCell.propTypes = { * @returns {wp.Element} The rendered element. */ export function getPositionalDataByState( props ) { - const { rowData, websiteId, keyphrase, onSelectKeyphrases, setIsChartShown } = props; + const { rowData, websiteId, keyphrase, onSelectKeyphrases } = props; /** * Fires when click on position over time @@ -233,8 +233,7 @@ export function getPositionalDataByState( props ) { */ const onPositionOverTimeClick = useCallback( () => { onSelectKeyphrases( [ keyphrase ] ); - setIsChartShown( true ); - }, [ onSelectKeyphrases, keyphrase, setIsChartShown ] ); + }, [ onSelectKeyphrases, keyphrase ] ); const isEnabled = ! isEmpty( rowData ); const hasFreshData = rowData && rowData.updated_at && moment( rowData.updated_at ) >= moment().subtract( 7, "days" ); From ff4a8a936fceb816fc021de3dadfea7b9f38f35f Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 20:20:50 +0100 Subject: [PATCH 40/55] Add keyphrase to Wincher chart when tracked --- packages/js/src/components/WincherSEOPerformance.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index a034431d31a..ae1838fe91c 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -2,6 +2,7 @@ /* External dependencies */ 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, orderBy } from "lodash"; @@ -353,6 +354,15 @@ const TableContent = ( props ) => { 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 ] ); From 7c3a7b1c24fdecac892f1542ce69bdb006dcf296 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 20:29:14 +0100 Subject: [PATCH 41/55] Show message for untracked keyphrases in Wincher table --- packages/js/src/components/WincherTableRow.js | 8 +++----- packages/js/tests/components/WincherTableRow.test.js | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 3a5d1a9dfa4..f7043dd6ab1 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -245,11 +245,9 @@ export function getPositionalDataByState( props ) { if ( ! isEnabled ) { return ( - - - - - + ); } if ( ! hasFreshData ) { diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index c0f5ec6683f..7b360b3bbe0 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -89,15 +89,14 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 4 ); + expect( component.find( "td" ).length ).toEqual( 2 ); expect( component.find( Toggle ).length ).toEqual( 1 ); expect( component.find( PositionOverTimeCell ).length ).toEqual( 0 ); 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( KeyphraseTdWrapper ).at( 0 ).text() ).toEqual( "yoast seo" ); - expect( component.find( "td" ).at( 1 ).text() ).toEqual( "?" ); - expect( component.find( "td" ).at( 2 ).text() ).toEqual( "?" ); + expect( component.find( "td" ).at( 0 ).text() ).toEqual( "Activate tracking to show the ranking position" ); } ); } ); From 6fd757d09094c0ac441e1ebf7ef934f8a3ef1f84 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 21:37:19 +0100 Subject: [PATCH 42/55] Sort Wincher table keyphrases by tracked --- packages/js/src/components/WincherKeyphrasesTable.js | 11 ++++++++--- packages/js/src/components/WincherTableRow.js | 6 ++---- packages/js/tests/components/WincherTableRow.test.js | 6 +++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 5cd64146aae..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 */ @@ -336,6 +336,11 @@ const WincherKeyphrasesTable = ( props ) => { 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 ) && @@ -384,7 +389,7 @@ const WincherKeyphrasesTable = ( props ) => { { - keyphrases.map( ( keyphrase, index ) => { + sortedKeyphrases.map( ( keyphrase, index ) => { return ( { websiteId={ websiteId } isDisabled={ ! isLoggedIn } isLoading={ isDataLoading || loadingKeyphrases.indexOf( keyphrase.toLowerCase() ) >= 0 } - selectedKeyphrases={ selectedKeyphrases } + isSelected={ selectedKeyphrases.includes( keyphrase ) } onSelectKeyphrases={ onSelectKeyphrases } /> ); } ) diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index f7043dd6ab1..52206bc0f35 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -295,14 +295,12 @@ export default function WincherTableRow( props ) { isFocusKeyphrase, isDisabled, isLoading, - selectedKeyphrases, + isSelected, onSelectKeyphrases, } = props; const isEnabled = ! isEmpty( rowData ); - const isSelected = selectedKeyphrases.includes( keyphrase ); - const hasHistory = ! isEmpty( rowData?.position?.history ); const toggleAction = useCallback( @@ -361,7 +359,7 @@ WincherTableRow.propTypes = { isLoading: PropTypes.bool, // eslint-disable-next-line react/no-unused-prop-types websiteId: PropTypes.string, - selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, + isSelected: PropTypes.bool.isRequired, onSelectKeyphrases: PropTypes.func.isRequired, }; diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index 7b360b3bbe0..44cb983092c 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -47,7 +47,7 @@ describe( "WincherTableRow", () => { const component = shallow( ); @@ -62,7 +62,7 @@ describe( "WincherTableRow", () => { const component = shallow( ); @@ -85,7 +85,7 @@ describe( "WincherTableRow", () => { const component = shallow( ); From f48079287225ce52e2a08c83f8b24ada03d0c519 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 22:24:03 +0100 Subject: [PATCH 43/55] Center tracking toggle in Wincher table row --- packages/js/src/components/WincherTableRow.js | 10 ++++++++-- packages/js/tests/components/WincherTableRow.test.js | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 52206bc0f35..5bee5aa2298 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -40,6 +40,12 @@ 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; @@ -343,9 +349,9 @@ export default function WincherTableRow( props ) { { getPositionalDataByState( props ) } - + ; } diff --git a/packages/js/tests/components/WincherTableRow.test.js b/packages/js/tests/components/WincherTableRow.test.js index 44cb983092c..c92111d11a2 100644 --- a/packages/js/tests/components/WincherTableRow.test.js +++ b/packages/js/tests/components/WincherTableRow.test.js @@ -7,6 +7,7 @@ import WincherTableRow, { PositionChangeValue, SelectKeyphraseCheckboxWrapper, KeyphraseTdWrapper, + TrackingTdWrapper, } from "../../src/components/WincherTableRow"; import { Toggle } from "@yoast/components"; import WincherSEOPerformanceLoading from "../../src/components/modals/WincherSEOPerformanceLoading"; @@ -51,11 +52,12 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 2 ); + expect( component.find( "td" ).length ).toEqual( 1 ); expect( component.find( "td" ).at( 0 ).getElement().props.children ).toEqual( ); 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", () => { @@ -66,11 +68,12 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 4 ); + expect( component.find( "td" ).length ).toEqual( 3 ); expect( component.find( Toggle ).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 ); @@ -89,9 +92,10 @@ describe( "WincherTableRow", () => { onSelectKeyphrases={ noop } /> ); - expect( component.find( "td" ).length ).toEqual( 2 ); + expect( component.find( "td" ).length ).toEqual( 1 ); expect( component.find( Toggle ).length ).toEqual( 1 ); 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 ); From 2277127e73e297fa590358190ced9b9d6770376f Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 23:17:50 +0100 Subject: [PATCH 44/55] Use modal instead of metabox for Wincher SEO performance --- .../components/WincherSEOPerformanceModal.js | 28 +++++++++++++++ .../js/src/components/fills/MetaboxFill.js | 36 ++++--------------- packages/js/src/containers/MetaboxFill.js | 10 +----- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/packages/js/src/components/WincherSEOPerformanceModal.js b/packages/js/src/components/WincherSEOPerformanceModal.js index 07b1f3cc8a5..257c42f7ca9 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 { CollapsibleStateless } 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( CollapsibleStateless )` + h2 > button { + padding-left: 24px; + padding-top: 16px; + + &:hover { + background-color: #f0f0f0; + } + } +`; + /** * Handles the click event on the "Track SEO performance" button. * @@ -106,6 +121,19 @@ export default function WincherSEOPerformanceModal( props ) { onClick={ onModalOpen } /> } + + { location === "metabox" && } ); } 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 } { settings.isKeywordAnalysisActive && settings.isWincherIntegrationActive && - - - - } + + + } { settings.isCornerstoneActive && } @@ -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/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 ); From b2b1d93010c308788a237d75dab7866c1d0952a2 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Mon, 25 Sep 2023 11:04:25 +0100 Subject: [PATCH 45/55] Fix focus keyphrase when there is no focus keyphrase --- packages/js/src/components/WincherSEOPerformanceModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformanceModal.js b/packages/js/src/components/WincherSEOPerformanceModal.js index 257c42f7ca9..8099d242ff4 100644 --- a/packages/js/src/components/WincherSEOPerformanceModal.js +++ b/packages/js/src/components/WincherSEOPerformanceModal.js @@ -48,7 +48,7 @@ export function openModal( props ) { if ( ! keyphrases.length ) { // This is fragile, should replace with a real React ref. - document.querySelector( "#focus-keyword-input-sidebar" ).focus(); + document.querySelector( "#focus-keyword-input-metabox" ).focus(); onNoKeyphraseSet(); return; From 1469c849b802a0ea217fd31a67d87aa124eec81f Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Mon, 25 Sep 2023 11:21:12 +0100 Subject: [PATCH 46/55] Replace deprecated babel plugin proposal --- packages/babel-preset/index.js | 2 +- packages/babel-preset/package.json | 2 +- yarn.lock | 182 ++++++++++++++++++++++------- 3 files changed, 139 insertions(+), 47 deletions(-) diff --git a/packages/babel-preset/index.js b/packages/babel-preset/index.js index f86678c29d0..51798047fda 100644 --- a/packages/babel-preset/index.js +++ b/packages/babel-preset/index.js @@ -6,7 +6,7 @@ module.exports = ( api ) => { plugins: [ "@babel/plugin-proposal-optional-chaining", "@babel/plugin-transform-runtime", - "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-class-properties", ], sourceType: "unambiguous", }; diff --git a/packages/babel-preset/package.json b/packages/babel-preset/package.json index 268536e4080..df81cbc8084 100644 --- a/packages/babel-preset/package.json +++ b/packages/babel-preset/package.json @@ -12,8 +12,8 @@ "license": "GPL-3.0", "private": false, "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.18.6", "@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/yarn.lock b/yarn.lock index c42ff8f662a..b586be195fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,6 +82,14 @@ 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" @@ -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" @@ -480,20 +495,6 @@ "@babel/helper-replace-supers" "^7.16.7" "@babel/helper-split-export-declaration" "^7.16.7" -"@babel/helper-create-class-features-plugin@^7.18.6": - version "7.21.4" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz#3a017163dc3c2ba7deb9a7950849a9586ea24c18" - integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-create-class-features-plugin@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz#327154eedfb12e977baa4ecc72e5806720a85a06" @@ -507,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" @@ -563,6 +579,11 @@ 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" @@ -603,6 +624,14 @@ "@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" @@ -645,12 +674,12 @@ dependencies: "@babel/types" "^7.18.9" -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz#319c6a940431a133897148515877d2f3269c3ba5" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== +"@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.21.0" + "@babel/types" "^7.23.0" "@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.13.12": version "7.13.12" @@ -757,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" @@ -777,11 +813,16 @@ resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.17.12.tgz#86c2347da5acbf5583ba0a10aed4c9bf9da9cf96" integrity sha512-JDkf04mqtN3y4iAbO1hv9U2ARpPyPL1zqyWs/2WG1pgSq9llHFjStX5jdxb84himgJm+8Ng+x0oiWF/nw/XQKA== -"@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2": +"@babel/helper-plugin-utils@^7.20.2": version "7.20.2" 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" @@ -823,17 +864,14 @@ "@babel/traverse" "^7.19.1" "@babel/types" "^7.19.0" -"@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== +"@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.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" + "@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" @@ -863,12 +901,12 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== +"@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.20.0" + "@babel/types" "^7.22.5" "@babel/helper-split-export-declaration@^7.12.13": version "7.12.13" @@ -891,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" @@ -911,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" @@ -1013,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" @@ -1053,6 +1117,11 @@ 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" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" @@ -1086,14 +1155,6 @@ "@babel/helper-create-class-features-plugin" "^7.16.7" "@babel/helper-plugin-utils" "^7.16.7" -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-proposal-class-static-block@^7.16.7": version "7.17.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz#164e8fd25f0d80fa48c5a4d1438a6629325ad83c" @@ -1458,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" @@ -2095,6 +2164,15 @@ "@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" @@ -2175,7 +2253,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4": +"@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== @@ -2234,7 +2312,7 @@ "@babel/helper-validator-identifier" "^7.19.1" to-fast-properties "^2.0.0" -"@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4": +"@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== @@ -2243,6 +2321,15 @@ "@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" @@ -28365,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" From 1d5a0755f80ee3a97f36f89e3d15a2404ccc01d5 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Mon, 25 Sep 2023 11:36:53 +0100 Subject: [PATCH 47/55] Fix last month period option when history limit is 31 days --- packages/js/src/components/WincherSEOPerformance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index ae1838fe91c..508c9c70b16 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -340,7 +340,7 @@ const TableContent = ( props ) => { return ; } - const historyLimitDate = moment().subtract( historyDaysLimit, "days" ); + const historyLimitDate = START_OF_TODAY.subtract( historyDaysLimit, "days" ); const periodOptions = WINCHER_PERIOD_OPTIONS.filter( opt => moment( opt.value ).isSameOrAfter( historyLimitDate ) From 524ac92b90081bc8c7567097d7d76451b50992e0 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Wed, 27 Sep 2023 11:57:53 +0100 Subject: [PATCH 48/55] Remove check limit call for not logged in users --- packages/js/src/components/WincherPerformanceReport.js | 2 +- packages/js/src/components/WincherSEOPerformance.js | 2 +- .../js/src/components/modals/WincherUpgradeCallout.js | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/js/src/components/WincherPerformanceReport.js b/packages/js/src/components/WincherPerformanceReport.js index ea1594f2c8d..c7a464b3f57 100644 --- a/packages/js/src/components/WincherPerformanceReport.js +++ b/packages/js/src/components/WincherPerformanceReport.js @@ -465,7 +465,7 @@ const WincherPerformanceReport = ( props ) => { const data = isLoggedIn ? props.data : fakeWincherPerformanceData; const isBlurred = ! isLoggedIn; const hasResults = checkHasResults( data ); - const trackingInfo = useTrackingInfo(); + const trackingInfo = useTrackingInfo( isLoggedIn ); return ( { onLoginOpen( props ); }, [ onLoginOpen, props ] ); - const trackingInfo = useTrackingInfo(); + const trackingInfo = useTrackingInfo( isLoggedIn ); return ( diff --git a/packages/js/src/components/modals/WincherUpgradeCallout.js b/packages/js/src/components/modals/WincherUpgradeCallout.js index fa4aa7012e1..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. */ -export 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 @@ export const useTrackingInfo = () => { return trackingInfo; }; +useTrackingInfo.propTypes = { + limit: PropTypes.bool.isRequired, +}; + /** * Hook to fetch the upgrade campaign. * From 709ca8c13f9faa1754923a8bd3e7355d21574a5e Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Wed, 27 Sep 2023 12:01:01 +0100 Subject: [PATCH 49/55] Use statefull collapsible instead of stateless one in WincherSEOPerformanceModal --- packages/js/src/components/WincherSEOPerformanceModal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/js/src/components/WincherSEOPerformanceModal.js b/packages/js/src/components/WincherSEOPerformanceModal.js index 8099d242ff4..5369c71c273 100644 --- a/packages/js/src/components/WincherSEOPerformanceModal.js +++ b/packages/js/src/components/WincherSEOPerformanceModal.js @@ -8,7 +8,7 @@ import styled from "styled-components"; /* Yoast dependencies */ import { colors } from "@yoast/style-guide"; -import { CollapsibleStateless } from "@yoast/components"; +import { Collapsible } from "@yoast/components"; /* Internal dependencies */ import { ModalContainer } from "./modals/Container"; @@ -25,7 +25,7 @@ const StyledHeroIcon = styled( ChartBarIcon )` margin: 3px; `; -const MetaboxModalButton = styled( CollapsibleStateless )` +const MetaboxModalButton = styled( Collapsible )` h2 > button { padding-left: 24px; padding-top: 16px; From 49be8ea6cfdbf105fefb28e8f05c32d9b72cf8ff Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 28 Sep 2023 13:01:16 +0100 Subject: [PATCH 50/55] Use utc for start at date --- packages/js/src/components/WincherSEOPerformance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index 3a1e7e849d2..ff13714070e 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -242,7 +242,7 @@ const ChartWrapper = styled.div` margin: 8px 0; `; -const START_OF_TODAY = moment().startOf( "day" ); +const START_OF_TODAY = moment.utc().startOf( "day" ); const WINCHER_PERIOD_OPTIONS = [ { From 2bd843b11f32892d56747cec0c2dc394a0e5aced Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Fri, 29 Sep 2023 14:05:26 +0100 Subject: [PATCH 51/55] Fix open modal in elementor --- packages/js/src/components/WincherSEOPerformanceModal.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformanceModal.js b/packages/js/src/components/WincherSEOPerformanceModal.js index 5369c71c273..29922bbbd71 100644 --- a/packages/js/src/components/WincherSEOPerformanceModal.js +++ b/packages/js/src/components/WincherSEOPerformanceModal.js @@ -48,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-metabox" ).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; From 8a1a4cb5a002e4282be01f558cf3f3ffdb3f15bd Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Fri, 29 Sep 2023 17:16:51 +0100 Subject: [PATCH 52/55] Fix last year is shown when it should not --- packages/js/src/components/WincherSEOPerformance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index ff13714070e..74d23307b17 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -340,7 +340,7 @@ const TableContent = ( props ) => { return ; } - const historyLimitDate = START_OF_TODAY.subtract( historyDaysLimit, "days" ); + const historyLimitDate = moment( START_OF_TODAY ).subtract( historyDaysLimit, "days" ); const periodOptions = WINCHER_PERIOD_OPTIONS.filter( opt => moment( opt.value ).isSameOrAfter( historyLimitDate ) From a7992acfb0863f70c54b7186214e7b10014433be Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Tue, 3 Oct 2023 13:13:10 +0100 Subject: [PATCH 53/55] Fix dataset color changing when deselected then selected in Wincher chart --- .../components/WincherRankingHistoryChart.js | 33 ++++++++++++++++--- .../src/components/WincherSEOPerformance.js | 4 +-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/js/src/components/WincherRankingHistoryChart.js b/packages/js/src/components/WincherRankingHistoryChart.js index 3a4262a6614..904303061e2 100644 --- a/packages/js/src/components/WincherRankingHistoryChart.js +++ b/packages/js/src/components/WincherRankingHistoryChart.js @@ -1,12 +1,35 @@ /* External dependencies */ import { Line } from "react-chartjs-2"; -import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement, TimeScale, Colors, Legend, Tooltip } from "chart.js"; +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, Colors, Legend, Tooltip ); +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. @@ -20,7 +43,7 @@ export default function WincherRankingHistoryChart( { datasets, isChartShown } ) return null; } - const data = datasets.map( dataset => ( { + const data = datasets.map( ( dataset, index ) => ( { ...dataset, data: dataset.data.map( ( { datetime, value } ) => ( { x: datetime, @@ -31,7 +54,8 @@ export default function WincherRankingHistoryChart( { datasets, isChartShown } ) pointHoverRadius: 4, borderWidth: 2, pointHitRadius: 6, - } ) ); + backgroundColor: CHART_COLORS[ index % CHART_COLORS.length ], + } ) ).filter( dataset => dataset.selected !== false ); return ( { if ( isEmpty( trackedKeyphrases ) ) { return []; } - return Object.values( trackedKeyphrases ).filter( keyphrase => selectedKeyphrases.includes( keyphrase.keyword ) ) - .filter( keyphrase => ! isEmpty( keyphrase.position?.history ) ) + return Object.values( trackedKeyphrases ) .map( keyphrase => ( { label: keyphrase.keyword, data: keyphrase.position.history, + selected: selectedKeyphrases.includes( keyphrase.keyword ) && ! isEmpty( keyphrase.position?.history ), } ) ); }, [ selectedKeyphrases, trackedKeyphrases ] ); From 4cccccf8d6dbb7dcf81740f07be40d09bf364a3e Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 5 Oct 2023 17:53:42 +0100 Subject: [PATCH 54/55] Do not display previous post keyphrases in Wincher table --- packages/js/src/redux/selectors/WincherSEOPerformance.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 From 6a4a84c16d3e23c8f356098bd7cfdfc7bd8227f3 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Wed, 11 Oct 2023 16:30:30 +0100 Subject: [PATCH 55/55] Fix tracking keyword is crashing Wincher chart --- packages/js/src/components/WincherSEOPerformance.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index c0e07e04d44..c808b3af28c 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -382,6 +382,7 @@ const TableContent = ( props ) => { return []; } return Object.values( trackedKeyphrases ) + .filter( keyphrase => !! keyphrase?.position?.history ) .map( keyphrase => ( { label: keyphrase.keyword, data: keyphrase.position.history,
{ websiteId={ websiteId } isDisabled={ ! isLoggedIn } isLoading={ isDataLoading || loadingKeyphrases.indexOf( keyphrase.toLowerCase() ) >= 0 } + selectedKeyphrases={ selectedKeyphrases } + onSelectKeyphrases={ onSelectKeyphrases } /> ); } ) } @@ -395,6 +433,8 @@ WincherKeyphrasesTable.propTypes = { permalink: PropTypes.string.isRequired, focusKeyphrase: PropTypes.string, startAt: PropTypes.string, + selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, + onSelectKeyphrases: PropTypes.func.isRequired, }; WincherKeyphrasesTable.defaultProps = { @@ -402,7 +442,6 @@ WincherKeyphrasesTable.defaultProps = { isNewlyAuthenticated: false, keyphrases: [], trackAll: false, - trackedKeyphrases: null, websiteId: "", focusKeyphrase: "", }; diff --git a/packages/js/src/components/WincherRankingHistory.js b/packages/js/src/components/WincherRankingHistory.js deleted file mode 100644 index bbd51a14de1..00000000000 --- a/packages/js/src/components/WincherRankingHistory.js +++ /dev/null @@ -1,68 +0,0 @@ -/* External dependencies */ -import { Line } from "react-chartjs-2"; -import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement } from "chart.js"; -import PropTypes from "prop-types"; - -Chart.register( CategoryScale, LineController, LineElement, PointElement, LinearScale ); - -/** - * Renders the Wincher SEO Performance modal content. - * - * @param {Object} props The props to use within the content. - * - * @returns {wp.Element} The Wincher SEO Performance modal content. - */ -export default function LineChart( { data } ) { - return ( - ( { - x: datetime, - y: value, - } ) ), - borderColor: "#7CB5EC", - backgroundColor: "#E5F0FB", - lineTension: 0, - pointRadius: 0, - pointHoverRadius: 0, - } ], - } } - options={ { - tooltips: { - enabled: false, - }, - legend: { - display: false, - }, - scales: { - xAxes: [ { - gridLines: { - color: "rgba(0, 0, 0, 0)", - }, - ticks: { - display: false, - }, - type: "time", - time: { - unit: "day", - }, - } ], - yAxes: [ { - ticks: { - display: false, - }, - gridLines: { - color: "rgba(0, 0, 0, 0)", - }, - } ], - }, - } } - /> - ); -} - -LineChart.propTypes = { - data: PropTypes.array.isRequired, -}; diff --git a/packages/js/src/components/WincherRankingHistoryChart.js b/packages/js/src/components/WincherRankingHistoryChart.js new file mode 100644 index 00000000000..57ba36303e0 --- /dev/null +++ b/packages/js/src/components/WincherRankingHistoryChart.js @@ -0,0 +1,99 @@ +/* External dependencies */ +import { Line } from "react-chartjs-2"; +import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement, TimeScale, Colors, Legend } from "chart.js"; +import "chartjs-adapter-moment"; +import PropTypes from "prop-types"; +import { noop } from "lodash"; + +Chart.register( CategoryScale, LineController, LineElement, PointElement, LinearScale, TimeScale, Colors, Legend ); + +/** + * 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 => ( { + ...dataset, + data: dataset.data.map( ( { datetime, value } ) => ( { + x: datetime, + y: value, + } ) ), + lineTension: 0, + pointRadius: 0, + pointHoverRadius: 0, + borderWidth: 2, + } ) ); + + return ( + + ); +} + +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, + } ) + ).isRequired, + isChartShown: PropTypes.bool.isRequired, +}; diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index a603677d696..d6f56fd1139 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -1,7 +1,7 @@ /* global wpseoAdminL10n */ /* External dependencies */ -import { useCallback, useEffect, useState } from "@wordpress/element"; +import { useCallback, useEffect, useMemo, useState } from "@wordpress/element"; import { __, sprintf } from "@wordpress/i18n"; import PropTypes from "prop-types"; import { isEmpty, orderBy } from "lodash"; @@ -26,7 +26,7 @@ import { authenticate, getAuthorizationUrl, trackKeyphrases } from "../helpers/w import { handleAPIResponse } from "../helpers/api"; import WincherReconnectAlert from "./modals/WincherReconnectAlert"; import WincherNoPermalinkAlert from "./modals/WincherNoPermalinkAlert"; -import LineChart from "./WincherRankingHistory"; +import WincherRankingHistoryChart from "./WincherRankingHistoryChart"; /** * Gets the proper error message component. @@ -230,6 +230,17 @@ 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: 0 20px; +`; + const START_OF_TODAY = moment().startOf( "day" ); const WINCHER_PERIOD_OPTIONS = [ @@ -274,27 +285,25 @@ const WincherPeriodPicker = ( props ) => { } return ( -
- -
+ ); }; @@ -305,6 +314,38 @@ WincherPeriodPicker.propTypes = { isLoggedIn: PropTypes.bool.isRequired, }; +/** + * Displays the Wincher show ranking history button. + * + * @param {Object} props The component props. + * + * @returns {null|wp.Element} The Wincher show ranking history button. + */ +const WincherShowRankingHistory = ( props ) => { + const { selectedKeyphrases, isLoggedIn, isChartShown, setIsChartShown } = props; + + if ( ! isLoggedIn ) { + return null; + } + + const title = isChartShown ? __( "Hide ranking history", "wordpress-seo" ) : __( "Show ranking history", "wordpress-seo" ); + + const onClick = useCallback( () => { + setIsChartShown( prev => ! prev ); + }, [ setIsChartShown ] ); + + return ( + { title } + ); +}; + +WincherShowRankingHistory.propTypes = { + selectedKeyphrases: PropTypes.arrayOf( PropTypes.string ).isRequired, + isLoggedIn: PropTypes.bool.isRequired, + isChartShown: PropTypes.bool.isRequired, + setIsChartShown: PropTypes.func.isRequired, +}; + /** * Creates the table content. * @@ -314,6 +355,7 @@ WincherPeriodPicker.propTypes = { */ const TableContent = ( props ) => { const { + trackedKeyphrases, isLoggedIn, keyphrases, shouldTrackAll, @@ -339,29 +381,80 @@ const TableContent = ( props ) => { const [ period, setPeriod ] = useState( defaultPeriod ); + const [ selectedKeyphrases, setSelectedKeyphrases ] = useState( [] ); + + const [ isChartShown, setIsChartShown ] = useState( false ); + useEffect( () => { setPeriod( defaultPeriod ); }, [ defaultPeriod?.name ] ); - const onSelect = useCallback( ( event ) => { + useEffect( () => { + if ( isChartShown && selectedKeyphrases.length === 0 ) { + setIsChartShown( false ); + } + }, [ isChartShown, setIsChartShown, selectedKeyphrases ] ); + + 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 => selectedKeyphrases.includes( keyphrase.keyword ) ) + .filter( keyphrase => ! isEmpty( keyphrase.position?.history ) ) + .map( keyphrase => ( { + label: keyphrase.keyword, + data: 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, @@ -401,19 +494,6 @@ export default function WincherSEOPerformance( props ) { /> - - @@ -424,6 +504,7 @@ export default function WincherSEOPerformance( props ) { } WincherSEOPerformance.propTypes = { + trackedKeyphrases: PropTypes.object, addTrackedKeyphrase: PropTypes.func.isRequired, isLoggedIn: PropTypes.bool, isNewlyAuthenticated: PropTypes.bool, @@ -435,6 +516,7 @@ WincherSEOPerformance.propTypes = { }; WincherSEOPerformance.defaultProps = { + trackedKeyphrases: null, isLoggedIn: false, isNewlyAuthenticated: false, keyphrases: [], diff --git a/packages/js/src/components/WincherTableRow.js b/packages/js/src/components/WincherTableRow.js index 577c16e41bd..768c7128ce7 100644 --- a/packages/js/src/components/WincherTableRow.js +++ b/packages/js/src/components/WincherTableRow.js @@ -6,7 +6,7 @@ import { isEmpty } from "lodash"; import moment from "moment"; /* Yoast dependencies */ -import { SvgIcon, Toggle } from "@yoast/components"; +import { Checkbox, SvgIcon, Toggle } from "@yoast/components"; import { makeOutboundLink } from "@yoast/helpers"; /* Internal dependencies */ @@ -31,6 +31,12 @@ export const PositionChangeValue = styled.span` margin-left: 12px; `; +const SelectKeyphraseCheckboxWrapper = styled.td` + & > div { + margin: 0px; + } +`; + /** * Transforms the Wincher Position data to x/y points for the SVG area chart. * @@ -257,10 +263,16 @@ export default function WincherTableRow( props ) { isFocusKeyphrase, isDisabled, isLoading, + selectedKeyphrases, + onSelectKeyphrases, } = props; const isEnabled = ! isEmpty( rowData ); + const isSelected = selectedKeyphrases.includes( keyphrase ); + + const hasHistory = ! isEmpty( rowData?.position?.history ); + const toggleAction = useCallback( () => { if ( isDisabled ) { @@ -276,7 +288,25 @@ export default function WincherTableRow( props ) { [ keyphrase, onTrackKeyphrase, onUntrackKeyphrase, isEnabled, rowData, isDisabled ] ); + /** + * 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
{ renderToggleState( { keyphrase, isEnabled, toggleAction, isLoading } ) } { getKeyphrasePosition( rowData ) } - + + + + { formatLastUpdated( rowData.updated_at ) } From 5450739744fdae09c35208cb0b172bc9fe0af57b Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Wed, 19 Jul 2023 22:58:38 +0100 Subject: [PATCH 36/55] Add tooltip to Wincher ranking history chart --- .../components/WincherRankingHistoryChart.js | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/js/src/components/WincherRankingHistoryChart.js b/packages/js/src/components/WincherRankingHistoryChart.js index 57ba36303e0..3a4262a6614 100644 --- a/packages/js/src/components/WincherRankingHistoryChart.js +++ b/packages/js/src/components/WincherRankingHistoryChart.js @@ -1,11 +1,12 @@ /* External dependencies */ import { Line } from "react-chartjs-2"; -import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement, TimeScale, Colors, Legend } from "chart.js"; +import { CategoryScale, Chart, LineController, LineElement, LinearScale, PointElement, TimeScale, Colors, 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, Colors, Legend ); +Chart.register( CategoryScale, LineController, LineElement, PointElement, LinearScale, TimeScale, Colors, Legend, Tooltip ); /** * Renders the Wincher ranking history chart. @@ -26,9 +27,10 @@ export default function WincherRankingHistoryChart( { datasets, isChartShown } ) y: value, } ) ), lineTension: 0, - pointRadius: 0, - pointHoverRadius: 0, + pointRadius: 1, + pointHoverRadius: 4, borderWidth: 2, + pointHitRadius: 6, } ) ); return ( @@ -50,6 +52,20 @@ export default function WincherRankingHistoryChart( { datasets, isChartShown } ) }, onClick: noop, }, + tooltip: { + enabled: true, + callbacks: { + title: ( x ) => 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: { From 2061932cad8888e5610df3c4c7d855e3d9d03c91 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Wed, 9 Aug 2023 21:54:55 +0100 Subject: [PATCH 37/55] Add horizontal margin to Wincher chart --- packages/js/src/components/WincherSEOPerformance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js/src/components/WincherSEOPerformance.js b/packages/js/src/components/WincherSEOPerformance.js index ce7f29b5ac9..7483bbec2d7 100644 --- a/packages/js/src/components/WincherSEOPerformance.js +++ b/packages/js/src/components/WincherSEOPerformance.js @@ -238,7 +238,7 @@ const WincherChartSettings = styled.div.attrs( { className: "yoast-field-group" `; const ChartWrapper = styled.div` - margin: 0 20px; + margin: 8px 0; `; const START_OF_TODAY = moment().startOf( "day" ); From e61f7d25717ea05ffe39e2d8b4664041ed701225 Mon Sep 17 00:00:00 2001 From: Kais Zaouali Date: Thu, 10 Aug 2023 17:03:32 +0100 Subject: [PATCH 38/55] Restyle Wincher table --- .../src/components/WincherKeyphrasesTable.js | 26 +++++----- packages/js/src/components/WincherTableRow.js | 52 ++++++++++++------- .../tests/components/WincherTableRow.test.js | 27 ++++++---- 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/packages/js/src/components/WincherKeyphrasesTable.js b/packages/js/src/components/WincherKeyphrasesTable.js index 41d1d7e542b..10d2a87b8c7 100644 --- a/packages/js/src/components/WincherKeyphrasesTable.js +++ b/packages/js/src/components/WincherKeyphrasesTable.js @@ -34,10 +34,6 @@ const FocusKeyphraseFootnote = styled.span` } `; -const ViewColumn = styled.th` - min-width: 60px; -`; - const TableWrapper = styled.div` width: 100%; overflow-y: auto; @@ -45,12 +41,17 @@ const TableWrapper = styled.div` 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. * @@ -350,18 +351,12 @@ const WincherKeyphrasesTable = ( props ) => { label="" /> - - { __( "Tracking", "wordpress-seo" ) } - { __( "Keyphrase", "wordpress-seo" ) } - { > { __( "Last updated", "wordpress-seo" ) } + { __( "Tracking", "wordpress-seo" ) } +
? ? ? ); } @@ -252,20 +263,20 @@ export function getPositionalDataByState( props ) { return ( - { getKeyphrasePosition( rowData ) } + + { getKeyphrasePosition( rowData ) } + + { __( "View", "wordpress-seo" ) } + + + { formatLastUpdated( rowData.updated_at ) } - { - - { __( "View", "wordpress-seo" ) } - - } -
{ renderToggleState( { keyphrase, isEnabled, toggleAction, isLoading } ) } { keyphrase }{ isFocusKeyphrase && * }
??? + { __( "Activate tracking to show the ranking position", "wordpress-seo" ) } +
+ { renderToggleState( { keyphrase, isEnabled, toggleAction, isLoading } ) } -