Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Global styles: enable user global styles revisions #46667

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ab00625
Initial commit
ramonjd Dec 20, 2022
241460c
Updating test
ramonjd Dec 20, 2022
0ba94e6
Sending and checking for settings as well since some style variations…
ramonjd Dec 21, 2022
7dcf0d0
Added a new endpoint to the global styles rest API (/revisions)
ramonjd Dec 22, 2022
f56cb4e
revisions is no longer a property of the response object
ramonjd Dec 22, 2022
fcfed1a
Update description for rest param
ramonjd Dec 23, 2022
025fcf5
There's no reason to use the stored userConfig at all if we're reinst…
ramonjd Dec 23, 2022
59cb723
added a test for the revisions endpoint
ramonjd Dec 23, 2022
af7420e
Added text domain
ramonjd Dec 23, 2022
9728372
Bumping minimum revision count until display to two:
ramonjd Dec 23, 2022
eeab42f
Added a very unpretty E2E test for global styles revisions. We can't …
ramonjd Dec 29, 2022
75b579f
Formatting the comments in the E2E test.
ramonjd Dec 29, 2022
7003d22
Fixing up things after a rebase, mainly usage of __experimentalGetCur…
ramonjd Apr 5, 2023
08a3eed
Moving changes to global styles controller to 6.2 compat
ramonjd Apr 5, 2023
363105d
JS LINT YO!
ramonjd Apr 5, 2023
a7a47df
Relocate js utils tests for isGlobalStyleConfigEqual
ramonjd Apr 5, 2023
760ae90
Move rest route registration to compat 6.3
ramonjd Apr 5, 2023
4b1b41d
update e2e tests so that they pass :D I was importing a non-existant …
ramonjd Apr 11, 2023
c1bb9f8
Testing with undo/reset buttons
ramonjd Apr 11, 2023
3a6c161
Moving the revisions link so that it's under the drop down
ramonjd Apr 12, 2023
461d7ad
Yoda?
ramonjd Apr 12, 2023
408a755
Adding author display name and avatar to rest response
ramonjd Apr 13, 2023
6445d13
Update PHP tests title > date
ramonjd Apr 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions lib/compat/wordpress-6.2/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,6 @@ function gutenberg_pattern_directory_collection_params_6_2( $query_params ) {
}
add_filter( 'rest_pattern_directory_collection_params', 'gutenberg_pattern_directory_collection_params_6_2' );

/**
* Registers the Global Styles REST API routes.
*/
function gutenberg_register_global_styles_endpoints() {
$editor_settings = new Gutenberg_REST_Global_Styles_Controller_6_2();
$editor_settings->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );

/**
* Updates REST API response for the sidebars and marks them as 'inactive'.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php
/**
* REST API: Gutenberg_REST_Global_Styles_Controller class
*
* @package Gutenberg
* @subpackage REST_API
*/

