diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md
index 52fa92440b2653..63114073b1b802 100644
--- a/docs/contributors/code/coding-guidelines.md
+++ b/docs/contributors/code/coding-guidelines.md
@@ -720,7 +720,7 @@ function getConfigurationValue( key ) {
}
```
-When documenting a [function type](https://github.com/WordPress/gutenberg/blob/add/typescript-jsdoc-guidelines/docs/contributors/coding-guidelines.md#record-types), you must always include the `void` return value type, as otherwise the function is inferred to return a mixed ("any") value, not a void result.
+When documenting a [function type](#generic-types), you must always include the `void` return value type, as otherwise the function is inferred to return a mixed ("any") value, not a void result.
```js
/**
diff --git a/packages/base-styles/_default-custom-properties.scss b/packages/base-styles/_default-custom-properties.scss
index 5760753c48ce85..98bb9ae75ea674 100644
--- a/packages/base-styles/_default-custom-properties.scss
+++ b/packages/base-styles/_default-custom-properties.scss
@@ -5,5 +5,7 @@
@include admin-scheme(#007cba);
--wp-block-synced-color: #7a00df;
--wp-block-synced-color--rgb: #{hex-to-rgb(#7a00df)};
- --wp-bound-block-color: #9747ff;
+ // This CSS variable is not used in Gutenberg project,
+ // but is maintained for backwards compatibility.
+ --wp-bound-block-color: var(--wp-block-synced-color);
}
diff --git a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss
index f4fb768b6ec37a..e1ce8bac6064b9 100644
--- a/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss
+++ b/packages/block-editor/src/components/block-bindings-toolbar-indicator/style.scss
@@ -4,7 +4,7 @@
justify-content: center;
padding: 6px;
svg {
- fill: var(--wp-bound-block-color);
+ fill: var(--wp-block-synced-color);
}
}
diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss
index 869b565fec3694..77c94bbc2fad0f 100644
--- a/packages/block-editor/src/components/block-list/content.scss
+++ b/packages/block-editor/src/components/block-list/content.scss
@@ -300,31 +300,31 @@ _::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-b
}
}
-@keyframes block-editor-has-editable-outline__fade-out-animation {
+// Add fade in/out background for editable blocks in synced patterns.
+@keyframes block-editor-is-editable__animation {
from {
- border-color: rgba(var(--wp-admin-theme-color--rgb), 1);
+ background-color: rgba(var(--wp-admin-theme-color--rgb), 0.1);
}
to {
- border-color: rgba(var(--wp-admin-theme-color--rgb), 0);
+ background-color: rgba(var(--wp-admin-theme-color--rgb), 0);
}
}
-.is-root-container:not([inert]) .block-editor-block-list__block.has-editable-outline {
- &::after {
- content: "";
- position: absolute;
- pointer-events: none;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- border: 1px dotted rgba(var(--wp-admin-theme-color--rgb), 1);
- border-radius: $radius-block-ui;
- animation: block-editor-has-editable-outline__fade-out-animation 0.3s ease-out;
- animation-delay: 1s;
- animation-fill-mode: forwards;
- @include reduce-motion("animation");
- }
+.is-root-container:not([inert]) .block-editor-block-list__block.is-reusable.is-selected .block-editor-block-list__block.has-editable-outline::after {
+ animation-name: block-editor-is-editable__animation;
+ animation-duration: 0.8s;
+ animation-timing-function: ease-out;
+ animation-delay: 0.1s;
+ animation-fill-mode: backwards;
+ border-radius: $radius-block-ui;
+ bottom: 0;
+ content: "";
+ left: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ @include reduce-motion("animation");
}
// Spotlight mode. Fade out blocks unless they contain a selected block.
diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js
index c929c1014dc030..af98f24b98785e 100644
--- a/packages/block-editor/src/components/block-list/use-block-props/index.js
+++ b/packages/block-editor/src/components/block-list/use-block-props/index.js
@@ -130,7 +130,11 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) {
const hasBlockBindings = !! blockEditContext[ blockBindingsKey ];
const bindingsStyle =
hasBlockBindings && canBindBlock( name )
- ? { '--wp-admin-theme-color': 'var(--wp-bound-block-color)' }
+ ? {
+ '--wp-admin-theme-color': 'var(--wp-block-synced-color)',
+ '--wp-admin-theme-color--rgb':
+ 'var(--wp-block-synced-color--rgb)',
+ }
: {};
// Ensures it warns only inside the `edit` implementation for the block.
diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js
index e59e564da3e6a8..293825a521e599 100644
--- a/packages/block-editor/src/components/iframe/index.js
+++ b/packages/block-editor/src/components/iframe/index.js
@@ -269,29 +269,29 @@ function Iframe( {
useEffect( () => cleanup, [ cleanup ] );
+ scale =
+ typeof scale === 'function'
+ ? scale( contentWidth, contentHeight )
+ : scale;
+
useEffect( () => {
if ( ! iframeDocument ) {
return;
}
- const _scale =
- typeof scale === 'function'
- ? scale( contentWidth, contentHeight )
- : scale;
-
- if ( _scale !== 1 ) {
+ if ( scale !== 1 ) {
// Hack to get proper margins when scaling the iframe document.
- const bottomFrameSize = frameSize - contentHeight * ( 1 - _scale );
+ const bottomFrameSize = frameSize - contentHeight * ( 1 - scale );
iframeDocument.body.classList.add( 'is-zoomed-out' );
- iframeDocument.documentElement.style.transform = `scale( ${ _scale } )`;
+ iframeDocument.documentElement.style.transform = `scale( ${ scale } )`;
iframeDocument.documentElement.style.marginTop = `${ frameSize }px`;
// TODO: `marginBottom` doesn't work in Firefox. We need another way to do this.
iframeDocument.documentElement.style.marginBottom = `${ bottomFrameSize }px`;
- if ( iframeWindowInnerHeight > contentHeight * _scale ) {
+ if ( iframeWindowInnerHeight > contentHeight * scale ) {
iframeDocument.body.style.minHeight = `${ Math.floor(
- ( iframeWindowInnerHeight - 2 * frameSize ) / _scale
+ ( iframeWindowInnerHeight - 2 * frameSize ) / scale
) }px`;
}
diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss
index 17f2e79212a799..0c5e12cb88bdf4 100644
--- a/packages/block-library/src/template-part/editor.scss
+++ b/packages/block-library/src/template-part/editor.scss
@@ -23,8 +23,10 @@
z-index: z-index(".block-library-template-part__selection-search");
}
-.is-outline-mode .block-editor-block-list__block:not(.remove-outline).wp-block-template-part,
-.is-outline-mode .block-editor-block-list__block:not(.remove-outline).is-reusable {
+// Removed .is-outline-mode so colors take effect properly in the block editor.
+// Will be a better result when outlines are not shadows, but outlines, so we can target outline-color, not redefined the entire shadow.
+.block-editor-block-list__block:not(.remove-outline).wp-block-template-part,
+.block-editor-block-list__block:not(.remove-outline).is-reusable {
&.is-highlighted::after,
&.is-selected::after {
box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-block-synced-color);
diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js
index 0c38d369e54f71..dce6f748b651d6 100644
--- a/packages/edit-site/src/components/page-pages/index.js
+++ b/packages/edit-site/src/components/page-pages/index.js
@@ -189,6 +189,16 @@ function FeaturedImage( { item, viewType } ) {
);
}
+const PAGE_ACTIONS = [
+ 'edit-post',
+ 'view-post',
+ 'restore',
+ 'permanently-delete',
+ 'view-post-revisions',
+ 'rename-post',
+ 'move-to-trash',
+];
+
export default function PagePages() {
const postType = 'page';
const [ view, setView ] = useView( postType );
@@ -353,7 +363,7 @@ export default function PagePages() {
},
[ history ]
);
- const actions = usePostActions( onActionPerformed );
+ const actions = usePostActions( onActionPerformed, PAGE_ACTIONS );
const onChangeView = useCallback(
( newView ) => {
if ( newView.type !== view.type ) {
diff --git a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js
index 9030b3bbc73c70..50baed5658e922 100644
--- a/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js
+++ b/packages/edit-site/src/components/page-patterns/dataviews-pattern-actions.js
@@ -23,6 +23,7 @@ import { useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { decodeEntities } from '@wordpress/html-entities';
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
+import { store as editorStore } from '@wordpress/editor';
import { privateApis as routerPrivateApis } from '@wordpress/router';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
@@ -198,7 +199,7 @@ export const deleteAction = {
useDispatch( reusableBlocksStore );
const { createErrorNotice, createSuccessNotice } =
useDispatch( noticesStore );
- const { removeTemplates } = unlock( useDispatch( editSiteStore ) );
+ const { removeTemplates } = unlock( useDispatch( editorStore ) );
const deletePattern = async () => {
const promiseResult = await Promise.allSettled(
diff --git a/packages/edit-site/src/components/page-templates-template-parts/actions.js b/packages/edit-site/src/components/page-templates-template-parts/actions.js
deleted file mode 100644
index d4038b5efeb58a..00000000000000
--- a/packages/edit-site/src/components/page-templates-template-parts/actions.js
+++ /dev/null
@@ -1,275 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __, sprintf, _n } from '@wordpress/i18n';
-import { useDispatch } from '@wordpress/data';
-import { useState } from '@wordpress/element';
-import { store as coreStore } from '@wordpress/core-data';
-import { store as noticesStore } from '@wordpress/notices';
-import { decodeEntities } from '@wordpress/html-entities';
-import {
- Button,
- TextControl,
- __experimentalText as Text,
- __experimentalHStack as HStack,
- __experimentalVStack as VStack,
-} from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { unlock } from '../../lock-unlock';
-import { store as editSiteStore } from '../../store';
-import isTemplateRevertable from '../../utils/is-template-revertable';
-import isTemplateRemovable from '../../utils/is-template-removable';
-import { TEMPLATE_POST_TYPE } from '../../utils/constants';
-
-export const resetTemplateAction = {
- id: 'reset-template',
- label: __( 'Reset' ),
- isEligible: isTemplateRevertable,
- supportsBulk: true,
- hideModalHeader: true,
- RenderModal: ( { items, closeModal, onPerform } ) => {
- const { revertTemplate } = useDispatch( editSiteStore );
- const { saveEditedEntityRecord } = useDispatch( coreStore );
- const { createSuccessNotice, createErrorNotice } =
- useDispatch( noticesStore );
- const onConfirm = async () => {
- try {
- for ( const template of items ) {
- await revertTemplate( template, {
- allowUndo: false,
- } );
- await saveEditedEntityRecord(
- 'postType',
- template.type,
- template.id
- );
- }
-
- createSuccessNotice(
- items.length > 1
- ? sprintf(
- /* translators: The number of items. */
- __( '%s items reset.' ),
- items.length
- )
- : sprintf(
- /* translators: The template/part's name. */
- __( '"%s" reset.' ),
- decodeEntities( items[ 0 ].title.rendered )
- ),
- {
- type: 'snackbar',
- id: 'edit-site-template-reverted',
- }
- );
- } catch ( error ) {
- let fallbackErrorMessage;
- if ( items[ 0 ].type === TEMPLATE_POST_TYPE ) {
- fallbackErrorMessage =
- items.length === 1
- ? __(
- 'An error occurred while reverting the template.'
- )
- : __(
- 'An error occurred while reverting the templates.'
- );
- } else {
- fallbackErrorMessage =
- items.length === 1
- ? __(
- 'An error occurred while reverting the template part.'
- )
- : __(
- 'An error occurred while reverting the template parts.'
- );
- }
- const errorMessage =
- error.message && error.code !== 'unknown_error'
- ? error.message
- : fallbackErrorMessage;
-
- createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
- };
- return (
-
-
- { __( 'Reset to default and clear all customizations?' ) }
-
-
-
-
-
-
- );
- },
-};
-
-export const deleteTemplateAction = {
- id: 'delete-template',
- label: __( 'Delete' ),
- isEligible: isTemplateRemovable,
- supportsBulk: true,
- hideModalHeader: true,
- RenderModal: ( { items: templates, closeModal, onPerform } ) => {
- const { removeTemplates } = unlock( useDispatch( editSiteStore ) );
- return (
-
-
- { templates.length > 1
- ? sprintf(
- // translators: %d: number of items to delete.
- _n(
- 'Delete %d item?',
- 'Delete %d items?',
- templates.length
- ),
- templates.length
- )
- : sprintf(
- // translators: %s: The template or template part's titles
- __( 'Delete "%s"?' ),
- decodeEntities(
- templates?.[ 0 ]?.title?.rendered
- )
- ) }
-
-
-
-
-
-
- );
- },
-};
-
-export const renameTemplateAction = {
- id: 'rename-template',
- label: __( 'Rename' ),
- isEligible: ( template ) => {
- // We can only remove templates or template parts that can be removed.
- // Additionally in the case of templates, we can only remove custom templates.
- if (
- ! isTemplateRemovable( template ) ||
- ( template.type === TEMPLATE_POST_TYPE && ! template.is_custom )
- ) {
- return false;
- }
- return true;
- },
- RenderModal: ( { items: templates, closeModal } ) => {
- const template = templates[ 0 ];
- const title = decodeEntities( template.title.rendered );
- const [ editedTitle, setEditedTitle ] = useState( title );
- const {
- editEntityRecord,
- __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits,
- } = useDispatch( coreStore );
- const { createSuccessNotice, createErrorNotice } =
- useDispatch( noticesStore );
- async function onTemplateRename( event ) {
- event.preventDefault();
- try {
- await editEntityRecord(
- 'postType',
- template.type,
- template.id,
- {
- title: editedTitle,
- }
- );
- // Update state before saving rerenders the list.
- setEditedTitle( '' );
- closeModal();
- // Persist edited entity.
- await saveSpecifiedEntityEdits(
- 'postType',
- template.type,
- template.id,
- [ 'title' ], // Only save title to avoid persisting other edits.
- {
- throwOnError: true,
- }
- );
- createSuccessNotice(
- template.type === TEMPLATE_POST_TYPE
- ? __( 'Template renamed.' )
- : __( 'Template part renamed.' ),
- {
- type: 'snackbar',
- }
- );
- } catch ( error ) {
- const fallbackErrorMessage =
- template.type === TEMPLATE_POST_TYPE
- ? __( 'An error occurred while renaming the template.' )
- : __(
- 'An error occurred while renaming the template part.'
- );
- const errorMessage =
- error.message && error.code !== 'unknown_error'
- ? error.message
- : fallbackErrorMessage;
-
- createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
- }
- return (
-
- );
- },
-};
diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js
index f12306f6727ce5..598637f98b8115 100644
--- a/packages/edit-site/src/components/page-templates-template-parts/index.js
+++ b/packages/edit-site/src/components/page-templates-template-parts/index.js
@@ -42,11 +42,7 @@ import {
LAYOUT_TABLE,
LAYOUT_LIST,
} from '../../utils/constants';
-import {
- resetTemplateAction,
- deleteTemplateAction,
- renameTemplateAction,
-} from './actions';
+
import usePatternSettings from '../page-patterns/use-pattern-settings';
import { unlock } from '../../lock-unlock';
import AddNewTemplatePart from './add-new-template-part';
@@ -201,6 +197,14 @@ function Preview( { item, viewType } ) {
);
}
+const TEMPLATE_ACTIONS = [
+ 'edit-post',
+ 'reset-template',
+ 'rename-template',
+ 'view-post-revisions',
+ 'delete-template',
+];
+
export default function PageTemplatesTemplateParts( { postType } ) {
const { params } = useLocation();
const { activeView = 'all', layout } = params;
@@ -361,20 +365,8 @@ export default function PageTemplatesTemplateParts( { postType } ) {
},
[ history ]
);
- const [ editAction, viewRevisionsAction ] = usePostActions(
- onActionPerformed,
- [ 'edit-post', 'view-post-revisions' ]
- );
- const actions = useMemo(
- () => [
- editAction,
- resetTemplateAction,
- renameTemplateAction,
- viewRevisionsAction,
- deleteTemplateAction,
- ],
- [ editAction, viewRevisionsAction ]
- );
+
+ const actions = usePostActions( onActionPerformed, TEMPLATE_ACTIONS );
const onChangeView = useCallback(
( newView ) => {
diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js
index dfe8f81ca21ccf..2806568d627c84 100644
--- a/packages/edit-site/src/store/actions.js
+++ b/packages/edit-site/src/store/actions.js
@@ -1,12 +1,8 @@
/**
* WordPress dependencies
*/
-import apiFetch from '@wordpress/api-fetch';
-import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
+import { parse } from '@wordpress/blocks';
import deprecated from '@wordpress/deprecated';
-import { addQueryArgs } from '@wordpress/url';
-import { __ } from '@wordpress/i18n';
-import { store as noticesStore } from '@wordpress/notices';
import { store as coreStore } from '@wordpress/core-data';
import { store as interfaceStore } from '@wordpress/interface';
import { store as blockEditorStore } from '@wordpress/block-editor';
@@ -17,13 +13,12 @@ import { store as preferencesStore } from '@wordpress/preferences';
* Internal dependencies
*/
import { STORE_NAME as editSiteStoreName } from './constants';
-import isTemplateRevertable from '../utils/is-template-revertable';
import {
TEMPLATE_POST_TYPE,
TEMPLATE_PART_POST_TYPE,
NAVIGATION_POST_TYPE,
} from '../utils/constants';
-import { removeTemplates } from './private-actions';
+import { unlock } from '../lock-unlock';
/**
* Dispatches an action that toggles a feature flag.
@@ -133,9 +128,13 @@ export const addTemplate =
*
* @param {Object} template The template object.
*/
-export const removeTemplate = ( template ) => {
- return removeTemplates( [ template ] );
-};
+export const removeTemplate =
+ ( template ) =>
+ ( { registry } ) => {
+ return unlock( registry.dispatch( editorStore ) ).removeTemplates( [
+ template,
+ ] );
+ };
/**
* Action that sets a template part.
@@ -345,130 +344,14 @@ export function setIsSaveViewOpened( isOpen ) {
* reverting the template. Default true.
*/
export const revertTemplate =
- ( template, { allowUndo = true } = {} ) =>
- async ( { registry } ) => {
- const noticeId = 'edit-site-template-reverted';
- registry.dispatch( noticesStore ).removeNotice( noticeId );
- if ( ! isTemplateRevertable( template ) ) {
- registry
- .dispatch( noticesStore )
- .createErrorNotice( __( 'This template is not revertable.' ), {
- type: 'snackbar',
- } );
- return;
- }
-
- try {
- const templateEntityConfig = registry
- .select( coreStore )
- .getEntityConfig( 'postType', template.type );
-
- if ( ! templateEntityConfig ) {
- registry
- .dispatch( noticesStore )
- .createErrorNotice(
- __(
- 'The editor has encountered an unexpected error. Please reload.'
- ),
- { type: 'snackbar' }
- );
- return;
- }
-
- const fileTemplatePath = addQueryArgs(
- `${ templateEntityConfig.baseURL }/${ template.id }`,
- { context: 'edit', source: 'theme' }
- );
-
- const fileTemplate = await apiFetch( { path: fileTemplatePath } );
- if ( ! fileTemplate ) {
- registry
- .dispatch( noticesStore )
- .createErrorNotice(
- __(
- 'The editor has encountered an unexpected error. Please reload.'
- ),
- { type: 'snackbar' }
- );
- return;
- }
-
- const serializeBlocks = ( {
- blocks: blocksForSerialization = [],
- } ) => __unstableSerializeAndClean( blocksForSerialization );
-
- const edited = registry
- .select( coreStore )
- .getEditedEntityRecord(
- 'postType',
- template.type,
- template.id
- );
-
- // We are fixing up the undo level here to make sure we can undo
- // the revert in the header toolbar correctly.
- registry.dispatch( coreStore ).editEntityRecord(
- 'postType',
- template.type,
- template.id,
- {
- content: serializeBlocks, // Required to make the `undo` behave correctly.
- blocks: edited.blocks, // Required to revert the blocks in the editor.
- source: 'custom', // required to avoid turning the editor into a dirty state
- },
- {
- undoIgnore: true, // Required to merge this edit with the last undo level.
- }
- );
-
- const blocks = parse( fileTemplate?.content?.raw );
- registry
- .dispatch( coreStore )
- .editEntityRecord( 'postType', template.type, fileTemplate.id, {
- content: serializeBlocks,
- blocks,
- source: 'theme',
- } );
-
- if ( allowUndo ) {
- const undoRevert = () => {
- registry
- .dispatch( coreStore )
- .editEntityRecord(
- 'postType',
- template.type,
- edited.id,
- {
- content: serializeBlocks,
- blocks: edited.blocks,
- source: 'custom',
- }
- );
- };
-
- registry
- .dispatch( noticesStore )
- .createSuccessNotice( __( 'Template reset.' ), {
- type: 'snackbar',
- id: noticeId,
- actions: [
- {
- label: __( 'Undo' ),
- onClick: undoRevert,
- },
- ],
- } );
- }
- } catch ( error ) {
- const errorMessage =
- error.message && error.code !== 'unknown_error'
- ? error.message
- : __( 'Template revert failed. Please reload.' );
- registry
- .dispatch( noticesStore )
- .createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
+ ( template, options ) =>
+ ( { registry } ) => {
+ return unlock( registry.dispatch( editorStore ) ).revertTemplate(
+ template,
+ options
+ );
};
+
/**
* Action that opens an editor sidebar.
*
diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js
index fd23903a6a05e4..0aaee3def948c1 100644
--- a/packages/edit-site/src/store/private-actions.js
+++ b/packages/edit-site/src/store/private-actions.js
@@ -4,15 +4,6 @@
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as preferencesStore } from '@wordpress/preferences';
import { store as editorStore } from '@wordpress/editor';
-import { store as coreStore } from '@wordpress/core-data';
-import { store as noticesStore } from '@wordpress/notices';
-import { __, sprintf } from '@wordpress/i18n';
-import { decodeEntities } from '@wordpress/html-entities';
-
-/**
- * Internal dependencies
- */
-import { TEMPLATE_POST_TYPE } from '../utils/constants';
/**
* Action that switches the canvas mode.
@@ -62,127 +53,3 @@ export const setEditorCanvasContainerView =
view,
} );
};
-
-/**
- * Action that removes an array of templates.
- *
- * @param {Array} items An array of template or template part objects to remove.
- */
-export const removeTemplates =
- ( items ) =>
- async ( { registry } ) => {
- const isTemplate = items[ 0 ].type === TEMPLATE_POST_TYPE;
- const promiseResult = await Promise.allSettled(
- items.map( ( item ) => {
- return registry
- .dispatch( coreStore )
- .deleteEntityRecord(
- 'postType',
- item.type,
- item.id,
- { force: true },
- { throwOnError: true }
- );
- } )
- );
-
- // If all the promises were fulfilled with sucess.
- if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) {
- let successMessage;
-
- if ( items.length === 1 ) {
- // Depending on how the entity was retrieved its title might be
- // an object or simple string.
- const title =
- typeof items[ 0 ].title === 'string'
- ? items[ 0 ].title
- : items[ 0 ].title?.rendered;
- successMessage = sprintf(
- /* translators: The template/part's name. */
- __( '"%s" deleted.' ),
- decodeEntities( title )
- );
- } else {
- successMessage = isTemplate
- ? __( 'Templates deleted.' )
- : __( 'Template parts deleted.' );
- }
-
- registry
- .dispatch( noticesStore )
- .createSuccessNotice( successMessage, {
- type: 'snackbar',
- id: 'site-editor-template-deleted-success',
- } );
- } else {
- // If there was at lease one failure.
- let errorMessage;
- // If we were trying to delete a single template.
- if ( promiseResult.length === 1 ) {
- if ( promiseResult[ 0 ].reason?.message ) {
- errorMessage = promiseResult[ 0 ].reason.message;
- } else {
- errorMessage = isTemplate
- ? __( 'An error occurred while deleting the template.' )
- : __(
- 'An error occurred while deleting the template part.'
- );
- }
- // If we were trying to delete a multiple templates
- } else {
- const errorMessages = new Set();
- const failedPromises = promiseResult.filter(
- ( { status } ) => status === 'rejected'
- );
- for ( const failedPromise of failedPromises ) {
- if ( failedPromise.reason?.message ) {
- errorMessages.add( failedPromise.reason.message );
- }
- }
- if ( errorMessages.size === 0 ) {
- errorMessage = isTemplate
- ? __(
- 'An error occurred while deleting the templates.'
- )
- : __(
- 'An error occurred while deleting the template parts.'
- );
- } else if ( errorMessages.size === 1 ) {
- errorMessage = isTemplate
- ? sprintf(
- /* translators: %s: an error message */
- __(
- 'An error occurred while deleting the templates: %s'
- ),
- [ ...errorMessages ][ 0 ]
- )
- : sprintf(
- /* translators: %s: an error message */
- __(
- 'An error occurred while deleting the template parts: %s'
- ),
- [ ...errorMessages ][ 0 ]
- );
- } else {
- errorMessage = isTemplate
- ? sprintf(
- /* translators: %s: a list of comma separated error messages */
- __(
- 'Some errors occurred while deleting the templates: %s'
- ),
- [ ...errorMessages ].join( ',' )
- )
- : sprintf(
- /* translators: %s: a list of comma separated error messages */
- __(
- 'Some errors occurred while deleting the template parts: %s'
- ),
- [ ...errorMessages ].join( ',' )
- );
- }
- }
- registry
- .dispatch( noticesStore )
- .createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
- };
diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js
index 42ebf3561305c4..8dded54a6a7564 100644
--- a/packages/editor/src/components/post-actions/actions.js
+++ b/packages/editor/src/components/post-actions/actions.js
@@ -18,6 +18,14 @@ import {
__experimentalVStack as VStack,
} from '@wordpress/components';
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_ORIGINS, TEMPLATE_POST_TYPE } from '../../store/constants';
+import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
+import isTemplateRevertable from '../../store/utils/is-template-revertable';
+
function getItemTitle( item ) {
if ( typeof item.title === 'string' ) {
return decodeEntities( item.title );
@@ -25,7 +33,7 @@ function getItemTitle( item ) {
return decodeEntities( item.title?.rendered || '' );
}
-export const trashPostAction = {
+const trashPostAction = {
id: 'move-to-trash',
label: __( 'Move to Trash' ),
isPrimary: true,
@@ -164,7 +172,7 @@ export const trashPostAction = {
},
};
-export function usePermanentlyDeletePostAction() {
+function usePermanentlyDeletePostAction() {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { deleteEntityRecord } = useDispatch( coreStore );
@@ -271,7 +279,7 @@ export function usePermanentlyDeletePostAction() {
);
}
-export function useRestorePostAction() {
+function useRestorePostAction() {
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { editEntityRecord, saveEditedEntityRecord } =
@@ -357,7 +365,7 @@ export function useRestorePostAction() {
);
}
-export const viewPostAction = {
+const viewPostAction = {
id: 'view-post',
label: __( 'View' ),
isPrimary: true,
@@ -374,7 +382,7 @@ export const viewPostAction = {
},
};
-export const editPostAction = {
+const editPostAction = {
id: 'edit-post',
label: __( 'Edit' ),
isPrimary: true,
@@ -388,7 +396,7 @@ export const editPostAction = {
}
},
};
-export const postRevisionsAction = {
+const postRevisionsAction = {
id: 'view-post-revisions',
label: __( 'View revisions' ),
isPrimary: false,
@@ -414,7 +422,7 @@ export const postRevisionsAction = {
},
};
-export const renamePostAction = {
+const renamePostAction = {
id: 'rename-post',
label: __( 'Rename' ),
isEligible( post ) {
@@ -491,6 +499,273 @@ export const renamePostAction = {
},
};
+const resetTemplateAction = {
+ id: 'reset-template',
+ label: __( 'Reset' ),
+ isEligible: isTemplateRevertable,
+ supportsBulk: true,
+ hideModalHeader: true,
+ RenderModal: ( { items, closeModal, onActionPerformed } ) => {
+ const { revertTemplate } = unlock( useDispatch( editorStore ) );
+ const { saveEditedEntityRecord } = useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+ const onConfirm = async () => {
+ try {
+ for ( const template of items ) {
+ await revertTemplate( template, {
+ allowUndo: false,
+ } );
+ await saveEditedEntityRecord(
+ 'postType',
+ template.type,
+ template.id
+ );
+ }
+
+ createSuccessNotice(
+ items.length > 1
+ ? sprintf(
+ /* translators: The number of items. */
+ __( '%s items reset.' ),
+ items.length
+ )
+ : sprintf(
+ /* translators: The template/part's name. */
+ __( '"%s" reset.' ),
+ decodeEntities( items[ 0 ].title.rendered )
+ ),
+ {
+ type: 'snackbar',
+ id: 'edit-site-template-reverted',
+ }
+ );
+ } catch ( error ) {
+ let fallbackErrorMessage;
+ if ( items[ 0 ].type === TEMPLATE_POST_TYPE ) {
+ fallbackErrorMessage =
+ items.length === 1
+ ? __(
+ 'An error occurred while reverting the template.'
+ )
+ : __(
+ 'An error occurred while reverting the templates.'
+ );
+ } else {
+ fallbackErrorMessage =
+ items.length === 1
+ ? __(
+ 'An error occurred while reverting the template part.'
+ )
+ : __(
+ 'An error occurred while reverting the template parts.'
+ );
+ }
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : fallbackErrorMessage;
+
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ };
+ return (
+
+
+ { __( 'Reset to default and clear all customizations?' ) }
+
+
+
+
+
+
+ );
+ },
+};
+
+/**
+ * Check if a template is removable.
+ * Copy from packages/edit-site/src/utils/is-template-removable.js.
+ *
+ * @param {Object} template The template entity to check.
+ * @return {boolean} Whether the template is revertable.
+ */
+function isTemplateRemovable( template ) {
+ if ( ! template ) {
+ return false;
+ }
+
+ return (
+ template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file
+ );
+}
+
+const deleteTemplateAction = {
+ id: 'delete-template',
+ label: __( 'Delete' ),
+ isEligible: isTemplateRemovable,
+ supportsBulk: true,
+ hideModalHeader: true,
+ RenderModal: ( { items: templates, closeModal, onActionPerformed } ) => {
+ const { removeTemplates } = unlock( useDispatch( editorStore ) );
+ return (
+
+
+ { templates.length > 1
+ ? sprintf(
+ // translators: %d: number of items to delete.
+ _n(
+ 'Delete %d item?',
+ 'Delete %d items?',
+ templates.length
+ ),
+ templates.length
+ )
+ : sprintf(
+ // translators: %s: The template or template part's titles
+ __( 'Delete "%s"?' ),
+ decodeEntities(
+ templates?.[ 0 ]?.title?.rendered
+ )
+ ) }
+
+
+
+
+
+
+ );
+ },
+};
+
+const renameTemplateAction = {
+ id: 'rename-template',
+ label: __( 'Rename' ),
+ isEligible: ( template ) => {
+ // We can only remove templates or template parts that can be removed.
+ // Additionally in the case of templates, we can only remove custom templates.
+ if (
+ ! isTemplateRemovable( template ) ||
+ ( template.type === TEMPLATE_POST_TYPE && ! template.is_custom )
+ ) {
+ return false;
+ }
+ return true;
+ },
+ RenderModal: ( { items: templates, closeModal } ) => {
+ const template = templates[ 0 ];
+ const title = decodeEntities( template.title.rendered );
+ const [ editedTitle, setEditedTitle ] = useState( title );
+ const {
+ editEntityRecord,
+ __experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits,
+ } = useDispatch( coreStore );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+ async function onTemplateRename( event ) {
+ event.preventDefault();
+ try {
+ await editEntityRecord(
+ 'postType',
+ template.type,
+ template.id,
+ {
+ title: editedTitle,
+ }
+ );
+ // Update state before saving rerenders the list.
+ setEditedTitle( '' );
+ closeModal();
+ // Persist edited entity.
+ await saveSpecifiedEntityEdits(
+ 'postType',
+ template.type,
+ template.id,
+ [ 'title' ], // Only save title to avoid persisting other edits.
+ {
+ throwOnError: true,
+ }
+ );
+ createSuccessNotice(
+ template.type === TEMPLATE_POST_TYPE
+ ? __( 'Template renamed.' )
+ : __( 'Template part renamed.' ),
+ {
+ type: 'snackbar',
+ }
+ );
+ } catch ( error ) {
+ const fallbackErrorMessage =
+ template.type === TEMPLATE_POST_TYPE
+ ? __( 'An error occurred while renaming the template.' )
+ : __(
+ 'An error occurred while renaming the template part.'
+ );
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : fallbackErrorMessage;
+
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ }
+ return (
+
+ );
+ },
+};
+
export function usePostActions( onActionPerformed, actionIds = null ) {
const permanentlyDeletePostAction = usePermanentlyDeletePostAction();
const restorePostAction = useRestorePostAction();
@@ -499,11 +774,14 @@ export function usePostActions( onActionPerformed, actionIds = null ) {
// By default, return all actions...
const defaultActions = [
editPostAction,
+ resetTemplateAction,
viewPostAction,
restorePostAction,
+ deleteTemplateAction,
permanentlyDeletePostAction,
postRevisionsAction,
renamePostAction,
+ renameTemplateAction,
trashPostAction,
];
diff --git a/packages/editor/src/store/constants.js b/packages/editor/src/store/constants.js
index 43b5491394fc81..73ce13066a6df3 100644
--- a/packages/editor/src/store/constants.js
+++ b/packages/editor/src/store/constants.js
@@ -21,3 +21,8 @@ export const AUTOSAVE_PROPERTIES = [ 'title', 'excerpt', 'content' ];
export const TEMPLATE_POST_TYPE = 'wp_template';
export const TEMPLATE_PART_POST_TYPE = 'wp_template_part';
export const PATTERN_POST_TYPE = 'wp_block';
+export const TEMPLATE_ORIGINS = {
+ custom: 'custom',
+ theme: 'theme',
+ plugin: 'plugin',
+};
diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js
index 3e87df48123452..0c76ffb6960ec1 100644
--- a/packages/editor/src/store/private-actions.js
+++ b/packages/editor/src/store/private-actions.js
@@ -2,10 +2,20 @@
* WordPress dependencies
*/
import { store as coreStore } from '@wordpress/core-data';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as preferencesStore } from '@wordpress/preferences';
+import { addQueryArgs } from '@wordpress/url';
+import apiFetch from '@wordpress/api-fetch';
+import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Internal dependencies
+ */
+import isTemplateRevertable from './utils/is-template-revertable';
+import { TEMPLATE_POST_TYPE } from './constants';
/**
* Returns an action object used to set which template is currently being used/edited.
@@ -220,3 +230,261 @@ export const saveDirtyEntities =
)
);
};
+
+/**
+ * Reverts a template to its original theme-provided file.
+ *
+ * @param {Object} template The template to revert.
+ * @param {Object} [options]
+ * @param {boolean} [options.allowUndo] Whether to allow the user to undo
+ * reverting the template. Default true.
+ */
+export const revertTemplate =
+ ( template, { allowUndo = true } = {} ) =>
+ async ( { registry } ) => {
+ const noticeId = 'edit-site-template-reverted';
+ registry.dispatch( noticesStore ).removeNotice( noticeId );
+ if ( ! isTemplateRevertable( template ) ) {
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice( __( 'This template is not revertable.' ), {
+ type: 'snackbar',
+ } );
+ return;
+ }
+
+ try {
+ const templateEntityConfig = registry
+ .select( coreStore )
+ .getEntityConfig( 'postType', template.type );
+
+ if ( ! templateEntityConfig ) {
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice(
+ __(
+ 'The editor has encountered an unexpected error. Please reload.'
+ ),
+ { type: 'snackbar' }
+ );
+ return;
+ }
+
+ const fileTemplatePath = addQueryArgs(
+ `${ templateEntityConfig.baseURL }/${ template.id }`,
+ { context: 'edit', source: 'theme' }
+ );
+
+ const fileTemplate = await apiFetch( { path: fileTemplatePath } );
+ if ( ! fileTemplate ) {
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice(
+ __(
+ 'The editor has encountered an unexpected error. Please reload.'
+ ),
+ { type: 'snackbar' }
+ );
+ return;
+ }
+
+ const serializeBlocks = ( {
+ blocks: blocksForSerialization = [],
+ } ) => __unstableSerializeAndClean( blocksForSerialization );
+
+ const edited = registry
+ .select( coreStore )
+ .getEditedEntityRecord(
+ 'postType',
+ template.type,
+ template.id
+ );
+
+ // We are fixing up the undo level here to make sure we can undo
+ // the revert in the header toolbar correctly.
+ registry.dispatch( coreStore ).editEntityRecord(
+ 'postType',
+ template.type,
+ template.id,
+ {
+ content: serializeBlocks, // Required to make the `undo` behave correctly.
+ blocks: edited.blocks, // Required to revert the blocks in the editor.
+ source: 'custom', // required to avoid turning the editor into a dirty state
+ },
+ {
+ undoIgnore: true, // Required to merge this edit with the last undo level.
+ }
+ );
+
+ const blocks = parse( fileTemplate?.content?.raw );
+ registry
+ .dispatch( coreStore )
+ .editEntityRecord( 'postType', template.type, fileTemplate.id, {
+ content: serializeBlocks,
+ blocks,
+ source: 'theme',
+ } );
+
+ if ( allowUndo ) {
+ const undoRevert = () => {
+ registry
+ .dispatch( coreStore )
+ .editEntityRecord(
+ 'postType',
+ template.type,
+ edited.id,
+ {
+ content: serializeBlocks,
+ blocks: edited.blocks,
+ source: 'custom',
+ }
+ );
+ };
+
+ registry
+ .dispatch( noticesStore )
+ .createSuccessNotice( __( 'Template reset.' ), {
+ type: 'snackbar',
+ id: noticeId,
+ actions: [
+ {
+ label: __( 'Undo' ),
+ onClick: undoRevert,
+ },
+ ],
+ } );
+ }
+ } catch ( error ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __( 'Template revert failed. Please reload.' );
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ };
+
+/**
+ * Action that removes an array of templates.
+ *
+ * @param {Array} items An array of template or template part objects to remove.
+ */
+export const removeTemplates =
+ ( items ) =>
+ async ( { registry } ) => {
+ const isTemplate = items[ 0 ].type === TEMPLATE_POST_TYPE;
+ const promiseResult = await Promise.allSettled(
+ items.map( ( item ) => {
+ return registry
+ .dispatch( coreStore )
+ .deleteEntityRecord(
+ 'postType',
+ item.type,
+ item.id,
+ { force: true },
+ { throwOnError: true }
+ );
+ } )
+ );
+
+ // If all the promises were fulfilled with sucess.
+ if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) {
+ let successMessage;
+
+ if ( items.length === 1 ) {
+ // Depending on how the entity was retrieved its title might be
+ // an object or simple string.
+ const title =
+ typeof items[ 0 ].title === 'string'
+ ? items[ 0 ].title
+ : items[ 0 ].title?.rendered;
+ successMessage = sprintf(
+ /* translators: The template/part's name. */
+ __( '"%s" deleted.' ),
+ decodeEntities( title )
+ );
+ } else {
+ successMessage = isTemplate
+ ? __( 'Templates deleted.' )
+ : __( 'Template parts deleted.' );
+ }
+
+ registry
+ .dispatch( noticesStore )
+ .createSuccessNotice( successMessage, {
+ type: 'snackbar',
+ id: 'site-editor-template-deleted-success',
+ } );
+ } else {
+ // If there was at lease one failure.
+ let errorMessage;
+ // If we were trying to delete a single template.
+ if ( promiseResult.length === 1 ) {
+ if ( promiseResult[ 0 ].reason?.message ) {
+ errorMessage = promiseResult[ 0 ].reason.message;
+ } else {
+ errorMessage = isTemplate
+ ? __( 'An error occurred while deleting the template.' )
+ : __(
+ 'An error occurred while deleting the template part.'
+ );
+ }
+ // If we were trying to delete a multiple templates
+ } else {
+ const errorMessages = new Set();
+ const failedPromises = promiseResult.filter(
+ ( { status } ) => status === 'rejected'
+ );
+ for ( const failedPromise of failedPromises ) {
+ if ( failedPromise.reason?.message ) {
+ errorMessages.add( failedPromise.reason.message );
+ }
+ }
+ if ( errorMessages.size === 0 ) {
+ errorMessage = isTemplate
+ ? __(
+ 'An error occurred while deleting the templates.'
+ )
+ : __(
+ 'An error occurred while deleting the template parts.'
+ );
+ } else if ( errorMessages.size === 1 ) {
+ errorMessage = isTemplate
+ ? sprintf(
+ /* translators: %s: an error message */
+ __(
+ 'An error occurred while deleting the templates: %s'
+ ),
+ [ ...errorMessages ][ 0 ]
+ )
+ : sprintf(
+ /* translators: %s: an error message */
+ __(
+ 'An error occurred while deleting the template parts: %s'
+ ),
+ [ ...errorMessages ][ 0 ]
+ );
+ } else {
+ errorMessage = isTemplate
+ ? sprintf(
+ /* translators: %s: a list of comma separated error messages */
+ __(
+ 'Some errors occurred while deleting the templates: %s'
+ ),
+ [ ...errorMessages ].join( ',' )
+ )
+ : sprintf(
+ /* translators: %s: a list of comma separated error messages */
+ __(
+ 'Some errors occurred while deleting the template parts: %s'
+ ),
+ [ ...errorMessages ].join( ',' )
+ );
+ }
+ }
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ };
diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js
new file mode 100644
index 00000000000000..efe4647f212801
--- /dev/null
+++ b/packages/editor/src/store/utils/is-template-revertable.js
@@ -0,0 +1,23 @@
+/**
+ * Internal dependencies
+ */
+import { TEMPLATE_ORIGINS } from '../constants';
+
+// Copy of the function from packages/edit-site/src/utils/is-template-revertable.js
+
+/**
+ * Check if a template is revertable to its original theme-provided template file.
+ *
+ * @param {Object} template The template entity to check.
+ * @return {boolean} Whether the template is revertable.
+ */
+export default function isTemplateRevertable( template ) {
+ if ( ! template ) {
+ return false;
+ }
+ /* eslint-disable camelcase */
+ return (
+ template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file
+ );
+ /* eslint-enable camelcase */
+}
diff --git a/packages/interface/src/components/interface-skeleton/index.js b/packages/interface/src/components/interface-skeleton/index.js
index 83ab8f6abf5a8a..d4d895e4e90b23 100644
--- a/packages/interface/src/components/interface-skeleton/index.js
+++ b/packages/interface/src/components/interface-skeleton/index.js
@@ -10,15 +10,23 @@ import { forwardRef, useEffect } from '@wordpress/element';
import {
__unstableUseNavigateRegions as useNavigateRegions,
__unstableMotion as motion,
+ __unstableAnimatePresence as AnimatePresence,
} from '@wordpress/components';
import { __, _x } from '@wordpress/i18n';
-import { useMergeRefs } from '@wordpress/compose';
+import {
+ useMergeRefs,
+ useReducedMotion,
+ useViewportMatch,
+} from '@wordpress/compose';
/**
* Internal dependencies
*/
import NavigableRegion from '../navigable-region';
+const SECONDARY_SIDEBAR_WIDTH = 350;
+const ANIMATION_DURATION = 0.25;
+
function useHTMLClass( className ) {
useEffect( () => {
const element =
@@ -42,6 +50,12 @@ const headerVariants = {
distractionFreeInactive: { opacity: 1, transition: { delay: 0 } },
};
+const secondarySidebarVariants = {
+ open: { width: SECONDARY_SIDEBAR_WIDTH },
+ closed: { width: 0 },
+ mobileOpen: { width: '100vw' },
+};
+
function InterfaceSkeleton(
{
isDistractionFree,
@@ -62,6 +76,13 @@ function InterfaceSkeleton(
},
ref
) {
+ const isMobileViewport = useViewportMatch( 'medium', '<' );
+ const disableMotion = useReducedMotion();
+ const defaultTransition = {
+ type: 'tween',
+ duration: disableMotion ? 0 : ANIMATION_DURATION,
+ ease: 'easeOut',
+ };
const navigateRegionsProps = useNavigateRegions( shortcuts );
useHTMLClass( 'interface-interface-skeleton__html-container' );
@@ -133,14 +154,35 @@ function InterfaceSkeleton(
) }
- { !! secondarySidebar && (
-
- { secondarySidebar }
-
- ) }
+
+ { !! secondarySidebar && (
+
+
+ { secondarySidebar }
+
+
+ ) }
+
{ !! notices && (
{ notices }