Commit d6ba81b6 authored by Dries's avatar Dries

Issue #2058321 by tim.plunkett, pameeela: Move the 'place block' UI into the block listing.

parent cd15b235
......@@ -16,7 +16,9 @@
function block_admin_demo($theme = NULL) {
return array(
'#attached' => array(
'css' => array(drupal_get_path('module', 'block') . '/css/block.admin.css'),
'library' => array(
array('block', 'drupal.block.admin'),
),
),
);
}
......
......@@ -61,7 +61,7 @@ function block_help($path, $arg) {
$output .= '<dd>' . t('Blocks can be configured to be visible only on certain pages, only to users of certain roles, or only on pages displaying certain <a href="@content-type">content types</a>. Some dynamic blocks, such as those generated by modules, will be displayed only on certain pages.', array('@content-type' => url('admin/structure/types'), '@user' => url('user'))) . '</dd>';
if (module_exists('custom_block')) {
$output .= '<dt>' . t('Creating custom blocks') . '</dt>';
$output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can <a href="@block-add">add custom blocks</a>, which are then listed on the <a href="@blocks">Blocks administration page</a>. Once created, custom blocks behave just like default and module-generated blocks.', array('@blocks' => url('admin/structure/block'), '@block-add' => url('admin/structure/block/list/' . Drupal::config('system.theme')->get('default') . '/add/custom_blocks'))) . '</dd>';
$output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can add custom blocks, which are then listed on the <a href="@blocks">Blocks administration page</a>. Once created, custom blocks behave just like default and module-generated blocks.', array('@blocks' => url('admin/structure/block'))) . '</dd>';
}
$output .= '</dl>';
return $output;
......@@ -74,7 +74,7 @@ function block_help($path, $arg) {
$demo_theme = Drupal::config('system.theme')->get('default');
}
$themes = list_themes();
$output = '<p>' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page. Click the <em>configure</em> link next to each block to configure its specific title and visibility settings.') . '</p>';
$output = '<p>' . t('This page provides a drag-and-drop interface for adding a block to a region, and for controlling the order of blocks within regions. To add a block to a region, or to configure its specific title and visibility settings, click the block title under <em>Place blocks</em>. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page.') . '</p>';
$output .= '<p>' . l(t('Demonstrate block regions (@theme)', array('@theme' => $themes[$demo_theme]->info['name'])), 'admin/structure/block/demo/' . $demo_theme) . '</p>';
return $output;
}
......@@ -147,11 +147,6 @@ function block_menu() {
'type' => $key == $default_theme ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
'route_name' => "block_admin_display.$key",
);
$items["admin/structure/block/list/$key/add"] = array(
'title' => 'Place blocks',
'type' => MENU_LOCAL_ACTION,
'route_name' => "block_plugin_ui.$key",
);
$items["admin/structure/block/demo/$key"] = array(
'title' => check_plain($theme->info['name']),
'page callback' => 'block_admin_demo',
......@@ -636,6 +631,20 @@ function block_library_info() {
array('system', 'drupal'),
),
);
$libraries['drupal.block.admin'] = array(
'title' => 'Block admin',
'version' => VERSION,
'js' => array(
drupal_get_path('module', 'block') . '/js/block.admin.js' => array(),
),
'css' => array(
drupal_get_path('module', 'block') . '/css/block.admin.css' => array(),
),
'dependencies' => array(
array('system', 'jquery'),
array('system', 'drupal'),
),
);
return $libraries;
}
......@@ -26,10 +26,3 @@ block_admin_add:
_content: '\Drupal\block\Controller\BlockAddController::blockAddConfigureForm'
requirements:
_permission: 'administer blocks'
block_autocomplete:
pattern: '/block/autocomplete'
defaults:
_controller: '\Drupal\block\Controller\BlockAutocompleteController::autocomplete'
requirements:
_permission: 'administer blocks'
......@@ -31,3 +31,87 @@ a.block-demo-backlink:visited {
a.block-demo-backlink:hover {
text-decoration: underline;
}
.block-list-region {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.block-list-right {
border: 1px solid #bfbfbf;
border-bottom-width: 0;
}
.block-list {
padding: 0 0.75em;
margin: 0;
}
.block-list li {
list-style: none;
padding: 0.1em 0;
}
.block-list a:before {
content: '+ ';
}
.block-list-right .form-type-search {
padding: 0 1em;
}
/* Wide screens */
@media
screen and (min-width: 780px),
(orientation: landscape) and (min-device-height: 780px) {
.block-list-left {
float: left; /* LTR */
width: 75%;
padding-right: 2em;
}
[dir="rtl"] .block-list-left {
float: right;
padding-left: 2em;
padding-right: 0;
}
.block-list-right {
float: right; /* LTR */
width: 25%;
}
[dir="rtl"] .block-list-right {
float: left;
}
/* @todo File an issue to add a standard class to all text-like inputs */
.block-list-right .form-autocomplete,
.block-list-right .form-text,
.block-list-right .form-tel,
.block-list-right .form-email,
.block-list-right .form-url,
.block-list-right .form-search,
.block-list-right .form-number,
.block-list-right .form-color,
.block-list-right textarea {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
max-width: 100%;
}
}
/**
* The vertical toolbar mode gets triggered for narrow screens, which throws off
* the intent of media queries written for the viewport width. When the vertical
* toolbar is on, we need to suppress layout for the original media width + the
* toolbar width (240px). In this case, 240px + 780px.
*/
@media
screen and (max-width: 1020px),
(orientation: landscape) and (max-device-height: 1020px) {
.toolbar-vertical .block-list-left,
.toolbar-vertical .block-list-right {
float: none;
width: auto;
padding-right: 0;
}
}
......@@ -3,3 +3,9 @@ custom_block_type_add:
title: 'Add custom block type'
appears_on:
- custom_block_type_list
custom_block_add_action:
route_name: custom_block_add_page
title: 'Add custom block'
appears_on:
- block_admin_display
......@@ -31,19 +31,6 @@ function custom_block_help($path, $arg) {
* Implements hook_menu_local_tasks().
*/
function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
// Add the "Add custom block" action link to the theme-specific block library
// listing page.
// @todo This should just be $root_path == 'admin/structure/block/list/%/add'
// but block_menu() registers static router paths instead of dynamic ones.
if (preg_match('@^admin/structure/block/list/(.*)/add$@', $root_path)) {
$item = menu_get_item('block/add');
if ($item['access']) {
$data['actions']['block/add'] = array(
'#theme' => 'menu_local_action',
'#link' => $item,
);
}
}
if ($router_item['route_name'] == 'custom_block_list') {
// @todo Move to a LocalAction plugin when https://drupal.org/node/2045267
// allows local actions to work with query strings.
......@@ -59,6 +46,19 @@ function custom_block_menu_local_tasks(&$data, $router_item, $root_path) {
}
}
/**
* Implements hook_menu_local_actions_alter().
*/
function custom_block_menu_local_actions_alter(&$actions) {
if (isset($actions['local_action_static:custom_block_add_action'])) {
foreach (list_themes() as $theme => $theme_info) {
if ($theme_info->status) {
$actions['local_action_static:custom_block_add_action']['appears_on'][] = "block_admin_display.$theme";
}
}
}
}
/**
* Implements hook_menu().
*/
......@@ -246,27 +246,6 @@ function custom_block_add_body_field($block_type_id, $label = 'Block body') {
return $instance;
}
/**
* Implements hook_form_FORM_ID_alter() for block_plugin_ui().
*/
function custom_block_form_block_plugin_ui_alter(&$form, $form_state) {
foreach ($form['left']['plugin_library']['#rows'] as $plugin_id => &$row) {
// @todo Clean up when http://drupal.org/node/1874498 lands.
if (strpos($plugin_id, ':') === FALSE) {
continue;
}
list($base, $derivative) = explode(':', $plugin_id);
if ($base !== 'custom_block') {
continue;
}
$custom_block = entity_load_by_uuid('custom_block', $derivative);
$row['1']['data']['#links']['edit'] = array(
'title' => t('Edit'),
'href' => 'block/' . $custom_block->id(),
);
}
}
/**
* Implements hook_admin_paths().
*/
......
<?php
/**
* @file
* Contains \Drupal\block\Controller\BlockAutocompleteController.
*/
namespace Drupal\block\Controller;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Controller\ControllerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Provides an autocomplete for blocks.
*/
class BlockAutocompleteController implements ControllerInterface {
/**
* The block plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* Constructs a new BlockAutocompleteController object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The block plugin manager.
*/
public function __construct(PluginManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
);
}
/**
* Autocompletes a block plugin ID.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The matched plugins as JSON.
*/
public function autocomplete(Request $request) {
$string_typed = $request->query->get('q');
// The passed string may be comma or space separated.
$string_typed = Tags::explode($string_typed);
// Take the last result and lowercase it.
$string = Unicode::strtolower(array_pop($string_typed));
$matches = array();
if ($string) {
$titles = array();
// Gather all block plugins and their admin titles.
foreach($this->manager->getDefinitions() as $plugin_id => $plugin) {
$titles[$plugin_id] = $plugin['admin_label'];
}
// Find any matching block plugin IDs.
$matches = preg_grep("/\b". $string . "/i", $titles);
}
return new JsonResponse($matches);
}
}
<?php
/**
* @file
* Contains \Drupal\block\Form\PlaceBlocksForm.
*/
namespace Drupal\block\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\String;
use Drupal\Core\Controller\ControllerInterface;
use Drupal\Core\Form\FormInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides the 'Place blocks' form.
*/
class PlaceBlocksForm implements FormInterface, ControllerInterface {
/**
* The block plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $manager;
/**
* The theme this block will be placed into.
*
* @var string
*/
protected $theme;
/**
* Constructs a new PlaceBlocksForm object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $manager
* The block plugin manager.
*/
public function __construct(PluginManagerInterface $manager) {
$this->manager = $manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
);
}
/**
* {@inheritdoc}
*/
public function getFormID() {
return 'block_plugin_ui';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, array &$form_state, $theme = NULL, $category = NULL) {
$this->theme = $theme;
$form['#theme'] = 'system_plugin_ui_form';
$rows = array();
$categories = array();
foreach ($this->manager->getDefinitions() as $plugin_id => $plugin_definition) {
if (empty($category) || $plugin_definition['category'] == $category) {
$rows[$plugin_id] = $this->row($plugin_id, $plugin_definition);
}
$categories[$plugin_definition['category']] = array(
'title' => $plugin_definition['category'],
'href' => 'admin/structure/block/list/' . $this->theme . '/add/' . $plugin_definition['category'],
);
}
$form['right']['block'] = array(
'#type' => 'textfield',
'#title' => t('Search'),
'#autocomplete_path' => 'block/autocomplete',
);
$form['right']['submit'] = array(
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => t('Next'),
);
$form['right']['all_plugins'] = array(
'#type' => 'link',
'#title' => t('All blocks'),
'#href' => 'admin/structure/block/list/' . $this->theme . '/add',
);
if (!empty($categories)) {
$form['right']['categories'] = array(
'#theme' => 'links',
'#heading' => array(
'text' => t('Categories'),
'level' => 'h3',
),
'#links' => $categories,
);
}
// Sort rows alphabetically.
asort($rows);
$form['left']['plugin_library'] = array(
'#theme' => 'table',
'#header' => array(t('Subject'), t('Operations')),
'#rows' => $rows,
);
return $form;
}
/**
* Generates the row data for a single block plugin.
*
* @param string $plugin_id
* The plugin ID.
* @param array $plugin_definition
* The plugin definition.
*
* @return array
* The row data for a single block plugin.
*/
protected function row($plugin_id, array $plugin_definition) {
$row = array();
$row[] = String::checkPlain($plugin_definition['admin_label']);
$row[] = array('data' => array(
'#type' => 'operations',
'#links' => array(
'configure' => array(
'title' => t('Place block'),
'href' => 'admin/structure/block/add/' . $plugin_id . '/' . $this->theme,
),
),
));
return $row;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, array &$form_state) {
if (!$this->manager->getDefinition($form_state['values']['block'])) {
form_set_error('block', t('You must select a valid block.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
$form_state['redirect'] = 'admin/structure/block/add/' . $form_state['values']['block'] . '/' . $this->theme;
}
}
......@@ -50,18 +50,6 @@ public function routes(RouteBuildEvent $event) {
)
);
$collection->add("block_admin_display.$key", $route);
// The block plugin listing page.
$route = new Route(
"admin/structure/block/list/$key/add/{category}",
array(
'_form' => '\Drupal\block\Form\PlaceBlocksForm',
'category' => NULL,
'theme' => $key,
),
array('_block_themes_access' => 'TRUE')
);
$collection->add("block_plugin_ui.$key", $route);
}
}
......
......@@ -70,7 +70,6 @@ public function testBlockLinks() {
// Create the block cache for all languages.
foreach ($this->langcodes as $langcode) {
$this->drupalGet('admin/structure/block', array('language' => $langcode));
$this->clickLink(t('Place blocks'));
}
// Create a feed in the default language.
......@@ -80,7 +79,6 @@ public function testBlockLinks() {
// Check that the block is listed for all languages.
foreach ($this->langcodes as $langcode) {
$this->drupalGet('admin/structure/block', array('language' => $langcode));
$this->clickLink(t('Place blocks'));
$this->assertText($feed->label());
}
}
......
<?php
/**
* @file
* Contains \Drupal\block\Tests\BlockLibrarySearchTest.
*/
namespace Drupal\block\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests the block library search.
*/
class BlockLibrarySearchTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('block');
/**
* An administrative user to configure the test environment.
*/
protected $adminUser;
public static function getInfo() {
return array(
'name' => 'Block library search',
'description' => 'Checks that the block library search works correctly.',
'group' => 'Block',
);
}
protected function setUp() {
parent::setUp();
// Create and log in an administrative user.
$this->adminUser = $this->drupalCreateUser(array(
'administer blocks',
'access administration pages',
));
$this->drupalLogin($this->adminUser);
}
/**
* Test block library search.
*/
function testBlockLibrarySearch() {
// Check that the block plugin is valid.
$this->drupalPost('admin/structure/block/list/stark/add', array('block' => 'invalid_block'), t('Next'));
$this->assertText('You must select a valid block.');
// Check that the block search form redirects to the correct block form.
$this->drupalPost('admin/structure/block/list/stark/add', array('block' => 'system_main_block'), t('Next'));
$this->assertUrl('admin/structure/block/add/system_main_block/stark');
}
}
......@@ -274,7 +274,7 @@ function testBlockModuleDisable() {
}
// Ensure that the disabled module's block plugin is no longer available.
$this->drupalGet('admin/structure/block/list/' . \Drupal::config('system.theme')->get('default') . '/add');
$this->drupalGet('admin/structure/block/list/' . \Drupal::config('system.theme')->get('default'));
$this->assertNoText(t('Test block caching'));
// Confirm that the block is no longer displayed on the front page.
......
......@@ -45,7 +45,7 @@ function testXSSInTitle() {
$this->drupalLogin($this->drupalCreateUser(array('administer blocks', 'access administration pages')));
$default_theme = \Drupal::config('system.theme')->get('default');
$this->drupalGet('admin/structure/block/list/' . $default_theme . '/add');
$this->drupalGet('admin/structure/block/list/' . $default_theme);
$this->assertNoRaw("<script>alert('XSS subject');</script>", 'The block title was properly sanitized in Block Plugin UI Admin page.');
}
......
......@@ -106,12 +106,27 @@ function testBlockAdminUiPage() {
}
/**
* Test block search.
* Tests the block categories on the listing page.
*/
function testBlockSearch() {
$block = t('Administration');
$blocks = drupal_json_decode($this->drupalGet('block/autocomplete', array('query' => array('q' => $block))));
$this->assertEqual($blocks['system_menu_block:menu-admin'], $block, t('Can search for block with name !block.', array('!block' => $block)));
public function testCandidateBlockList() {
$arguments = array(
':ul_class' => 'block-list',
':li_class' => 'test-block-instantiation',
':href' => 'admin/structure/block/add/test_block_instantiation/stark',
':text' => 'Display message',
);
$this->drupalGet('admin/structure/block');
$elements = $this->xpath('//details[@id="edit-block-test"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
$this->assertTrue(!empty($elements), 'The test block appears in the category for its module.');
// Trigger the custom category addition in block_test_block_alter().
$this->container->get('state')->set('block_test_info_alter', TRUE);
$this->container->get('plugin.manager.block')->clearCachedDefinitions();
$this->drupalGet('admin/structure/block');
$elements = $this->xpath('//details[@id="edit-custom-category"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
$this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().');
}
/**
......
......@@ -12,3 +12,12 @@ function block_test_system_theme_info() {
$themes['block_test_theme'] = drupal_get_path('module', 'block_test') . '/themes/block_test_theme/block_test_theme.info.yml';
return $themes;
}
/**
* Implements hook_block_alter().
*/
function block_test_block_alter(&$block_info) {
if (Drupal::state()->get('block_test_info_alter') && isset($block_info['test_block_instantiation'])) {
$block_info['test_block_instantiation']['category'] = t('Custom category');
}
}
......@@ -161,7 +161,7 @@ function addCustomMenu() {
// Enable the custom menu block.
$menu_name = 'menu-' . $menu_name; // Drupal prepends the name with 'menu-'.
// Confirm that the custom menu block is available.
$this->drupalGet('admin/structure/block/list/' . \Drupal::config('system.theme')->get('default') . '/add');
$this->drupalGet('admin/structure/block/list/' . \Drupal::config('system.theme')->get('default'));
$this->assertText($label);
// Enable the block.
......@@ -379,7 +379,7 @@ function testSystemMenuRename() {
// Make sure menu shows up with new name in block addition.
$default_theme = variable_get('theme_default', 'stark');
$this->drupalget('admin/structure/block/list/' . $default_theme . '/add');
$this->drupalget('admin/structure/block/list/' . $default_theme);
$this->assertText($edit['label']);
}
......
#block-library .left-col,
#block-library .right-col {
float:left;
width:66%;
height:100%;
background-color:#ffffff;
}
#block-library .right-col {
width:34%;
background-color:#f7f7f7;
}
#block-library .right-col h3 {
margin: 1em -20px;
background-color:#d7d7d7;
color:#333333;
padding:8px 15px;
font-size:1.1em;
}
#block-library .inside {
margin:0 20px;
}
#block-library .bottom-bar {
width:100%;
clear:both;
}
......@@ -18,6 +18,7 @@
* id = "system_menu_block",
* admin_label = @Translation("System Menu"),
* module = "system",
* category = "menu",
* derivative = "Drupal\system\Plugin\Derivative\SystemMenuBlock"
* )
*/
......
......@@ -190,10 +190,6 @@ function system_theme() {
'system_date_format_localize_form' => array(
'render element' => 'form',