Skip to content
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
46 changes: 46 additions & 0 deletions lib/compat/wordpress-7.0/php-only-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,49 @@ function gutenberg_register_auto_register_blocks() {
}

add_action( 'enqueue_block_editor_assets', 'gutenberg_register_auto_register_blocks', 5 );

/**
* Mark user-defined attributes for auto-generated inspector controls.
*
* This filter runs during block type registration, before the WP_Block_Type
* is instantiated. Block supports add their attributes AFTER the block type
* is created (via WP_Block_Supports::register_attributes()), so any attributes
* present at this stage are user-defined.
*
* The marker tells generateFieldsFromAttributes() which attributes should
* get auto-generated inspector controls. Attributes are excluded if they:
* - Have a 'source' (HTML-derived, edited inline not via inspector)
* - Have role 'local' (internal state, not user-configurable)
* - Were added by block supports (added after this filter runs)
*
* @param array $settings Array of block type arguments for registration.
* @return array Modified settings with marked attributes.
*/
function gutenberg_mark_auto_inspector_control_attributes( $settings ) {
if ( empty( $settings['attributes'] ) || ! is_array( $settings['attributes'] ) ) {
return $settings;
}

// Only process blocks with auto_register flag.
$has_auto_register = ! empty( $settings['supports']['auto_register'] );
if ( ! $has_auto_register ) {
return $settings;
}

foreach ( $settings['attributes'] as $name => $def ) {
// Skip HTML-derived attributes (edited inline, not via inspector).
if ( ! empty( $def['source'] ) ) {
continue;
}
// Skip internal attributes (not user-configurable).
if ( isset( $def['role'] ) && 'local' === $def['role'] ) {
continue;
}
$settings['attributes'][ $name ]['__experimentalAutoInspectorControl'] = true;
}

return $settings;
}

