diff --git a/package-lock.json b/package-lock.json
index a40aa8aab4934..93da356b222be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -54197,6 +54197,7 @@
"license": "GPL-2.0-or-later",
"dependencies": {
"@babel/runtime": "^7.16.0",
+ "@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
@@ -68945,6 +68946,7 @@
"version": "file:packages/fields",
"requires": {
"@babel/runtime": "^7.16.0",
+ "@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
diff --git a/packages/editor/src/dataviews/actions/delete-post.tsx b/packages/editor/src/dataviews/actions/delete-post.tsx
deleted file mode 100644
index 381c2964f943f..0000000000000
--- a/packages/editor/src/dataviews/actions/delete-post.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { trash } from '@wordpress/icons';
-import { useDispatch } from '@wordpress/data';
-import { __, _n, sprintf } from '@wordpress/i18n';
-import { useState } from '@wordpress/element';
-import {
- Button,
- __experimentalText as Text,
- __experimentalHStack as HStack,
- __experimentalVStack as VStack,
-} from '@wordpress/components';
-// @ts-ignore
-import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
-import type { Action } from '@wordpress/dataviews';
-import type { StoreDescriptor } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import {
- isTemplateRemovable,
- getItemTitle,
- isTemplateOrTemplatePart,
-} from './utils';
-// @ts-ignore
-import { store as editorStore } from '../../store';
-import { unlock } from '../../lock-unlock';
-import type { Post } from '../types';
-
-const { PATTERN_TYPES } = unlock( patternsPrivateApis );
-
-// This action is used for templates, patterns and template parts.
-// Every other post type uses the similar `trashPostAction` which
-// moves the post to trash.
-const deletePostAction: Action< Post > = {
- id: 'delete-post',
- label: __( 'Delete' ),
- isPrimary: true,
- icon: trash,
- isEligible( post ) {
- if ( isTemplateOrTemplatePart( post ) ) {
- return isTemplateRemovable( post );
- }
- // We can only remove user patterns.
- return post.type === PATTERN_TYPES.user;
- },
- supportsBulk: true,
- hideModalHeader: true,
- RenderModal: ( { items, closeModal, onActionPerformed } ) => {
- const [ isBusy, setIsBusy ] = useState( false );
- const { removeTemplates } = unlock(
- useDispatch( editorStore as StoreDescriptor )
- );
- return (
-
-
- { items.length > 1
- ? sprintf(
- // translators: %d: number of items to delete.
- _n(
- 'Delete %d item?',
- 'Delete %d items?',
- items.length
- ),
- items.length
- )
- : sprintf(
- // translators: %s: The template or template part's titles
- __( 'Delete "%s"?' ),
- getItemTitle( items[ 0 ] )
- ) }
-
-
-
-
-
-
- );
- },
-};
-
-export default deletePostAction;
diff --git a/packages/editor/src/dataviews/actions/reset-post.tsx b/packages/editor/src/dataviews/actions/reset-post.tsx
deleted file mode 100644
index d0b5521a34833..0000000000000
--- a/packages/editor/src/dataviews/actions/reset-post.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { backup } from '@wordpress/icons';
-import { useDispatch } from '@wordpress/data';
-import { store as coreStore } from '@wordpress/core-data';
-import { __, sprintf } from '@wordpress/i18n';
-import { store as noticesStore } from '@wordpress/notices';
-import { useState } from '@wordpress/element';
-import {
- Button,
- __experimentalText as Text,
- __experimentalHStack as HStack,
- __experimentalVStack as VStack,
-} from '@wordpress/components';
-import type { Action } from '@wordpress/dataviews';
-import type { StoreDescriptor } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import { TEMPLATE_POST_TYPE, TEMPLATE_ORIGINS } from '../../store/constants';
-import { store as editorStore } from '../../store';
-import { unlock } from '../../lock-unlock';
-import type { Post, CoreDataError } from '../types';
-import { isTemplateOrTemplatePart, getItemTitle } from './utils';
-
-const resetPost: Action< Post > = {
- id: 'reset-post',
- label: __( 'Reset' ),
- isEligible: ( item ) => {
- return (
- isTemplateOrTemplatePart( item ) &&
- item?.source === TEMPLATE_ORIGINS.custom &&
- ( Boolean( item.type === 'wp_template' && item?.plugin ) ||
- item?.has_theme_file )
- );
- },
- icon: backup,
- supportsBulk: true,
- hideModalHeader: true,
- RenderModal: ( { items, closeModal, onActionPerformed } ) => {
- const [ isBusy, setIsBusy ] = useState( false );
- const { revertTemplate } = unlock(
- useDispatch( editorStore as StoreDescriptor )
- );
- 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.' ),
- getItemTitle( items[ 0 ] )
- ),
- {
- type: 'snackbar',
- id: 'revert-template-action',
- }
- );
- } 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 typedError = error as CoreDataError;
- const errorMessage =
- typedError.message && typedError.code !== 'unknown_error'
- ? typedError.message
- : fallbackErrorMessage;
-
- createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
- };
- return (
-
-
- { __( 'Reset to default and clear all customizations?' ) }
-
-
-
-
-
-
- );
- },
-};
-
-export default resetPost;
diff --git a/packages/editor/src/dataviews/fields/index.ts b/packages/editor/src/dataviews/fields/index.ts
deleted file mode 100644
index b215172eaf7f0..0000000000000
--- a/packages/editor/src/dataviews/fields/index.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { __ } from '@wordpress/i18n';
-import type { Field } from '@wordpress/dataviews';
-
-/**
- * Internal dependencies
- */
-import type { BasePost } from '../types';
-import { getItemTitle } from '../actions/utils';
-
-export const titleField: Field< BasePost > = {
- type: 'text',
- id: 'title',
- label: __( 'Title' ),
- placeholder: __( 'No title' ),
- getValue: ( { item } ) => getItemTitle( item ),
-};
-
-export const orderField: Field< BasePost > = {
- type: 'integer',
- id: 'menu_order',
- label: __( 'Order' ),
- description: __( 'Determines the order of pages.' ),
-};
diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts
index e685493641f3b..10f2b9ce872d5 100644
--- a/packages/editor/src/dataviews/store/private-actions.ts
+++ b/packages/editor/src/dataviews/store/private-actions.ts
@@ -8,11 +8,6 @@ import { doAction } from '@wordpress/hooks';
/**
* Internal dependencies
*/
-import duplicateTemplatePart from '../actions/duplicate-template-part';
-import resetPost from '../actions/reset-post';
-import trashPost from '../actions/trash-post';
-import renamePost from '../actions/rename-post';
-import restorePost from '../actions/restore-post';
import type { PostType } from '../types';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
@@ -24,8 +19,13 @@ import {
reorderPage,
exportPattern,
permanentlyDeletePost,
+ restorePost,
+ trashPost,
+ renamePost,
+ resetPost,
+ deletePost,
} from '@wordpress/fields';
-import deletePost from '../actions/delete-post';
+import duplicateTemplatePart from '../actions/duplicate-template-part';
export function registerEntityAction< Item >(
kind: string,
@@ -117,8 +117,8 @@ export const registerPostTypeActions =
? reorderPage
: undefined,
postTypeConfig.slug === 'wp_block' ? exportPattern : undefined,
- resetPost,
restorePost,
+ resetPost,
deletePost,
trashPost,
permanentlyDeletePost,
diff --git a/packages/fields/README.md b/packages/fields/README.md
index 842fab02606af..b4e45103600da 100644
--- a/packages/fields/README.md
+++ b/packages/fields/README.md
@@ -14,6 +14,10 @@ npm install @wordpress/fields --save
+### deletePost
+
+Undocumented declaration.
+
### duplicatePattern
Undocumented declaration.
@@ -42,6 +46,10 @@ Undocumented declaration.
Undocumented declaration.
+### renamePost
+
+Undocumented declaration.
+
### reorderPage
Undocumented declaration.
@@ -50,10 +58,22 @@ Undocumented declaration.
Undocumented declaration.
+### resetPost
+
+Undocumented declaration.
+
+### restorePost
+
+Undocumented declaration.
+
### titleField
Undocumented declaration.
+### trashPost
+
+Undocumented declaration.
+
### viewPost
Undocumented declaration.
diff --git a/packages/fields/package.json b/packages/fields/package.json
index ba687e6db1bc8..3da913d1ee9ae 100644
--- a/packages/fields/package.json
+++ b/packages/fields/package.json
@@ -32,6 +32,7 @@
],
"dependencies": {
"@babel/runtime": "^7.16.0",
+ "@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/blocks": "file:../blocks",
"@wordpress/components": "file:../components",
diff --git a/packages/fields/src/actions/base-post/index.ts b/packages/fields/src/actions/base-post/index.ts
deleted file mode 100644
index 7541be86c48b1..0000000000000
--- a/packages/fields/src/actions/base-post/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export { default as viewPost } from './view-post';
-export { default as reorderPage } from './reorder-page';
-export { default as reorderPageNative } from './reorder-page.native';
-export { default as duplicatePost } from './duplicate-post';
-export { default as duplicatePostNative } from './duplicate-post.native';
diff --git a/packages/fields/src/actions/common/index.ts b/packages/fields/src/actions/common/index.ts
deleted file mode 100644
index 3590b2e270892..0000000000000
--- a/packages/fields/src/actions/common/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as viewPostRevisions } from './view-post-revisions';
-export { default as permanentlyDeletePost } from './permanently-delete-post';
diff --git a/packages/fields/src/actions/delete-post.tsx b/packages/fields/src/actions/delete-post.tsx
new file mode 100644
index 0000000000000..c5ab866e12479
--- /dev/null
+++ b/packages/fields/src/actions/delete-post.tsx
@@ -0,0 +1,203 @@
+/**
+ * WordPress dependencies
+ */
+import { trash } from '@wordpress/icons';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { useState } from '@wordpress/element';
+import {
+ Button,
+ __experimentalText as Text,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+// @ts-ignore
+import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
+import type { Action } from '@wordpress/dataviews';
+import { decodeEntities } from '@wordpress/html-entities';
+
+/**
+ * Internal dependencies
+ */
+import {
+ getItemTitle,
+ isTemplateOrTemplatePart,
+ isTemplateRemovable,
+} from './utils';
+import type { Pattern, Template, TemplatePart } from '../types';
+import type { NoticeSettings } from '../mutation';
+import { deletePostWithNotices } from '../mutation';
+import { unlock } from '../lock-unlock';
+
+const { PATTERN_TYPES } = unlock( patternsPrivateApis );
+
+// This action is used for templates, patterns and template parts.
+// Every other post type uses the similar `trashPostAction` which
+// moves the post to trash.
+const deletePostAction: Action< Template | TemplatePart | Pattern > = {
+ id: 'delete-post',
+ label: __( 'Delete' ),
+ isPrimary: true,
+ icon: trash,
+ isEligible( post ) {
+ if ( isTemplateOrTemplatePart( post ) ) {
+ return isTemplateRemovable( post );
+ }
+ // We can only remove user patterns.
+ return post.type === PATTERN_TYPES.user;
+ },
+ supportsBulk: true,
+ hideModalHeader: true,
+ RenderModal: ( { items, closeModal, onActionPerformed } ) => {
+ const [ isBusy, setIsBusy ] = useState( false );
+ const isResetting = items.every(
+ ( item ) => isTemplateOrTemplatePart( item ) && item?.has_theme_file
+ );
+ return (
+
+
+ { items.length > 1
+ ? sprintf(
+ // translators: %d: number of items to delete.
+ _n(
+ 'Delete %d item?',
+ 'Delete %d items?',
+ items.length
+ ),
+ items.length
+ )
+ : sprintf(
+ // translators: %s: The template or template part's titles
+ __( 'Delete "%s"?' ),
+ getItemTitle( items[ 0 ] )
+ ) }
+
+
+
+
+
+
+ );
+ },
+};
+
+export default deletePostAction;
diff --git a/packages/fields/src/actions/pattern/duplicate-pattern.tsx b/packages/fields/src/actions/duplicate-pattern.tsx
similarity index 91%
rename from packages/fields/src/actions/pattern/duplicate-pattern.tsx
rename to packages/fields/src/actions/duplicate-pattern.tsx
index 7c71a271997f1..bf2820f951dba 100644
--- a/packages/fields/src/actions/pattern/duplicate-pattern.tsx
+++ b/packages/fields/src/actions/duplicate-pattern.tsx
@@ -9,8 +9,8 @@ import type { Action } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import { unlock } from '../../lock-unlock';
-import type { Pattern } from '../../types';
+import { unlock } from '../lock-unlock';
+import type { Pattern } from '../types';
// Patterns.
const { CreatePatternModalContents, useDuplicatePatternProps } =
diff --git a/packages/fields/src/actions/base-post/duplicate-post.native.tsx b/packages/fields/src/actions/duplicate-post.native.tsx
similarity index 100%
rename from packages/fields/src/actions/base-post/duplicate-post.native.tsx
rename to packages/fields/src/actions/duplicate-post.native.tsx
diff --git a/packages/fields/src/actions/base-post/duplicate-post.tsx b/packages/fields/src/actions/duplicate-post.tsx
similarity index 96%
rename from packages/fields/src/actions/base-post/duplicate-post.tsx
rename to packages/fields/src/actions/duplicate-post.tsx
index 0035a40c00934..d153073f4b6c1 100644
--- a/packages/fields/src/actions/base-post/duplicate-post.tsx
+++ b/packages/fields/src/actions/duplicate-post.tsx
@@ -18,9 +18,9 @@ import type { Action } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import { titleField } from '../../fields';
-import type { BasePost, CoreDataError } from '../../types';
-import { getItemTitle } from '../utils';
+import { titleField } from '../fields';
+import type { BasePost, CoreDataError } from '../types';
+import { getItemTitle } from './utils';
const fields = [ titleField ];
const formDuplicateAction = {
diff --git a/packages/fields/src/actions/pattern/export-pattern.native.tsx b/packages/fields/src/actions/export-pattern.native.tsx
similarity index 100%
rename from packages/fields/src/actions/pattern/export-pattern.native.tsx
rename to packages/fields/src/actions/export-pattern.native.tsx
diff --git a/packages/fields/src/actions/pattern/export-pattern.tsx b/packages/fields/src/actions/export-pattern.tsx
similarity index 95%
rename from packages/fields/src/actions/pattern/export-pattern.tsx
rename to packages/fields/src/actions/export-pattern.tsx
index b0f6c3335544c..b6be83eeda84b 100644
--- a/packages/fields/src/actions/pattern/export-pattern.tsx
+++ b/packages/fields/src/actions/export-pattern.tsx
@@ -15,8 +15,8 @@ import type { Action } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import type { Pattern } from '../../types';
-import { getItemTitle } from '../utils';
+import type { Pattern } from '../types';
+import { getItemTitle } from './utils';
function getJsonFromItem( item: Pattern ) {
return JSON.stringify(
diff --git a/packages/fields/src/actions/index.ts b/packages/fields/src/actions/index.ts
index cf4fd6833f3fb..08e22836e68fd 100644
--- a/packages/fields/src/actions/index.ts
+++ b/packages/fields/src/actions/index.ts
@@ -1,3 +1,15 @@
-export * from './base-post';
-export * from './common';
-export * from './pattern';
+export { default as viewPost } from './view-post';
+export { default as reorderPage } from './reorder-page';
+export { default as reorderPageNative } from './reorder-page.native';
+export { default as duplicatePost } from './duplicate-post';
+export { default as duplicatePostNative } from './duplicate-post.native';
+export { default as renamePost } from './rename-post';
+export { default as resetPost } from './reset-post';
+export { default as duplicatePattern } from './duplicate-pattern';
+export { default as exportPattern } from './export-pattern';
+export { default as exportPatternNative } from './export-pattern.native';
+export { default as viewPostRevisions } from './view-post-revisions';
+export { default as permanentlyDeletePost } from './permanently-delete-post';
+export { default as restorePost } from './restore-post';
+export { default as trashPost } from './trash-post';
+export { default as deletePost } from './delete-post';
diff --git a/packages/fields/src/actions/pattern/index.ts b/packages/fields/src/actions/pattern/index.ts
deleted file mode 100644
index 827c2ce365c2c..0000000000000
--- a/packages/fields/src/actions/pattern/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { default as duplicatePattern } from './duplicate-pattern';
-export { default as exportPattern } from './export-pattern';
-export { default as exportPatternNative } from './export-pattern.native';
diff --git a/packages/fields/src/actions/common/permanently-delete-post.tsx b/packages/fields/src/actions/permanently-delete-post.tsx
similarity index 96%
rename from packages/fields/src/actions/common/permanently-delete-post.tsx
rename to packages/fields/src/actions/permanently-delete-post.tsx
index e0c1de96871f1..afbb84ae12c74 100644
--- a/packages/fields/src/actions/common/permanently-delete-post.tsx
+++ b/packages/fields/src/actions/permanently-delete-post.tsx
@@ -10,8 +10,8 @@ import { trash } from '@wordpress/icons';
/**
* Internal dependencies
*/
-import { getItemTitle, isTemplateOrTemplatePart } from '../utils';
-import type { CoreDataError, PostWithPermissions } from '../../types';
+import { getItemTitle, isTemplateOrTemplatePart } from './utils';
+import type { CoreDataError, PostWithPermissions } from '../types';
const permanentlyDeletePost: Action< PostWithPermissions > = {
id: 'permanently-delete',
diff --git a/packages/editor/src/dataviews/actions/rename-post.tsx b/packages/fields/src/actions/rename-post.tsx
similarity index 97%
rename from packages/editor/src/dataviews/actions/rename-post.tsx
rename to packages/fields/src/actions/rename-post.tsx
index ef9da271111ea..da1fd46669f0d 100644
--- a/packages/editor/src/dataviews/actions/rename-post.tsx
+++ b/packages/fields/src/actions/rename-post.tsx
@@ -19,17 +19,16 @@ import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
*/
-import {
- TEMPLATE_ORIGINS,
- TEMPLATE_PART_POST_TYPE,
- TEMPLATE_POST_TYPE,
-} from '../../store/constants';
-import { unlock } from '../../lock-unlock';
+
+import { unlock } from '../lock-unlock';
import {
getItemTitle,
isTemplateRemovable,
isTemplate,
isTemplatePart,
+ TEMPLATE_ORIGINS,
+ TEMPLATE_PART_POST_TYPE,
+ TEMPLATE_POST_TYPE,
} from './utils';
import type { CoreDataError, PostWithPermissions } from '../types';
diff --git a/packages/fields/src/actions/base-post/reorder-page.native.tsx b/packages/fields/src/actions/reorder-page.native.tsx
similarity index 100%
rename from packages/fields/src/actions/base-post/reorder-page.native.tsx
rename to packages/fields/src/actions/reorder-page.native.tsx
diff --git a/packages/fields/src/actions/base-post/reorder-page.tsx b/packages/fields/src/actions/reorder-page.tsx
similarity index 96%
rename from packages/fields/src/actions/base-post/reorder-page.tsx
rename to packages/fields/src/actions/reorder-page.tsx
index 7f3bca59c471c..1820884d8d8c7 100644
--- a/packages/fields/src/actions/base-post/reorder-page.tsx
+++ b/packages/fields/src/actions/reorder-page.tsx
@@ -17,8 +17,8 @@ import type { Action, RenderModalProps } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import type { CoreDataError, BasePost } from '../../types';
-import { orderField } from '../../fields';
+import type { CoreDataError, BasePost } from '../types';
+import { orderField } from '../fields';
const fields = [ orderField ];
const formOrderAction = {
diff --git a/packages/fields/src/actions/reset-post.tsx b/packages/fields/src/actions/reset-post.tsx
new file mode 100644
index 0000000000000..105d7b283b833
--- /dev/null
+++ b/packages/fields/src/actions/reset-post.tsx
@@ -0,0 +1,300 @@
+/**
+ * WordPress dependencies
+ */
+import { backup } from '@wordpress/icons';
+import { dispatch, select, useDispatch } from '@wordpress/data';
+import { store as coreStore } from '@wordpress/core-data';
+import { __, sprintf } from '@wordpress/i18n';
+import { store as noticesStore } from '@wordpress/notices';
+import { useState } from '@wordpress/element';
+// @ts-ignore
+import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
+import {
+ Button,
+ __experimentalText as Text,
+ __experimentalHStack as HStack,
+ __experimentalVStack as VStack,
+} from '@wordpress/components';
+import type { Action } from '@wordpress/dataviews';
+import { addQueryArgs } from '@wordpress/url';
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import {
+ getItemTitle,
+ isTemplateOrTemplatePart,
+ TEMPLATE_ORIGINS,
+ TEMPLATE_POST_TYPE,
+} from './utils';
+import type { CoreDataError, Template, TemplatePart } from '../types';
+
+const isTemplateRevertable = (
+ templateOrTemplatePart: Template | TemplatePart
+) => {
+ if ( ! templateOrTemplatePart ) {
+ return false;
+ }
+
+ return (
+ templateOrTemplatePart.source === TEMPLATE_ORIGINS.custom &&
+ ( Boolean( templateOrTemplatePart?.plugin ) ||
+ templateOrTemplatePart?.has_theme_file )
+ );
+};
+
+/**
+ * Copied - pasted from https://github.com/WordPress/gutenberg/blob/bf1462ad37d4637ebbf63270b9c244b23c69e2a8/packages/editor/src/store/private-actions.js#L233-L365
+ *
+ * @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.
+ */
+const revertTemplate = async (
+ template: TemplatePart | Template,
+ { allowUndo = true } = {}
+) => {
+ const noticeId = 'edit-site-template-reverted';
+ dispatch( noticesStore ).removeNotice( noticeId );
+ if ( ! isTemplateRevertable( template ) ) {
+ dispatch( noticesStore ).createErrorNotice(
+ __( 'This template is not revertable.' ),
+ {
+ type: 'snackbar',
+ }
+ );
+ return;
+ }
+
+ try {
+ const templateEntityConfig = select( coreStore ).getEntityConfig(
+ 'postType',
+ template.type
+ );
+
+ if ( ! templateEntityConfig ) {
+ 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: template.origin }
+ );
+
+ const fileTemplate = ( await apiFetch( {
+ path: fileTemplatePath,
+ } ) ) as any;
+ if ( ! fileTemplate ) {
+ dispatch( noticesStore ).createErrorNotice(
+ __(
+ 'The editor has encountered an unexpected error. Please reload.'
+ ),
+ { type: 'snackbar' }
+ );
+ return;
+ }
+
+ const serializeBlocks = ( { blocks: blocksForSerialization = [] } ) =>
+ __unstableSerializeAndClean( blocksForSerialization );
+
+ const edited = select( coreStore ).getEditedEntityRecord(
+ 'postType',
+ template.type,
+ template.id
+ ) as any;
+
+ // We are fixing up the undo level here to make sure we can undo
+ // the revert in the header toolbar correctly.
+ 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 );
+
+ dispatch( coreStore ).editEntityRecord(
+ 'postType',
+ template.type,
+ fileTemplate.id,
+ {
+ content: serializeBlocks,
+ blocks,
+ source: 'theme',
+ }
+ );
+
+ if ( allowUndo ) {
+ const undoRevert = () => {
+ dispatch( coreStore ).editEntityRecord(
+ 'postType',
+ template.type,
+ edited.id,
+ {
+ content: serializeBlocks,
+ blocks: edited.blocks,
+ source: 'custom',
+ }
+ );
+ };
+
+ dispatch( noticesStore ).createSuccessNotice(
+ __( 'Template reset.' ),
+ {
+ type: 'snackbar',
+ id: noticeId,
+ actions: [
+ {
+ label: __( 'Undo' ),
+ onClick: undoRevert,
+ },
+ ],
+ }
+ );
+ }
+ } catch ( error: any ) {
+ const errorMessage =
+ error.message && error.code !== 'unknown_error'
+ ? error.message
+ : __( 'Template revert failed. Please reload.' );
+
+ dispatch( noticesStore ).createErrorNotice( errorMessage, {
+ type: 'snackbar',
+ } );
+ }
+};
+
+const resetPostAction: Action< Template | TemplatePart > = {
+ id: 'reset-post',
+ label: __( 'Reset' ),
+ isEligible: ( item ) => {
+ return (
+ isTemplateOrTemplatePart( item ) &&
+ item?.source === TEMPLATE_ORIGINS.custom &&
+ ( Boolean( item.type === 'wp_template' && item?.plugin ) ||
+ item?.has_theme_file )
+ );
+ },
+ icon: backup,
+ supportsBulk: true,
+ hideModalHeader: true,
+ RenderModal: ( { items, closeModal, onActionPerformed } ) => {
+ const [ isBusy, setIsBusy ] = useState( false );
+
+ 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.' ),
+ getItemTitle( items[ 0 ] )
+ ),
+ {
+ type: 'snackbar',
+ id: 'revert-template-action',
+ }
+ );
+ } 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 typedError = error as CoreDataError;
+ const errorMessage =
+ typedError.message && typedError.code !== 'unknown_error'
+ ? typedError.message
+ : fallbackErrorMessage;
+
+ createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ };
+ return (
+
+
+ { __( 'Reset to default and clear all customizations?' ) }
+
+
+
+
+
+
+ );
+ },
+};
+
+export default resetPostAction;
diff --git a/packages/editor/src/dataviews/actions/restore-post.tsx b/packages/fields/src/actions/restore-post.tsx
similarity index 100%
rename from packages/editor/src/dataviews/actions/restore-post.tsx
rename to packages/fields/src/actions/restore-post.tsx
diff --git a/packages/editor/src/dataviews/actions/trash-post.tsx b/packages/fields/src/actions/trash-post.tsx
similarity index 100%
rename from packages/editor/src/dataviews/actions/trash-post.tsx
rename to packages/fields/src/actions/trash-post.tsx
diff --git a/packages/fields/src/actions/common/view-post-revisions.tsx b/packages/fields/src/actions/view-post-revisions.tsx
similarity index 96%
rename from packages/fields/src/actions/common/view-post-revisions.tsx
rename to packages/fields/src/actions/view-post-revisions.tsx
index 617a5263a707d..875b925b94f07 100644
--- a/packages/fields/src/actions/common/view-post-revisions.tsx
+++ b/packages/fields/src/actions/view-post-revisions.tsx
@@ -8,7 +8,7 @@ import type { Action } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import type { Post } from '../../types';
+import type { Post } from '../types';
const viewPostRevisions: Action< Post > = {
id: 'view-post-revisions',
diff --git a/packages/fields/src/actions/base-post/view-post.tsx b/packages/fields/src/actions/view-post.tsx
similarity index 92%
rename from packages/fields/src/actions/base-post/view-post.tsx
rename to packages/fields/src/actions/view-post.tsx
index 8c581877e473b..187faffafb5d3 100644
--- a/packages/fields/src/actions/base-post/view-post.tsx
+++ b/packages/fields/src/actions/view-post.tsx
@@ -8,7 +8,7 @@ import type { Action } from '@wordpress/dataviews';
/**
* Internal dependencies
*/
-import type { BasePost } from '../../types';
+import type { BasePost } from '../types';
const viewPost: Action< BasePost > = {
id: 'view-post',
diff --git a/packages/fields/src/index.native.ts b/packages/fields/src/index.native.ts
index e4d3134d72f84..33a26e3c2e6e2 100644
--- a/packages/fields/src/index.native.ts
+++ b/packages/fields/src/index.native.ts
@@ -1,2 +1,2 @@
-export * from './actions/base-post/duplicate-post.native';
-export * from './actions/base-post/reorder-page.native';
+export * from './actions/duplicate-post.native';
+export * from './actions/reorder-page.native';
diff --git a/packages/fields/src/mutation/index.ts b/packages/fields/src/mutation/index.ts
new file mode 100644
index 0000000000000..80e399d74e947
--- /dev/null
+++ b/packages/fields/src/mutation/index.ts
@@ -0,0 +1,184 @@
+/**
+ * WordPress dependencies
+ */
+import { store as noticesStore } from '@wordpress/notices';
+import { store as coreStore } from '@wordpress/core-data';
+import { dispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import type { CoreDataError, Post } from '../types';
+
+const getErrorMessagesFromPromises = < T >(
+ allSettledResults: PromiseSettledResult< T >[]
+) => {
+ const errorMessages = new Set< string >();
+ // If there was at lease one failure.
+ if ( allSettledResults.length === 1 ) {
+ const typedError = allSettledResults[ 0 ] as {
+ reason?: CoreDataError;
+ };
+ if ( typedError.reason?.message ) {
+ errorMessages.add( typedError.reason.message );
+ }
+ } else {
+ const failedPromises = allSettledResults.filter(
+ ( { status } ) => status === 'rejected'
+ );
+ for ( const failedPromise of failedPromises ) {
+ const typedError = failedPromise as {
+ reason?: CoreDataError;
+ };
+ if ( typedError.reason?.message ) {
+ errorMessages.add( typedError.reason.message );
+ }
+ }
+ }
+ return errorMessages;
+};
+
+export type NoticeSettings< T extends Post > = {
+ success: {
+ id?: string;
+ type?: string;
+ messages: {
+ getMessage: ( posts: T ) => string;
+ getBatchMessage: ( posts: T[] ) => string;
+ };
+ };
+ error: {
+ id?: string;
+ type?: string;
+ messages: {
+ getMessage: ( errors: Set< string > ) => string;
+ getBatchMessage: ( errors: Set< string > ) => string;
+ };
+ };
+};
+
+export const deletePostWithNotices = async < T extends Post >(
+ posts: T[],
+ notice: NoticeSettings< T >,
+ callbacks: {
+ onActionPerformed?: ( posts: T[] ) => void;
+ onActionError?: () => void;
+ }
+) => {
+ const { createSuccessNotice, createErrorNotice } = dispatch( noticesStore );
+ const { deleteEntityRecord } = dispatch( coreStore );
+ const allSettledResults = await Promise.allSettled(
+ posts.map( ( post ) => {
+ return deleteEntityRecord(
+ 'postType',
+ post.type,
+ post.id,
+ { force: true },
+ { throwOnError: true }
+ );
+ } )
+ );
+ // If all the promises were fulfilled with success.
+ if ( allSettledResults.every( ( { status } ) => status === 'fulfilled' ) ) {
+ let successMessage;
+ if ( allSettledResults.length === 1 ) {
+ successMessage = notice.success.messages.getMessage( posts[ 0 ] );
+ } else {
+ successMessage = notice.success.messages.getBatchMessage( posts );
+ }
+ createSuccessNotice( successMessage, {
+ type: notice.success.type ?? 'snackbar',
+ id: notice.success.id,
+ } );
+ callbacks.onActionPerformed?.( posts );
+ } else {
+ const errorMessages = getErrorMessagesFromPromises( allSettledResults );
+ let errorMessage = '';
+ if ( allSettledResults.length === 1 ) {
+ errorMessage = notice.error.messages.getMessage( errorMessages );
+ } else {
+ errorMessage =
+ notice.error.messages.getBatchMessage( errorMessages );
+ }
+
+ createErrorNotice( errorMessage, {
+ type: notice.error.type ?? 'snackbar',
+ id: notice.error.id,
+ } );
+ callbacks.onActionError?.();
+ }
+};
+
+export const editPostWithNotices = async < T extends Post >(
+ postsWithUpdates: {
+ originalPost: T;
+ changes: Partial< T >;
+ }[],
+ notice: NoticeSettings< T >,
+ callbacks: {
+ onActionPerformed?: ( posts: T[] ) => void;
+ onActionError?: () => void;
+ }
+) => {
+ const { createSuccessNotice, createErrorNotice } = dispatch( noticesStore );
+ const { editEntityRecord, saveEditedEntityRecord } = dispatch( coreStore );
+ await Promise.allSettled(
+ postsWithUpdates.map( ( post ) => {
+ return editEntityRecord(
+ 'postType',
+ post.originalPost.type,
+ post.originalPost.id,
+ {
+ ...post.changes,
+ }
+ );
+ } )
+ );
+ const allSettledResults = await Promise.allSettled(
+ postsWithUpdates.map( ( post ) => {
+ return saveEditedEntityRecord(
+ 'postType',
+ post.originalPost.type,
+ post.originalPost.id,
+ {
+ throwOnError: true,
+ }
+ );
+ } )
+ );
+ // If all the promises were fulfilled with success.
+ if ( allSettledResults.every( ( { status } ) => status === 'fulfilled' ) ) {
+ let successMessage;
+ if ( allSettledResults.length === 1 ) {
+ successMessage = notice.success.messages.getMessage(
+ postsWithUpdates[ 0 ].originalPost
+ );
+ } else {
+ successMessage = notice.success.messages.getBatchMessage(
+ postsWithUpdates.map( ( post ) => post.originalPost )
+ );
+ }
+ createSuccessNotice( successMessage, {
+ type: notice.success.type ?? 'snackbar',
+ id: notice.success.id,
+ } );
+ callbacks.onActionPerformed?.(
+ postsWithUpdates.map( ( post ) => post.originalPost )
+ );
+ } else {
+ const errorMessages = getErrorMessagesFromPromises( allSettledResults );
+ let errorMessage = '';
+ if ( allSettledResults.length === 1 ) {
+ errorMessage = notice.error.messages.getMessage( errorMessages );
+ } else {
+ errorMessage =
+ notice.error.messages.getBatchMessage( errorMessages );
+ }
+
+ createErrorNotice( errorMessage, {
+ type: notice.error.type ?? 'snackbar',
+ id: notice.error.id,
+ } );
+ callbacks.onActionError?.();
+ }
+};
diff --git a/packages/fields/src/types.ts b/packages/fields/src/types.ts
index 664c2dd417201..a5ed9596b07df 100644
--- a/packages/fields/src/types.ts
+++ b/packages/fields/src/types.ts
@@ -54,6 +54,7 @@ export interface TemplatePart extends CommonPost {
has_theme_file: boolean;
id: string;
area: string;
+ plugin?: string;
}
export interface Pattern extends CommonPost {
diff --git a/packages/fields/src/wordpress-editor.d.ts b/packages/fields/src/wordpress-editor.d.ts
deleted file mode 100644
index 915dacd5f05a9..0000000000000
--- a/packages/fields/src/wordpress-editor.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module '@wordpress/editor';
diff --git a/packages/fields/tsconfig.json b/packages/fields/tsconfig.json
index c55be59acf40f..69dbd076d0574 100644
--- a/packages/fields/tsconfig.json
+++ b/packages/fields/tsconfig.json
@@ -7,6 +7,7 @@
"checkJs": false
},
"references": [
+ { "path": "../api-fetch" },
{ "path": "../components" },
{ "path": "../compose" },
{ "path": "../data" },
@@ -24,6 +25,5 @@
{ "path": "../hooks" },
{ "path": "../html-entities" }
],
- "include": [ "src" ],
- "exclude": [ "@wordpress/editor" ]
+ "include": [ "src" ]
}