PluginProbe
Parse.ly / 3.24.3
Parse.ly v3.24.3
3.24.3 3.24.2 3.24.1 3.24.0 3.23.7 3.23.6 3.23.5 3.23.4 3.23.3 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.17.0 3.18.0 3.18.1 3.19.0 3.19.1 3.19.2 3.19.3 3.2.0 3.2.1 3.20.0 3.20.1 All 107 releases
wp-parsely / src / content-helper / editor-sidebar / smart-linking / component.tsx

component.tsx in Parse.ly 3.24.3, at src/content-helper/editor-sidebar/smart-linking/component.tsx

767 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WordPress dependencies
3 */
4 import { getBlockContent } from '@wordpress/blocks';
5 // eslint-disable-next-line import/named
6 import { Button, Notice, PanelRow } from '@wordpress/components';
7 import { useDebounce } from '@wordpress/compose';
8 import { select, useDispatch, useSelect } from '@wordpress/data';
9 import { useEffect, useMemo, useRef, useState } from '@wordpress/element';
10 import { __, _n, sprintf } from '@wordpress/i18n';
11 import { Icon, external } from '@wordpress/icons';
12
13 /**
14 * Internal dependencies
15 */
16 import { GutenbergFunction, dispatchCoreEditor } from '../../../@types/gutenberg/types';
17 import { Telemetry } from '../../../js/telemetry/telemetry';
18 import { ContentHelperError, ContentHelperErrorCode } from '../../common/content-helper-error';
19 import { SidebarSettings, SmartLinkingSettings, useSettings } from '../../common/settings';
20 import { generateProtocolVariants } from '../../common/utils/functions';
21 import { ContentHelperPermissions } from '../../common/utils/permissions';
22 import { LinkMonitor } from './component-link-monitor';
23 import { SmartLinkingSettings as SmartLinkingSettingsComponent } from './component-settings';
24 import { useSaveSmartLinksOnPostSave, useSmartLinksValidation } from './hooks';
25 import { SmartLink, SmartLinkingProvider } from './provider';
26 import { SmartLinkingReviewModal } from './review-modal/component-modal';
27 import { ApplyToOptions, SmartLinkingSettingsProps, SmartLinkingStore } from './store';
28 import {
29 calculateSmartLinkingMatches,
30 getAllSmartLinksInPost,
31 getAllSmartLinksURLs,
32 validateAndFixSmartLinksInBlock,
33 validateAndFixSmartLinksInPost,
34 } from './utils';
35
36 /**
37 * Defines the props structure for SmartLinkingPanel.
38 *
39 * @since 3.14.0
40 */
41 type SmartLinkingPanelProps = {
42 className?: string;
43 selectedBlockClientId?: string;
44 context?: SmartLinkingPanelContext;
45 permissions: ContentHelperPermissions;
46 }
47
48 /**
49 * Defines the possible contexts in which the Smart Linking panel can be used.
50 *
51 * @since 3.14.0
52 */
53 export enum SmartLinkingPanelContext {
54 Unknown = 'unknown',
55 ContentHelperSidebar = 'content_helper_sidebar',
56 BlockInspector = 'block_inspector',
57 }
58
59 /**
60 * The maximum number of retries for fetching smart links.
61 *
62 * @since 3.15.0
63 * @since 3.20.0 Renamed from MAX_NUMBER_OF_RETRIES to MAX_FETCH_RETRIES.
64 */
65 const MAX_FETCH_RETRIES = 3;
66
67 /**
68 * The Editor header's save buttons. WordPress 6.5 renamed the wrapper from
69 * `edit-post-header__settings` when it unified the Editor's header.
70 *
71 * @since 3.23.6
72 */
73 const SAVE_BUTTONS_SELECTOR =
74 '.editor-header__settings>[type="button"], .edit-post-header__settings>[type="button"]';
75
76 /**
77 * Smart Linking Panel.
78 *
79 * @since 3.14.0
80 *
81 * @param {Readonly<SmartLinkingPanelProps>} props The component's props.
82 *
83 * @return {import('react').JSX.Element} The JSX Element.
84 */
85 export const SmartLinkingPanel = ( {
86 className,
87 selectedBlockClientId,
88 context = SmartLinkingPanelContext.Unknown,
89 permissions,
90 }: Readonly<SmartLinkingPanelProps> ): React.JSX.Element => {
91 const { settings, setSettings } = useSettings<SidebarSettings>();
92
93 /**
94 * Saving hooks.
95 *
96 * The useSmartLinksValidation hook will validate the smart links before saving the post,
97 * and the useSaveSmartLinksOnPostSave hook will save the smart links when the post is saved,
98 * only after the validation is complete.
99 */
100 const [ validationComplete, setValidationComplete ] = useState<boolean>( false );
101 useSmartLinksValidation( setValidationComplete );
102 useSaveSmartLinksOnPostSave( validationComplete );
103
104 const setSettingsDebounced = useDebounce( setSettings, 500 );
105
106 const [ numAddedLinks, setNumAddedLinks ] = useState<number>( 0 );
107 const [ isReviewDone, setIsReviewDone ] = useState<boolean>( false );
108
109 const { createNotice } = useDispatch( 'core/notices' );
110
111 /**
112 * Loads the Smart Linking store.
113 *
114 * @since 3.14.0
115 */
116 const {
117 ready,
118 loading,
119 reviewModalIsOpen,
120 isFullContent,
121 overlayBlocks,
122 error,
123 suggestedLinks,
124 maxLinks,
125 smartLinkingSettings,
126 applyTo,
127 retrying,
128 retryAttempt,
129 smartLinks,
130 inboundSmartLinks,
131 } = useSelect( ( selectFn ) => {
132 const {
133 isReady,
134 isLoading,
135 isReviewModalOpen,
136 getOverlayBlocks,
137 getSuggestedLinks,
138 getError,
139 isFullContent, // eslint-disable-line @typescript-eslint/no-shadow
140 getMaxLinks,
141 getSmartLinkingSettings,
142 getApplyTo,
143 isRetrying,
144 getRetryAttempt,
145 getSmartLinks,
146 getInboundSmartLinks,
147 } = selectFn( SmartLinkingStore );
148 return {
149 ready: isReady(),
150 loading: isLoading(),
151 reviewModalIsOpen: isReviewModalOpen(),
152 error: getError(),
153 maxLinks: getMaxLinks(),
154 isFullContent: isFullContent(),
155 overlayBlocks: getOverlayBlocks(),
156 suggestedLinks: getSuggestedLinks(),
157 smartLinkingSettings: getSmartLinkingSettings(),
158 applyTo: getApplyTo(),
159 retrying: isRetrying(),
160 retryAttempt: getRetryAttempt(),
161 smartLinks: getSmartLinks(),
162 inboundSmartLinks: getInboundSmartLinks(),
163 };
164 }, [] );
165
166 /**
167 * The filtered list of smart links that have been applied.
168 *
169 * This list is memoized to prevent unnecessary re-renders.
170 *
171 * @since 3.16.0
172 */
173 const appliedLinks = useMemo( () => smartLinks.filter( ( link ) => link.applied ), [ smartLinks ] );
174
175 /**
176 * Loads the Smart Linking store actions.
177 *
178 * @since 3.14.0
179 */
180 const {
181 setIsReady,
182 setLoading,
183 setError,
184 addSmartLinks,
185 addInboundSmartLinks,
186 addOverlayBlock,
187 removeOverlayBlock,
188 setSmartLinkingSettings,
189 setApplyTo,
190 setMaxLinks,
191 setIsRetrying,
192 incrementRetryAttempt,
193 purgeSmartLinksSuggestions,
194 setIsReviewModalOpen,
195 } = useDispatch( SmartLinkingStore );
196
197 const { postId } = useSelect( ( selectFn ) => {
198 const { getCurrentPostId } = selectFn( 'core/editor' ) as GutenbergFunction;
199 return {
200 postId: getCurrentPostId(),
201 };
202 }, [] );
203
204 const initializationRef = useRef( false );
205 /**
206 * Handles the initialization of the Smart Linking existing links by getting the
207 * existing smart links from the post content and the database.
208 *
209 * @since 3.16.0
210 */
211 useEffect( () => {
212 if ( true !== permissions.SmartLinking ) {
213 return;
214 }
215
216 if ( ready ) {
217 // Return early if the Smart Linking store is already initialized.
218 return;
219 }
220
221 if ( initializationRef.current ) {
222 // Return early if the component has already started initialization.
223 return;
224 }
225 initializationRef.current = true;
226
227 // Get the existing smart links from the post content.
228 const existingSmartLinks = getAllSmartLinksInPost();
229
230 // Get the Smart Links from the database and store them in the Smart Linking store.
231 if ( postId ) {
232 SmartLinkingProvider.getInstance().getSmartLinks( postId ).then( async ( savedSmartLinks ) => {
233 let outboundLinks = savedSmartLinks.outbound;
234 const inboundLinks = savedSmartLinks.inbound;
235
236 // Calculate the smart links matches for each block.
237 const blocks = select( 'core/block-editor' ).getBlocks();
238 outboundLinks = calculateSmartLinkingMatches( blocks, outboundLinks );
239
240 // Add the saved smart links to the store.
241 await addInboundSmartLinks( inboundLinks );
242 return addSmartLinks( outboundLinks );
243 } ).then( () => {
244 // Add the existing smart links to the store.
245 return addSmartLinks( existingSmartLinks );
246 } ).then( () => {
247 setIsReady( true );
248 } );
249 } else {
250 // If there is no post ID, just add the existing smart links to the store.
251 addSmartLinks( existingSmartLinks ).then( () => {
252 setIsReady( true );
253 } );
254 }
255 }, [
256 permissions.SmartLinking,
257 ready,
258 postId,
259 addInboundSmartLinks,
260 addSmartLinks,
261 setIsReady,
262 initializationRef,
263 ] );
264
265 /**
266 * Handles the ending of the review process.
267 * Shows a success notice if the review is done and there are added links.
268 *
269 * @since 3.16.0
270 */
271 useEffect( () => {
272 if ( ! isReviewDone ) {
273 setNumAddedLinks( 0 );
274 } else if ( numAddedLinks > 0 ) {
275 createNotice(
276 'success',
277 /* translators: %s: number of smart links applied */
278 sprintf( __( '%s Smart Links successfully applied.', 'wp-parsely' ), numAddedLinks ),
279 {
280 type: 'snackbar',
281 },
282 );
283 }
284 }, [ isReviewDone ] ); // eslint-disable-line react-hooks/exhaustive-deps
285
286 /**
287 * Handles the change of a setting.
288 *
289 * Updates the settings in the Smart Linking store and the Settings Context.
290 *
291 * @since 3.14.0
292 *
293 * @param {keyof SmartLinkingSettingsComponent} setting The setting to change.
294 * @param {string | boolean | number} value The new value of the setting.
295 */
296 const onSettingChange = (
297 setting: keyof SmartLinkingSettings,
298 value: string | boolean | number,
299 ): void => {
300 setSettingsDebounced( {
301 SmartLinking: {
302 ...settings.SmartLinking,
303 [ setting ]: value,
304 },
305 } );
306 if ( setting === 'MaxLinks' ) {
307 setMaxLinks( value as number );
308 }
309 };
310
311 /**
312 * Loads and prepares the Smart Linking settings from the Settings Context,
313 * if they are not already loaded.
314 *
315 * @since 3.14.0
316 */
317 useEffect( () => {
318 // If the smartLinkingSettings are not empty object, return early.
319 if ( Object.keys( smartLinkingSettings ).length > 0 ) {
320 return;
321 }
322
323 // Load the settings from the WordPress database and store them in the Smart Linking store.
324 const newSmartLinkingSettings: SmartLinkingSettingsProps = {
325 maxLinksPerPost: settings.SmartLinking.MaxLinks,
326 };
327 setSmartLinkingSettings( newSmartLinkingSettings );
328 }, [ setSmartLinkingSettings, settings ] ); // eslint-disable-line react-hooks/exhaustive-deps
329
330 /**
331 * Loads the selected block and post content.
332 *
333 * @since 3.14.0
334 */
335 const {
336 allBlocks,
337 selectedBlock,
338 postContent,
339 postPermalink,
340 } = useSelect(
341 ( selectFn ) => {
342 const { getSelectedBlock, getBlock, getBlocks } = selectFn(
343 'core/block-editor',
344 ) as GutenbergFunction;
345 const { getEditedPostContent, getCurrentPostAttribute } = selectFn(
346 'core/editor',
347 ) as GutenbergFunction;
348
349 return {
350 allBlocks: getBlocks(),
351 selectedBlock: selectedBlockClientId ? getBlock( selectedBlockClientId ) : getSelectedBlock(),
352 postContent: getEditedPostContent(),
353 postPermalink: getCurrentPostAttribute( 'link' ),
354 };
355 },
356 [ selectedBlockClientId ],
357 );
358
359 /**
360 * Processes the smart links generated by the Smart Linking provider.
361 *
362 * The processing step are:
363 * - Exclude the links that have been applied already.
364 * - Strip the protocol and trailing slashes from the post permalink.
365 * - Filter out self-referencing links.
366 * - Calculate the smart links matches for each block.
367 * - Update the stored smart links with the new matches.
368 *
369 * @since 3.16.0
370 *
371 * @param {SmartLink[]} links The smart links to process.
372 *
373 * @return {Promise<SmartLink[]>} The processed smart links.
374 */
375 const processSmartLinks = async ( links: SmartLink[] ): Promise<SmartLink[]> => {
376 // Exclude the links that have been applied already.
377 links = links.filter(
378 ( link ) => ! smartLinks.some( ( sl ) => sl.uid === link.uid && sl.applied )
379 );
380
381 // Strip the protocol and trailing slashes from the post permalink.
382 const strippedPermalink = postPermalink
383 .replace( /^https?:\/\//, '' ).replace( /\/+$/, '' );
384
385 // Filter out self-referencing links.
386 links = links.filter( ( link ) => {
387 if ( link.href.raw.includes( strippedPermalink ) ) {
388 // eslint-disable-next-line no-console
389 console.warn( `PCH Smart Linking: Skipping self-reference link: ${ link.href }` );
390 return false;
391 }
392 return true;
393 } );
394
395 // Exclude Smart Links text and URLs that are already in the post, to avoid duplicates.
396 links = links.filter( ( link ) => {
397 return ! smartLinks.some( ( sl ) => {
398 if ( sl.href === link.href ) {
399 // eslint-disable-next-line no-console
400 console.warn( `PCH Smart Linking: Skipping duplicate link: ${ link.href }` );
401 return true;
402 }
403 if ( sl.text === link.text ) {
404 // If the offset is the same, we want to keep the link, so it can replace the old smart link.
405 if ( sl.offset === link.offset ) {
406 // TODO: Flag smart link as updated.
407 return false;
408 }
409 // eslint-disable-next-line no-console
410 console.warn( `PCH Smart Linking: Skipping duplicate link text: ${ link.text }` );
411 return true;
412 }
413 return false;
414 } );
415 } );
416
417 // Depending on the context, we may need to process all blocks or just the selected one.
418 const blocksToProcess = isFullContent ? allBlocks : [ selectedBlock! ];
419
420 // Calculate the smart links matches for each block.
421 links = calculateSmartLinkingMatches( blocksToProcess, links, {} )
422 // Filter out links without a match.
423 .filter( ( link ) => link.match );
424
425 // Filter out links without match and smart links being inserted inside another smart link.
426 links = links.filter( ( link ) => {
427 if ( ! link.match ) {
428 return false;
429 }
430 const linkStart = link.match.blockLinkPosition;
431 const linkEnd = linkStart + link.text.length;
432
433 return ! smartLinks.some( ( sl ) => {
434 if ( ! sl.match ) {
435 return false;
436 }
437
438 if ( link.match!.blockId !== sl.match!.blockId ) {
439 return false;
440 }
441 const slStart = sl.match!.blockLinkPosition;
442 const slEnd = slStart + sl.text.length;
443
444 return ( linkStart >= slStart && linkEnd <= slEnd );
445 } );
446 } );
447
448 // Update the link suggestions with the new matches.
449 await addSmartLinks( links );
450
451 return links;
452 };
453
454 /**
455 * Generates smart links for the selected block or the entire post content.
456 *
457 * @since 3.14.0
458 */
459 const generateSmartLinks = async () => {
460 await setLoading( true );
461 await purgeSmartLinksSuggestions();
462 await setError( null );
463 setIsReviewDone( false );
464
465 Telemetry.trackEvent( 'smart_linking_generate_pressed', {
466 is_full_content: isFullContent,
467 selected_block: selectedBlock?.name ?? 'none',
468 context,
469 } );
470
471 // If selected block is not set, the overlay will be applied to the entire content.
472 await applyOverlay( isFullContent ? 'all' : selectedBlock?.clientId );
473
474 // After 60 * MAX_NUMBER_OR_RETRIES seconds without a response, timeout and remove any overlay.
475 const timeout = setTimeout( () => {
476 setLoading( false );
477 Telemetry.trackEvent( 'smart_linking_generate_timeout', {
478 is_full_content: isFullContent,
479 selected_block: selectedBlock?.name ?? 'none',
480 context,
481 } );
482
483 // If selected block is not set, the overlay will be removed from the entire content.
484 removeOverlay( isFullContent ? 'all' : selectedBlock?.clientId );
485 }, 60000 * MAX_FETCH_RETRIES );
486
487 const previousApplyTo = applyTo;
488 try {
489 const generatedLinks = await generateSmartLinksWithRetry( MAX_FETCH_RETRIES );
490 const processedSmartLinks = await processSmartLinks( generatedLinks );
491
492 // If after processing the smart links there are no links to suggest, show an error message.
493 if ( processedSmartLinks.length === 0 ) {
494 throw new ContentHelperError(
495 __( 'No Smart Links were generated.', 'wp-parsely' ),
496 ContentHelperErrorCode.ParselySuggestionsApiNoData,
497 ''
498 );
499 }
500
501 setIsReviewModalOpen( true );
502 } catch ( e: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
503 const contentHelperError = new ContentHelperError(
504 e.message ?? 'An unknown error has occurred.',
505 e.code ?? ContentHelperErrorCode.UnknownError
506 );
507
508 // Handle the case where the operation was aborted by the user.
509 if ( e.code && e.code === ContentHelperErrorCode.ParselyAborted ) {
510 contentHelperError.message = sprintf(
511 /* translators: 1: number of retry attempts, 2: attempt plural */
512 __( 'The Smart Linking process was cancelled after %1$d %2$s.', 'wp-parsely' ),
513 e.numRetries,
514 _n( 'attempt', 'attempts', e.numRetries, 'wp-parsely' )
515 );
516 }
517
518 console.error( e ); // eslint-disable-line no-console
519 await setError( contentHelperError );
520 contentHelperError.createErrorSnackbar();
521 } finally {
522 await setLoading( false );
523 await setApplyTo( previousApplyTo );
524 await setIsRetrying( false );
525 await removeOverlay( isFullContent ? 'all' : selectedBlock?.clientId );
526 clearTimeout( timeout );
527 }
528 };
529
530 /**
531 * Generates smart links for the selected block or the entire post content,
532 * and retries the fetch if it fails.
533 *
534 * @since 3.15.0
535 *
536 * @param {number} retries The number of retries remaining.
537 *
538 * @return {Promise<SmartLink[]>} The generated smart links.
539 */
540 const generateSmartLinksWithRetry = async ( retries: number ): Promise<SmartLink[]> => {
541 let generatedLinks: SmartLink[] = [];
542 try {
543 const generatingFullContent = isFullContent || ! selectedBlock;
544 await setApplyTo( generatingFullContent ? ApplyToOptions.All : ApplyToOptions.Selected );
545
546 const urlExclusionList = generateProtocolVariants( postPermalink );
547
548 // Get all the existing smart links URLs to exclude them from the new smart links.
549 const existingSmartLinksURLs = getAllSmartLinksURLs( smartLinks );
550
551 urlExclusionList.push( ...existingSmartLinksURLs );
552
553 generatedLinks = await SmartLinkingProvider.getInstance().generateSmartLinks(
554 ( selectedBlock && ! generatingFullContent )
555 ? getBlockContent( selectedBlock )
556 : postContent,
557 maxLinks,
558 urlExclusionList
559 );
560 } catch ( err: any ) { // eslint-disable-line @typescript-eslint/no-explicit-any
561 // If the request was aborted, throw the AbortError to be handled elsewhere.
562 if ( err.code && err.code === ContentHelperErrorCode.ParselyAborted ) {
563 err.numRetries = MAX_FETCH_RETRIES - retries;
564 throw err;
565 }
566
567 // If the error is a retryable fetch error, retry the fetch.
568 if ( retries > 0 && err.retryFetch ) {
569 // Print the error to the console to help with debugging.
570 console.error( err ); // eslint-disable-line no-console
571 await setIsRetrying( true );
572 await incrementRetryAttempt();
573 return await generateSmartLinksWithRetry( retries - 1 );
574 }
575
576 // Throw the error to be handled elsewhere.
577 throw err;
578 }
579
580 return generatedLinks;
581 };
582
583 /**
584 * Applies the overlay to the selected block or the entire post content.
585 *
586 * @since 3.14.0
587 *
588 * @param {string} clientId The client ID of the block to apply the overlay to.\
589 * If set to 'all', the overlay will be applied to the entire post content.
590 */
591 const applyOverlay = async ( clientId: string = 'all' ): Promise<void> => {
592 await addOverlayBlock( clientId );
593 disableSave();
594 };
595
596 /**
597 * Removes the overlay from the selected block or the entire post content.
598 *
599 * @since 3.14.0
600 *
601 * @param {string} clientId The client ID of the block to remove the overlay from.
602 * If set to 'all', the overlay will be removed from the entire post content.
603 */
604 const removeOverlay = async ( clientId: string = 'all' ): Promise<void> => {
605 await removeOverlayBlock( clientId );
606
607 // If there are no more overlay blocks, enable save.
608 if ( overlayBlocks.length === 0 ) {
609 enableSave();
610 }
611 };
612
613 /**
614 * Disables the save button and locks post auto-saving.
615 *
616 * @since 3.14.0
617 */
618 const disableSave = (): void => {
619 // Lock post saving.
620 dispatchCoreEditor.lockPostSaving( 'wp-parsely-block-overlay' );
621
622 // Disable save buttons.
623 const saveButtons = document.querySelectorAll( SAVE_BUTTONS_SELECTOR );
624 saveButtons.forEach( ( button ) => {
625 button.setAttribute( 'disabled', 'disabled' );
626 } );
627 };
628
629 /**
630 * Enables the save button and unlocks post auto-saving.
631 *
632 * @since 3.14.0
633 */
634 const enableSave = (): void => {
635 // Enable save buttons.
636 const saveButtons = document.querySelectorAll( SAVE_BUTTONS_SELECTOR );
637 saveButtons.forEach( ( button ) => {
638 button.removeAttribute( 'disabled' );
639 } );
640
641 // Unlock post saving.
642 dispatchCoreEditor.unlockPostSaving( 'wp-parsely-block-overlay' );
643 };
644
645 /**
646 * Returns the message for the generate button.
647 *
648 * @since 3.15.0
649 *
650 * @return {string} The message for the generate button.
651 */
652 const getGenerateButtonMessage = (): string => {
653 if ( retrying ) {
654 return sprintf(
655 /* translators: %1$d: number of retry attempts, %2$d: maximum number of retries */
656 __( 'Retrying… Attempt %1$d of %2$d', 'wp-parsely' ),
657 retryAttempt,
658 MAX_FETCH_RETRIES
659 );
660 }
661 if ( loading ) {
662 return __( 'Generating Smart Links…', 'wp-parsely' );
663 }
664 return __( 'Add Smart Links', 'wp-parsely' );
665 };
666
667 return (
668 <div className="wp-parsely-smart-linking">
669 <LinkMonitor
670 isDetectingEnabled={ ! reviewModalIsOpen } // Disable link detection when the review modal is open.
671 onLinkRemove={ ( changes ) => {
672 // When a link is removed, validate and fix any smart-link that got the data-smartlink attribute removed.
673 validateAndFixSmartLinksInBlock( changes.block );
674 } }
675 />
676 <PanelRow className={ className }>
677 <div className="smart-linking-text">
678 { __(
679 'Automatically insert links to your most relevant, top performing content.',
680 'wp-parsely',
681 ) }
682 <Button
683 href="https://docs.wpvip.com/parse-ly/wp-parsely-features/smart-linking/"
684 target="_blank"
685 variant="link"
686 >
687 { __( 'Learn more about Smart Linking', 'wp-parsely' ) }
688 <Icon icon={ external } size={ 18 } className="parsely-external-link-icon" />
689 </Button>
690 </div>
691 { error && (
692 <Notice
693 status="info"
694 onRemove={ () => setError( null ) }
695 className="wp-parsely-content-helper-error"
696 >
697 { error.Message() }
698 </Notice>
699 ) }
700 { isReviewDone && numAddedLinks > 0 && (
701 <Notice
702 status="success"
703 onRemove={ () => setIsReviewDone( false ) }
704 className="wp-parsely-smart-linking-suggested-links"
705 >
706 { sprintf(
707 /* translators: %s: number of smart links generated */
708 __( 'Successfully added %s Smart Links.', 'wp-parsely' ),
709 numAddedLinks > 0 ? numAddedLinks : suggestedLinks.length,
710 ) }
711 </Notice>
712 ) }
713 <SmartLinkingSettingsComponent
714 disabled={ loading }
715 selectedBlock={ selectedBlock }
716 onSettingChange={ onSettingChange }
717 />
718 <div className="smart-linking-generate">
719 <Button
720 onClick={ generateSmartLinks }
721 variant="secondary"
722 isBusy={ loading }
723 disabled={ loading }
724 >
725 { getGenerateButtonMessage() }
726 </Button>
727 </div>
728 { ( appliedLinks.length > 0 || inboundSmartLinks.length > 0 ) && (
729 <div className="smart-linking-manage">
730 <Button
731 onClick={ async () => {
732 // Update the smart links in the store.
733 const fixedSmartLinks = await validateAndFixSmartLinksInPost();
734 const existingSmartLinks = getAllSmartLinksInPost();
735 await addSmartLinks( existingSmartLinks );
736 setIsReviewModalOpen( true );
737 Telemetry.trackEvent( 'smart_linking_review_pressed', {
738 num_smart_links: smartLinks.length,
739 has_fixed_links: fixedSmartLinks,
740 context,
741 } );
742 } }
743 variant="secondary"
744 disabled={ loading }
745 >
746 { __( 'Review Smart Links', 'wp-parsely' ) }
747 </Button>
748 </div>
749 ) }
750 </PanelRow>
751
752 { reviewModalIsOpen && (
753 <SmartLinkingReviewModal
754 isOpen={ reviewModalIsOpen }
755 onAppliedLink={ () => {
756 setNumAddedLinks( ( num ) => num + 1 );
757 } }
758 onClose={ () => {
759 setIsReviewDone( true );
760 setIsReviewModalOpen( false );
761 } }
762 />
763 ) }
764 </div>
765 );
766 };
767