Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions packages/block-editor/src/autocompleters/block.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,36 @@ function createBlockCompleter() {
blocks,
} = inserterItem;

const unsyncedFallbackBlock = createBlock(
name,
initialAttributes
);

const unsyncedBlocks = ( blocks ?? [] ).map( ( block ) =>
cloneBlock( block )
);

let value;
if ( syncStatus === 'unsynced' ) {
if ( name === 'core/block' && initialAttributes?.ref ) {
value = unsyncedFallbackBlock;
} else if ( unsyncedBlocks.length ) {
value = unsyncedBlocks;
} else {
value = unsyncedFallbackBlock;
}
} else {
value = createBlock(
name,
initialAttributes,
createBlocksFromInnerBlocksTemplate( innerBlocks ),
innerContent
);
}

return {
action: 'replace',
value:
syncStatus === 'unsynced'
? ( blocks ?? [] ).map( ( block ) =>
cloneBlock( block )
)
: createBlock(
name,
initialAttributes,
createBlocksFromInnerBlocksTemplate(
innerBlocks
),
innerContent
),
value,
};
},
};
Expand Down
89 changes: 89 additions & 0 deletions packages/block-editor/src/autocompleters/test/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import blockCompleter from '../block';

describe( 'block autocompleter', () => {
beforeAll( () => {
registerBlockType( 'core/block', {
apiVersion: 3,
save: () => null,
category: 'reusable',
title: 'Pattern',
attributes: {
ref: {
type: 'number',
},
},
} );
registerBlockType( 'core/html', {
apiVersion: 3,
save: () => null,
Expand All @@ -17,11 +28,30 @@ describe( 'block autocompleter', () => {
title: 'Paragraph',
attributes: { content: { type: 'string', source: 'html' } },
} );
registerBlockType( 'core/buttons', {
apiVersion: 3,
save: () => null,
category: 'design',
title: 'Buttons',
} );
registerBlockType( 'core/button', {
apiVersion: 3,
save: () => null,
category: 'design',
title: 'Button',
parent: [ 'core/buttons' ],
attributes: {
text: { type: 'string' },
},
} );
} );

afterAll( () => {
unregisterBlockType( 'core/block' );
unregisterBlockType( 'core/html' );
unregisterBlockType( 'core/paragraph' );
unregisterBlockType( 'core/button' );
unregisterBlockType( 'core/buttons' );
} );

describe( 'getOptionCompletion', () => {
Expand All @@ -43,5 +73,64 @@ describe( 'block autocompleter', () => {
expect( value.innerBlocks ).toHaveLength( 1 );
expect( value.innerBlocks[ 0 ].name ).toBe( 'core/paragraph' );
} );

it( 'falls back when an unsynced pattern has no parsed blocks', () => {
const { action, value } = blockCompleter.getOptionCompletion( {
name: 'core/block',
initialAttributes: { ref: 9 },
innerBlocks: [ [ 'core/paragraph', { content: 'Fallback' } ] ],
innerContent: [ '<div>', null, '</div>' ],
syncStatus: 'unsynced',
blocks: [],
} );

expect( action ).toBe( 'replace' );
expect( value.name ).toBe( 'core/block' );
expect( value.attributes ).toEqual( { ref: 9 } );
expect( value.innerBlocks ).toEqual( [] );
} );

it( 'keeps cloned blocks when an unsynced pattern has parsed blocks', () => {
const sourceBlock = {
name: 'core/paragraph',
attributes: { content: 'From pattern' },
innerBlocks: [],
clientId: 'source-client-id',
};

const { value } = blockCompleter.getOptionCompletion( {
name: 'core/html',
syncStatus: 'unsynced',
blocks: [ sourceBlock ],
} );

expect( Array.isArray( value ) ).toBe( true );
expect( value ).toHaveLength( 1 );
expect( value[ 0 ] ).not.toBe( sourceBlock );
expect( value[ 0 ].name ).toBe( 'core/paragraph' );
expect( value[ 0 ].attributes ).toEqual( {
content: 'From pattern',
} );
} );

it( 'keeps unsynced reusable patterns as a pattern block when parsed blocks are child-only', () => {
const sourceBlock = {
name: 'core/button',
attributes: { text: 'Fake Text!' },
innerBlocks: [],
clientId: 'child-only-client-id',
};

const { value } = blockCompleter.getOptionCompletion( {
name: 'core/block',
initialAttributes: { ref: 18 },
syncStatus: 'unsynced',
blocks: [ sourceBlock ],
} );

expect( Array.isArray( value ) ).toBe( false );
expect( value.name ).toBe( 'core/block' );
expect( value.attributes ).toEqual( { ref: 18 } );
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { registerBlockType, unregisterBlockType } from '@wordpress/blocks';
import { getPatternBlocksForInsertion } from '../use-patterns-state';
import { INSERTER_PATTERN_TYPES } from '../../block-patterns-tab/utils';

describe( 'getPatternBlocksForInsertion', () => {
beforeAll( () => {
registerBlockType( 'core/block', {
apiVersion: 3,
save: () => null,
category: 'reusable',
title: 'Pattern',
attributes: {
ref: {
type: 'number',
},
},
} );
registerBlockType( 'core/buttons', {
apiVersion: 3,
save: () => null,
category: 'design',
title: 'Buttons',
} );
registerBlockType( 'core/button', {
apiVersion: 3,
save: () => null,
category: 'design',
title: 'Button',
parent: [ 'core/buttons' ],
attributes: {
text: {
type: 'string',
},
},
} );
} );

afterAll( () => {
unregisterBlockType( 'core/block' );
unregisterBlockType( 'core/button' );
unregisterBlockType( 'core/buttons' );
} );

it( 'falls back to a reusable block when an unsynced user pattern has no parsed blocks', () => {
expect(
getPatternBlocksForInsertion(
{
type: INSERTER_PATTERN_TYPES.user,
syncStatus: 'unsynced',
id: 42,
},
[]
)
).toEqual( [
expect.objectContaining( {
name: 'core/block',
attributes: { ref: 42 },
} ),
] );
} );

it( 'falls back to a reusable block when a synced user pattern has parsed blocks', () => {
const result = getPatternBlocksForInsertion(
{
type: INSERTER_PATTERN_TYPES.user,
syncStatus: undefined,
id: 42,
},
[
{
name: 'core/button',
attributes: { text: 'Pattern content' },
innerBlocks: [],
},
]
);

expect( result ).toHaveLength( 1 );
expect( result[ 0 ].name ).toBe( 'core/block' );
expect( result[ 0 ].attributes ).toEqual( { ref: 42 } );
} );

it( 'keeps child-only root blocks for an unsynced user pattern', () => {
const result = getPatternBlocksForInsertion(
{
type: INSERTER_PATTERN_TYPES.user,
syncStatus: 'unsynced',
id: 42,
},
[
{
name: 'core/button',
attributes: { text: 'Pattern content' },
innerBlocks: [],
},
],
undefined
);

expect( result ).toHaveLength( 1 );
expect( result[ 0 ].name ).toBe( 'core/button' );
expect( result[ 0 ].attributes ).toEqual( {
text: 'Pattern content',
} );
} );
} );
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import {
getBlockType,
createBlock,
createBlocksFromInnerBlocksTemplate,
store as blocksStore,
parse,
} from '@wordpress/blocks';
import { useSelect } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { store as blockEditorStore } from '../../../store';
import { unlock } from '../../../lock-unlock';
import { isFiltered } from '../../../store/utils';

// Shared so the selector cache survives the inserter closing and reopening.
Expand Down Expand Up @@ -37,6 +41,10 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
const { getCategories, getCollections } = select( blocksStore );
return [ getCategories(), getCollections() ];
}, [] );
const { getClosestAllowedInsertionPoint } = unlock(
useSelect( blockEditorStore )
);
const { createErrorNotice } = useDispatch( noticesStore );

const onSelectItem = useCallback(
(
Expand All @@ -50,20 +58,65 @@ const useBlockTypesState = ( rootClientId, onInsert, isQuick ) => {
},
shouldFocusBlock
) => {
const insertedBlock =
syncStatus === 'unsynced'
? parse( content, {
__unstableSkipMigrationLogs: true,
} )
: createBlock(
name,
initialAttributes,
createBlocksFromInnerBlocksTemplate( innerBlocks ),
innerContent
);
onInsert( insertedBlock, undefined, shouldFocusBlock );
const destinationClientId = getClosestAllowedInsertionPoint(
name,
rootClientId
);
if ( destinationClientId === null ) {
const title = getBlockType( name )?.title ?? name;
createErrorNotice(
sprintf(
/* translators: %s: block pattern title. */
__( 'Block "%s" can\'t be inserted.' ),
title
),
{
type: 'snackbar',
id: 'inserter-notice',
}
);
return;
}

const unsyncedFallbackBlock = createBlock(
name,
initialAttributes
);

const unsyncedBlocks = parse( content, {
__unstableSkipMigrationLogs: true,
} );

let insertedBlock;
if ( syncStatus === 'unsynced' ) {
if ( name === 'core/block' && initialAttributes?.ref ) {
insertedBlock = [ unsyncedFallbackBlock ];
} else if ( unsyncedBlocks.length ) {
insertedBlock = unsyncedBlocks;
} else {
insertedBlock = [ unsyncedFallbackBlock ];
}
} else {
insertedBlock = createBlock(
name,
initialAttributes,
createBlocksFromInnerBlocksTemplate( innerBlocks ),
innerContent
);
}
onInsert(
insertedBlock,
undefined,
shouldFocusBlock,
destinationClientId
);
},
[ onInsert ]
[
getClosestAllowedInsertionPoint,
rootClientId,
onInsert,
createErrorNotice,
]
);

return [ items, categories, collections, onSelectItem ];
Expand Down
Loading
Loading