-
Notifications
You must be signed in to change notification settings - Fork 4.8k
PHP-only blocks: Generate inspector controls from attributes #74102
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
0803b24
Generate inspector controls from attributes
priethor d44c2a9
Add tests
priethor 4e9e83a
Avoid creating inspector controls for block supports
priethor 2a0b953
Add unit test for the new marker and fix existing ones
priethor 366d41f
Consolidate exclusion logic in PHP, before marking attributes as need…
priethor 222d2f2
Lint test file
priethor 77a975a
Add dataviews reference to block-library tsconfig
priethor ae964dd
Refactor: extract the inspector control generation to `block-editor`
priethor 74f9dcc
Refactor: move the field mapping to the `block-editor` package, too
priethor d9f91b5
Don't do special treatment for union types
priethor c56109d
Remove e2e assertions that became redundant after refactor
priethor 3083ac5
Remove label humanization
priethor 682ffb5
Support custom labels
priethor 479431c
Address feedback
priethor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
95 changes: 95 additions & 0 deletions
95
packages/block-editor/src/hooks/auto-inspector-controls.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 ); | ||
| }, | ||
| }; |
65 changes: 65 additions & 0 deletions
65
packages/block-editor/src/hooks/generate-fields-from-attributes.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
packages/block-editor/src/hooks/test/auto-inspector-controls.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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( [] ); | ||
| } ); | ||
| } ); | ||
| } ); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.