diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js
index aa3473e6e55d45..10f4a9838c8dad 100644
--- a/packages/edit-post/src/index.js
+++ b/packages/edit-post/src/index.js
@@ -27,7 +27,6 @@ import { unlock } from './lock-unlock';
const {
BackButton: __experimentalMainDashboardButton,
- registerDefaultActions,
registerCoreBlockBindingsSources,
bootstrapBlockBindingsSourcesFromServer,
} = unlock( editorPrivateApis );
@@ -97,7 +96,6 @@ export function initializeEditor(
enableFSEBlocks: settings.__unstableEnableFullSiteEditingBlocks,
} );
}
- registerDefaultActions();
// Show a console log warning if the browser is not in Standards rendering mode.
const documentMode =
diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js
index 922e2f6ab933ab..9face28c1bfe19 100644
--- a/packages/edit-site/src/index.js
+++ b/packages/edit-site/src/index.js
@@ -29,7 +29,6 @@ import { unlock } from './lock-unlock';
import App from './components/app';
const {
- registerDefaultActions,
registerCoreBlockBindingsSources,
bootstrapBlockBindingsSourcesFromServer,
} = unlock( editorPrivateApis );
@@ -59,7 +58,6 @@ export function initializeEditor( id, settings ) {
enableFSEBlocks: true,
} );
}
- registerDefaultActions();
// We dispatch actions and update the store synchronously before rendering
// so that we won't trigger unnecessary re-renders with useEffect.
diff --git a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap b/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap
deleted file mode 100644
index 27804e33f508b6..00000000000000
--- a/packages/editor/src/components/document-outline/test/__snapshots__/index.js.snap
+++ /dev/null
@@ -1,111 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`DocumentOutline header blocks present should match snapshot 1`] = `
-
-`;
-
-exports[`DocumentOutline header blocks present should render warnings for multiple h1 headings 1`] = `
-
-`;
diff --git a/packages/editor/src/components/document-outline/test/index.js b/packages/editor/src/components/document-outline/test/index.js
deleted file mode 100644
index b396d7cc78349f..00000000000000
--- a/packages/editor/src/components/document-outline/test/index.js
+++ /dev/null
@@ -1,185 +0,0 @@
-/**
- * External dependencies
- */
-import { render, screen, within } from '@testing-library/react';
-
-/**
- * WordPress dependencies
- */
-import {
- createBlock,
- registerBlockType,
- unregisterBlockType,
-} from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import DocumentOutline from '../';
-
-jest.mock( '@wordpress/block-editor', () => ( {
- BlockTitle: () => 'Block Title',
-} ) );
-jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() );
-
-function setupMockSelect( blocks ) {
- useSelect.mockImplementation( ( mapSelect ) => {
- return mapSelect( () => ( {
- getBlocks: () => blocks,
- getEditedPostAttribute: () => null,
- getPostType: () => null,
- } ) );
- } );
-}
-
-describe( 'DocumentOutline', () => {
- let paragraph, headingH1, headingH2, headingH3, nestedHeading;
- beforeAll( () => {
- registerBlockType( 'core/heading', {
- category: 'text',
- title: 'Heading',
- edit: () => {},
- save: () => {},
- attributes: {
- level: {
- type: 'number',
- default: 2,
- },
- content: {
- type: 'string',
- },
- },
- } );
-
- registerBlockType( 'core/paragraph', {
- category: 'text',
- title: 'Paragraph',
- edit: () => {},
- save: () => {},
- } );
-
- registerBlockType( 'core/columns', {
- category: 'text',
- title: 'Paragraph',
- edit: () => {},
- save: () => {},
- } );
-
- paragraph = createBlock( 'core/paragraph' );
- headingH1 = createBlock( 'core/heading', {
- content: 'Heading 1',
- level: 1,
- } );
- headingH2 = createBlock( 'core/heading', {
- content: 'Heading 2',
- level: 2,
- } );
- headingH3 = createBlock( 'core/heading', {
- content: 'Heading 3',
- level: 3,
- } );
- nestedHeading = createBlock( 'core/columns', undefined, [ headingH3 ] );
- } );
-
- afterAll( () => {
- unregisterBlockType( 'core/heading' );
- unregisterBlockType( 'core/paragraph' );
- } );
-
- describe( 'no header blocks present', () => {
- it( 'should not render when no blocks provided', () => {
- setupMockSelect( [] );
- render( );
-
- expect( screen.queryByRole( 'list' ) ).not.toBeInTheDocument();
- } );
-
- it( 'should not render when no heading blocks provided', () => {
- const blocks = [ paragraph ].map( ( block, index ) => {
- // Set client IDs to a predictable value.
- return { ...block, clientId: `clientId_${ index }` };
- } );
- setupMockSelect( blocks );
- render( );
-
- expect( screen.queryByRole( 'list' ) ).not.toBeInTheDocument();
- } );
- } );
-
- describe( 'header blocks present', () => {
- it( 'should match snapshot', () => {
- const blocks = [ headingH2, headingH3 ].map( ( block, index ) => {
- // Set client IDs to a predictable value.
- return { ...block, clientId: `clientId_${ index }` };
- } );
- setupMockSelect( blocks );
- render( );
-
- expect( screen.getByRole( 'list' ) ).toMatchSnapshot();
- } );
-
- it( 'should render an item when only one heading provided', () => {
- const blocks = [ headingH2 ];
- setupMockSelect( blocks );
- render( );
-
- const tableOfContentItem = within(
- screen.getByRole( 'list' )
- ).getByRole( 'listitem' );
- expect( tableOfContentItem ).toBeInTheDocument();
- expect( tableOfContentItem ).toHaveTextContent( 'Heading 2' );
- } );
-
- it( 'should render two items when two headings and some paragraphs provided', () => {
- const blocks = [
- paragraph,
- headingH2,
- paragraph,
- headingH3,
- paragraph,
- ];
- setupMockSelect( blocks );
- render( );
-
- expect(
- within( screen.getByRole( 'list' ) ).getAllByRole( 'listitem' )
- ).toHaveLength( 2 );
- } );
-
- it( 'should render warnings for multiple h1 headings', () => {
- const blocks = [ headingH1, paragraph, headingH1, paragraph ].map(
- ( block, index ) => {
- // Set client IDs to a predictable value.
- return { ...block, clientId: `clientId_${ index }` };
- }
- );
- setupMockSelect( blocks );
- render( );
-
- expect( screen.getByRole( 'list' ) ).toMatchSnapshot();
- } );
- } );
-
- describe( 'nested headings', () => {
- it( 'should render even if the heading is nested', () => {
- const blocks = [ headingH2, nestedHeading ];
- setupMockSelect( blocks );
- render( );
-
- // Unnested heading and nested heading should appear as items.
- const tableOfContentItems = within(
- screen.getByRole( 'list' )
- ).getAllByRole( 'listitem' );
- expect( tableOfContentItems ).toHaveLength( 2 );
-
- // Unnested heading test.
- expect( tableOfContentItems[ 0 ] ).toHaveTextContent( 'H2' );
- expect( tableOfContentItems[ 0 ] ).toHaveTextContent( 'Heading 2' );
-
- // Nested heading test.
- expect( tableOfContentItems[ 1 ] ).toHaveTextContent( 'H3' );
- expect( tableOfContentItems[ 1 ] ).toHaveTextContent( 'Heading 3' );
- } );
- } );
-} );
diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js
index 92a5cd0eec87d8..4afa2f614fa2c3 100644
--- a/packages/editor/src/components/post-actions/actions.js
+++ b/packages/editor/src/components/post-actions/actions.js
@@ -8,7 +8,7 @@ import { decodeEntities } from '@wordpress/html-entities';
import { store as coreStore } from '@wordpress/core-data';
import { __, sprintf, _x } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
-import { useMemo, useState } from '@wordpress/element';
+import { useMemo, useState, useEffect } from '@wordpress/element';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';
import { parse } from '@wordpress/blocks';
import { DataForm, isItemValid } from '@wordpress/dataviews';
@@ -589,6 +589,10 @@ export function usePostActions( { postType, onActionPerformed, context } ) {
},
[ postType ]
);
+ const { registerPostTypeActions } = unlock( useDispatch( editorStore ) );
+ useEffect( () => {
+ registerPostTypeActions( postType );
+ }, [ registerPostTypeActions, postType ] );
const duplicatePostAction = useDuplicatePostAction( postType );
const reorderPagesAction = useReorderPagesAction( postType );
diff --git a/packages/editor/src/dataviews/actions/export-pattern.native.tsx b/packages/editor/src/dataviews/actions/export-pattern.native.tsx
new file mode 100644
index 00000000000000..c58cffcbd79e89
--- /dev/null
+++ b/packages/editor/src/dataviews/actions/export-pattern.native.tsx
@@ -0,0 +1,3 @@
+const exportPattern = undefined;
+
+export default exportPattern;
diff --git a/packages/editor/src/dataviews/actions/index.ts b/packages/editor/src/dataviews/actions/index.ts
deleted file mode 100644
index 04addcb1cde4d2..00000000000000
--- a/packages/editor/src/dataviews/actions/index.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { type StoreDescriptor, dispatch } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import deletePost from './delete-post';
-import exportPattern from './export-pattern';
-import resetPost from './reset-post';
-import trashPost from './trash-post';
-import permanentlyDeletePost from './permanently-delete-post';
-import restorePost from './restore-post';
-
-// @ts-ignore
-import { store as editorStore } from '../../store';
-import { unlock } from '../../lock-unlock';
-
-export default function registerDefaultActions() {
- const { registerEntityAction } = unlock(
- dispatch( editorStore as StoreDescriptor )
- );
-
- registerEntityAction( 'postType', 'wp_block', exportPattern );
- registerEntityAction( 'postType', '*', resetPost );
- registerEntityAction( 'postType', '*', restorePost );
- registerEntityAction( 'postType', '*', deletePost );
- registerEntityAction( 'postType', '*', trashPost );
- registerEntityAction( 'postType', '*', permanentlyDeletePost );
-}
diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts
index 562e4140ed806a..745cc0ad82e934 100644
--- a/packages/editor/src/dataviews/store/private-actions.ts
+++ b/packages/editor/src/dataviews/store/private-actions.ts
@@ -1,7 +1,22 @@
/**
* WordPress dependencies
*/
+import { store as coreStore } from '@wordpress/core-data';
import type { Action } from '@wordpress/dataviews';
+import { doAction } from '@wordpress/hooks';
+
+/**
+ * Internal dependencies
+ */
+import deletePost from '../actions/delete-post';
+import exportPattern from '../actions/export-pattern';
+import resetPost from '../actions/reset-post';
+import trashPost from '../actions/trash-post';
+import permanentlyDeletePost from '../actions/permanently-delete-post';
+import restorePost from '../actions/restore-post';
+import type { PostType } from '../types';
+import { store as editorStore } from '../../store';
+import { unlock } from '../../lock-unlock';
export function registerEntityAction< Item >(
kind: string,
@@ -28,3 +43,56 @@ export function unregisterEntityAction(
actionId,
};
}
+
+export function setIsReady( kind: string, name: string ) {
+ return {
+ type: 'SET_IS_READY' as const,
+ kind,
+ name,
+ };
+}
+
+export const registerPostTypeActions =
+ ( postType: string ) =>
+ async ( { registry }: { registry: any } ) => {
+ const isReady = unlock( registry.select( editorStore ) ).isEntityReady(
+ 'postType',
+ postType
+ );
+ if ( isReady ) {
+ return;
+ }
+
+ unlock( registry.dispatch( editorStore ) ).setIsReady(
+ 'postType',
+ postType
+ );
+
+ const postTypeConfig = ( await registry
+ .resolveSelect( coreStore )
+ .getPostType( postType ) ) as PostType;
+
+ const actions = [
+ postTypeConfig.slug === 'wp_block' ? exportPattern : undefined,
+ resetPost,
+ restorePost,
+ deletePost,
+ trashPost,
+ permanentlyDeletePost,
+ ];
+
+ registry.batch( () => {
+ actions.forEach( ( action ) => {
+ if ( action === undefined ) {
+ return;
+ }
+ unlock( registry.dispatch( editorStore ) ).registerEntityAction(
+ 'postType',
+ postType,
+ action
+ );
+ } );
+ } );
+
+ doAction( 'core.registerPostTypeActions', postType );
+ };
diff --git a/packages/editor/src/dataviews/store/private-selectors.ts b/packages/editor/src/dataviews/store/private-selectors.ts
index bbe3d7ca95c7c1..117c5b30966a39 100644
--- a/packages/editor/src/dataviews/store/private-selectors.ts
+++ b/packages/editor/src/dataviews/store/private-selectors.ts
@@ -1,22 +1,14 @@
-/**
- * WordPress dependencies
- */
-import { createSelector } from '@wordpress/data';
-
/**
* Internal dependencies
*/
import type { State } from './reducer';
-export const getEntityActions = createSelector(
- ( state: State, kind: string, name: string ) => {
- return [
- ...( state.actions[ kind ]?.[ name ] ?? [] ),
- ...( state.actions[ kind ]?.[ '*' ] ?? [] ),
- ];
- },
- ( state: State, kind: string, name: string ) => [
- state.actions[ kind ]?.[ name ],
- state.actions[ kind ]?.[ '*' ],
- ]
-);
+const EMPTY_ARRAY = [] as [];
+
+export function getEntityActions( state: State, kind: string, name: string ) {
+ return state.actions[ kind ]?.[ name ] ?? EMPTY_ARRAY;
+}
+
+export function isEntityReady( state: State, kind: string, name: string ) {
+ return state.isReady[ kind ]?.[ name ];
+}
diff --git a/packages/editor/src/dataviews/store/reducer.ts b/packages/editor/src/dataviews/store/reducer.ts
index 0e66fc0fcac72c..9124b74f02860a 100644
--- a/packages/editor/src/dataviews/store/reducer.ts
+++ b/packages/editor/src/dataviews/store/reducer.ts
@@ -6,13 +6,31 @@ import type { Action } from '@wordpress/dataviews';
type ReduxAction =
| ReturnType< typeof import('./private-actions').registerEntityAction >
- | ReturnType< typeof import('./private-actions').unregisterEntityAction >;
+ | ReturnType< typeof import('./private-actions').unregisterEntityAction >
+ | ReturnType< typeof import('./private-actions').setIsReady >;
export type ActionState = Record< string, Record< string, Action< any >[] > >;
+export type ReadyState = Record< string, Record< string, boolean > >;
export type State = {
actions: ActionState;
+ isReady: ReadyState;
};
+function isReady( state: ReadyState = {}, action: ReduxAction ) {
+ switch ( action.type ) {
+ case 'SET_IS_READY':
+ return {
+ ...state,
+ [ action.kind ]: {
+ ...state[ action.kind ],
+ [ action.name ]: true,
+ },
+ };
+ }
+
+ return state;
+}
+
function actions( state: ActionState = {}, action: ReduxAction ) {
switch ( action.type ) {
case 'REGISTER_ENTITY_ACTION':
@@ -48,4 +66,5 @@ function actions( state: ActionState = {}, action: ReduxAction ) {
export default combineReducers( {
actions,
+ isReady,
} );
diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts
index fd569aa21769c9..47f11c88bfb978 100644
--- a/packages/editor/src/dataviews/types.ts
+++ b/packages/editor/src/dataviews/types.ts
@@ -36,5 +36,9 @@ export type PostWithPermissions = Post & {
};
};
+export interface PostType {
+ slug: string;
+}
+
// Will be unnecessary after typescript 5.0 upgrade.
export type CoreDataError = { message?: string; code?: string };
diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js
index f949be8e9321fb..a8a74d20261ce4 100644
--- a/packages/editor/src/private-apis.js
+++ b/packages/editor/src/private-apis.js
@@ -23,7 +23,6 @@ import {
mergeBaseAndUserConfigs,
GlobalStylesProvider,
} from './components/global-styles-provider';
-import registerDefaultActions from './dataviews/actions';
import {
registerCoreBlockBindingsSources,
bootstrapBlockBindingsSourcesFromServer,
@@ -46,7 +45,6 @@ lock( privateApis, {
ToolsMoreMenuGroup,
ViewMoreMenuGroup,
ResizableEditor,
- registerDefaultActions,
registerCoreBlockBindingsSources,
bootstrapBlockBindingsSourcesFromServer,
diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js
index 6a6c8702a02217..357a7344f631d4 100644
--- a/packages/editor/src/store/private-selectors.js
+++ b/packages/editor/src/store/private-selectors.js
@@ -25,7 +25,10 @@ import {
getCurrentPost,
__experimentalGetDefaultTemplatePartAreas,
} from './selectors';
-import { getEntityActions as _getEntityActions } from '../dataviews/store/private-selectors';
+import {
+ getEntityActions as _getEntityActions,
+ isEntityReady as _isEntityReady,
+} from '../dataviews/store/private-selectors';
const EMPTY_INSERTION_POINT = {
rootClientId: undefined,
@@ -164,6 +167,10 @@ export function getEntityActions( state, ...args ) {
return _getEntityActions( state.dataviews, ...args );
}
+export function isEntityReady( state, ...args ) {
+ return _isEntityReady( state.dataviews, ...args );
+}
+
/**
* Similar to getBlocksByName in @wordpress/block-editor, but only returns the top-most
* blocks that aren't descendants of the query block.