@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
330 lines (320 loc) • 12.5 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
}) => {
var _registry$select$get;
const existingBlockNames = (_registry$select$get = registry.select(preferencesStore).get('core', 'hiddenBlockTypes')) !== null && _registry$select$get !== void 0 ? _registry$select$get : [];
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
}) => {
var _registry$select$get2;
const existingBlockNames = (_registry$select$get2 = registry.select(preferencesStore).get('core', 'hiddenBlockTypes')) !== null && _registry$select$get2 !== void 0 ? _registry$select$get2 : [];
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'
});
}
};
//# sourceMappingURL=private-actions.js.map