Skip to content

Commit

Permalink
Add block directory to inserter menu (#17431)
Browse files Browse the repository at this point in the history
* Add new properties and panel for discover items

* Add basic component with styling for discover blocks

* Dynamically load block into editor.

* Increase icon size.

* Handle insert block properly.

* Added another discover block.

* Revert unncessary changes.

* Retrieve discover blocks from a mock api.

* Only show discover blocks when no blocks are found.

* Style discover items header

* Fill discover item footer elements.

* Use BlockIcon components.

* Update documentation.

* Update mock URL.

* Revert changes in BlockIcon for size and use Icon directly.

* Use Fragment from wp.element.

* Created BlockRatings component.

* Update classNames to follow guidelines.

* Moved mock api to WordPress rest api.

* Separate DiscoverBlocksPanel to a component.

* Implement basic block search.

* Handle user without permission to install blocks.

* Styling for discover blocks search description.

* Handle error when script can't load.
Cache search results.

* Removed text to follow design.

* Fixed search not returning as array from the mock API.

* Refactor stars component to a shorter code using @talldan's recommendation.

* Renamed class names based on guideline and recommendation.

* Make React avoid rendering style attribute when icon is falsey.

* Removed redundant styles.

* Refactor DiscoverBlockListItem to smaller components.

* Renamed class names based on guideline and recommendation.

* Align @param descriptions.

* Removed redundant comments.

* Refactor DiscoverBlockListItem to smaller components.

Renamed class names based on guideline and recommendation.

* Tidy up blocks search controller code.

* Implement Retry button when Block previews can't load.

* Implement block search to api.wordpress.org.

* Map plugin info to discover block.

* Map available plugin properties to block.

Comment for unavailable and ambigous properties.

* Fix typo in comment.

* Map updated metadata from api.

Handle multiple script loads correctly.

* Updated unit test.

* Show loading state when requesting for discover blocks.

* Only show uninstalled blocks.

* Prevent fetching if user has not typed anything.

* Update comments

* Debounce search for blocks.

* Implement spinner for loading state.

* Install and activate plugin after loading to editor.

* Handle error while installing block.

* Added functionality to retry installing block.

* Use regular HTML attributes without string evaluation.

* Use shorthand property names.

* Use WP_Error, improve and fix for feedback from PR.

props @TimothyBJacobs.

* Change install REST methods to allow PUT and POST only.
Update to use get_item_schema.

* Update REST to return response with snake case.

* New downloadable-blocks package to fill a slot in inserter menu.

* Update documentation.

* Passing props to downloadable blocks.

* Moved components from block-editor to its own package.

* Load css for downloadable-blocks.

* Moved debounce filterValue function to editor.

* Moved to use its own data store in downloadable-blocks package.

* Moved code to load block assets out of menu.js

* Remove comment.

* Removed block-icon from downloadable-blocks package.

* Fixed bad reference in scss file.

*  Moved installBlock function to a be next to handleDownloadableBlock.

* Moved the handleDownloadableBlock to actions.

* Renamed downloadable-blocks package to block-directory.

* Export DiscoverBlocksPanel as named exports.

* Update README and package.json.

* Renamed store to core/block-directory.

* Renamed css class name.

* Renamed css class name.

* Renamed discover-block to downloadable-block

* Rename block-ratings to prefix with block-directory.

* Rename rest controller to block directory.

* Limit the number of words in description.

* Update background color to design specs.

* Refactor to call loadAssets asynchronously.

* Refactor downloadBlock and installBlock to be more extensible.

* Rename handleDownloadableBlock to downloadBlock

* Handle if FS_METHOD is not direct.

* Fix retry of installing block.

* Only install block after download is successful.
Add error handling of block has no assets in the metadata.

* Fix onSelect of  item on success.

* Remove block if failed to install block.

* Update documentation.

* Check for permission before fetch.

* Fix unit test

* Uninstall new blocks if user has removed them.

* Remove debug messages from API.

* Fix bug to filter installedBlockType after uninstall.

* Add dependencies into package.json

* Update package-lock.json

* Fix flickering when search for blocks.

* Update package.json

* Update package-lock.json

* Fix the search results flickering.

* Update packages/block-directory/package.json

Co-Authored-By: Grzegorz (Greg) Ziółkowski <grzegorz@gziolo.pl>

* Update packages/block-directory/package.json

Co-Authored-By: Grzegorz (Greg) Ziółkowski <grzegorz@gziolo.pl>

* Update packages/block-directory/CHANGELOG.md

Co-Authored-By: Grzegorz (Greg) Ziółkowski <grzegorz@gziolo.pl>

* Improve accessibility.

* Remove redudant line.

* Rename the selector method.

* Remove isDownloadableBlocksEnabled settings.

If no fills registered for the slot, default to original behaviour.

* Update data-core-block-editor.md

* Added feature toggle for block directory in the experiment settings page.

* Update README.md

* Fix typo

* Fixed phpcs errors.

* Fix phpcs warning.

* Rename REST base endpoint to block-directory.

* Update comment for class.

* Use noop instead of empty function.

* Rename discover blocks to downloadable blocks.

* Tidy up code.

* Update bottom section to be footer in an article.

* Fix phpcs warning.

* Mapped author information for block directory

* Remove experimental feature flag for block directory

* Fix block inner-content  overflowing rounded corners

* Remove superfluous css rule

* Use existing scss variables where possible

* Use a `p` tag for the block description

* Remove @access comments

* s/get_items_permissions_check/permissions_check/

* Update @SInCE to 6.5.0

* Mark parse_block_metadata() as private

* Update @SInCE to 6.5.0

* Include end of string anchor in image extension RegExp

* Remove item.isDisabled

* Use unicode apostrophe

* Improve spoken message

* Improve copy

* Add whitespace

* Fix comments and remove unused params

* Pass storeConfig straight to registerStore

* Remove empty quotes

* Use Array.isArray

* Add a comment explaining the block uninstall feature

* Fix plugin rating calculation

* s/isMenuEmpty/hasItems/

* Revert "Remove experimental feature flag for block directory"

This reverts commit 882d178.

Co-authored-by: CK <ck-lee@taggun.io>
Co-authored-by: Robert Anderson <robert@noisysocks.com>
  • Loading branch information
3 people authored Sep 13, 2019
1 parent 570a219 commit f37be8e
Show file tree
Hide file tree
Showing 43 changed files with 1,592 additions and 10 deletions.
6 changes: 6 additions & 0 deletions docs/manifest-devhub.json
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,12 @@
"markdown_source": "../packages/blob/README.md",
"parent": "packages"
},
{
"title": "@wordpress/block-directory",
"slug": "packages-block-directory",
"markdown_source": "../packages/block-directory/README.md",
"parent": "packages"
},
{
"title": "@wordpress/block-editor",
"slug": "packages-block-editor",
Expand Down
307 changes: 307 additions & 0 deletions lib/class-wp-rest-block-directory-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
<?php
/**
* Start: Include for phase 2
* Block Directory REST API: WP_REST_Blocks_Controller class
*
* @package gutenberg
* @since 6.5.0
*/

/**
* Controller which provides REST endpoint for the blocks.
*
* @since 6.5.0
*
* @see WP_REST_Controller
*/
class WP_REST_Block_Directory_Controller extends WP_REST_Controller {

/**
* Constructs the controller.
*/
public function __construct() {
$this->namespace = '__experimental';
$this->rest_base = 'block-directory';
}

/**
* Registers the necessary REST API routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/search',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
array(
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'install_block' ),
'permission_callback' => array( $this, 'permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/uninstall',
array(
array(
'methods' => WP_REST_Server::DELETABLE,
'callback' => array( $this, 'uninstall_block' ),
'permission_callback' => array( $this, 'permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}

/**
* Checks whether a given request has permission to install and activate plugins.
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|bool True if the request has permission, WP_Error object otherwise.
*/
public function permissions_check( $request ) {
if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
return new WP_Error(
'rest_user_cannot_view',
__( 'Sorry, you are not allowed to install blocks.', 'gutenberg' )
);
}

return true;
}

/**
* Installs and activates a plugin
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function install_block( $request ) {

include_once( ABSPATH . 'wp-admin/includes/file.php' );
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' );

$api = plugins_api(
'plugin_information',
array(
'slug' => $request->get_param( 'slug' ),
'fields' => array(
'sections' => false,
),
)
);

if ( is_wp_error( $api ) ) {
return WP_Error( $api->get_error_code(), $api->get_error_message() );
}

$skin = new WP_Ajax_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );

$filesystem_method = get_filesystem_method();

if ( 'direct' !== $filesystem_method ) {
return WP_Error( null, 'Only direct FS_METHOD is supported.' );
}

$result = $upgrader->install( $api->download_link );

if ( is_wp_error( $result ) ) {
return WP_Error( $result->get_error_code(), $result->get_error_message() );
}

if ( is_wp_error( $skin->result ) ) {
return WP_Error( $skin->$result->get_error_code(), $skin->$result->get_error_message() );
}

if ( $skin->get_errors()->has_errors() ) {
return WP_Error( $skin->$result->get_error_code(), $skin->$result->get_error_messages() );
}

if ( is_null( $result ) ) {
global $wp_filesystem;
// Pass through the error from WP_Filesystem if one was raised.
if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) {
return WP_Error( 'unable_to_connect_to_filesystem', esc_html( $wp_filesystem->errors->get_error_message() ) );
}
return WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'gutenberg' ) );
}

$install_status = install_plugin_install_status( $api );

$activate_result = activate_plugin( $install_status['file'] );

if ( is_wp_error( $activate_result ) ) {
return WP_Error( $activate_result->get_error_code(), $activate_result->get_error_message() );
}

return rest_ensure_response( true );
}

/**
* Deactivates and deletes a plugin
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function uninstall_block( $request ) {

include_once( ABSPATH . 'wp-admin/includes/file.php' );
include_once( ABSPATH . 'wp-admin/includes/plugin.php' );
include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' );
include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' );

$api = plugins_api(
'plugin_information',
array(
'slug' => $request->get_param( 'slug' ),
'fields' => array(
'sections' => false,
),
)
);

if ( is_wp_error( $api ) ) {
return WP_Error( $api->get_error_code(), $api->get_error_message() );
}

$install_status = install_plugin_install_status( $api );

$deactivate_result = deactivate_plugins( $install_status['file'] );

if ( is_wp_error( $deactivate_result ) ) {
return WP_Error( $deactivate_result->get_error_code(), $deactivate_result->get_error_message() );
}

$delete_result = delete_plugins( array( $install_status['file'] ) );

if ( is_wp_error( $delete_result ) ) {
return WP_Error( $delete_result->get_error_code(), $delete_result->get_error_message() );
}

return rest_ensure_response( true );
}

/**
* Search and retrieve blocks metadata
*
* @since 6.5.0
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
public function get_items( $request ) {

$search_string = $request->get_param( 'term' );

if ( empty( $search_string ) ) {
return rest_ensure_response( array() );
}

include( ABSPATH . WPINC . '/version.php' );

$url = 'http://api.wordpress.org/plugins/info/1.2/';
$url = add_query_arg(
array(
'action' => 'query_plugins',
'request[block]' => $search_string,
'request[wp_version]' => '5.3',
'request[per_page]' => '3',
),
$url
);
$http_url = $url;
$ssl = wp_http_supports( array( 'ssl' ) );
if ( $ssl ) {
$url = set_url_scheme( $url, 'https' );
}
$http_args = array(
'timeout' => 15,
'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ),
);

$request = wp_remote_get( $url, $http_args );
$response = json_decode( wp_remote_retrieve_body( $request ), true );

if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$result = array();

foreach ( $response['plugins'] as $plugin ) {
$installed_plugins = get_plugins( '/' . $plugin['slug'] );

// Only show uninstalled blocks.
if ( empty( $installed_plugins ) ) {
$result[] = self::parse_block_metadata( $plugin );
}
}

return rest_ensure_response( $result );
}

/**
* Parse block metadata for a block
*
* @since 6.5.0
*
* @param WP_Object $plugin The plugin metadata.
* @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure.
*/
private static function parse_block_metadata( $plugin ) {
$block = new stdClass();

// There might be multiple blocks in a plugin. Only the first block is mapped.
$block_data = reset( $plugin['blocks'] );
$block->name = $block_data['name'];
$block->title = $block_data['title'];

// Plugin's description, not description in block.json.
$block->description = wp_trim_words( wp_strip_all_tags( $plugin['description'] ), 30, '...' );

$block->id = $plugin['slug'];
$block->rating = $plugin['rating'] / 20;
$block->rating_count = $plugin['num_ratings'];
$block->active_installs = $plugin['active_installs'];
$block->author_block_rating = $plugin['author_block_rating'] / 20;
$block->author_block_count = $plugin['author_block_count'];

// Plugin's author, not author in block.json.
$block->author = wp_strip_all_tags( $plugin['author'] );

// Plugin's icons or icon in block.json.
$block->icon = isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default';

$block->assets = array();

foreach ( $plugin['block_assets'] as $asset ) {
$block->assets[] = 'https://plugins.svn.wordpress.org/' . $plugin['slug'] . $asset;
}

$block->humanized_updated = human_time_diff( strtotime( $plugin['last_updated'] ), current_time( 'timestamp' ) ) . __( ' ago', 'gutenberg' );

return $block;
}
}
10 changes: 9 additions & 1 deletion lib/client-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ function gutenberg_register_scripts_and_styles() {
gutenberg_override_style(
'wp-editor',
gutenberg_url( 'build/editor/style.css' ),
array( 'wp-components', 'wp-block-editor', 'wp-nux' ),
array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-block-directory' ),
filemtime( gutenberg_dir_path() . 'build/editor/style.css' )
);
wp_style_add_data( 'wp-editor', 'rtl', 'replace' );
Expand Down Expand Up @@ -373,6 +373,14 @@ function gutenberg_register_scripts_and_styles() {
);
wp_style_add_data( 'wp-edit-widgets', 'rtl', 'replace' );

gutenberg_override_style(
'wp-block-directory',
gutenberg_url( 'build/block-directory/style.css' ),
array( 'wp-components' ),
filemtime( gutenberg_dir_path() . 'build/block-directory/style.css' )
);
wp_style_add_data( 'wp-block-directory', 'rtl', 'replace' );

if ( defined( 'GUTENBERG_LIVE_RELOAD' ) && GUTENBERG_LIVE_RELOAD ) {
$live_reload_url = ( GUTENBERG_LIVE_RELOAD === true ) ? 'http://localhost:35729/livereload.js' : GUTENBERG_LIVE_RELOAD;

Expand Down
15 changes: 14 additions & 1 deletion lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class="wrap"
<?php settings_errors(); ?>
<form method="post" action="options.php">
<?php settings_fields( 'gutenberg-experiments' ); ?>
<?php do_settings_sections( 'gutenberg-experiments' ); ?>
<?php do_settings_sections( 'gutenberg-experiments' ); ?>
<?php submit_button(); ?>
</form>
</div>
Expand Down Expand Up @@ -64,6 +64,17 @@ function gutenberg_initialize_experiments_settings() {
'id' => 'gutenberg-menu-block',
)
);
add_settings_field(
'gutenberg-block-directory',
__( 'Block Directory', 'gutenberg' ),
'gutenberg_display_experiment_field',
'gutenberg-experiments',
'gutenberg_experiments_section',
array(
'label' => __( 'Enable Block Directory search', 'gutenberg' ),
'id' => 'gutenberg-block-directory',
)
);
register_setting(
'gutenberg-experiments',
'gutenberg-experiments'
Expand Down Expand Up @@ -114,6 +125,8 @@ function gutenberg_experiments_editor_settings( $settings ) {
$experiments_settings = array(
'__experimentalEnableLegacyWidgetBlock' => $experiments_exist ? array_key_exists( 'gutenberg-widget-experiments', get_option( 'gutenberg-experiments' ) ) : false,
'__experimentalEnableMenuBlock' => $experiments_exist ? array_key_exists( 'gutenberg-menu-block', get_option( 'gutenberg-experiments' ) ) : false,
'__experimentalBlockDirectory' => $experiments_exist ? array_key_exists( 'gutenberg-block-directory', get_option( 'gutenberg-experiments' ) ) : false,

);
return array_merge( $settings, $experiments_settings );
}
Expand Down
5 changes: 5 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
/**
* End: Include for phase 2
*/

if ( ! class_exists( 'WP_REST_Block_Directory_Controller' ) ) {
require dirname( __FILE__ ) . '/class-wp-rest-block-directory-controller.php';
}

require dirname( __FILE__ ) . '/rest-api.php';
}

Expand Down
Loading

0 comments on commit f37be8e

Please sign in to comment.