// Priority 5 to mark original attributes before other filters (priority 10+) might add their own.
add_filter( 'register_block_type_args', 'gutenberg_mark_auto_inspector_control_attributes', 5 );
Comment thread
priethor marked this conversation as resolved.
95 changes: 95 additions & 0 deletions packages/block-editor/src/hooks/auto-inspector-controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* WordPress dependencies
*/
import { getBlockType } from '@wordpress/blocks';
import { PanelBody } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { DataForm } from '@wordpress/dataviews';
import { useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import InspectorControls from '../components/inspector-controls';
import { useBlockEditingMode } from '../components/block-editing-mode';
import { store as blockEditorStore } from '../store';
import { generateFieldsFromAttributes } from './generate-fields-from-attributes';

/**
* Checks if a block has any attributes marked for auto-generated inspector controls.
*
* @param {Object} blockTypeAttributes - The block type's attributes object.
* @return {boolean} True if any attribute has __experimentalAutoInspectorControl marker.
*/
function hasAutoInspectorControlAttributes( blockTypeAttributes ) {
if ( ! blockTypeAttributes ) {
return false;
}
return Object.values( blockTypeAttributes ).some(
( attr ) => attr?.__experimentalAutoInspectorControl
);
}

/**
* Renders DataForm-based inspector controls for auto-registered PHP-only blocks.
*
* Fields are generated on-the-fly from attributes marked with `__experimentalAutoInspectorControl`
* during PHP registration.
*
* @param {Object} props Component props.
* @param {string} props.name Block name.
* @param {string} props.clientId Block client ID.
* @param {Function} props.setAttributes Function to update block attributes.
*/
function AutoRegisterControls( { name, clientId, setAttributes } ) {
const blockEditingMode = useBlockEditingMode();

const attributes = useSelect(
( select ) => select( blockEditorStore ).getBlockAttributes( clientId ),
[ clientId ]
);

const blockType = getBlockType( name );

// Generate fields from user-defined attributes marked by PHP.
// The __experimentalAutoInspectorControl marker excludes block support attributes
// (which have their own UI) and internal state (role: 'local').
// Memoized since blockType.attributes don't change after registration.
const { fields, form } = useMemo( () => {
if ( ! blockType?.attributes ) {
return { fields: [], form: { fields: [] } };
}
return generateFieldsFromAttributes( blockType.attributes );
}, [ blockType?.attributes ] );

if ( blockEditingMode !== 'default' ) {
return null;
}

if ( ! fields || fields.length === 0 ) {
return null;
}

return (
<InspectorControls>
<PanelBody title={ __( 'Settings' ) }>
<DataForm
data={ attributes }
fields={ fields }
form={ form }
onChange={ setAttributes }
/>
</PanelBody>
</InspectorControls>
);
}

export default {
edit: AutoRegisterControls,
attributeKeys: [],
hasSupport( name ) {
const blockType = getBlockType( name );
return hasAutoInspectorControlAttributes( blockType?.attributes );
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Generates DataForm field definitions from block attributes.
*
* This utility enables PHP-only blocks to have auto-generated inspector controls
* by converting block attribute definitions into DataForm field definitions.
*
* @param {Object} attributes - Block type attributes from block registration
* @return {{ fields: Array, form: Object }} fieldsKey and formKey values
*/
export function generateFieldsFromAttributes( attributes ) {
const fields = [];
const fieldIds = [];

Object.entries( attributes ).forEach( ( [ name, def ] ) => {
if ( ! def.__experimentalAutoInspectorControl ) {
return;
}

const field = createFieldFromAttribute( name, def );
if ( field ) {
fields.push( field );
fieldIds.push( name );
}
} );

return {
fields,
form: { fields: fieldIds },
};
}

/**
* Creates a DataForm field definition from a block attribute definition.
*
* @param {string} name - The attribute name
* @param {Object} def - The attribute definition from block.json
* @return {Object|null} DataForm field definition or null if type not supported
*/
function createFieldFromAttribute( name, def ) {
const type = def.type;

// Skip unsupported types (object, union types, etc.)
// Supported: string→text, number, integer, boolean (1:1 with DataForm)
if ( ! [ 'string', 'number', 'integer', 'boolean' ].includes( type ) ) {
return null;
}

const field = {
id: name,
label: def.label || name,
// Only 'string' needs mapping to 'text'; others are 1:1 with DataForm types.
// This mapping will be unnecessary once #74105 lands.
type: type === 'string' ? 'text' : type,
};

// Add elements for enums (DataForm shows select UI when elements are present)
if ( def.enum && Array.isArray( def.enum ) ) {
field.elements = def.enum.map( ( value ) => ( {
value,
label: String( value ),
} ) );
}

return field;
}
2 changes: 2 additions & 0 deletions packages/block-editor/src/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import blockBindingsPanel from './block-bindings';
import listView from './list-view';
import './block-renaming';
import './grid-visualizer';
import autoInspectorControls from './auto-inspector-controls';

createBlockEditFilter(
[
Expand All @@ -54,6 +55,7 @@ createBlockEditFilter(
childLayout,
allowedBlocks,
listView,
autoInspectorControls,
].filter( Boolean )
);
createBlockListBlockFilter( [
Expand Down
115 changes: 115 additions & 0 deletions packages/block-editor/src/hooks/test/auto-inspector-controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* WordPress dependencies
*/
import {
registerBlockType,
unregisterBlockType,
getBlockType,
} from '@wordpress/blocks';

/**
* Internal dependencies
*/
import autoInspectorControls from '../auto-inspector-controls';

describe( 'auto-inspector-controls', () => {
const blockName = 'test/auto-inspector-controls-block';

afterEach( () => {
if ( getBlockType( blockName ) ) {
unregisterBlockType( blockName );
}
} );

describe( 'hasSupport()', () => {
it( 'should return false for blocks without __experimentalAutoInspectorControl markers', () => {
registerBlockType( blockName, {
title: 'Test Block',
category: 'text',
attributes: {
content: {
type: 'string',
},
},
edit: () => null,
save: () => null,
} );

expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
false
);
} );

it( 'should return true for blocks with __experimentalAutoInspectorControl markers', () => {
registerBlockType( blockName, {
title: 'Test Block',
category: 'text',
attributes: {
title: {
type: 'string',
__experimentalAutoInspectorControl: true,
},
count: {
type: 'integer',
__experimentalAutoInspectorControl: true,
},
},
edit: () => null,
save: () => null,
} );

expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
true
);
} );

it( 'should return false for unregistered blocks', () => {
expect(
autoInspectorControls.hasSupport( 'non/existent-block' )
).toBe( false );
} );

it( 'should return false for blocks with no attributes', () => {
registerBlockType( blockName, {
title: 'Test Block',
category: 'text',
edit: () => null,
save: () => null,
} );

expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
false
);
} );

it( 'should return true when at least one attribute has __experimentalAutoInspectorControl', () => {
registerBlockType( blockName, {
title: 'Test Block',
category: 'text',
attributes: {
// This one has the marker
title: {
type: 'string',
__experimentalAutoInspectorControl: true,
},
// This one doesn't (e.g., added by block supports)
className: {
type: 'string',
},
},
edit: () => null,
save: () => null,
} );

expect( autoInspectorControls.hasSupport( blockName ) ).toBe(
true
);
} );
} );

describe( 'attributeKeys', () => {
it( 'should be an empty array', () => {
expect( autoInspectorControls.attributeKeys ).toEqual( [] );
} );
} );
} );
Loading
Loading