From e9a685115ec7173a9a66e4a58a4507618ee2e3ec Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 26 Apr 2023 11:30:45 +1000 Subject: [PATCH 01/15] initial commit. --- .../src/components/revisions/index.js | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/edit-site/src/components/revisions/index.js diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js new file mode 100644 index 0000000000000..8967f32423d0e --- /dev/null +++ b/packages/edit-site/src/components/revisions/index.js @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import { isEmpty } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Disabled } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + BlockList, + privateApis as blockEditorPrivateApis, + store as blockEditorStore, + __unstableEditorStyles as EditorStyles, + __unstableIframe as Iframe, +} from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { + useFocusOnMount, + useFocusReturn, +} from '@wordpress/compose'; +import { useMemo } from '@wordpress/element'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ + +import { unlock } from '../../private-apis'; +import { mergeBaseAndUserConfigs } from '../global-styles/global-styles-provider'; +import EditorCanvasContainer from '../editor-canvas'; + +const { + ExperimentalBlockEditorProvider, + useGlobalStyle, + useGlobalStylesOutput, +} = unlock( blockEditorPrivateApis ); + +function Revisions( { onClose, userConfig, blocks } ) { + const focusOnMountRef = useFocusOnMount( 'firstElement' ); + const sectionFocusReturnRef = useFocusReturn(); + const [ textColor ] = useGlobalStyle( 'color.text' ); + const [ backgroundColor ] = useGlobalStyle( 'color.background' ); + + const { baseConfig } = useSelect( + ( select ) => ( { + baseConfig: + select( + coreStore + ).__experimentalGetCurrentThemeBaseGlobalStyles(), + } ), + [] + ); + + const mergedConfig = useMemo( () => { + if ( ! isEmpty( userConfig ) && ! isEmpty( baseConfig ) ) { + return mergeBaseAndUserConfigs( baseConfig, userConfig ); + } + return null; + }, [ baseConfig, userConfig ] ); + + + // BLOCKS + /* + const renderedBlocks = useSelect( + ( select ) => select( blockEditorStore ).getBlocks(), + [] + ); + + const renderedBlocksArray = useMemo( + () => + Array.isArray( renderedBlocks ) + ? renderedBlocks + : [ renderedBlocks ], + [ renderedBlocks ] + ); + + */ + + const originalSettings = useSelect( + ( select ) => select( blockEditorStore ).getSettings(), + [] + ); + const settings = useMemo( + () => ( { ...originalSettings, __unstableIsPreviewMode: true } ), + [ originalSettings ] + ); + + const [ globalStyles ] = useGlobalStylesOutput( mergedConfig ); + const editorStyles = + ! isEmpty( globalStyles ) && ! isEmpty( globalStylesRevision ) + ? globalStyles + : settings.styles; + + return ( + + + + ); +} + +export default Revisions; From af6e10065a253bfa14602f268f6632edac3712c1 Mon Sep 17 00:00:00 2001 From: ramon Date: Wed, 26 Apr 2023 14:28:28 +1000 Subject: [PATCH 02/15] Adding state for revisions Adding revisions global styles sidebar UI Adding revisions fill Adding revisions components and styles. Added e2e tests --- .../src/components/global-styles/index.js | 1 + .../components/global-styles/test/utils.js | 57 ++- .../global-styles/use-global-styles-output.js | 5 +- .../src/components/global-styles/utils.js | 27 ++ packages/core-data/src/actions.js | 22 ++ packages/core-data/src/reducer.js | 21 ++ packages/core-data/src/resolvers.js | 46 ++- packages/core-data/src/selectors.ts | 21 ++ .../src/request-utils/index.ts | 12 +- .../src/request-utils/themes.ts | 52 ++- .../editor-canvas-container/index.js | 2 + .../global-styles/screen-revisions.js | 345 ++++++++++++++++++ .../style-variations-container.js | 14 +- .../src/components/global-styles/style.scss | 54 +++ .../src/components/global-styles/ui.js | 36 +- .../src/components/revisions/index.js | 53 +-- .../user-global-styles-revisions.spec.js | 124 +++++++ 17 files changed, 836 insertions(+), 56 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/screen-revisions.js create mode 100644 test/e2e/specs/site-editor/user-global-styles-revisions.spec.js diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index 7e41c11c507e7..45c2b3b11ef2d 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -20,3 +20,4 @@ export { default as ColorPanel, useHasColorPanel } from './color-panel'; export { default as EffectsPanel, useHasEffectsPanel } from './effects-panel'; export { default as FiltersPanel, useHasFiltersPanel } from './filters-panel'; export { default as AdvancedPanel } from './advanced-panel'; +export { isGlobalStyleConfigEqual } from './utils'; diff --git a/packages/block-editor/src/components/global-styles/test/utils.js b/packages/block-editor/src/components/global-styles/test/utils.js index 7d0e3464557f6..1b1cc4e12703c 100644 --- a/packages/block-editor/src/components/global-styles/test/utils.js +++ b/packages/block-editor/src/components/global-styles/test/utils.js @@ -1,7 +1,11 @@ /** * Internal dependencies */ -import { getPresetVariableFromValue, getValueFromVariable } from '../utils'; +import { + isGlobalStyleConfigEqual, + getPresetVariableFromValue, + getValueFromVariable, +} 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/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 8777869d43621..172a06a093c64 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -1106,8 +1106,11 @@ const processCSSNesting = ( css, blockSelector ) => { return processedCSS; }; -export function useGlobalStylesOutput() { +export function useGlobalStylesOutput( customMergedConfig = null ) { let { merged: mergedConfig } = useContext( GlobalStylesContext ); + if ( !! customMergedConfig ) { + mergedConfig = customMergedConfig; + } const [ blockGap ] = useGlobalSetting( 'spacing.blockGap' ); const hasBlockGapSupport = blockGap !== null; diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index d1abd0a57dbc2..3610b4f82b5a3 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/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 @@ -376,3 +377,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 ) + ); +} diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index bd6839c8b8afb..3ffba340c5fbd 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -210,6 +210,28 @@ export function receiveThemeSupports() { }; } +/** + * 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 preview data for * a given URl has been received. diff --git a/packages/core-data/src/reducer.js b/packages/core-data/src/reducer.js index 28ef468a487e7..f04d543919b8c 100644 --- a/packages/core-data/src/reducer.js +++ b/packages/core-data/src/reducer.js @@ -651,6 +651,26 @@ export function navigationFallbackId( state = null, 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; +} + export default combineReducers( { terms, users, @@ -659,6 +679,7 @@ export default combineReducers( { currentUser, themeGlobalStyleVariations, themeBaseGlobalStyles, + themeGlobalStyleRevisions, taxonomies, entities, undo, diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 41f95b7d357c1..8bf38d437a4ca 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -180,7 +180,7 @@ export const getEntityRecords = let records = Object.values( await apiFetch( { path } ) ); // If we request fields but the result doesn't contain the fields, - // explicitely set these fields as "undefined" + // explicitly set these fields as "undefined" // that way we consider the query "fullfilled". if ( query._fields ) { records = records.map( ( record ) => { @@ -500,6 +500,50 @@ export const __experimentalGetCurrentThemeGlobalStylesVariations = ); }; +export const __experimentalGetCurrentThemeGlobalStylesRevisions = + () => + async ( { resolveSelect, dispatch } ) => { + const globalStylesId = + await resolveSelect.__experimentalGetCurrentGlobalStylesId(); + const record = globalStylesId + ? await resolveSelect.getEntityRecord( + 'root', + 'globalStyles', + globalStylesId + ) + : undefined; + const revisionsURL = record?._links?.[ 'version-history' ]?.[ 0 ]?.href; + + if ( revisionsURL ) { + const resetRevisions = await apiFetch( { + url: revisionsURL, + } ); + const revisions = resetRevisions?.map( ( revision ) => + Object.fromEntries( + Object.entries( revision ).map( ( [ key, value ] ) => [ + camelCase( key ), + value, + ] ) + ) + ); + dispatch.__experimentalReceiveThemeGlobalStyleRevisions( + globalStylesId, + 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 ebf803c8055ab..d2a00244e2f34 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -38,6 +38,7 @@ export interface State { entities: EntitiesState; themeBaseGlobalStyles: Record< string, Object >; themeGlobalStyleVariations: Record< string, string >; + themeGlobalStyleRevisions: Record< number, Object >; undo: UndoState; userPermissions: Record< string, boolean >; users: UserState; @@ -1247,3 +1248,23 @@ export function getNavigationFallbackId( ): EntityRecordKey | undefined { return state.navigationFallbackId; } + +/** + * Returns the revisions of the current global styles theme. + * + * @param state Data state. + * + * @return The current global styles. + */ +export function __experimentalGetCurrentThemeGlobalStylesRevisions( + state: State +): Object | null { + const currentGlobalStylesId = + __experimentalGetCurrentGlobalStylesId( state ); + + if ( ! currentGlobalStylesId ) { + return null; + } + + return state.themeGlobalStyleRevisions[ currentGlobalStylesId ]; +} diff --git a/packages/e2e-test-utils-playwright/src/request-utils/index.ts b/packages/e2e-test-utils-playwright/src/request-utils/index.ts index e05e59bacc673..99a11bf995679 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/index.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/index.ts @@ -17,7 +17,11 @@ import { createUser, deleteAllUsers } from './users'; import { setupRest, rest, getMaxBatchSize, batchRest } from './rest'; import { getPluginsMap, activatePlugin, deactivatePlugin } from './plugins'; import { deleteAllTemplates } from './templates'; -import { activateTheme } from './themes'; +import { + activateTheme, + getCurrentThemeGlobalStylesPostId, + getThemeGlobalStylesRevisions, +} from './themes'; import { deleteAllBlocks } from './blocks'; import { createComment, deleteAllComments } from './comments'; import { createPost, deleteAllPosts } from './posts'; @@ -188,6 +192,12 @@ class RequestUtils { deleteAllPages: typeof deleteAllPages = deleteAllPages.bind( this ); /** @borrows createPage as this.createPage */ createPage: typeof createPage = createPage.bind( this ); + /** @borrows getCurrentThemeGlobalStylesPostId as this.getCurrentThemeGlobalStylesPostId */ + getCurrentThemeGlobalStylesPostId: typeof getCurrentThemeGlobalStylesPostId = + getCurrentThemeGlobalStylesPostId.bind( this ); + /** @borrows getThemeGlobalStylesRevisions as this.getThemeGlobalStylesRevisions */ + getThemeGlobalStylesRevisions: typeof getThemeGlobalStylesRevisions = + getThemeGlobalStylesRevisions.bind( this ); } export type { StorageState }; diff --git a/packages/e2e-test-utils-playwright/src/request-utils/themes.ts b/packages/e2e-test-utils-playwright/src/request-utils/themes.ts index 802530d947b0f..6b4fd17588737 100644 --- a/packages/e2e-test-utils-playwright/src/request-utils/themes.ts +++ b/packages/e2e-test-utils-playwright/src/request-utils/themes.ts @@ -37,4 +37,54 @@ async function activateTheme( await response.dispose(); } -export { activateTheme }; +// https://developer.wordpress.org/rest-api/reference/themes/#definition +async function getCurrentThemeGlobalStylesPostId( this: RequestUtils ) { + type ThemeItem = { + stylesheet: string; + status: string; + _links: { 'wp:user-global-styles': { href: string }[] }; + }; + const themes = await this.rest< ThemeItem[] >( { + path: '/wp/v2/themes', + } ); + let themeGlobalStylesId: string = ''; + if ( themes && themes.length ) { + const currentTheme: ThemeItem | undefined = themes.find( + ( { status } ) => status === 'active' + ); + + const globalStylesURL = + currentTheme?._links?.[ 'wp:user-global-styles' ]?.[ 0 ]?.href; + if ( globalStylesURL ) { + themeGlobalStylesId = globalStylesURL?.split( + 'rest_route=/wp/v2/global-styles/' + )[ 1 ]; + } + } + return themeGlobalStylesId; +} + +/** + * Deletes all post revisions using the REST API. + * + * @param {} this RequestUtils. + * @param {string|number} parentId Post attributes. + */ +async function getThemeGlobalStylesRevisions( + this: RequestUtils, + parentId: number | string +) { + // Lists all global styles revisions. + return await this.rest< Record< string, Object >[] >( { + path: `/wp/v2/global-styles/${ parentId }/revisions`, + params: { + per_page: 100, + }, + } ); +} + +export { + activateTheme, + getCurrentThemeGlobalStylesPostId, + getThemeGlobalStylesRevisions, +}; diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js index c8fcca4a3a99c..50543331da419 100644 --- a/packages/edit-site/src/components/editor-canvas-container/index.js +++ b/packages/edit-site/src/components/editor-canvas-container/index.js @@ -30,6 +30,8 @@ function getEditorCanvasContainerTitle( view ) { switch ( view ) { case 'style-book': return __( 'Style Book' ); + case 'global-styles-revisions': + return __( 'Global styles revisions' ); default: return ''; } 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..f6a9d608de1f0 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-revisions.js @@ -0,0 +1,345 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + __experimentalVStack as VStack, + Button, + SelectControl, + __experimentalUseNavigator as useNavigator, +} from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { + useContext, + useCallback, + useState, + useEffect, + useMemo, +} from '@wordpress/element'; +import { + privateApis as blockEditorPrivateApis, + store as blockEditorStore, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import ScreenHeader from './header'; +import Subtitle from './subtitle'; +import { decodeEntities } from '@wordpress/html-entities'; +import { unlock } from '../../private-apis'; +import Revisions from '../revisions'; +import { store as editSiteStore } from '../../store'; + +const SELECTOR_MINIMUM_REVISION_COUNT = 10; +const { GlobalStylesContext, isGlobalStyleConfigEqual } = unlock( + blockEditorPrivateApis +); + +function RevisionsSelect( { userRevisions, currentRevisionId, onChange } ) { + const userRevisionsOptions = useMemo( () => { + return ( userRevisions ?? [] ).map( ( revision, index ) => { + const { id, dateDisplay, authorDisplayName } = revision; + const isLatest = 0 === index; + const revisionTitle = decodeEntities( dateDisplay ); + return { + value: id, + label: isLatest + ? sprintf( + /* translators: %(name)s author display name, %(date)s: human-friendly revision creation date */ + __( 'Current revision by %(name)s from %(date)s)' ), + { + name: authorDisplayName, + date: revisionTitle, + } + ) + : sprintf( + /* translators: %(name)s author display name, %(date)s: human-friendly revision creation date */ + __( 'Revision by %(name)s from %(date)s' ), + { + name: authorDisplayName, + date: revisionTitle, + } + ), + }; + } ); + }, [ userRevisions ] ); + const setCurrentRevisionId = ( value ) => { + const revisionId = Number( value ); + onChange( + userRevisions.find( ( revision ) => revision.id === revisionId ) + ); + }; + return ( + + ); +} + +function RevisionsButtons( { userRevisions, currentRevisionId, onChange } ) { + return ( + <> + { __( 'Styles revisions' ) } + +
    + { userRevisions.map( ( revision, index ) => { + const { + id, + dateDisplay, + authorAvatarUrl, + authorDisplayName, + } = revision; + const isLatest = 0 === index; + const isActive = id === currentRevisionId; + const revisionTitle = decodeEntities( dateDisplay ); + + return ( +
  1. + +
  2. + ); + } ) } +
+ + ); +} + +function ScreenRevisions() { + const { goBack } = useNavigator(); + const { user: userConfig, setUserConfig } = + useContext( GlobalStylesContext ); + const { blocks, userRevisions, isDirty, editorCanvasContainerView } = + useSelect( ( select ) => { + const { + __experimentalGetDirtyEntityRecords, + isSavingEntityRecord, + } = select( coreStore ); + const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); + + return { + isDirty: dirtyEntityRecords.length > 0, + isSaving: dirtyEntityRecords.some( ( record ) => + isSavingEntityRecord( record.kind, record.name, record.key ) + ), + userRevisions: + select( + coreStore + ).__experimentalGetCurrentThemeGlobalStylesRevisions() || + [], + editorCanvasContainerView: unlock( + select( editSiteStore ) + ).getEditorCanvasContainerView(), + blocks: select( blockEditorStore ).getBlocks(), + }; + }, [] ); + + const [ globalStylesRevision, setGlobalStylesRevision ] = useState( {} ); + const [ currentRevisionId, setCurrentRevisionId ] = useState(); + const [ isRestoringRevision, setIsRestoringRevision ] = useState( false ); + + useEffect( () => { + let currentRevision = null; + for ( let i = 0; i < userRevisions.length; i++ ) { + if ( isGlobalStyleConfigEqual( userConfig, userRevisions[ i ] ) ) { + currentRevision = userRevisions[ i ]; + break; + } + } + setCurrentRevisionId( currentRevision?.id ); + }, [ userRevisions, userConfig ] ); + + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); + useEffect( () => { + if ( editorCanvasContainerView !== 'global-styles-revisions' ) { + goBack(); + setEditorCanvasContainerView( editorCanvasContainerView ); + } + }, [ editorCanvasContainerView ] ); + + const restoreRevision = useCallback( + ( revision ) => { + setUserConfig( () => ( { + styles: revision?.styles, + settings: revision?.settings, + } ) ); + onCloseRevisions(); + }, + [ userConfig ] + ); + + const onCloseRevisions = () => { + goBack(); + }; + + const selectRevision = ( revision ) => { + setGlobalStylesRevision( { + styles: revision?.styles, + settings: revision?.settings, + id: revision?.id, + } ); + setCurrentRevisionId( revision?.id ); + }; + + const RevisionsComponent = + userRevisions.length >= SELECTOR_MINIMUM_REVISION_COUNT + ? RevisionsSelect + : RevisionsButtons; + + return ( + <> + +
+ + { isRestoringRevision ? ( + <> + + + + ) : ( + <> + + + + ) } + +
+ + + ); +} + +export default ScreenRevisions; diff --git a/packages/edit-site/src/components/global-styles/style-variations-container.js b/packages/edit-site/src/components/global-styles/style-variations-container.js index 10d93a715405f..3d99c5302ab61 100644 --- a/packages/edit-site/src/components/global-styles/style-variations-container.js +++ b/packages/edit-site/src/components/global-styles/style-variations-container.js @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import fastDeepEqual from 'fast-deep-equal/es6'; /** * WordPress dependencies @@ -22,14 +21,9 @@ import { mergeBaseAndUserConfigs } from './global-styles-provider'; import StylesPreview from './preview'; import { unlock } from '../../private-apis'; -const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); - -function compareVariations( a, b ) { - return ( - fastDeepEqual( a.styles, b.styles ) && - fastDeepEqual( a.settings, b.settings ) - ); -} +const { GlobalStylesContext, isGlobalStyleConfigEqual } = unlock( + blockEditorPrivateApis +); function Variation( { variation } ) { const [ isFocused, setIsFocused ] = useState( false ); @@ -63,7 +57,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 87f0cfdac44d1..a75c0a277827a 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -176,3 +176,57 @@ .edit-site-global-styles-sidebar__panel .block-editor-block-icon svg { fill: currentColor; } + +.edit-site-global-styles-screen-revisions { + margin: $grid-unit-20; +} + +.edit-site-global-styles-screen-revisions__revisions-list { + list-style: none; + margin: 0; + li { + margin-bottom: $grid-unit-20; + } +} + +.edit-site-global-styles-screen-revisions__revision-item { + width: 100%; + height: auto; + &.is-current { + border: 1px solid $black; + } +} + +.edit-site-global-styles-screen-revisions__button { + justify-content: center; +} + +.edit-site-global-styles-screen-revisions__description { + display: grid; + grid-template-columns: max-content 1fr; + grid-template-rows: 1fr 1fr; + gap: $grid-unit-05 $grid-unit-10; + grid-auto-flow: row; + align-items: start; + justify-items: start; + grid-template-areas: + "edit-site-global-styles-screen-revisions__avatar ." + "edit-site-global-styles-screen-revisions__avatar ."; + .edit-site-global-styles-screen-revisions__avatar { + grid-area: edit-site-global-styles-screen-revisions__avatar; + width: 24px; + height: 24px; + position: relative; + overflow: hidden; + border-radius: 50%; + img { + display: inline; + margin: 0 auto; + height: 100%; + width: auto; + } + } + .edit-site-global-styles-screen-revisions__date { + font-size: 11px; + } +} diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index ebc2ae164aa41..133e8d61d1148 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -14,7 +14,7 @@ import { privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf, _n } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; import { moreVertical } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; @@ -37,6 +37,7 @@ import ScreenLayout from './screen-layout'; import ScreenStyleVariations from './screen-style-variations'; import StyleBook from '../style-book'; import ScreenCSS from './screen-css'; +import ScreenRevisions from './screen-revisions'; import { unlock } from '../../private-apis'; import { store as editSiteStore } from '../../store'; @@ -46,7 +47,7 @@ const { Slot: GlobalStylesMenuSlot, Fill: GlobalStylesMenuFill } = function GlobalStylesActionMenu() { const { toggle } = useDispatch( preferencesStore ); - const { canEditCSS } = useSelect( ( select ) => { + const { canEditCSS, revisionsCount } = useSelect( ( select ) => { const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = select( coreStore ); @@ -58,12 +59,23 @@ function GlobalStylesActionMenu() { return { canEditCSS: !! globalStyles?._links?.[ 'wp:action-edit-css' ] ?? false, + revisionsCount: + globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, }; }, [] ); const { useGlobalStylesReset } = unlock( blockEditorPrivateApis ); const [ canReset, onReset ] = useGlobalStylesReset(); const { goTo } = useNavigator(); + const { setEditorCanvasContainerView } = unlock( + useDispatch( editSiteStore ) + ); const loadCustomCSS = () => goTo( '/css' ); + const loadRevisions = () => { + goTo( '/revisions' ); + setEditorCanvasContainerView( 'global-styles-revisions' ); + }; + const hasRevisions = revisionsCount >= 2; + return ( @@ -267,6 +295,10 @@ function GlobalStylesUI() { + + + + { blocks.map( ( block ) => ( ( { baseConfig: @@ -60,23 +49,10 @@ function Revisions( { onClose, userConfig, blocks } ) { return null; }, [ baseConfig, userConfig ] ); - - // BLOCKS - /* - const renderedBlocks = useSelect( - ( select ) => select( blockEditorStore ).getBlocks(), - [] - ); - - const renderedBlocksArray = useMemo( - () => - Array.isArray( renderedBlocks ) - ? renderedBlocks - : [ renderedBlocks ], - [ renderedBlocks ] - ); - - */ + const renderedBlocksArray = useMemo( + () => ( Array.isArray( blocks ) ? blocks : [ blocks ] ), + [ blocks ] + ); const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), @@ -89,15 +65,15 @@ function Revisions( { onClose, userConfig, blocks } ) { const [ globalStyles ] = useGlobalStylesOutput( mergedConfig ); const editorStyles = - ! isEmpty( globalStyles ) && ! isEmpty( globalStylesRevision ) + ! isEmpty( globalStyles ) && ! isEmpty( userConfig ) ? globalStyles : settings.styles; return (