diff --git a/core/modules/layout_builder/layout_builder.links.contextual.yml b/core/modules/layout_builder/layout_builder.links.contextual.yml
index bcf2a9cf06..c2c7bff44d 100644
--- a/core/modules/layout_builder/layout_builder.links.contextual.yml
+++ b/core/modules/layout_builder/layout_builder.links.contextual.yml
@@ -17,3 +17,13 @@ layout_builder_block_remove:
class: ['use-ajax']
data-dialog-type: dialog
data-dialog-renderer: off_canvas
+
+layout_builder_block_visibility:
+ title: 'Control visibility'
+ route_name: 'layout_builder.visibility'
+ group: 'layout_builder_block'
+ options:
+ attributes:
+ class: ['use-ajax']
+ data-dialog-type: dialog
+ data-dialog-renderer: off_canvas
diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml
index 085eb30111..9b7b067312 100644
--- a/core/modules/layout_builder/layout_builder.routing.yml
+++ b/core/modules/layout_builder/layout_builder.routing.yml
@@ -116,5 +116,41 @@ layout_builder.move_block:
section_storage:
layout_builder_tempstore: TRUE
+layout_builder.visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\BlockVisibilityForm'
+ requirements:
+ _permission: 'configure any layout'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
+
+layout_builder.add_visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}/{plugin_id}'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\ConfigureVisibility'
+ requirements:
+ _permission: 'configure any layout'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
+
+layout_builder.delete_visibility:
+ path: '/layout_builder/visibility/block/{section_storage_type}/{section_storage}/{delta}/{uuid}/{plugin_id}/delete'
+ defaults:
+ _form: '\Drupal\layout_builder\Form\DeleteVisibility'
+ requirements:
+ _permission: 'configure any layout'
+ options:
+ _admin_route: TRUE
+ parameters:
+ section_storage:
+ layout_builder_tempstore: TRUE
+
route_callbacks:
- 'layout_builder.routes:getRoutes'
diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
index 62164f65f0..d513708c25 100644
--- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -180,7 +180,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s
$section = $section_storage->getSection($delta);
$layout = $section->getLayout();
- $build = $section->toRenderArray($this->getAvailableContexts($section_storage));
+ $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE);
$layout_definition = $layout->getPluginDefinition();
foreach ($layout_definition->getRegions() as $region => $info) {
diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
index faa4421d1a..6108250879 100644
--- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -259,7 +259,7 @@ public function buildMultiple(array $entities) {
// https://www.drupal.org/node/2932462.
$contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
foreach ($sections as $delta => $section) {
- $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
+ $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts, FALSE);
}
}
}
diff --git a/core/modules/layout_builder/src/Form/BlockVisibilityForm.php b/core/modules/layout_builder/src/Form/BlockVisibilityForm.php
new file mode 100644
index 0000000000..07477d589b
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/BlockVisibilityForm.php
@@ -0,0 +1,198 @@
+conditionManager = $condition_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.condition')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_block_visibility';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+
+ $visibility_conditions = $section_storage->getSection($delta)->getComponent($uuid)->get('visibility');
+ if (!$visibility_conditions) {
+ $visibility_conditions = [];
+ }
+ $conditions = [];
+ foreach ($this->conditionManager->getDefinitionsForContexts($this->getAvailableContexts($section_storage)) as $plugin_id => $definition) {
+ $conditions[$plugin_id] = $definition['label'];
+ }
+ $form['condition'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Add a visibility condition'),
+ '#options' => $conditions,
+ '#empty_value' => '',
+ ];
+ $items = [];
+ foreach ($visibility_conditions as $visibility_id => $configuration) {
+ /** @var \Drupal\Core\Condition\ConditionInterface $condition */
+ $condition = $this->conditionManager->createInstance($configuration['id'], $configuration);
+ $condition->summary();
+ $options = [
+ 'attributes' => [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'dialog',
+ 'data-dialog-renderer' => 'off_canvas',
+ 'data-outside-in-edit' => TRUE,
+ ],
+ ];
+ $items[] = [
+ ['#markup' => $condition->getPluginId() . '
' . $condition->summary()],
+ [
+ '#type' => 'operations',
+ '#links' => [
+ 'edit' => [
+ 'title' => $this->t('Edit'),
+ 'url' => Url::fromRoute('layout_builder.add_visibility', $this->getParameters($visibility_id), $options),
+ ],
+ 'delete' => [
+ 'title' => $this->t('Delete'),
+ 'url' => Url::fromRoute('layout_builder.delete_visibility', $this->getParameters($visibility_id), $options),
+ ],
+ ],
+ ],
+ ];
+ }
+ if ($items) {
+ $form['visibility_title'] = [
+ '#markup' => '
' . $this->t('Configured Conditions') . '
',
+ ];
+ $form['visibility'] = [
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#theme' => 'item_list',
+ '#items' => $items,
+ '#empty' => $this->t('No required conditions have been configured.'),
+ ];
+ }
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add Condition'),
+ ];
+ if ($this->isAjax()) {
+ $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+ $form['actions']['submit']['#ajax']['event'] = 'click';
+ }
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+ $condition = $form_state->getValue('condition');
+ $parameters = $this->getParameters($condition);
+ $new_form = \Drupal::formBuilder()->getForm('\Drupal\layout_builder\Form\ConfigureVisibility', $this->sectionStorage, $parameters['delta'], $parameters['uuid'], $parameters['plugin_id']);
+ $new_form['#action'] = (new Url('layout_builder.add_visibility', $parameters))->toString();
+ $new_form['actions']['submit']['#attached']['drupalSettings']['ajax'][$new_form['actions']['submit']['#id']]['url'] = new Url('layout_builder.add_visibility', $parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]);
+ $response = new AjaxResponse();
+ $response->addCommand(new OpenOffCanvasDialogCommand($this->t("Configure Condition"), $new_form));
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $parameters = $this->getParameters($form_state->getValue('condition'));
+ $url = new Url('layout_builder.add_visibility', $parameters);
+ $response = new RedirectResponse($url->toString());
+ $form_state->setResponse($response);
+ }
+
+ /**
+ * Gets the parameters needed for the various url and form invocations.
+ *
+ * @param string $visibility_id
+ * The id of the visibility plugin.
+ *
+ * @return array
+ */
+ protected function getParameters($visibility_id) {
+ return [
+ 'section_storage_type' => $this->sectionStorage->getStorageType(),
+ 'section_storage' => $this->sectionStorage->getStorageId(),
+ 'delta' => $this->delta,
+ 'uuid' => $this->uuid,
+ 'plugin_id' => $visibility_id,
+ ];
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/ConfigureVisibility.php b/core/modules/layout_builder/src/Form/ConfigureVisibility.php
new file mode 100644
index 0000000000..37044b14e2
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/ConfigureVisibility.php
@@ -0,0 +1,241 @@
+layoutTempstoreRepository = $layout_tempstore_repository;
+ $this->conditionManager = $condition_manager;
+ $this->uuidGenerator = $uuid_generator;
+ $this->pluginFormFactory = $plugin_form_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('layout_builder.tempstore_repository'),
+ $container->get('plugin.manager.condition'),
+ $container->get('uuid'),
+ $container->get('plugin_form.factory')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_configure_visibility';
+ }
+
+ /**
+ * Prepares the condition plugin based on the condition ID.
+ *
+ * @param string $condition_id
+ * A condition UUID, or the plugin ID used to create a new condition.
+ *
+ * @param array $value
+ * The condition configuration.
+ *
+ * @return \Drupal\Core\Condition\ConditionInterface
+ * The condition plugin.
+ */
+ protected function prepareCondition($condition_id, array $value) {
+ if ($value) {
+ return $this->conditionManager->createInstance($value['id'], $value);
+ }
+ /** @var \Drupal\Core\Condition\ConditionInterface $condition */
+ $condition = $this->conditionManager->createInstance($condition_id);
+ $configuration = $condition->getConfiguration();
+ $configuration['uuid'] = $this->uuidGenerator->generate();
+ $condition->setConfiguration($configuration);
+ return $condition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL, $plugin_id = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+
+ $visibility_conditions = $section_storage->getSection($delta)->getComponent($uuid)->get('visibility');
+ $configuration = !empty($visibility_conditions[$plugin_id]) ? $visibility_conditions[$plugin_id] : [];
+ $this->condition = $this->prepareCondition($plugin_id, $configuration);
+
+ $form_state->setTemporaryValue('gathered_contexts', $this->getAvailableContexts($section_storage));
+
+ $form['#tree'] = TRUE;
+ $form['settings'] = [];
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $form['settings'] = $this->getPluginForm($this->condition)->buildConfigurationForm($form['settings'], $subform_state);
+ $form['settings']['id'] = [
+ '#type' => 'value',
+ '#value' => $this->condition->getPluginId(),
+ ];
+
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => $configuration ? $this->t('Update') : $this->t('Add Condition'),
+ '#button_type' => 'primary',
+ ];
+
+ if ($this->isAjax()) {
+ $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $this->getPluginForm($this->condition)->validateConfigurationForm($form['settings'], $subform_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Call the plugin submit handler.
+ $subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
+ $this->getPluginForm($this->condition)->submitConfigurationForm($form, $subform_state);
+
+ // If this block is context-aware, set the context mapping.
+ if ($this->condition instanceof ContextAwarePluginInterface) {
+ $this->condition->setContextMapping($subform_state->getValue('context_mapping', []));
+ }
+
+ $configuration = $this->condition->getConfiguration();
+
+ $component = $this->sectionStorage->getSection($this->delta)->getComponent($this->uuid);
+ $visibility_conditions = $component->get('visibility');
+ $visibility_conditions[$configuration['uuid']] = $configuration;
+ $component->set('visibility', $visibility_conditions);
+
+ $this->layoutTempstoreRepository->set($this->sectionStorage);
+ $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
+ return $this->rebuildAndClose($this->sectionStorage);
+ }
+
+ /**
+ * Retrieves the plugin form for a given condition.
+ *
+ * @param \Drupal\Core\Condition\ConditionInterface $condition
+ * The condition plugin.
+ *
+ * @return \Drupal\Core\Plugin\PluginFormInterface
+ * The plugin form for the condition.
+ */
+ protected function getPluginForm(ConditionInterface $condition) {
+ if ($condition instanceof PluginWithFormsInterface) {
+ return $this->pluginFormFactory->createInstance($condition, 'configure');
+ }
+ return $condition;
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/DeleteVisibility.php b/core/modules/layout_builder/src/Form/DeleteVisibility.php
new file mode 100644
index 0000000000..bced5e6851
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/DeleteVisibility.php
@@ -0,0 +1,160 @@
+layoutTempstoreRepository = $layout_tempstore_repository;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('layout_builder.tempstore_repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $uuid = NULL, $plugin_id = NULL) {
+ $this->sectionStorage = $section_storage;
+ $this->delta = $delta;
+ $this->uuid = $uuid;
+ $this->plugin_id = $plugin_id;
+ $form = parent::buildForm($form, $form_state);
+ $form['actions']['cancel'] = $this->buildCancelLink();
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t("Are you sure you want to delete this visibility condition?");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ $parameters = $this->getParameters();
+ return new Url('layout_builder.visibility', $parameters);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_delete_visibility';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $component = $this->sectionStorage->getSection($this->delta)->getComponent($this->uuid);
+ $visibility_conditions = $component->get('visibility');
+ unset($visibility_conditions[$this->plugin_id]);
+ $component->set('visibility', $visibility_conditions);
+ $this->layoutTempstoreRepository->set($this->sectionStorage);
+ $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
+ }
+
+ /**
+ * Build a cancel button for the confirm form.
+ */
+ protected function buildCancelLink() {
+ return [
+ '#type' => 'button',
+ '#value' => $this->getCancelText(),
+ '#ajax' => [
+ 'callback' => '::ajaxCancel',
+ ],
+ ];
+ }
+
+ /**
+ * Provides an ajax callback for the cancel button.
+ */
+ public function ajaxCancel(array &$form, FormStateInterface $form_state) {
+ $parameters = $this->getParameters();
+ $new_form = \Drupal::formBuilder()->getForm('\Drupal\layout_builder\Form\BlockVisibilityForm', $parameters['section_storage_type'], $parameters['section_storage'], $parameters['delta'], $parameters['uuid']);
+ $new_form['#action'] = $this->getCancelUrl()->toString();
+ $response = new AjaxResponse();
+ $response->addCommand(new OpenOffCanvasDialogCommand($this->t("Configure Condition"), $new_form));
+ return $response;
+ }
+
+ /**
+ * Gets the parameters needed for the various url and form invocations.
+ *
+ * @return array
+ */
+ protected function getParameters() {
+ return [
+ 'section_storage_type' => $this->sectionStorage->getStorageType(),
+ 'section_storage' => $this->sectionStorage->getStorageId(),
+ 'delta' => $this->delta,
+ 'uuid' => $this->uuid,
+ ];
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php
index e183fb6713..7cdc845906 100644
--- a/core/modules/layout_builder/src/Section.php
+++ b/core/modules/layout_builder/src/Section.php
@@ -69,10 +69,13 @@ public function __construct($layout_id, array $layout_settings = [], array $comp
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
*
- * @return array
- * A renderable array representing the content of the section.
+ * @param bool $admin
+ * Determines if we're rendering administratively or not.
+ *
+ * @return array A renderable array representing the content of the section.
+ * A renderable array representing the content of the section.
*/
- public function toRenderArray(array $contexts = []) {
+ public function toRenderArray(array $contexts = [], $admin) {
$layout = $this->getLayout();
// @todo Add the regions to the $build in the correct order. This is done
@@ -81,7 +84,7 @@ public function toRenderArray(array $contexts = []) {
$regions = array_fill_keys($layout->getPluginDefinition()->getRegionNames(), []);
foreach ($this->getComponents() as $component) {
- if ($output = $component->toRenderArray($contexts)) {
+ if ($output = $component->toRenderArray($contexts, $admin)) {
$regions[$component->getRegion()][$component->getUuid()] = $output;
}
}
diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php
index 1f31738d65..c2ce3b7fd1 100644
--- a/core/modules/layout_builder/src/SectionComponent.php
+++ b/core/modules/layout_builder/src/SectionComponent.php
@@ -5,6 +5,7 @@
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\Condition\ConditionAccessResolverTrait;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
/**
@@ -31,6 +32,8 @@
*/
class SectionComponent {
+ use ConditionAccessResolverTrait;
+
/**
* The UUID of the component.
*
@@ -91,11 +94,15 @@ public function __construct($uuid, $region, array $configuration = [], array $ad
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
*
- * @return array
- * A renderable array representing the content of the component.
+ * @param bool $admin
+ * Determines if we're rendering administratively or not.
+ *
+ * @return array A renderable array representing the content of the component.
+ * A renderable array representing the content of the component.
*/
- public function toRenderArray(array $contexts = []) {
+ public function toRenderArray(array $contexts = [], $admin) {
$output = [];
+ $conditions = [];
$plugin = $this->getPlugin($contexts);
// @todo Figure out the best way to unify fields and blocks and components
@@ -103,7 +110,24 @@ public function toRenderArray(array $contexts = []) {
if ($plugin instanceof BlockPluginInterface) {
$access = $plugin->access($this->currentUser(), TRUE);
$cacheability = CacheableMetadata::createFromObject($access);
+ if (!$admin) {
+ foreach ($this->get('visibility') as $uuid => $condition) {
+ $condition = $this->conditionManager()->createInstance($condition['id'], $condition);
+ if ($condition instanceof ContextAwarePluginInterface) {
+ $this->contextHandler()->applyContextMapping($condition, $contexts);
+ }
+ $cacheability->addCacheableDependency($condition);
+ $conditions[$uuid] = $condition;
+ }
+ }
+
+ if ($conditions && !$this->resolveConditions($conditions, 'and')) {
+ $cacheability->addCacheableDependency($plugin);
+ $cacheability->applyTo($output);
+ return $output;
+ }
+ // @todo Add and/or resolution form element.
if ($access->isAllowed()) {
$cacheability->addCacheableDependency($plugin);
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
@@ -286,6 +310,16 @@ protected function pluginManager() {
return \Drupal::service('plugin.manager.block');
}
+ /**
+ * Wraps the condition plugin manager.
+ *
+ * @return \Drupal\Core\Condition\ConditionManager
+ * The condition plugin manager.
+ */
+ protected function conditionManager() {
+ return \Drupal::service('plugin.manager.condition');
+ }
+
/**
* Wraps the context handler.
*