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 684786ef22d76..7c368ce602944 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 @@ -19,6 +19,72 @@ public function register_routes() { parent::register_routes(); } + /** + * Retrieves the global styles type' schema, conforming to JSON Schema. + * + * @since 5.9.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'ID of global styles config.' ), + 'type' => 'string', + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), + 'styles' => array( + 'description' => __( 'Global styles.' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'settings' => array( + 'description' => __( 'Global settings.' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + ), + 'title' => array( + 'description' => __( 'Title of the global styles variation.' ), + 'type' => array( 'object', 'string' ), + 'default' => '', + 'context' => array( 'embed', 'view', 'edit' ), + 'properties' => array( + 'raw' => array( + 'description' => __( 'Title for the global styles variation, as it exists in the database.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + ), + 'rendered' => array( + 'description' => __( 'HTML title for the post, transformed for display.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ), + 'revisions' => array( + 'description' => __( 'Global styles revisions.' ), + 'type' => array( 'object' ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + /** * Prepare a global styles config output for response. * @@ -67,6 +133,37 @@ 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(), + '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 ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions.js new file mode 100644 index 0000000000000..faf1e67f16bf5 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-revisions.js @@ -0,0 +1,111 @@ +/** + * External dependencies + */ +import { set } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { __experimentalVStack as VStack, Button } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useContext, useCallback, useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import ScreenHeader from './header'; +import Subtitle from './subtitle'; +import { GlobalStylesContext } from './context'; + +function cloneDeep( object ) { + return ! object ? {} : JSON.parse( JSON.stringify( object ) ); +} + +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 [ currentRevision, setCurrentRevision ] = useState( + userRevisions?.[ 0 ].id + ); + const restoreRevision = useCallback( + ( revision ) => { + const newUserConfig = cloneDeep( userConfig ); + set( newUserConfig, [ 'styles' ], revision?.styles ); + setUserConfig( () => newUserConfig ); + setCurrentRevision( revision?.id ); + }, + [ userConfig ] + ); + + const hasRevisions = userRevisions.length > 0; + + return ( + <> + +
+ + { __( 'REVISIONS' ) } + { hasRevisions + ? userRevisions.map( ( revision ) => ( + + ) ) + : __( 'There are currently no revisions.' ) } + +
+ + ); +} + +export default ScreenRevisions; 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 0d7959f59fe95..956005e7e3f24 100644 --- a/packages/edit-site/src/components/global-styles/screen-root.js +++ b/packages/edit-site/src/components/global-styles/screen-root.js @@ -134,6 +134,34 @@ function ScreenRoot() { ) } + + + + + + { __( + "View the last ten revisions to your site's styles." + ) } + + + + + { __( 'Revisions' ) } + + + + + ); } diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 5616a068b594c..25cd2c61edaa2 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -146,3 +146,21 @@ $block-preview-height: 150px; max-height: 200px; overflow-y: scroll; } + +.edit-site-global-styles-screen-revisions { + margin: $grid-unit-20; +} + +.edit-site-global-styles-screen-revisions__revision-item { + justify-content: center; + &.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/ui.js b/packages/edit-site/src/components/global-styles/ui.js index d7c9eed7d8474..f16872f7b239b 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -28,6 +28,7 @@ import ScreenStyleVariations from './screen-style-variations'; import ScreenBorder from './screen-border'; import StyleBook from '../style-book'; import ScreenCSS from './screen-css'; +import ScreenRevisions from './screen-revisions'; function GlobalStylesNavigationScreen( { className, ...props } ) { return ( @@ -195,6 +196,10 @@ function GlobalStylesUI( { isStyleBookOpened, onCloseStyleBook } ) { + + + + ); }