@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
495 lines (467 loc) • 13.4 kB
JavaScript
/**
* WordPress dependencies
*/
import { store as coreStore } from '@wordpress/core-data';
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';
export * from '../dataviews/store/private-actions';
/**
* Returns an action object used to set which template is currently being used/edited.
*
* @param {string} id Template Id.
*
* @return {Object} Action object.
*/
export function setCurrentTemplateId( id ) {
return {
type: 'SET_CURRENT_TEMPLATE_ID',
id,
};
}
/**
* Create a block based template.
*
* @param {Object?} template Template to create and assign.
*/
export const createTemplate =
( template ) =>
async ( { select, dispatch, registry } ) => {
const savedTemplate = await registry
.dispatch( coreStore )
.saveEntityRecord( 'postType', 'wp_template', template );
registry
.dispatch( coreStore )
.editEntityRecord(
'postType',
select.getCurrentPostType(),
select.getCurrentPostId(),
{
template: savedTemplate.slug,
}
);
registry
.dispatch( noticesStore )
.createSuccessNotice(
__( "Custom template created. You're in template mode now." ),
{
type: 'snackbar',
actions: [
{
label: __( 'Go back' ),
onClick: () =>
dispatch.setRenderingMode(
select.getEditorSettings()
.defaultRenderingMode
),
},
],
}
);
return savedTemplate;
};
/**
* Update the provided block types to be visible.
*
* @param {string[]} blockNames Names of block types to show.
*/
export const showBlockTypes =
( blockNames ) =>
( { registry } ) => {
const existingBlockNames =
registry
.select( preferencesStore )
.get( 'core', 'hiddenBlockTypes' ) ?? [];
const newBlockNames = existingBlockNames.filter(
( type ) =>
! (
Array.isArray( blockNames ) ? blockNames : [ blockNames ]
).includes( type )
);
registry
.dispatch( preferencesStore )
.set( 'core', 'hiddenBlockTypes', newBlockNames );
};
/**
* Update the provided block types to be hidden.
*
* @param {string[]} blockNames Names of block types to hide.
*/
export const hideBlockTypes =
( blockNames ) =>
( { registry } ) => {
const existingBlockNames =
registry
.select( preferencesStore )
.get( 'core', 'hiddenBlockTypes' ) ?? [];
const mergedBlockNames = new Set( [
...existingBlockNames,
...( Array.isArray( blockNames ) ? blockNames : [ blockNames ] ),
] );
registry
.dispatch( preferencesStore )
.set( 'core', 'hiddenBlockTypes', [ ...mergedBlockNames ] );
};
/**
* Save entity records marked as dirty.
*
* @param {Object} options Options for the action.
* @param {Function} [options.onSave] Callback when saving happens.
* @param {object[]} [options.dirtyEntityRecords] Array of dirty entities.
* @param {object[]} [options.entitiesToSkip] Array of entities to skip saving.
* @param {Function} [options.close] Callback when the actions is called. It should be consolidated with `onSave`.
*/
export const saveDirtyEntities =
( { onSave, dirtyEntityRecords = [], entitiesToSkip = [], close } = {} ) =>
( { registry } ) => {
const PUBLISH_ON_SAVE_ENTITIES = [
{ kind: 'postType', name: 'wp_navigation' },
];
const saveNoticeId = 'site-editor-save-success';
const homeUrl = registry
.select( coreStore )
.getEntityRecord( 'root', '__unstableBase' )?.home;
registry.dispatch( noticesStore ).removeNotice( saveNoticeId );
const entitiesToSave = dirtyEntityRecords.filter(
( { kind, name, key, property } ) => {
return ! entitiesToSkip.some(
( elt ) =>
elt.kind === kind &&
elt.name === name &&
elt.key === key &&
elt.property === property
);
}
);
close?.( entitiesToSave );
const siteItemsToSave = [];
const pendingSavedRecords = [];
entitiesToSave.forEach( ( { kind, name, key, property } ) => {
if ( 'root' === kind && 'site' === name ) {
siteItemsToSave.push( property );
} else {
if (
PUBLISH_ON_SAVE_ENTITIES.some(
( typeToPublish ) =>
typeToPublish.kind === kind &&
typeToPublish.name === name
)
) {
registry
.dispatch( coreStore )
.editEntityRecord( kind, name, key, {
status: 'publish',
} );
}
pendingSavedRecords.push(
registry
.dispatch( coreStore )
.saveEditedEntityRecord( kind, name, key )
);
}
} );
if ( siteItemsToSave.length ) {
pendingSavedRecords.push(
registry
.dispatch( coreStore )
.__experimentalSaveSpecifiedEntityEdits(
'root',
'site',
undefined,
siteItemsToSave
)
);
}
registry
.dispatch( blockEditorStore )
.__unstableMarkLastChangeAsPersistent();
Promise.all( pendingSavedRecords )
.then( ( values ) => {
return onSave ? onSave( values ) : values;
} )
.then( ( values ) => {
if (
values.some( ( value ) => typeof value === 'undefined' )
) {
registry
.dispatch( noticesStore )
.createErrorNotice( __( 'Saving failed.' ) );
} else {
registry
.dispatch( noticesStore )
.createSuccessNotice( __( 'Site updated.' ), {
type: 'snackbar',
id: saveNoticeId,
actions: [
{
label: __( 'View site' ),
url: homeUrl,
},
],
} );
}
} )
.catch( ( error ) =>
registry
.dispatch( noticesStore )
.createErrorNotice(
`${ __( 'Saving failed.' ) } ${ error }`
)
);
};
/**
* 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: template.origin }
);
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, template parts or patterns.
*
* @param {Array} items An array of template,template part or pattern objects to remove.
*/
export const removeTemplates =
( items ) =>
async ( { registry } ) => {
const isResetting = items.every( ( item ) => item?.has_theme_file );
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.
let title;
if ( typeof items[ 0 ].title === 'string' ) {
title = items[ 0 ].title;
} else if ( typeof items[ 0 ].title?.rendered === 'string' ) {
title = items[ 0 ].title?.rendered;
} else if ( typeof items[ 0 ].title?.raw === 'string' ) {
title = items[ 0 ].title?.raw;
}
successMessage = isResetting
? sprintf(
/* translators: The template/part's name. */
__( '"%s" reset.' ),
decodeEntities( title )
)
: sprintf(
/* translators: The template/part's name. */
__( '"%s" deleted.' ),
decodeEntities( title )
);
} else {
successMessage = isResetting
? __( 'Items reset.' )
: __( 'Items deleted.' );
}
registry
.dispatch( noticesStore )
.createSuccessNotice( successMessage, {
type: 'snackbar',
id: '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 = isResetting
? __( 'An error occurred while reverting the item.' )
: __( 'An error occurred while deleting the item.' );
}
// 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 items.'
);
} else if ( errorMessages.size === 1 ) {
errorMessage = isResetting
? sprintf(
/* translators: %s: an error message */
__(
'An error occurred while reverting the items: %s'
),
[ ...errorMessages ][ 0 ]
)
: sprintf(
/* translators: %s: an error message */
__(
'An error occurred while deleting the items: %s'
),
[ ...errorMessages ][ 0 ]
);
} else {
errorMessage = isResetting
? sprintf(
/* translators: %s: a list of comma separated error messages */
__(
'Some errors occurred while reverting the items: %s'
),
[ ...errorMessages ].join( ',' )
)
: sprintf(
/* translators: %s: a list of comma separated error messages */
__(
'Some errors occurred while deleting the items: %s'
),
[ ...errorMessages ].join( ',' )
);
}
}
registry
.dispatch( noticesStore )
.createErrorNotice( errorMessage, { type: 'snackbar' } );
}
};