diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 9278240..6263d37 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -332,6 +332,9 @@ block_settings: sequence: type: string +block.settings.*: + type: block_settings + condition.plugin: type: mapping label: 'Condition' diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index df2a2fd..18742ac 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -78,6 +78,27 @@ core.entity_view_display.*.*.*: label: 'Third party settings' sequence: type: field.formatter.third_party.[%key] + blocks: + type: sequence + label: 'Block fields' + sequence: + type: mapping + mapping: + id: + type: string + label: 'ID' + plugin_id: + type: string + label: 'Plugin ID' + weight: + type: integer + label: 'Weight' + region: + type: string + label: 'Region' + settings: + type: block.settings.[id] + hidden: type: sequence label: 'Field display setting' diff --git a/core/core.services.yml b/core/core.services.yml index 30fbcc2..b98fae8 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -338,7 +338,7 @@ services: - [setValidationConstraintManager, ['@validation.constraint']] context.handler: class: Drupal\Core\Plugin\Context\ContextHandler - arguments: ['@typed_data_manager'] + arguments: ['@context.repository'] context.repository: class: Drupal\Core\Plugin\Context\LazyContextRepository arguments: ['@service_container'] diff --git a/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php b/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php index 3598b51..a930f30 100644 --- a/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php +++ b/core/lib/Drupal/Core/Entity/Display/EntityViewDisplayInterface.php @@ -46,4 +46,24 @@ public function build(FieldableEntityInterface $entity); */ public function buildMultiple(array $entities); + /** + * @todo. + */ + public function getBlocks(); + + /** + * @todo. + */ + public function getBlock($name); + + /** + * @todo. + */ + public function removeBlock($name); + + /** + * @todo. + */ + public function setBlock($name, $block); + } diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php index 74b15b6..0a29fb1 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityViewDisplay.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\EntityDisplayBase; +use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\TypedData\TranslatableInterface; /** @@ -27,6 +28,7 @@ * "bundle", * "mode", * "content", + * "blocks", * "hidden", * } * ) @@ -39,6 +41,47 @@ class EntityViewDisplay extends EntityDisplayBase implements EntityViewDisplayIn protected $displayContext = 'view'; /** + * @todo. + * + * @var array + */ + protected $blocks = []; + + /** + * {@inheritdoc} + */ + public function getBlocks() { + return $this->blocks; + } + + /** + * {@inheritdoc} + */ + public function getBlock($name) { + $blocks = $this->getBlocks(); + return isset($blocks[$name]) ? $blocks[$name] : NULL; + } + + /** + * {@inheritdoc} + */ + public function removeBlock($name) { + unset($this->blocks[$name]); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setBlock($name, $block) { + $this->blocks[$name] = $block + [ + 'weight' => 0, + 'region' => $this->getDefaultRegion(), + ]; + return $this; + } + + /** * Returns the display objects used to render a set of entities. * * Depending on the configuration of the view mode for each bundle, this can @@ -263,6 +306,21 @@ public function buildMultiple(array $entities) { } } + foreach ($this->getBlocks() as $name => $block_info) { + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + $block = \Drupal::service('plugin.manager.block')->createInstance($block_info['plugin_id'], $block_info['settings']); + + if ($block instanceof ContextAwarePluginInterface) { + \Drupal::service('context.handler')->applyRuntimeContext($block); + } + + $block_access = $block->access(\Drupal::currentUser(), TRUE); + foreach ($entities as $id => $entity) { + $build_list[$id][$name] = $block_access->isAllowed() ? $block->build() : []; + $this->renderer->addCacheableDependency($build_list[$id][$name], $block_access); + } + } + foreach ($entities as $id => $entity) { // Assign the configured weights. foreach ($this->getComponents() as $name => $options) { diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php index 2a13d36..470511d 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandler.php @@ -12,6 +12,23 @@ class ContextHandler implements ContextHandlerInterface { /** + * The context repository. + * + * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface + */ + protected $contextRepository; + + /** + * Constructs a new ContextHandler. + * + * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository + * The context repository. + */ + public function __construct(ContextRepositoryInterface $context_repository) { + $this->contextRepository = $context_repository; + } + + /** * {@inheritdoc} */ public function filterPluginDefinitionsByContexts(array $contexts, array $definitions) { @@ -118,4 +135,12 @@ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contex } } + /** + * {@inheritdoc} + */ + public function applyRuntimeContext(ContextAwarePluginInterface $plugin) { + $contexts = $this->contextRepository->getRuntimeContexts(array_values($plugin->getContextMapping())); + $this->applyContextMapping($plugin, $contexts); + } + } diff --git a/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php b/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php index a0d5703..1c5c622 100644 --- a/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php +++ b/core/lib/Drupal/Core/Plugin/Context/ContextHandlerInterface.php @@ -61,6 +61,9 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface /** * Prepares a plugin for evaluation. * + * This method is for more complex use cases, see ::applyRuntimeContext() for + * the more common approach. + * * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin * A plugin about to be evaluated. * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts @@ -77,4 +80,12 @@ public function getMatchingContexts(array $contexts, ContextDefinitionInterface */ public function applyContextMapping(ContextAwarePluginInterface $plugin, $contexts, $mappings = []); + /** + * Applies all relevant runtime contexts to a plugin. + * + * @param \Drupal\Core\Plugin\ContextAwarePluginInterface $plugin + * A context-aware plugin. + */ + public function applyRuntimeContext(ContextAwarePluginInterface $plugin); + } diff --git a/core/modules/block/config/schema/block.schema.yml b/core/modules/block/config/schema/block.schema.yml index 16d79bc..7dc4f53 100644 --- a/core/modules/block/config/schema/block.schema.yml +++ b/core/modules/block/config/schema/block.schema.yml @@ -30,6 +30,3 @@ block.block.*: sequence: type: condition.plugin.[id] label: 'Visibility Condition' - -block.settings.*: - type: block_settings diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php index 176c326..73af3c4 100644 --- a/core/modules/block/src/BlockAccessControlHandler.php +++ b/core/modules/block/src/BlockAccessControlHandler.php @@ -12,7 +12,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Plugin\Context\ContextHandlerInterface; -use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -34,20 +33,12 @@ class BlockAccessControlHandler extends EntityAccessControlHandler implements En protected $contextHandler; /** - * The context manager service. - * - * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface - */ - protected $contextRepository; - - /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $entity_type, - $container->get('context.handler'), - $container->get('context.repository') + $container->get('context.handler') ); } @@ -58,13 +49,10 @@ public static function createInstance(ContainerInterface $container, EntityTypeI * The entity type definition. * @param \Drupal\Core\Plugin\Context\ContextHandlerInterface $context_handler * The ContextHandler for applying contexts to conditions properly. - * @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository - * The lazy context repository service. */ - public function __construct(EntityTypeInterface $entity_type, ContextHandlerInterface $context_handler, ContextRepositoryInterface $context_repository ) { + public function __construct(EntityTypeInterface $entity_type, ContextHandlerInterface $context_handler) { parent::__construct($entity_type); $this->contextHandler = $context_handler; - $this->contextRepository = $context_repository; } @@ -87,8 +75,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter foreach ($entity->getVisibilityConditions() as $condition_id => $condition) { if ($condition instanceof ContextAwarePluginInterface) { try { - $contexts = $this->contextRepository->getRuntimeContexts(array_values($condition->getContextMapping())); - $this->contextHandler->applyContextMapping($condition, $contexts); + $this->contextHandler->applyRuntimeContext($condition); } catch (ContextException $e) { $missing_context = TRUE; @@ -113,8 +100,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter $block_plugin = $entity->getPlugin(); try { if ($block_plugin instanceof ContextAwarePluginInterface) { - $contexts = $this->contextRepository->getRuntimeContexts(array_values($block_plugin->getContextMapping())); - $this->contextHandler->applyContextMapping($block_plugin, $contexts); + $this->contextHandler->applyRuntimeContext($block_plugin); } $access = $block_plugin->access($account, TRUE); } diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index 3b20d7b..018d617 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -140,8 +140,7 @@ protected static function buildPreRenderableBlock($entity, ModuleHandlerInterfac // Inject runtime contexts. if ($plugin instanceof ContextAwarePluginInterface) { - $contexts = \Drupal::service('context.repository')->getRuntimeContexts($plugin->getContextMapping()); - \Drupal::service('context.handler')->applyContextMapping($plugin, $contexts); + \Drupal::service('context.handler')->applyRuntimeContext($plugin); } // Create the render array for the block as a whole. diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php index 92bfbde..11725d8 100644 --- a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php @@ -3,6 +3,7 @@ namespace Drupal\field_layout\Form; use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Block\BlockManagerInterface; use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Layout\LayoutPluginManagerInterface; use Drupal\field_ui\Form\EntityViewDisplayEditForm; @@ -22,11 +23,13 @@ class FieldLayoutEntityViewDisplayEditForm extends EntityViewDisplayEditForm { * The field type manager. * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager * The formatter plugin manager. + * @param \Drupal\Core\Block\BlockManagerInterface $block_manager + * The block manager. * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager * The field layout plugin manager. */ - public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $layout_plugin_manager) { - parent::__construct($field_type_manager, $plugin_manager); + public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, BlockManagerInterface $block_manager, LayoutPluginManagerInterface $layout_plugin_manager) { + parent::__construct($field_type_manager, $plugin_manager, $block_manager); $this->layoutPluginManager = $layout_plugin_manager; } @@ -37,6 +40,7 @@ public static function create(ContainerInterface $container) { return new static( $container->get('plugin.manager.field.field_type'), $container->get('plugin.manager.field.formatter'), + $container->get('plugin.manager.block'), $container->get('plugin.manager.core.layout') ); } diff --git a/core/modules/field_ui/field_ui.module b/core/modules/field_ui/field_ui.module index 79b3f15..8f54bdf 100644 --- a/core/modules/field_ui/field_ui.module +++ b/core/modules/field_ui/field_ui.module @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityViewModeInterface; use Drupal\Core\Entity\EntityFormModeInterface; use Drupal\Core\Url; +use Drupal\field_ui\Controller\FieldAddBlockController; use Drupal\field_ui\FieldUI; use Drupal\field_ui\Plugin\Derivative\FieldUiLocalTask; @@ -81,6 +82,7 @@ function field_ui_entity_type_build(array &$entity_types) { $entity_types['entity_form_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityFormDisplayEditForm'); $entity_types['entity_view_display']->setFormClass('edit', 'Drupal\field_ui\Form\EntityViewDisplayEditForm'); + $entity_types['entity_view_display']->setFormClass('add_block', FieldAddBlockController::class); $form_mode = $entity_types['entity_form_mode']; $form_mode->setListBuilderClass('Drupal\field_ui\EntityFormModeListBuilder'); diff --git a/core/modules/field_ui/src/Controller/FieldAddBlockController.php b/core/modules/field_ui/src/Controller/FieldAddBlockController.php new file mode 100644 index 0000000..273becc --- /dev/null +++ b/core/modules/field_ui/src/Controller/FieldAddBlockController.php @@ -0,0 +1,205 @@ +blockManager = $block_manager; + $this->contextRepository = $context_repository; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.block'), + $container->get('context.repository'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) { + $route_parameters = $route_match->getParameters()->all(); + + return $this->getEntityDisplay($route_parameters['entity_type_id'], $route_parameters['bundle'], $route_parameters['view_mode_name']); + } + + /** + * {@inheritdoc} + */ + protected function getEntityDisplay($entity_type_id, $bundle, $mode) { + return entity_get_display($entity_type_id, $bundle, $mode); + } + + /** + * @todo. + * + * @return array + * A render array as expected by the renderer. + */ + public function form(array $form, FormStateInterface $form_state) { + $form['filter'] = [ + '#type' => 'search', + '#title' => $this->t('Filter'), + '#title_display' => 'invisible', + '#size' => 30, + '#placeholder' => $this->t('Filter by block name'), + '#attributes' => [ + 'class' => ['block-filter-text'], + 'data-element' => '.block-add-table', + 'title' => $this->t('Enter a part of the block name to filter by.'), + ], + ]; + $form['blocks'] = [ + '#type' => 'tableselect', + '#header' => [ + 'title' => $this->t('Block'), + 'category' => $this->t('Category'), + ], + '#js_select' => FALSE, + '#empty' => $this->t('No blocks available.'), + '#attributes' => [ + 'class' => ['block-add-table'], + ], + '#element_validate' => [[$this, 'validateTableselect']], + ]; + + // Only add blocks which work without any available context. + $definitions = $this->blockManager->getDefinitionsForContexts($this->contextRepository->getAvailableContexts()); + // Order by category, and then by admin label. + $definitions = $this->blockManager->getSortedDefinitions($definitions); + + foreach ($definitions as $plugin_id => $plugin_definition) { + $form['blocks']['#options'][$plugin_id] = [ + 'title' => [ + 'data' => [ + '#type' => 'inline_template', + '#template' => '
{{ label }}
', + '#context' => [ + 'label' => $plugin_definition['admin_label'], + ], + ], + ], + 'category' => $plugin_definition['category'], + ]; + } + + $form['#attached']['library'][] = 'block/drupal.block.admin'; + + + return $form; + } + + /** + * @todo. + */ + public function validateTableselect($element, FormStateInterface $form_state) { + $values = array_filter(NestedArray::getValue($form_state->getValues(), $element['#parents'])); + $form_state->setValueForElement($element, $values); + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + $block_ids = []; + foreach (array_keys(array_filter($form_state->getValue('blocks'))) as $block_id) { + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + $block = $this->blockManager->createInstance($block_id); + $suggestion = $block->getMachineNameSuggestion(); + $count = 1; + $machine_default = $suggestion; + while (isset($block_ids[$machine_default])) { + $machine_default = $suggestion . '_' . ++$count; + } + $block->setConfigurationValue('label', $block->label()); + $entity->setBlock($machine_default, [ + 'id' => $machine_default, + 'plugin_id' => $block_id, + 'settings' => $block->getConfiguration(), + ]); + $form_state->unsetValue(['blocks', $block_id]); + } + $this->entityFieldManager->clearCachedFieldDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + $entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId()); + $form_state->setRedirect('entity.entity_view_display.' . $this->entity->getTargetEntityTypeId() . '.view_mode', [ + 'view_mode_name' => $this->entity->getMode(), + ] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle())); + drupal_set_message($this->t('Your settings have been saved.')); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Add block'); + return $actions; + } + +} diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php index c27f3d4..303da96 100644 --- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php +++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php @@ -2,9 +2,16 @@ namespace Drupal\field_ui\Form; +use Drupal\Component\Plugin\ContextAwarePluginInterface; +use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Component\Serialization\Json; +use Drupal\Core\Block\BlockManagerInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; use Drupal\Core\Field\PluginSettingsInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\SubformState; use Drupal\Core\Url; use Drupal\field_ui\FieldUI; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -15,17 +22,47 @@ class EntityViewDisplayEditForm extends EntityDisplayFormBase { /** + * The entity being used by this form. + * + * @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface + */ + protected $entity; + + /** * {@inheritdoc} */ protected $displayContext = 'view'; /** + * The block manager. + * + * @var \Drupal\Core\Block\BlockManagerInterface + */ + protected $blockManager; + + /** + * EntityViewDisplayEditForm constructor. + * + * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager + * The field type manager. + * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager + * The formatter plugin manager. + * @param \Drupal\Core\Block\BlockManagerInterface $block_manager + * The block manager. + */ + public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, BlockManagerInterface $block_manager) { + parent::__construct($field_type_manager, $plugin_manager); + $this->blockManager = $block_manager; + } + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('plugin.manager.field.field_type'), - $container->get('plugin.manager.field.formatter') + $container->get('plugin.manager.field.formatter'), + $container->get('plugin.manager.block') ); } @@ -83,6 +120,203 @@ protected function buildExtraFieldRow($field_id, $extra_field) { /** * {@inheritdoc} */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form_state->setTemporaryValue('gathered_contexts', \Drupal::service('context.repository')->getAvailableContexts()); + foreach ($this->entity->getBlocks() as $block_id => $block_info) { + $form['fields'][$block_id] = $this->buildBlockFieldRow($block_id, $block_info, $form, $form_state); + } + + $form['add_block'] = [ + '#weight' => -100, + '#type' => 'link', + '#title' => $this->t('Add field block'), + '#url' => Url::fromRoute('entity.entity_view_display.' . $this->entity->getTargetEntityTypeId() . '.add_block', $this->getRouteBundleParameters($this->entity->getMode())), + '#attributes' => [ + 'class' => ['use-ajax', 'button', 'button--small'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ], + ]; + return $form; + } + + /** + * @todo. + */ + protected function getRouteBundleParameters($mode) { + $entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId()); + return ['view_mode_name' => $mode] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle()); + } + + /** + * @todo. + */ + protected function buildBlockFieldRow($field_id, $extra_field, $form, FormStateInterface $form_state) { + $display_options = $this->entity->getBlock($field_id); + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + $block = $this->blockManager->createInstance($extra_field['plugin_id'], $extra_field['settings']); + + $regions = array_keys($this->getRegions()); + $label = $block->label(); + $extra_field_row = [ + '#attributes' => ['class' => ['draggable', 'tabledrag-leaf']], + '#row_type' => 'extra_field', + '#region_callback' => [$this, 'getRowRegion'], + '#js_settings' => ['rowHandler' => 'field'], + 'human_name' => [ + '#markup' => $label, + ], + 'weight' => [ + '#type' => 'textfield', + '#title' => $this->t('Weight for @title', ['@title' => $label]), + '#title_display' => 'invisible', + '#default_value' => $display_options ? $display_options['weight'] : 0, + '#size' => 3, + '#attributes' => ['class' => ['field-weight']], + ], + 'parent_wrapper' => [ + 'parent' => [ + '#type' => 'select', + '#title' => $this->t('Parents for @title', ['@title' => $label]), + '#title_display' => 'invisible', + '#options' => array_combine($regions, $regions), + '#empty_value' => '', + '#attributes' => ['class' => ['js-field-parent', 'field-parent']], + '#parents' => ['fields', $field_id, 'parent'], + ], + 'hidden_name' => [ + '#type' => 'hidden', + '#default_value' => $field_id, + '#attributes' => ['class' => ['field-name']], + ], + ], + 'region' => [ + '#type' => 'select', + '#title' => $this->t('Region for @title', ['@title' => $label]), + '#title_display' => 'invisible', + '#options' => $this->getRegionOptions(), + '#default_value' => $display_options ? $display_options['region'] : 'visible', + '#attributes' => ['class' => ['field-region']], + ], + 'empty_cell' => [ + '#markup' => ' ', + ], + 'plugin' => [ + 'type' => [ + '#type' => 'hidden', + '#value' => $display_options ? 'visible' : 'hidden', + '#parents' => ['fields', $field_id, 'type'], + '#attributes' => ['class' => ['field-plugin-type']], + ], + ], + 'settings_summary' => [], + 'settings_edit' => [], + ]; + + // Base button element for the various plugin settings actions. + $base_button = [ + '#submit' => ['::multistepSubmit'], + '#ajax' => [ + 'callback' => '::multistepAjax', + 'wrapper' => 'field-display-overview-wrapper', + 'effect' => 'fade', + ], + '#field_name' => $field_id, + ]; + + if ($form_state->get('plugin_settings_edit') == $field_id) { + // Generate the settings form and allow other modules to alter it. + $extra_field_row['plugin']['#cell_attributes'] = ['colspan' => 3]; + $extra_field_row['plugin']['settings_edit_form'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['field-plugin-settings-edit-form']], + '#parents' => ['fields', $field_id, 'settings_edit_form'], + 'label' => [ + '#markup' => $this->t('Plugin settings'), + ], + 'settings' => [], + 'actions' => [ + '#type' => 'actions', + 'save_settings' => $base_button + [ + '#type' => 'submit', + '#button_type' => 'primary', + '#name' => $field_id . '_plugin_settings_update', + '#value' => $this->t('Update'), + '#op' => 'update', + ], + 'cancel_settings' => $base_button + [ + '#type' => 'submit', + '#name' => $field_id . '_plugin_settings_cancel', + '#value' => $this->t('Cancel'), + '#op' => 'cancel', + // Do not check errors for the 'Cancel' button, but make sure we + // get the value of the 'plugin type' select. + '#limit_validation_errors' => [['fields', $field_id, 'type']], + ], + ], + ]; + $extra_field_row['#attributes']['class'][] = 'field-plugin-settings-editing'; + $subform_state = SubformState::createForSubform($extra_field_row['plugin']['settings_edit_form']['settings'], $form, $form_state); + $extra_field_row['plugin']['settings_edit_form']['settings'] = $block->buildConfigurationForm($extra_field_row['plugin']['settings_edit_form']['settings'], $subform_state); + } + else { + // Check selected plugin settings to display edit link or not. + $extra_field_row['settings_edit'] = $base_button + [ + '#type' => 'image_button', + '#name' => $field_id . '_settings_edit', + '#src' => 'core/misc/icons/787878/cog.svg', + '#attributes' => ['class' => ['field-plugin-settings-edit'], 'alt' => $this->t('Edit')], + '#op' => 'edit', + // Do not check errors for the 'Edit' button, but make sure we get + // the value of the 'plugin type' select. + '#limit_validation_errors' => [['fields', $field_id, 'type']], + '#prefix' => '
', + '#suffix' => '
', + ]; + } + + return $extra_field_row; + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity */ + parent::copyFormValuesToEntity($entity, $form, $form_state); + + foreach ($entity->getBlocks() as $name => $block_info) { + $form_values = $form_state->getValue(['fields', $name]); + $options = $entity->getBlock($name); + if (!isset($options['id']) || $form_values['region'] == 'hidden') { + $entity->removeBlock($name); + } + else { + if ($form_state->get('plugin_settings_update') === $name) { + $form_state->set('plugin_settings_update', NULL); + $block = $this->blockManager->createInstance($options['plugin_id'], $options['settings']); + $sub_form_state = SubformState::createForSubform($form['fields'][$name]['plugin']['settings_edit_form']['settings'], $form, $form_state); + $block->submitConfigurationForm($form, $sub_form_state); + if ($block instanceof ContextAwarePluginInterface && $block->getContextDefinitions()) { + $context_mapping = $sub_form_state->getValue('context_mapping', []); + $block->setContextMapping($context_mapping); + } + $options['settings'] = $block->getConfiguration(); + } + $options['weight'] = $form_values['weight']; + $options['region'] = $form_values['region']; + $entity->setBlock($name, $options); + } + } + } + + /** + * {@inheritdoc} + */ protected function getEntityDisplay($entity_type_id, $bundle, $mode) { return entity_get_display($entity_type_id, $bundle, $mode); } @@ -137,10 +371,7 @@ protected function getTableHeader() { * {@inheritdoc} */ protected function getOverviewUrl($mode) { - $entity_type = $this->entityManager->getDefinition($this->entity->getTargetEntityTypeId()); - return Url::fromRoute('entity.entity_view_display.' . $this->entity->getTargetEntityTypeId() . '.view_mode', [ - 'view_mode_name' => $mode, - ] + FieldUI::getRouteBundleParameter($entity_type, $this->entity->getTargetBundle())); + return Url::fromRoute('entity.entity_view_display.' . $this->entity->getTargetEntityTypeId() . '.view_mode', $this->getRouteBundleParameters($mode)); } /** diff --git a/core/modules/field_ui/src/Routing/RouteSubscriber.php b/core/modules/field_ui/src/Routing/RouteSubscriber.php index b08e7f8..0f9a66b 100644 --- a/core/modules/field_ui/src/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/src/Routing/RouteSubscriber.php @@ -154,6 +154,17 @@ protected function alterRoutes(RouteCollection $collection) { $options ); $collection->add("entity.entity_view_display.{$entity_type_id}.view_mode", $route); + + $route = new Route( + "$path/display/{view_mode_name}/add-block", + [ + '_entity_form' => 'entity_view_display.add_block', + '_title' => 'Add block', + ] + $defaults, + ['_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'], + $options + ); + $collection->add("entity.entity_view_display.{$entity_type_id}.add_block", $route); } } } diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php index 6da3c41..b6a092b 100644 --- a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php @@ -15,7 +15,12 @@ class EntityDisplayTest extends JavascriptTestBase { /** * {@inheritdoc} */ - public static $modules = ['field_ui', 'entity_test']; + public static $modules = ['field_ui', 'entity_test', 'block_test']; + + /** + * @var \Drupal\Core\Session\AccountInterface + */ + protected $user; /** * {@inheritdoc} @@ -30,7 +35,7 @@ protected function setUp() { ]], ]); $entity->save(); - $this->drupalLogin($this->drupalCreateUser([ + $this->user = $this->drupalCreateUser([ 'access administration pages', 'view test entity', 'administer entity_test content', @@ -38,7 +43,8 @@ protected function setUp() { 'administer entity_test display', 'administer entity_test form display', 'view the administration theme', - ])); + ]); + $this->drupalLogin($this->user); } /** @@ -87,6 +93,41 @@ public function testEntityView() { } /** + * Tests that block-based extra fields are available. + */ + public function testEntityViewWithBlocks() { + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->clickLink('Add field block'); + $this->assertSession()->assertWaitOnAjaxRequest(); + // Ensure that blocks with unsatisfiable contexts are not shown. + $this->assertSession()->pageTextNotContains('Test context-aware unsatisfied block'); + // Ensure that context-aware blocks are shown. + $this->assertSession()->pageTextContains('Test context-aware block'); + + // Set the context-aware to visible, but do not assign a context mapping. + $this->getSession()->getPage()->checkField('blocks[test_context_aware]'); + $this->getSession()->getPage()->find('css', '.ui-dialog-buttonpane .button--primary')->press(); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + $this->drupalGet('entity_test/1'); + $this->assertSession()->pageTextContains('No context mapping selected.'); + + // Set the context mapping. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->click('#edit-fields-testcontextawareblock-settings-edit'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->getSession()->getPage()->selectFieldOption('fields[testcontextawareblock][settings_edit_form][settings][context_mapping][user]', '@user.current_user_context:current_user'); + $this->submitForm([], 'Update'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + $this->drupalGet('entity_test/1'); + $this->assertSession()->pageTextNotContains('No context mapping selected'); + $this->assertSession()->pageTextContains($this->user->getAccountName()); + } + + /** * Tests extra fields. */ public function testExtraFields() { diff --git a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php index 4ed5e6e..989af05 100644 --- a/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Plugin/ContextHandlerTest.php @@ -11,6 +11,8 @@ use Drupal\Component\Plugin\Exception\ContextException; use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\ContextHandler; +use Drupal\Core\Plugin\Context\ContextInterface; +use Drupal\Core\Plugin\Context\ContextRepositoryInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\TypedData\DataDefinition; use Drupal\Core\TypedData\Plugin\DataType\StringData; @@ -30,12 +32,20 @@ class ContextHandlerTest extends UnitTestCase { protected $contextHandler; /** + * The context repository. + * + * @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface + */ + protected $contextRepository; + + /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); - $this->contextHandler = new ContextHandler(); + $this->contextRepository = $this->prophesize(ContextRepositoryInterface::class); + $this->contextHandler = new ContextHandler($this->contextRepository->reveal()); } /** @@ -487,6 +497,37 @@ public function testApplyContextMappingConfigurableAssignedMiss() { $this->contextHandler->applyContextMapping($plugin, $contexts, ['miss' => 'name']); } + /** + * @covers ::applyRuntimeContext + * @covers ::applyContextMapping + */ + public function testApplyRuntimeContext() { + $context_data = StringData::createInstance(DataDefinition::create('string')); + $context_data->setValue('foo'); + $context = $this->prophesize(ContextInterface::class); + $context->getContextData()->willReturn($context_data); + $context->hasContextValue()->willReturn(TRUE); + + $contexts = [ + 'name' => $context->reveal(), + ]; + + $context_definition = (new ContextDefinition())->setRequired(FALSE); + + $plugin_context = $this->prophesize(ContextInterface::class); + $plugin_context->addCacheableDependency($context)->shouldBeCalled(); + + $plugin = $this->prophesize(ContextAwarePluginInterface::class); + $plugin->getContextMapping()->willReturn(['hit' => 'name']); + $plugin->getContextDefinitions()->willReturn(['hit' => $context_definition]); + $plugin->getContext('hit')->willReturn($plugin_context->reveal()); + $plugin->setContextValue('hit', $context_data)->shouldBeCalled(); + + $this->contextRepository->getRuntimeContexts(['name'])->willReturn($contexts)->shouldBeCalled(); + + $this->contextHandler->applyRuntimeContext($plugin->reveal()); + } + } interface TestConfigurableContextAwarePluginInterface extends ContextAwarePluginInterface, ConfigurablePluginInterface {