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. *