• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / 15936d2b846c322fa3b4b91e32ba3f7c002a45a6

13 May 2025 08:35AM UTC coverage: 58.702% (+0.06%) from 58.644%
15936d2b846c322fa3b4b91e32ba3f7c002a45a6

Pull #22258

github

web-flow
Merge pull request #22254 from Yoast/fix/ai-optimize-woo-upsell

Adds an upsell for AI Optimize on products
Pull Request #22258: Merges the feature branch `feature/ai-optimize-classic` to `trunk`

8175 of 14231 branches covered (57.45%)

Branch coverage included in aggregate %.

37 of 68 new or added lines in 11 files covered. (54.41%)

3 existing lines in 2 files now uncovered.

14070 of 23664 relevant lines covered (59.46%)

100886.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.0
/packages/js/src/ai-optimizer/components/ai-optimize-button.js
1
import PropTypes from "prop-types";
2
import { __ } from "@wordpress/i18n";
3
import { useCallback, useRef, useState } from "@wordpress/element";
4
import { doAction } from "@wordpress/hooks";
5
import { useSelect, useDispatch } from "@wordpress/data";
6

7
/* Yoast dependencies */
8
import { IconAIFixesButton, SparklesIcon } from "@yoast/components";
9
import { Modal, useToggleState } from "@yoast/ui-library";
10
import { Paper } from "yoastseo";
11

12
/* Internal dependencies */
13
import { ModalContent } from "./modal-content";
14
import { getAllBlocks } from "../../helpers/getAllBlocks";
15
import { LockClosedIcon } from "@heroicons/react/solid";
16
import { isTextViewActive } from "../../lib/tinymce";
17

18
/**
19
 * Returns the editor mode based on the editor type.
20
 * @returns {string} The editor mode, either "visual" or "text".
21
 */
22
const getEditorMode = () => {
2✔
23
        const editorType = useSelect( ( select ) => select( "yoast-seo/editor" ).getEditorType(), [] );
28✔
24

25
        if ( editorType === "blockEditor" ) {
28✔
26
                return useSelect( ( select ) => select( "core/edit-post" ).getEditorMode(), [] );
24✔
27
        } else if ( editorType === "classicEditor" ) {
4!
28
                return isTextViewActive() ? "text" : "visual";
4✔
29
        }
NEW
30
        return "";
×
31
};
32

33
/**
34
 * The AI Optimize button component.
35
 *
36
 * @param {string} id The assessment ID which AI Optimize should be applied to.
37
 * @param {boolean} isPremium Whether the Premium add-on is active.
38
 *
39
 * @returns {JSX.Element} The AI Optimize button.
40
 */
