diff --git a/packages/edit-site/src/components/add-new-pattern/index.js b/packages/edit-site/src/components/add-new-pattern/index.js
index 5e0f1626fc8fd..e4b22031b1582 100644
--- a/packages/edit-site/src/components/add-new-pattern/index.js
+++ b/packages/edit-site/src/components/add-new-pattern/index.js
@@ -2,12 +2,16 @@
* WordPress dependencies
*/
import { DropdownMenu } from '@wordpress/components';
-import { useState } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { useState, useRef } from '@wordpress/element';
+import { __, sprintf } from '@wordpress/i18n';
import { plus, symbol, symbolFilled } from '@wordpress/icons';
-import { useSelect } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
import { privateApis as routerPrivateApis } from '@wordpress/router';
-import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns';
+import {
+ privateApis as editPatternsPrivateApis,
+ store as patternsStore,
+} from '@wordpress/patterns';
+import { store as noticesStore } from '@wordpress/notices';
/**
* Internal dependencies
@@ -16,12 +20,19 @@ import CreateTemplatePartModal from '../create-template-part-modal';
import SidebarButton from '../sidebar-button';
import { unlock } from '../../lock-unlock';
import { store as editSiteStore } from '../../store';
+import {
+ PATTERN_TYPES,
+ PATTERN_DEFAULT_CATEGORY,
+ TEMPLATE_PART_POST_TYPE,
+} from '../../utils/constants';
+import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories';
-const { useHistory } = unlock( routerPrivateApis );
+const { useHistory, useLocation } = unlock( routerPrivateApis );
const { CreatePatternModal } = unlock( editPatternsPrivateApis );
export default function AddNewPattern() {
const history = useHistory();
+ const { params } = useLocation();
const [ showPatternModal, setShowPatternModal ] = useState( false );
const [ showTemplatePartModal, setShowTemplatePartModal ] =
useState( false );
@@ -29,6 +40,11 @@ export default function AddNewPattern() {
const settings = select( editSiteStore ).getSettings();
return !! settings.supportsTemplatePartsMode;
}, [] );
+ const { createPatternFromFile } = unlock( useDispatch( patternsStore ) );
+ const { createSuccessNotice, createErrorNotice } =
+ useDispatch( noticesStore );
+ const patternUploadInputRef = useRef();
+ const { patternCategories } = usePatternCategories();
function handleCreatePattern( { pattern, categoryId } ) {
setShowPatternModal( false );
@@ -76,6 +92,14 @@ export default function AddNewPattern() {
} );
}
+ controls.push( {
+ icon: symbol,
+ onClick: () => {
+ patternUploadInputRef.current.click();
+ },
+ title: __( 'Import pattern from JSON' ),
+ } );
+
return (
<>
) }
+
+ {
+ const file = event.target.files?.[ 0 ];
+ if ( ! file ) return;
+ try {
+ const currentCategoryId =
+ params.categoryType !== TEMPLATE_PART_POST_TYPE &&
+ patternCategories.find(
+ ( category ) =>
+ category.name === params.categoryId
+ )?.id;
+ const pattern = await createPatternFromFile(
+ file,
+ currentCategoryId
+ ? [ currentCategoryId ]
+ : undefined
+ );
+
+ // Navigate to the All patterns category for the newly created pattern
+ // if we're not on that page already.
+ if ( ! currentCategoryId ) {
+ history.push( {
+ path: `/patterns`,
+ categoryType: PATTERN_TYPES.theme,
+ categoryId: PATTERN_DEFAULT_CATEGORY,
+ } );
+ }
+
+ createSuccessNotice(
+ sprintf(
+ // translators: %s: The imported pattern's title.
+ __( 'Imported "%s" from JSON.' ),
+ pattern.title.raw
+ ),
+ {
+ type: 'snackbar',
+ id: 'import-pattern-success',
+ }
+ );
+ } catch ( err ) {
+ createErrorNotice( err.message, {
+ type: 'snackbar',
+ id: 'import-pattern-error',
+ } );
+ } finally {
+ event.target.value = '';
+ }
+ } }
+ />
>
);
}
diff --git a/packages/edit-site/src/components/page-patterns/grid-item.js b/packages/edit-site/src/components/page-patterns/grid-item.js
index a9c6fdc2d9d1a..f1da1f925229d 100644
--- a/packages/edit-site/src/components/page-patterns/grid-item.js
+++ b/packages/edit-site/src/components/page-patterns/grid-item.js
@@ -2,6 +2,8 @@
* External dependencies
*/
import classnames from 'classnames';
+import downloadjs from 'downloadjs';
+import { paramCase as kebabCase } from 'change-case';
/**
* WordPress dependencies
@@ -108,6 +110,20 @@ function GridItem( { categoryId, item, ...props } ) {
};
const deleteItem = () =>
isTemplatePart ? removeTemplate( item ) : deletePattern();
+ const exportAsJSON = () => {
+ const json = {
+ __file: item.type,
+ title: item.title,
+ content: item.patternBlock.content.raw,
+ syncStatus: item.patternBlock.wp_pattern_sync_status,
+ };
+
+ return downloadjs(
+ JSON.stringify( json, null, 2 ),
+ `${ kebabCase( item.title ) }.json`,
+ 'application/json'
+ );
+ };
// Only custom patterns or custom template parts can be renamed or deleted.
const isCustomPattern =
@@ -276,6 +292,12 @@ function GridItem( { categoryId, item, ...props } ) {
onClose={ onClose }
label={ __( 'Duplicate' ) }
/>
+ { item.type === PATTERN_TYPES.user && (
+
+ ) }
+
{ isCustomPattern && (
{ isModalOpen && (
{
handleSuccess( pattern );
} }
diff --git a/packages/patterns/src/components/patterns-manage-button.js b/packages/patterns/src/components/patterns-manage-button.js
index eae307f1838de..5f407682bea0f 100644
--- a/packages/patterns/src/components/patterns-manage-button.js
+++ b/packages/patterns/src/components/patterns-manage-button.js
@@ -12,7 +12,8 @@ import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
-import { store as editorStore } from '../store';
+import { store as patternsStore } from '../store';
+import { unlock } from '../lock-unlock';
function PatternsManageButton( { clientId } ) {
const { canRemove, isVisible, innerBlockCount, managePatternsUrl } =
@@ -51,7 +52,11 @@ function PatternsManageButton( { clientId } ) {
[ clientId ]
);
- const { convertSyncedPatternToStatic } = useDispatch( editorStore );
+ // Ignore reason: false positive of the lint rule.
+ // eslint-disable-next-line @wordpress/no-unused-vars-before-return
+ const { convertSyncedPatternToStatic } = unlock(
+ useDispatch( patternsStore )
+ );
if ( ! isVisible ) {
return null;
diff --git a/packages/patterns/src/index.js b/packages/patterns/src/index.js
index ed74eba99ffae..38cbd8e95737d 100644
--- a/packages/patterns/src/index.js
+++ b/packages/patterns/src/index.js
@@ -1,6 +1,5 @@
/**
* Internal dependencies
*/
-import './store';
-
+export { store } from './store';
export * from './private-apis';
diff --git a/packages/patterns/src/store/actions.js b/packages/patterns/src/store/actions.js
index 589dad326d3b6..2861e4ce2dac0 100644
--- a/packages/patterns/src/store/actions.js
+++ b/packages/patterns/src/store/actions.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
-import { parse, serialize, createBlock } from '@wordpress/blocks';
+import { parse } from '@wordpress/blocks';
import { store as coreStore } from '@wordpress/core-data';
import { store as blockEditorStore } from '@wordpress/block-editor';
@@ -14,14 +14,14 @@ import { PATTERN_SYNC_TYPES } from '../constants';
/**
* Returns a generator converting one or more static blocks into a pattern, or creating a new empty pattern.
*
- * @param {string} title Pattern title.
- * @param {'full'|'unsynced'} syncType They way block is synced, 'full' or 'unsynced'.
- * @param {string[]|undefined} clientIds Optional client IDs of blocks to convert to pattern.
- * @param {number[]|undefined} categories Ids of any selected categories.
+ * @param {string} title Pattern title.
+ * @param {'full'|'unsynced'} syncType They way block is synced, 'full' or 'unsynced'.
+ * @param {string|undefined} [content] Optional serialized content of blocks to convert to pattern.
+ * @param {number[]|undefined} [categories] Ids of any selected categories.
*/
export const createPattern =
- ( title, syncType, clientIds, categories ) =>
- async ( { registry, dispatch } ) => {
+ ( title, syncType, content, categories ) =>
+ async ( { registry } ) => {
const meta =
syncType === PATTERN_SYNC_TYPES.unsynced
? {
@@ -31,13 +31,7 @@ export const createPattern =
const reusableBlock = {
title,
- content: clientIds
- ? serialize(
- registry
- .select( blockEditorStore )
- .getBlocksByClientId( clientIds )
- )
- : undefined,
+ content,
status: 'publish',
meta,
wp_pattern_category: categories,
@@ -47,18 +41,45 @@ export const createPattern =
.dispatch( coreStore )
.saveEntityRecord( 'postType', 'wp_block', reusableBlock );
- if ( syncType === 'unsynced' || ! clientIds ) {
- return updatedRecord;
+ return updatedRecord;
+ };
+
+/**
+ * Create a pattern from a JSON file.
+ * @param {File} file The JSON file instance of the pattern.
+ * @param {number[]|undefined} [categories] Ids of any selected categories.
+ */
+export const createPatternFromFile =
+ ( file, categories ) =>
+ async ( { dispatch } ) => {
+ const fileContent = await file.text();
+ /** @type {import('./types').PatternJSON} */
+ let parsedContent;
+ try {
+ parsedContent = JSON.parse( fileContent );
+ } catch ( e ) {
+ throw new Error( 'Invalid JSON file' );
+ }
+ if (
+ parsedContent.__file !== 'wp_block' ||
+ ! parsedContent.title ||
+ ! parsedContent.content ||
+ typeof parsedContent.title !== 'string' ||
+ typeof parsedContent.content !== 'string' ||
+ ( parsedContent.syncStatus &&
+ typeof parsedContent.syncStatus !== 'string' )
+ ) {
+ throw new Error( 'Invalid Pattern JSON file' );
}
- const newBlock = createBlock( 'core/block', {
- ref: updatedRecord.id,
- } );
- registry
- .dispatch( blockEditorStore )
- .replaceBlocks( clientIds, newBlock );
- dispatch.setEditingPattern( newBlock.clientId, true );
- return updatedRecord;
+ const pattern = await dispatch.createPattern(
+ parsedContent.title,
+ parsedContent.syncStatus,
+ parsedContent.content,
+ categories
+ );
+
+ return pattern;
};
/**
diff --git a/packages/patterns/src/store/index.js b/packages/patterns/src/store/index.js
index 6293a7b33408e..af3da7f0ff498 100644
--- a/packages/patterns/src/store/index.js
+++ b/packages/patterns/src/store/index.js
@@ -10,6 +10,7 @@ import reducer from './reducer';
import * as actions from './actions';
import { STORE_NAME } from './constants';
import * as selectors from './selectors';
+import { unlock } from '../lock-unlock';
/**
* Post editor data store configuration.
@@ -20,8 +21,6 @@ import * as selectors from './selectors';
*/
export const storeConfig = {
reducer,
- selectors,
- actions,
};
/**
@@ -36,3 +35,5 @@ export const store = createReduxStore( STORE_NAME, {
} );
register( store );
+unlock( store ).registerPrivateActions( actions );
+unlock( store ).registerPrivateSelectors( selectors );