Skip to content

Commit 1e6538b

Browse files
Mamadukanoisysocks
andauthored
Template Parts: Add an option to import widgets from the sidebars (#45509)
* Template Part: Introduce an option to import widgets * Only display active sidebars with widgets as options * Use the sidebar name as the new template part title * Support legacy widgets * Prefix imported template parts to avoid name collision * Add missing docblock * Update labels * Move settings into the advanced inspector controls panel * Update API response for sidebars and mark them as 'inactive' * A minor design adjustments * Fix the rendering order bug * Transform legacy widgets before importing * Avoid hardcoding names of the blocks with wildcard transformations * Skip 'wp_inactive_widgets' widget area * Use 'HStack' * Allow overriding Legacy Widget block settings during registration * Override only supports settings * Improve spacing * Add the legacy widget to the post editor Co-authored-by: Robert Anderson <robert@noisysocks.com>
1 parent 489d48a commit 1e6538b

File tree

14 files changed

+322
-2
lines changed

14 files changed

+322
-2
lines changed

lib/compat/wordpress-6.2/rest-api.php

+15
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,18 @@ function gutenberg_register_global_styles_endpoints() {
101101
$editor_settings->register_routes();
102102
}
103103
add_action( 'rest_api_init', 'gutenberg_register_global_styles_endpoints' );
104+
105+
/**
106+
* Updates REST API response for the sidebars and marks them as 'inactive'.
107+
*
108+
* Note: This can be a part of the `prepare_item_for_response` in `class-wp-rest-sidebars-controller.php`.
109+
*
110+
* @param WP_REST_Response $response The sidebar response object.
111+
* @return WP_REST_Response $response Updated response object.
112+
*/
113+
function gutenberg_modify_rest_sidebars_response( $response ) {
114+
$response->data['status'] = wp_is_block_theme() ? 'inactive' : 'active';
115+
116+
return $response;
117+
}
118+
add_filter( 'rest_prepare_sidebar', 'gutenberg_modify_rest_sidebars_response' );

lib/compat/wordpress-6.2/theme.php

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
/**
3+
* Theme overrides for WP 6.2.
4+
*
5+
* @package gutenberg
6+
*/
7+
8+
/**
9+
* Store legacy sidebars for later use by block themes.
10+
*
11+
* Note: This can be a part of the `switch_theme` method in `wp-includes/theme.php`.
12+
*
13+
* @param string $new_name Name of the new theme.
14+
* @param WP_Theme $new_theme WP_Theme instance of the new theme.
15+
*/
16+
function gutenberg_set_legacy_sidebars( $new_name, $new_theme ) {
17+
global $wp_registered_sidebars;
18+
19+
if ( $new_theme->is_block_theme() ) {
20+
set_theme_mod( 'wp_legacy_sidebars', $wp_registered_sidebars );
21+
}
22+
}
23+
add_action( 'switch_theme', 'gutenberg_set_legacy_sidebars', 10, 2 );

lib/compat/wordpress-6.2/widgets.php

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
/**
3+
* Core Widget APIs for WP 6.2.
4+
*
5+
* @package gutenberg
6+
*/
7+
8+
if ( ! function_exists( '_wp_block_theme_stub_sidebars' ) ) {
9+
/**
10+
* Register the previous theme's sidebars for the block themes.
11+
*
12+
* @return void
13+
*/
14+
function _wp_block_theme_stub_sidebars() {
15+
global $wp_registered_sidebars;
16+
17+
if ( ! wp_is_block_theme() ) {
18+
return;
19+
}
20+
21+
$legacy_sidebars = get_theme_mod( 'wp_legacy_sidebars' );
22+
if ( empty( $legacy_sidebars ) ) {
23+
return;
24+
}
25+
26+
// Don't use `register_sidebar` since it will enable the `widgets` support for a theme.
27+
foreach ( $legacy_sidebars as $sidebar ) {
28+
$wp_registered_sidebars[ $sidebar['id'] ] = $sidebar;
29+
}
30+
}
31+
add_action( 'widgets_init', '_wp_block_theme_stub_sidebars' );
32+
}

lib/load.php

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ function gutenberg_is_experiment_enabled( $name ) {
8585
require __DIR__ . '/compat/wordpress-6.2/edit-form-blocks.php';
8686
require __DIR__ . '/compat/wordpress-6.2/site-editor.php';
8787
require __DIR__ . '/compat/wordpress-6.2/block-editor-settings.php';
88+
require __DIR__ . '/compat/wordpress-6.2/theme.php';
89+
require __DIR__ . '/compat/wordpress-6.2/widgets.php';
8890

8991
// Experimental features.
9092
remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WP 6.0's stopgap handler for Webfonts API.

package-lock.json

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/block-library/src/template-part/edit/advanced-controls.js

+12
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ import { sprintf, __ } from '@wordpress/i18n';
77
import { InspectorControls } from '@wordpress/block-editor';
88
import { useSelect } from '@wordpress/data';
99

10+
/**
11+
* Internal dependencies
12+
*/
13+
import { TemplatePartImportControls } from './import-controls';
14+
1015
export function TemplatePartAdvancedControls( {
1116
tagName,
1217
setAttributes,
1318
isEntityAvailable,
1419
templatePartId,
1520
defaultWrapper,
21+
hasInnerBlocks,
1622
} ) {
1723
const [ area, setArea ] = useEntityProp(
1824
'postType',
@@ -87,6 +93,12 @@ export function TemplatePartAdvancedControls( {
8793
value={ tagName || '' }
8894
onChange={ ( value ) => setAttributes( { tagName: value } ) }
8995
/>
96+
{ ! hasInnerBlocks && (
97+
<TemplatePartImportControls
98+
area={ area }
99+
setAttributes={ setAttributes }
100+
/>
101+
) }
90102
</InspectorControls>
91103
);
92104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { __, sprintf } from '@wordpress/i18n';
5+
import { useMemo, useState } from '@wordpress/element';
6+
import { useDispatch, useSelect, useRegistry } from '@wordpress/data';
7+
import {
8+
Button,
9+
FlexBlock,
10+
FlexItem,
11+
SelectControl,
12+
__experimentalHStack as HStack,
13+
__experimentalSpacer as Spacer,
14+
} from '@wordpress/components';
15+
import {
16+
switchToBlockType,
17+
getPossibleBlockTransformations,
18+
} from '@wordpress/blocks';
19+
import { store as coreStore } from '@wordpress/core-data';
20+
import { store as noticesStore } from '@wordpress/notices';
21+
22+
/**
23+
* Internal dependencies
24+
*/
25+
import { useCreateTemplatePartFromBlocks } from './utils/hooks';
26+
import { transformWidgetToBlock } from './utils/transformers';
27+
28+
export function TemplatePartImportControls( { area, setAttributes } ) {
29+
const [ selectedSidebar, setSelectedSidebar ] = useState( '' );
30+
const [ isBusy, setIsBusy ] = useState( false );
31+
32+
const registry = useRegistry();
33+
const sidebars = useSelect( ( select ) => {
34+
return select( coreStore ).getSidebars( {
35+
per_page: -1,
36+
_fields: 'id,name,description,status,widgets',
37+
} );
38+
}, [] );
39+
const { createErrorNotice } = useDispatch( noticesStore );
40+
41+
const createFromBlocks = useCreateTemplatePartFromBlocks(
42+
area,
43+
setAttributes
44+
);
45+
46+
const options = useMemo( () => {
47+
const sidebarOptions = ( sidebars ?? [] )
48+
.filter(
49+
( widgetArea ) =>
50+
widgetArea.id !== 'wp_inactive_widgets' &&
51+
widgetArea.widgets.length > 0
52+
)
53+
.map( ( widgetArea ) => {
54+
return {
55+
value: widgetArea.id,
56+
label: widgetArea.name,
57+
};
58+
} );
59+
60+
if ( ! sidebarOptions.length ) {
61+
return [];
62+
}
63+
64+
return [
65+
{ value: '', label: __( 'Select widget area' ) },
66+
...sidebarOptions,
67+
];
68+
}, [ sidebars ] );
69+
70+
async function createFromWidgets( event ) {
71+
event.preventDefault();
72+
73+
if ( isBusy || ! selectedSidebar ) {
74+
return;
75+
}
76+
77+
setIsBusy( true );
78+
79+
const sidebar = options.find(
80+
( { value } ) => value === selectedSidebar
81+
);
82+
const { getWidgets } = registry.resolveSelect( coreStore );
83+
84+
// The widgets API always returns a successful response.
85+
const widgets = await getWidgets( {
86+
sidebar: sidebar.value,
87+
_embed: 'about',
88+
} );
89+
90+
const skippedWidgets = new Set();
91+
const blocks = widgets.flatMap( ( widget ) => {
92+
const block = transformWidgetToBlock( widget );
93+
94+
if ( block.name !== 'core/legacy-widget' ) {
95+
return block;
96+
}
97+
98+
const transforms = getPossibleBlockTransformations( [
99+
block,
100+
] ).filter( ( item ) => {
101+
// The block without any transformations can't be a wildcard.
102+
if ( ! item.transforms ) {
103+
return true;
104+
}
105+
106+
const hasWildCardFrom = item.transforms?.from?.find(
107+
( from ) => from.blocks && from.blocks.includes( '*' )
108+
);
109+
const hasWildCardTo = item.transforms?.to?.find(
110+
( to ) => to.blocks && to.blocks.includes( '*' )
111+
);
112+
113+
return ! hasWildCardFrom && ! hasWildCardTo;
114+
} );
115+
116+
// Skip the block if we have no matching transformations.
117+
if ( ! transforms.length ) {
118+
skippedWidgets.add( widget.id_base );
119+
return [];
120+
}
121+
122+
// Try transforming the Legacy Widget into a first matching block.
123+
return switchToBlockType( block, transforms[ 0 ].name );
124+
} );
125+
126+
await createFromBlocks(
127+
blocks,
128+
/* translators: %s: name of the widget area */
129+
sprintf( __( 'Widget area: %s' ), sidebar.label )
130+
);
131+
132+
if ( skippedWidgets.size ) {
133+
createErrorNotice(
134+
sprintf(
135+
/* translators: %s: the list of widgets */
136+
__( 'Unable to import the following widgets: %s.' ),
137+
Array.from( skippedWidgets ).join( ', ' )
138+
),
139+
{
140+
type: 'snackbar',
141+
}
142+
);
143+
}
144+
145+
setIsBusy( false );
146+
}
147+
148+
return (
149+
<Spacer marginBottom="4">
150+
<HStack as="form" onSubmit={ createFromWidgets }>
151+
<FlexBlock>
152+
<SelectControl
153+
label={ __( 'Import widget area' ) }
154+
value={ selectedSidebar }
155+
options={ options }
156+
onChange={ ( value ) => setSelectedSidebar( value ) }
157+
disabled={ ! options.length }
158+
__next36pxDefaultSize
159+
__nextHasNoMarginBottom
160+
/>
161+
</FlexBlock>
162+
<FlexItem
163+
style={ {
164+
marginBottom: '8px',
165+
marginTop: 'auto',
166+
} }
167+
>
168+
<Button
169+
variant="primary"
170+
type="submit"
171+
isBusy={ isBusy }
172+
aria-disabled={ isBusy || ! selectedSidebar }
173+
>
174+
{ __( 'Import' ) }
175+
</Button>
176+
</FlexItem>
177+
</HStack>
178+
</Spacer>
179+
);
180+
}

packages/block-library/src/template-part/edit/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export default function TemplatePartEdit( {
141141
isEntityAvailable={ isEntityAvailable }
142142
templatePartId={ templatePartId }
143143
defaultWrapper={ areaObject.tagName }
144+
hasInnerBlocks={ innerBlocks.length > 0 }
144145
/>
145146
{ isPlaceholder && (
146147
<TagName { ...blockProps }>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { createBlock, parse } from '@wordpress/blocks';
5+
6+
/**
7+
* Converts a widget entity record into a block.
8+
*
9+
* @param {Object} widget The widget entity record.
10+
* @return {Object} a block (converted from the entity record).
11+
*/
12+
export function transformWidgetToBlock( widget ) {
13+
if ( widget.id_base === 'block' ) {
14+
const parsedBlocks = parse( widget.instance.raw.content, {
15+
__unstableSkipAutop: true,
16+
} );
17+
if ( ! parsedBlocks.length ) {
18+
return createBlock( 'core/paragraph', {}, [] );
19+
}
20+
21+
return parsedBlocks[ 0 ];
22+
}
23+
24+
let attributes;
25+
if ( widget._embedded.about[ 0 ].is_multi ) {
26+
attributes = {
27+
idBase: widget.id_base,
28+
instance: widget.instance,
29+
};
30+
} else {
31+
attributes = {
32+
id: widget.id,
33+
};
34+
}
35+
36+
return createBlock( 'core/legacy-widget', attributes, [] );
37+
}

packages/edit-post/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@wordpress/url": "file:../url",
5353
"@wordpress/viewport": "file:../viewport",
5454
"@wordpress/warning": "file:../warning",
55+
"@wordpress/widgets": "file:../widgets",
5556
"classnames": "^2.3.1",
5657
"lodash": "^4.17.21",
5758
"memize": "^1.1.0",

packages/edit-post/src/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { render, unmountComponentAtNode } from '@wordpress/element';
1010
import { dispatch, select } from '@wordpress/data';
1111
import { addFilter } from '@wordpress/hooks';
1212
import { store as preferencesStore } from '@wordpress/preferences';
13+
import { registerLegacyWidgetBlock } from '@wordpress/widgets';
1314

1415
/**
1516
* Internal dependencies
@@ -115,6 +116,7 @@ export function initializeEditor(
115116
}
116117

117118
registerCoreBlocks();
119+
registerLegacyWidgetBlock( { inserter: false } );
118120
if ( process.env.IS_GUTENBERG_PLUGIN ) {
119121
__experimentalRegisterExperimentalCoreBlocks( {
120122
enableFSEBlocks: settings.__unstableEnableFullSiteEditingBlocks,

packages/edit-site/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@wordpress/style-engine": "file:../style-engine",
5555
"@wordpress/url": "file:../url",
5656
"@wordpress/viewport": "file:../viewport",
57+
"@wordpress/widgets": "file:../widgets",
5758
"classnames": "^2.3.1",
5859
"colord": "^2.9.2",
5960
"downloadjs": "^1.4.7",

0 commit comments

Comments
 (0)