diff --git a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php index 353d0d7a785b8b..8203b6edad7669 100644 --- a/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php +++ b/lib/compat/wordpress-6.2/class-gutenberg-rest-global-styles-controller-6-2.php @@ -16,6 +16,25 @@ class Gutenberg_REST_Global_Styles_Controller_6_2 extends WP_REST_Global_Styles_ * @return void */ public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\/\w-]+)/revisions', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item_revisions' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'id' => array( + 'description' => __( 'The id of a template' ), + 'type' => 'string', + 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); parent::register_routes(); } @@ -84,6 +103,59 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + /** + * Returns revisions of the given global styles config custom post type. + * + * @since 6.2 + * + * @param WP_REST_Request $request The request instance. + * + * @return WP_REST_Response|WP_Error + */ + public function get_item_revisions( $request ) { + $post = $this->get_post( $request['id'] ); + if ( is_wp_error( $post ) ) { + return $post; + } + $revisions = array(); + $raw_config = json_decode( $post->post_content, true ); + $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; + + if ( $is_global_styles_user_theme_json ) { + $user_theme_revisions = wp_get_post_revisions( + $post->ID, + array( + 'author' => $post->post_author, + 'posts_per_page' => 10, + ) + ); + + if ( ! empty( $user_theme_revisions ) ) { + // Mostly taken from wp_prepare_revisions_for_js(). + foreach ( $user_theme_revisions as $revision ) { + $raw_revision_config = json_decode( $revision->post_content, true ); + $config = ( new WP_Theme_JSON_Gutenberg( $raw_revision_config, 'custom' ) )->get_raw_data(); + $now_gmt = time(); + $modified = strtotime( $revision->post_modified ); + $modified_gmt = strtotime( $revision->post_modified_gmt . ' +0000' ); + /* translators: %s: Human-readable time difference. */ + $time_ago = sprintf( __( '%s ago', 'gutenberg' ), human_time_diff( $modified_gmt, $now_gmt ) ); + $date_short = date_i18n( _x( 'j M @ H:i', 'revision date short format', 'gutenberg' ), $modified ); + $revisions[] = array( + 'styles' => ! empty( $config['styles'] ) ? $config['styles'] : new stdClass(), + 'settings' => ! empty( $config['settings'] ) ? $config['settings'] : new stdClass(), + 'title' => array( + 'raw' => $revision->post_modified, + /* translators: 1: Human-readable time difference, 2: short date combined to show rendered revision date. */ + 'rendered' => sprintf( __( '%1$s (%2$s)', 'gutenberg' ), $time_ago, $date_short ), + ), + 'id' => $revision->ID, + ); + } + } + } + return rest_ensure_response( $revisions ); + } /** * Prepare a global styles config output for response. @@ -133,38 +205,6 @@ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore V $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); } - if ( $is_global_styles_user_theme_json && rest_is_field_included( 'revisions', $fields ) ) { - $user_theme_revisions = wp_get_post_revisions( - $post->ID, - array( - 'author' => $post->post_author, - 'posts_per_page' => 10, - ) - ); - if ( empty( $user_theme_revisions ) ) { - $data['revisions'] = array(); - } else { - $user_revisions = array(); - // Mostly taken from wp_prepare_revisions_for_js(). - foreach ( $user_theme_revisions as $revision ) { - $raw_revision_config = json_decode( $revision->post_content, true ); - $config = ( new WP_Theme_JSON_Gutenberg( $raw_revision_config, 'custom' ) )->get_raw_data(); - $now_gmt = time(); - $modified = strtotime( $revision->post_modified ); - $modified_gmt = strtotime( $revision->post_modified_gmt . ' +0000' ); - $user_revisions[] = array( - 'styles' => ! empty( $config['styles'] ) ? $config['styles'] : new stdClass(), - 'settings' => ! empty( $config['settings'] ) ? $config['settings'] : new stdClass(), - 'dateShort' => date_i18n( _x( 'j M @ H:i', 'revision date short format' ), $modified ), - /* translators: %s: Human-readable time difference. */ - 'timeAgo' => sprintf( __( '%s ago' ), human_time_diff( $modified_gmt, $now_gmt ) ), - 'id' => $revision->ID, - ); - } - $data['revisions'] = $user_revisions; - } - } - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; $data = $this->add_additional_fields_to_object( $data, $request ); $data = $this->filter_response_by_context( $data, $context ); @@ -174,6 +214,15 @@ public function prepare_item_for_response( $post, $request ) { // phpcs:ignore V if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { $links = $this->prepare_links( $post->ID ); + if ( $is_global_styles_user_theme_json ) { + $revisions = wp_get_latest_revision_id_and_total_count( $post->ID ); + $revisions_count = ! is_wp_error( $revisions ) ? $revisions['count'] : 0; + $revisions_base = sprintf( '/%s/%s/%d/revisions', $this->namespace, $this->rest_base, $post->ID ); + $links['version-history'] = array( + 'href' => rest_url( $revisions_base ), + 'count' => $revisions_count, + ); + } $response->add_links( $links ); if ( ! empty( $links['self']['href'] ) ) { $actions = $this->get_available_actions(); diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 307e5cef0e2296..46f6be7ac6dbcb 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -131,21 +131,21 @@ export function receiveCurrentTheme( currentTheme ) { } /** - * Returns an action object used in signalling that the current global styles id has been received. + * Returns an action object used in signalling that the current global styles has been received. * Ignored from documentation as it's internal to the data store. * * @ignore * - * @param {string} currentGlobalStylesId The current global styles id. + * @param {Object} currentGlobalStyles The current global styles CPT. * * @return {Object} Action object. */ -export function __experimentalReceiveCurrentGlobalStylesId( - currentGlobalStylesId +export function __experimentalReceiveCurrentGlobalStyles( + currentGlobalStyles ) { return { - type: 'RECEIVE_CURRENT_GLOBAL_STYLES_ID', - id: currentGlobalStylesId, + type: 'RECEIVE_CURRENT_GLOBAL_STYLES', + globalStyles: currentGlobalStyles, }; } @@ -193,6 +193,28 @@ export function __experimentalReceiveThemeGlobalStyleVariations( }; } +/** + * Returns an action object used in signalling that the theme global styles CPT post revisions have been received. + * Ignored from documentation as it's internal to the data store. + * + * @ignore + * + * @param {number} currentId The post id. + * @param {Array} revisions The global styles revisions. + * + * @return {Object} Action object. + */ +export function __experimentalReceiveThemeGlobalStyleRevisions( + currentId, + revisions +) { + return { + type: 'RECEIVE_THEME_GLOBAL_STYLE_REVISIONS', + currentId, + revisions, + }; +} + /** * Returns an action object used in signalling that the index has been received. * diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 21ecaff436c72c..e15637e77ae68d 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -138,10 +138,10 @@ export function currentTheme( state = undefined, action ) { * * @return {string|undefined} Updated state. */ -export function currentGlobalStylesId( state = undefined, action ) { +export function currentGlobalStyles( state = undefined, action ) { switch ( action.type ) { - case 'RECEIVE_CURRENT_GLOBAL_STYLES_ID': - return action.id; + case 'RECEIVE_CURRENT_GLOBAL_STYLES': + return action.globalStyles; } return state; @@ -187,6 +187,26 @@ export function themeGlobalStyleVariations( state = {}, action ) { return state; } +/** + * Reducer managing the theme global styles revisions. + * + * @param {Record} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Record} Updated state. + */ +export function themeGlobalStyleRevisions( state = {}, action ) { + switch ( action.type ) { + case 'RECEIVE_THEME_GLOBAL_STYLE_REVISIONS': + return { + ...state, + [ action.currentId ]: action.revisions, + }; + } + + return state; +} + /** * Higher Order Reducer for a given entity config. It supports: * @@ -646,9 +666,10 @@ export default combineReducers( { terms, users, currentTheme, - currentGlobalStylesId, + currentGlobalStyles, currentUser, themeGlobalStyleVariations, + themeGlobalStyleRevisions, themeBaseGlobalStyles, taxonomies, entities, diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index b33bb42e653379..824a1f2455773d 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -453,7 +453,7 @@ __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => { ); }; -export const __experimentalGetCurrentGlobalStylesId = +export const __experimentalGetCurrentGlobalStyles = () => async ( { dispatch, resolveSelect } ) => { const activeThemes = await resolveSelect.getEntityRecords( @@ -468,8 +468,8 @@ export const __experimentalGetCurrentGlobalStylesId = const globalStylesObject = await apiFetch( { url: globalStylesURL, } ); - dispatch.__experimentalReceiveCurrentGlobalStylesId( - globalStylesObject.id + dispatch.__experimentalReceiveCurrentGlobalStyles( + globalStylesObject ); } }; @@ -500,6 +500,35 @@ export const __experimentalGetCurrentThemeGlobalStylesVariations = ); }; +export const __experimentalGetCurrentThemeGlobalStylesRevisions = + () => + async ( { resolveSelect, dispatch } ) => { + const currentGlobalStyles = + await resolveSelect.__experimentalGetCurrentGlobalStyles(); + const revisionsURL = + currentGlobalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.href; + if ( revisionsURL ) { + const revisions = await apiFetch( { + url: revisionsURL, + } ); + dispatch.__experimentalReceiveThemeGlobalStyleRevisions( + currentGlobalStyles?.id, + revisions + ); + } + }; + +__experimentalGetCurrentThemeGlobalStylesRevisions.shouldInvalidate = ( + action +) => { + return ( + action.type === 'SAVE_ENTITY_RECORD_FINISH' && + action.kind === 'root' && + ! action.error && + action.name === 'globalStyles' + ); +}; + export const getBlockPatterns = () => async ( { dispatch } ) => { diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 9998d67728e745..ed2d869428ae7c 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -28,13 +28,14 @@ export interface State { autosaves: Record< string | number, Array< unknown > >; blockPatterns: Array< unknown >; blockPatternCategories: Array< unknown >; - currentGlobalStylesId: string; + currentGlobalStyles: GlobalStyles; currentTheme: string; currentUser: ET.User< 'edit' >; embedPreviews: Record< string, { html: string } >; entities: EntitiesState; themeBaseGlobalStyles: Record< string, Object >; themeGlobalStyleVariations: Record< string, string >; + themeGlobalStyleRevisions: Record< number, Object >; undo: UndoState; users: UserState; } @@ -66,6 +67,13 @@ interface UserState { byId: Record< EntityRecordKey, ET.User< 'edit' > >; } +type GlobalStyles = { + title: { raw: string; rendered: string }; + id: string; + settings: Record< string, Object >; + styles: Record< string, Object >; +}; + type Optional< T > = T | undefined; /** @@ -983,10 +991,12 @@ export function getCurrentTheme( state: State ): any { * * @param state Data state. * - * @return The current global styles ID. + * @return The current global styles. */ -export function __experimentalGetCurrentGlobalStylesId( state: State ): string { - return state.currentGlobalStylesId; +export function __experimentalGetCurrentGlobalStyles( + state: State +): GlobalStyles { + return state.currentGlobalStyles; } /** @@ -1238,7 +1248,7 @@ export function __experimentalGetCurrentThemeBaseGlobalStyles( } /** - * Return the ID of the current global styles object. + * Returns the variations of the current global styles theme. * * @param state Data state. * @@ -1254,6 +1264,23 @@ export function __experimentalGetCurrentThemeGlobalStylesVariations( return state.themeGlobalStyleVariations[ currentTheme.stylesheet ]; } +/** + * Returns the revisions of the current global styles theme. + * + * @param state Data state. + * + * @return The current global styles. + */ +export function __experimentalGetCurrentThemeGlobalStylesRevisions( + state: State +): GlobalStyles | null { + const currentGlobalStyles = __experimentalGetCurrentGlobalStyles( state ); + if ( ! currentGlobalStyles?.id ) { + return null; + } + return state.themeGlobalStyleRevisions[ currentGlobalStyles.id ]; +} + /** * Retrieve the list of registered block patterns. * diff --git a/packages/edit-site/src/components/global-styles/context.js b/packages/edit-site/src/components/global-styles/context.js index 630b5a2b9059d5..1cb318cab40402 100644 --- a/packages/edit-site/src/components/global-styles/context.js +++ b/packages/edit-site/src/components/global-styles/context.js @@ -8,6 +8,7 @@ export const DEFAULT_GLOBAL_STYLES_CONTEXT = { base: {}, merged: {}, setUserConfig: () => {}, + userConfigRevisionsCount: 0, }; export const GlobalStylesContext = createContext( diff --git a/packages/edit-site/src/components/global-styles/global-styles-provider.js b/packages/edit-site/src/components/global-styles/global-styles-provider.js index 0813dc63b2cb86..1279915ec935fe 100644 --- a/packages/edit-site/src/components/global-styles/global-styles-provider.js +++ b/packages/edit-site/src/components/global-styles/global-styles-provider.js @@ -45,44 +45,41 @@ const cleanEmptyObject = ( object ) => { }; function useGlobalStylesUserConfig() { - const { globalStylesId, isReady, settings, styles } = useSelect( - ( select ) => { - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const _globalStylesId = - select( coreStore ).__experimentalGetCurrentGlobalStylesId(); - const record = _globalStylesId - ? getEditedEntityRecord( + const { + globalStylesId, + isReady, + settings, + styles, + userConfigRevisionsCount, + } = useSelect( ( select ) => { + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const _globalStyles = + select( coreStore ).__experimentalGetCurrentGlobalStyles(); + const record = _globalStyles?.id + ? getEditedEntityRecord( 'root', 'globalStyles', _globalStyles.id ) + : undefined; + + let hasResolved = false; + if ( hasFinishedResolution( '__experimentalGetCurrentGlobalStyles' ) ) { + hasResolved = _globalStyles?.id + ? hasFinishedResolution( 'getEditedEntityRecord', [ 'root', 'globalStyles', - _globalStylesId - ) - : undefined; - - let hasResolved = false; - if ( - hasFinishedResolution( - '__experimentalGetCurrentGlobalStylesId' - ) - ) { - hasResolved = _globalStylesId - ? hasFinishedResolution( 'getEditedEntityRecord', [ - 'root', - 'globalStyles', - _globalStylesId, - ] ) - : true; - } - - return { - globalStylesId: _globalStylesId, - isReady: hasResolved, - settings: record?.settings, - styles: record?.styles, - }; - }, - [] - ); + _globalStyles.id, + ] ) + : true; + } + + return { + globalStylesId: _globalStyles?.id, + isReady: hasResolved, + settings: record?.settings, + styles: record?.styles, + userConfigRevisionsCount: + record?._links?.[ 'version-history' ]?.[ 0 ]?.count || 0, + }; + }, [] ); const { getEditedEntityRecord } = useSelect( coreStore ); const { editEntityRecord } = useDispatch( coreStore ); @@ -119,7 +116,7 @@ function useGlobalStylesUserConfig() { [ globalStylesId ] ); - return [ isReady, config, setConfig ]; + return [ isReady, config, setConfig, userConfigRevisionsCount ]; } function useGlobalStylesBaseConfig() { @@ -133,8 +130,12 @@ function useGlobalStylesBaseConfig() { } function useGlobalStylesContext() { - const [ isUserConfigReady, userConfig, setUserConfig ] = - useGlobalStylesUserConfig(); + const [ + isUserConfigReady, + userConfig, + setUserConfig, + userConfigRevisionsCount, + ] = useGlobalStylesUserConfig(); const [ isBaseConfigReady, baseConfig ] = useGlobalStylesBaseConfig(); const mergedConfig = useMemo( () => { if ( ! baseConfig || ! userConfig ) { @@ -148,6 +149,7 @@ function useGlobalStylesContext() { user: userConfig, base: baseConfig, merged: mergedConfig, + userConfigRevisionsCount, setUserConfig, }; }, [ @@ -157,6 +159,7 @@ function useGlobalStylesContext() { setUserConfig, isUserConfigReady, isBaseConfigReady, + userConfigRevisionsCount, ] ); return context; diff --git a/packages/edit-site/src/components/global-styles/screen-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions.js index cd29a68f5c9387..29f604497bc193 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions.js @@ -3,16 +3,25 @@ */ import { set } from 'lodash'; import classnames from 'classnames'; -import fastDeepEqual from 'fast-deep-equal/es6'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { __experimentalVStack as VStack, Button } from '@wordpress/components'; +import { + __experimentalVStack as VStack, + Button, + SelectControl, +} from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { useContext, useCallback, useState } from '@wordpress/element'; +import { + useContext, + useCallback, + useState, + useEffect, + useMemo, +} from '@wordpress/element'; /** * Internal dependencies @@ -20,60 +29,100 @@ import { useContext, useCallback, useState } from '@wordpress/element'; import ScreenHeader from './header'; import Subtitle from './subtitle'; import { GlobalStylesContext } from './context'; +import { decodeEntities } from '@wordpress/html-entities'; +import { isGlobalStyleConfigEqual } from './utils'; -// Taken from packages/edit-site/src/hooks/push-changes-to-global-styles/index.js. -// TODO abstract -function cloneDeep( object ) { - return ! object ? {} : JSON.parse( JSON.stringify( object ) ); +function RevisionsSelect( { userRevisions, currentRevisionId, onChange } ) { + const userRevisionsOptions = useMemo( () => { + return ( userRevisions ?? [] ).map( ( revision ) => { + return { + value: revision.id, + label: decodeEntities( revision.title.rendered ), + }; + } ); + }, [ userRevisions ] ); + const setCurrentRevisionId = ( value ) => { + const revisionId = Number( value ); + onChange( + userRevisions.find( ( revision ) => revision.id === revisionId ) + ); + }; + return ( + + ); } -// Taken from packages/edit-site/src/components/global-styles/screen-style-variations.js. -// TODO abstract -function compareVariations( a, b ) { +function RevisionsButtons( { userRevisions, currentRevisionId, onChange } ) { return ( - fastDeepEqual( a.styles, b.styles ) && - fastDeepEqual( a.settings, b.settings ) +
    + { userRevisions.map( ( revision ) => { + const isActive = revision?.id === currentRevisionId; + return ( +
  1. + +
  2. + ); + } ) } +
); } function ScreenRevisions() { const { user: userConfig, setUserConfig } = useContext( GlobalStylesContext ); - const { userRevisions } = useSelect( ( select ) => { - const { getEditedEntityRecord } = select( coreStore ); - const _globalStylesId = - select( coreStore ).__experimentalGetCurrentGlobalStylesId(); - - // Maybe we can return the whole object from __experimentalGetCurrentGlobalStylesId - // and rename it to __experimentalGetCurrentGlobalStyles, - // otherwise we're grabbing this twice. - const record = _globalStylesId - ? getEditedEntityRecord( 'root', 'globalStyles', _globalStylesId ) - : undefined; - - return { - userRevisions: record?.revisions || [], - }; - }, [] ); - + const { userRevisions } = useSelect( + ( select ) => ( { + userRevisions: + select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesRevisions() || [], + } ), + [] + ); + const [ currentRevisionId, setCurrentRevisionId ] = useState(); const hasRevisions = userRevisions.length > 0; - const [ currentRevisionId, setCurrentRevisionId ] = useState( () => { + + useEffect( () => { if ( ! hasRevisions ) { - return 0; + return; } let currentRevision = userRevisions[ 0 ]; for ( let i = 0; i < userRevisions.length; i++ ) { - if ( compareVariations( userConfig, userRevisions[ i ] ) ) { + if ( isGlobalStyleConfigEqual( userConfig, userRevisions[ i ] ) ) { currentRevision = userRevisions[ i ]; break; } } - return currentRevision?.id; - } ); + setCurrentRevisionId( currentRevision?.id ); + }, [ userRevisions, hasRevisions ] ); const restoreRevision = useCallback( ( revision ) => { - const newUserConfig = cloneDeep( userConfig ); + const newUserConfig = ! userConfig + ? {} + : JSON.parse( JSON.stringify( userConfig ) ); set( newUserConfig, [ 'styles' ], revision?.styles ); set( newUserConfig, [ 'settings' ], revision?.settings ); setUserConfig( () => newUserConfig ); @@ -82,46 +131,26 @@ function ScreenRevisions() { [ userConfig ] ); + const RevisionsComponent = + userRevisions.length >= 10 ? RevisionsSelect : RevisionsButtons; + return ( <>
{ __( 'REVISIONS' ) } { hasRevisions ? ( - userRevisions.map( ( revision ) => { - const isActive = revision?.id === currentRevisionId; - return ( - - ); - } ) + ) : (

{ __( 'There are currently no revisions.' ) }

) } diff --git a/packages/edit-site/src/components/global-styles/screen-root.js b/packages/edit-site/src/components/global-styles/screen-root.js index 956005e7e3f246..aafba134aa6c1a 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -12,9 +12,10 @@ import { CardDivider, CardMedia, } from '@wordpress/components'; -import { isRTL, __ } from '@wordpress/i18n'; -import { chevronLeft, chevronRight } from '@wordpress/icons'; +import { isRTL, __, sprintf, _n } from '@wordpress/i18n'; +import { backup, chevronLeft, chevronRight } from '@wordpress/icons'; import { useSelect } from '@wordpress/data'; +import { useContext } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -24,8 +25,11 @@ import { IconWithCurrentColor } from './icon-with-current-color'; import { NavigationButtonAsItem } from './navigation-button'; import ContextMenu from './context-menu'; import StylesPreview from './preview'; +import { GlobalStylesContext } from './context'; function ScreenRoot() { + const { userConfigRevisionsCount: revisionsCount } = + useContext( GlobalStylesContext ); const { variations } = useSelect( ( select ) => { return { variations: @@ -37,6 +41,8 @@ function ScreenRoot() { const __experimentalGlobalStylesCustomCSS = window?.__experimentalEnableGlobalStylesCustomCSS; + const chevronIcon = isRTL() ? chevronLeft : chevronRight; + return ( @@ -57,9 +63,7 @@ function ScreenRoot() { { __( 'Browse styles' ) } @@ -94,9 +98,7 @@ function ScreenRoot() { > { __( 'Blocks' ) } - + @@ -124,9 +126,7 @@ function ScreenRoot() { { __( 'Custom' ) } @@ -135,33 +135,45 @@ function ScreenRoot() { ) } - - - - - { __( - "View the last ten revisions to your site's styles." - ) } - - - - - { __( 'Revisions' ) } - - - - - + { revisionsCount > 0 ? ( + <> + + + + { __( "View revisions to your site's styles." ) } + + + + + + { sprintf( + /* translators: %d: number of revisions */ + _n( + '%d Revision', + '%d Revisions', + revisionsCount + ), + revisionsCount + ) } + + + + + + + + ) : null } ); } diff --git a/packages/edit-site/src/components/global-styles/screen-style-variations.js b/packages/edit-site/src/components/global-styles/screen-style-variations.js index b88b81a0c08d17..02845ebc3fbce2 100644 --- a/packages/edit-site/src/components/global-styles/screen-style-variations.js +++ b/packages/edit-site/src/components/global-styles/screen-style-variations.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import fastDeepEqual from 'fast-deep-equal/es6'; /** * WordPress dependencies @@ -32,13 +31,7 @@ import { mergeBaseAndUserConfigs } from './global-styles-provider'; import { GlobalStylesContext } from './context'; import StylesPreview from './preview'; import ScreenHeader from './header'; - -function compareVariations( a, b ) { - return ( - fastDeepEqual( a.styles, b.styles ) && - fastDeepEqual( a.settings, b.settings ) - ); -} +import { isGlobalStyleConfigEqual } from './utils'; function Variation( { variation } ) { const [ isFocused, setIsFocused ] = useState( false ); @@ -72,7 +65,7 @@ function Variation( { variation } ) { }; const isActive = useMemo( () => { - return compareVariations( user, variation ); + return isGlobalStyleConfigEqual( user, variation ); }, [ user, variation ] ); return ( diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 25cd2c61edaa22..8006389c01d3e7 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -151,16 +151,17 @@ $block-preview-height: 150px; margin: $grid-unit-20; } +.edit-site-global-styles-screen-revisions__revisions-list { + list-style: none; + margin: 0; +} + .edit-site-global-styles-screen-revisions__revision-item { justify-content: center; + width: 100%; &.is-current:disabled { color: $white; background: $gray-900; } } -.edit-site-global-styles-screen-revisions__time-ago { - display: inline-block; - margin-right: $grid-unit-10; - font-style: italic; -} diff --git a/packages/edit-site/src/components/global-styles/test/utils.js b/packages/edit-site/src/components/global-styles/test/utils.js index 7d0e3464557f61..d7a00fec9b3844 100644 --- a/packages/edit-site/src/components/global-styles/test/utils.js +++ b/packages/edit-site/src/components/global-styles/test/utils.js @@ -1,7 +1,11 @@ /** * Internal dependencies */ -import { getPresetVariableFromValue, getValueFromVariable } from '../utils'; +import { + getPresetVariableFromValue, + getValueFromVariable, + isGlobalStyleConfigEqual, +} from '../utils'; describe( 'editor utils', () => { const themeJson = { @@ -203,4 +207,55 @@ describe( 'editor utils', () => { } ); } ); } ); + + describe( 'isGlobalStyleConfigEqual', () => { + test.each( [ + { original: null, variation: null, expected: false }, + { original: {}, variation: undefined, expected: false }, + { + original: { + styles: { + color: { text: 'var(--wp--preset--color--red)' }, + }, + }, + variation: { + styles: { + color: { text: 'var(--wp--preset--color--blue)' }, + }, + }, + expected: false, + }, + { original: {}, variation: undefined, expected: false }, + { + original: { + styles: { + color: { text: 'var(--wp--preset--color--red)' }, + }, + settings: { + typography: { + fontSize: true, + }, + }, + }, + variation: { + styles: { + color: { text: 'var(--wp--preset--color--red)' }, + }, + settings: { + typography: { + fontSize: true, + }, + }, + }, + expected: true, + }, + ] )( + '.isGlobalStyleConfigEqual( $original, $variation )', + ( { original, variation, expected } ) => { + expect( isGlobalStyleConfigEqual( original, variation ) ).toBe( + expected + ); + } + ); + } ); } ); diff --git a/packages/edit-site/src/components/global-styles/utils.js b/packages/edit-site/src/components/global-styles/utils.js index 14f3b868294172..ebcae3f5f32da6 100644 --- a/packages/edit-site/src/components/global-styles/utils.js +++ b/packages/edit-site/src/components/global-styles/utils.js @@ -2,6 +2,7 @@ * External dependencies */ import { get } from 'lodash'; +import fastDeepEqual from 'fast-deep-equal/es6'; /** * Internal dependencies @@ -333,3 +334,29 @@ export function scopeSelector( scope, selector ) { return selectorsScoped.join( ', ' ); } + +/** + * Compares global style variations according to their styles and settings properties. + * + * @example + * ```js + * const globalStyles = { styles: { typography: { fontSize: '10px' } }, settings: {} }; + * const variation = { styles: { typography: { fontSize: '10000px' } }, settings: {} }; + * const isEqual = isGlobalStyleConfigEqual( globalStyles, variation ); + * // false + * ``` + * + * @param {Object} original A global styles object. + * @param {Object} variation A global styles object. + * + * @return {boolean} Whether `original` and `variation` match. + */ +export function isGlobalStyleConfigEqual( original, variation ) { + if ( ! original || ! variation ) { + return false; + } + return ( + fastDeepEqual( original?.styles, variation?.styles ) && + fastDeepEqual( original?.settings, variation?.settings ) + ); +}