From c98bbfd5b12c3c4f6d6fdb25d240917c0fa6d89c Mon Sep 17 00:00:00 2001 From: Bogdan Ungureanu Date: Fri, 20 Dec 2024 19:45:00 +0200 Subject: [PATCH 1/8] Fix/rdv regressions on treatment (#40690) * RDV: Fix regressions for Duplicate views * RDV: Fix regressions for Duplicate views * Add docblock. * Select Calypso Stats menu for post stats and fix the upsell upgrade nudge styling. * Linting --- .../fix-rdv-regressions-on-treatment | 4 ++ .../src/class-jetpack-mu-wpcom.php | 2 +- .../wpcom-admin-bar/wpcom-admin-bar.php | 6 +- .../wpcom-admin-interface.php | 63 ++++++++++++++++++- .../wpcom-sidebar-notice.php | 6 +- 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/fix-rdv-regressions-on-treatment diff --git a/projects/packages/jetpack-mu-wpcom/changelog/fix-rdv-regressions-on-treatment b/projects/packages/jetpack-mu-wpcom/changelog/fix-rdv-regressions-on-treatment new file mode 100644 index 0000000000000..5ce69e6f92aaf --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/fix-rdv-regressions-on-treatment @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed several regressions for Stats, Blaze and notices for RDV experiment diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index 89597cf7ce895..22b6eec4e9fc2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -452,7 +452,7 @@ public static function load_verbum_comments_admin() { * Load Odyssey Stats in Simple sites. */ public static function load_wpcom_simple_odyssey_stats() { - if ( get_option( 'wpcom_admin_interface' ) === 'wp-admin' ) { + if ( get_option( 'wpcom_admin_interface' ) === 'wp-admin' || wpcom_is_duplicate_views_experiment_enabled() ) { require_once __DIR__ . '/features/wpcom-simple-odyssey-stats/wpcom-simple-odyssey-stats.php'; } } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-bar/wpcom-admin-bar.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-bar/wpcom-admin-bar.php index 575a7e3218f49..a8c7fdf75b1b8 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-bar/wpcom-admin-bar.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-bar/wpcom-admin-bar.php @@ -263,7 +263,11 @@ function wpcom_replace_edit_profile_menu_to_me( $wp_admin_bar ) { * @return string Name of the admin bar class. */ function wpcom_custom_wpcom_admin_bar_class( $wp_admin_bar_class ) { - if ( get_option( 'wpcom_admin_interface' ) === 'wp-admin' ) { + remove_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option' ); + $is_wp_admin = get_option( 'wpcom_admin_interface' ) === 'wp-admin'; + add_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option', 10 ); + + if ( $is_wp_admin ) { return $wp_admin_bar_class; } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-interface/wpcom-admin-interface.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-interface/wpcom-admin-interface.php index 91e2394b86a7b..dc66f5af8145c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-interface/wpcom-admin-interface.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-admin-interface/wpcom-admin-interface.php @@ -8,6 +8,7 @@ use Automattic\Jetpack\Connection\Client; use Automattic\Jetpack\Connection\Manager as Jetpack_Connection; use Automattic\Jetpack\Jetpack_Mu_Wpcom; +use Automattic\Jetpack\Status; use Automattic\Jetpack\Status\Host; /** @@ -118,8 +119,6 @@ function ( $location ) { const WPCOM_DUPLICATED_VIEW = array( 'edit.php', - 'admin.php?page=stats', - 'tools.php?page=advertising', 'edit.php?post_type=jetpack-portfolio', 'edit.php?post_type=jetpack-testimonial', 'edit-comments.php', @@ -548,3 +547,63 @@ function wpcom_dismiss_removed_calypso_screen_notice() { wp_die(); } add_action( 'wp_ajax_wpcom_dismiss_removed_calypso_screen_notice', 'wpcom_dismiss_removed_calypso_screen_notice' ); + +/** + * Enable the Blaze dashboard (WP-Admin) for users that have the RDV experiment enabled. + * + * @param bool $activation_status The activation status - use WP-Admin or Calypso. + * @return mixed|true + */ +function wpcom_enable_blaze_dashboard_for_experiment( $activation_status ) { + if ( ! wpcom_is_duplicate_views_experiment_enabled() ) { + return $activation_status; + } + + return true; +} + +add_filter( 'jetpack_blaze_dashboard_enable', 'wpcom_enable_blaze_dashboard_for_experiment' ); + +/** + * Make the Jetpack Stats page to point to the Calypso Stats Admin menu - temporary. This is needed because WP-Admin pages are rolled-out individually. + * + * This should be removed when the sites are fully untangled (or with the Jetpack Stats). + * + * This is enabled only for the stats page for users that are part of the remove duplicate views experiment. + * + * @param string $file The parent_file of the page. + * + * @return mixed + */ +function wpcom_select_calypso_admin_menu_stats_for_jetpack_post_stats( $file ) { + global $_wp_real_parent_file, $pagenow; + + $is_on_stats_page = 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'stats' === $_GET['page']; + + if ( ! $is_on_stats_page || ! wpcom_is_duplicate_views_experiment_enabled() ) { + return $file; + } + + remove_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option' ); + $is_using_wp_admin = get_option( 'wpcom_admin_interface' ) === 'wp-admin'; + if ( function_exists( 'wpcom_admin_interface_pre_get_option' ) ) { + add_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option' ); + } + + if ( $is_using_wp_admin ) { + return $file; + } + + if ( ! wpcom_get_custom_admin_menu_class() ) { + return $file; + } + + /** + * Not ideal... We shouldn't be doing this. + */ + $_wp_real_parent_file['jetpack'] = 'https://wordpress.com/stats/day/' . ( new Status() )->get_site_suffix(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + return $file; +} + +add_filter( 'parent_file', 'wpcom_select_calypso_admin_menu_stats_for_jetpack_post_stats' ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-sidebar-notice/wpcom-sidebar-notice.php b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-sidebar-notice/wpcom-sidebar-notice.php index 90b1dd2898ff6..ada1b6fc1383d 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/wpcom-sidebar-notice/wpcom-sidebar-notice.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/wpcom-sidebar-notice/wpcom-sidebar-notice.php @@ -10,7 +10,11 @@ use Automattic\Jetpack\Connection\Manager as Connection_Manager; use Automattic\Jetpack\Jetpack_Mu_Wpcom; -if ( get_option( 'wpcom_admin_interface' ) !== 'wp-admin' ) { +remove_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option' ); +$is_wp_admin = get_option( 'wpcom_admin_interface' ) === 'wp-admin'; +add_filter( 'pre_option_wpcom_admin_interface', 'wpcom_admin_interface_pre_get_option', 10 ); + +if ( ! $is_wp_admin ) { return; } From 80a5b844a7cf217f9c29c4c5e0f87dc80a06f690 Mon Sep 17 00:00:00 2001 From: Bogdan Ungureanu Date: Fri, 20 Dec 2024 20:46:49 +0200 Subject: [PATCH 2/8] RDV: Add a function_exists guard that prevents fatals (#40708) --- .../jetpack-mu-wpcom/changelog/fix-php-fatal-in-wpcom | 4 ++++ .../packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/fix-php-fatal-in-wpcom diff --git a/projects/packages/jetpack-mu-wpcom/changelog/fix-php-fatal-in-wpcom b/projects/packages/jetpack-mu-wpcom/changelog/fix-php-fatal-in-wpcom new file mode 100644 index 0000000000000..e460fda027a8f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/fix-php-fatal-in-wpcom @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Add a function_exists guard for wpcom_is_duplicate_views_experiment_enabled function diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index 22b6eec4e9fc2..5bbc4ad0e9d6c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -452,7 +452,7 @@ public static function load_verbum_comments_admin() { * Load Odyssey Stats in Simple sites. */ public static function load_wpcom_simple_odyssey_stats() { - if ( get_option( 'wpcom_admin_interface' ) === 'wp-admin' || wpcom_is_duplicate_views_experiment_enabled() ) { + if ( get_option( 'wpcom_admin_interface' ) === 'wp-admin' || ( function_exists( 'wpcom_is_duplicate_views_experiment_enabled' ) && wpcom_is_duplicate_views_experiment_enabled() ) ) { require_once __DIR__ . '/features/wpcom-simple-odyssey-stats/wpcom-simple-odyssey-stats.php'; } } From fc13aac50b82e22f2434aee2b4cd6227ae939aec Mon Sep 17 00:00:00 2001 From: Mike Watson Date: Fri, 20 Dec 2024 15:34:24 -0500 Subject: [PATCH 3/8] Jetpack AI: Add thumbs up/down component to AI logo generator (#40610) * Jetpack AI: Add thumbs up/down component to AI logo generator * changelog * Attemp #1 to fix some build errors * changelog * add base-styles to jetpack ai client * move AiFeedbackThumbs to ai client * avoid multiple events on same rating * store rating with other logo information * fix issue with persisting ratings with modal open * add mediaLibraryId, prompt and revisedPrompt to event --------- Co-authored-by: Douglas --- pnpm-lock.yaml | 3 + .../change-jetpack-ai-rate-logo-generator | 4 + projects/js-packages/ai-client/package.json | 1 + .../src/components/ai-feedback/index.tsx | 133 ++++++++++++++++++ .../src}/components/ai-feedback/style.scss | 2 +- .../ai-client/src/components/index.ts | 1 + .../components/logo-presenter.tsx | 41 ++++++ .../hooks/use-logo-generator.ts | 4 + .../src/logo-generator/lib/logo-storage.ts | 26 ++-- .../src/logo-generator/store/types.ts | 2 + .../ai-client/src/logo-generator/types.ts | 1 + .../change-jetpack-ai-rate-logo-generator | 4 + .../lib/utils/get-feature-availability.ts | 1 + .../components/ai-feedback/index.tsx | 72 ---------- .../ai-image/components/carrousel.tsx | 17 ++- .../components/ai-image/hooks/use-ai-image.ts | 6 +- 16 files changed, 232 insertions(+), 86 deletions(-) create mode 100644 projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator create mode 100644 projects/js-packages/ai-client/src/components/ai-feedback/index.tsx rename projects/{plugins/jetpack/extensions/plugins/ai-assistant-plugin => js-packages/ai-client/src}/components/ai-feedback/style.scss (70%) create mode 100644 projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator delete mode 100644 projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 480cdf0529f9e..2c0398584de7a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,6 +138,9 @@ importers: '@wordpress/api-fetch': specifier: 7.14.0 version: 7.14.0 + '@wordpress/base-styles': + specifier: 5.14.0 + version: 5.14.0 '@wordpress/blob': specifier: 4.14.0 version: 4.14.0 diff --git a/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator b/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator new file mode 100644 index 0000000000000..7050a2b22fc3a --- /dev/null +++ b/projects/js-packages/ai-client/changelog/change-jetpack-ai-rate-logo-generator @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Jetpack AI: Add thumbs up/down component to AI logo generator diff --git a/projects/js-packages/ai-client/package.json b/projects/js-packages/ai-client/package.json index f101983f9841d..30c9e9be2f2bf 100644 --- a/projects/js-packages/ai-client/package.json +++ b/projects/js-packages/ai-client/package.json @@ -52,6 +52,7 @@ "@types/react": "18.3.12", "@types/wordpress__block-editor": "11.5.15", "@wordpress/api-fetch": "7.14.0", + "@wordpress/base-styles": "5.14.0", "@wordpress/blob": "4.14.0", "@wordpress/block-editor": "14.9.0", "@wordpress/components": "29.0.0", diff --git a/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx b/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx new file mode 100644 index 0000000000000..a6f6ed67ebbf1 --- /dev/null +++ b/projects/js-packages/ai-client/src/components/ai-feedback/index.tsx @@ -0,0 +1,133 @@ +/** + * External dependencies + */ +import { + useAnalytics, + getJetpackExtensionAvailability, +} from '@automattic/jetpack-shared-extension-utils'; +import { Button, Tooltip } from '@wordpress/components'; +import { useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { thumbsUp, thumbsDown } from '@wordpress/icons'; +import clsx from 'clsx'; +/* + * Internal dependencies + */ +import './style.scss'; +/** + * Types + */ +import type React from 'react'; + +type AiFeedbackThumbsProps = { + disabled?: boolean; + iconSize?: number; + ratedItem?: string; + feature?: string; + savedRatings?: Record< string, string >; + options?: { + mediaLibraryId?: number; + prompt?: string; + revisedPrompt?: string; + }; + onRate?: ( rating: string ) => void; +}; + +/** + * Get the availability of a feature. + * + * @param {string} feature - The feature to check availability for. + * @return {boolean} - Whether the feature is available. + */ +function getFeatureAvailability( feature: string ): boolean { + return getJetpackExtensionAvailability( feature ).available === true; +} + +/** + * AiFeedbackThumbs component. + * + * @param {AiFeedbackThumbsProps} props - component props. + * @return {React.ReactElement} - rendered component. + */ +export default function AiFeedbackThumbs( { + disabled = false, + iconSize = 24, + ratedItem = '', + feature = '', + savedRatings = {}, + options = {}, + onRate, +}: AiFeedbackThumbsProps ): React.ReactElement { + if ( ! getFeatureAvailability( 'ai-response-feedback' ) ) { + return null; + } + + const [ itemsRated, setItemsRated ] = useState( {} ); + const { tracks } = useAnalytics(); + + useEffect( () => { + const newItemsRated = { ...savedRatings, ...itemsRated }; + + if ( JSON.stringify( newItemsRated ) !== JSON.stringify( itemsRated ) ) { + setItemsRated( newItemsRated ); + } + }, [ savedRatings ] ); + + const checkThumb = ( thumbValue: string ) => { + if ( ! itemsRated[ ratedItem ] ) { + return false; + } + + return itemsRated[ ratedItem ] === thumbValue; + }; + + const rateAI = ( isThumbsUp: boolean ) => { + const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down'; + + if ( ! checkThumb( aiRating ) ) { + setItemsRated( { + ...itemsRated, + [ ratedItem ]: aiRating, + } ); + + onRate?.( aiRating ); + + tracks.recordEvent( 'jetpack_ai_feedback', { + type: feature, + rating: aiRating, + mediaLibraryId: options.mediaLibraryId || null, + prompt: options.prompt || null, + revisedPrompt: options.revisedPrompt || null, + } ); + } + }; + + return ( +
+ +
+ ); +} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss b/projects/js-packages/ai-client/src/components/ai-feedback/style.scss similarity index 70% rename from projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss rename to projects/js-packages/ai-client/src/components/ai-feedback/style.scss index 9697d17aff1ad..89b8f9f2053bc 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/style.scss +++ b/projects/js-packages/ai-client/src/components/ai-feedback/style.scss @@ -11,6 +11,6 @@ } &__thumb-selected { - color: var(--wp-components-color-accent,var(--wp-admin-theme-color,#3858e9)); + color: var( --wp-components-color-accent, var( --wp-admin-theme-color, #3858e9 ) ); } } diff --git a/projects/js-packages/ai-client/src/components/index.ts b/projects/js-packages/ai-client/src/components/index.ts index 20d5714db6baf..1ef8b40b80426 100644 --- a/projects/js-packages/ai-client/src/components/index.ts +++ b/projects/js-packages/ai-client/src/components/index.ts @@ -1,4 +1,5 @@ export { AIControl, BlockAIControl, ExtensionAIControl } from './ai-control/index.js'; +export { default as AiFeedbackThumbs } from './ai-feedback/index.js'; export { default as AiStatusIndicator } from './ai-status-indicator/index.js'; export { default as AudioDurationDisplay } from './audio-duration-display/index.js'; export { default as AiModalFooter } from './ai-modal-footer/index.js'; diff --git a/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx b/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx index e539e1d751b6a..2b7e63ff9c9e6 100644 --- a/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx +++ b/projects/js-packages/ai-client/src/logo-generator/components/logo-presenter.tsx @@ -9,6 +9,7 @@ import debugFactory from 'debug'; /** * Internal dependencies */ +import AiFeedbackThumbs from '../../components/ai-feedback/index.js'; import CheckIcon from '../assets/icons/check.js'; import LogoIcon from '../assets/icons/logo.js'; import MediaIcon from '../assets/icons/media.js'; @@ -152,11 +153,50 @@ const LogoEmpty: React.FC = () => { ); }; +const RateLogo: React.FC< { + disabled: boolean; + ratedItem: string; + onRate: ( rating: string ) => void; +} > = ( { disabled, ratedItem, onRate } ) => { + const { logos, selectedLogo } = useLogoGenerator(); + const savedRatings = logos + .filter( logo => logo.rating ) + .reduce( ( acc, logo ) => { + acc[ logo.url ] = logo.rating; + return acc; + }, {} ); + + return ( + + ); +}; + const LogoReady: React.FC< { siteId: string; logo: Logo; onApplyLogo: ( mediaId: number ) => void; } > = ( { siteId, logo, onApplyLogo } ) => { + const handleRateLogo = ( rating: string ) => { + // Update localStorage + updateLogo( { + siteId, + url: logo.url, + newUrl: logo.url, + mediaId: logo.mediaId, + rating, + } ); + }; + return ( <> + diff --git a/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts b/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts index 70acbf3fe98e0..d2d232da900d0 100644 --- a/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts +++ b/projects/js-packages/ai-client/src/logo-generator/hooks/use-logo-generator.ts @@ -412,10 +412,13 @@ User request:${ prompt }`; throw error; } + const revisedPrompt = image.data[ 0 ].revised_prompt || null; + // response_format=url returns object with url, otherwise b64_json const logo: Logo = { url: 'data:image/png;base64,' + image.data[ 0 ].b64_json, description: prompt, + revisedPrompt, }; try { @@ -424,6 +427,7 @@ User request:${ prompt }`; url: savedLogo.mediaURL, description: prompt, mediaId: savedLogo.mediaId, + revisedPrompt, } ); } catch ( error ) { storeLogo( logo ); diff --git a/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts b/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts index 81e9b08319575..e6c6bb1176e83 100644 --- a/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts +++ b/projects/js-packages/ai-client/src/logo-generator/lib/logo-storage.ts @@ -10,21 +10,28 @@ const MAX_LOGOS = 10; /** * Add an entry to the site's logo history. * - * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage - * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID - * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo - * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it - * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend - * + * @param {SaveToStorageProps} saveToStorageProps - The properties to save to storage + * @param {SaveToStorageProps.siteId} saveToStorageProps.siteId - The site ID + * @param {SaveToStorageProps.url} saveToStorageProps.url - The URL of the logo + * @param {SaveToStorageProps.description} saveToStorageProps.description - The description of the logo, based on the prompt used to generate it + * @param {SaveToStorageProps.mediaId} saveToStorageProps.mediaId - The media ID of the logo on the backend + * @param {SaveToStorageProps.revisedPrompt} saveToStorageProps.revisedPrompt - The revised prompt of the logo * @return {Logo} The logo that was saved */ -export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageProps ) { +export function stashLogo( { + siteId, + url, + description, + mediaId, + revisedPrompt, +}: SaveToStorageProps ) { const storedContent = getSiteLogoHistory( siteId ); const logo: Logo = { url, description, mediaId, + revisedPrompt, }; storedContent.push( logo ); @@ -45,9 +52,10 @@ export function stashLogo( { siteId, url, description, mediaId }: SaveToStorageP * @param {UpdateInStorageProps.url} updateInStorageProps.url - The URL of the logo to update * @param {UpdateInStorageProps.newUrl} updateInStorageProps.newUrl - The new URL of the logo * @param {UpdateInStorageProps.mediaId} updateInStorageProps.mediaId - The new media ID of the logo + * @param {UpdateInStorageProps.rating} updateInStorageProps.rating - The new rating of the logo * @return {Logo} The logo that was updated */ -export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStorageProps ) { +export function updateLogo( { siteId, url, newUrl, mediaId, rating }: UpdateInStorageProps ) { const storedContent = getSiteLogoHistory( siteId ); const index = storedContent.findIndex( logo => logo.url === url ); @@ -55,6 +63,7 @@ export function updateLogo( { siteId, url, newUrl, mediaId }: UpdateInStoragePro if ( index > -1 ) { storedContent[ index ].url = newUrl; storedContent[ index ].mediaId = mediaId; + storedContent[ index ].rating = rating; } localStorage.setItem( @@ -96,6 +105,7 @@ export function getSiteLogoHistory( siteId: string ) { url: logo.url, description: logo.description, mediaId: logo.mediaId, + rating: logo.rating, } ) ); return storedContent; diff --git a/projects/js-packages/ai-client/src/logo-generator/store/types.ts b/projects/js-packages/ai-client/src/logo-generator/store/types.ts index b34f3b9f8a22e..d8c0f0fbcb1bf 100644 --- a/projects/js-packages/ai-client/src/logo-generator/store/types.ts +++ b/projects/js-packages/ai-client/src/logo-generator/store/types.ts @@ -140,6 +140,8 @@ export type Logo = { url: string; description: string; mediaId?: number; + rating?: string; + revisedPrompt?: string; }; export type RequestError = string | Error | null; diff --git a/projects/js-packages/ai-client/src/logo-generator/types.ts b/projects/js-packages/ai-client/src/logo-generator/types.ts index e54cf774ac98e..2a617a2b97d58 100644 --- a/projects/js-packages/ai-client/src/logo-generator/types.ts +++ b/projects/js-packages/ai-client/src/logo-generator/types.ts @@ -92,6 +92,7 @@ export type UpdateInStorageProps = { url: Logo[ 'url' ]; newUrl: Logo[ 'url' ]; mediaId: Logo[ 'mediaId' ]; + rating?: Logo[ 'rating' ]; }; export type RemoveFromStorageProps = { diff --git a/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator b/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator new file mode 100644 index 0000000000000..931f46e447031 --- /dev/null +++ b/projects/plugins/jetpack/changelog/change-jetpack-ai-rate-logo-generator @@ -0,0 +1,4 @@ +Significance: patch +Type: other + +Jetpack AI: Add thumbs up/down component to AI logo generator diff --git a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts index b18ea2a171d07..f6be56c958493 100644 --- a/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts +++ b/projects/plugins/jetpack/extensions/blocks/ai-assistant/lib/utils/get-feature-availability.ts @@ -3,6 +3,7 @@ */ import { getJetpackExtensionAvailability } from '@automattic/jetpack-shared-extension-utils'; +// TODO: Move to the AI Client js-package export function getFeatureAvailability( feature: string ): boolean { return getJetpackExtensionAvailability( feature ).available === true; } diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx deleted file mode 100644 index fb99d2cb0fd0d..0000000000000 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-feedback/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useAnalytics } from '@automattic/jetpack-shared-extension-utils'; -import { Button, Tooltip } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { thumbsUp, thumbsDown } from '@wordpress/icons'; -import clsx from 'clsx'; -import { useState } from 'react'; -import { getFeatureAvailability } from '../../../../blocks/ai-assistant/lib/utils/get-feature-availability'; - -import './style.scss'; - -export default function AiFeedbackThumbs( { - disabled = false, - iconSize = 24, - ratedItem, - feature, -} ) { - const [ itemsRated, setItemsRated ] = useState( {} ); - const { tracks } = useAnalytics(); - - const rateAI = ( isThumbsUp: boolean ) => { - const aiRating = isThumbsUp ? 'thumbs-up' : 'thumbs-down'; - - setItemsRated( { - ...itemsRated, - [ ratedItem ]: aiRating, - } ); - - tracks.recordEvent( 'jetpack_ai_feedback', { - type: feature, - rating: aiRating, - } ); - }; - - const checkThumb = ( thumbValue: string ) => { - if ( ! itemsRated[ ratedItem ] ) { - return false; - } - - return itemsRated[ ratedItem ] === thumbValue; - }; - - return getFeatureAvailability( 'ai-response-feedback' ) ? ( -
- -
- ) : ( - <> - ); -} diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx index 97fc5acec4950..f9dec07e3cb2b 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/components/carrousel.tsx @@ -1,6 +1,7 @@ /** * External dependencies */ +import { AiFeedbackThumbs } from '@automattic/jetpack-ai-client'; import { Spinner } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Icon, chevronLeft, chevronRight } from '@wordpress/icons'; @@ -8,13 +9,14 @@ import clsx from 'clsx'; /** * Internal dependencies */ -import AiFeedbackThumbs from '../../ai-feedback'; import AiIcon from '../../ai-icon'; import './carrousel.scss'; export type CarrouselImageData = { image?: string; libraryId?: number | string; + prompt?: string; + revisedPrompt?: string; libraryUrl?: string; generating?: boolean; error?: { @@ -108,7 +110,7 @@ export default function Carrousel( {
{ images.length > 1 && prevButton } - { images.map( ( { image, generating, error }, index ) => ( + { images.map( ( { image, generating, error, revisedPrompt }, index ) => (
) : ( - + { ) } ) } @@ -171,6 +177,11 @@ export default function Carrousel( { disabled={ aiFeedbackDisabled( images[ current ] ) } ratedItem={ images[ current ].libraryUrl || '' } iconSize={ 20 } + options={ { + mediaLibraryId: Number( images[ current ].libraryId ), + prompt: images[ current ].prompt, + revisedPrompt: images[ current ].revisedPrompt, + } } feature="image-generator" />
diff --git a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts index b8897913d99a4..bd2fcdd96695a 100644 --- a/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts +++ b/projects/plugins/jetpack/extensions/plugins/ai-assistant-plugin/components/ai-image/hooks/use-ai-image.ts @@ -167,7 +167,9 @@ export default function useAiImage( { .then( result => { if ( result.data.length > 0 ) { const image = 'data:image/png;base64,' + result.data[ 0 ].b64_json; - updateImages( { image }, pointer.current ); + const prompt = userPrompt || null; + const revisedPrompt = result.data[ 0 ].revised_prompt || null; + updateImages( { image, prompt, revisedPrompt }, pointer.current ); updateRequestsCount(); saveToMediaLibrary( image, name ) .then( savedImage => { @@ -181,7 +183,7 @@ export default function useAiImage( { image, libraryId: savedImage?.id, libraryUrl: savedImage?.url, - revisedPrompt: result.data[ 0 ].revised_prompt || '', + revisedPrompt, } ); } ) .catch( () => { From e564fe0d3a5d24651d6dc6c059678712955b5ebc Mon Sep 17 00:00:00 2001 From: Peter Petrov Date: Fri, 20 Dec 2024 23:36:25 +0200 Subject: [PATCH 4/8] Boost: Fix ISA showing an error if no report was found (#40660) * Fix boost API client not properly parsing errors returned by wpcom * Fix showing error when a report wasn't found User might not have requested an analysis yet. That doesn't mean the UI should show an error. This restores behavior from pre-Boost 3.6.0. * Add changelogs * Revert boost core changes * remove changelog * Revert "Revert boost core changes" This reverts commit afa7179a61bdc347cb203233f9f4f33778ce2a29. * Revert "remove changelog" This reverts commit 870e13d0c0fd09966e3f36d1834bb9b5d67c6cff. * Clarify comment --------- Co-authored-by: Adnan Haque --- ...fix-not-parsing-some-wpcom-errors-properly | 4 ++++ .../boost-core/src/lib/class-utils.php | 23 +++++++++++++++---- .../recommendations-meta.tsx | 2 +- ...fix-isa-showing-error-if-no-report-present | 4 ++++ 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 projects/packages/boost-core/changelog/fix-not-parsing-some-wpcom-errors-properly create mode 100644 projects/plugins/boost/changelog/fix-isa-showing-error-if-no-report-present diff --git a/projects/packages/boost-core/changelog/fix-not-parsing-some-wpcom-errors-properly b/projects/packages/boost-core/changelog/fix-not-parsing-some-wpcom-errors-properly new file mode 100644 index 0000000000000..d24d162efc1e6 --- /dev/null +++ b/projects/packages/boost-core/changelog/fix-not-parsing-some-wpcom-errors-properly @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +General: Fixed not parsing error responses from WordPress.com properly. diff --git a/projects/packages/boost-core/src/lib/class-utils.php b/projects/packages/boost-core/src/lib/class-utils.php index 848cf3009c0b3..52207e115f075 100644 --- a/projects/packages/boost-core/src/lib/class-utils.php +++ b/projects/packages/boost-core/src/lib/class-utils.php @@ -133,11 +133,26 @@ public static function send_wpcom_request( $method, $endpoint, $args = null, $bo $code ); + /* + * Normalize error responses from WordPress.com. + * + * When WordPress.com returns an error from Boost Cloud, the body contains + * statusCode and error. When it returns a WP_Error, it contains code and message. + */ // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $err_code = empty( $data['statusCode'] ) ? 'http_error' : $data['statusCode']; - $message = empty( $data['error'] ) ? $default_message : $data['error']; - - return new \WP_Error( $err_code, $message ); + if ( isset( $data['statusCode'] ) && isset( $data['error'] ) ) { + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $data_code = $data['statusCode']; + $data_message = $data['error']; + } elseif ( isset( $data['code'] ) && isset( $data['message'] ) ) { + $data_code = $data['code']; + $data_message = $data['message']; + } + + $error_code = empty( $data_code ) ? 'http_error' : $data_code; + $message = empty( $data_message ) ? $default_message : $data_message; + + return new \WP_Error( $error_code, $message ); } return $data; diff --git a/projects/plugins/boost/app/assets/src/js/features/image-size-analysis/recommendations-meta/recommendations-meta.tsx b/projects/plugins/boost/app/assets/src/js/features/image-size-analysis/recommendations-meta/recommendations-meta.tsx index 049d1bb448a59..f475249d3f8b6 100644 --- a/projects/plugins/boost/app/assets/src/js/features/image-size-analysis/recommendations-meta/recommendations-meta.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/image-size-analysis/recommendations-meta/recommendations-meta.tsx @@ -87,7 +87,7 @@ const RecommendationsMeta: React.FC< Props > = ( { isCdnActive } ) => { isaRequest.isError; const getErrorMessage = ( report: typeof isaReport ) => { - if ( report?.status === 'error' || report?.status === 'not-found' ) { + if ( report?.status === 'error' ) { return report.message; } diff --git a/projects/plugins/boost/changelog/fix-isa-showing-error-if-no-report-present b/projects/plugins/boost/changelog/fix-isa-showing-error-if-no-report-present new file mode 100644 index 0000000000000..ec4939628d4b8 --- /dev/null +++ b/projects/plugins/boost/changelog/fix-isa-showing-error-if-no-report-present @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +UI: Fixed showing an error if no ISA report was found. From 8109adee274c336579b8f6bfd17814768c96c0a7 Mon Sep 17 00:00:00 2001 From: Peter Petrov Date: Sat, 21 Dec 2024 00:10:54 +0200 Subject: [PATCH 5/8] Boost: Fix sending multiple tracks when speed score changes (#40700) * Fix sending multiple tracks when speed score changes * Fix sending speed score changed tracks when popup was dismissed * add changelog --------- Co-authored-by: Adnan Haque --- .../js/features/speed-score/pop-out/pop-out.tsx | 14 ++++++++------ .../boost/changelog/fix-speed-score-changed-tracks | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 projects/plugins/boost/changelog/fix-speed-score-changed-tracks diff --git a/projects/plugins/boost/app/assets/src/js/features/speed-score/pop-out/pop-out.tsx b/projects/plugins/boost/app/assets/src/js/features/speed-score/pop-out/pop-out.tsx index 7b2338d27e4e5..715b2d3e17318 100644 --- a/projects/plugins/boost/app/assets/src/js/features/speed-score/pop-out/pop-out.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/speed-score/pop-out/pop-out.tsx @@ -2,7 +2,7 @@ import { animated, useSpring } from '@react-spring/web'; import CloseButton from '$features/ui/close-button/close-button'; import styles from './pop-out.module.scss'; import { __ } from '@wordpress/i18n'; -import { ReactNode, useState } from 'react'; +import { ReactNode, useState, useEffect } from 'react'; import { Button } from '@wordpress/components'; import { useDismissibleAlertState } from '$features/performance-history/lib/hooks'; import { getRedirectUrl } from '@automattic/jetpack-components'; @@ -77,11 +77,13 @@ function PopOut( { scoreChange }: Props ) { const hideAlert = () => setClose( true ); - if ( hasScoreChanged ) { - recordBoostEvent( 'speed_score_alert_shown', { - score_direction: scoreChange > 0 ? 'up' : 'down', - } ); - } + useEffect( () => { + if ( hasScoreChanged && ! isDismissed && ! isClosed ) { + recordBoostEvent( 'speed_score_alert_shown', { + score_direction: scoreChange > 0 ? 'up' : 'down', + } ); + } + }, [ hasScoreChanged, scoreChange, isDismissed, isClosed ] ); const animationStyles = useSpring( { from: { diff --git a/projects/plugins/boost/changelog/fix-speed-score-changed-tracks b/projects/plugins/boost/changelog/fix-speed-score-changed-tracks new file mode 100644 index 0000000000000..5d4a87253a8ea --- /dev/null +++ b/projects/plugins/boost/changelog/fix-speed-score-changed-tracks @@ -0,0 +1,5 @@ +Significance: patch +Type: fixed +Comment: Fix for an unreleased change. + + From acacbbe34e341fb2e5b4825b506f6d16e6fd0858 Mon Sep 17 00:00:00 2001 From: Adnan Haque <3737780+haqadn@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:11:15 -0500 Subject: [PATCH 6/8] Boost: Add edge-cache header on WP Cloud (#40557) * Add edge-cache header on atomic * changelog * Update atomic check * Add edge-cache header when uncached as well --- .../plugins/boost/app/lib/minify/functions-service.php | 8 ++++++++ projects/plugins/boost/changelog/add-edge-cache-header | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 projects/plugins/boost/changelog/add-edge-cache-header diff --git a/projects/plugins/boost/app/lib/minify/functions-service.php b/projects/plugins/boost/app/lib/minify/functions-service.php index 20812b65b62d2..08fbd8997447d 100644 --- a/projects/plugins/boost/app/lib/minify/functions-service.php +++ b/projects/plugins/boost/app/lib/minify/functions-service.php @@ -60,6 +60,10 @@ function jetpack_boost_page_optimize_service_request() { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $etag = '"' . md5( file_get_contents( $cache_file ) ) . '"'; + // Check if we're on Atomic and take advantage of the Atomic Edge Cache. + if ( defined( 'ATOMIC_CLIENT_ID' ) ) { + header( 'A8c-Edge-Cache: cache' ); + } header( 'X-Page-Optimize: cached' ); header( 'Cache-Control: max-age=' . 31536000 ); header( 'ETag: ' . $etag ); @@ -77,6 +81,10 @@ function jetpack_boost_page_optimize_service_request() { foreach ( $headers as $header ) { header( $header ); } + // Check if we're on Atomic and take advantage of the Atomic Edge Cache. + if ( defined( 'ATOMIC_CLIENT_ID' ) ) { + header( 'A8c-Edge-Cache: cache' ); + } header( 'X-Page-Optimize: uncached' ); header( 'Cache-Control: max-age=' . 31536000 ); header( 'ETag: "' . md5( $content ) . '"' ); diff --git a/projects/plugins/boost/changelog/add-edge-cache-header b/projects/plugins/boost/changelog/add-edge-cache-header new file mode 100644 index 0000000000000..534f4a2e449ff --- /dev/null +++ b/projects/plugins/boost/changelog/add-edge-cache-header @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Minify: Added HTTP header to take advantage of WordPress.com edge caching From 753c548f2983cb92859dbde6431328ccbf408c81 Mon Sep 17 00:00:00 2001 From: Adnan Haque <3737780+haqadn@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:38:43 -0500 Subject: [PATCH 7/8] Boost: Improve responsiveness during CCSS generation retry (#40675) * Move C.CSS retry down the element chain * Optimistic update critical CSS regen * Only start generation if there is any provider in the list * changelog * Change the order of state update * Fix showstopper behavior for cloud CSS * Use CCSS context also for cloud and move retried state to it * Rename variable for clarity * Update retried state after regeneration has started --- .../cloud-css-meta/cloud-css-meta.tsx | 4 -- .../critical-css-context-provider.tsx} | 40 ++++++++++++++----- .../critical-css-meta/critical-css-meta.tsx | 8 +--- .../critical-css/lib/critical-css-errors.ts | 5 +++ .../lib/stores/critical-css-state.ts | 38 +++++++++++++++--- .../critical-css/lib/use-retry-regenerate.ts | 9 ++--- .../show-stopper-error/show-stopper-error.tsx | 20 ++++------ .../features/critical-css/status/status.tsx | 6 --- .../js/features/speed-score/speed-score.tsx | 2 +- .../js/layout/settings-page/settings-page.tsx | 6 +-- .../boost/changelog/fix-critical-css-status | 4 ++ 11 files changed, 87 insertions(+), 55 deletions(-) rename projects/plugins/boost/app/assets/src/js/features/critical-css/{local-generator/local-generator-provider.tsx => critical-css-context/critical-css-context-provider.tsx} (73%) create mode 100644 projects/plugins/boost/changelog/fix-critical-css-status diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/cloud-css-meta/cloud-css-meta.tsx b/projects/plugins/boost/app/assets/src/js/features/critical-css/cloud-css-meta/cloud-css-meta.tsx index 8f1f3ae65660c..7dc510ff0e5ce 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/cloud-css-meta/cloud-css-meta.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/cloud-css-meta/cloud-css-meta.tsx @@ -1,12 +1,10 @@ import { __ } from '@wordpress/i18n'; import Status from '../status/status'; import { useCriticalCssState } from '../lib/stores/critical-css-state'; -import { useRetryRegenerate } from '../lib/use-retry-regenerate'; import { isFatalError } from '../lib/critical-css-errors'; export default function CloudCssMetaProps() { const [ cssState ] = useCriticalCssState(); - const [ hasRetried, retry ] = useRetryRegenerate(); const isPending = cssState.status === 'pending'; const hasCompletedSome = cssState.providers.some( provider => provider.status !== 'pending' ); @@ -29,8 +27,6 @@ export default function CloudCssMetaProps() { cssState={ cssState } isCloud={ true } showFatalError={ isFatalError( cssState ) } - hasRetried={ hasRetried } - retry={ retry } extraText={ extraText || undefined } overrideText={ overrideText || undefined } /> diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/local-generator/local-generator-provider.tsx b/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-context/critical-css-context-provider.tsx similarity index 73% rename from projects/plugins/boost/app/assets/src/js/features/critical-css/local-generator/local-generator-provider.tsx rename to projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-context/critical-css-context-provider.tsx index deb0e59881b6e..0b2d52bffddfd 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/local-generator/local-generator-provider.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-context/critical-css-context-provider.tsx @@ -11,19 +11,23 @@ import { import { runLocalGenerator } from '../lib/generate-critical-css'; import { CriticalCssErrorDetails } from '../lib/stores/critical-css-state-types'; -type LocalGeneratorContext = { +type CriticalCssContextValues = { isGenerating: boolean; setGenerating: ( generating: boolean ) => void; providerProgress: number; setProviderProgress: ( progress: number ) => void; + + // Whether we've retried generating critical CSS after an error. + hasRetriedAfterError: boolean; + setHasRetriedAfterError: ( hasRetried: boolean ) => void; }; type ProviderProps = { children: ReactNode; }; -const CssGeneratorContext = createContext< LocalGeneratorContext | null >( null ); +const CriticalCssContext = createContext< CriticalCssContextValues | null >( null ); /** * Local Critical CSS Context Provider component - provides context for any descendants that want to @@ -31,28 +35,34 @@ const CssGeneratorContext = createContext< LocalGeneratorContext | null >( null * * @param {ProviderProps} props - Component props. */ -export default function LocalCriticalCssGeneratorProvider( { children }: ProviderProps ) { +export default function CriticalCssProvider( { children }: ProviderProps ) { const [ isGenerating, setGenerating ] = useState< boolean >( false ); const [ providerProgress, setProviderProgress ] = useState< number >( 0 ); + const [ hasRetriedAfterError, setHasRetriedAfterError ] = useState< boolean >( false ); const value = { + // Local Generator status. isGenerating, setGenerating, providerProgress, setProviderProgress, + + // Whether we've retried generating critical CSS after an error. + hasRetriedAfterError, + setHasRetriedAfterError, }; - return { children }; + return { children }; } /** * Internal helper function: Use the raw Critical CSS Generator context, and verify it's inside a provider. */ -function useLocalCriticalCssGeneratorContext() { - const status = useContext( CssGeneratorContext ); +function useCriticalCssContext() { + const status = useContext( CriticalCssContext ); if ( ! status ) { - throw new Error( 'Local critical CSS generator status not available' ); + throw new Error( 'Critical CSS status not available' ); } return status; @@ -62,18 +72,26 @@ function useLocalCriticalCssGeneratorContext() { * For status consumers: Get an overview of the local critical CSS generator status. Is it running or not? */ export function useLocalCriticalCssGeneratorStatus() { - const { isGenerating, providerProgress } = useLocalCriticalCssGeneratorContext(); + const { isGenerating, providerProgress } = useCriticalCssContext(); return { isGenerating, providerProgress }; } +/** The retried state of critical CSS. */ +export function useCriticalCssRetriedAfterErrorState() { + const { hasRetriedAfterError: hasRetried, setHasRetriedAfterError: setHasRetried } = + useCriticalCssContext(); + + return [ hasRetried, setHasRetried ] as const; +} + /** * For Critical CSS UI: Actually run the local generator and return its status. */ export function useLocalCriticalCssGenerator() { // Local Generator status context. const { isGenerating, setGenerating, providerProgress, setProviderProgress } = - useLocalCriticalCssGeneratorContext(); + useCriticalCssContext(); // Critical CSS state and actions. const [ cssState, setCssState ] = useCriticalCssState(); @@ -86,7 +104,7 @@ export function useLocalCriticalCssGenerator() { useEffect( () => { - if ( cssState.status === 'pending' ) { + if ( cssState.status === 'pending' && cssState.providers.length > 0 ) { let abortController: AbortController | undefined; setGenerating( true ); @@ -119,7 +137,7 @@ export function useLocalCriticalCssGenerator() { // This effect triggers an actual process that is costly to start and stop, so we don't want to start/stop it // every time an object ref like `cssState` is changed for a trivial reason. // eslint-disable-next-line react-hooks/exhaustive-deps - [ cssState.status ] + [ cssState.status, cssState.providers.length ] ); const progress = diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-meta/critical-css-meta.tsx b/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-meta/critical-css-meta.tsx index c55cdc1532b17..bde75368908b5 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-meta/critical-css-meta.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/critical-css-meta/critical-css-meta.tsx @@ -4,8 +4,7 @@ import ProgressBar from '$features/ui/progress-bar/progress-bar'; import styles from './critical-css-meta.module.scss'; import { useCriticalCssState } from '../lib/stores/critical-css-state'; import { RegenerateCriticalCssSuggestion, useRegenerationReason } from '..'; -import { useLocalCriticalCssGenerator } from '../local-generator/local-generator-provider'; -import { useRetryRegenerate } from '../lib/use-retry-regenerate'; +import { useLocalCriticalCssGenerator } from '../critical-css-context/critical-css-context-provider'; import { isFatalError } from '../lib/critical-css-errors'; /** @@ -14,12 +13,11 @@ import { isFatalError } from '../lib/critical-css-errors'; */ export default function CriticalCssMeta() { const [ cssState ] = useCriticalCssState(); - const [ hasRetried, retry ] = useRetryRegenerate(); const [ { data: regenerateReason } ] = useRegenerationReason(); const { progress } = useLocalCriticalCssGenerator(); const showFatalError = isFatalError( cssState ); - if ( cssState.status === 'pending' ) { + if ( cssState.status === 'pending' || cssState.status === 'not_generated' ) { return (
@@ -39,8 +37,6 @@ export default function CriticalCssMeta() { cssState={ cssState } isCloud={ false } showFatalError={ showFatalError } - hasRetried={ hasRetried } - retry={ retry } highlightRegenerateButton={ !! regenerateReason } extraText={ __( 'Remember to regenerate each time you make changes that affect your HTML or CSS structure.', diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/critical-css-errors.ts b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/critical-css-errors.ts index 52260adb4b0b3..7967660eead18 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/critical-css-errors.ts +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/critical-css-errors.ts @@ -34,6 +34,11 @@ export function isFatalError( cssState: CriticalCssState ): boolean { return false; } + // If there are no providers, the state is being re-initialized. So dismiss any show-stopper errors. + if ( cssState.providers.length === 0 ) { + return false; + } + const hasActiveProvider = cssState.providers.some( provider => provider.status === 'success' || provider.status === 'pending' ); diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/stores/critical-css-state.ts b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/stores/critical-css-state.ts index ec0b282d2250b..b5e7da70af7b7 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/stores/critical-css-state.ts +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/stores/critical-css-state.ts @@ -57,14 +57,20 @@ export function criticalCssErrorState( message: string ): CriticalCssState { * All Critical CSS State actions return a success flag and the new state. This hook wraps the * common logic for handling the result of these actions. * - * @param {string} action - The name of the action. - * @param {z.ZodSchema} schema - The schema for the action request. - * @param {Function} onSuccess - Optional callback for handling the new state. + * @param {string} action - The name of the action. + * @param {z.ZodSchema} schema - The schema for the action request. + * @param {CriticalCssState} optimisticState - The state to use for optimistic updates. + * @param {Function} onSuccess - Optional callback for handling the new state. */ function useCriticalCssAction< ActionSchema extends z.ZodSchema, ActionRequestData extends z.infer< ActionSchema >, ->( action: string, schema: ActionRequestData, onSuccess?: ( state: CriticalCssState ) => void ) { +>( + action: string, + schema: ActionRequestData, + optimisticState?: CriticalCssState, + onSuccess?: ( state: CriticalCssState ) => void +) { const responseSchema = z.object( { success: z.boolean(), state: CriticalCssStateSchema, @@ -90,6 +96,13 @@ function useCriticalCssAction< action_response: responseSchema, }, callbacks: { + optimisticUpdate: ( _requestData, state: CriticalCssState ) => { + if ( optimisticState ) { + return optimisticState; + } + + return state; + }, onResult: ( result, _state ): CriticalCssState => { if ( result.success ) { if ( onSuccess ) { @@ -150,10 +163,23 @@ export function useSetProviderErrorsAction() { /** * Hook which creates a callable action for regenerating Critical CSS. + * + * @param {Function} callback - Optional callback to call when a regeneration starts successfully. */ -export function useRegenerateCriticalCssAction() { +export function useRegenerateCriticalCssAction( callback?: () => void ) { const [ , resetReason ] = useRegenerationReason(); - return useCriticalCssAction( 'request-regenerate', z.void(), resetReason ); + + const onSuccess = () => { + if ( callback ) { + callback(); + } + + resetReason(); + }; + + // Optimistically update the state to hide any errors and immediately show the pending state. + const optimisticState: CriticalCssState = { status: 'pending', providers: [] }; + return useCriticalCssAction( 'request-regenerate', z.void(), optimisticState, onSuccess ); } /** diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/use-retry-regenerate.ts b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/use-retry-regenerate.ts index c8ef9b5bf6a97..01e4b193271c1 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/use-retry-regenerate.ts +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/lib/use-retry-regenerate.ts @@ -1,5 +1,5 @@ -import { useState } from 'react'; import { useRegenerateCriticalCssAction } from './stores/critical-css-state'; +import { useCriticalCssRetriedAfterErrorState } from '../critical-css-context/critical-css-context-provider'; /** * Helper for "Retry" buttons for Critical CSS which need to track whether they have been clicked @@ -8,13 +8,12 @@ import { useRegenerateCriticalCssAction } from './stores/critical-css-state'; * Returns a boolean indicating whether retrying has been attempted, and a function to call to retry. */ export function useRetryRegenerate(): [ boolean, () => void ] { - const [ retried, setRetried ] = useState( false ); - const regenerateAction = useRegenerateCriticalCssAction(); + const [ retriedAfterError, setRetriedAfterError ] = useCriticalCssRetriedAfterErrorState(); + const regenerateAction = useRegenerateCriticalCssAction( () => setRetriedAfterError( true ) ); function retry() { - setRetried( true ); regenerateAction.mutate(); } - return [ retried, retry ]; + return [ retriedAfterError, retry ]; } diff --git a/projects/plugins/boost/app/assets/src/js/features/critical-css/show-stopper-error/show-stopper-error.tsx b/projects/plugins/boost/app/assets/src/js/features/critical-css/show-stopper-error/show-stopper-error.tsx index 5e640257c58f6..e46c527cd8a3d 100644 --- a/projects/plugins/boost/app/assets/src/js/features/critical-css/show-stopper-error/show-stopper-error.tsx +++ b/projects/plugins/boost/app/assets/src/js/features/critical-css/show-stopper-error/show-stopper-error.tsx @@ -11,19 +11,16 @@ import getCriticalCssErrorSetInterpolateVars from '$lib/utils/get-critical-css-e import formatErrorSetUrls from '$lib/utils/format-error-set-urls'; import actionLinkInterpolateVar from '$lib/utils/action-link-interpolate-var'; import { recordBoostEvent } from '$lib/utils/analytics'; +import { useRetryRegenerate } from '../lib/use-retry-regenerate'; type ShowStopperErrorTypes = { supportLink?: string; cssState: CriticalCssState; - retry: () => void; - showRetry?: boolean; }; const ShowStopperError: React.FC< ShowStopperErrorTypes > = ( { supportLink = 'https://wordpress.org/support/plugin/jetpack-boost/', cssState, - retry, - showRetry, } ) => { const primaryErrorSet = getPrimaryErrorSet( cssState ); const showLearnSection = primaryErrorSet && cssState.status === 'generated'; @@ -55,12 +52,7 @@ const ShowStopperError: React.FC< ShowStopperErrorTypes > = ( { ) : ( - + ) } @@ -136,7 +128,9 @@ const DocumentationSection = ( { ); }; -const OtherErrors = ( { cssState, retry, showRetry, supportLink }: ShowStopperErrorTypes ) => { +const OtherErrors = ( { cssState, supportLink }: ShowStopperErrorTypes ) => { + const [ hasRetried, retry ] = useRetryRegenerate(); + const firstTimeError = __( 'An unexpected error has occurred. As this error may be temporary, please try and refresh the Critical CSS.', 'jetpack-boost' @@ -184,7 +178,7 @@ const OtherErrors = ( { cssState, retry, showRetry, supportLink }: ShowStopperEr ) : ( <> -

{ showRetry ? firstTimeError : secondTimeError }

+

{ ! hasRetried ? firstTimeError : secondTimeError }

{ sprintf( /* translators: %s: error message */ @@ -192,7 +186,7 @@ const OtherErrors = ( { cssState, retry, showRetry, supportLink }: ShowStopperEr cssState.status_error ) }

- { showRetry ? ( + { ! hasRetried ? (
diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/logins-blocked-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/logins-blocked-status.tsx index 384fe1329ce4b..71aa5cada5027 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/logins-blocked-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/logins-blocked-status.tsx @@ -79,10 +79,8 @@ function BlockedStatus( { status }: { status: 'active' | 'inactive' | 'off' } ) message: 'no data yet', } } > - <> -

{ blockedLoginsTooltip.title }

-

{ blockedLoginsTooltip.text }

- +

{ blockedLoginsTooltip.title }

+

{ blockedLoginsTooltip.text }

@@ -113,10 +111,8 @@ function BlockedStatus( { status }: { status: 'active' | 'inactive' | 'off' } ) status: status, } } > - <> -

{ blockedLoginsTooltip.title }

-

{ blockedLoginsTooltip.text }

- +

{ blockedLoginsTooltip.title }

+

{ blockedLoginsTooltip.text }

diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/protect-value-section.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/protect-value-section.tsx index 015cce91ea44f..bb744c6f503b4 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/protect-value-section.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/protect-value-section.tsx @@ -29,10 +29,8 @@ const ProtectValueSection = () => { status: 'inactive', } } > - <> -

{ pluginsThemesTooltip.title }

-

{ pluginsThemesTooltip.text }

- +

{ pluginsThemesTooltip.title }

+

{ pluginsThemesTooltip.text }

) }
diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx index af25fe770b8e0..6cda8a7331bdf 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/scan-threats-status.tsx @@ -138,10 +138,10 @@ function ThreatStatus( { focusOnMount={ 'container' } onClose={ hideTooltip } > - <> +

{ scanThreatsTooltip.title }

{ scanThreatsTooltip.text }

- +
) }
@@ -157,8 +157,20 @@ function ThreatStatus( { return ( <> -
+
{ __( 'Threats', 'jetpack-my-jetpack' ) } + +

{ scanThreatsTooltip.title }

+

{ scanThreatsTooltip.text }

+
{ numThreats }
@@ -213,10 +225,8 @@ function ScanStatus( { status }: { status: 'success' | 'partial' | 'off' } ) { threats: 0, } } > - <> -

{ scanThreatsTooltip.title }

-

{ scanThreatsTooltip.text }

- +

{ scanThreatsTooltip.title }

+

{ scanThreatsTooltip.text }

diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts index 6f95e251ea099..c6bdab86dcfc9 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts @@ -27,8 +27,11 @@ export type TooltipContent = { export function useProtectTooltipCopy(): TooltipContent { const slug = PRODUCT_SLUGS.PROTECT; const { detail } = useProduct( slug ); - const { isPluginActive: isProtectPluginActive, hasPaidPlanForProduct: hasProtectPaidPlan } = - detail || {}; + const { + isPluginActive: isProtectPluginActive, + hasPaidPlanForProduct: hasProtectPaidPlan, + manageUrl: protectDashboardUrl, + } = detail || {}; const { recordEvent } = useAnalytics(); const { plugins, @@ -39,6 +42,7 @@ export function useProtectTooltipCopy(): TooltipContent { plugins: fromScanPlugins, themes: fromScanThemes, num_threats: numThreats = 0, + threats = [], } = scanData || {}; const { jetpack_waf_automatic_rules: isAutoFirewallEnabled, @@ -49,6 +53,12 @@ export function useProtectTooltipCopy(): TooltipContent { const pluginsCount = fromScanPlugins.length || Object.keys( plugins ).length; const themesCount = fromScanThemes.length || Object.keys( themes ).length; + const criticalThreatCount: number = useMemo( () => { + return threats.length + ? threats.reduce( ( accum, threat ) => ( threat.severity >= 5 ? ( accum += 1 ) : accum ), 0 ) + : 0; + }, [ threats ] ); + const settingsLink = useMemo( () => { if ( isProtectPluginActive ) { return 'admin.php?page=jetpack-protect#/firewall'; @@ -65,6 +75,15 @@ export function useProtectTooltipCopy(): TooltipContent { } ); }, [ recordEvent, settingsLink ] ); + const trackProtectDashboardLinkClick = useCallback( () => { + recordEvent( 'jetpack_protect_card_tooltip_content_link_click', { + page: 'my-jetpack', + feature: 'jetpack-protect', + location: 'scan-threats-tooltip', + path: protectDashboardUrl, + } ); + }, [ recordEvent, protectDashboardUrl ] ); + const isBruteForcePluginsActive = isProtectPluginActive || isJetpackPluginActive(); const blockedLoginsTooltip = useMemo( () => { @@ -173,23 +192,50 @@ export function useProtectTooltipCopy(): TooltipContent { hasProtectPaidPlan && numThreats ? { title: __( 'Auto-fix threats', 'jetpack-my-jetpack' ), - text: sprintf( - /* translators: %s is the singular or plural of number of detected critical threats on the site. */ - __( - 'The last scan identified %s. But don’t worry, use the “Auto-fix” button in the product to automatically fix most threats.', - 'jetpack-my-jetpack' - ), - sprintf( - /* translators: %d is the number of detected scan threats on the site. */ - _n( - '%d critical threat.', - '%d critical threats.', - numThreats, - 'jetpack-my-jetpack' - ), - numThreats - ) - ), + text: criticalThreatCount + ? createInterpolateElement( + sprintf( + /* translators: %1$s is the number of threats, %2$s is the numner of critical threats on the site, and %3$s is either "Scan" or "Protect" (the type of dashboard). */ + __( + 'The last scan identified %1$s (%2$d\u00A0critical). But don’t worry, Protect is usually able to “Auto-fix” threats, in most cases. Visit the %3$s dashboard to view more details.', + 'jetpack-my-jetpack' + ), + sprintf( + /* translators: %d is the number of detected scan threats on the site. */ + _n( '%d threat', '%d threats', numThreats, 'jetpack-my-jetpack' ), + numThreats + ), + criticalThreatCount, + isProtectPluginActive ? 'Protect' : 'Scan' + ), + { + a: createElement( 'a', { + href: protectDashboardUrl, + onClick: trackProtectDashboardLinkClick, + } ), + } + ) + : createInterpolateElement( + sprintf( + /* translators: %1$s is the singular or plural of number of detected threats on the site, and %2$s is either "Scan" or "Protect" (the type of dashboard). */ + __( + 'The last scan identified %1$s. But don’t worry, Protect is usually able to “Auto-fix” threats, in most cases. Visit the %2$s dashboard to view more details.', + 'jetpack-my-jetpack' + ), + sprintf( + /* translators: %d is the number of detected scan threats on the site. */ + _n( '%d threat', '%d threats', numThreats, 'jetpack-my-jetpack' ), + numThreats + ), + isProtectPluginActive ? 'Protect' : 'Scan' + ), + { + a: createElement( 'a', { + href: protectDashboardUrl, + onClick: trackProtectDashboardLinkClick, + } ), + } + ), } : { title: __( 'Elevate your malware protection', 'jetpack-my-jetpack' ), diff --git a/projects/packages/my-jetpack/changelog/update-my-jetpack-fix-protect-card-tooltips b/projects/packages/my-jetpack/changelog/update-my-jetpack-fix-protect-card-tooltips new file mode 100644 index 0000000000000..481fa13dec6a7 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/update-my-jetpack-fix-protect-card-tooltips @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Protect card- Fixed Tooltip placement & content issues. diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 5ae0a3a54fcf6..134f55d8e4ef0 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -268,6 +268,7 @@ interface Window { plugins: ScanItem[]; status: string; themes: ScanItem[]; + threats?: ThreatItem[]; }; wafConfig: { automatic_rules_available: boolean; diff --git a/projects/packages/my-jetpack/src/products/class-protect.php b/projects/packages/my-jetpack/src/products/class-protect.php index 7d325c4302cb6..b3e14499ddda9 100644 --- a/projects/packages/my-jetpack/src/products/class-protect.php +++ b/projects/packages/my-jetpack/src/products/class-protect.php @@ -8,15 +8,16 @@ namespace Automattic\Jetpack\My_Jetpack\Products; use Automattic\Jetpack\Connection\Client; -use Automattic\Jetpack\My_Jetpack\Product; +use Automattic\Jetpack\My_Jetpack\Hybrid_Product; use Automattic\Jetpack\My_Jetpack\Wpcom_Products; +use Automattic\Jetpack\Redirect; use Jetpack_Options; use WP_Error; /** * Class responsible for handling the Protect product */ -class Protect extends Product { +class Protect extends Hybrid_Product { const FREE_TIER_SLUG = 'free'; const UPGRADED_TIER_SLUG = 'upgraded'; @@ -321,7 +322,13 @@ public static function get_post_checkout_urls_by_feature() { * @return ?string */ public static function get_manage_url() { - return admin_url( 'admin.php?page=jetpack-protect' ); + // check standalone first + if ( static::is_standalone_plugin_active() ) { + return admin_url( 'admin.php?page=jetpack-protect' ); + // otherwise, check for the main Jetpack plugin + } elseif ( static::is_jetpack_plugin_active() ) { + return Redirect::get_url( 'my-jetpack-manage-scan' ); + } } /**