From f0567724df3de6458d28060ea830e879307fdf17 Mon Sep 17 00:00:00 2001 From: webchick <drupal@webchick.net> Date: Fri, 25 Sep 2015 05:38:53 -0700 Subject: [PATCH] Issue #2377757 by EclipseGc, tim.plunkett, Frando, Berdir, japerry, dsnopek, Wim Leers, Bojhan: Expose Block Context mapping in the UI --- core/config/schema/core.data_types.schema.yml | 5 ++ core/lib/Drupal/Core/Block/BlockBase.php | 6 ++ .../ContextAwarePluginAssignmentTrait.php | 3 +- core/modules/block/src/BlockForm.php | 6 ++ core/modules/block/src/BlockViewBuilder.php | 7 ++ .../src/Controller/BlockLibraryController.php | 16 +++- .../block/src/Tests/BlockInterfaceTest.php | 1 + core/modules/block/src/Tests/BlockUiTest.php | 48 +++++++++-- .../block_test/block_test.services.yml | 6 ++ .../ContextProvider/MultipleStaticContext.php | 80 +++++++++++++++++++ .../Plugin/Block/TestContextAwareBlock.php | 2 + .../TestContextAwareUnsatisfiedBlock.php | 34 ++++++++ .../src/ContextProvider/NodeRouteContext.php | 5 +- 13 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 core/modules/block/tests/modules/block_test/block_test.services.yml create mode 100644 core/modules/block/tests/modules/block_test/src/ContextProvider/MultipleStaticContext.php create mode 100644 core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index e7fb7c2c8254..ab6b94406524 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -307,6 +307,11 @@ block_settings: provider: type: string label: 'Provider' + context_mapping: + type: sequence + label: 'Context assignments' + sequence: + type: string condition.plugin: type: mapping diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php index 2544254586d6..5684e3d1c79d 100644 --- a/core/lib/Drupal/Core/Block/BlockBase.php +++ b/core/lib/Drupal/Core/Block/BlockBase.php @@ -10,6 +10,7 @@ use Drupal\block\BlockInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait; use Drupal\Core\Plugin\ContextAwarePluginBase; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\NestedArray; @@ -28,6 +29,8 @@ */ abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface { + use ContextAwarePluginAssignmentTrait; + /** * The transliteration service. * @@ -175,6 +178,9 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta '#return_value' => BlockInterface::BLOCK_LABEL_VISIBLE, ); + // Add context mapping UI form elements. + $contexts = $form_state->getTemporaryValue('gathered_contexts') ?: []; + $form['context_mapping'] = $this->addContextAssignmentElement($this, $contexts); // Add plugin-specific settings for this block type. $form += $this->blockForm($form, $form_state); return $form; diff --git a/core/lib/Drupal/Core/Plugin/ContextAwarePluginAssignmentTrait.php b/core/lib/Drupal/Core/Plugin/ContextAwarePluginAssignmentTrait.php index 19d756ce8443..f9e349ed69e0 100644 --- a/core/lib/Drupal/Core/Plugin/ContextAwarePluginAssignmentTrait.php +++ b/core/lib/Drupal/Core/Plugin/ContextAwarePluginAssignmentTrait.php @@ -56,11 +56,12 @@ protected function addContextAssignmentElement(ContextAwarePluginInterface $plug if (count($options) > 1) { $assignments = $plugin->getContextMapping(); $element[$context_slot] = [ - '#title' => $this->t('Select a @context value:', ['@context' => $context_slot]), + '#title' => $definition->getLabel() ?: $this->t('Select a @context value:', ['@context' => $context_slot]), '#type' => 'select', '#options' => $options, '#required' => $definition->isRequired(), '#default_value' => !empty($assignments[$context_slot]) ? $assignments[$context_slot] : '', + '#description' => $definition->getDescription(), ]; } } diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php index 8c13bbbe8a1d..50e4d38e95cd 100644 --- a/core/modules/block/src/BlockForm.php +++ b/core/modules/block/src/BlockForm.php @@ -335,6 +335,12 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // Call the plugin submit handler. $entity->getPlugin()->submitConfigurationForm($form, $settings); + $block = $entity->getPlugin(); + // If this block is context-aware, set the context mapping. + if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { + $context_mapping = $settings->getValue('context_mapping', []); + $block->setContextMapping($context_mapping); + } // Update the original form values. $form_state->setValue('settings', $settings->getValues()); diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index 9a582352343e..7972bbcaf874 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -16,6 +16,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Render\Element; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -139,6 +140,12 @@ protected static function buildPreRenderableBlock($entity, ModuleHandlerInterfac $derivative_id = $plugin->getDerivativeId(); $configuration = $plugin->getConfiguration(); + // Inject runtime contexts. + if ($plugin instanceof ContextAwarePluginInterface) { + $contexts = \Drupal::service('context.repository')->getRuntimeContexts($plugin->getContextMapping()); + \Drupal::service('context.handler')->applyContextMapping($plugin, $contexts); + } + // Create the render array for the block as a whole. // @see template_preprocess_block(). $build = [ diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php index 1dcd15defe86..0f6c96546fd4 100644 --- a/core/modules/block/src/Controller/BlockLibraryController.php +++ b/core/modules/block/src/Controller/BlockLibraryController.php @@ -12,6 +12,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Menu\LocalActionManagerInterface; +use Drupal\Core\Plugin\Context\LazyContextRepository; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -29,6 +30,13 @@ class BlockLibraryController extends ControllerBase { */ protected $blockManager; + /** + * The context repository. + * + * @var \Drupal\Core\Plugin\Context\LazyContextRepository + */ + protected $contextRepository; + /** * The route match. * @@ -48,15 +56,18 @@ class BlockLibraryController extends ControllerBase { * * @param \Drupal\Core\Block\BlockManagerInterface $block_manager * The block manager. + * @param \Drupal\Core\Plugin\Context\LazyContextRepository $context_repository + * The context repository. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. * @param \Drupal\Core\Menu\LocalActionManagerInterface $local_action_manager * The local action manager. */ - public function __construct(BlockManagerInterface $block_manager, RouteMatchInterface $route_match, LocalActionManagerInterface $local_action_manager) { + public function __construct(BlockManagerInterface $block_manager, LazyContextRepository $context_repository, RouteMatchInterface $route_match, LocalActionManagerInterface $local_action_manager) { $this->blockManager = $block_manager; $this->routeMatch = $route_match; $this->localActionManager = $local_action_manager; + $this->contextRepository = $context_repository; } /** @@ -65,6 +76,7 @@ public function __construct(BlockManagerInterface $block_manager, RouteMatchInte public static function create(ContainerInterface $container) { return new static( $container->get('plugin.manager.block'), + $container->get('context.repository'), $container->get('current_route_match'), $container->get('plugin.manager.menu.local_action') ); @@ -95,7 +107,7 @@ public function listBlocks(Request $request, $theme) { ]; // Only add blocks which work without any available context. - $definitions = $this->blockManager->getDefinitionsForContexts(); + $definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts()); // Order by category, and then by admin label. $definitions = $this->blockManager->getSortedDefinitions($definitions); diff --git a/core/modules/block/src/Tests/BlockInterfaceTest.php b/core/modules/block/src/Tests/BlockInterfaceTest.php index 28208ac93361..5af8ec0dc74d 100644 --- a/core/modules/block/src/Tests/BlockInterfaceTest.php +++ b/core/modules/block/src/Tests/BlockInterfaceTest.php @@ -79,6 +79,7 @@ public function testBlockInterface() { '#default_value' => TRUE, '#return_value' => 'visible', ), + 'context_mapping' => array(), 'display_message' => array( '#type' => 'textfield', '#title' => t('Display message'), diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index 721a7e2e518c..532094265726 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -165,22 +165,58 @@ public function testCandidateBlockList() { $this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().'); } + /** + * Tests the behavior of unsatisfied context-aware blocks. + */ + public function testContextAwareUnsatisfiedBlocks() { + $arguments = array( + ':category' => 'Block test', + ':href' => 'admin/structure/block/add/test_context_aware_unsatisfied/classy', + ':text' => 'Test context-aware unsatisfied block', + ); + + $this->drupalGet('admin/structure/block'); + $this->clickLinkPartialName('Place block'); + $elements = $this->xpath('//tr[.//td/div[text()=:text] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]', $arguments); + $this->assertTrue(empty($elements), 'The context-aware test block does not appear.'); + + $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware_unsatisfied'); + $this->assertTrue(!empty($definition), 'The context-aware test block does not exist.'); + } + /** * Tests the behavior of context-aware blocks. */ public function testContextAwareBlocks() { + $expected_text = '<div id="test_context_aware--username">' . \Drupal::currentUser()->getUsername() . '</div>'; + $this->drupalGet(''); + $this->assertNoText('Test context-aware block'); + $this->assertNoRaw($expected_text); + + $block_url = 'admin/structure/block/add/test_context_aware/classy'; $arguments = array( - ':ul_class' => 'block-list', - ':li_class' => 'test-context-aware', - ':href' => 'admin/structure/block/add/test_context_aware/classy', - ':text' => 'Test context-aware block', + ':title' => 'Test context-aware block', + ':category' => 'Block test', + ':href' => $block_url, ); + $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]'; $this->drupalGet('admin/structure/block'); - $elements = $this->xpath('//details[@id="edit-category-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 context-aware test block does not appear.'); + $this->clickLinkPartialName('Place block'); + $elements = $this->xpath($pattern, $arguments); + $this->assertTrue(!empty($elements), 'The context-aware test block appears.'); $definition = \Drupal::service('plugin.manager.block')->getDefinition('test_context_aware'); $this->assertTrue(!empty($definition), 'The context-aware test block exists.'); + + $edit = [ + 'region' => 'content', + 'settings[context_mapping][user]' => '@block_test.multiple_static_context:user2', + ]; + $this->drupalPostForm($block_url, $edit, 'Save block'); + + $this->drupalGet(''); + $this->assertText('Test context-aware block'); + $this->assertRaw($expected_text); } /** diff --git a/core/modules/block/tests/modules/block_test/block_test.services.yml b/core/modules/block/tests/modules/block_test/block_test.services.yml new file mode 100644 index 000000000000..f165a02470fc --- /dev/null +++ b/core/modules/block/tests/modules/block_test/block_test.services.yml @@ -0,0 +1,6 @@ +services: + block_test.multiple_static_context: + class: Drupal\block_test\ContextProvider\MultipleStaticContext + arguments: ['@current_user', '@entity.manager'] + tags: + - { name: 'context_provider' } diff --git a/core/modules/block/tests/modules/block_test/src/ContextProvider/MultipleStaticContext.php b/core/modules/block/tests/modules/block_test/src/ContextProvider/MultipleStaticContext.php new file mode 100644 index 000000000000..fea7f6d9f167 --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/ContextProvider/MultipleStaticContext.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Contains \Drupal\block_test\ContextProvider\MultipleStaticContext. + */ + +namespace Drupal\block_test\ContextProvider; + +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\ContextProviderInterface; +use Drupal\Core\Session\AccountInterface; + +/** + * Sets multiple contexts for a static value. + */ +class MultipleStaticContext implements ContextProviderInterface { + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * The user storage. + * + * @var \Drupal\user\UserStorageInterface + */ + protected $userStorage; + + /** + * Constructs a new MultipleStaticContext. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The current user. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct(AccountInterface $account, EntityManagerInterface $entity_manager) { + $this->account = $account; + $this->userStorage = $entity_manager->getStorage('user'); + } + + /** + * {@inheritdoc} + */ + public function getRuntimeContexts(array $unqualified_context_ids) { + $current_user = $this->userStorage->load($this->account->id()); + + $context1 = new Context(new ContextDefinition('entity:user', 'User 1')); + $context1->setContextValue($current_user); + + $context2 = new Context(new ContextDefinition('entity:user', 'User 2')); + $context2->setContextValue($current_user); + + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['user']); + + $context1->addCacheableDependency($cacheability); + $context2->addCacheableDependency($cacheability); + + return [ + 'user1' => $context1, + 'user2' => $context2, + ]; + } + + /** + * {@inheritdoc} + */ + public function getAvailableContexts() { + return $this->getRuntimeContexts([]); + } + +} diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php index 56b127b13fcf..c4223d0ea79f 100644 --- a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareBlock.php @@ -29,6 +29,8 @@ public function build() { /** @var $user \Drupal\user\UserInterface */ $user = $this->getContextValue('user'); return array( + '#prefix' => '<div id="' . $this->getPluginId() . '--username">', + '#suffix' => '</div>', '#markup' => $user->getUsername(), ); } diff --git a/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php new file mode 100644 index 000000000000..7232e745a9ba --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/Plugin/Block/TestContextAwareUnsatisfiedBlock.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\block_test\Plugin\Block\TestContextAwareBlock. + */ + +namespace Drupal\block_test\Plugin\Block; + +use Drupal\Core\Block\BlockBase; + +/** + * Provides a context-aware block. + * + * @Block( + * id = "test_context_aware_unsatisfied", + * admin_label = @Translation("Test context-aware unsatisfied block"), + * context = { + * "user" = @ContextDefinition("entity:foobar") + * } + * ) + */ +class TestContextAwareUnsatisfiedBlock extends BlockBase { + + /** + * {@inheritdoc} + */ + public function build() { + return [ + '#markup' => 'test', + ]; + } + +} diff --git a/core/modules/node/src/ContextProvider/NodeRouteContext.php b/core/modules/node/src/ContextProvider/NodeRouteContext.php index e6f5781ce79f..6f55b08567c7 100644 --- a/core/modules/node/src/ContextProvider/NodeRouteContext.php +++ b/core/modules/node/src/ContextProvider/NodeRouteContext.php @@ -13,12 +13,15 @@ use Drupal\Core\Plugin\Context\ContextProviderInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\node\Entity\Node; +use Drupal\Core\StringTranslation\StringTranslationTrait; /** * Sets the current node as a context on node routes. */ class NodeRouteContext implements ContextProviderInterface { + use StringTranslationTrait; + /** * The route match object. * @@ -63,7 +66,7 @@ public function getRuntimeContexts(array $unqualified_context_ids) { * {@inheritdoc} */ public function getAvailableContexts() { - $context = new Context(new ContextDefinition('entity:node')); + $context = new Context(new ContextDefinition('entity:node', $this->t('Node from URL'))); return ['node' => $context]; } -- GitLab