-
+
+ { hasBulkActions && (
+
+
+ |
+ ) }
{ visibleFields.map( ( field, index ) => (
|
{ hasData &&
- usedData.map( ( item ) => (
-
+ usedData.map( ( item, index ) => (
+
+ { hasBulkActions && (
+
+
+ |
+ ) }
{ visibleFields.map( ( field ) => (
|
{
+ RenderModal: ( { items: posts, closeModal } ) => {
+ // Todo - handle multiple posts
+ const post = posts[ 0 ];
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { deleteEntityRecord } = useDispatch( coreStore );
@@ -109,7 +111,9 @@ export function usePermanentlyDeletePostAction() {
isEligible( { status } ) {
return status === 'trash';
},
- async callback( post ) {
+ async callback( posts ) {
+ // Todo - handle multiple posts
+ const post = posts[ 0 ];
try {
await deleteEntityRecord(
'postType',
@@ -160,7 +164,9 @@ export function useRestorePostAction() {
isEligible( { status } ) {
return status === 'trash';
},
- async callback( post ) {
+ async callback( posts ) {
+ // Todo - handle multiple posts
+ const post = posts[ 0 ];
await editEntityRecord( 'postType', post.type, post.id, {
status: 'draft',
} );
@@ -211,7 +217,8 @@ export const viewPostAction = {
isEligible( post ) {
return post.status !== 'trash';
},
- callback( post ) {
+ callback( posts ) {
+ const post = posts[ 0 ];
document.location.href = post.link;
},
};
@@ -225,7 +232,8 @@ export function useEditPostAction() {
isEligible( { status } ) {
return status !== 'trash';
},
- callback( post ) {
+ callback( posts ) {
+ const post = posts[ 0 ];
history.push( {
postId: post.id,
postType: post.type,
@@ -250,7 +258,8 @@ export const postRevisionsAction = {
post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0;
return lastRevisionId && revisionsCount > 1;
},
- callback( post ) {
+ callback( posts ) {
+ const post = posts[ 0 ];
const href = addQueryArgs( 'revision.php', {
revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id,
} );
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 bf5210beb49fbf..0c44c996ed373b 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
@@ -45,7 +45,7 @@ export const exportJSONaction = {
id: 'export-pattern',
label: __( 'Export as JSON' ),
isEligible: ( item ) => item.type === PATTERN_TYPES.user,
- callback: ( item ) => {
+ callback: ( [ item ] ) => {
const json = {
__file: item.type,
title: item.title || item.name,
@@ -71,7 +71,8 @@ export const renameAction = {
const hasThemeFile = isTemplatePart && item.templatePart.has_theme_file;
return isCustomPattern && ! hasThemeFile;
},
- RenderModal: ( { item, closeModal } ) => {
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
const [ title, setTitle ] = useState( () => item.title );
const { editEntityRecord, saveEditedEntityRecord } =
useDispatch( coreStore );
@@ -160,7 +161,8 @@ export const deleteAction = {
return canDeleteOrReset( item ) && ! hasThemeFile;
},
hideModalHeader: true,
- RenderModal: ( { item, closeModal } ) => {
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
const { __experimentalDeleteReusableBlock } =
useDispatch( reusableBlocksStore );
const { createErrorNotice, createSuccessNotice } =
@@ -224,7 +226,8 @@ export const resetAction = {
return canDeleteOrReset( item ) && hasThemeFile;
},
hideModalHeader: true,
- RenderModal: ( { item, closeModal } ) => {
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
const { removeTemplate } = useDispatch( editSiteStore );
return (
@@ -254,7 +257,8 @@ export const duplicatePatternAction = {
label: _x( 'Duplicate', 'action label' ),
isEligible: ( item ) => item.type !== TEMPLATE_PART_POST_TYPE,
modalHeader: _x( 'Duplicate pattern', 'action label' ),
- RenderModal: ( { item, closeModal } ) => {
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs(
window.location.href
);
@@ -288,7 +292,8 @@ export const duplicateTemplatePartAction = {
label: _x( 'Duplicate', 'action label' ),
isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE,
modalHeader: _x( 'Duplicate template part', 'action label' ),
- RenderModal: ( { item, closeModal } ) => {
+ RenderModal: ( { items, closeModal } ) => {
+ const [ item ] = items;
const { createSuccessNotice } = useDispatch( noticesStore );
const { categoryId = PATTERN_DEFAULT_CATEGORY } = getQueryArgs(
window.location.href
diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js
index c0e0289311db6a..ddc48542ee1b7a 100644
--- a/packages/edit-site/src/components/page-templates/index.js
+++ b/packages/edit-site/src/components/page-templates/index.js
@@ -65,7 +65,9 @@ const { useHistory } = unlock( routerPrivateApis );
const EMPTY_ARRAY = [];
const defaultConfigPerViewType = {
- [ LAYOUT_TABLE ]: {},
+ [ LAYOUT_TABLE ]: {
+ primaryField: 'title',
+ },
[ LAYOUT_GRID ]: {
mediaField: 'preview',
primaryField: 'title',
@@ -84,7 +86,7 @@ const DEFAULT_VIEW = {
// All fields are visible by default, so it's
// better to keep track of the hidden ones.
hiddenFields: [ 'preview' ],
- layout: {},
+ layout: defaultConfigPerViewType[ LAYOUT_TABLE ],
filters: [],
};
diff --git a/packages/edit-site/src/components/page-templates/template-actions.js b/packages/edit-site/src/components/page-templates/template-actions.js
index 9f5897e31fb93e..7029d464ca8671 100644
--- a/packages/edit-site/src/components/page-templates/template-actions.js
+++ b/packages/edit-site/src/components/page-templates/template-actions.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { backup, trash } from '@wordpress/icons';
-import { __, sprintf } from '@wordpress/i18n';
+import { __, sprintf, _n } from '@wordpress/i18n';
import { useDispatch } from '@wordpress/data';
import { useMemo, useState } from '@wordpress/element';
import { store as coreStore } from '@wordpress/core-data';
@@ -19,6 +19,7 @@ import {
/**
* 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';
@@ -32,39 +33,64 @@ export function useResetTemplateAction() {
return useMemo(
() => ( {
id: 'reset-template',
- label: __( 'Reset template' ),
+ label: __( 'Reset' ),
isPrimary: true,
icon: backup,
isEligible: isTemplateRevertable,
- async callback( template ) {
+ supportsBulk: true,
+ async callback( templates ) {
try {
- await revertTemplate( template, { allowUndo: false } );
- await saveEditedEntityRecord(
- 'postType',
- template.type,
- template.id
- );
+ for ( const template of templates ) {
+ await revertTemplate( template, {
+ allowUndo: false,
+ } );
+ await saveEditedEntityRecord(
+ 'postType',
+ template.type,
+ template.id
+ );
+ }
createSuccessNotice(
- sprintf(
- /* translators: The template/part's name. */
- __( '"%s" reverted.' ),
- decodeEntities( template.title.rendered )
- ),
+ templates.length > 1
+ ? sprintf(
+ /* translators: The number of items. */
+ __( '%s items reverted.' ),
+ templates.length
+ )
+ : sprintf(
+ /* translators: The template/part's name. */
+ __( '"%s" reverted.' ),
+ decodeEntities(
+ templates[ 0 ].title.rendered
+ )
+ ),
{
type: 'snackbar',
id: 'edit-site-template-reverted',
}
);
} catch ( error ) {
- const fallbackErrorMessage =
- template.type === TEMPLATE_POST_TYPE
- ? __(
- 'An error occurred while reverting the template.'
- )
- : __(
- 'An error occurred while reverting the template part.'
- );
+ let fallbackErrorMessage;
+ if ( templates[ 0 ].type === TEMPLATE_POST_TYPE ) {
+ fallbackErrorMessage =
+ templates.length === 1
+ ? __(
+ 'An error occurred while reverting the template.'
+ )
+ : __(
+ 'An error occurred while reverting the templates.'
+ );
+ } else {
+ fallbackErrorMessage =
+ templates.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
@@ -85,21 +111,34 @@ export function useResetTemplateAction() {
export const deleteTemplateAction = {
id: 'delete-template',
- label: __( 'Delete template' ),
+ label: __( 'Delete' ),
isPrimary: true,
icon: trash,
isEligible: isTemplateRemovable,
+ supportsBulk: true,
hideModalHeader: true,
- RenderModal: ( { item: template, closeModal } ) => {
- const { removeTemplate } = useDispatch( editSiteStore );
+ RenderModal: ( { items: templates, closeModal, onPerform } ) => {
+ const { removeTemplates } = unlock( useDispatch( editSiteStore ) );
return (
- { sprintf(
- // translators: %s: The template or template part's title.
- __( 'Are you sure you want to delete "%s"?' ),
- decodeEntities( template.title.rendered )
- ) }
+ { 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
+ )
+ ) }
@@ -126,7 +169,8 @@ export const renameTemplateAction = {
label: __( 'Rename' ),
isEligible: ( template ) =>
isTemplateRemovable( template ) && template.is_custom,
- RenderModal: ( { item: template, closeModal } ) => {
+ RenderModal: ( { items: templates, closeModal } ) => {
+ const template = templates[ 0 ];
const title = decodeEntities( template.title.rendered );
const [ editedTitle, setEditedTitle ] = useState( title );
const {
diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js
index 5a8adad8e198b8..e7f2671784e1d0 100644
--- a/packages/edit-site/src/store/actions.js
+++ b/packages/edit-site/src/store/actions.js
@@ -5,7 +5,7 @@ import apiFetch from '@wordpress/api-fetch';
import { parse, __unstableSerializeAndClean } from '@wordpress/blocks';
import deprecated from '@wordpress/deprecated';
import { addQueryArgs } from '@wordpress/url';
-import { __, sprintf } from '@wordpress/i18n';
+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';
@@ -13,7 +13,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor';
import { store as editorStore } from '@wordpress/editor';
import { speak } from '@wordpress/a11y';
import { store as preferencesStore } from '@wordpress/preferences';
-import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
@@ -25,6 +24,8 @@ import {
TEMPLATE_PART_POST_TYPE,
NAVIGATION_POST_TYPE,
} from '../utils/constants';
+import { removeTemplates } from './private-actions';
+
/**
* Dispatches an action that toggles a feature flag.
*
@@ -133,54 +134,9 @@ export const addTemplate =
*
* @param {Object} template The template object.
*/
-export const removeTemplate =
- ( template ) =>
- async ( { registry } ) => {
- try {
- await registry
- .dispatch( coreStore )
- .deleteEntityRecord( 'postType', template.type, template.id, {
- force: true,
- } );
-
- const lastError = registry
- .select( coreStore )
- .getLastEntityDeleteError(
- 'postType',
- template.type,
- template.id
- );
-
- if ( lastError ) {
- throw lastError;
- }
-
- // Depending on how the entity was retrieved it's title might be
- // an object or simple string.
- const templateTitle =
- typeof template.title === 'string'
- ? template.title
- : template.title?.rendered;
-
- registry.dispatch( noticesStore ).createSuccessNotice(
- sprintf(
- /* translators: The template/part's name. */
- __( '"%s" deleted.' ),
- decodeEntities( templateTitle )
- ),
- { type: 'snackbar', id: 'site-editor-template-deleted-success' }
- );
- } catch ( error ) {
- const errorMessage =
- error.message && error.code !== 'unknown_error'
- ? error.message
- : __( 'An error occurred while deleting the template.' );
-
- registry
- .dispatch( noticesStore )
- .createErrorNotice( errorMessage, { type: 'snackbar' } );
- }
- };
+export const removeTemplate = ( template ) => {
+ return removeTemplates( [ template ] );
+};
/**
* Action that sets a template part.
diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js
index 7354f7b9b8843a..71f35dc66399ee 100644
--- a/packages/edit-site/src/store/private-actions.js
+++ b/packages/edit-site/src/store/private-actions.js
@@ -4,6 +4,10 @@
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';
/**
* Action that switches the canvas mode.
@@ -49,3 +53,102 @@ export const setEditorCanvasContainerView =
view,
} );
};
+
+/**
+ * Action that removes an array of templates.
+ *
+ * @param {Array} templates An array of template objects to remove.
+ */
+export const removeTemplates =
+ ( templates ) =>
+ async ( { registry } ) => {
+ const promiseResult = await Promise.allSettled(
+ templates.map( ( template ) => {
+ return registry
+ .dispatch( coreStore )
+ .deleteEntityRecord(
+ 'postType',
+ template.type,
+ template.id,
+ { force: true },
+ { throwOnError: true }
+ );
+ } )
+ );
+
+ // If all the promises were fulfilled with sucess.
+ if ( promiseResult.every( ( { status } ) => status === 'fulfilled' ) ) {
+ let successMessage;
+
+ if ( templates.length === 1 ) {
+ // Depending on how the entity was retrieved its title might be
+ // an object or simple string.
+ const templateTitle =
+ typeof templates[ 0 ].title === 'string'
+ ? templates[ 0 ].title
+ : templates[ 0 ].title?.rendered;
+ successMessage = sprintf(
+ /* translators: The template/part's name. */
+ __( '"%s" deleted.' ),
+ decodeEntities( templateTitle )
+ );
+ } else {
+ successMessage = __( 'Templates 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 = __(
+ 'An error occurred while deleting the template.'
+ );
+ }
+ // 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 = __(
+ 'An error occurred while deleting the templates.'
+ );
+ } else if ( errorMessages.size === 1 ) {
+ errorMessage = sprintf(
+ /* translators: %s: an error message */
+ __(
+ 'An error occurred while deleting the templates: %s'
+ ),
+ [ ...errorMessages ][ 0 ]
+ );
+ } else {
+ errorMessage = sprintf(
+ /* translators: %s: a list of comma separated error messages */
+ __(
+ 'Some errors occurred while deleting the templates: %s'
+ ),
+ [ ...errorMessages ].join( ',' )
+ );
+ }
+ }
+ registry
+ .dispatch( noticesStore )
+ .createErrorNotice( errorMessage, { type: 'snackbar' } );
+ }
+ };