41
const AIOptimizeButton = ( { id, isPremium } ) => {
2✔
42
        // The AI Optimize button ID is the same as the assessment ID, with "AIFixes" appended to it.
43
        // We continue to use "AIFixes" in the ID to keep it consistent with the Premium implementation.
44
        const aiOptimizeId = id + "AIFixes";
28✔
45
        const [ isModalOpen, , , setIsModalOpenTrue, setIsModalOpenFalse ] = useToggleState( false );
28✔
46
        const { activeMarker, activeAIButtonId, editorType, isWooSeoUpsellPost } = useSelect( ( select ) => ( {
28✔
47
                activeMarker: select( "yoast-seo/editor" ).getActiveMarker(),
48
                activeAIButtonId: select( "yoast-seo/editor" ).getActiveAIFixesButton(),
49
                editorType: select( "yoast-seo/editor" ).getEditorType(),
50
                isWooSeoUpsellPost: select( "yoast-seo/editor" ).getIsWooSeoUpsell(),
51
        } ), [] );
52
        const editorMode = getEditorMode();
28✔
53

54
        const shouldShowUpsell = ! isPremium || isWooSeoUpsellPost;
28✔
55

56
        const { setActiveAIFixesButton, setActiveMarker, setMarkerPauseStatus, setMarkerStatus } = useDispatch( "yoast-seo/editor" );
28✔
57
        const focusElementRef = useRef( null );
28✔
58
        const [ buttonClass, setButtonClass ] = useState( "" );
28✔
59

60
        const defaultLabel = __( "Optimize with AI", "wordpress-seo" );
28✔
61
        const htmlLabel = __( "Please switch to the visual editor to optimize with AI.", "wordpress-seo" );
28✔
62

63
        // The button is pressed when the active AI button id is the same as the current button id.
64
        const isButtonPressed = activeAIButtonId === aiOptimizeId;
28✔
65

66
        // Enable the button when:
67
        // (1) other AI buttons are not pressed.
68
        // (2) the AI button is not disabled.
69
        // (3) the editor is in visual mode.
70
        // (4) all blocks are in visual mode.
71
        // eslint-disable-next-line complexity
72
        const { isEnabled, ariaLabel } = useSelect( ( select ) => {
28✔
73
                if ( activeAIButtonId !== null && ! isButtonPressed ) {
28!
74
                        return {
×
75
                                isEnabled: false,
76
                                ariaLabel: null,
77
                        };
78
                }
79

80
                const disabledAIButtons = select( "yoast-seo/editor" ).getDisabledAIFixesButtons();
28✔
81
                if ( Object.keys( disabledAIButtons ).includes( aiOptimizeId ) ) {
28✔
82
                        return {
2✔
83
                                isEnabled: false,
84
                                ariaLabel: disabledAIButtons[ aiOptimizeId ],
85
                        };
86
                }
87

88
                if ( editorMode !== "visual" ) {
26✔
89
                        return {
4✔
90
                                isEnabled: false,
91
                                ariaLabel: htmlLabel,
92
                        };
93
                }
94

95
                if ( editorType === "blockEditor" ) {
22✔
96
                        const blocks = getAllBlocks( select( "core/block-editor" ).getBlocks() );
20✔
97
                        const allVisual = blocks.every( block => select( "core/block-editor" ).getBlockMode( block.clientId ) === "visual" );
20✔
98
                        return {
20✔
99
                                isEnabled: allVisual,
100
                                ariaLabel: allVisual ? defaultLabel : htmlLabel,
10✔
101
                        };
102
                }
103

104
                return {
2✔
105
                        isEnabled: true,
106
                        ariaLabel: defaultLabel,
107
                };
108
        }, [ isButtonPressed, activeAIButtonId, editorMode ] );
109

110
        /**
111
         * Handles the button press state.
112
         * @returns {void}
113
         */
114
        const handlePressedButton = () => {
28✔
115
                // If there is an active marker when the AI fixes button is clicked, remove it.
116
                if ( activeMarker ) {
6✔
117
                        setActiveMarker( null );
2✔
118
                        setMarkerPauseStatus( false );
2✔
119
                        // Remove highlighting from the editor.
120
                        window.YoastSEO.analysis.applyMarks( new Paper( "", {} ), [] );
2✔
121
                }
122

123
                /* If the current pressed button ID is the same as the active AI button id,
124
                we want to set the active AI button to null and enable back the highlighting button that was disabled
125
                when the AI button was pressed the first time. Otherwise, update the active AI button ID. */
126
                if ( aiOptimizeId === activeAIButtonId ) {
6✔
127
                        setActiveAIFixesButton( null );
4✔
128
                        // Enable the highlighting button when the AI button is not pressed.
129
                        setMarkerStatus( "enabled" );
4✔
130
                } else {
131
                        setActiveAIFixesButton( aiOptimizeId );
2✔
132
                        /*
133
                        Disable the highlighting button when the AI button is pressed.
134
                        This is because clicking on the highlighting button will remove the AI suggestion from the editor.
135
                         */
136
                        setMarkerStatus( "disabled" );
2✔
137
                }
138
                // Dismiss the tooltip when the button is pressed.
139
                setButtonClass( "" );
6✔
140
        };
141

142
        const handleClick = useCallback( () => {
28✔
143
                // eslint-disable-next-line no-negated-condition -- Let's handle the happy path first.
144
                if ( ! shouldShowUpsell ) {
6!
145
                        doAction( "yoast.ai.fixAssessments", aiOptimizeId );
6✔
146
                        /* Only handle the pressed button state in Premium.
147
                        We don't want to change the background color of the button and other styling when it's pressed in Free.
148
                        This is because clicking on the button in Free will open an upsell modal, and the button will not be in a pressed state. */
149
                        handlePressedButton();
6✔
150
                } else {
151
                        setIsModalOpenTrue();
×
152
                }
153
        }, [ handlePressedButton, setIsModalOpenTrue ] );
154

155
        // Add tooltip classes on mouse enter and remove them on mouse leave.
156
        const handleMouseEnter = useCallback( () => {
28✔
157
                if ( ariaLabel ) {
×
158
                        const direction = isEnabled ? "yoast-tooltip-w" : "yoast-tooltip-nw";
×
159
                        setButtonClass( `yoast-tooltip yoast-tooltip-multiline ${ direction }` );
×
160
                }
161
        }, [ isEnabled, ariaLabel ] );
162

163
        const handleMouseLeave = useCallback( () => {
28✔
164
                // Remove tooltip classes on mouse leave
165
                setButtonClass( "" );
×
166
        }, [] );
167

168
        return (
28✔
169
                <IconAIFixesButton
170
                        onClick={ handleClick }
171
                        ariaLabel={ ariaLabel }
172
                        onPointerEnter={ handleMouseEnter }
173
                        onPointerLeave={ handleMouseLeave }
174
                        id={ aiOptimizeId }
175
                        className={ `ai-button ${buttonClass}` }
176
                        pressed={ isButtonPressed }
177
                        disabled={ ! isEnabled }
178
                >
179
                        { shouldShowUpsell && <LockClosedIcon className="yst-fixes-button__lock-icon yst-text-amber-900" /> }
17✔
180
                        <SparklesIcon pressed={ isButtonPressed } />
181
                        {
182
                                isModalOpen && <Modal className="yst-introduction-modal" isOpen={ isModalOpen } onClose={ setIsModalOpenFalse } initialFocus={ focusElementRef }>
14!
183
                                        <Modal.Panel className="yst-max-w-lg yst-p-0 yst-rounded-3xl yst-introduction-modal-panel">
184
                                                <ModalContent onClose={ setIsModalOpenFalse } focusElementRef={ focusElementRef } />
185
                                        </Modal.Panel>
186
                                </Modal>
187
                        }
188
                </IconAIFixesButton>
189
        );
190
};
191

192
AIOptimizeButton.propTypes = {
2✔
193
        id: PropTypes.string.isRequired,
194
        isPremium: PropTypes.bool,
195
};
196

197
AIOptimizeButton.defaultProps = {
2✔
198
        isPremium: false,
199
};
200

201
export default AIOptimizeButton;
202

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc