Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix moving inner blocks in the Widgets Customizer #33243

Merged
merged 2 commits into from
Jul 8, 2021
Merged
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
Original file line number Diff line number Diff line change
@@ -1,100 +1,19 @@
/**
* External dependencies
*/
import { omit, isEqual } from 'lodash';
import { isEqual } from 'lodash';

/**
* WordPress dependencies
*/
import { serialize, parse, createBlock } from '@wordpress/blocks';
import { useState, useEffect, useCallback } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { getWidgetIdFromBlock, addWidgetIdToBlock } from '@wordpress/widgets';

function blockToWidget( block, existingWidget = null ) {
let widget;

const isValidLegacyWidgetBlock =
block.name === 'core/legacy-widget' &&
( block.attributes.id || block.attributes.instance );

if ( isValidLegacyWidgetBlock ) {
if ( block.attributes.id ) {
// Widget that does not extend WP_Widget.
widget = {
id: block.attributes.id,
};
} else {
const { encoded, hash, raw, ...rest } = block.attributes.instance;

// Widget that extends WP_Widget.
widget = {
idBase: block.attributes.idBase,
instance: {
...existingWidget?.instance,
// Required only for the customizer.
is_widget_customizer_js_value: true,
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest,
},
};
}
} else {
const instance = {
content: serialize( block ),
};
widget = {
idBase: 'block',
widgetClass: 'WP_Widget_Block',
instance: {
raw_instance: instance,
},
};
}

return {
...omit( existingWidget, [ 'form', 'rendered' ] ),
...widget,
};
}

function widgetToBlock( { id, idBase, number, instance } ) {
let block;

const {
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest
} = instance;

if ( idBase === 'block' ) {
const parsedBlocks = parse( raw.content );
block = parsedBlocks.length
? parsedBlocks[ 0 ]
: createBlock( 'core/paragraph', {} );
} else if ( number ) {
// Widget that extends WP_Widget.
block = createBlock( 'core/legacy-widget', {
idBase,
instance: {
encoded,
hash,
raw,
...rest,
},
} );
} else {
// Widget that does not extend WP_Widget.
block = createBlock( 'core/legacy-widget', {
id,
} );
}

return addWidgetIdToBlock( block, id );
}
/**
* Internal dependencies
*/
import { blockToWidget, widgetToBlock } from '../../utils';

function widgetsToBlocks( widgets ) {
return widgets.map( ( widget ) => widgetToBlock( widget ) );
Expand Down
44 changes: 37 additions & 7 deletions packages/customize-widgets/src/filters/move-to-sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import { createHigherOrderComponent } from '@wordpress/compose';
import { useSelect } from '@wordpress/data';
import { useSelect, useDispatch } from '@wordpress/data';
import { addFilter } from '@wordpress/hooks';
import { MoveToWidgetArea, getWidgetIdFromBlock } from '@wordpress/widgets';

Expand All @@ -22,14 +22,17 @@ import {
useSidebarControls,
useActiveSidebarControl,
} from '../components/sidebar-controls';
import { useFocusControl } from '../components/focus-control';
import { blockToWidget } from '../utils';

const withMoveToSidebarToolbarItem = createHigherOrderComponent(
( BlockEdit ) => ( props ) => {
const widgetId = getWidgetIdFromBlock( props );
let widgetId = getWidgetIdFromBlock( props );
const sidebarControls = useSidebarControls();
const activeSidebarControl = useActiveSidebarControl();
const hasMultipleSidebars = sidebarControls?.length > 1;
const blockName = props.name;
const clientId = props.clientId;
const canInsertBlockInSidebar = useSelect(
( select ) => {
// Use an empty string to represent the root block list, which
Expand All @@ -41,19 +44,46 @@ const withMoveToSidebarToolbarItem = createHigherOrderComponent(
},
[ blockName ]
);
const block = useSelect(
( select ) => select( blockEditorStore ).getBlock( clientId ),
[ clientId ]
);
const { removeBlock } = useDispatch( blockEditorStore );
const [ , focusWidget ] = useFocusControl();

function moveToSidebar( sidebarControlId ) {
const newSidebarControl = sidebarControls.find(
( sidebarControl ) => sidebarControl.id === sidebarControlId
);

const oldSetting = activeSidebarControl.setting;
const newSetting = newSidebarControl.setting;
if ( widgetId ) {
/**
* If there's a widgetId, move it to the other sidebar.
*/
const oldSetting = activeSidebarControl.setting;
const newSetting = newSidebarControl.setting;

oldSetting( without( oldSetting(), widgetId ) );
newSetting( [ ...newSetting(), widgetId ] );
} else {
/**
* If there isn't a widgetId, it's most likely a inner block.
* First, remove the block in the original sidebar,
* then, create a new widget in the new sidebar and get back its widgetId.
*/
const sidebarAdapter = newSidebarControl.sidebarAdapter;

oldSetting( without( oldSetting(), widgetId ) );
newSetting( [ ...newSetting(), widgetId ] );
removeBlock( clientId );
const addedWidgetIds = sidebarAdapter.setWidgets( [
...sidebarAdapter.getWidgets(),
blockToWidget( block ),
] );
// The last non-null id is the added widget's id.
widgetId = addedWidgetIds.reverse().find( ( id ) => !! id );
}

newSidebarControl.expand();
// Move focus to the moved widget and expand the sidebar.
focusWidget( widgetId );
}

return (
Expand Down
112 changes: 112 additions & 0 deletions packages/customize-widgets/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
// @ts-check
/**
* WordPress dependencies
*/
import { serialize, parse, createBlock } from '@wordpress/blocks';
import { addWidgetIdToBlock } from '@wordpress/widgets';

/**
* External dependencies
*/
import { omit } from 'lodash';

/**
* Convert settingId to widgetId.
Expand All @@ -18,3 +28,105 @@ export function settingIdToWidgetId( settingId ) {

return settingId;
}

/**
* Transform a block to a customizable widget.
*
* @param {WPBlock} block The block to be transformed from.
* @param {Object} existingWidget The widget to be extended from.
* @return {Object} The transformed widget.
*/
export function blockToWidget( block, existingWidget = null ) {
let widget;

const isValidLegacyWidgetBlock =
block.name === 'core/legacy-widget' &&
( block.attributes.id || block.attributes.instance );

if ( isValidLegacyWidgetBlock ) {
if ( block.attributes.id ) {
// Widget that does not extend WP_Widget.
widget = {
id: block.attributes.id,
};
} else {
const { encoded, hash, raw, ...rest } = block.attributes.instance;

// Widget that extends WP_Widget.
widget = {
idBase: block.attributes.idBase,
instance: {
...existingWidget?.instance,
// Required only for the customizer.
is_widget_customizer_js_value: true,
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest,
},
};
}
} else {
const instance = {
content: serialize( block ),
};
widget = {
idBase: 'block',
widgetClass: 'WP_Widget_Block',
instance: {
raw_instance: instance,
},
};
}

return {
...omit( existingWidget, [ 'form', 'rendered' ] ),
...widget,
};
}

/**
* Transform a widget to a block.
*
* @param {Object} widget The widget to be transformed from.
* @param {string} widget.id The widget id.
* @param {string} widget.idBase The id base of the widget.
* @param {number} widget.number The number/index of the widget.
* @param {Object} widget.instance The instance of the widget.
* @return {WPBlock} The transformed block.
*/
export function widgetToBlock( { id, idBase, number, instance } ) {
let block;

const {
encoded_serialized_instance: encoded,
instance_hash_key: hash,
raw_instance: raw,
...rest
} = instance;

if ( idBase === 'block' ) {
const parsedBlocks = parse( raw.content );
block = parsedBlocks.length
? parsedBlocks[ 0 ]
: createBlock( 'core/paragraph', {} );
} else if ( number ) {
// Widget that extends WP_Widget.
block = createBlock( 'core/legacy-widget', {
idBase,
instance: {
encoded,
hash,
raw,
...rest,
},
} );
} else {
// Widget that does not extend WP_Widget.
block = createBlock( 'core/legacy-widget', {
id,
} );
}

return addWidgetIdToBlock( block, id );
}
63 changes: 63 additions & 0 deletions packages/e2e-tests/specs/widgets/customizing-widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,69 @@ describe( 'Widgets Customizer', () => {
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );

it( 'should move (inner) blocks to another sidebar', async () => {
const widgetsPanel = await find( {
role: 'heading',
name: /Widgets/,
level: 3,
} );
await widgetsPanel.click();

const footer1Section = await find( {
role: 'heading',
name: /Footer #1/,
level: 3,
} );
await footer1Section.click();

await addBlock( 'Paragraph' );
await page.keyboard.type( 'First Paragraph' );

await showBlockToolbar();
await clickBlockToolbarButton( 'Options' );
const groupButton = await find( {
role: 'menuitem',
name: 'Group',
} );
await groupButton.click();

// Refocus the paragraph block.
const paragraphBlock = await find( {
role: 'group',
name: 'Paragraph block',
value: 'First Paragraph',
} );
await paragraphBlock.focus();
await showBlockToolbar();
await clickBlockToolbarButton( 'Move to widget area' );

const footer2Option = await find( {
role: 'menuitemradio',
name: 'Footer #2',
} );
await footer2Option.click();

// Should switch to and expand Footer #2.
await expect( {
role: 'heading',
name: 'Customizing ▸ Widgets Footer #2',
} ).toBeFound();

// The paragraph block should be moved to the new sidebar and have focus.
const movedParagraphBlockQuery = {
role: 'group',
name: 'Paragraph block',
value: 'First Paragraph',
};
await expect( movedParagraphBlockQuery ).toBeFound();
const movedParagraphBlock = await find( movedParagraphBlockQuery );
await expect( movedParagraphBlock ).toHaveFocus();

expect( console ).toHaveWarned(
"The page delivered both an 'X-Frame-Options' header and a 'Content-Security-Policy' header with a 'frame-ancestors' directive. Although the 'X-Frame-Options' header alone would have blocked embedding, it has been ignored."
);
} );
} );

/**
Expand Down