/**
* Base Global Styles REST API Controller.
*/
class Gutenberg_REST_Global_Styles_Controller_6_3 extends Gutenberg_REST_Global_Styles_Controller_6_2 {
/**
* Registers the controllers routes.
*
* @since 6.3 Registers `/revisions` endpoint.
*
* @return void
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\/\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 the global styles post.', 'gutenberg' ),
'type' => 'string',
'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ),
),
),
),
)
);
parent::register_routes();
}

/**
* Returns revisions of the given global styles config custom post type.
*
* @since 6.3
*
* @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(
'posts_per_page' => 100,
)
);

if ( ! empty( $user_theme_revisions ) ) {
// Mostly taken from wp_prepare_revisions_for_js().
foreach ( $user_theme_revisions as $id => $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(),
'date' => 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' => $id,
'is_latest' => array_key_first( $user_theme_revisions ) === $id,
'author' => array(
'display_name' => get_the_author_meta( 'display_name', $post->post_author ),
'avatar_url' => get_avatar_url(
$post->post_author,
array(
'size' => 24,
)
),
),
);
}
}
}
return rest_ensure_response( $revisions );
}

/**
* Prepare a global styles config output for response.
*
* @since 5.9.0
* @since 6.2 Handling of style.css was added to WP_Theme_JSON.
* @since 6.3 Adds version-history to the response object.
*
* @param WP_Post $post Global Styles post object.
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$raw_config = json_decode( $post->post_content, true );
$is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON'];
$config = array();
if ( $is_global_styles_user_theme_json ) {
$config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data();
}

// Base fields for every post.
$data = array();
$fields = $this->get_fields_for_response( $request );

if ( rest_is_field_included( 'id', $fields ) ) {
$data['id'] = $post->ID;
}

if ( rest_is_field_included( 'title', $fields ) ) {
$data['title'] = array();
}
if ( rest_is_field_included( 'title.raw', $fields ) ) {
$data['title']['raw'] = $post->post_title;
}
if ( rest_is_field_included( 'title.rendered', $fields ) ) {
add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );

$data['title']['rendered'] = get_the_title( $post->ID );

remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) );
}

if ( rest_is_field_included( 'settings', $fields ) ) {
$data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass();
}

if ( rest_is_field_included( 'styles', $fields ) ) {
$data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass();
}

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );

// Wrap the data in a response object.
$response = rest_ensure_response( $data );

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();
$self = $links['self']['href'];
foreach ( $actions as $rel ) {
$response->add_link( $rel, $self );
}
}
}

return $response;
}
}
9 changes: 9 additions & 0 deletions lib/compat/wordpress-6.3/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,12 @@ function gutenberg_update_templates_template_parts_rest_controller( $args, $post
return $args;
}
add_filter( 'register_post_type_args', 'gutenberg_update_templates_template_parts_rest_controller', 10, 2 );

/**
* Registers the Global Styles REST API routes.
*/
function gutenberg_register_global_styles_endpoints() {
$editor_settings = new Gutenberg_REST_Global_Styles_Controller_6_3();
$editor_settings->register_routes();
}
add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-pattern-directory-controller-6-3.php';
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-templates-controller-6-3.php';
require_once __DIR__ . '/compat/wordpress-6.3/rest-api.php';
require_once __DIR__ . '/compat/wordpress-6.3/class-gutenberg-rest-global-styles-controller-6-3.php';

// Experimental.
if ( ! class_exists( 'WP_Rest_Customizer_Nonces' ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const DEFAULT_GLOBAL_STYLES_CONTEXT = {
base: {},
merged: {},
setUserConfig: () => {},
userConfigRevisionsCount: 0,
};

export const GlobalStylesContext = createContext(
Expand Down
34 changes: 28 additions & 6 deletions packages/core-data/src/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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.
*
Expand Down
29 changes: 25 additions & 4 deletions packages/core-data/src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,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;
Expand Down Expand Up @@ -183,6 +183,26 @@ export function themeGlobalStyleVariations( state = {}, action ) {
return state;
}

/**
* Reducer managing the theme global styles revisions.
*
* @param {Record<string, object>} state Current state.
* @param {Object} action Dispatched action.
*
* @return {Record<string, object>} 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:
*
Expand Down Expand Up @@ -646,9 +666,10 @@ export default combineReducers( {
terms,
users,
currentTheme,
currentGlobalStylesId,
currentGlobalStyles,
currentUser,
themeGlobalStyleVariations,
themeGlobalStyleRevisions,
themeBaseGlobalStyles,
taxonomies,
entities,
Expand Down
35 changes: 32 additions & 3 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ __experimentalGetTemplateForLink.shouldInvalidate = ( action ) => {
);
};

export const __experimentalGetCurrentGlobalStylesId =
export const __experimentalGetCurrentGlobalStyles =
Copy link
Member Author

@ramonjd ramonjd Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I'm returning the entire global style object instead of just the ID is so we can access the revisions url.

E.g.: currentGlobalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.href;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed a waste not to repurpose this method since it was fetching what we wanted anyway and only returning the id.

Plus it's experimental so I didn't see much harm in changing it.

() =>
async ( { dispatch, resolveSelect } ) => {
const activeThemes = await resolveSelect.getEntityRecords(
Expand All @@ -468,8 +468,8 @@ export const __experimentalGetCurrentGlobalStylesId =
const globalStylesObject = await apiFetch( {
url: globalStylesURL,
} );
dispatch.__experimentalReceiveCurrentGlobalStylesId(
globalStylesObject.id
dispatch.__experimentalReceiveCurrentGlobalStyles(
globalStylesObject
);
}
};
Expand Down Expand Up @@ -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 } ) => {
Expand Down
Loading