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

Yoast / wordpress-seo / 79498a351b2ddb942f3f0f8c97de97707acced3c

27 Feb 2024 03:31PM UTC coverage: 41.466% (-11.7%) from 53.156%
79498a351b2ddb942f3f0f8c97de97707acced3c

Pull #21121

github

web-flow
Merge 434fcab1a into d617035e7
Pull Request #21121: UI-Library / Align button heights with input fields

4568 of 10443 branches covered (43.74%)

Branch coverage included in aggregate %.

0 of 5 new or added lines in 3 files covered. (0.0%)

1 existing line in 1 file now uncovered.

12971 of 31854 relevant lines covered (40.72%)

70210.34 hits per line

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

0.0
/packages/js/src/first-time-configuration/tailwind-components/steps/indexation/indexation.js
1
/* global yoastIndexingData */
2
import { Component, flushSync, Fragment } from "@wordpress/element";
3
import { Transition } from "@headlessui/react";
4
import { __ } from "@wordpress/i18n";
5
import { Button } from "@yoast/ui-library";
6
import PropTypes from "prop-types";
7
import AnimateHeight from "react-animate-height";
8

9
import { addHistoryState, removeSearchParam } from "../../../../helpers/urlHelpers";
10
import IndexingError from "./indexing-error";
11
import Alert from "../../base/alert";
12
import RequestError from "../../../../errors/RequestError";
13
import ParseError from "../../../../errors/ParseError";
14

15
const STATE = {
×
16
        /**
17
         * When the process has not started yet, or has been stopped manually.
18
         */
19
        IDLE: "idle",
20
        /**
21
         * When the indexing process is in progress.
22
         */
23
        IN_PROGRESS: "in_progress",
24
        /**
25
         * When an error has occurred during the indexing process that has stopped the process.
26
         */
27
        ERRORED: "errored",
28
        /**
29
         * When the indexing process has finished.
30
         */
31
        COMPLETED: "completed",
32
};
33

34
/**
35
 * Indexes the site and shows a progress bar indicating the indexing process' progress.
36
 */
