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
3 changes: 3 additions & 0 deletions backport-changelog/7.1/12151.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/12151

* https://github.com/WordPress/gutenberg/pull/79102
20 changes: 17 additions & 3 deletions lib/class-wp-icons-registry-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ protected function __construct() {
$this->register(
'core/' . $icon_name,
array(
'label' => $icon_data['label'],
'file_path' => $icons_directory . $icon_data['filePath'],
'label' => $icon_data['label'],
'file_path' => $icons_directory . $icon_data['filePath'],
'show_in_rest' => ! empty( $icon_data['showInRest'] ),
)
);
}
Expand All @@ -62,6 +63,7 @@ protected function __construct() {
* If not provided, the content will be retrieved from the `file_path` if set.
* If both `content` and `file_path` are not set, the icon will not be registered.
* @type string $file_path Optional. The full path to the file containing the icon content.
* @type bool $show_in_rest Optional. Whether the icon is exposed through the REST API.
* }
* @return bool True if the icon was registered with success and false otherwise.
*/
Expand Down Expand Up @@ -103,7 +105,7 @@ protected function register( $icon_name, $icon_properties ) {
return false;
}

$allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 );
$allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path', 'show_in_rest' ), 1 );
foreach ( array_keys( $icon_properties ) as $key ) {
if ( ! array_key_exists( $key, $allowed_keys ) ) {
_doing_it_wrong(
Expand Down Expand Up @@ -161,6 +163,15 @@ protected function register( $icon_name, $icon_properties ) {
}
}

if ( isset( $icon_properties['show_in_rest'] ) && ! is_bool( $icon_properties['show_in_rest'] ) ) {
_doing_it_wrong(
__METHOD__,
__( 'Icon show_in_rest flag must be a boolean.', 'gutenberg' ),
'7.1.0'
);
return false;
}

$icon = array_merge(
$icon_properties,
array( 'name' => $icon_name )
Expand Down Expand Up @@ -280,6 +291,9 @@ function gutenberg_override_wp_icons_registry() {
} else {
continue;
}
if ( isset( $icon['show_in_rest'] ) ) {
$icon_properties['show_in_rest'] = $icon['show_in_rest'];
}
$register_method->invoke( $gutenberg_registry, $icon['name'], $icon_properties );
}
}
Expand Down
43 changes: 43 additions & 0 deletions lib/class-wp-rest-icons-controller-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,47 @@
* @since 7.1.0
*/
class WP_REST_Icons_Controller_Gutenberg extends WP_REST_Icons_Controller {
/**
* Retrieves all icons.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be fully reimplementing the parent class method. Should we try to reuse it as much as possible and just sprinkle the show_in_rest filter logic on top?

$response = array();
$search = $request->get_param( 'search' );
$icons = WP_Icons_Registry::get_instance()->get_registered_icons( $search );
foreach ( $icons as $icon ) {
if ( empty( $icon['show_in_rest'] ) ) {
continue;
}
$prepared_icon = $this->prepare_item_for_response( $icon, $request );
$response[] = $this->prepare_response_for_collection( $prepared_icon );
}
return rest_ensure_response( $response );
}

/**
* Retrieves a specific icon from the registry.
*
* @param string $name Icon name.
* @return array|WP_Error Icon data on success, or WP_Error object on failure.
*/
public function get_icon( $name ) {
$icon = parent::get_icon( $name );

if ( ! is_wp_error( $icon ) && empty( $icon['show_in_rest'] ) ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also needs a test IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, what's the scenario to this path of the code? Won't parent::get_icon() already return a 404 for non-REST icons?

return new WP_Error(
'rest_icon_not_found',
sprintf(
// translators: %s is the name of any user-provided name
__( 'Icon not found: "%s".', 'gutenberg' ),
$name
),
array( 'status' => 404 )
);
}

return $icon;
}
}
17 changes: 14 additions & 3 deletions lib/compat/wordpress-7.0/class-wp-icons-registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ protected function __construct() {
$this->register(
'core/' . $icon_name,
array(
'label' => $icon_data['label'],
'file_path' => $icons_directory . $icon_data['filePath'],
'label' => $icon_data['label'],
'file_path' => $icons_directory . $icon_data['filePath'],
'show_in_rest' => ! empty( $icon_data['showInRest'] ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add test for the show_in_rest REST API exclusion behavior?

)
);
}
Expand All @@ -87,6 +88,7 @@ protected function __construct() {
* If not provided, the content will be retrieved from the `file_path` if set.
* If both `content` and `file_path` are not set, the icon will not be registered.
* @type string $file_path Optional. The full path to the file containing the icon content.
* @type bool $show_in_rest Optional. Whether the icon is exposed through the REST API.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that spacing will need to be aligned here.

* }
* @return bool True if the icon was registered with success and false otherwise.
*/
Expand All @@ -100,7 +102,7 @@ protected function register( $icon_name, $icon_properties ) {
return false;
}

$allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path' ), 1 );
$allowed_keys = array_fill_keys( array( 'label', 'content', 'file_path', 'show_in_rest' ), 1 );
foreach ( array_keys( $icon_properties ) as $key ) {
if ( ! array_key_exists( $key, $allowed_keys ) ) {
_doing_it_wrong(
Expand Down Expand Up @@ -158,6 +160,15 @@ protected function register( $icon_name, $icon_properties ) {
}
}

if ( isset( $icon_properties['show_in_rest'] ) && ! is_bool( $icon_properties['show_in_rest'] ) ) {
_doing_it_wrong(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test for this too?

__METHOD__,
__( 'Icon show_in_rest flag must be a boolean.', 'gutenberg' ),
'7.1.0'
);
return false;
}

$icon = array_merge(
$icon_properties,
array( 'name' => $icon_name )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ public function get_items( $request ) {
$search = $request->get_param( 'search' );
$icons = WP_Icons_Registry::get_instance()->get_registered_icons( $search );
foreach ( $icons as $icon ) {
if ( empty( $icon['show_in_rest'] ) ) {
continue;
}
$prepared_icon = $this->prepare_item_for_response( $icon, $request );
$response[] = $this->prepare_response_for_collection( $prepared_icon );
}
Expand Down Expand Up @@ -142,7 +145,7 @@ public function get_icon( $name ) {
$registry = WP_Icons_Registry::get_instance();
$icon = $registry->get_registered_icon( $name );

if ( null === $icon ) {
if ( null === $icon || empty( $icon['show_in_rest'] ) ) {
return new WP_Error(
'rest_icon_not_found',
sprintf(
Expand Down
17 changes: 11 additions & 6 deletions packages/icons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,22 @@ You can browse the icons docs and examples at [https://wordpress.github.io/guten

## Adding new icons

To add a new icon to the library, follow these steps:

1. **Add the SVG file**: Place your SVG file in the `src/library/` directory. The filename should be in kebab-case (e.g., `my-new-icon.svg`).
2. **TypeScript files are auto-generated**: The TypeScript component files (`.tsx`) are automatically generated by the build script from the SVG files. You do not need to manually create or edit these files.
2. **TypeScript files are auto-generated**: The TypeScript component files (`.tsx`) are generated from the SVG files by the build script, so you do not need to create or edit them manually. They are generated automatically when you commit changes under `src/library/`.

### Publishing as a WordPress core icon

To ship an icon to WordPress core and register it in the icon registry, add it to `manifest.json`:

> [!IMPORTANT]
> Once an icon has shipped in a WordPress core release, never remove it or change its `slug`. Existing content, integrations, and imports reference icons by their slug, so doing so is a breaking change.
3. **Add the icon to `manifest.json`**: Add an entry for your new icon in `src/manifest.json`. The entry should include:
1. **Add the icon to `manifest.json`**: Add an entry for your icon in `src/manifest.json`. The entry should include:
- `slug`: The icon identifier (should match the SVG filename without the `.svg` extension)
- `label`: The human-readable label for the icon. Use Title Case (for example, `My New Icon`).
- `filePath`: The relative path to the SVG file (e.g., `library/my-new-icon.svg`)
- `public` (optional): Set to `true` if you want to expose this icon as a core icon through the SVG Icons API. **Important**: Once an icon is made public, removing it is difficult, so carefully consider whether to make it public before setting this field to `true`.
4. **Do not edit `manifest.php`**: The `manifest.php` file is automatically generated from `manifest.json` by the build script. Do not edit it manually, as your changes will be overwritten when the build runs.
- `showInRest` (optional): Set to `true` to list the icon in the SVG Icons REST API and offer it in the Icon block. Defaults to `false` when omitted, registering the icon without exposing it through the REST API.
2. **Do not edit `manifest.php`**: The `manifest.php` file is automatically generated from `manifest.json` by the build script. Do not edit it manually, as your changes will be overwritten when the build runs.

After adding your icon, run `npm run build` to generate the TypeScript files and update `manifest.php`.

Expand Down
10 changes: 5 additions & 5 deletions packages/icons/lib/generate-manifest-php.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ function generatePHPArray( manifest ) {
const key = formatPHPKey( item.slug, maxKeyLength );
const label = escapePHPString( item.label );
const filePath = escapePHPString( item.filePath );
const showInRest = item.showInRest ? 'true' : 'false';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting that the icon manifest obviously grew a lot with this change. That's likely fine on its own, but I'm also considering the changes in #79100 where we're now essentially hydrating the content for every icon whose content is on the filesystem. That might mean hundreds of disk I/O operations; is it possible this will lead to a performance regression?

I'm not sure what the solution is, but maybe it makes sense to consider lazy hydrating the icon content, or maybe introducing some filtering so we don't call get_content() on icons that we won't really need yet.


return `${ key } => array(
'label' => _x( '${ label }', 'icon label', 'gutenberg' ),
'filePath' => '${ filePath }',
'label' => _x( '${ label }', 'icon label', 'gutenberg' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When are we actually going to surface the translations for icons that we don't show in the REST API? It seems a bit wasteful to localize all those strings and add hundreds of new non-translated strings to core if we're never going to surface them to end users.

'filePath' => '${ filePath }',
'showInRest' => ${ showInRest },
),`;
} );

Expand All @@ -59,20 +61,18 @@ ${ phpEntries.join( '\n' ) }

/**
* Generates manifest.php from manifest.json.
* Only includes icons with public: true.
*/
async function generateManifestPHP() {
const manifestJson = await readFile( MANIFEST_JSON_PATH, 'utf8' );
const manifest = JSON.parse( manifestJson );
const publicIcons = manifest.filter( ( item ) => item.public === true );
const phpHeader = `<?php
// This file is automatically generated. Do not edit directly.
if ( ! defined( 'ABSPATH' ) ) {
die( 'Silence is golden.' );
}

`;
const phpArray = generatePHPArray( publicIcons );
const phpArray = generatePHPArray( manifest );
const phpContent = phpHeader + phpArray + '\n';
await writeFile( MANIFEST_PHP_PATH, phpContent, 'utf8' );
}
Expand Down
Loading
Loading