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.