37
class Indexation extends Component {
38
        /**
39
         * Indexing constructor.
40
         *
41
         * @param {Object} props The properties.
42
         */
43
        constructor( props ) {
44
                super( props );
×
45

46
                this.settings = yoastIndexingData;
×
47

48
                this.state = {
×
49
                        state: STATE.IDLE,
50
                        processed: 0,
51
                        error: null,
52
                        amount: parseInt( this.settings.amount, 10 ),
53
                        firstTime: (
54
                                this.settings.firstTime === "1"
55
                        ),
56
                };
57

58
                this.startIndexing = this.startIndexing.bind( this );
×
59
                this.stopIndexing = this.stopIndexing.bind( this );
×
60
        }
61

62
        /**
63
         * Does an indexing request.
64
         *
65
         * @param {string} url   The url of the indexing that should be done.
66
         * @param {string} nonce The WordPress nonce value for in the header.
67
         *
68
         * @returns {Promise} The request promise.
69
         */
70
        async doIndexingRequest( url, nonce ) {
71
                const response = await fetch( url, {
×
72
                        method: "POST",
73
                        headers: {
74
                                "X-WP-Nonce": nonce,
75
                        },
76
                } );
77

78
                const responseText = await response.text();
×
79

80
                let data;
81
                try {
×
82
                        /*
83
                         * Sometimes, in case of a fatal error, or if WP_DEBUG is on and a DB query fails,
84
                         * non-JSON is dumped into the HTTP response body, so account for that here.
85
                         */
86
                        data = JSON.parse( responseText );
×
87
                } catch ( error ) {
88
                        throw new ParseError( "Error parsing the response to JSON.", responseText );
×
89
                }
90

91
                // Throw an error when the response's status code is not in the 200-299 range.
92
                if ( ! response.ok ) {
×
93
                        const stackTrace = data.data ? data.data.stackTrace : "";
×
94
                        throw new RequestError( data.message, url, "POST", response.status, stackTrace );
×
95
                }
96

97
                return data;
×
98
        }
99

100
        /**
101
         * Does any registered indexing action *before* a call to an index endpoint.
102
         *
103
         * @param {string} endpoint The endpoint that has been called.
104
         *
105
         * @returns {Promise<void>} An empty promise.
106
         */
107
        async doPreIndexingAction( endpoint ) {
108
                if ( typeof this.props.preIndexingActions[ endpoint ] === "function" ) {
×
109
                        await this.props.preIndexingActions[ endpoint ]( this.settings );
×
110
                }
111
        }
112

113
        /**
114
         * Does any registered indexing action *after* a call to an index endpoint.
115
         *
116
         * @param {string} endpoint The endpoint that has been called.
117
         * @param {Object} response The response of the call to the endpoint.
118
         *
119
         * @returns {Promise<void>} An empty promise.
120
         */
121
        async doPostIndexingAction( endpoint, response ) {
122
                if ( typeof this.props.indexingActions[ endpoint ] === "function" ) {
×
123
                        await this.props.indexingActions[ endpoint ]( response.objects, this.settings );
×
124
                }
125
        }
126

127
        /**
128
         * Does the indexing of a given endpoint.
129
         *
130
         * @param {string} endpoint The endpoint.
131
         *
132
         * @returns {Promise} The indexing promise.
133
         */
134
        async doIndexing( endpoint ) {
135
                let url = this.settings.restApi.root + this.settings.restApi.indexing_endpoints[ endpoint ];
×
136

137
                while ( this.isState( STATE.IN_PROGRESS ) && url !== false ) {
×
138
                        try {
×
139
                                await this.doPreIndexingAction( endpoint );
×
140
                                const response = await this.doIndexingRequest( url, this.settings.restApi.nonce );
×
141
                                await this.doPostIndexingAction( endpoint, response );
×
142

143
                                flushSync( () => {
×
144
                                        this.setState( previousState => (
×
145
                                                {
×
146
                                                        processed: previousState.processed + response.objects.length,
147
                                                        firstTime: false,
148
                                                }
149
                                        ) );
150
                                } );
151

152
                                url = response.next_url;
×
153
                        } catch ( error ) {
154
                                flushSync( () => {
×
155
                                        this.setState( {
×
156
                                                state: STATE.ERRORED,
157
                                                error: error,
158
                                                firstTime: false,
159
                                        } );
160
                                } );
161
                        }
162
                }
163
        }
164

165
        /**
166
         * Indexes the objects by calling each indexing endpoint in turn.
167
         *
168
         * @returns {Promise<void>} The indexing promise.
169
         */
170
        async index() {
171
                for ( const endpoint of Object.keys( this.settings.restApi.indexing_endpoints ) ) {
×
172
                        await this.doIndexing( endpoint );
×
173
                }
174
                /*
175
                 * Set the indexing process as completed only when there is no error
176
                 * and the user has not stopped the process manually.
177
                 */
178
                if ( ! this.isState( STATE.ERRORED ) && ! this.isState( STATE.IDLE ) ) {
×
179
                        this.completeIndexing();
×
180
                }
181
        }
182

183
        /**
184
         * Starts the indexing process.
185
         *
186
         * @returns {Promise<void>} The start indexing promise.
187
         */
188
        async startIndexing() {
189
                /*
190
                 * Since `setState` is asynchronous in nature, we have to supply a callback
191
                 * to make sure the state is correctly set before trying to call the first
192
                 * endpoint.
193
                 */
194
                this.setState( { processed: 0, state: STATE.IN_PROGRESS }, this.index );
×
195
        }
196

197
        /**
198
         * Sets the state of the indexing process to completed.
199
         *
200
         * @returns {void}
201
         */
202
        completeIndexing() {
203
                this.setState( { state: STATE.COMPLETED } );
×
204
        }
205

206
        /**
207
         * Stops the indexing process.
208
         *
209
         * @returns {void}
210
         */
211
        stopIndexing() {
212
                this.setState( previousState => (
×
213
                        {
×
214
                                state: STATE.IDLE,
215
                                processed: 0,
216
                                amount: previousState.amount - previousState.processed,
217
                        }
218
                ) );
219
        }
220

221
        /**
222
         * Start indexation on mount, when redirected from the "Start SEO data optimization" button in the dashboard notification.
223
         *
224
         * @returns {void}
225
         */
226
        componentDidMount() {
227
                if ( this.settings.disabled ) {
×
228
                        return;
×
229
                }
230

231
                this.props.indexingStateCallback( this.state.amount === 0 ? "already_done" : this.state.state );
×
232

233
                const shouldStart = new URLSearchParams( window.location.search ).get( "start-indexation" ) === "true";
×
234

235
                if ( shouldStart ) {
×
236
                        const currentURL = removeSearchParam( window.location.href, "start-indexation" );
×
237
                        addHistoryState( null, document.title, currentURL );
×
238

239
                        this.startIndexing();
×
240
                }
241
        }
242

243
        /**
244
         * Signals state changes to an optional callback function.
245
         *
246
         * @param {Object} _prevProps The previous props, unused in the current implementation.
247
         * @param {Object} prevState  The previous state.
248
         *
249
         * @returns {void}
250
         */
251
        componentDidUpdate( _prevProps, prevState ) {
252
                if ( this.state.state !== prevState.state ) {
×
253
                        this.props.indexingStateCallback( this.state.state );
×
254
                }
255
        }
256

257
        /**
258
         * If the current state of the indexing process is the given state.
259
         *
260
         * @param {STATE.IDLE|STATE.ERRORED|STATE.IN_PROGRESS|STATE.COMPLETED} state The state value to check against.
261
         *
262
         * @returns {boolean} If the current state of the indexing process is the given state.
263
         */
264
        isState( state ) {
265
                return this.state.state === state;
×
266
        }
267

268
        /**
269
         * Renders a notice if it is the first time the indexation is performed.
270
         *
271
         * @returns {JSX.Element} The rendered component.
272
         */
273
        renderFirstIndexationNotice() {
274
                return (
×
275
                        <Alert type={ "info" } className="yst-mt-6">
276
                                { __( "This feature includes and replaces the Text Link Counter and Internal Linking Analysis", "wordpress-seo" ) }
277
                        </Alert>
278
                );
279
        }
280

281
        /**
282
         * Renders the start button.
283
         *
284
         * @returns {JSX.Element|null} The start button.
285
         */
286
        renderStartButton() {
NEW
287
                return <Button
×
288
                        variant="secondary"
289
                        onClick={ this.startIndexing }
290
                        id="indexation-data-optimization"
291
                        data-hiive-event-name="clicked_start_data_optimization"
292
                >
293
                        { __( "Start SEO data optimization", "wordpress-seo" ) }
294
                </Button>;
295
        }
296

297
        /**
298
         * Renders the stop button.
299
         *
300
         * @returns {JSX.Element|null} The stop button.
301
         */
302
        renderStopButton() {
NEW
303
                return <Button
×
304
                        variant="secondary"
305
                        onClick={ this.stopIndexing }
306
                >
307
                        { __( "Stop SEO data optimization", "wordpress-seo" ) }
308
                </Button>;
309
        }
310

311
        /**
312
         * Renders the disabled tool.
313
         *
314
         * @returns {JSX.Element} The disabled tool.
315
         */
316
        renderDisabledTool() {
317
                return <Fragment>
×
318
                        <p>
319
                                <Button
320
                                        variant="secondary"
321
                                        disabled={ true }
322
                                        id="indexation-data-optimization"
323
                                >
324
                                        { __( "Start SEO data optimization", "wordpress-seo" ) }
325
                                </Button>
326
                        </p>
327
                        <Alert type={ "info" } className="yst-mt-6">
328
                                { __( "SEO data optimization is disabled for non-production environments.", "wordpress-seo" ) }
329
                        </Alert>
330
                </Fragment>;
331
        }
332

333
        /**
334
         * Renders the progress bar.
335
         *
336
         * @returns {WPElement} The progress bar.
337
         */
338
        renderProgressBar() {
339
                let percentageIndexed = 0;
×
340
                if ( this.isState( STATE.COMPLETED ) ) {
×
341
                        percentageIndexed = 100;
×
342
                }
343
                if ( this.isState( STATE.IN_PROGRESS ) ) {
×
344
                        percentageIndexed = ( this.state.processed / parseInt( this.state.amount, 10 ) ) * 100;
×
345
                }
346

347
                return <div className="yst-w-full yst-bg-slate-200 yst-rounded-full yst-h-2.5 yst-mb-4">
×
348
                        <div
349
                                className="yst-transition-[width] yst-ease-linear yst-bg-primary-500 yst-h-2.5 yst-rounded-full"
350
                                style={ { width: `${ percentageIndexed }%` } }
351
                        />
352
                </div>;
353
        }
354

355
        /**
356
         * Renders the italics caption.
357
         *
358
         * @returns {WPElement} the italics caption.
359
         */
360
        renderCaption() {
361
                return <AnimateHeight
×
362
                        id="optimization-in-progress-text"
363
                        height={ this.isState( STATE.IN_PROGRESS ) ? "auto" : 0 }
×
364
                        easing="linear"
365
                        duration={ 300 }
366
                >
367
                        <p className={ "yst-text-sm yst-italic yst-mb-4 yst-mt-4" }>
368
                                {
369
                                        __( "SEO data optimization is running… You can safely move on to the next steps of this configuration.",
370
                                                "wordpress-seo" )
371
                                }
372
                        </p>
373
                </AnimateHeight>;
374
        }
375

376
        /**
377
         * Renders the error alert.
378
         *
379
         * @returns {JSX.Element} The error alert.
380
         */
381
        renderErrorAlert() {
382
                return <IndexingError
×
383
                        message={ yoastIndexingData.errorMessage }
384
                        error={ this.state.error }
385
                        className={ "yst-mb-4" }
386
                />;
387
        }
388

389
        /* eslint-disable complexity */
390
        /**
391
         * Renders the component
392
         *
393
         * @returns {WPElement} The rendered component.
394
         */
395
         render() {
396
                if ( this.settings.disabled ) {
×
397
                        return this.renderDisabledTool();
×
398
                }
399

400
                return (
×
401
                        <div className="yst-relative">
402
                                { this.props.children }
403
                                <Transition
404
                                        unmount={ false }
405
                                        show={ this.isState( STATE.ERRORED ) ||
×
406
                                                this.isState( STATE.IN_PROGRESS ) ||
407
                                                ( this.isState( STATE.IDLE ) && this.state.amount > 0 ) }
408
                                        leave="yst-transition-opacity yst-duration-1000"
409
                                        leaveFrom="yst-opacity-100"
410
                                        leaveTo="yst-opacity-0"
411
                                >
412
                                        { this.renderProgressBar() }
413
                                        { this.isState( STATE.ERRORED ) && this.renderErrorAlert() }
×
414
                                        { this.isState( STATE.IN_PROGRESS )
×
415
                                                ? this.renderStopButton()
416
                                                : this.renderStartButton()
417
                                        }
418
                                        { this.renderCaption() }
419
                                        { this.isState( STATE.IDLE ) && this.state.firstTime && this.renderFirstIndexationNotice() }
×
420
                                </Transition>
421
                        </div>
422
                );
423
        }
424
}
425
Indexation.propTypes = {
×
426
        indexingActions: PropTypes.object,
427
        preIndexingActions: PropTypes.object,
428
        indexingStateCallback: PropTypes.func,
429
        children: PropTypes.node,
430
};
431

432
Indexation.defaultProps = {
×
433
        indexingActions: {},
434
        preIndexingActions: {},
435
        indexingStateCallback: () => {},
436
        children: null,
437
};
438

439
export default Indexation;
440
/* eslint-enable complexity */
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