updated core from 8.4 to 8.5 : bug with login_destination
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
core.entity_view_display.*.*.*.third_party.layout_builder:
|
||||
type: mapping
|
||||
label: 'Per-view-mode Layout Builder settings'
|
||||
mapping:
|
||||
allow_custom:
|
||||
type: boolean
|
||||
label: 'Allow a customized layout'
|
||||
sections:
|
||||
type: sequence
|
||||
sequence:
|
||||
type: layout_builder.section
|
||||
|
||||
layout_builder.section:
|
||||
type: mapping
|
||||
label: 'Layout section'
|
||||
mapping:
|
||||
layout_id:
|
||||
type: string
|
||||
label: 'Layout ID'
|
||||
layout_settings:
|
||||
type: layout_plugin.settings.[%parent.layout_id]
|
||||
label: 'Layout settings'
|
||||
components:
|
||||
type: sequence
|
||||
label: 'Components'
|
||||
sequence:
|
||||
type: layout_builder.component
|
||||
|
||||
layout_builder.component:
|
||||
type: mapping
|
||||
label: 'Component'
|
||||
mapping:
|
||||
configuration:
|
||||
type: block.settings.[id]
|
||||
region:
|
||||
type: string
|
||||
label: 'Region'
|
||||
uuid:
|
||||
type: uuid
|
||||
label: 'UUID'
|
||||
weight:
|
||||
type: integer
|
||||
label: 'Weight'
|
||||
additional:
|
||||
type: ignore
|
||||
label: 'Additional data'
|
||||
@@ -0,0 +1,51 @@
|
||||
.add-section {
|
||||
width: 100%;
|
||||
outline: 2px dashed #979797;
|
||||
padding: 1.5em 0;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5em;
|
||||
transition: visually-hidden 2s ease-out, height 2s ease-in;
|
||||
}
|
||||
|
||||
.layout-section {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.layout-section .layout-builder--layout__region {
|
||||
outline: 2px dashed #2f91da;
|
||||
padding: 1.5em 0;
|
||||
}
|
||||
|
||||
.layout-section .layout-builder--layout__region .add-block {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.layout-section .remove-section {
|
||||
position: relative;
|
||||
background: url(../../../misc/icons/bebebe/ex.svg) #fff center center / 16px 16px no-repeat;
|
||||
border: 1px solid #ccc;
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
white-space: nowrap;
|
||||
text-indent: -9999px;
|
||||
display: inline-block;
|
||||
border-radius: 26px;
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.layout-section .remove-section:hover {
|
||||
background-image: url(../../../misc/icons/787878/ex.svg);
|
||||
}
|
||||
|
||||
#drupal-off-canvas .layout-selection li {
|
||||
display: block;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .layout-selection li a {
|
||||
display: block;
|
||||
padding-top: 0.55em;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
(($, { ajax, behaviors }) => {
|
||||
behaviors.layoutBuilder = {
|
||||
attach(context) {
|
||||
$(context).find('.layout-builder--layout__region').sortable({
|
||||
items: '> .draggable',
|
||||
connectWith: '.layout-builder--layout__region',
|
||||
|
||||
/**
|
||||
* Updates the layout with the new position of the block.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The jQuery Event object.
|
||||
* @param {Object} ui
|
||||
* An object containing information about the item being sorted.
|
||||
*/
|
||||
update(event, ui) {
|
||||
// Check if the region from the event and region for the item match.
|
||||
const itemRegion = ui.item.closest('.layout-builder--layout__region');
|
||||
if (event.target === itemRegion[0]) {
|
||||
// Find the destination delta.
|
||||
const deltaTo = ui.item.closest('[data-layout-delta]').data('layout-delta');
|
||||
// If the block didn't leave the original delta use the destination.
|
||||
const deltaFrom = ui.sender ? ui.sender.closest('[data-layout-delta]').data('layout-delta') : deltaTo;
|
||||
ajax({
|
||||
url: [
|
||||
ui.item.closest('[data-layout-update-url]').data('layout-update-url'),
|
||||
deltaFrom,
|
||||
deltaTo,
|
||||
itemRegion.data('region'),
|
||||
ui.item.data('layout-block-uuid'),
|
||||
ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid'),
|
||||
]
|
||||
.filter(element => element !== undefined)
|
||||
.join('/'),
|
||||
}).execute();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, _ref) {
|
||||
var ajax = _ref.ajax,
|
||||
behaviors = _ref.behaviors;
|
||||
|
||||
behaviors.layoutBuilder = {
|
||||
attach: function attach(context) {
|
||||
$(context).find('.layout-builder--layout__region').sortable({
|
||||
items: '> .draggable',
|
||||
connectWith: '.layout-builder--layout__region',
|
||||
|
||||
update: function update(event, ui) {
|
||||
var itemRegion = ui.item.closest('.layout-builder--layout__region');
|
||||
if (event.target === itemRegion[0]) {
|
||||
var deltaTo = ui.item.closest('[data-layout-delta]').data('layout-delta');
|
||||
|
||||
var deltaFrom = ui.sender ? ui.sender.closest('[data-layout-delta]').data('layout-delta') : deltaTo;
|
||||
ajax({
|
||||
url: [ui.item.closest('[data-layout-update-url]').data('layout-update-url'), deltaFrom, deltaTo, itemRegion.data('region'), ui.item.data('layout-block-uuid'), ui.item.prev('[data-layout-block-uuid]').data('layout-block-uuid')].filter(function (element) {
|
||||
return element !== undefined;
|
||||
}).join('/')
|
||||
}).execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,17 @@
|
||||
name: 'Layout Builder'
|
||||
type: module
|
||||
description: 'Provides layout building utility.'
|
||||
package: Core (Experimental)
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
dependencies:
|
||||
- layout_discovery
|
||||
- contextual
|
||||
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
|
||||
- field_ui
|
||||
|
||||
# Information added by Drupal.org packaging script on 2018-03-07
|
||||
version: '8.5.0'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1520457825
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains install and update functions for Layout Builder.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function layout_builder_install() {
|
||||
$displays = LayoutBuilderEntityViewDisplay::loadMultiple();
|
||||
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
|
||||
foreach ($displays as $display) {
|
||||
// Create the first section from any existing Field Layout settings.
|
||||
$field_layout = $display->getThirdPartySettings('field_layout');
|
||||
if (isset($field_layout['id'])) {
|
||||
$field_layout += ['settings' => []];
|
||||
$display->appendSection(new Section($field_layout['id'], $field_layout['settings']));
|
||||
}
|
||||
|
||||
// Sort the components by weight.
|
||||
$components = $display->get('content');
|
||||
uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
|
||||
foreach ($components as $name => $component) {
|
||||
$display->setComponent($name, $component);
|
||||
}
|
||||
$display->save();
|
||||
}
|
||||
|
||||
// Clear the rendered cache to ensure the new layout builder flow is used.
|
||||
// While in many cases the above change will not affect the rendered output,
|
||||
// the cacheability metadata will have changed and should be processed to
|
||||
// prepare for future changes.
|
||||
Cache::invalidateTags(['rendered']);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
drupal.layout_builder:
|
||||
version: VERSION
|
||||
css:
|
||||
theme:
|
||||
css/layout-builder.css: {}
|
||||
js:
|
||||
js/layout-builder.js: {}
|
||||
dependencies:
|
||||
- core/jquery.ui.sortable
|
||||
- core/drupal.dialog.off_canvas
|
||||
@@ -0,0 +1,19 @@
|
||||
layout_builder_block_update:
|
||||
title: 'Configure'
|
||||
route_name: 'layout_builder.update_block'
|
||||
group: 'layout_builder_block'
|
||||
options:
|
||||
attributes:
|
||||
class: ['use-ajax']
|
||||
data-dialog-type: dialog
|
||||
data-dialog-renderer: off_canvas
|
||||
|
||||
layout_builder_block_remove:
|
||||
title: 'Remove block'
|
||||
route_name: 'layout_builder.remove_block'
|
||||
group: 'layout_builder_block'
|
||||
options:
|
||||
attributes:
|
||||
class: ['use-ajax']
|
||||
data-dialog-type: dialog
|
||||
data-dialog-renderer: off_canvas
|
||||
@@ -0,0 +1,2 @@
|
||||
layout_builder_ui:
|
||||
deriver: '\Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver'
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides hook implementations for Layout Builder.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\FieldConfigInterface;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
|
||||
use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function layout_builder_help($route_name, RouteMatchInterface $route_match) {
|
||||
// Add help text to the Layout Builder UI.
|
||||
if ($route_match->getRouteObject()->getOption('_layout_builder')) {
|
||||
$output = '<p>' . t('This layout builder tool allows you to configure the layout of the main content area.') . '</p>';
|
||||
if (\Drupal::currentUser()->hasPermission('administer blocks')) {
|
||||
$output .= '<p>' . t('To manage other areas of the page, use the <a href="@block-ui">block administration page</a>.', ['@block-ui' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
|
||||
}
|
||||
else {
|
||||
$output .= '<p>' . t('To manage other areas of the page, use the block administration page.') . '</p>';
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
switch ($route_name) {
|
||||
case 'help.page.layout_builder':
|
||||
$output = '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('Layout Builder provides layout building utility.') . '</p>';
|
||||
$output .= '<p>' . t('For more information, see the <a href=":layout-builder-documentation">online documentation for the Layout Builder module</a>.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout_builder']) . '</p>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*/
|
||||
function layout_builder_entity_type_alter(array &$entity_types) {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
|
||||
$entity_types['entity_view_display']
|
||||
->setClass(LayoutBuilderEntityViewDisplay::class)
|
||||
->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
|
||||
->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
|
||||
*/
|
||||
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
|
||||
// Hides the Layout Builder field. It is rendered directly in
|
||||
// \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
|
||||
unset($form['fields']['layout_builder__layout']);
|
||||
$key = array_search('layout_builder__layout', $form['#fields']);
|
||||
if ($key !== FALSE) {
|
||||
unset($form['#fields'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_config_insert().
|
||||
*/
|
||||
function layout_builder_field_config_insert(FieldConfigInterface $field_config) {
|
||||
// Clear the sample entity for this entity type and bundle.
|
||||
$sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
|
||||
$sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
|
||||
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_config_delete().
|
||||
*/
|
||||
function layout_builder_field_config_delete(FieldConfigInterface $field_config) {
|
||||
// Clear the sample entity for this entity type and bundle.
|
||||
$sample_entity_generator = \Drupal::service('layout_builder.sample_entity_generator');
|
||||
$sample_entity_generator->delete($field_config->getTargetEntityTypeId(), $field_config->getTargetBundle());
|
||||
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# @todo Expand permissions to be more granular in
|
||||
# https://www.drupal.org/node/2914486.
|
||||
configure any layout:
|
||||
title: 'Configure any layout'
|
||||
restrict access: true
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update functions for Layout Builder.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Rebuild plugin dependencies for all entity view displays.
|
||||
*/
|
||||
function layout_builder_post_update_rebuild_plugin_dependencies(&$sandbox = NULL) {
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('entity_view_display');
|
||||
if (!isset($sandbox['ids'])) {
|
||||
$sandbox['ids'] = $storage->getQuery()->accessCheck(FALSE)->execute();
|
||||
$sandbox['count'] = count($sandbox['ids']);
|
||||
}
|
||||
|
||||
for ($i = 0; $i < 10 && count($sandbox['ids']); $i++) {
|
||||
$id = array_shift($sandbox['ids']);
|
||||
if ($display = $storage->load($id)) {
|
||||
$display->save();
|
||||
}
|
||||
}
|
||||
|
||||
$sandbox['#finished'] = empty($sandbox['ids']) ? 1 : ($sandbox['count'] - count($sandbox['ids'])) / $sandbox['count'];
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
layout_builder.choose_section:
|
||||
path: '/layout_builder/choose/section/{section_storage_type}/{section_storage}/{delta}'
|
||||
defaults:
|
||||
_controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.add_section:
|
||||
path: '/layout_builder/add/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
|
||||
defaults:
|
||||
_controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.configure_section:
|
||||
path: '/layout_builder/configure/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
|
||||
defaults:
|
||||
_title: 'Configure section'
|
||||
_form: '\Drupal\layout_builder\Form\ConfigureSectionForm'
|
||||
# Adding a new section requires a plugin_id, while configuring an existing
|
||||
# section does not.
|
||||
plugin_id: null
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.remove_section:
|
||||
path: '/layout_builder/remove/section/{section_storage_type}/{section_storage}/{delta}'
|
||||
defaults:
|
||||
_form: '\Drupal\layout_builder\Form\RemoveSectionForm'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.choose_block:
|
||||
path: '/layout_builder/choose/block/{section_storage_type}/{section_storage}/{delta}/{region}'
|
||||
defaults:
|
||||
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.add_block:
|
||||
path: '/layout_builder/add/block/{section_storage_type}/{section_storage}/{delta}/{region}/{plugin_id}'
|
||||
defaults:
|
||||
_form: '\Drupal\layout_builder\Form\AddBlockForm'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.update_block:
|
||||
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
|
||||
defaults:
|
||||
_form: '\Drupal\layout_builder\Form\UpdateBlockForm'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.remove_block:
|
||||
path: '/layout_builder/remove/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
|
||||
defaults:
|
||||
_form: '\Drupal\layout_builder\Form\RemoveBlockForm'
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
|
||||
layout_builder.move_block:
|
||||
path: '/layout_builder/move/block/{section_storage_type}/{section_storage}/{delta_from}/{delta_to}/{region_to}/{block_uuid}/{preceding_block_uuid}'
|
||||
defaults:
|
||||
_controller: '\Drupal\layout_builder\Controller\MoveBlockController::build'
|
||||
delta_from: null
|
||||
delta_to: null
|
||||
region_from: null
|
||||
region_to: null
|
||||
block_uuid: null
|
||||
preceding_block_uuid: null
|
||||
requirements:
|
||||
_permission: 'configure any layout'
|
||||
options:
|
||||
_admin_route: TRUE
|
||||
parameters:
|
||||
section_storage:
|
||||
layout_builder_tempstore: TRUE
|
||||
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
layout_builder.tempstore_repository:
|
||||
class: Drupal\layout_builder\LayoutTempstoreRepository
|
||||
arguments: ['@tempstore.shared']
|
||||
access_check.entity.layout:
|
||||
class: Drupal\layout_builder\Access\LayoutSectionAccessCheck
|
||||
tags:
|
||||
- { name: access_check, applies_to: _has_layout_section }
|
||||
plugin.manager.layout_builder.section_storage:
|
||||
class: Drupal\layout_builder\SectionStorage\SectionStorageManager
|
||||
parent: default_plugin_manager
|
||||
layout_builder.routes:
|
||||
class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
|
||||
arguments: ['@plugin.manager.layout_builder.section_storage']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
layout_builder.route_enhancer:
|
||||
class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
|
||||
tags:
|
||||
- { name: route_enhancer }
|
||||
layout_builder.param_converter:
|
||||
class: Drupal\layout_builder\Routing\LayoutTempstoreParamConverter
|
||||
arguments: ['@layout_builder.tempstore_repository', '@plugin.manager.layout_builder.section_storage']
|
||||
tags:
|
||||
- { name: paramconverter, priority: 10 }
|
||||
cache_context.layout_builder_is_active:
|
||||
class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext
|
||||
arguments: ['@current_route_match']
|
||||
tags:
|
||||
- { name: cache.context}
|
||||
layout_builder.sample_entity_generator:
|
||||
class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator
|
||||
arguments: ['@tempstore.shared', '@entity_type.manager']
|
||||
layout_builder.render_block_component_subscriber:
|
||||
class: Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray
|
||||
arguments: ['@current_user']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides an access check for the Layout Builder UI.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutSectionAccessCheck implements AccessInterface {
|
||||
|
||||
/**
|
||||
* Checks routing access to the layout.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The current route match.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
|
||||
$section_storage = $route_match->getParameter('section_storage');
|
||||
|
||||
if (empty($section_storage)) {
|
||||
return AccessResult::forbidden()->addCacheContexts(['route']);
|
||||
}
|
||||
|
||||
if (!$section_storage instanceof SectionStorageInterface) {
|
||||
$access = AccessResult::forbidden();
|
||||
}
|
||||
else {
|
||||
$access = AccessResult::allowedIfHasPermission($account, 'configure any layout');
|
||||
}
|
||||
|
||||
return $access->addCacheableDependency($section_storage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
|
||||
|
||||
/**
|
||||
* Defines a Section Storage type annotation object.
|
||||
*
|
||||
* @see \Drupal\layout_builder\SectionStorage\SectionStorageManager
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class SectionStorage extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get() {
|
||||
return new SectionStorageDefinition($this->definition);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Cache;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\layout_builder\OverridesSectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Determines whether Layout Builder is active for a given entity type or not.
|
||||
*
|
||||
* Cache context ID: 'layout_builder_is_active:%entity_type_id', e.g.
|
||||
* 'layout_builder_is_active:node' (to vary by whether the Node entity type has
|
||||
* Layout Builder enabled).
|
||||
*/
|
||||
class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterface {
|
||||
|
||||
/**
|
||||
* The current route match.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteMatchInterface
|
||||
*/
|
||||
protected $routeMatch;
|
||||
|
||||
/**
|
||||
* LayoutBuilderCacheContext constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
|
||||
* The current route match.
|
||||
*/
|
||||
public function __construct(RouteMatchInterface $route_match) {
|
||||
$this->routeMatch = $route_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getLabel() {
|
||||
return t('Layout Builder');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContext($entity_type_id = NULL) {
|
||||
if (!$entity_type_id) {
|
||||
throw new \LogicException('Missing entity type ID');
|
||||
}
|
||||
|
||||
$display = $this->getDisplay($entity_type_id);
|
||||
return ($display && $display->isOverridable()) ? '1' : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheableMetadata($entity_type_id = NULL) {
|
||||
if (!$entity_type_id) {
|
||||
throw new \LogicException('Missing entity type ID');
|
||||
}
|
||||
|
||||
$cacheable_metadata = new CacheableMetadata();
|
||||
if ($display = $this->getDisplay($entity_type_id)) {
|
||||
$cacheable_metadata->addCacheableDependency($display);
|
||||
}
|
||||
return $cacheable_metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entity view display for a given entity type and view mode.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface|null
|
||||
* The entity view display, if it exists.
|
||||
*/
|
||||
protected function getDisplay($entity_type_id) {
|
||||
if ($entity = $this->routeMatch->getParameter($entity_type_id)) {
|
||||
if ($entity instanceof OverridesSectionStorageInterface) {
|
||||
return $entity->getDefaultSectionStorage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Context;
|
||||
|
||||
use Drupal\Core\Plugin\Context\ContextInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a wrapper around getting contexts from a section storage object.
|
||||
*/
|
||||
trait LayoutBuilderContextTrait {
|
||||
|
||||
/**
|
||||
* The context repository.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
|
||||
*/
|
||||
protected $contextRepository;
|
||||
|
||||
/**
|
||||
* Gets the context repository service.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
|
||||
* The context repository service.
|
||||
*/
|
||||
protected function contextRepository() {
|
||||
if (!$this->contextRepository) {
|
||||
$this->contextRepository = \Drupal::service('context.repository');
|
||||
}
|
||||
return $this->contextRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides all available contexts, both global and section_storage-specific.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
|
||||
* The array of context objects.
|
||||
*/
|
||||
protected function getAvailableContexts(SectionStorageInterface $section_storage) {
|
||||
// Get all globally available contexts that have a defined value.
|
||||
$contexts = array_filter($this->contextRepository()->getAvailableContexts(), function (ContextInterface $context) {
|
||||
return $context->hasContextValue();
|
||||
});
|
||||
|
||||
// Add in the per-section_storage contexts.
|
||||
$contexts += $section_storage->getContexts();
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
/**
|
||||
* Defines a controller to add a new section.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AddSectionController implements ContainerInjectionInterface {
|
||||
|
||||
use AjaxHelperTrait;
|
||||
use LayoutRebuildTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* AddSectionController constructor.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->classResolver = $class_resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('class_resolver')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the new section.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID of the layout to add.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The controller response.
|
||||
*/
|
||||
public function build(SectionStorageInterface $section_storage, $delta, $plugin_id) {
|
||||
$section_storage->insertSection($delta, new Section($plugin_id));
|
||||
|
||||
$this->layoutTempstoreRepository->set($section_storage);
|
||||
|
||||
if ($this->isAjax()) {
|
||||
return $this->rebuildAndClose($section_storage);
|
||||
}
|
||||
else {
|
||||
$url = $section_storage->getLayoutBuilderUrl();
|
||||
return new RedirectResponse($url->setAbsolute()->toString());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
|
||||
/**
|
||||
* Provides a helper to determine if the current request is via AJAX.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @todo Move to \Drupal\Core in https://www.drupal.org/node/2896535.
|
||||
*/
|
||||
trait AjaxHelperTrait {
|
||||
|
||||
/**
|
||||
* Determines if the current request is via AJAX.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the current request is via AJAX, FALSE otherwise.
|
||||
*/
|
||||
protected function isAjax() {
|
||||
return in_array($this->getRequestWrapperFormat(), [
|
||||
'drupal_ajax',
|
||||
'drupal_dialog',
|
||||
'drupal_dialog.off_canvas',
|
||||
'drupal_modal',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the wrapper format of the current request.
|
||||
*
|
||||
* @string
|
||||
* The wrapper format.
|
||||
*/
|
||||
protected function getRequestWrapperFormat() {
|
||||
return \Drupal::request()->get(MainContentViewSubscriber::WRAPPER_FORMAT);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a controller to choose a new block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ChooseBlockController implements ContainerInjectionInterface {
|
||||
|
||||
use AjaxHelperTrait;
|
||||
use LayoutBuilderContextTrait;
|
||||
|
||||
/**
|
||||
* The block manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* ChooseBlockController constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
|
||||
* The block manager.
|
||||
*/
|
||||
public function __construct(BlockManagerInterface $block_manager) {
|
||||
$this->blockManager = $block_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('plugin.manager.block')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the UI for choosing a new block.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
* @param string $region
|
||||
* The region the block is going in.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
public function build(SectionStorageInterface $section_storage, $delta, $region) {
|
||||
$build['#type'] = 'container';
|
||||
$build['#attributes']['class'][] = 'block-categories';
|
||||
|
||||
$definitions = $this->blockManager->getDefinitionsForContexts($this->getAvailableContexts($section_storage));
|
||||
foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
|
||||
$build[$category]['#type'] = 'details';
|
||||
$build[$category]['#open'] = TRUE;
|
||||
$build[$category]['#title'] = $category;
|
||||
$build[$category]['links'] = [
|
||||
'#theme' => 'links',
|
||||
];
|
||||
foreach ($blocks as $block_id => $block) {
|
||||
$link = [
|
||||
'title' => $block['admin_label'],
|
||||
'url' => Url::fromRoute('layout_builder.add_block',
|
||||
[
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
'plugin_id' => $block_id,
|
||||
]
|
||||
),
|
||||
];
|
||||
if ($this->isAjax()) {
|
||||
$link['attributes']['class'][] = 'use-ajax';
|
||||
$link['attributes']['data-dialog-type'][] = 'dialog';
|
||||
$link['attributes']['data-dialog-renderer'][] = 'off_canvas';
|
||||
}
|
||||
$build[$category]['links']['#links'][] = $link;
|
||||
}
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Layout\LayoutPluginManagerInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a controller to choose a new section.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ChooseSectionController implements ContainerInjectionInterface {
|
||||
|
||||
use AjaxHelperTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The layout manager.
|
||||
*
|
||||
* @var \Drupal\Core\Layout\LayoutPluginManagerInterface
|
||||
*/
|
||||
protected $layoutManager;
|
||||
|
||||
/**
|
||||
* ChooseSectionController constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_manager
|
||||
* The layout manager.
|
||||
*/
|
||||
public function __construct(LayoutPluginManagerInterface $layout_manager) {
|
||||
$this->layoutManager = $layout_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('plugin.manager.core.layout')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose a layout plugin to add as a section.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
*
|
||||
* @return array
|
||||
* The render array.
|
||||
*/
|
||||
public function build(SectionStorageInterface $section_storage, $delta) {
|
||||
$output['#title'] = $this->t('Choose a layout');
|
||||
|
||||
$items = [];
|
||||
foreach ($this->layoutManager->getDefinitions() as $plugin_id => $definition) {
|
||||
$layout = $this->layoutManager->createInstance($plugin_id);
|
||||
$item = [
|
||||
'#type' => 'link',
|
||||
'#title' => [
|
||||
$definition->getIcon(60, 80, 1, 3),
|
||||
[
|
||||
'#type' => 'container',
|
||||
'#children' => $definition->getLabel(),
|
||||
],
|
||||
],
|
||||
'#url' => Url::fromRoute(
|
||||
$layout instanceof PluginFormInterface ? 'layout_builder.configure_section' : 'layout_builder.add_section',
|
||||
[
|
||||
'section_storage_type' => $section_storage->getStorageType(),
|
||||
'section_storage' => $section_storage->getStorageId(),
|
||||
'delta' => $delta,
|
||||
'plugin_id' => $plugin_id,
|
||||
]
|
||||
),
|
||||
];
|
||||
if ($this->isAjax()) {
|
||||
$item['#attributes']['class'][] = 'use-ajax';
|
||||
$item['#attributes']['data-dialog-type'][] = 'dialog';
|
||||
$item['#attributes']['data-dialog-renderer'][] = 'off_canvas';
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
$output['layouts'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => $items,
|
||||
'#attributes' => [
|
||||
'class' => [
|
||||
'layout-selection',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\OverridesSectionStorageInterface;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
/**
|
||||
* Defines a controller to provide the Layout Builder admin UI.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutBuilderController implements ContainerInjectionInterface {
|
||||
|
||||
use LayoutBuilderContextTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* LayoutBuilderController constructor.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('messenger')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a title callback.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return string
|
||||
* The title for the layout page.
|
||||
*/
|
||||
public function title(SectionStorageInterface $section_storage) {
|
||||
return $this->t('Edit layout for %label', ['%label' => $section_storage->label()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the Layout UI.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param bool $is_rebuilding
|
||||
* (optional) Indicates if the layout is rebuilding, defaults to FALSE.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
public function layout(SectionStorageInterface $section_storage, $is_rebuilding = FALSE) {
|
||||
$this->prepareLayout($section_storage, $is_rebuilding);
|
||||
|
||||
$output = [];
|
||||
$count = 0;
|
||||
for ($i = 0; $i < $section_storage->count(); $i++) {
|
||||
$output[] = $this->buildAddSectionLink($section_storage, $count);
|
||||
$output[] = $this->buildAdministrativeSection($section_storage, $count);
|
||||
$count++;
|
||||
}
|
||||
$output[] = $this->buildAddSectionLink($section_storage, $count);
|
||||
$output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
|
||||
$output['#type'] = 'container';
|
||||
$output['#attributes']['id'] = 'layout-builder';
|
||||
// Mark this UI as uncacheable.
|
||||
$output['#cache']['max-age'] = 0;
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a layout for use in the UI.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param bool $is_rebuilding
|
||||
* Indicates if the layout is rebuilding.
|
||||
*/
|
||||
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
|
||||
// Only add sections if the layout is new and empty.
|
||||
if (!$is_rebuilding && $section_storage->count() === 0) {
|
||||
$sections = [];
|
||||
// If this is an empty override, copy the sections from the corresponding
|
||||
// default.
|
||||
if ($section_storage instanceof OverridesSectionStorageInterface) {
|
||||
$sections = $section_storage->getDefaultSectionStorage()->getSections();
|
||||
}
|
||||
|
||||
// For an empty layout, begin with a single section of one column.
|
||||
if (!$sections) {
|
||||
$sections[] = new Section('layout_onecol');
|
||||
}
|
||||
|
||||
foreach ($sections as $section) {
|
||||
$section_storage->appendSection($section);
|
||||
}
|
||||
$this->layoutTempstoreRepository->set($section_storage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a link to add a new section at a given delta.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section to splice.
|
||||
*
|
||||
* @return array
|
||||
* A render array for a link.
|
||||
*/
|
||||
protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
|
||||
$storage_type = $section_storage->getStorageType();
|
||||
$storage_id = $section_storage->getStorageId();
|
||||
return [
|
||||
'link' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Add Section'),
|
||||
'#url' => Url::fromRoute('layout_builder.choose_section',
|
||||
[
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
'delta' => $delta,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
],
|
||||
]
|
||||
),
|
||||
],
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['add-section'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the render array for the layout section while editing.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
*
|
||||
* @return array
|
||||
* The render array for a given section.
|
||||
*/
|
||||
protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
|
||||
$storage_type = $section_storage->getStorageType();
|
||||
$storage_id = $section_storage->getStorageId();
|
||||
$section = $section_storage->getSection($delta);
|
||||
|
||||
$layout = $section->getLayout();
|
||||
$build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE);
|
||||
$layout_definition = $layout->getPluginDefinition();
|
||||
|
||||
foreach ($layout_definition->getRegions() as $region => $info) {
|
||||
if (!empty($build[$region])) {
|
||||
foreach ($build[$region] as $uuid => $block) {
|
||||
$build[$region][$uuid]['#attributes']['class'][] = 'draggable';
|
||||
$build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid;
|
||||
$build[$region][$uuid]['#contextual_links'] = [
|
||||
'layout_builder_block' => [
|
||||
'route_parameters' => [
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
'uuid' => $uuid,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$build[$region]['layout_builder_add_block']['link'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Add Block'),
|
||||
'#url' => Url::fromRoute('layout_builder.choose_block',
|
||||
[
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
'delta' => $delta,
|
||||
'region' => $region,
|
||||
],
|
||||
[
|
||||
'attributes' => [
|
||||
'class' => ['use-ajax'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
],
|
||||
]
|
||||
),
|
||||
];
|
||||
$build[$region]['layout_builder_add_block']['#type'] = 'container';
|
||||
$build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['add-block']];
|
||||
$build[$region]['layout_builder_add_block']['#weight'] = 1000;
|
||||
$build[$region]['#attributes']['data-region'] = $region;
|
||||
$build[$region]['#attributes']['class'][] = 'layout-builder--layout__region';
|
||||
}
|
||||
|
||||
$build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
])->toString();
|
||||
$build['#attributes']['data-layout-delta'] = $delta;
|
||||
$build['#attributes']['class'][] = 'layout-builder--layout';
|
||||
|
||||
return [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['layout-section'],
|
||||
],
|
||||
'configure' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Configure section'),
|
||||
'#access' => $layout instanceof PluginFormInterface,
|
||||
'#url' => Url::fromRoute('layout_builder.configure_section', [
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
'delta' => $delta,
|
||||
]),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'configure-section'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
],
|
||||
],
|
||||
'remove' => [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Remove section'),
|
||||
'#url' => Url::fromRoute('layout_builder.remove_section', [
|
||||
'section_storage_type' => $storage_type,
|
||||
'section_storage' => $storage_id,
|
||||
'delta' => $delta,
|
||||
]),
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'remove-section'],
|
||||
'data-dialog-type' => 'dialog',
|
||||
'data-dialog-renderer' => 'off_canvas',
|
||||
],
|
||||
],
|
||||
'layout-section' => $build,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the layout.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect response.
|
||||
*/
|
||||
public function saveLayout(SectionStorageInterface $section_storage) {
|
||||
$section_storage->save();
|
||||
$this->layoutTempstoreRepository->delete($section_storage);
|
||||
|
||||
if ($section_storage instanceof OverridesSectionStorageInterface) {
|
||||
$this->messenger->addMessage($this->t('The layout override has been saved.'));
|
||||
}
|
||||
else {
|
||||
$this->messenger->addMessage($this->t('The layout has been saved.'));
|
||||
}
|
||||
|
||||
return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the layout.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* A redirect response.
|
||||
*/
|
||||
public function cancelLayout(SectionStorageInterface $section_storage) {
|
||||
$this->layoutTempstoreRepository->delete($section_storage);
|
||||
|
||||
$this->messenger->addMessage($this->t('The changes to the layout have been discarded.'));
|
||||
|
||||
return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\CloseDialogCommand;
|
||||
use Drupal\Core\Ajax\ReplaceCommand;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides AJAX responses to rebuild the Layout Builder.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
trait LayoutRebuildTrait {
|
||||
|
||||
/**
|
||||
* The class resolver.
|
||||
*
|
||||
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
|
||||
*/
|
||||
protected $classResolver;
|
||||
|
||||
/**
|
||||
* Rebuilds the layout.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* An AJAX response to either rebuild the layout and close the dialog, or
|
||||
* reload the page.
|
||||
*/
|
||||
protected function rebuildAndClose(SectionStorageInterface $section_storage) {
|
||||
$response = $this->rebuildLayout($section_storage);
|
||||
$response->addCommand(new CloseDialogCommand('#drupal-off-canvas'));
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the layout.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* An AJAX response to either rebuild the layout and close the dialog, or
|
||||
* reload the page.
|
||||
*/
|
||||
protected function rebuildLayout(SectionStorageInterface $section_storage) {
|
||||
$response = new AjaxResponse();
|
||||
$layout_controller = $this->classResolver->getInstanceFromDefinition(LayoutBuilderController::class);
|
||||
$layout = $layout_controller->layout($section_storage, TRUE);
|
||||
$response->addCommand(new ReplaceCommand('#layout-builder', $layout));
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Controller;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a controller to move a block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MoveBlockController implements ContainerInjectionInterface {
|
||||
|
||||
use LayoutRebuildTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* LayoutController constructor.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->classResolver = $class_resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('class_resolver')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a block to another region.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param int $delta_from
|
||||
* The delta of the original section.
|
||||
* @param int $delta_to
|
||||
* The delta of the destination section.
|
||||
* @param string $region_to
|
||||
* The new region for this block.
|
||||
* @param string $block_uuid
|
||||
* The UUID for this block.
|
||||
* @param string|null $preceding_block_uuid
|
||||
* (optional) If provided, the UUID of the block to insert this block after.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* An AJAX response.
|
||||
*/
|
||||
public function build(SectionStorageInterface $section_storage, $delta_from, $delta_to, $region_to, $block_uuid, $preceding_block_uuid = NULL) {
|
||||
$section = $section_storage->getSection($delta_from);
|
||||
|
||||
$component = $section->getComponent($block_uuid);
|
||||
$section->removeComponent($block_uuid);
|
||||
|
||||
// If the block is moving from one section to another, update the original
|
||||
// section and load the new one.
|
||||
if ($delta_from !== $delta_to) {
|
||||
$section = $section_storage->getSection($delta_to);
|
||||
}
|
||||
|
||||
// If a preceding block was specified, insert after that. Otherwise add the
|
||||
// block to the front.
|
||||
$component->setRegion($region_to);
|
||||
if (isset($preceding_block_uuid)) {
|
||||
$section->insertAfterComponent($preceding_block_uuid, $component);
|
||||
}
|
||||
else {
|
||||
$section->insertComponent(0, $component);
|
||||
}
|
||||
|
||||
$this->layoutTempstoreRepository->set($section_storage);
|
||||
return $this->rebuildLayout($section_storage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Defines an interface for an object that stores layout sections for defaults.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface DefaultsSectionStorageInterface extends SectionStorageInterface {
|
||||
|
||||
/**
|
||||
* Determines if the defaults allow custom overrides.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if custom overrides are allowed, FALSE otherwise.
|
||||
*/
|
||||
public function isOverridable();
|
||||
|
||||
/**
|
||||
* Sets the defaults to allow or disallow overrides.
|
||||
*
|
||||
* @param bool $overridable
|
||||
* TRUE if the display should allow overrides, FALSE otherwise.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setOverridable($overridable = TRUE);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Entity;
|
||||
|
||||
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
|
||||
use Drupal\Component\Plugin\DependentPluginInterface;
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Plugin\Context\Context;
|
||||
use Drupal\Core\Plugin\Context\ContextDefinition;
|
||||
use Drupal\Core\Plugin\Definition\DependentPluginDefinitionInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
|
||||
|
||||
/**
|
||||
* Provides an entity view display entity that has a layout.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
|
||||
|
||||
use SectionStorageTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isOverridable() {
|
||||
return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOverridable($overridable = TRUE) {
|
||||
$this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSections() {
|
||||
return $this->getThirdPartySetting('layout_builder', 'sections', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setSections(array $sections) {
|
||||
$this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function preSave(EntityStorageInterface $storage) {
|
||||
parent::preSave($storage);
|
||||
|
||||
$original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
|
||||
$new_value = $this->isOverridable();
|
||||
if ($original_value !== $new_value) {
|
||||
$entity_type_id = $this->getTargetEntityTypeId();
|
||||
$bundle = $this->getTargetBundle();
|
||||
|
||||
if ($new_value) {
|
||||
$this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
|
||||
}
|
||||
elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) {
|
||||
$field->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a layout section field to a given bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param string $bundle
|
||||
* The bundle.
|
||||
* @param string $field_name
|
||||
* The name for the layout section field.
|
||||
*/
|
||||
protected function addSectionField($entity_type_id, $bundle, $field_name) {
|
||||
$field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
|
||||
if (!$field) {
|
||||
$field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
|
||||
if (!$field_storage) {
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'entity_type' => $entity_type_id,
|
||||
'field_name' => $field_name,
|
||||
'type' => 'layout_section',
|
||||
'locked' => TRUE,
|
||||
]);
|
||||
$field_storage->save();
|
||||
}
|
||||
|
||||
$field = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => $bundle,
|
||||
'label' => t('Layout'),
|
||||
]);
|
||||
$field->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getDefaultRegion() {
|
||||
if ($this->hasSection(0)) {
|
||||
return $this->getSection(0)->getDefaultRegion();
|
||||
}
|
||||
|
||||
return parent::getDefaultRegion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the context repository service.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
|
||||
* The context repository service.
|
||||
*/
|
||||
protected function contextRepository() {
|
||||
return \Drupal::service('context.repository');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildMultiple(array $entities) {
|
||||
$build_list = parent::buildMultiple($entities);
|
||||
|
||||
foreach ($entities as $id => $entity) {
|
||||
$sections = $this->getRuntimeSections($entity);
|
||||
if ($sections) {
|
||||
foreach ($build_list[$id] as $name => $build_part) {
|
||||
$field_definition = $this->getFieldDefinition($name);
|
||||
if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
|
||||
unset($build_list[$id][$name]);
|
||||
}
|
||||
}
|
||||
|
||||
// Bypass ::getContexts() in order to use the runtime entity, not a
|
||||
// sample entity.
|
||||
$contexts = $this->contextRepository()->getAvailableContexts();
|
||||
// @todo Use EntityContextDefinition after resolving
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $build_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the runtime sections for a given entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Section[]
|
||||
* The sections.
|
||||
*/
|
||||
protected function getRuntimeSections(FieldableEntityInterface $entity) {
|
||||
if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
|
||||
return $entity->get('layout_builder__layout')->getSections();
|
||||
}
|
||||
|
||||
return $this->getSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Move this upstream in https://www.drupal.org/node/2939931.
|
||||
*/
|
||||
public function label() {
|
||||
$bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
|
||||
$bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
|
||||
$target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
|
||||
return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
parent::calculateDependencies();
|
||||
|
||||
foreach ($this->getSections() as $delta => $section) {
|
||||
$this->calculatePluginDependencies($section->getLayout());
|
||||
foreach ($section->getComponents() as $uuid => $component) {
|
||||
$this->calculatePluginDependencies($component->getPlugin());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onDependencyRemoval(array $dependencies) {
|
||||
$changed = parent::onDependencyRemoval($dependencies);
|
||||
|
||||
// Loop through all sections and determine if the removed dependencies are
|
||||
// used by their layout plugins.
|
||||
foreach ($this->getSections() as $delta => $section) {
|
||||
$layout_dependencies = $this->getPluginDependencies($section->getLayout());
|
||||
$layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
|
||||
if ($layout_removed_dependencies) {
|
||||
// @todo Allow the plugins to react to their dependency removal in
|
||||
// https://www.drupal.org/project/drupal/issues/2579743.
|
||||
$this->removeSection($delta);
|
||||
$changed = TRUE;
|
||||
}
|
||||
// If the section is not removed, loop through all components.
|
||||
else {
|
||||
foreach ($section->getComponents() as $uuid => $component) {
|
||||
$plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
|
||||
$component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
|
||||
if ($component_removed_dependencies) {
|
||||
// @todo Allow the plugins to react to their dependency removal in
|
||||
// https://www.drupal.org/project/drupal/issues/2579743.
|
||||
$section->removeComponent($uuid);
|
||||
$changed = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and returns dependencies of a specific plugin instance.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginInspectionInterface $instance
|
||||
* The plugin instance.
|
||||
*
|
||||
* @return array
|
||||
* An array of dependencies keyed by the type of dependency.
|
||||
*
|
||||
* @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925.
|
||||
*/
|
||||
protected function getPluginDependencies(PluginInspectionInterface $instance) {
|
||||
$dependencies = [];
|
||||
$definition = $instance->getPluginDefinition();
|
||||
if ($definition instanceof PluginDefinitionInterface) {
|
||||
$dependencies['module'][] = $definition->getProvider();
|
||||
if ($definition instanceof DependentPluginDefinitionInterface && $config_dependencies = $definition->getConfigDependencies()) {
|
||||
$dependencies = NestedArray::mergeDeep($dependencies, $config_dependencies);
|
||||
}
|
||||
}
|
||||
elseif (is_array($definition)) {
|
||||
$dependencies['module'][] = $definition['provider'];
|
||||
// Plugins can declare additional dependencies in their definition.
|
||||
if (isset($definition['config_dependencies'])) {
|
||||
$dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']);
|
||||
}
|
||||
}
|
||||
|
||||
// If a plugin is dependent, calculate its dependencies.
|
||||
if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
|
||||
$dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies);
|
||||
}
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setComponent($name, array $options = []) {
|
||||
parent::setComponent($name, $options);
|
||||
|
||||
// @todo Remove workaround for EntityViewBuilder::getSingleFieldDisplay() in
|
||||
// https://www.drupal.org/project/drupal/issues/2936464.
|
||||
if ($this->getMode() === static::CUSTOM_MODE) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Retrieve the updated options after the parent:: call.
|
||||
$options = $this->content[$name];
|
||||
// Provide backwards compatibility by converting to a section component.
|
||||
$field_definition = $this->getFieldDefinition($name);
|
||||
if ($field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type'])) {
|
||||
$configuration = [];
|
||||
$configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
|
||||
$configuration['label_display'] = FALSE;
|
||||
$keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
|
||||
$configuration['formatter'] = array_intersect_key($options, $keys);
|
||||
$configuration['context_mapping']['entity'] = 'layout_builder.entity';
|
||||
|
||||
$section = $this->getDefaultSection();
|
||||
$region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
|
||||
$new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
|
||||
$section->appendComponent($new_component);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a default section.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Section
|
||||
* The default section.
|
||||
*/
|
||||
protected function getDefaultSection() {
|
||||
// If no section exists, append a new one.
|
||||
if (!$this->hasSection(0)) {
|
||||
$this->appendSection(new Section('layout_onecol'));
|
||||
}
|
||||
|
||||
// Return the first section.
|
||||
return $this->getSection(0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityStorage;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
|
||||
/**
|
||||
* Provides storage for entity view display entities that have layouts.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class LayoutBuilderEntityViewDisplayStorage extends ConfigEntityStorage {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function mapToStorageRecord(EntityInterface $entity) {
|
||||
$record = parent::mapToStorageRecord($entity);
|
||||
|
||||
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
|
||||
$record['third_party_settings']['layout_builder']['sections'] = array_map(function (Section $section) {
|
||||
return $section->toArray();
|
||||
}, $record['third_party_settings']['layout_builder']['sections']);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function mapFromStorageRecords(array $records) {
|
||||
foreach ($records as $id => &$record) {
|
||||
if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
|
||||
$sections = &$record['third_party_settings']['layout_builder']['sections'];
|
||||
foreach ($sections as $section_delta => $section) {
|
||||
$sections[$section_delta] = new Section(
|
||||
$section['layout_id'],
|
||||
$section['layout_settings'],
|
||||
array_map(function (array $component) {
|
||||
return (new SectionComponent(
|
||||
$component['uuid'],
|
||||
$component['region'],
|
||||
$component['configuration'],
|
||||
$component['additional']
|
||||
))->setWeight($component['weight']);
|
||||
}, $section['components'])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return parent::mapFromStorageRecords($records);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Entity;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\TempStore\SharedTempStoreFactory;
|
||||
|
||||
/**
|
||||
* Generates a sample entity for use by the Layout Builder.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class LayoutBuilderSampleEntityGenerator {
|
||||
|
||||
/**
|
||||
* The shared tempstore factory.
|
||||
*
|
||||
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
|
||||
*/
|
||||
protected $tempStoreFactory;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* LayoutBuilderSampleEntityGenerator constructor.
|
||||
*
|
||||
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
|
||||
* The tempstore factory.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->tempStoreFactory = $temp_store_factory;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a sample entity for a given entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* An entity.
|
||||
*/
|
||||
public function get($entity_type_id, $bundle_id) {
|
||||
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
|
||||
if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
if (!$entity_storage instanceof ContentEntityStorageInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id));
|
||||
}
|
||||
|
||||
$entity = $entity_storage->createWithSampleValues($bundle_id);
|
||||
// Mark the sample entity as being a preview.
|
||||
$entity->in_preview = TRUE;
|
||||
$tempstore->set("$entity_type_id.$bundle_id", $entity);
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a sample entity for a given entity type and bundle.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param string $bundle_id
|
||||
* The bundle ID.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function delete($entity_type_id, $bundle_id) {
|
||||
$tempstore = $this->tempStoreFactory->get('layout_builder.sample_entity');
|
||||
$tempstore->delete("$entity_type_id.$bundle_id");
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Entity;
|
||||
|
||||
use Drupal\Core\Entity\Display\EntityDisplayInterface;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for entity displays that have layout.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface {
|
||||
|
||||
/**
|
||||
* Determines if the display allows custom overrides.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if custom overrides are allowed, FALSE otherwise.
|
||||
*/
|
||||
public function isOverridable();
|
||||
|
||||
/**
|
||||
* Sets the display to allow or disallow overrides.
|
||||
*
|
||||
* @param bool $overridable
|
||||
* TRUE if the display should allow overrides, FALSE otherwise.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setOverridable($overridable = TRUE);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Event;
|
||||
|
||||
use Drupal\Core\Cache\CacheableResponseTrait;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Symfony\Component\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Event fired when a section component's render array is being built.
|
||||
*
|
||||
* Subscribers to this event should manipulate the cacheability object and the
|
||||
* build array in this event.
|
||||
*
|
||||
* @see \Drupal\layout_builder\LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class SectionComponentBuildRenderArrayEvent extends Event {
|
||||
|
||||
use CacheableResponseTrait;
|
||||
|
||||
/**
|
||||
* The section component whose render array is being built.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionComponent
|
||||
*/
|
||||
protected $component;
|
||||
|
||||
/**
|
||||
* The available contexts.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\Context\ContextInterface[]
|
||||
*/
|
||||
protected $contexts;
|
||||
|
||||
/**
|
||||
* The plugin for the section component being built.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginInspectionInterface
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* Whether the component is in preview mode or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $inPreview;
|
||||
|
||||
/**
|
||||
* The render array built by the event subscribers.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $build = [];
|
||||
|
||||
/**
|
||||
* Creates a new SectionComponentBuildRenderArrayEvent object.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionComponent $component
|
||||
* The section component whose render array is being built.
|
||||
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
|
||||
* The available contexts.
|
||||
* @param bool $in_preview
|
||||
* (optional) Whether the component is in preview mode or not.
|
||||
*/
|
||||
public function __construct(SectionComponent $component, array $contexts, $in_preview = FALSE) {
|
||||
$this->component = $component;
|
||||
$this->contexts = $contexts;
|
||||
$this->plugin = $component->getPlugin($contexts);
|
||||
$this->inPreview = $in_preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the section component whose render array is being built.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionComponent
|
||||
* The section component whose render array is being built.
|
||||
*/
|
||||
public function getComponent() {
|
||||
return $this->component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available contexts.
|
||||
*
|
||||
* @return array|\Drupal\Core\Plugin\Context\ContextInterface[]
|
||||
* The available contexts.
|
||||
*/
|
||||
public function getContexts() {
|
||||
return $this->contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin for the section component being built.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\PluginInspectionInterface
|
||||
* The plugin for the section component being built.
|
||||
*/
|
||||
public function getPlugin() {
|
||||
return $this->plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the component is in preview mode.
|
||||
*
|
||||
* @return bool
|
||||
* Whether the component is in preview mode or not.
|
||||
*/
|
||||
public function inPreview() {
|
||||
return $this->inPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the render array in its current state.
|
||||
*
|
||||
* @return array
|
||||
* The render array built by the event subscribers.
|
||||
*/
|
||||
public function getBuild() {
|
||||
return $this->build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the render array.
|
||||
*
|
||||
* @param array $build
|
||||
* A render array.
|
||||
*/
|
||||
public function setBuild(array $build) {
|
||||
$this->build = $build;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
|
||||
use Drupal\layout_builder\LayoutBuilderEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Builds render arrays and handles access for all block components.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class BlockComponentRenderArray implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* Creates a BlockComponentRenderArray object.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
*/
|
||||
public function __construct(AccountInterface $current_user) {
|
||||
$this->currentUser = $current_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY] = ['onBuildRender', 100];
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds render arrays for block plugins and sets it on the event.
|
||||
*
|
||||
* @param \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event
|
||||
* The section component render event.
|
||||
*/
|
||||
public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
|
||||
$block = $event->getPlugin();
|
||||
if (!$block instanceof BlockPluginInterface) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only check access if the component is not being previewed.
|
||||
if ($event->inPreview()) {
|
||||
$access = AccessResult::allowed()->setCacheMaxAge(0);
|
||||
}
|
||||
else {
|
||||
$access = $block->access($this->currentUser, TRUE);
|
||||
}
|
||||
|
||||
$event->addCacheableDependency($access);
|
||||
if ($access->isAllowed()) {
|
||||
$event->addCacheableDependency($block);
|
||||
|
||||
$build = [
|
||||
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
|
||||
'#theme' => 'block',
|
||||
'#configuration' => $block->getConfiguration(),
|
||||
'#plugin_id' => $block->getPluginId(),
|
||||
'#base_plugin_id' => $block->getBaseId(),
|
||||
'#derivative_plugin_id' => $block->getDerivativeId(),
|
||||
'#weight' => $event->getComponent()->getWeight(),
|
||||
'content' => $block->build(),
|
||||
];
|
||||
$event->setBuild($build);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Field;
|
||||
|
||||
use Drupal\Core\Field\FieldItemList;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
|
||||
|
||||
/**
|
||||
* Defines a item list class for layout section fields.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
|
||||
*/
|
||||
class LayoutSectionItemList extends FieldItemList implements SectionListInterface {
|
||||
|
||||
use SectionStorageTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSections() {
|
||||
$sections = [];
|
||||
/** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */
|
||||
foreach ($this->list as $delta => $item) {
|
||||
$sections[$delta] = $item->section;
|
||||
}
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setSections(array $sections) {
|
||||
$this->list = [];
|
||||
$sections = array_values($sections);
|
||||
/** @var \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem $item */
|
||||
foreach ($sections as $section) {
|
||||
$item = $this->appendItem();
|
||||
$item->section = $section;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __wakeup() {
|
||||
// Ensure the entity is updated with the latest value.
|
||||
$this->getEntity()->set($this->getName(), $this->getValue());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a form to add a block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AddBlockForm extends ConfigureBlockFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_add_block';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function submitLabel() {
|
||||
return $this->t('Add Block');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the form for the block.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage being configured.
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
* @param string $region
|
||||
* The region of the block.
|
||||
* @param string|null $plugin_id
|
||||
* The plugin ID of the block to add.
|
||||
*
|
||||
* @return array
|
||||
* The form array.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL) {
|
||||
// Only generate a new component once per form submission.
|
||||
if (!$component = $form_state->getTemporaryValue('layout_builder__component')) {
|
||||
$component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]);
|
||||
$section_storage->getSection($delta)->appendComponent($component);
|
||||
$form_state->setTemporaryValue('layout_builder__component', $component);
|
||||
}
|
||||
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\ReplaceCommand;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\Controller\AjaxHelperTrait;
|
||||
|
||||
/**
|
||||
* Provides a helper to for submitting an AJAX form.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @todo Move to \Drupal\Core in https://www.drupal.org/node/2896535.
|
||||
*/
|
||||
trait AjaxFormHelperTrait {
|
||||
|
||||
use AjaxHelperTrait;
|
||||
|
||||
/**
|
||||
* Submit form dialog #ajax callback.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* An AJAX response that display validation error messages or represents a
|
||||
* successful submission.
|
||||
*/
|
||||
public function ajaxSubmit(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->hasAnyErrors()) {
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -1000,
|
||||
];
|
||||
$response = new AjaxResponse();
|
||||
$response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form));
|
||||
}
|
||||
else {
|
||||
$response = $this->successfulAjaxSubmit($form, $form_state);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the form to respond to a successful AJAX submission.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* An AJAX response.
|
||||
*/
|
||||
abstract protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Component\Uuid\UuidInterface;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Form\SubformState;
|
||||
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginInterface;
|
||||
use Drupal\Core\Plugin\PluginFormFactoryInterface;
|
||||
use Drupal\Core\Plugin\PluginWithFormsInterface;
|
||||
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
|
||||
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a base form for configuring a block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class ConfigureBlockFormBase extends FormBase {
|
||||
|
||||
use AjaxFormHelperTrait;
|
||||
use ContextAwarePluginAssignmentTrait;
|
||||
use LayoutBuilderContextTrait;
|
||||
use LayoutRebuildTrait;
|
||||
|
||||
/**
|
||||
* The plugin being configured.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockPluginInterface
|
||||
*/
|
||||
protected $block;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The block manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* The UUID generator.
|
||||
*
|
||||
* @var \Drupal\Component\Uuid\UuidInterface
|
||||
*/
|
||||
protected $uuidGenerator;
|
||||
|
||||
/**
|
||||
* The plugin form manager.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
|
||||
*/
|
||||
protected $pluginFormFactory;
|
||||
|
||||
/**
|
||||
* The field delta.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $delta;
|
||||
|
||||
/**
|
||||
* The current region.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $region;
|
||||
|
||||
/**
|
||||
* The UUID of the component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uuid;
|
||||
|
||||
/**
|
||||
* The section storage.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* Constructs a new block form.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\Plugin\Context\ContextRepositoryInterface $context_repository
|
||||
* The context repository.
|
||||
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
|
||||
* The block manager.
|
||||
* @param \Drupal\Component\Uuid\UuidInterface $uuid
|
||||
* The UUID generator.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
|
||||
* The plugin form manager.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ContextRepositoryInterface $context_repository, BlockManagerInterface $block_manager, UuidInterface $uuid, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->contextRepository = $context_repository;
|
||||
$this->blockManager = $block_manager;
|
||||
$this->uuidGenerator = $uuid;
|
||||
$this->classResolver = $class_resolver;
|
||||
$this->pluginFormFactory = $plugin_form_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('context.repository'),
|
||||
$container->get('plugin.manager.block'),
|
||||
$container->get('uuid'),
|
||||
$container->get('class_resolver'),
|
||||
$container->get('plugin_form.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the form for the block.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage being configured.
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
* @param \Drupal\layout_builder\SectionComponent $component
|
||||
* The section component containing the block.
|
||||
*
|
||||
* @return array
|
||||
* The form array.
|
||||
*/
|
||||
public function doBuildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, SectionComponent $component = NULL) {
|
||||
$this->sectionStorage = $section_storage;
|
||||
$this->delta = $delta;
|
||||
$this->uuid = $component->getUuid();
|
||||
$this->block = $component->getPlugin();
|
||||
|
||||
$form_state->setTemporaryValue('gathered_contexts', $this->getAvailableContexts($section_storage));
|
||||
|
||||
// @todo Remove once https://www.drupal.org/node/2268787 is resolved.
|
||||
$form_state->set('block_theme', $this->config('system.theme')->get('default'));
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['settings'] = [];
|
||||
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
|
||||
$form['settings'] = $this->getPluginForm($this->block)->buildConfigurationForm($form['settings'], $subform_state);
|
||||
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->submitLabel(),
|
||||
'#button_type' => 'primary',
|
||||
];
|
||||
if ($this->isAjax()) {
|
||||
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label for the submit button.
|
||||
*
|
||||
* @return string
|
||||
* Submit label.
|
||||
*/
|
||||
abstract protected function submitLabel();
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
$subform_state = SubformState::createForSubform($form['settings'], $form, $form_state);
|
||||
$this->getPluginForm($this->block)->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->block)->submitConfigurationForm($form, $subform_state);
|
||||
|
||||
// If this block is context-aware, set the context mapping.
|
||||
if ($this->block instanceof ContextAwarePluginInterface) {
|
||||
$this->block->setContextMapping($subform_state->getValue('context_mapping', []));
|
||||
}
|
||||
|
||||
$configuration = $this->block->getConfiguration();
|
||||
|
||||
$section = $this->sectionStorage->getSection($this->delta);
|
||||
$section->getComponent($this->uuid)->setConfiguration($configuration);
|
||||
|
||||
$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 block.
|
||||
*
|
||||
* @param \Drupal\Core\Block\BlockPluginInterface $block
|
||||
* The block plugin.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\PluginFormInterface
|
||||
* The plugin form for the block.
|
||||
*/
|
||||
protected function getPluginForm(BlockPluginInterface $block) {
|
||||
if ($block instanceof PluginWithFormsInterface) {
|
||||
return $this->pluginFormFactory->createInstance($block, 'configure');
|
||||
}
|
||||
return $block;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Form\SubformState;
|
||||
use Drupal\Core\Layout\LayoutInterface;
|
||||
use Drupal\Core\Plugin\PluginFormFactoryInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
use Drupal\Core\Plugin\PluginWithFormsInterface;
|
||||
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a form for configuring a layout section.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ConfigureSectionForm extends FormBase {
|
||||
|
||||
use AjaxFormHelperTrait;
|
||||
use LayoutRebuildTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The plugin being configured.
|
||||
*
|
||||
* @var \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface
|
||||
*/
|
||||
protected $layout;
|
||||
|
||||
/**
|
||||
* The plugin form manager.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
|
||||
*/
|
||||
protected $pluginFormFactory;
|
||||
|
||||
/**
|
||||
* The section storage.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* The field delta.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $delta;
|
||||
|
||||
/**
|
||||
* Indicates whether the section is being added or updated.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $isUpdate;
|
||||
|
||||
/**
|
||||
* Constructs a new ConfigureSectionForm.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager
|
||||
* The plugin form manager.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver, PluginFormFactoryInterface $plugin_form_manager) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->classResolver = $class_resolver;
|
||||
$this->pluginFormFactory = $plugin_form_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('class_resolver'),
|
||||
$container->get('plugin_form.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_configure_section';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $plugin_id = NULL) {
|
||||
$this->sectionStorage = $section_storage;
|
||||
$this->delta = $delta;
|
||||
$this->isUpdate = is_null($plugin_id);
|
||||
|
||||
if ($this->isUpdate) {
|
||||
$section = $this->sectionStorage->getSection($this->delta);
|
||||
}
|
||||
else {
|
||||
$section = new Section($plugin_id);
|
||||
}
|
||||
$this->layout = $section->getLayout();
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['layout_settings'] = [];
|
||||
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
|
||||
$form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state);
|
||||
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'),
|
||||
'#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['layout_settings'], $form, $form_state);
|
||||
$this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// Call the plugin submit handler.
|
||||
$subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state);
|
||||
$this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state);
|
||||
|
||||
$plugin_id = $this->layout->getPluginId();
|
||||
$configuration = $this->layout->getConfiguration();
|
||||
|
||||
if ($this->isUpdate) {
|
||||
$this->sectionStorage->getSection($this->delta)->setLayoutSettings($configuration);
|
||||
}
|
||||
else {
|
||||
$this->sectionStorage->insertSection($this->delta, new Section($plugin_id, $configuration));
|
||||
}
|
||||
|
||||
$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 layout.
|
||||
*
|
||||
* @param \Drupal\Core\Layout\LayoutInterface $layout
|
||||
* The layout plugin.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\PluginFormInterface
|
||||
* The plugin form for the layout.
|
||||
*/
|
||||
protected function getPluginForm(LayoutInterface $layout) {
|
||||
if ($layout instanceof PluginWithFormsInterface) {
|
||||
return $this->pluginFormFactory->createInstance($layout, 'configure');
|
||||
}
|
||||
|
||||
if ($layout instanceof PluginFormInterface) {
|
||||
return $layout;
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
|
||||
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Edit form for the LayoutBuilderEntityViewDisplay entity type.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
|
||||
|
||||
/**
|
||||
* The entity being used by this form.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The storage section.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
|
||||
$this->sectionStorage = $section_storage;
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
// Hide the table of fields.
|
||||
$form['fields']['#access'] = FALSE;
|
||||
$form['#fields'] = [];
|
||||
$form['#extra'] = [];
|
||||
|
||||
$form['manage_layout'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Manage layout'),
|
||||
'#weight' => -10,
|
||||
'#attributes' => ['class' => ['button']],
|
||||
'#url' => $this->sectionStorage->getLayoutBuilderUrl(),
|
||||
];
|
||||
|
||||
// @todo Expand to work for all view modes in
|
||||
// https://www.drupal.org/node/2907413.
|
||||
if ($this->entity->getMode() === 'default') {
|
||||
$form['layout'] = [
|
||||
'#type' => 'details',
|
||||
'#open' => TRUE,
|
||||
'#title' => $this->t('Layout options'),
|
||||
'#tree' => TRUE,
|
||||
];
|
||||
|
||||
$entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
|
||||
$form['layout']['allow_custom'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Allow each @entity to have its layout customized.', [
|
||||
'@entity' => $entity_type->getSingularLabel(),
|
||||
]),
|
||||
'#default_value' => $this->entity->isOverridable(),
|
||||
];
|
||||
// Prevent turning off overrides while any exist.
|
||||
if ($this->hasOverrides($this->entity)) {
|
||||
$form['layout']['allow_custom']['#disabled'] = TRUE;
|
||||
$form['layout']['allow_custom']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
|
||||
}
|
||||
else {
|
||||
$form['#entity_builders'][] = '::entityFormEntityBuild';
|
||||
}
|
||||
}
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the defaults have any overrides.
|
||||
*
|
||||
* @param \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display
|
||||
* The entity display.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there are any overrides of this default, FALSE otherwise.
|
||||
*/
|
||||
protected function hasOverrides(LayoutEntityDisplayInterface $display) {
|
||||
if (!$display->isOverridable()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
|
||||
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
|
||||
->exists('layout_builder__layout');
|
||||
if ($bundle_key = $entity_type->getKey('bundle')) {
|
||||
$query->condition($bundle_key, $display->getTargetBundle());
|
||||
}
|
||||
return (bool) $query->count()->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity builder for layout options on the entity view display form.
|
||||
*/
|
||||
public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) {
|
||||
$new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
|
||||
$display->setOverridable($new_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
|
||||
// Intentionally empty.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildExtraFieldRow($field_id, $extra_field) {
|
||||
// Intentionally empty.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Form\ConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a base class for confirmation forms that rebuild the Layout Builder.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
|
||||
|
||||
use AjaxFormHelperTrait;
|
||||
use LayoutRebuildTrait;
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The section storage.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* The field delta.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $delta;
|
||||
|
||||
/**
|
||||
* Constructs a new RemoveSectionForm.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, ClassResolverInterface $class_resolver) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->classResolver = $class_resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('class_resolver')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return $this->sectionStorage->getLayoutBuilderUrl()->setOption('query', ['layout_is_rebuilding' => TRUE]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL) {
|
||||
$this->sectionStorage = $section_storage;
|
||||
$this->delta = $delta;
|
||||
|
||||
$form = parent::buildForm($form, $form_state);
|
||||
|
||||
if ($this->isAjax()) {
|
||||
$form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit';
|
||||
$form['actions']['cancel']['#attributes']['class'][] = 'dialog-cancel';
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->handleSectionStorage($this->sectionStorage, $form_state);
|
||||
|
||||
$this->layoutTempstoreRepository->set($this->sectionStorage);
|
||||
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) {
|
||||
return $this->rebuildAndClose($this->sectionStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs any actions on the section storage before saving.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
abstract protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a form to confirm the removal of a block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RemoveBlockForm extends LayoutRebuildConfirmFormBase {
|
||||
|
||||
/**
|
||||
* The current region.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $region;
|
||||
|
||||
/**
|
||||
* The UUID of the block being removed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uuid;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to remove this block?');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Remove');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_remove_block';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
|
||||
$this->region = $region;
|
||||
$this->uuid = $uuid;
|
||||
return parent::buildForm($form, $form_state, $section_storage, $delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
|
||||
$section_storage->getSection($this->delta)->removeComponent($this->uuid);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a form to confirm the removal of a section.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RemoveSectionForm extends LayoutRebuildConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_remove_section';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to remove this section?');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Remove');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function handleSectionStorage(SectionStorageInterface $section_storage, FormStateInterface $form_state) {
|
||||
$section_storage->removeSection($this->delta);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Form\ConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\OverridesSectionStorageInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Reverts the overridden layout to the defaults.
|
||||
*/
|
||||
class RevertOverridesForm extends ConfirmFormBase {
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The section storage.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* Constructs a new RevertOverridesForm.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('layout_builder.tempstore_repository'),
|
||||
$container->get('messenger')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_revert_overrides';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Are you sure you want to revert this to defaults?');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfirmText() {
|
||||
return $this->t('Revert');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return $this->sectionStorage->getLayoutBuilderUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
|
||||
if (!$section_storage instanceof OverridesSectionStorageInterface) {
|
||||
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide overrides', $section_storage->getStorageType(), $section_storage->getStorageId()));
|
||||
}
|
||||
|
||||
$this->sectionStorage = $section_storage;
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
// Remove all sections.
|
||||
while ($this->sectionStorage->count()) {
|
||||
$this->sectionStorage->removeSection(0);
|
||||
}
|
||||
$this->sectionStorage->save();
|
||||
$this->layoutTempstoreRepository->delete($this->sectionStorage);
|
||||
|
||||
$this->messenger->addMessage($this->t('The layout has been reverted back to defaults.'));
|
||||
$form_state->setRedirectUrl($this->getCancelUrl());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Form;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a form to update a block.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class UpdateBlockForm extends ConfigureBlockFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'layout_builder_update_block';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the block form.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage being configured.
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
* @param string $region
|
||||
* The region of the block.
|
||||
* @param string $uuid
|
||||
* The UUID of the block being updated.
|
||||
*
|
||||
* @return array
|
||||
* The form array.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
|
||||
$component = $section_storage->getSection($delta)->getComponent($uuid);
|
||||
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function submitLabel() {
|
||||
return $this->t('Update');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Defines events for the layout_builder module.
|
||||
*
|
||||
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
final class LayoutBuilderEvents {
|
||||
|
||||
/**
|
||||
* Name of the event fired when a component's render array is built.
|
||||
*
|
||||
* This event allows modules to collaborate on creating the render array of
|
||||
* the SectionComponent object. The event listener method receives a
|
||||
* \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
|
||||
* instance.
|
||||
*
|
||||
* @Event
|
||||
*
|
||||
* @see \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent
|
||||
* @see \Drupal\layout_builder\SectionComponent::toRenderArray()
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
use Drupal\Core\TempStore\SharedTempStoreFactory;
|
||||
|
||||
/**
|
||||
* Provides a mechanism for loading layouts from tempstore.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
|
||||
|
||||
/**
|
||||
* The shared tempstore factory.
|
||||
*
|
||||
* @var \Drupal\Core\TempStore\SharedTempStoreFactory
|
||||
*/
|
||||
protected $tempStoreFactory;
|
||||
|
||||
/**
|
||||
* LayoutTempstoreRepository constructor.
|
||||
*
|
||||
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $temp_store_factory
|
||||
* The shared tempstore factory.
|
||||
*/
|
||||
public function __construct(SharedTempStoreFactory $temp_store_factory) {
|
||||
$this->tempStoreFactory = $temp_store_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(SectionStorageInterface $section_storage) {
|
||||
$id = $section_storage->getStorageId();
|
||||
$tempstore = $this->getTempstore($section_storage)->get($id);
|
||||
if (!empty($tempstore['section_storage'])) {
|
||||
$storage_type = $section_storage->getStorageType();
|
||||
$section_storage = $tempstore['section_storage'];
|
||||
|
||||
if (!($section_storage instanceof SectionStorageInterface)) {
|
||||
throw new \UnexpectedValueException(sprintf('The entry with storage type "%s" and ID "%s" is invalid', $storage_type, $id));
|
||||
}
|
||||
}
|
||||
return $section_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function set(SectionStorageInterface $section_storage) {
|
||||
$id = $section_storage->getStorageId();
|
||||
$this->getTempstore($section_storage)->set($id, ['section_storage' => $section_storage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function delete(SectionStorageInterface $section_storage) {
|
||||
$id = $section_storage->getStorageId();
|
||||
$this->getTempstore($section_storage)->delete($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the shared tempstore.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage.
|
||||
*
|
||||
* @return \Drupal\Core\TempStore\SharedTempStore
|
||||
* The tempstore.
|
||||
*/
|
||||
protected function getTempstore(SectionStorageInterface $section_storage) {
|
||||
$collection = 'layout_builder.section_storage.' . $section_storage->getStorageType();
|
||||
return $this->tempStoreFactory->get($collection);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Provides an interface for loading layouts from tempstore.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface LayoutTempstoreRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Gets the tempstore version of a section storage, if it exists.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage to check for in tempstore.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionStorageInterface
|
||||
* Either the version of this section storage from tempstore, or the passed
|
||||
* section storage if none exists.
|
||||
*
|
||||
* @throw \UnexpectedValueException
|
||||
* Thrown if a value exists, but is not a section storage.
|
||||
*/
|
||||
public function get(SectionStorageInterface $section_storage);
|
||||
|
||||
/**
|
||||
* Stores this section storage in tempstore.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage to set in tempstore.
|
||||
*/
|
||||
public function set(SectionStorageInterface $section_storage);
|
||||
|
||||
/**
|
||||
* Removes the tempstore version of a section storage.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
|
||||
* The section storage to remove from tempstore.
|
||||
*/
|
||||
public function delete(SectionStorageInterface $section_storage);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Defines an interface for an object that stores layout sections for overrides.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface OverridesSectionStorageInterface extends SectionStorageInterface {
|
||||
|
||||
/**
|
||||
* Returns the corresponding defaults section storage for this override.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionStorageInterface
|
||||
* The defaults section storage.
|
||||
*
|
||||
* @todo Determine if this method needs a parameter in
|
||||
* https://www.drupal.org/project/drupal/issues/2936507.
|
||||
*/
|
||||
public function getDefaultSectionStorage();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\Block;
|
||||
|
||||
use Drupal\Component\Plugin\Factory\DefaultFactory;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Entity\EntityDisplayBase;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FormatterInterface;
|
||||
use Drupal\Core\Field\FormatterPluginManager;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a block that renders a field from an entity.
|
||||
*
|
||||
* @Block(
|
||||
* id = "field_block",
|
||||
* deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
|
||||
* )
|
||||
*/
|
||||
class FieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The formatter manager.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FormatterPluginManager
|
||||
*/
|
||||
protected $formatterManager;
|
||||
|
||||
/**
|
||||
* The entity type ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* The bundle ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* The field name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName;
|
||||
|
||||
/**
|
||||
* The field definition.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldDefinitionInterface
|
||||
*/
|
||||
protected $fieldDefinition;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Constructs a new FieldBlock.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* The entity field manager.
|
||||
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
|
||||
* The formatter manager.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, FormatterPluginManager $formatter_manager, ModuleHandlerInterface $module_handler) {
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
$this->formatterManager = $formatter_manager;
|
||||
$this->moduleHandler = $module_handler;
|
||||
|
||||
// Get the entity type and field name from the plugin ID.
|
||||
list (, $entity_type_id, $bundle, $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
|
||||
$this->entityTypeId = $entity_type_id;
|
||||
$this->bundle = $bundle;
|
||||
$this->fieldName = $field_name;
|
||||
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('plugin.manager.field.formatter'),
|
||||
$container->get('module_handler')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity that has the field.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\FieldableEntityInterface
|
||||
* The entity.
|
||||
*/
|
||||
protected function getEntity() {
|
||||
return $this->getContextValue('entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build() {
|
||||
$display_settings = $this->getConfiguration()['formatter'];
|
||||
$entity = $this->getEntity();
|
||||
$build = $entity->get($this->fieldName)->view($display_settings);
|
||||
if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
|
||||
$build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
|
||||
}
|
||||
CacheableMetadata::createFromObject($this)->applyTo($build);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function blockAccess(AccountInterface $account) {
|
||||
$entity = $this->getEntity();
|
||||
|
||||
// First consult the entity.
|
||||
$access = $entity->access('view', $account, TRUE);
|
||||
if (!$access->isAllowed()) {
|
||||
return $access;
|
||||
}
|
||||
|
||||
// Check that the entity in question has this field.
|
||||
if (!$entity instanceof FieldableEntityInterface || !$entity->hasField($this->fieldName)) {
|
||||
return $access->andIf(AccessResult::forbidden());
|
||||
}
|
||||
|
||||
// Check field access.
|
||||
$field = $entity->get($this->fieldName);
|
||||
$access = $access->andIf($field->access('view', $account, TRUE));
|
||||
if (!$access->isAllowed()) {
|
||||
return $access;
|
||||
}
|
||||
|
||||
// Check to see if the field has any values.
|
||||
if ($field->isEmpty()) {
|
||||
return $access->andIf(AccessResult::forbidden());
|
||||
}
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return [
|
||||
'label_display' => FALSE,
|
||||
'formatter' => [
|
||||
'label' => 'above',
|
||||
'type' => $this->pluginDefinition['default_formatter'],
|
||||
'settings' => [],
|
||||
'third_party_settings' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function blockForm($form, FormStateInterface $form_state) {
|
||||
$config = $this->getConfiguration();
|
||||
|
||||
$form['formatter'] = [
|
||||
'#tree' => TRUE,
|
||||
'#process' => [
|
||||
[$this, 'formatterSettingsProcessCallback'],
|
||||
],
|
||||
];
|
||||
$form['formatter']['label'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Label'),
|
||||
// @todo This is directly copied from
|
||||
// \Drupal\field_ui\Form\EntityViewDisplayEditForm::getFieldLabelOptions(),
|
||||
// resolve this in https://www.drupal.org/project/drupal/issues/2933924.
|
||||
'#options' => [
|
||||
'above' => $this->t('Above'),
|
||||
'inline' => $this->t('Inline'),
|
||||
'hidden' => '- ' . $this->t('Hidden') . ' -',
|
||||
'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -',
|
||||
],
|
||||
'#default_value' => $config['formatter']['label'],
|
||||
];
|
||||
|
||||
$form['formatter']['type'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Formatter'),
|
||||
'#options' => $this->getApplicablePluginOptions($this->getFieldDefinition()),
|
||||
'#required' => TRUE,
|
||||
'#default_value' => $config['formatter']['type'],
|
||||
'#ajax' => [
|
||||
'callback' => [static::class, 'formatterSettingsAjaxCallback'],
|
||||
'wrapper' => 'formatter-settings-wrapper',
|
||||
],
|
||||
];
|
||||
|
||||
// Add the formatter settings to the form via AJAX.
|
||||
$form['formatter']['settings_wrapper'] = [
|
||||
'#prefix' => '<div id="formatter-settings-wrapper">',
|
||||
'#suffix' => '</div>',
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render API callback: builds the formatter settings elements.
|
||||
*/
|
||||
public function formatterSettingsProcessCallback(array &$element, FormStateInterface $form_state, array &$complete_form) {
|
||||
if ($formatter = $this->getFormatter($element['#parents'], $form_state)) {
|
||||
$element['settings_wrapper']['settings'] = $formatter->settingsForm($complete_form, $form_state);
|
||||
$element['settings_wrapper']['settings']['#parents'] = array_merge($element['#parents'], ['settings']);
|
||||
$element['settings_wrapper']['third_party_settings'] = $this->thirdPartySettingsForm($formatter, $this->getFieldDefinition(), $complete_form, $form_state);
|
||||
$element['settings_wrapper']['third_party_settings']['#parents'] = array_merge($element['#parents'], ['third_party_settings']);
|
||||
|
||||
// Store the array parents for our element so that we can retrieve the
|
||||
// formatter settings in our AJAX callback.
|
||||
$form_state->set('field_block_array_parents', $element['#array_parents']);
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the formatter third party settings forms.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FormatterInterface $plugin
|
||||
* The formatter.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The field definition.
|
||||
* @param array $form
|
||||
* The (entire) configuration form array.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The form state.
|
||||
*
|
||||
* @return array
|
||||
* The formatter third party settings form.
|
||||
*/
|
||||
protected function thirdPartySettingsForm(FormatterInterface $plugin, FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
|
||||
$settings_form = [];
|
||||
// Invoke hook_field_formatter_third_party_settings_form(), keying resulting
|
||||
// subforms by module name.
|
||||
foreach ($this->moduleHandler->getImplementations('field_formatter_third_party_settings_form') as $module) {
|
||||
$settings_form[$module] = $this->moduleHandler->invoke($module, 'field_formatter_third_party_settings_form', [
|
||||
$plugin,
|
||||
$field_definition,
|
||||
EntityDisplayBase::CUSTOM_MODE,
|
||||
$form,
|
||||
$form_state,
|
||||
]);
|
||||
}
|
||||
return $settings_form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render API callback: gets the layout settings elements.
|
||||
*/
|
||||
public static function formatterSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
|
||||
$formatter_array_parents = $form_state->get('field_block_array_parents');
|
||||
return NestedArray::getValue($form, array_merge($formatter_array_parents, ['settings_wrapper']));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function blockSubmit($form, FormStateInterface $form_state) {
|
||||
$this->configuration['formatter'] = $form_state->getValue('formatter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the field definition.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldDefinitionInterface
|
||||
* The field definition.
|
||||
*/
|
||||
protected function getFieldDefinition() {
|
||||
if (empty($this->fieldDefinition)) {
|
||||
$field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $this->bundle);
|
||||
$this->fieldDefinition = $field_definitions[$this->fieldName];
|
||||
}
|
||||
return $this->fieldDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of applicable formatter options for a field.
|
||||
*
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The field definition.
|
||||
*
|
||||
* @return array
|
||||
* An array of applicable formatter options.
|
||||
*
|
||||
* @see \Drupal\field_ui\Form\EntityDisplayFormBase::getApplicablePluginOptions()
|
||||
*/
|
||||
protected function getApplicablePluginOptions(FieldDefinitionInterface $field_definition) {
|
||||
$options = $this->formatterManager->getOptions($field_definition->getType());
|
||||
$applicable_options = [];
|
||||
foreach ($options as $option => $label) {
|
||||
$plugin_class = DefaultFactory::getPluginClass($option, $this->formatterManager->getDefinition($option));
|
||||
if ($plugin_class::isApplicable($field_definition)) {
|
||||
$applicable_options[$option] = $label;
|
||||
}
|
||||
}
|
||||
return $applicable_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the formatter object.
|
||||
*
|
||||
* @param array $parents
|
||||
* The #parents of the element representing the formatter.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FormatterInterface
|
||||
* The formatter object.
|
||||
*/
|
||||
protected function getFormatter(array $parents, FormStateInterface $form_state) {
|
||||
// Use the processed values, if available.
|
||||
$configuration = NestedArray::getValue($form_state->getValues(), $parents);
|
||||
if (!$configuration) {
|
||||
// Next check the raw user input.
|
||||
$configuration = NestedArray::getValue($form_state->getUserInput(), $parents);
|
||||
if (!$configuration) {
|
||||
// If no user input exists, use the default values.
|
||||
$configuration = $this->getConfiguration()['formatter'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->formatterManager->getInstance([
|
||||
'configuration' => $configuration,
|
||||
'field_definition' => $this->getFieldDefinition(),
|
||||
'view_mode' => EntityDisplayBase::CUSTOM_MODE,
|
||||
'prepare' => TRUE,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\DataType;
|
||||
|
||||
use Drupal\Core\TypedData\TypedData;
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Provides a data type wrapping \Drupal\layout_builder\Section.
|
||||
*
|
||||
* @DataType(
|
||||
* id = "layout_section",
|
||||
* label = @Translation("Layout Section"),
|
||||
* description = @Translation("A layout section"),
|
||||
* )
|
||||
*/
|
||||
class SectionData extends TypedData {
|
||||
|
||||
/**
|
||||
* The section object.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Section
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setValue($value, $notify = TRUE) {
|
||||
if ($value && !$value instanceof Section) {
|
||||
throw new \InvalidArgumentException(sprintf('Value assigned to "%s" is not a valid section', $this->getName()));
|
||||
}
|
||||
parent::setValue($value, $notify);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
|
||||
use Drupal\Core\Field\FieldConfigInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\Field\FormatterPluginManager;
|
||||
use Drupal\Core\Plugin\Context\ContextDefinition;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides entity field block definitions for every field.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeRepositoryInterface
|
||||
*/
|
||||
protected $entityTypeRepository;
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The field type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
|
||||
*/
|
||||
protected $fieldTypeManager;
|
||||
|
||||
/**
|
||||
* The formatter manager.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FormatterPluginManager
|
||||
*/
|
||||
protected $formatterManager;
|
||||
|
||||
/**
|
||||
* Constructs new FieldBlockDeriver.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeRepositoryInterface $entity_type_repository
|
||||
* The entity type repository.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* The entity field manager.
|
||||
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||
* The field type manager.
|
||||
* @param \Drupal\Core\Field\FormatterPluginManager $formatter_manager
|
||||
* The formatter manager.
|
||||
*/
|
||||
public function __construct(EntityTypeRepositoryInterface $entity_type_repository, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, FormatterPluginManager $formatter_manager) {
|
||||
$this->entityTypeRepository = $entity_type_repository;
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
$this->fieldTypeManager = $field_type_manager;
|
||||
$this->formatterManager = $formatter_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity_type.repository'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('plugin.manager.field.field_type'),
|
||||
$container->get('plugin.manager.field.formatter')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$entity_type_labels = $this->entityTypeRepository->getEntityTypeLabels();
|
||||
foreach ($this->entityFieldManager->getFieldMap() as $entity_type_id => $entity_field_map) {
|
||||
foreach ($entity_field_map as $field_name => $field_info) {
|
||||
// Skip fields without any formatters.
|
||||
$options = $this->formatterManager->getOptions($field_info['type']);
|
||||
if (empty($options)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($field_info['bundles'] as $bundle) {
|
||||
$derivative = $base_plugin_definition;
|
||||
$field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
|
||||
|
||||
// Store the default formatter on the definition.
|
||||
$derivative['default_formatter'] = '';
|
||||
$field_type_definition = $this->fieldTypeManager->getDefinition($field_info['type']);
|
||||
if (isset($field_type_definition['default_formatter'])) {
|
||||
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
|
||||
}
|
||||
|
||||
$derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
|
||||
|
||||
$derivative['admin_label'] = $field_definition->getLabel();
|
||||
|
||||
// Add a dependency on the field if it is configurable.
|
||||
if ($field_definition instanceof FieldConfigInterface) {
|
||||
$derivative['config_dependencies'][$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName();
|
||||
}
|
||||
// For any field that is not display configurable, mark it as
|
||||
// unavailable to place in the block UI.
|
||||
$derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view');
|
||||
|
||||
// @todo Use EntityContextDefinition after resolving
|
||||
// https://www.drupal.org/node/2932462.
|
||||
$context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE);
|
||||
$context_definition->addConstraint('Bundle', [$bundle]);
|
||||
$derivative['context'] = [
|
||||
'entity' => $context_definition,
|
||||
];
|
||||
|
||||
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
|
||||
$this->derivatives[$derivative_id] = $derivative;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides local task definitions for the layout builder user interface.
|
||||
*
|
||||
* @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new LayoutBuilderLocalTaskDeriver.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
|
||||
// Overrides.
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 15,
|
||||
'title' => $this->t('Layout'),
|
||||
'base_route' => "entity.$entity_type_id.canonical",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 5,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
// @todo This link should be conditionally displayed, see
|
||||
// https://www.drupal.org/node/2917777.
|
||||
$this->derivatives["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
|
||||
'title' => $this->t('Revert to defaults'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
|
||||
'weight' => 10,
|
||||
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
|
||||
// Defaults.
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.view",
|
||||
'title' => $this->t('Manage layout'),
|
||||
'base_route' => "layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.save",
|
||||
'title' => $this->t('Save Layout'),
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
$this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
|
||||
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
|
||||
'title' => $this->t('Cancel Layout'),
|
||||
'weight' => 5,
|
||||
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
|
||||
];
|
||||
}
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity types relevant for defaults.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypesForDefaults() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of entity types relevant for overrides.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypesForOverrides() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\Field\FieldType;
|
||||
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemBase;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\TypedData\DataDefinition;
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'layout_section' field type.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @FieldType(
|
||||
* id = "layout_section",
|
||||
* label = @Translation("Layout Section"),
|
||||
* description = @Translation("Layout Section"),
|
||||
* list_class = "\Drupal\layout_builder\Field\LayoutSectionItemList",
|
||||
* no_ui = TRUE,
|
||||
* cardinality = \Drupal\Core\Field\FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
|
||||
* )
|
||||
*
|
||||
* @property \Drupal\layout_builder\Section section
|
||||
*/
|
||||
class LayoutSectionItem extends FieldItemBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
|
||||
$properties['section'] = DataDefinition::create('layout_section')
|
||||
->setLabel(new TranslatableMarkup('Layout Section'))
|
||||
->setRequired(FALSE);
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __get($name) {
|
||||
// @todo \Drupal\Core\Field\FieldItemBase::__get() does not return default
|
||||
// values for uninstantiated properties. This will forcibly instantiate
|
||||
// all properties with the side-effect of a performance hit, resolve
|
||||
// properly in https://www.drupal.org/node/2413471.
|
||||
$this->getProperties();
|
||||
|
||||
return parent::__get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function mainPropertyName() {
|
||||
return 'section';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function schema(FieldStorageDefinitionInterface $field_definition) {
|
||||
$schema = [
|
||||
'columns' => [
|
||||
'section' => [
|
||||
'type' => 'blob',
|
||||
'size' => 'normal',
|
||||
'serialize' => TRUE,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
|
||||
// @todo Expand this in https://www.drupal.org/node/2912331.
|
||||
$values['section'] = new Section('layout_onecol');
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEmpty() {
|
||||
return empty($this->section);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\SectionStorage;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Plugin\Context\Context;
|
||||
use Drupal\Core\Plugin\Context\ContextDefinition;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field_ui\FieldUI;
|
||||
use Drupal\layout_builder\DefaultsSectionStorageInterface;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator;
|
||||
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Defines the 'defaults' section storage type.
|
||||
*
|
||||
* @SectionStorage(
|
||||
* id = "defaults",
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity type bundle info.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
|
||||
*/
|
||||
protected $sectionList;
|
||||
|
||||
/**
|
||||
* The sample entity generator.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator
|
||||
*/
|
||||
protected $sampleEntityGenerator;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, LayoutBuilderSampleEntityGenerator $sample_entity_generator) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityTypeBundleInfo = $entity_type_bundle_info;
|
||||
$this->sampleEntityGenerator = $sample_entity_generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('layout_builder.sample_entity_generator')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSectionList(SectionListInterface $section_list) {
|
||||
if (!$section_list instanceof LayoutEntityDisplayInterface) {
|
||||
throw new \InvalidArgumentException('Defaults expect a display-based section list');
|
||||
}
|
||||
|
||||
return parent::setSectionList($section_list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity storing the overrides.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
|
||||
* The entity storing the defaults.
|
||||
*/
|
||||
protected function getDisplay() {
|
||||
return $this->getSectionList();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStorageId() {
|
||||
return $this->getDisplay()->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRedirectUrl() {
|
||||
return Url::fromRoute("entity.entity_view_display.{$this->getDisplay()->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLayoutBuilderUrl() {
|
||||
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.view", $this->getRouteParameters());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the route parameters needed to generate a URL for this object.
|
||||
*
|
||||
* @return mixed[]
|
||||
* An associative array of parameter names and values.
|
||||
*/
|
||||
protected function getRouteParameters() {
|
||||
$display = $this->getDisplay();
|
||||
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
|
||||
$route_parameters = FieldUI::getRouteBundleParameter($entity_type, $display->getTargetBundle());
|
||||
$route_parameters['view_mode_name'] = $display->getMode();
|
||||
return $route_parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRoutes(RouteCollection $collection) {
|
||||
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
|
||||
// Try to get the route from the current collection.
|
||||
if (!$entity_route = $collection->get($entity_type->get('field_ui_base_route'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $entity_route->getPath() . '/display-layout/{view_mode_name}';
|
||||
|
||||
$defaults = [];
|
||||
$defaults['entity_type_id'] = $entity_type_id;
|
||||
// If the entity type has no bundles and it doesn't use {bundle} in its
|
||||
// admin path, use the entity type.
|
||||
if (strpos($path, '{bundle}') === FALSE) {
|
||||
if (!$entity_type->hasKey('bundle')) {
|
||||
$defaults['bundle'] = $entity_type_id;
|
||||
}
|
||||
else {
|
||||
$defaults['bundle_key'] = $entity_type->getBundleEntityType();
|
||||
}
|
||||
}
|
||||
|
||||
$requirements = [];
|
||||
$requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display';
|
||||
|
||||
$options = $entity_route->getOptions();
|
||||
$options['_admin_route'] = FALSE;
|
||||
|
||||
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id);
|
||||
|
||||
$route_names = [
|
||||
"entity.entity_view_display.{$entity_type_id}.default",
|
||||
"entity.entity_view_display.{$entity_type_id}.view_mode",
|
||||
];
|
||||
foreach ($route_names as $route_name) {
|
||||
if (!$route = $collection->get($route_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$route->addDefaults([
|
||||
'section_storage_type' => $this->getStorageType(),
|
||||
'section_storage' => '',
|
||||
] + $defaults);
|
||||
$parameters['section_storage']['layout_builder_tempstore'] = TRUE;
|
||||
$parameters = NestedArray::mergeDeep($parameters, $route->getOption('parameters') ?: []);
|
||||
$route->setOption('parameters', $parameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of relevant entity types.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypes() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extractIdFromRoute($value, $definition, $name, array $defaults) {
|
||||
if (is_string($value) && strpos($value, '.') !== FALSE) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// If a bundle is not provided but a value corresponding to the bundle key
|
||||
// is, use that for the bundle value.
|
||||
if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) {
|
||||
$defaults['bundle'] = $defaults[$defaults['bundle_key']];
|
||||
}
|
||||
|
||||
if (!empty($defaults['entity_type_id']) && !empty($defaults['bundle']) && !empty($defaults['view_mode_name'])) {
|
||||
return $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSectionListFromId($id) {
|
||||
if (strpos($id, '.') === FALSE) {
|
||||
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
|
||||
}
|
||||
|
||||
$storage = $this->entityTypeManager->getStorage('entity_view_display');
|
||||
// If the display does not exist, create a new one.
|
||||
if (!$display = $storage->load($id)) {
|
||||
list($entity_type_id, $bundle, $view_mode) = explode('.', $id, 3);
|
||||
$display = $storage->create([
|
||||
'targetEntityType' => $entity_type_id,
|
||||
'bundle' => $bundle,
|
||||
'mode' => $view_mode,
|
||||
'status' => TRUE,
|
||||
]);
|
||||
}
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContexts() {
|
||||
$display = $this->getDisplay();
|
||||
$entity = $this->sampleEntityGenerator->get($display->getTargetEntityTypeId(), $display->getTargetBundle());
|
||||
$context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]);
|
||||
|
||||
// @todo Use EntityContextDefinition after resolving
|
||||
// https://www.drupal.org/node/2932462.
|
||||
$contexts = [];
|
||||
$contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity);
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->getDisplay()->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save() {
|
||||
return $this->getDisplay()->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isOverridable() {
|
||||
return $this->getDisplay()->isOverridable();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOverridable($overridable = TRUE) {
|
||||
$this->getDisplay()->setOverridable($overridable);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\SectionStorage;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Plugin\Context\Context;
|
||||
use Drupal\Core\Plugin\Context\ContextDefinition;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
use Drupal\layout_builder\OverridesSectionStorageInterface;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Defines the 'overrides' section storage type.
|
||||
*
|
||||
* @SectionStorage(
|
||||
* id = "overrides",
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionListInterface|\Drupal\Core\Field\FieldItemListInterface
|
||||
*/
|
||||
protected $sectionList;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_field.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSectionList(SectionListInterface $section_list) {
|
||||
if (!$section_list instanceof FieldItemListInterface) {
|
||||
throw new \InvalidArgumentException('Overrides expect a field-based section list');
|
||||
}
|
||||
|
||||
return parent::setSectionList($section_list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity storing the overrides.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\FieldableEntityInterface
|
||||
* The entity storing the overrides.
|
||||
*/
|
||||
protected function getEntity() {
|
||||
return $this->getSectionList()->getEntity();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStorageId() {
|
||||
$entity = $this->getEntity();
|
||||
return $entity->getEntityTypeId() . '.' . $entity->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function extractIdFromRoute($value, $definition, $name, array $defaults) {
|
||||
if (strpos($value, '.') !== FALSE) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (isset($defaults['entity_type_id']) && !empty($defaults[$defaults['entity_type_id']])) {
|
||||
$entity_type_id = $defaults['entity_type_id'];
|
||||
$entity_id = $defaults[$entity_type_id];
|
||||
return $entity_type_id . '.' . $entity_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSectionListFromId($id) {
|
||||
if (strpos($id, '.') !== FALSE) {
|
||||
list($entity_type_id, $entity_id) = explode('.', $id, 2);
|
||||
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
|
||||
if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) {
|
||||
return $entity->get('layout_builder__layout');
|
||||
}
|
||||
}
|
||||
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRoutes(RouteCollection $collection) {
|
||||
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
|
||||
$defaults = [];
|
||||
$defaults['entity_type_id'] = $entity_type_id;
|
||||
|
||||
$requirements = [];
|
||||
if ($this->hasIntegerId($entity_type)) {
|
||||
$requirements[$entity_type_id] = '\d+';
|
||||
}
|
||||
|
||||
$options = [];
|
||||
// Ensure that upcasting is run in the correct order.
|
||||
$options['parameters']['section_storage'] = [];
|
||||
$options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id;
|
||||
|
||||
$template = $entity_type->getLinkTemplate('canonical') . '/layout';
|
||||
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this entity type's ID is stored as an integer.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* An entity type.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if this entity type's ID key is always an integer, FALSE otherwise.
|
||||
*/
|
||||
protected function hasIntegerId(EntityTypeInterface $entity_type) {
|
||||
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
|
||||
return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of relevant entity types.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* An array of entity types.
|
||||
*/
|
||||
protected function getEntityTypes() {
|
||||
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
|
||||
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSectionStorage() {
|
||||
return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRedirectUrl() {
|
||||
return $this->getEntity()->toUrl('canonical');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLayoutBuilderUrl() {
|
||||
$entity = $this->getEntity();
|
||||
$route_parameters[$entity->getEntityTypeId()] = $entity->id();
|
||||
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.view", $route_parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContexts() {
|
||||
$entity = $this->getEntity();
|
||||
// @todo Use EntityContextDefinition after resolving
|
||||
// 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);
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->getEntity()->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save() {
|
||||
return $this->getEntity()->save();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Plugin\SectionStorage;
|
||||
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides a base class for Section Storage types.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
abstract class SectionStorageBase extends PluginBase implements SectionStorageInterface {
|
||||
|
||||
use LayoutBuilderRoutesTrait;
|
||||
|
||||
/**
|
||||
* The section storage instance.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionListInterface|null
|
||||
*/
|
||||
protected $sectionList;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSectionList(SectionListInterface $section_list) {
|
||||
$this->sectionList = $section_list;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the section list.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionListInterface
|
||||
* The section list.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* Thrown if ::setSectionList() is not called first.
|
||||
*/
|
||||
protected function getSectionList() {
|
||||
if (!$this->sectionList) {
|
||||
throw new \RuntimeException(sprintf('%s::setSectionList() must be called first', static::class));
|
||||
}
|
||||
return $this->sectionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStorageType() {
|
||||
return $this->getPluginId();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count() {
|
||||
return $this->getSectionList()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSections() {
|
||||
return $this->getSectionList()->getSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSection($delta) {
|
||||
return $this->getSectionList()->getSection($delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appendSection(Section $section) {
|
||||
$this->getSectionList()->appendSection($section);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function insertSection($delta, Section $section) {
|
||||
$this->getSectionList()->insertSection($delta, $section);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSection($delta) {
|
||||
$this->getSectionList()->removeSection($delta);
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Routing;
|
||||
|
||||
use Drupal\Core\Routing\EnhancerInterface;
|
||||
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Converts the query parameter for layout rebuild status to a route default.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutBuilderRouteEnhancer implements EnhancerInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function enhance(array $defaults, Request $request) {
|
||||
$route = $defaults[RouteObjectInterface::ROUTE_OBJECT];
|
||||
if ($route->hasOption('_layout_builder')) {
|
||||
$defaults['is_rebuilding'] = (bool) $request->query->get('layout_is_rebuilding', FALSE);
|
||||
}
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Routing;
|
||||
|
||||
use Drupal\Core\Routing\RouteBuildEvent;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Provides routes for the Layout Builder UI.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutBuilderRoutes implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The section storage manager.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
|
||||
*/
|
||||
protected $sectionStorageManager;
|
||||
|
||||
/**
|
||||
* Constructs a new LayoutBuilderRoutes.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
|
||||
* The section storage manager.
|
||||
*/
|
||||
public function __construct(SectionStorageManagerInterface $section_storage_manager) {
|
||||
$this->sectionStorageManager = $section_storage_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters existing routes for a specific collection.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteBuildEvent $event
|
||||
* The route build event.
|
||||
*/
|
||||
public function onAlterRoutes(RouteBuildEvent $event) {
|
||||
$collection = $event->getRouteCollection();
|
||||
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
|
||||
$this->sectionStorageManager->loadEmpty($plugin_id)->buildRoutes($collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
// Run after \Drupal\field_ui\Routing\RouteSubscriber.
|
||||
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -110];
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Routing;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\layout_builder\OverridesSectionStorageInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Provides a trait for building routes for a Layout Builder UI.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
trait LayoutBuilderRoutesTrait {
|
||||
|
||||
/**
|
||||
* Builds the layout routes for the given values.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\RouteCollection $collection
|
||||
* The route collection.
|
||||
* @param \Drupal\layout_builder\SectionStorage\SectionStorageDefinition $definition
|
||||
* The definition of the section storage.
|
||||
* @param string $path
|
||||
* The path patten for the routes.
|
||||
* @param array $defaults
|
||||
* (optional) An array of default parameter values.
|
||||
* @param array $requirements
|
||||
* (optional) An array of requirements for parameters.
|
||||
* @param array $options
|
||||
* (optional) An array of options.
|
||||
* @param string $route_name_prefix
|
||||
* (optional) The prefix to use for the route name.
|
||||
*/
|
||||
protected function buildLayoutRoutes(RouteCollection $collection, SectionStorageDefinition $definition, $path, array $defaults = [], array $requirements = [], array $options = [], $route_name_prefix = '') {
|
||||
$type = $definition->id();
|
||||
$defaults['section_storage_type'] = $type;
|
||||
// Provide an empty value to allow the section storage to be upcast.
|
||||
$defaults['section_storage'] = '';
|
||||
// Trigger the layout builder access check.
|
||||
$requirements['_has_layout_section'] = 'true';
|
||||
// Trigger the layout builder RouteEnhancer.
|
||||
$options['_layout_builder'] = TRUE;
|
||||
// Trigger the layout builder param converter.
|
||||
$parameters['section_storage']['layout_builder_tempstore'] = TRUE;
|
||||
// Merge the passed in options in after Layout Builder's parameters.
|
||||
$options = NestedArray::mergeDeep(['parameters' => $parameters], $options);
|
||||
|
||||
if ($route_name_prefix) {
|
||||
$route_name_prefix = "layout_builder.$type.$route_name_prefix";
|
||||
}
|
||||
else {
|
||||
$route_name_prefix = "layout_builder.$type";
|
||||
}
|
||||
|
||||
$main_defaults = $defaults;
|
||||
$main_defaults['is_rebuilding'] = FALSE;
|
||||
$main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
|
||||
$main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title';
|
||||
$route = (new Route($path))
|
||||
->setDefaults($main_defaults)
|
||||
->setRequirements($requirements)
|
||||
->setOptions($options);
|
||||
$collection->add("$route_name_prefix.view", $route);
|
||||
|
||||
$save_defaults = $defaults;
|
||||
$save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout';
|
||||
$route = (new Route("$path/save"))
|
||||
->setDefaults($save_defaults)
|
||||
->setRequirements($requirements)
|
||||
->setOptions($options);
|
||||
$collection->add("$route_name_prefix.save", $route);
|
||||
|
||||
$cancel_defaults = $defaults;
|
||||
$cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout';
|
||||
$route = (new Route("$path/cancel"))
|
||||
->setDefaults($cancel_defaults)
|
||||
->setRequirements($requirements)
|
||||
->setOptions($options);
|
||||
$collection->add("$route_name_prefix.cancel", $route);
|
||||
|
||||
if (is_subclass_of($definition->getClass(), OverridesSectionStorageInterface::class)) {
|
||||
$revert_defaults = $defaults;
|
||||
$revert_defaults['_form'] = '\Drupal\layout_builder\Form\RevertOverridesForm';
|
||||
$route = (new Route("$path/revert"))
|
||||
->setDefaults($revert_defaults)
|
||||
->setRequirements($requirements)
|
||||
->setOptions($options);
|
||||
$collection->add("$route_name_prefix.revert", $route);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\Routing;
|
||||
|
||||
use Drupal\Core\ParamConverter\ParamConverterInterface;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Loads the section storage from the layout tempstore.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LayoutTempstoreParamConverter implements ParamConverterInterface {
|
||||
|
||||
/**
|
||||
* The layout tempstore repository.
|
||||
*
|
||||
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
|
||||
*/
|
||||
protected $layoutTempstoreRepository;
|
||||
|
||||
/**
|
||||
* The section storage manager.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
|
||||
*/
|
||||
protected $sectionStorageManager;
|
||||
|
||||
/**
|
||||
* Constructs a new LayoutTempstoreParamConverter.
|
||||
*
|
||||
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
|
||||
* The layout tempstore repository.
|
||||
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
|
||||
* The section storage manager.
|
||||
*/
|
||||
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, SectionStorageManagerInterface $section_storage_manager) {
|
||||
$this->layoutTempstoreRepository = $layout_tempstore_repository;
|
||||
$this->sectionStorageManager = $section_storage_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function convert($value, $definition, $name, array $defaults) {
|
||||
if (isset($defaults['section_storage_type']) && $this->sectionStorageManager->hasDefinition($defaults['section_storage_type'])) {
|
||||
if ($section_storage = $this->sectionStorageManager->loadFromRoute($defaults['section_storage_type'], $value, $definition, $name, $defaults)) {
|
||||
// Pass the plugin through the tempstore repository.
|
||||
return $this->layoutTempstoreRepository->get($section_storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies($definition, $name, Route $route) {
|
||||
return !empty($definition['layout_builder_tempstore']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Provides a domain object for layout sections.
|
||||
*
|
||||
* A section consists of three parts:
|
||||
* - The layout plugin ID for the layout applied to the section (for example,
|
||||
* 'layout_onecol').
|
||||
* - An array of settings for the layout plugin.
|
||||
* - An array of components that can be rendered in the section.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*
|
||||
* @see \Drupal\Core\Layout\LayoutDefinition
|
||||
* @see \Drupal\layout_builder\SectionComponent
|
||||
*
|
||||
* @todo Determine whether an interface will be provided for this in
|
||||
* https://www.drupal.org/project/drupal/issues/2930334.
|
||||
*/
|
||||
class Section {
|
||||
|
||||
/**
|
||||
* The layout plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $layoutId;
|
||||
|
||||
/**
|
||||
* The layout plugin settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $layoutSettings = [];
|
||||
|
||||
/**
|
||||
* An array of components, keyed by UUID.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionComponent[]
|
||||
*/
|
||||
protected $components = [];
|
||||
|
||||
/**
|
||||
* Constructs a new Section.
|
||||
*
|
||||
* @param string $layout_id
|
||||
* The layout plugin ID.
|
||||
* @param array $layout_settings
|
||||
* (optional) The layout plugin settings.
|
||||
* @param \Drupal\layout_builder\SectionComponent[] $components
|
||||
* (optional) The components.
|
||||
*/
|
||||
public function __construct($layout_id, array $layout_settings = [], array $components = []) {
|
||||
$this->layoutId = $layout_id;
|
||||
$this->layoutSettings = $layout_settings;
|
||||
foreach ($components as $component) {
|
||||
$this->setComponent($component);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the renderable array for this section.
|
||||
*
|
||||
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
|
||||
* An array of available contexts.
|
||||
* @param bool $in_preview
|
||||
* TRUE if the section is being previewed, FALSE otherwise.
|
||||
*
|
||||
* @return array
|
||||
* A renderable array representing the content of the section.
|
||||
*/
|
||||
public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
|
||||
$regions = [];
|
||||
foreach ($this->getComponents() as $component) {
|
||||
if ($output = $component->toRenderArray($contexts, $in_preview)) {
|
||||
$regions[$component->getRegion()][$component->getUuid()] = $output;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->getLayout()->build($regions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the layout plugin for this section.
|
||||
*
|
||||
* @return \Drupal\Core\Layout\LayoutInterface
|
||||
* The layout plugin.
|
||||
*/
|
||||
public function getLayout() {
|
||||
return $this->layoutPluginManager()->createInstance($this->getLayoutId(), $this->getLayoutSettings());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the layout plugin ID for this section.
|
||||
*
|
||||
* @return string
|
||||
* The layout plugin ID.
|
||||
*
|
||||
* @internal
|
||||
* This method should only be used by code responsible for storing the data.
|
||||
*/
|
||||
public function getLayoutId() {
|
||||
return $this->layoutId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the layout plugin settings for this section.
|
||||
*
|
||||
* @return mixed[]
|
||||
* The layout plugin settings.
|
||||
*
|
||||
* @internal
|
||||
* This method should only be used by code responsible for storing the data.
|
||||
*/
|
||||
public function getLayoutSettings() {
|
||||
return $this->layoutSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the layout plugin settings for this section.
|
||||
*
|
||||
* @param mixed[] $layout_settings
|
||||
* The layout plugin settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLayoutSettings(array $layout_settings) {
|
||||
$this->layoutSettings = $layout_settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default region.
|
||||
*
|
||||
* @return string
|
||||
* The machine-readable name of the default region.
|
||||
*/
|
||||
public function getDefaultRegion() {
|
||||
return $this->layoutPluginManager()->getDefinition($this->getLayoutId())->getDefaultRegion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the components of the section.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionComponent[]
|
||||
* The components.
|
||||
*/
|
||||
public function getComponents() {
|
||||
return $this->components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the component for a given UUID.
|
||||
*
|
||||
* @param string $uuid
|
||||
* The UUID of the component to retrieve.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionComponent
|
||||
* The component.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* Thrown when the expected UUID does not exist.
|
||||
*/
|
||||
public function getComponent($uuid) {
|
||||
if (!isset($this->components[$uuid])) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid UUID "%s"', $uuid));
|
||||
}
|
||||
|
||||
return $this->components[$uuid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to set a component.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionComponent $component
|
||||
* The component.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setComponent(SectionComponent $component) {
|
||||
$this->components[$component->getUuid()] = $component;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a given component from a region.
|
||||
*
|
||||
* @param string $uuid
|
||||
* The UUID of the component to remove.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeComponent($uuid) {
|
||||
unset($this->components[$uuid]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a component to the end of a region.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionComponent $component
|
||||
* The component being appended.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendComponent(SectionComponent $component) {
|
||||
$component->setWeight($this->getNextHighestWeight($component->getRegion()));
|
||||
$this->setComponent($component);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next highest weight of the component in a region.
|
||||
*
|
||||
* @param string $region
|
||||
* The region name.
|
||||
*
|
||||
* @return int
|
||||
* A number higher than the highest weight of the component in the region.
|
||||
*/
|
||||
protected function getNextHighestWeight($region) {
|
||||
$components = $this->getComponentsByRegion($region);
|
||||
$weights = array_map(function (SectionComponent $component) {
|
||||
return $component->getWeight();
|
||||
}, $components);
|
||||
return $weights ? max($weights) + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the components for a specific region.
|
||||
*
|
||||
* @param string $region
|
||||
* The region name.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionComponent[]
|
||||
* An array of components in the specified region, sorted by weight.
|
||||
*/
|
||||
protected function getComponentsByRegion($region) {
|
||||
$components = array_filter($this->getComponents(), function (SectionComponent $component) use ($region) {
|
||||
return $component->getRegion() === $region;
|
||||
});
|
||||
uasort($components, function (SectionComponent $a, SectionComponent $b) {
|
||||
return $a->getWeight() > $b->getWeight() ? 1 : -1;
|
||||
});
|
||||
return $components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a component after a specified existing component.
|
||||
*
|
||||
* @param string $preceding_uuid
|
||||
* The UUID of the existing component to insert after.
|
||||
* @param \Drupal\layout_builder\SectionComponent $component
|
||||
* The component being inserted.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* Thrown when the expected UUID does not exist.
|
||||
*/
|
||||
public function insertAfterComponent($preceding_uuid, SectionComponent $component) {
|
||||
// Find the delta of the specified UUID.
|
||||
$uuids = array_keys($this->getComponentsByRegion($component->getRegion()));
|
||||
$delta = array_search($preceding_uuid, $uuids, TRUE);
|
||||
if ($delta === FALSE) {
|
||||
throw new \InvalidArgumentException(sprintf('Invalid preceding UUID "%s"', $preceding_uuid));
|
||||
}
|
||||
return $this->insertComponent($delta + 1, $component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a component at a specified delta.
|
||||
*
|
||||
* @param int $delta
|
||||
* The zero-based delta in which to insert the component.
|
||||
* @param \Drupal\layout_builder\SectionComponent $new_component
|
||||
* The component being inserted.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \OutOfBoundsException
|
||||
* Thrown when the specified delta is invalid.
|
||||
*/
|
||||
public function insertComponent($delta, SectionComponent $new_component) {
|
||||
$components = $this->getComponentsByRegion($new_component->getRegion());
|
||||
$count = count($components);
|
||||
if ($delta > $count) {
|
||||
throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" component', $delta, $new_component->getUuid()));
|
||||
}
|
||||
|
||||
// If the delta is the end of the list, append the component instead.
|
||||
if ($delta === $count) {
|
||||
return $this->appendComponent($new_component);
|
||||
}
|
||||
|
||||
// Find the weight of the component that exists at the specified delta.
|
||||
$weight = array_values($components)[$delta]->getWeight();
|
||||
$this->setComponent($new_component->setWeight($weight++));
|
||||
|
||||
// Increase the weight of every subsequent component.
|
||||
foreach (array_slice($components, $delta) as $component) {
|
||||
$component->setWeight($weight++);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the layout plugin manager.
|
||||
*
|
||||
* @return \Drupal\Core\Layout\LayoutPluginManagerInterface
|
||||
* The layout plugin manager.
|
||||
*/
|
||||
protected function layoutPluginManager() {
|
||||
return \Drupal::service('plugin.manager.core.layout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array representation of the section.
|
||||
*
|
||||
* @internal
|
||||
* This is intended for use by a storage mechanism for sections.
|
||||
*
|
||||
* @return array
|
||||
* An array representation of the section component.
|
||||
*/
|
||||
public function toArray() {
|
||||
return [
|
||||
'layout_id' => $this->getLayoutId(),
|
||||
'layout_settings' => $this->getLayoutSettings(),
|
||||
'components' => array_map(function (SectionComponent $component) {
|
||||
return $component->toArray();
|
||||
}, $this->getComponents()),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
use Drupal\Component\Plugin\Exception\PluginException;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginInterface;
|
||||
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
|
||||
|
||||
/**
|
||||
* Provides a value object for a section component.
|
||||
*
|
||||
* A component represents the smallest part of a layout (for example, a block).
|
||||
* Components wrap a renderable plugin, currently using
|
||||
* \Drupal\Core\Block\BlockPluginInterface, and contain the layout region
|
||||
* within the section layout where the component will be rendered.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*
|
||||
* @see \Drupal\Core\Layout\LayoutDefinition
|
||||
* @see \Drupal\layout_builder\Section
|
||||
* @see \Drupal\layout_builder\SectionStorageInterface
|
||||
*
|
||||
* @todo Determine whether to retain the name 'component' in
|
||||
* https://www.drupal.org/project/drupal/issues/2929783.
|
||||
* @todo Determine whether an interface will be provided for this in
|
||||
* https://www.drupal.org/project/drupal/issues/2930334.
|
||||
*/
|
||||
class SectionComponent {
|
||||
|
||||
/**
|
||||
* The UUID of the component.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $uuid;
|
||||
|
||||
/**
|
||||
* The region the component is placed in.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $region;
|
||||
|
||||
/**
|
||||
* An array of plugin configuration.
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* The weight of the component.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $weight = 0;
|
||||
|
||||
/**
|
||||
* Any additional properties and values.
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
protected $additional = [];
|
||||
|
||||
/**
|
||||
* Constructs a new SectionComponent.
|
||||
*
|
||||
* @param string $uuid
|
||||
* The UUID.
|
||||
* @param string $region
|
||||
* The region.
|
||||
* @param mixed[] $configuration
|
||||
* The plugin configuration.
|
||||
* @param mixed[] $additional
|
||||
* An additional values.
|
||||
*/
|
||||
public function __construct($uuid, $region, array $configuration = [], array $additional = []) {
|
||||
$this->uuid = $uuid;
|
||||
$this->region = $region;
|
||||
$this->configuration = $configuration;
|
||||
$this->additional = $additional;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the renderable array for this component.
|
||||
*
|
||||
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
|
||||
* An array of available contexts.
|
||||
* @param bool $in_preview
|
||||
* TRUE if the component is being previewed, FALSE otherwise.
|
||||
*
|
||||
* @return array
|
||||
* A renderable array representing the content of the component.
|
||||
*/
|
||||
public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
|
||||
$event = new SectionComponentBuildRenderArrayEvent($this, $contexts, $in_preview);
|
||||
$this->eventDispatcher()->dispatch(LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY, $event);
|
||||
$output = $event->getBuild();
|
||||
$event->getCacheableMetadata()->applyTo($output);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any arbitrary property for the component.
|
||||
*
|
||||
* @param string $property
|
||||
* The property to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
* The value for that property, or NULL if the property does not exist.
|
||||
*/
|
||||
public function get($property) {
|
||||
if (property_exists($this, $property)) {
|
||||
$value = isset($this->{$property}) ? $this->{$property} : NULL;
|
||||
}
|
||||
else {
|
||||
$value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value to an arbitrary property for the component.
|
||||
*
|
||||
* @param string $property
|
||||
* The property to use for the value.
|
||||
* @param mixed $value
|
||||
* The value to set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function set($property, $value) {
|
||||
if (property_exists($this, $property)) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
else {
|
||||
$this->additional[$property] = $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the region for the component.
|
||||
*
|
||||
* @return string
|
||||
* The region.
|
||||
*/
|
||||
public function getRegion() {
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the region for the component.
|
||||
*
|
||||
* @param string $region
|
||||
* The region.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRegion($region) {
|
||||
$this->region = $region;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the weight of the component.
|
||||
*
|
||||
* @return int
|
||||
* The zero-based weight of the component.
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
* Thrown if the weight was never set.
|
||||
*/
|
||||
public function getWeight() {
|
||||
return $this->weight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the weight of the component.
|
||||
*
|
||||
* @param int $weight
|
||||
* The zero-based weight of the component.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setWeight($weight) {
|
||||
$this->weight = $weight;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the component plugin configuration.
|
||||
*
|
||||
* @return mixed[]
|
||||
* The component plugin configuration.
|
||||
*/
|
||||
protected function getConfiguration() {
|
||||
return $this->configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the plugin configuration.
|
||||
*
|
||||
* @param mixed[] $configuration
|
||||
* The plugin configuration.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setConfiguration(array $configuration) {
|
||||
$this->configuration = $configuration;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin ID.
|
||||
*
|
||||
* @return string
|
||||
* The plugin ID.
|
||||
*
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginException
|
||||
* Thrown if the plugin ID cannot be found.
|
||||
*/
|
||||
public function getPluginId() {
|
||||
if (empty($this->configuration['id'])) {
|
||||
throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid));
|
||||
}
|
||||
return $this->configuration['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the UUID for this component.
|
||||
*
|
||||
* @return string
|
||||
* The UUID.
|
||||
*/
|
||||
public function getUuid() {
|
||||
return $this->uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plugin for this component.
|
||||
*
|
||||
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
|
||||
* An array of contexts to set on the plugin.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\PluginInspectionInterface
|
||||
* The plugin.
|
||||
*/
|
||||
public function getPlugin(array $contexts = []) {
|
||||
$plugin = $this->pluginManager()->createInstance($this->getPluginId(), $this->getConfiguration());
|
||||
if ($contexts && $plugin instanceof ContextAwarePluginInterface) {
|
||||
$this->contextHandler()->applyContextMapping($plugin, $contexts);
|
||||
}
|
||||
return $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the component plugin manager.
|
||||
*
|
||||
* @return \Drupal\Core\Block\BlockManagerInterface
|
||||
* The plugin manager.
|
||||
*/
|
||||
protected function pluginManager() {
|
||||
// @todo Figure out the best way to unify fields and blocks and components
|
||||
// in https://www.drupal.org/node/1875974.
|
||||
return \Drupal::service('plugin.manager.block');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the context handler.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\Context\ContextHandlerInterface
|
||||
* The context handler.
|
||||
*/
|
||||
protected function contextHandler() {
|
||||
return \Drupal::service('context.handler');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the event dispatcher.
|
||||
*
|
||||
* @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
|
||||
* The event dispatcher.
|
||||
*/
|
||||
protected function eventDispatcher() {
|
||||
return \Drupal::service('event_dispatcher');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array representation of the section component.
|
||||
*
|
||||
* @internal
|
||||
* This is intended for use by a storage mechanism for section components.
|
||||
*
|
||||
* @return array
|
||||
* An array representation of the section component.
|
||||
*/
|
||||
public function toArray() {
|
||||
return [
|
||||
'uuid' => $this->getUuid(),
|
||||
'region' => $this->getRegion(),
|
||||
'configuration' => $this->getConfiguration(),
|
||||
'additional' => $this->additional,
|
||||
'weight' => $this->getWeight(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
/**
|
||||
* Defines the interface for an object that stores layout sections.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*
|
||||
* @see \Drupal\layout_builder\Section
|
||||
*/
|
||||
interface SectionListInterface extends \Countable {
|
||||
|
||||
/**
|
||||
* Gets the layout sections.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Section[]
|
||||
* A sequentially and numerically keyed array of section objects.
|
||||
*/
|
||||
public function getSections();
|
||||
|
||||
/**
|
||||
* Gets a domain object for the layout section.
|
||||
*
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Section
|
||||
* The layout section.
|
||||
*/
|
||||
public function getSection($delta);
|
||||
|
||||
/**
|
||||
* Appends a new section to the end of the list.
|
||||
*
|
||||
* @param \Drupal\layout_builder\Section $section
|
||||
* The section to append.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function appendSection(Section $section);
|
||||
|
||||
/**
|
||||
* Inserts a new section at a given delta.
|
||||
*
|
||||
* If a section exists at the given index, the section at that position and
|
||||
* others after it are shifted backward.
|
||||
*
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
* @param \Drupal\layout_builder\Section $section
|
||||
* The section to insert.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function insertSection($delta, Section $section);
|
||||
|
||||
/**
|
||||
* Removes the section at the given delta.
|
||||
*
|
||||
* As sections are stored sequentially and numerically this will re-key every
|
||||
* subsequent section, shifting them forward.
|
||||
*
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function removeSection($delta);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\SectionStorage;
|
||||
|
||||
use Drupal\Component\Plugin\Definition\PluginDefinition;
|
||||
|
||||
/**
|
||||
* Provides section storage type plugin definition.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class SectionStorageDefinition extends PluginDefinition {
|
||||
|
||||
/**
|
||||
* Any additional properties and values.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $additional = [];
|
||||
|
||||
/**
|
||||
* LayoutDefinition constructor.
|
||||
*
|
||||
* @param array $definition
|
||||
* An array of values from the annotation.
|
||||
*/
|
||||
public function __construct(array $definition = []) {
|
||||
foreach ($definition as $property => $value) {
|
||||
$this->set($property, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets any arbitrary property.
|
||||
*
|
||||
* @param string $property
|
||||
* The property to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
* The value for that property, or NULL if the property does not exist.
|
||||
*/
|
||||
public function get($property) {
|
||||
if (property_exists($this, $property)) {
|
||||
$value = isset($this->{$property}) ? $this->{$property} : NULL;
|
||||
}
|
||||
else {
|
||||
$value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a value to an arbitrary property.
|
||||
*
|
||||
* @param string $property
|
||||
* The property to use for the value.
|
||||
* @param mixed $value
|
||||
* The value to set.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function set($property, $value) {
|
||||
if (property_exists($this, $property)) {
|
||||
$this->{$property} = $value;
|
||||
}
|
||||
else {
|
||||
$this->additional[$property] = $value;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\SectionStorage;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\layout_builder\Annotation\SectionStorage;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides the Section Storage type plugin manager.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
class SectionStorageManager extends DefaultPluginManager implements SectionStorageManagerInterface {
|
||||
|
||||
/**
|
||||
* Constructs a new SectionStorageManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/SectionStorage', $namespaces, $module_handler, SectionStorageInterface::class, SectionStorage::class);
|
||||
|
||||
$this->alterInfo('layout_builder_section_storage');
|
||||
$this->setCacheBackend($cache_backend, 'layout_builder_section_storage_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadEmpty($id) {
|
||||
return $this->createInstance($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadFromStorageId($type, $id) {
|
||||
/** @var \Drupal\layout_builder\SectionStorageInterface $plugin */
|
||||
$plugin = $this->createInstance($type);
|
||||
return $plugin->setSectionList($plugin->getSectionListFromId($id));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadFromRoute($type, $value, $definition, $name, array $defaults) {
|
||||
/** @var \Drupal\layout_builder\SectionStorageInterface $plugin */
|
||||
$plugin = $this->createInstance($type);
|
||||
if ($id = $plugin->extractIdFromRoute($value, $definition, $name, $defaults)) {
|
||||
try {
|
||||
return $plugin->setSectionList($plugin->getSectionListFromId($id));
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
// Intentionally empty.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\SectionStorage;
|
||||
|
||||
use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
|
||||
|
||||
/**
|
||||
* Provides the interface for a plugin manager of section storage types.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface SectionStorageManagerInterface extends DiscoveryInterface {
|
||||
|
||||
/**
|
||||
* Loads a section storage with no associated section list.
|
||||
*
|
||||
* @param string $id
|
||||
* The ID of the section storage being instantiated.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionStorageInterface
|
||||
* The section storage.
|
||||
*/
|
||||
public function loadEmpty($id);
|
||||
|
||||
/**
|
||||
* Loads a section storage populated with an existing section list.
|
||||
*
|
||||
* @param string $type
|
||||
* The section storage type.
|
||||
* @param string $id
|
||||
* The section list ID.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionStorageInterface
|
||||
* The section storage.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* Thrown if the ID is invalid.
|
||||
*/
|
||||
public function loadFromStorageId($type, $id);
|
||||
|
||||
/**
|
||||
* Loads a section storage populated with a section list derived from a route.
|
||||
*
|
||||
* @param string $type
|
||||
* The section storage type.
|
||||
* @param string $value
|
||||
* The raw value.
|
||||
* @param mixed $definition
|
||||
* The parameter definition provided in the route options.
|
||||
* @param string $name
|
||||
* The name of the parameter.
|
||||
* @param array $defaults
|
||||
* The route defaults array.
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionStorageInterface|null
|
||||
* The section storage if it could be loaded, or NULL otherwise.
|
||||
*
|
||||
* @see \Drupal\Core\ParamConverter\ParamConverterInterface::convert()
|
||||
*/
|
||||
public function loadFromRoute($type, $value, $definition, $name, array $defaults);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder\SectionStorage;
|
||||
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Provides a trait for storing sections on an object.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
trait SectionStorageTrait {
|
||||
|
||||
/**
|
||||
* Stores the information for all sections.
|
||||
*
|
||||
* Implementations of this method are expected to call array_values() to rekey
|
||||
* the list of sections.
|
||||
*
|
||||
* @param \Drupal\layout_builder\Section[] $sections
|
||||
* An array of section objects.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
abstract protected function setSections(array $sections);
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function count() {
|
||||
return count($this->getSections());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSection($delta) {
|
||||
if (!$this->hasSection($delta)) {
|
||||
throw new \OutOfBoundsException(sprintf('Invalid delta "%s"', $delta));
|
||||
}
|
||||
|
||||
return $this->getSections()[$delta];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the section for the given delta on the display.
|
||||
*
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
* @param \Drupal\layout_builder\Section $section
|
||||
* The layout section.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
protected function setSection($delta, Section $section) {
|
||||
$sections = $this->getSections();
|
||||
$sections[$delta] = $section;
|
||||
$this->setSections($sections);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function appendSection(Section $section) {
|
||||
$delta = $this->count();
|
||||
|
||||
$this->setSection($delta, $section);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function insertSection($delta, Section $section) {
|
||||
if ($this->hasSection($delta)) {
|
||||
// @todo Use https://www.drupal.org/node/66183 once resolved.
|
||||
$start = array_slice($this->getSections(), 0, $delta);
|
||||
$end = array_slice($this->getSections(), $delta);
|
||||
$this->setSections(array_merge($start, [$section], $end));
|
||||
}
|
||||
else {
|
||||
$this->appendSection($section);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeSection($delta) {
|
||||
$sections = $this->getSections();
|
||||
unset($sections[$delta]);
|
||||
$this->setSections($sections);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there is a section at the specified delta.
|
||||
*
|
||||
* @param int $delta
|
||||
* The delta of the section.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there is a section for this delta, FALSE otherwise.
|
||||
*/
|
||||
protected function hasSection($delta) {
|
||||
return isset($this->getSections()[$delta]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\layout_builder;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Defines an interface for Section Storage type plugins.
|
||||
*
|
||||
* @internal
|
||||
* Layout Builder is currently experimental and should only be leveraged by
|
||||
* experimental modules and development releases of contributed modules.
|
||||
* See https://www.drupal.org/core/experimental for more information.
|
||||
*/
|
||||
interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Returns an identifier for this storage.
|
||||
*
|
||||
* @return string
|
||||
* The unique identifier for this storage.
|
||||
*/
|
||||
public function getStorageId();
|
||||
|
||||
/**
|
||||
* Returns the type of this storage.
|
||||
*
|
||||
* Used in conjunction with the storage ID.
|
||||
*
|
||||
* @return string
|
||||
* The type of storage.
|
||||
*/
|
||||
public function getStorageType();
|
||||
|
||||
/**
|
||||
* Sets the section list on the storage.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionListInterface $section_list
|
||||
* The section list.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @internal
|
||||
* This should only be called during section storage instantiation.
|
||||
*/
|
||||
public function setSectionList(SectionListInterface $section_list);
|
||||
|
||||
/**
|
||||
* Derives the section list from the storage ID.
|
||||
*
|
||||
* @param string $id
|
||||
* The storage ID, see ::getStorageId().
|
||||
*
|
||||
* @return \Drupal\layout_builder\SectionListInterface
|
||||
* The section list.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* Thrown if the ID is invalid.
|
||||
*
|
||||
* @internal
|
||||
* This should only be called during section storage instantiation.
|
||||
*/
|
||||
public function getSectionListFromId($id);
|
||||
|
||||
/**
|
||||
* Provides the routes needed for Layout Builder UI.
|
||||
*
|
||||
* Allows the plugin to add or alter routes during the route building process.
|
||||
* \Drupal\layout_builder\Routing\LayoutBuilderRoutesTrait is provided for the
|
||||
* typical use case of building a standard Layout Builder UI.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\RouteCollection $collection
|
||||
* The route collection.
|
||||
*
|
||||
* @see \Drupal\Core\Routing\RoutingEvents::ALTER
|
||||
*/
|
||||
public function buildRoutes(RouteCollection $collection);
|
||||
|
||||
/**
|
||||
* Gets the URL used when redirecting away from the Layout Builder UI.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The URL object.
|
||||
*/
|
||||
public function getRedirectUrl();
|
||||
|
||||
/**
|
||||
* Gets the URL used to display the Layout Builder UI.
|
||||
*
|
||||
* @return \Drupal\Core\Url
|
||||
* The URL object.
|
||||
*/
|
||||
public function getLayoutBuilderUrl();
|
||||
|
||||
/**
|
||||
* Configures the plugin based on route values.
|
||||
*
|
||||
* @param mixed $value
|
||||
* The raw value.
|
||||
* @param mixed $definition
|
||||
* The parameter definition provided in the route options.
|
||||
* @param string $name
|
||||
* The name of the parameter.
|
||||
* @param array $defaults
|
||||
* The route defaults array.
|
||||
*
|
||||
* @return string|null
|
||||
* The section storage ID if it could be extracted, NULL otherwise.
|
||||
*
|
||||
* @internal
|
||||
* This should only be called during section storage instantiation.
|
||||
*/
|
||||
public function extractIdFromRoute($value, $definition, $name, array $defaults);
|
||||
|
||||
/**
|
||||
* Provides any available contexts for the object using the sections.
|
||||
*
|
||||
* @return \Drupal\Core\Plugin\Context\ContextInterface[]
|
||||
* The array of context objects.
|
||||
*/
|
||||
public function getContexts();
|
||||
|
||||
/**
|
||||
* Gets the label for the object using the sections.
|
||||
*
|
||||
* @return string
|
||||
* The label, or NULL if there is no label defined.
|
||||
*/
|
||||
public function label();
|
||||
|
||||
/**
|
||||
* Saves the sections.
|
||||
*
|
||||
* @return int
|
||||
* SAVED_NEW or SAVED_UPDATED is returned depending on the operation
|
||||
* performed.
|
||||
*/
|
||||
public function save();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test fixture.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
|
||||
$connection = Database::getConnection();
|
||||
|
||||
// Update core.extension.
|
||||
$extensions = $connection->select('config')
|
||||
->fields('config', ['data'])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.extension')
|
||||
->execute()
|
||||
->fetchField();
|
||||
$extensions = unserialize($extensions);
|
||||
$extensions['module']['layout_builder'] = 0;
|
||||
$extensions['module']['layout_discovery'] = 0;
|
||||
$extensions['module']['layout_test'] = 0;
|
||||
$connection->update('config')
|
||||
->fields([
|
||||
'data' => serialize($extensions),
|
||||
'collection' => '',
|
||||
'name' => 'core.extension',
|
||||
])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.extension')
|
||||
->execute();
|
||||
|
||||
// Add a layout plugin with a dependency to an existing entity view display.
|
||||
$display = $connection->select('config')
|
||||
->fields('config', ['data'])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.entity_view_display.node.article.teaser')
|
||||
->execute()
|
||||
->fetchField();
|
||||
$display = unserialize($display);
|
||||
$display['third_party_settings']['layout_builder']['sections'][] = [
|
||||
'layout_id' => 'layout_test_dependencies_plugin',
|
||||
'layout_settings' => [],
|
||||
'components' => [],
|
||||
];
|
||||
$connection->update('config')
|
||||
->fields([
|
||||
'data' => serialize($display),
|
||||
'collection' => '',
|
||||
'name' => 'core.entity_view_display.node.article.teaser',
|
||||
])
|
||||
->condition('collection', '')
|
||||
->condition('name', 'core.entity_view_display.node.article.teaser')
|
||||
->execute();
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the Layout Builder UI.
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'layout_builder',
|
||||
'layout_test',
|
||||
'block',
|
||||
'node',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// @todo The Layout Builder UI relies on local tasks; fix in
|
||||
// https://www.drupal.org/project/drupal/issues/2917777.
|
||||
$this->drupalPlaceBlock('local_tasks_block');
|
||||
|
||||
// Create two nodes.
|
||||
$this->createContentType(['type' => 'bundle_with_section_field']);
|
||||
$this->createNode([
|
||||
'type' => 'bundle_with_section_field',
|
||||
'title' => 'The first node title',
|
||||
'body' => [
|
||||
[
|
||||
'value' => 'The first node body',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->createNode([
|
||||
'type' => 'bundle_with_section_field',
|
||||
'title' => 'The second node title',
|
||||
'body' => [
|
||||
[
|
||||
'value' => 'The second node body',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testLayoutBuilderUi() {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'configure any layout',
|
||||
'administer node display',
|
||||
'administer node fields',
|
||||
]));
|
||||
|
||||
$this->drupalGet('node/1');
|
||||
$assert_session->pageTextContains('The first node body');
|
||||
$assert_session->pageTextNotContains('Powered by Drupal');
|
||||
$assert_session->linkNotExists('Layout');
|
||||
|
||||
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
|
||||
|
||||
// From the manage display page, go to manage the layout.
|
||||
$this->drupalGet("$field_ui_prefix/display/default");
|
||||
$assert_session->linkExists('Manage layout');
|
||||
$this->clickLink('Manage layout');
|
||||
$assert_session->addressEquals("$field_ui_prefix/display-layout/default");
|
||||
// The body field is present.
|
||||
$assert_session->elementExists('css', '.field--name-body');
|
||||
|
||||
// Add a new block.
|
||||
$assert_session->linkExists('Add Block');
|
||||
$this->clickLink('Add Block');
|
||||
$assert_session->linkExists('Powered by Drupal');
|
||||
$this->clickLink('Powered by Drupal');
|
||||
$page->fillField('settings[label]', 'This is the label');
|
||||
$page->checkField('settings[label_display]');
|
||||
$page->pressButton('Add Block');
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
$assert_session->pageTextContains('This is the label');
|
||||
$assert_session->addressEquals("$field_ui_prefix/display-layout/default");
|
||||
|
||||
// Save the defaults.
|
||||
$assert_session->linkExists('Save Layout');
|
||||
$this->clickLink('Save Layout');
|
||||
$assert_session->addressEquals("$field_ui_prefix/display/default");
|
||||
|
||||
// The node uses the defaults, no overrides available.
|
||||
$this->drupalGet('node/1');
|
||||
$assert_session->pageTextContains('The first node body');
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
$assert_session->linkNotExists('Layout');
|
||||
|
||||
// Enable overrides.
|
||||
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
|
||||
$this->drupalGet('node/1');
|
||||
|
||||
// Remove the section from the defaults.
|
||||
$assert_session->linkExists('Layout');
|
||||
$this->clickLink('Layout');
|
||||
$assert_session->linkExists('Remove section');
|
||||
$this->clickLink('Remove section');
|
||||
$page->pressButton('Remove');
|
||||
|
||||
// Add a new section.
|
||||
$this->clickLink('Add Section');
|
||||
$assert_session->linkExists('Two column');
|
||||
$this->clickLink('Two column');
|
||||
$assert_session->linkExists('Save Layout');
|
||||
$this->clickLink('Save Layout');
|
||||
$assert_session->pageTextNotContains('The first node body');
|
||||
$assert_session->pageTextNotContains('Powered by Drupal');
|
||||
|
||||
// Assert that overrides cannot be turned off while overrides exist.
|
||||
$this->drupalGet("$field_ui_prefix/display/default");
|
||||
$assert_session->fieldDisabled('layout[allow_custom]');
|
||||
|
||||
// Alter the defaults.
|
||||
$this->drupalGet("$field_ui_prefix/display-layout/default");
|
||||
$assert_session->linkExists('Add Block');
|
||||
$this->clickLink('Add Block');
|
||||
$assert_session->linkExists('Title');
|
||||
$this->clickLink('Title');
|
||||
$page->pressButton('Add Block');
|
||||
// The title field is present.
|
||||
$assert_session->elementExists('css', '.field--name-title');
|
||||
$this->clickLink('Save Layout');
|
||||
|
||||
// View the other node, which is still using the defaults.
|
||||
$this->drupalGet('node/2');
|
||||
$assert_session->pageTextContains('The second node title');
|
||||
$assert_session->pageTextContains('The second node body');
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
|
||||
// The overridden node does not pick up the changes to defaults.
|
||||
$this->drupalGet('node/1');
|
||||
$assert_session->elementNotExists('css', '.field--name-title');
|
||||
$assert_session->pageTextNotContains('The first node body');
|
||||
$assert_session->pageTextNotContains('Powered by Drupal');
|
||||
$assert_session->linkExists('Layout');
|
||||
|
||||
// Reverting the override returns it to the defaults.
|
||||
$this->clickLink('Layout');
|
||||
$assert_session->linkExists('Revert to defaults');
|
||||
$this->clickLink('Revert to defaults');
|
||||
$page->pressButton('Revert');
|
||||
$assert_session->pageTextContains('The layout has been reverted back to defaults.');
|
||||
$assert_session->elementExists('css', '.field--name-title');
|
||||
$assert_session->pageTextContains('The first node body');
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
|
||||
// Assert that overrides can be turned off now that all overrides are gone.
|
||||
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => FALSE], 'Save');
|
||||
$this->drupalGet('node/1');
|
||||
$assert_session->linkNotExists('Layout');
|
||||
|
||||
// Add a new field.
|
||||
$edit = [
|
||||
'new_storage_type' => 'string',
|
||||
'label' => 'My text field',
|
||||
'field_name' => 'my_text',
|
||||
];
|
||||
$this->drupalPostForm("$field_ui_prefix/fields/add-field", $edit, 'Save and continue');
|
||||
$page->pressButton('Save field settings');
|
||||
$page->pressButton('Save settings');
|
||||
$this->drupalGet("$field_ui_prefix/display-layout/default");
|
||||
$assert_session->pageTextContains('My text field');
|
||||
$assert_session->elementExists('css', '.field--name-field-my-text');
|
||||
|
||||
// Delete the field.
|
||||
$this->drupalPostForm("$field_ui_prefix/fields/node.bundle_with_section_field.field_my_text/delete", [], 'Delete');
|
||||
$this->drupalGet("$field_ui_prefix/display-layout/default");
|
||||
$assert_session->pageTextNotContains('My text field');
|
||||
$assert_session->elementNotExists('css', '.field--name-field-my-text');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that component's dependencies are respected during removal.
|
||||
*/
|
||||
public function testPluginDependencies() {
|
||||
$assert_session = $this->assertSession();
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->container->get('module_installer')->install(['menu_ui']);
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'configure any layout',
|
||||
'administer node display',
|
||||
'administer menu',
|
||||
]));
|
||||
|
||||
// Create a new menu.
|
||||
$this->drupalGet('admin/structure/menu/add');
|
||||
$page->fillField('label', 'My Menu');
|
||||
$page->fillField('id', 'mymenu');
|
||||
$page->pressButton('Save');
|
||||
$this->drupalGet('admin/structure/menu/add');
|
||||
$page->fillField('label', 'My Menu');
|
||||
$page->fillField('id', 'myothermenu');
|
||||
$page->pressButton('Save');
|
||||
|
||||
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->linkExists('Add Section');
|
||||
$this->clickLink('Add Section');
|
||||
$assert_session->linkExists('Layout plugin (with dependencies)');
|
||||
$this->clickLink('Layout plugin (with dependencies)');
|
||||
$assert_session->elementExists('css', '.layout--layout-test-dependencies-plugin');
|
||||
$assert_session->elementExists('css', '.field--name-body');
|
||||
$assert_session->linkExists('Save Layout');
|
||||
$this->clickLink('Save Layout');
|
||||
$this->drupalPostForm('admin/structure/menu/manage/myothermenu/delete', [], 'Delete');
|
||||
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->elementNotExists('css', '.layout--layout-test-dependencies-plugin');
|
||||
$assert_session->elementExists('css', '.field--name-body');
|
||||
|
||||
// Add a menu block.
|
||||
$assert_session->linkExists('Add Block');
|
||||
$this->clickLink('Add Block');
|
||||
$assert_session->linkExists('My Menu');
|
||||
$this->clickLink('My Menu');
|
||||
$page->pressButton('Add Block');
|
||||
|
||||
// Add another block alongside the menu.
|
||||
$assert_session->linkExists('Add Block');
|
||||
$this->clickLink('Add Block');
|
||||
$assert_session->linkExists('Powered by Drupal');
|
||||
$this->clickLink('Powered by Drupal');
|
||||
$page->pressButton('Add Block');
|
||||
|
||||
// Assert that the blocks are visible, and save the layout.
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
$assert_session->pageTextContains('My Menu');
|
||||
$assert_session->elementExists('css', '.block.menu--mymenu');
|
||||
$assert_session->linkExists('Save Layout');
|
||||
$this->clickLink('Save Layout');
|
||||
|
||||
// Delete the menu.
|
||||
$this->drupalPostForm('admin/structure/menu/manage/mymenu/delete', [], 'Delete');
|
||||
|
||||
// Ensure that the menu block is gone, but that the other block remains.
|
||||
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->pageTextContains('Powered by Drupal');
|
||||
$assert_session->pageTextNotContains('My Menu');
|
||||
$assert_session->elementNotExists('css', '.block.menu--mymenu');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the rendering of a layout section field.
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutSectionTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
|
||||
|
||||
/**
|
||||
* The name of the layout section field.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName = 'layout_builder__layout';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->createContentType([
|
||||
'type' => 'bundle_without_section_field',
|
||||
]);
|
||||
$this->createContentType([
|
||||
'type' => 'bundle_with_section_field',
|
||||
]);
|
||||
|
||||
LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
|
||||
->setOverridable()
|
||||
->save();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'configure any layout',
|
||||
'administer node display',
|
||||
'administer node fields',
|
||||
'administer content types',
|
||||
], 'foobar'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for ::testLayoutSectionFormatter().
|
||||
*/
|
||||
public function providerTestLayoutSectionFormatter() {
|
||||
$data = [];
|
||||
$data['block_with_global_context'] = [
|
||||
[
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'test_context_aware',
|
||||
'context_mapping' => [
|
||||
'user' => '@user.current_user_context:current_user',
|
||||
],
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'.layout--onecol',
|
||||
'#test_context_aware--username',
|
||||
],
|
||||
[
|
||||
'foobar',
|
||||
],
|
||||
'user',
|
||||
'user:2',
|
||||
'UNCACHEABLE',
|
||||
];
|
||||
$data['block_with_entity_context'] = [
|
||||
[
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'field_block:node:bundle_with_section_field:body',
|
||||
'context_mapping' => [
|
||||
'entity' => 'layout_builder.entity',
|
||||
],
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'.layout--onecol',
|
||||
'.field--name-body',
|
||||
],
|
||||
[
|
||||
'Body',
|
||||
'The node body',
|
||||
],
|
||||
'',
|
||||
'',
|
||||
'MISS',
|
||||
];
|
||||
$data['single_section_single_block'] = [
|
||||
[
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'system_powered_by_block',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
'.layout--onecol',
|
||||
'Powered by',
|
||||
'',
|
||||
'',
|
||||
'MISS',
|
||||
];
|
||||
$data['multiple_sections'] = [
|
||||
[
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'system_powered_by_block',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
[
|
||||
'section' => new Section('layout_twocol', [], [
|
||||
'foo' => new SectionComponent('foo', 'first', [
|
||||
'id' => 'test_block_instantiation',
|
||||
'display_message' => 'foo text',
|
||||
]),
|
||||
'bar' => new SectionComponent('bar', 'second', [
|
||||
'id' => 'test_block_instantiation',
|
||||
'display_message' => 'bar text',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
'.layout--onecol',
|
||||
'.layout--twocol',
|
||||
],
|
||||
[
|
||||
'Powered by',
|
||||
'foo text',
|
||||
'bar text',
|
||||
],
|
||||
'user.permissions',
|
||||
'',
|
||||
'MISS',
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests layout_section formatter output.
|
||||
*
|
||||
* @dataProvider providerTestLayoutSectionFormatter
|
||||
*/
|
||||
public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) {
|
||||
$node = $this->createSectionNode($layout_data);
|
||||
|
||||
$canonical_url = $node->toUrl('canonical');
|
||||
$this->drupalGet($canonical_url);
|
||||
$this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache);
|
||||
|
||||
$this->drupalGet($canonical_url->toString() . '/layout');
|
||||
$this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the access checking of the section formatter.
|
||||
*/
|
||||
public function testLayoutSectionFormatterAccess() {
|
||||
$node = $this->createSectionNode([
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'test_access',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
]);
|
||||
|
||||
// Restrict access to the block.
|
||||
$this->container->get('state')->set('test_block_access', FALSE);
|
||||
|
||||
$this->drupalGet($node->toUrl('canonical'));
|
||||
$this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE');
|
||||
// Ensure the block was not rendered.
|
||||
$this->assertSession()->pageTextNotContains('Hello test world');
|
||||
|
||||
// Grant access to the block, and ensure it was rendered.
|
||||
$this->container->get('state')->set('test_block_access', TRUE);
|
||||
$this->drupalGet($node->toUrl('canonical'));
|
||||
$this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the multilingual support of the section formatter.
|
||||
*/
|
||||
public function testMultilingualLayoutSectionFormatter() {
|
||||
$this->container->get('module_installer')->install(['content_translation']);
|
||||
$this->rebuildContainer();
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('es')->save();
|
||||
$this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE);
|
||||
|
||||
$entity = $this->createSectionNode([
|
||||
[
|
||||
'section' => new Section('layout_onecol', [], [
|
||||
'baz' => new SectionComponent('baz', 'content', [
|
||||
'id' => 'system_powered_by_block',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
]);
|
||||
$entity->addTranslation('es', [
|
||||
'title' => 'Translated node title',
|
||||
$this->fieldName => [
|
||||
[
|
||||
'section' => new Section('layout_twocol', [], [
|
||||
'foo' => new SectionComponent('foo', 'first', [
|
||||
'id' => 'test_block_instantiation',
|
||||
'display_message' => 'foo text',
|
||||
]),
|
||||
'bar' => new SectionComponent('bar', 'second', [
|
||||
'id' => 'test_block_instantiation',
|
||||
'display_message' => 'bar text',
|
||||
]),
|
||||
]),
|
||||
],
|
||||
],
|
||||
]);
|
||||
$entity->save();
|
||||
|
||||
$this->drupalGet($entity->toUrl('canonical'));
|
||||
$this->assertLayoutSection('.layout--onecol', 'Powered by');
|
||||
$this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/'));
|
||||
$this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the entity title is displayed.
|
||||
*/
|
||||
public function testLayoutPageTitle() {
|
||||
$this->drupalPlaceBlock('page_title_block');
|
||||
$node = $this->createSectionNode([]);
|
||||
|
||||
$this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
|
||||
$this->assertSession()->titleEquals('Edit layout for The node title | Drupal');
|
||||
$this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that no Layout link shows without a section field.
|
||||
*/
|
||||
public function testLayoutUrlNoSectionField() {
|
||||
$node = $this->createNode([
|
||||
'type' => 'bundle_without_section_field',
|
||||
'title' => 'The node title',
|
||||
'body' => [
|
||||
[
|
||||
'value' => 'The node body',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$this->drupalGet($node->toUrl('canonical')->toString() . '/layout');
|
||||
$this->assertSession()->statusCodeEquals(404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that deleting a field removes it from the layout.
|
||||
*/
|
||||
public function testLayoutDeletingField() {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->statusCodeEquals(200);
|
||||
$assert_session->elementExists('css', '.field--name-body');
|
||||
|
||||
// Delete the field from both bundles.
|
||||
$this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
|
||||
$this->submitForm([], 'Delete');
|
||||
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->statusCodeEquals(200);
|
||||
$assert_session->elementExists('css', '.field--name-body');
|
||||
|
||||
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
|
||||
$this->submitForm([], 'Delete');
|
||||
$this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
|
||||
$assert_session->statusCodeEquals(200);
|
||||
$assert_session->elementNotExists('css', '.field--name-body');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that deleting a bundle removes the layout.
|
||||
*/
|
||||
public function testLayoutDeletingBundle() {
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
|
||||
$this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
|
||||
|
||||
$this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete');
|
||||
$assert_session->statusCodeEquals(200);
|
||||
|
||||
$display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
|
||||
$this->assertNull($display);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the output of a layout section.
|
||||
*
|
||||
* @param string|array $expected_selector
|
||||
* A selector or list of CSS selectors to find.
|
||||
* @param string|array $expected_content
|
||||
* A string or list of strings to find.
|
||||
* @param string $expected_cache_contexts
|
||||
* A string of cache contexts to be found in the header.
|
||||
* @param string $expected_cache_tags
|
||||
* A string of cache tags to be found in the header.
|
||||
* @param string $expected_dynamic_cache
|
||||
* The expected dynamic cache header. Either 'HIT', 'MISS' or 'UNCACHEABLE'.
|
||||
*/
|
||||
protected function assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts = '', $expected_cache_tags = '', $expected_dynamic_cache = 'MISS') {
|
||||
$assert_session = $this->assertSession();
|
||||
// Find the given selector.
|
||||
foreach ((array) $expected_selector as $selector) {
|
||||
$element = $this->cssSelect($selector);
|
||||
$this->assertNotEmpty($element);
|
||||
}
|
||||
|
||||
// Find the given content.
|
||||
foreach ((array) $expected_content as $content) {
|
||||
$assert_session->pageTextContains($content);
|
||||
}
|
||||
if ($expected_cache_contexts) {
|
||||
$assert_session->responseHeaderContains('X-Drupal-Cache-Contexts', $expected_cache_contexts);
|
||||
}
|
||||
if ($expected_cache_tags) {
|
||||
$assert_session->responseHeaderContains('X-Drupal-Cache-Tags', $expected_cache_tags);
|
||||
}
|
||||
$assert_session->responseHeaderEquals('X-Drupal-Dynamic-Cache', $expected_dynamic_cache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a node with a section field.
|
||||
*
|
||||
* @param array $section_values
|
||||
* An array of values for a section field.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface
|
||||
* The node object.
|
||||
*/
|
||||
protected function createSectionNode(array $section_values) {
|
||||
return $this->createNode([
|
||||
'type' => 'bundle_with_section_field',
|
||||
'title' => 'The node title',
|
||||
'body' => [
|
||||
[
|
||||
'value' => 'The node body',
|
||||
],
|
||||
],
|
||||
$this->fieldName => $section_values,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Functional\Update;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
|
||||
|
||||
/**
|
||||
* Tests the upgrade path for Layout Builder section dependencies.
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class SectionDependenciesUpdatePathTest extends UpdatePathTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setDatabaseDumpFiles() {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
|
||||
__DIR__ . '/../../../fixtures/update/section-dependencies.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the upgrade path for Layout Builder section dependencies.
|
||||
*/
|
||||
public function testRunUpdates() {
|
||||
$data = EntityViewDisplay::load('node.article.teaser')->toArray();
|
||||
$this->assertNotContains('system.menu.myothermenu', $data['dependencies']['config']);
|
||||
$this->assertNotContains('layout_builder', $data['dependencies']['module']);
|
||||
$this->assertNotContains('layout_test', $data['dependencies']['module']);
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
$data = EntityViewDisplay::load('node.article.teaser')->toArray();
|
||||
$this->assertContains('system.menu.myothermenu', $data['dependencies']['config']);
|
||||
$this->assertContains('layout_builder', $data['dependencies']['module']);
|
||||
$this->assertContains('layout_test', $data['dependencies']['module']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
|
||||
*
|
||||
* @group field
|
||||
*/
|
||||
class FieldBlockTest extends JavascriptTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'field_name' => 'field_date',
|
||||
'entity_type' => 'user',
|
||||
'type' => 'datetime',
|
||||
]);
|
||||
$field_storage->save();
|
||||
$field = FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'user',
|
||||
'label' => 'Date field',
|
||||
]);
|
||||
$field->save();
|
||||
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer blocks',
|
||||
'access administration pages',
|
||||
]);
|
||||
$user->field_date = '1978-11-19T05:00:00';
|
||||
$user->save();
|
||||
$this->drupalLogin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests configuring a field block for a user field.
|
||||
*/
|
||||
public function testFieldBlock() {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Assert that the field value is not displayed.
|
||||
$this->drupalGet('admin');
|
||||
$assert_session->pageTextNotContains('Sunday, November 19, 1978 - 16:00');
|
||||
|
||||
$this->drupalGet('admin/structure/block');
|
||||
$this->clickLink('Place block');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
|
||||
// Ensure that fields without any formatters are not available.
|
||||
$assert_session->pageTextNotContains('Password');
|
||||
// Ensure that non-display-configurable fields are not available.
|
||||
$assert_session->pageTextNotContains('Initial email');
|
||||
|
||||
$assert_session->pageTextContains('Date field');
|
||||
$block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
|
||||
$assert_session->linkByHrefExists($block_url);
|
||||
|
||||
$this->drupalGet($block_url);
|
||||
$page->fillField('region', 'content');
|
||||
|
||||
// Assert the default formatter configuration.
|
||||
$assert_session->fieldValueEquals('settings[formatter][type]', 'datetime_default');
|
||||
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
|
||||
|
||||
// Change the formatter.
|
||||
$page->selectFieldOption('settings[formatter][type]', 'datetime_time_ago');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
// Changing the formatter removes the old settings and introduces new ones.
|
||||
$assert_session->fieldNotExists('settings[formatter][settings][format_type]');
|
||||
$assert_session->fieldExists('settings[formatter][settings][granularity]');
|
||||
$page->pressButton('Save block');
|
||||
$assert_session->pageTextContains('The block configuration has been saved.');
|
||||
|
||||
// Configure the block and change the formatter again.
|
||||
$this->clickLink('Configure');
|
||||
$page->selectFieldOption('settings[formatter][type]', 'datetime_default');
|
||||
$assert_session->assertWaitOnAjaxRequest();
|
||||
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'medium');
|
||||
$page->selectFieldOption('settings[formatter][settings][format_type]', 'long');
|
||||
|
||||
$page->pressButton('Save block');
|
||||
$assert_session->pageTextContains('The block configuration has been saved.');
|
||||
|
||||
// Assert that the field value is updated.
|
||||
$this->clickLink('Configure');
|
||||
$assert_session->fieldValueEquals('settings[formatter][settings][format_type]', 'long');
|
||||
|
||||
// Assert that the field block is configured as expected.
|
||||
$expected = [
|
||||
'label' => 'above',
|
||||
'type' => 'datetime_default',
|
||||
'settings' => [
|
||||
'format_type' => 'long',
|
||||
'timezone_override' => '',
|
||||
],
|
||||
'third_party_settings' => [],
|
||||
];
|
||||
$config = $this->container->get('config.factory')->get('block.block.datefield');
|
||||
$this->assertEquals($expected, $config->get('settings.formatter'));
|
||||
$this->assertEquals(['field.field.user.user.field_date'], $config->get('dependencies.config'));
|
||||
|
||||
// Assert that the block is displaying the user field.
|
||||
$this->drupalGet('admin');
|
||||
$assert_session->pageTextContains('Sunday, November 19, 1978 - 16:00');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\FormatterPluginManager;
|
||||
use Drupal\Core\Plugin\Context\ContextDefinition;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\layout_builder\Plugin\Block\FieldBlock;
|
||||
use Prophecy\Prophecy\ProphecyInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
|
||||
* @group Field
|
||||
*/
|
||||
class FieldBlockTest extends EntityKernelTestBase {
|
||||
|
||||
/**
|
||||
* Tests entity access.
|
||||
*
|
||||
* @covers ::blockAccess
|
||||
* @dataProvider providerTestBlockAccessNotAllowed
|
||||
*/
|
||||
public function testBlockAccessEntityNotAllowed($expected, $entity_access) {
|
||||
$entity = $this->prophesize(FieldableEntityInterface::class);
|
||||
$block = $this->getTestBlock($entity);
|
||||
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$entity->access('view', $account->reveal(), TRUE)->willReturn($entity_access);
|
||||
$entity->hasField()->shouldNotBeCalled();
|
||||
|
||||
$access = $block->access($account->reveal(), TRUE);
|
||||
$this->assertSame($expected, $access->isAllowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for ::testBlockAccessEntityNotAllowed().
|
||||
*/
|
||||
public function providerTestBlockAccessNotAllowed() {
|
||||
$data = [];
|
||||
$data['entity_forbidden'] = [
|
||||
FALSE,
|
||||
AccessResult::forbidden(),
|
||||
];
|
||||
$data['entity_neutral'] = [
|
||||
FALSE,
|
||||
AccessResult::neutral(),
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests unfieldable entity.
|
||||
*
|
||||
* @covers ::blockAccess
|
||||
*/
|
||||
public function testBlockAccessEntityAllowedNotFieldable() {
|
||||
$entity = $this->prophesize(EntityInterface::class);
|
||||
$block = $this->getTestBlock($entity);
|
||||
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
|
||||
|
||||
$access = $block->access($account->reveal(), TRUE);
|
||||
$this->assertSame(FALSE, $access->isAllowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests fieldable entity without a particular field.
|
||||
*
|
||||
* @covers ::blockAccess
|
||||
*/
|
||||
public function testBlockAccessEntityAllowedNoField() {
|
||||
$entity = $this->prophesize(FieldableEntityInterface::class);
|
||||
$block = $this->getTestBlock($entity);
|
||||
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
|
||||
$entity->hasField('the_field_name')->willReturn(FALSE);
|
||||
$entity->get('the_field_name')->shouldNotBeCalled();
|
||||
|
||||
$access = $block->access($account->reveal(), TRUE);
|
||||
$this->assertSame(FALSE, $access->isAllowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests field access.
|
||||
*
|
||||
* @covers ::blockAccess
|
||||
* @dataProvider providerTestBlockAccessNotAllowed
|
||||
*/
|
||||
public function testBlockAccessEntityAllowedFieldNotAllowed($expected, $field_access) {
|
||||
$entity = $this->prophesize(FieldableEntityInterface::class);
|
||||
$block = $this->getTestBlock($entity);
|
||||
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
|
||||
$entity->hasField('the_field_name')->willReturn(TRUE);
|
||||
$field = $this->prophesize(FieldItemListInterface::class);
|
||||
$entity->get('the_field_name')->willReturn($field->reveal());
|
||||
|
||||
$field->access('view', $account->reveal(), TRUE)->willReturn($field_access);
|
||||
$field->isEmpty()->shouldNotBeCalled();
|
||||
|
||||
$access = $block->access($account->reveal(), TRUE);
|
||||
$this->assertSame($expected, $access->isAllowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests populated vs empty build.
|
||||
*
|
||||
* @covers ::blockAccess
|
||||
* @covers ::build
|
||||
* @dataProvider providerTestBlockAccessEntityAllowedFieldHasValue
|
||||
*/
|
||||
public function testBlockAccessEntityAllowedFieldHasValue($expected, $is_empty) {
|
||||
$entity = $this->prophesize(FieldableEntityInterface::class);
|
||||
$block = $this->getTestBlock($entity);
|
||||
|
||||
$account = $this->prophesize(AccountInterface::class);
|
||||
$entity->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
|
||||
$entity->hasField('the_field_name')->willReturn(TRUE);
|
||||
$field = $this->prophesize(FieldItemListInterface::class);
|
||||
$entity->get('the_field_name')->willReturn($field->reveal());
|
||||
|
||||
$field->access('view', $account->reveal(), TRUE)->willReturn(AccessResult::allowed());
|
||||
$field->isEmpty()->willReturn($is_empty)->shouldBeCalled();
|
||||
|
||||
$access = $block->access($account->reveal(), TRUE);
|
||||
$this->assertSame($expected, $access->isAllowed());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for ::testBlockAccessEntityAllowedFieldHasValue().
|
||||
*/
|
||||
public function providerTestBlockAccessEntityAllowedFieldHasValue() {
|
||||
$data = [];
|
||||
$data['empty'] = [
|
||||
FALSE,
|
||||
TRUE,
|
||||
];
|
||||
$data['populated'] = [
|
||||
TRUE,
|
||||
FALSE,
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a block for testing.
|
||||
*
|
||||
* @param \Prophecy\Prophecy\ProphecyInterface $entity_prophecy
|
||||
* An entity prophecy for use as an entity context value.
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param array $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
*
|
||||
* @return \Drupal\layout_builder\Plugin\Block\FieldBlock
|
||||
* The block to test.
|
||||
*/
|
||||
protected function getTestBlock(ProphecyInterface $entity_prophecy, array $configuration = [], array $plugin_definition = []) {
|
||||
$entity_prophecy->getCacheContexts()->willReturn([]);
|
||||
$entity_prophecy->getCacheTags()->willReturn([]);
|
||||
$entity_prophecy->getCacheMaxAge()->willReturn(0);
|
||||
|
||||
$plugin_definition += [
|
||||
'provider' => 'test',
|
||||
'default_formatter' => '',
|
||||
'category' => 'Test',
|
||||
'admin_label' => 'Test Block',
|
||||
'bundles' => ['entity_test'],
|
||||
'context' => [
|
||||
'entity' => new ContextDefinition('entity:entity_test', 'Test', TRUE),
|
||||
],
|
||||
];
|
||||
$entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class);
|
||||
$formatter_manager = $this->prophesize(FormatterPluginManager::class);
|
||||
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
|
||||
|
||||
$block = new FieldBlock(
|
||||
$configuration,
|
||||
'field_block:entity_test:entity_test:the_field_name',
|
||||
$plugin_definition,
|
||||
$entity_field_manager->reveal(),
|
||||
$formatter_manager->reveal(),
|
||||
$module_handler->reveal()
|
||||
);
|
||||
$block->setContextValue('entity', $entity_prophecy->reveal());
|
||||
return $block;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests Layout Builder's compatibility with existing systems.
|
||||
*/
|
||||
abstract class LayoutBuilderCompatibilityTestBase extends EntityKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'layout_discovery',
|
||||
];
|
||||
|
||||
/**
|
||||
* The entity view display.
|
||||
*
|
||||
* @var \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
|
||||
*/
|
||||
protected $display;
|
||||
|
||||
/**
|
||||
* The entity being rendered.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\FieldableEntityInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('entity_test_base_field_display');
|
||||
$this->installConfig(['filter']);
|
||||
$this->installSchema('system', ['key_value_expire']);
|
||||
|
||||
// Set up a non-admin user that is allowed to view test entities.
|
||||
\Drupal::currentUser()->setAccount($this->createUser(['uid' => 2], ['view test entity']));
|
||||
|
||||
\Drupal::service('theme_handler')->install(['classy']);
|
||||
$this->config('system.theme')->set('default', 'classy')->save();
|
||||
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'entity_type' => 'entity_test_base_field_display',
|
||||
'field_name' => 'test_field_display_configurable',
|
||||
'type' => 'boolean',
|
||||
]);
|
||||
$field_storage->save();
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'entity_test_base_field_display',
|
||||
'label' => 'FieldConfig with configurable display',
|
||||
])->save();
|
||||
|
||||
$this->display = EntityViewDisplay::create([
|
||||
'targetEntityType' => 'entity_test_base_field_display',
|
||||
'bundle' => 'entity_test_base_field_display',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
]);
|
||||
$this->display
|
||||
->setComponent('test_field_display_configurable', ['weight' => 5])
|
||||
->save();
|
||||
|
||||
// Create an entity with fields that are configurable and non-configurable.
|
||||
$entity_storage = $this->container->get('entity_type.manager')->getStorage('entity_test_base_field_display');
|
||||
// @todo Remove langcode workarounds after resolving
|
||||
// https://www.drupal.org/node/2915034.
|
||||
$this->entity = $entity_storage->createWithSampleValues('entity_test_base_field_display', [
|
||||
'langcode' => 'en',
|
||||
'langcode_default' => TRUE,
|
||||
]);
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs the Layout Builder.
|
||||
*
|
||||
* Also configures and reloads the entity display, and reloads the entity.
|
||||
*/
|
||||
protected function installLayoutBuilder() {
|
||||
$this->container->get('module_installer')->install(['layout_builder']);
|
||||
$this->refreshServices();
|
||||
|
||||
$this->display = $this->reloadEntity($this->display);
|
||||
$this->display->setOverridable()->save();
|
||||
$this->entity = $this->reloadEntity($this->entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the rendered entity has the correct fields.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity to render.
|
||||
* @param array $attributes
|
||||
* An array of field attributes to assert.
|
||||
*
|
||||
* @return string
|
||||
* The rendered string output (typically HTML).
|
||||
*/
|
||||
protected function assertFieldAttributes(EntityInterface $entity, array $attributes) {
|
||||
$view_builder = $this->container->get('entity_type.manager')->getViewBuilder($entity->getEntityTypeId());
|
||||
$build = $view_builder->view($entity);
|
||||
$this->render($build);
|
||||
|
||||
$actual = array_map(function (\SimpleXMLElement $element) {
|
||||
return (string) $element->attributes();
|
||||
}, $this->cssSelect('.field'));
|
||||
$this->assertSame($attributes, $actual);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\Core\Config\Schema\SchemaIncompleteException;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderEntityViewDisplayTest extends SectionStorageTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSectionStorage(array $section_data) {
|
||||
$display = LayoutBuilderEntityViewDisplay::create([
|
||||
'targetEntityType' => 'entity_test',
|
||||
'bundle' => 'entity_test',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
'third_party_settings' => [
|
||||
'layout_builder' => [
|
||||
'sections' => $section_data,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$display->save();
|
||||
return $display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that configuration schema enforces valid values.
|
||||
*/
|
||||
public function testInvalidConfiguration() {
|
||||
$this->setExpectedException(SchemaIncompleteException::class);
|
||||
$this->sectionStorage->getSection(0)->getComponent('first-uuid')->setConfiguration(['id' => 'foo', 'bar' => 'baz']);
|
||||
$this->sectionStorage->save();
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Ensures that Layout Builder and Field Layout are compatible with each other.
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderFieldLayoutCompatibilityTest extends LayoutBuilderCompatibilityTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'field_layout',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->display
|
||||
->setLayoutId('layout_twocol')
|
||||
->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the compatibility of Layout Builder and Field Layout.
|
||||
*/
|
||||
public function testCompatibility() {
|
||||
// Ensure that the configurable field is shown in the correct region and
|
||||
// that the non-configurable field is shown outside the layout.
|
||||
$expected_fields = [
|
||||
'field field--name-name field--type-string field--label-hidden field__item',
|
||||
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
|
||||
];
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
$this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-display-configurable'));
|
||||
$this->assertNotEmpty($this->cssSelect('.layout__region--first .field--name-test-field-display-configurable'));
|
||||
$this->assertNotEmpty($this->cssSelect('.field--name-test-display-non-configurable'));
|
||||
$this->assertEmpty($this->cssSelect('.layout__region .field--name-test-display-non-configurable'));
|
||||
|
||||
$this->installLayoutBuilder();
|
||||
|
||||
// Without using Layout Builder for an override, the result has not changed.
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
|
||||
// Add a layout override.
|
||||
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
|
||||
$field_list = $this->entity->get('layout_builder__layout');
|
||||
$field_list->appendSection(new Section('layout_onecol'));
|
||||
$this->entity->save();
|
||||
|
||||
// The rendered entity has now changed. The non-configurable field is shown
|
||||
// outside the layout, the configurable field is not shown at all, and the
|
||||
// layout itself is rendered (but empty).
|
||||
$new_expected_fields = [
|
||||
'field field--name-name field--type-string field--label-hidden field__item',
|
||||
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
|
||||
];
|
||||
$this->assertFieldAttributes($this->entity, $new_expected_fields);
|
||||
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
|
||||
|
||||
// Removing the layout restores the original rendering of the entity.
|
||||
$field_list->removeSection(0);
|
||||
$this->entity->save();
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\layout_builder\Section;
|
||||
|
||||
/**
|
||||
* Ensures that Layout Builder and core EntityViewDisplays are compatible.
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
|
||||
|
||||
/**
|
||||
* Tests the compatibility of Layout Builder with existing entity displays.
|
||||
*/
|
||||
public function testCompatibility() {
|
||||
// Ensure that the fields are shown.
|
||||
$expected_fields = [
|
||||
'field field--name-name field--type-string field--label-hidden field__item',
|
||||
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
|
||||
];
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
|
||||
$this->installLayoutBuilder();
|
||||
|
||||
// Without using Layout Builder for an override, the result has not changed.
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
|
||||
// Add a layout override.
|
||||
$this->entity->get('layout_builder__layout')->appendSection(new Section('layout_onecol'));
|
||||
$this->entity->save();
|
||||
|
||||
// The rendered entity has now changed. The non-configurable field is shown
|
||||
// outside the layout, the configurable field is not shown at all, and the
|
||||
// layout itself is rendered (but empty).
|
||||
$new_expected_fields = [
|
||||
'field field--name-name field--type-string field--label-hidden field__item',
|
||||
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
|
||||
];
|
||||
$this->assertFieldAttributes($this->entity, $new_expected_fields);
|
||||
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
|
||||
|
||||
// Removing the layout restores the original rendering of the entity.
|
||||
$this->entity->get('layout_builder__layout')->removeSection(0);
|
||||
$this->entity->save();
|
||||
$this->assertFieldAttributes($this->entity, $expected_fields);
|
||||
|
||||
// Test that adding a new field after Layout Builder has been installed will
|
||||
// add the new field to the default region of the first section.
|
||||
$field_storage = FieldStorageConfig::create([
|
||||
'entity_type' => 'entity_test_base_field_display',
|
||||
'field_name' => 'test_field_display_post_install',
|
||||
'type' => 'text',
|
||||
]);
|
||||
$field_storage->save();
|
||||
FieldConfig::create([
|
||||
'field_storage' => $field_storage,
|
||||
'bundle' => 'entity_test_base_field_display',
|
||||
'label' => 'FieldConfig with configurable display',
|
||||
])->save();
|
||||
|
||||
$this->entity = $this->reloadEntity($this->entity);
|
||||
$this->entity->test_field_display_post_install = 'Test string';
|
||||
$this->entity->save();
|
||||
|
||||
$this->display = $this->reloadEntity($this->display);
|
||||
$this->display
|
||||
->setComponent('test_field_display_post_install', ['weight' => 50])
|
||||
->save();
|
||||
$new_expected_fields = [
|
||||
'field field--name-name field--type-string field--label-hidden field__item',
|
||||
'field field--name-test-field-display-configurable field--type-boolean field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-field-display-post-install field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
|
||||
'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
|
||||
];
|
||||
$this->assertFieldAttributes($this->entity, $new_expected_fields);
|
||||
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
|
||||
$this->assertText('Test string');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
|
||||
|
||||
/**
|
||||
* Tests the field type for Layout Sections.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\layout_builder\Field\LayoutSectionItemList
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutSectionItemListTest extends SectionStorageTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'field',
|
||||
'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSectionStorage(array $section_data) {
|
||||
$this->installEntitySchema('entity_test_base_field_display');
|
||||
LayoutBuilderEntityViewDisplay::create([
|
||||
'targetEntityType' => 'entity_test_base_field_display',
|
||||
'bundle' => 'entity_test_base_field_display',
|
||||
'mode' => 'default',
|
||||
'status' => TRUE,
|
||||
])->setOverridable()->save();
|
||||
|
||||
array_map(function ($row) {
|
||||
return ['section' => $row];
|
||||
}, $section_data);
|
||||
$entity = EntityTestBaseFieldDisplay::create([
|
||||
'name' => 'The test entity',
|
||||
'layout_builder__layout' => $section_data,
|
||||
]);
|
||||
$entity->save();
|
||||
return $entity->get('layout_builder__layout');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Kernel;
|
||||
|
||||
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
|
||||
/**
|
||||
* Provides a base class for testing implementations of section storage.
|
||||
*/
|
||||
abstract class SectionStorageTestBase extends EntityKernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'layout_builder',
|
||||
'layout_discovery',
|
||||
'layout_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* The section storage implementation.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $sectionStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('system', ['key_value_expire']);
|
||||
|
||||
$section_data = [
|
||||
new Section('layout_test_plugin', [], [
|
||||
'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
|
||||
'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
];
|
||||
$this->sectionStorage = $this->getSectionStorage($section_data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the section storage entity.
|
||||
*
|
||||
* @param array $section_data
|
||||
* An array of section data.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The entity.
|
||||
*/
|
||||
abstract protected function getSectionStorage(array $section_data);
|
||||
|
||||
/**
|
||||
* @covers ::getSections
|
||||
*/
|
||||
public function testGetSections() {
|
||||
$expected = [
|
||||
new Section('layout_test_plugin', [], [
|
||||
'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
|
||||
'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
];
|
||||
$this->assertSections($expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSection
|
||||
*/
|
||||
public function testGetSection() {
|
||||
$this->assertInstanceOf(Section::class, $this->sectionStorage->getSection(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSection
|
||||
*/
|
||||
public function testGetSectionInvalidDelta() {
|
||||
$this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2"');
|
||||
$this->sectionStorage->getSection(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertSection
|
||||
*/
|
||||
public function testInsertSection() {
|
||||
$expected = [
|
||||
new Section('layout_test_plugin', [], [
|
||||
'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
new Section('setting_1'),
|
||||
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
|
||||
'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
];
|
||||
|
||||
$this->sectionStorage->insertSection(1, new Section('setting_1'));
|
||||
$this->assertSections($expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::appendSection
|
||||
*/
|
||||
public function testAppendSection() {
|
||||
$expected = [
|
||||
new Section('layout_test_plugin', [], [
|
||||
'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
|
||||
'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
new Section('foo'),
|
||||
];
|
||||
|
||||
$this->sectionStorage->appendSection(new Section('foo'));
|
||||
$this->assertSections($expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::removeSection
|
||||
*/
|
||||
public function testRemoveSection() {
|
||||
$expected = [
|
||||
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
|
||||
'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
|
||||
]),
|
||||
];
|
||||
|
||||
$this->sectionStorage->removeSection(0);
|
||||
$this->assertSections($expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the field list has the expected sections.
|
||||
*
|
||||
* @param \Drupal\layout_builder\Section[] $expected
|
||||
* The expected sections.
|
||||
*/
|
||||
protected function assertSections(array $expected) {
|
||||
$result = $this->sectionStorage->getSections();
|
||||
$this->assertEquals($expected, $result);
|
||||
$this->assertSame(array_keys($expected), array_keys($result));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
|
||||
use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray
|
||||
* @group layout_builder
|
||||
*/
|
||||
class BlockComponentRenderArrayTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* The block plugin manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->blockManager = $this->prophesize(BlockManagerInterface::class);
|
||||
$this->account = $this->prophesize(AccountInterface::class);
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('plugin.manager.block', $this->blockManager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onBuildRender
|
||||
*/
|
||||
public function testOnBuildRender() {
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$access_result = AccessResult::allowed();
|
||||
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn(['test']);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
$block->getConfiguration()->willReturn([]);
|
||||
$block->getPluginId()->willReturn('block_plugin_id');
|
||||
$block->getBaseId()->willReturn('block_plugin_id');
|
||||
$block->getDerivativeId()->willReturn(NULL);
|
||||
|
||||
$block_content = ['#markup' => 'The block content.'];
|
||||
$block->build()->willReturn($block_content);
|
||||
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
|
||||
|
||||
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
|
||||
$contexts = [];
|
||||
$in_preview = FALSE;
|
||||
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
|
||||
|
||||
$subscriber = new BlockComponentRenderArray($this->account->reveal());
|
||||
|
||||
$expected_build = [
|
||||
'#theme' => 'block',
|
||||
'#weight' => 0,
|
||||
'#configuration' => [],
|
||||
'#plugin_id' => 'block_plugin_id',
|
||||
'#base_plugin_id' => 'block_plugin_id',
|
||||
'#derivative_plugin_id' => NULL,
|
||||
'content' => $block_content,
|
||||
];
|
||||
|
||||
$expected_cache = $expected_build + [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['test'],
|
||||
'max-age' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
$subscriber->onBuildRender($event);
|
||||
$result = $event->getBuild();
|
||||
$this->assertEquals($expected_build, $result);
|
||||
$event->getCacheableMetadata()->applyTo($result);
|
||||
$this->assertEquals($expected_cache, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onBuildRender
|
||||
*/
|
||||
public function testOnBuildRenderDenied() {
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$access_result = AccessResult::forbidden();
|
||||
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
|
||||
$block->getCacheContexts()->shouldNotBeCalled();
|
||||
$block->getCacheTags()->shouldNotBeCalled();
|
||||
$block->getCacheMaxAge()->shouldNotBeCalled();
|
||||
$block->getConfiguration()->shouldNotBeCalled();
|
||||
$block->getPluginId()->shouldNotBeCalled();
|
||||
$block->getBaseId()->shouldNotBeCalled();
|
||||
$block->getDerivativeId()->shouldNotBeCalled();
|
||||
|
||||
$block_content = ['#markup' => 'The block content.'];
|
||||
$block->build()->willReturn($block_content);
|
||||
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
|
||||
|
||||
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
|
||||
$contexts = [];
|
||||
$in_preview = FALSE;
|
||||
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
|
||||
|
||||
$subscriber = new BlockComponentRenderArray($this->account->reveal());
|
||||
|
||||
$expected_build = [];
|
||||
$expected_cache = [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
$subscriber->onBuildRender($event);
|
||||
$result = $event->getBuild();
|
||||
$this->assertEquals($expected_build, $result);
|
||||
$event->getCacheableMetadata()->applyTo($result);
|
||||
$this->assertEquals($expected_cache, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onBuildRender
|
||||
*/
|
||||
public function testOnBuildRenderInPreview() {
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn(['test']);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
$block->getConfiguration()->willReturn([]);
|
||||
$block->getPluginId()->willReturn('block_plugin_id');
|
||||
$block->getBaseId()->willReturn('block_plugin_id');
|
||||
$block->getDerivativeId()->willReturn(NULL);
|
||||
|
||||
$block_content = ['#markup' => 'The block content.'];
|
||||
$block->build()->willReturn($block_content);
|
||||
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
|
||||
|
||||
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
|
||||
$contexts = [];
|
||||
$in_preview = TRUE;
|
||||
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
|
||||
|
||||
$subscriber = new BlockComponentRenderArray($this->account->reveal());
|
||||
|
||||
$expected_build = [
|
||||
'#theme' => 'block',
|
||||
'#weight' => 0,
|
||||
'#configuration' => [],
|
||||
'#plugin_id' => 'block_plugin_id',
|
||||
'#base_plugin_id' => 'block_plugin_id',
|
||||
'#derivative_plugin_id' => NULL,
|
||||
'content' => $block_content,
|
||||
];
|
||||
|
||||
$expected_cache = $expected_build + [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => ['test'],
|
||||
'max-age' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
$subscriber->onBuildRender($event);
|
||||
$result = $event->getBuild();
|
||||
$this->assertEquals($expected_build, $result);
|
||||
$event->getCacheableMetadata()->applyTo($result);
|
||||
$this->assertEquals($expected_cache, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onBuildRender
|
||||
*/
|
||||
public function testOnBuildRenderNoBlock() {
|
||||
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn(NULL);
|
||||
|
||||
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
|
||||
$contexts = [];
|
||||
$in_preview = FALSE;
|
||||
$event = new SectionComponentBuildRenderArrayEvent($component, $contexts, $in_preview);
|
||||
|
||||
$subscriber = new BlockComponentRenderArray($this->account->reveal());
|
||||
|
||||
$expected_build = [];
|
||||
$expected_cache = [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
$subscriber->onBuildRender($event);
|
||||
$result = $event->getBuild();
|
||||
$this->assertEquals($expected_build, $result);
|
||||
$event->getCacheableMetadata()->applyTo($result);
|
||||
$this->assertEquals($expected_cache, $result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityType;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class DefaultsSectionStorageTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The plugin.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* The entity manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$entity_type_bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
|
||||
$sample_entity_generator = $this->prophesize(LayoutBuilderSampleEntityGenerator::class);
|
||||
|
||||
$definition = new SectionStorageDefinition([
|
||||
'id' => 'defaults',
|
||||
'class' => DefaultsSectionStorage::class,
|
||||
]);
|
||||
$this->plugin = new DefaultsSectionStorage([], '', $definition, $this->entityTypeManager->reveal(), $entity_type_bundle_info->reveal(), $sample_entity_generator->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::extractIdFromRoute
|
||||
*
|
||||
* @dataProvider providerTestExtractIdFromRoute
|
||||
*/
|
||||
public function testExtractIdFromRoute($expected, $value, array $defaults) {
|
||||
$result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for ::testExtractIdFromRoute().
|
||||
*/
|
||||
public function providerTestExtractIdFromRoute() {
|
||||
$data = [];
|
||||
$data['with value'] = [
|
||||
'foo.bar.baz',
|
||||
'foo.bar.baz',
|
||||
[],
|
||||
];
|
||||
$data['empty value, without bundle'] = [
|
||||
'my_entity_type.bundle_name.default',
|
||||
'',
|
||||
[
|
||||
'entity_type_id' => 'my_entity_type',
|
||||
'view_mode_name' => 'default',
|
||||
'bundle_key' => 'my_bundle',
|
||||
'my_bundle' => 'bundle_name',
|
||||
],
|
||||
];
|
||||
$data['empty value, with bundle'] = [
|
||||
'my_entity_type.bundle_name.default',
|
||||
'',
|
||||
[
|
||||
'entity_type_id' => 'my_entity_type',
|
||||
'view_mode_name' => 'default',
|
||||
'bundle' => 'bundle_name',
|
||||
],
|
||||
];
|
||||
$data['without value, empty defaults'] = [
|
||||
NULL,
|
||||
'',
|
||||
[],
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSectionListFromId
|
||||
*
|
||||
* @dataProvider providerTestGetSectionListFromId
|
||||
*/
|
||||
public function testGetSectionListFromId($success, $expected_entity_id, $value) {
|
||||
if ($expected_entity_id) {
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$entity_storage->load($expected_entity_id)->willReturn('the_return_value');
|
||||
|
||||
$this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display']));
|
||||
$this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal());
|
||||
}
|
||||
else {
|
||||
$this->entityTypeManager->getDefinition('entity_view_display')->shouldNotBeCalled();
|
||||
$this->entityTypeManager->getStorage('entity_view_display')->shouldNotBeCalled();
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$this->setExpectedException(\InvalidArgumentException::class);
|
||||
}
|
||||
|
||||
$result = $this->plugin->getSectionListFromId($value);
|
||||
if ($success) {
|
||||
$this->assertEquals('the_return_value', $result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for ::testGetSectionListFromId().
|
||||
*/
|
||||
public function providerTestGetSectionListFromId() {
|
||||
$data = [];
|
||||
$data['with value'] = [
|
||||
TRUE,
|
||||
'foo.bar.baz',
|
||||
'foo.bar.baz',
|
||||
];
|
||||
$data['without value, empty defaults'] = [
|
||||
FALSE,
|
||||
NULL,
|
||||
'',
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSectionListFromId
|
||||
*/
|
||||
public function testGetSectionListFromIdCreate() {
|
||||
$expected = 'the_return_value';
|
||||
$value = 'foo.bar.baz';
|
||||
$expected_create_values = [
|
||||
'targetEntityType' => 'foo',
|
||||
'bundle' => 'bar',
|
||||
'mode' => 'baz',
|
||||
'status' => TRUE,
|
||||
];
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
$entity_storage->load($value)->willReturn(NULL);
|
||||
$entity_storage->create($expected_create_values)->willReturn($expected);
|
||||
|
||||
$this->entityTypeManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display']));
|
||||
$this->entityTypeManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal());
|
||||
|
||||
$result = $this->plugin->getSectionListFromId($value);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::buildRoutes
|
||||
* @covers ::getEntityTypes
|
||||
*/
|
||||
public function testBuildRoutes() {
|
||||
$entity_types = [];
|
||||
|
||||
$not_fieldable = $this->prophesize(EntityTypeInterface::class);
|
||||
$not_fieldable->entityClassImplements(FieldableEntityInterface::class)->willReturn(FALSE);
|
||||
$entity_types['not_fieldable'] = $not_fieldable->reveal();
|
||||
|
||||
$no_view_builder = $this->prophesize(EntityTypeInterface::class);
|
||||
$no_view_builder->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$no_view_builder->hasViewBuilderClass()->willReturn(FALSE);
|
||||
$entity_types['no_view_builder'] = $no_view_builder->reveal();
|
||||
|
||||
$no_field_ui_route = $this->prophesize(EntityTypeInterface::class);
|
||||
$no_field_ui_route->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$no_field_ui_route->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$no_field_ui_route->get('field_ui_base_route')->willReturn(NULL);
|
||||
$entity_types['no_field_ui_route'] = $no_field_ui_route->reveal();
|
||||
|
||||
$unknown_field_ui_route = $this->prophesize(EntityTypeInterface::class);
|
||||
$unknown_field_ui_route->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$unknown_field_ui_route->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$unknown_field_ui_route->get('field_ui_base_route')->willReturn('unknown');
|
||||
$entity_types['unknown_field_ui_route'] = $unknown_field_ui_route->reveal();
|
||||
|
||||
$with_bundle_key = $this->prophesize(EntityTypeInterface::class);
|
||||
$with_bundle_key->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$with_bundle_key->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$with_bundle_key->get('field_ui_base_route')->willReturn('known');
|
||||
$with_bundle_key->hasKey('bundle')->willReturn(TRUE);
|
||||
$with_bundle_key->getBundleEntityType()->willReturn('my_bundle_type');
|
||||
$entity_types['with_bundle_key'] = $with_bundle_key->reveal();
|
||||
|
||||
$with_bundle_parameter = $this->prophesize(EntityTypeInterface::class);
|
||||
$with_bundle_parameter->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$with_bundle_parameter->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$with_bundle_parameter->get('field_ui_base_route')->willReturn('with_bundle');
|
||||
$entity_types['with_bundle_parameter'] = $with_bundle_parameter->reveal();
|
||||
$this->entityTypeManager->getDefinitions()->willReturn($entity_types);
|
||||
|
||||
$expected = [
|
||||
'known' => new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]),
|
||||
'with_bundle' => new Route('/admin/entity/{bundle}'),
|
||||
'layout_builder.defaults.with_bundle_key.view' => new Route(
|
||||
'/admin/entity/whatever/display-layout/{view_mode_name}',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_key',
|
||||
'bundle_key' => 'my_bundle_type',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'is_rebuilding' => FALSE,
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
|
||||
'_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_key display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
'layout_builder.defaults.with_bundle_key.save' => new Route(
|
||||
'/admin/entity/whatever/display-layout/{view_mode_name}/save',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_key',
|
||||
'bundle_key' => 'my_bundle_type',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_key display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
'layout_builder.defaults.with_bundle_key.cancel' => new Route(
|
||||
'/admin/entity/whatever/display-layout/{view_mode_name}/cancel',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_key',
|
||||
'bundle_key' => 'my_bundle_type',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_key display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
'layout_builder.defaults.with_bundle_parameter.view' => new Route(
|
||||
'/admin/entity/{bundle}/display-layout/{view_mode_name}',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_parameter',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'is_rebuilding' => FALSE,
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
|
||||
'_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
'layout_builder.defaults.with_bundle_parameter.save' => new Route(
|
||||
'/admin/entity/{bundle}/display-layout/{view_mode_name}/save',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_parameter',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
'layout_builder.defaults.with_bundle_parameter.cancel' => new Route(
|
||||
'/admin/entity/{bundle}/display-layout/{view_mode_name}/cancel',
|
||||
[
|
||||
'entity_type_id' => 'with_bundle_parameter',
|
||||
'section_storage_type' => 'defaults',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
|
||||
],
|
||||
[
|
||||
'_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
'_admin_route' => FALSE,
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
$collection = new RouteCollection();
|
||||
$collection->add('known', new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]));
|
||||
$collection->add('with_bundle', new Route('/admin/entity/{bundle}'));
|
||||
|
||||
$this->plugin->buildRoutes($collection);
|
||||
$this->assertEquals($expected, $collection->all());
|
||||
$this->assertSame(array_keys($expected), array_keys($collection->all()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderRouteEnhancerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::enhance
|
||||
*/
|
||||
public function testEnhanceValidDefaults() {
|
||||
$route = new Route('/the/path', [], [], ['_layout_builder' => TRUE]);
|
||||
$route_enhancer = new LayoutBuilderRouteEnhancer();
|
||||
$defaults = [
|
||||
RouteObjectInterface::ROUTE_OBJECT => $route,
|
||||
];
|
||||
// Ensure that the 'section_storage' key now contains the value stored for a
|
||||
// given entity type.
|
||||
$expected = [
|
||||
RouteObjectInterface::ROUTE_OBJECT => $route,
|
||||
'is_rebuilding' => TRUE,
|
||||
];
|
||||
$result = $route_enhancer->enhance($defaults, new Request(['layout_is_rebuilding' => TRUE]));
|
||||
$this->assertEquals($expected, $result);
|
||||
|
||||
$expected['is_rebuilding'] = FALSE;
|
||||
$result = $route_enhancer->enhance($defaults, new Request());
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\Routing\RouteBuildEvent;
|
||||
use Drupal\layout_builder\Routing\LayoutBuilderRoutes;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Routing\LayoutBuilderRoutes
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutBuilderRoutesTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The Layout Builder route builder.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
|
||||
*/
|
||||
protected $sectionStorageManager;
|
||||
|
||||
/**
|
||||
* The Layout Builder route builder.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Routing\LayoutBuilderRoutes
|
||||
*/
|
||||
protected $routeBuilder;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->sectionStorageManager = $this->prophesize(SectionStorageManagerInterface::class);
|
||||
$this->routeBuilder = new LayoutBuilderRoutes($this->sectionStorageManager->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onAlterRoutes
|
||||
*/
|
||||
public function testOnAlterRoutes() {
|
||||
$expected = [
|
||||
'test_route1' => new Route('/test/path1'),
|
||||
'test_route_shared' => new Route('/test/path/shared2'),
|
||||
'test_route2' => new Route('/test/path2'),
|
||||
];
|
||||
|
||||
$section_storage_first = $this->prophesize(SectionStorageInterface::class);
|
||||
$section_storage_first->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) {
|
||||
/** @var \Symfony\Component\Routing\RouteCollection $collection */
|
||||
$collection = $args[0];
|
||||
$collection->add('test_route_shared', new Route('/test/path/shared1'));
|
||||
$collection->add('test_route1', new Route('/test/path1'));
|
||||
});
|
||||
$section_storage_second = $this->prophesize(SectionStorageInterface::class);
|
||||
$section_storage_second->buildRoutes(Argument::type(RouteCollection::class))->shouldBeCalled()->will(function ($args) {
|
||||
/** @var \Symfony\Component\Routing\RouteCollection $collection */
|
||||
$collection = $args[0];
|
||||
$collection->add('test_route_shared', new Route('/test/path/shared2'));
|
||||
$collection->add('test_route2', new Route('/test/path2'));
|
||||
});
|
||||
|
||||
$this->sectionStorageManager->loadEmpty('first')->willReturn($section_storage_first->reveal());
|
||||
$this->sectionStorageManager->loadEmpty('second')->willReturn($section_storage_second->reveal());
|
||||
$definitions['first'] = new SectionStorageDefinition();
|
||||
$definitions['second'] = new SectionStorageDefinition();
|
||||
$this->sectionStorageManager->getDefinitions()->willReturn($definitions);
|
||||
|
||||
$collection = new RouteCollection();
|
||||
$event = new RouteBuildEvent($collection);
|
||||
$this->routeBuilder->onAlterRoutes($event);
|
||||
$this->assertEquals($expected, $collection->all());
|
||||
$this->assertSame(array_keys($expected), array_keys($collection->all()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
|
||||
use Drupal\layout_builder\Routing\LayoutTempstoreParamConverter;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Routing\LayoutTempstoreParamConverter
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutTempstoreParamConverterTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::convert
|
||||
*/
|
||||
public function testConvert() {
|
||||
$layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class);
|
||||
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
|
||||
$converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal());
|
||||
|
||||
$section_storage = $this->prophesize(SectionStorageInterface::class);
|
||||
|
||||
$value = 'some_value';
|
||||
$definition = ['layout_builder_tempstore' => TRUE];
|
||||
$name = 'the_parameter_name';
|
||||
$defaults = ['section_storage_type' => 'my_type'];
|
||||
$expected = 'the_return_value';
|
||||
|
||||
$section_storage_manager->hasDefinition('my_type')->willReturn(TRUE);
|
||||
$section_storage_manager->loadFromRoute('my_type', $value, $definition, $name, $defaults)->willReturn($section_storage);
|
||||
|
||||
$layout_tempstore_repository->get($section_storage->reveal())->willReturn($expected);
|
||||
|
||||
$result = $converter->convert($value, $definition, $name, $defaults);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::convert
|
||||
*/
|
||||
public function testConvertNoType() {
|
||||
$layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class);
|
||||
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
|
||||
$converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal());
|
||||
|
||||
$value = 'some_value';
|
||||
$definition = ['layout_builder_tempstore' => TRUE];
|
||||
$name = 'the_parameter_name';
|
||||
$defaults = ['section_storage_type' => NULL];
|
||||
|
||||
$section_storage_manager->hasDefinition()->shouldNotBeCalled();
|
||||
$section_storage_manager->loadFromRoute()->shouldNotBeCalled();
|
||||
$layout_tempstore_repository->get()->shouldNotBeCalled();
|
||||
|
||||
$result = $converter->convert($value, $definition, $name, $defaults);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::convert
|
||||
*/
|
||||
public function testConvertInvalidConverter() {
|
||||
$layout_tempstore_repository = $this->prophesize(LayoutTempstoreRepositoryInterface::class);
|
||||
$section_storage_manager = $this->prophesize(SectionStorageManagerInterface::class);
|
||||
$converter = new LayoutTempstoreParamConverter($layout_tempstore_repository->reveal(), $section_storage_manager->reveal());
|
||||
|
||||
$value = 'some_value';
|
||||
$definition = ['layout_builder_tempstore' => TRUE];
|
||||
$name = 'the_parameter_name';
|
||||
$defaults = ['section_storage_type' => 'invalid'];
|
||||
|
||||
$section_storage_manager->hasDefinition('invalid')->willReturn(FALSE);
|
||||
$section_storage_manager->loadFromRoute()->shouldNotBeCalled();
|
||||
$layout_tempstore_repository->get()->shouldNotBeCalled();
|
||||
|
||||
$result = $converter->convert($value, $definition, $name, $defaults);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\TempStore\SharedTempStore;
|
||||
use Drupal\Core\TempStore\SharedTempStoreFactory;
|
||||
use Drupal\layout_builder\LayoutTempstoreRepository;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\LayoutTempstoreRepository
|
||||
* @group layout_builder
|
||||
*/
|
||||
class LayoutTempstoreRepositoryTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::get
|
||||
*/
|
||||
public function testGetEmptyTempstore() {
|
||||
$section_storage = $this->prophesize(SectionStorageInterface::class);
|
||||
$section_storage->getStorageType()->willReturn('my_storage_type');
|
||||
$section_storage->getStorageId()->willReturn('my_storage_id');
|
||||
|
||||
$tempstore = $this->prophesize(SharedTempStore::class);
|
||||
$tempstore->get('my_storage_id')->shouldBeCalled();
|
||||
|
||||
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
|
||||
$tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
|
||||
|
||||
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
|
||||
|
||||
$result = $repository->get($section_storage->reveal());
|
||||
$this->assertSame($section_storage->reveal(), $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::get
|
||||
*/
|
||||
public function testGetLoadedTempstore() {
|
||||
$section_storage = $this->prophesize(SectionStorageInterface::class);
|
||||
$section_storage->getStorageType()->willReturn('my_storage_type');
|
||||
$section_storage->getStorageId()->willReturn('my_storage_id');
|
||||
|
||||
$tempstore_section_storage = $this->prophesize(SectionStorageInterface::class);
|
||||
$tempstore = $this->prophesize(SharedTempStore::class);
|
||||
$tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage->reveal()]);
|
||||
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
|
||||
$tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
|
||||
|
||||
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
|
||||
|
||||
$result = $repository->get($section_storage->reveal());
|
||||
$this->assertSame($tempstore_section_storage->reveal(), $result);
|
||||
$this->assertNotSame($section_storage->reveal(), $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::get
|
||||
*/
|
||||
public function testGetInvalidEntry() {
|
||||
$section_storage = $this->prophesize(SectionStorageInterface::class);
|
||||
$section_storage->getStorageType()->willReturn('my_storage_type');
|
||||
$section_storage->getStorageId()->willReturn('my_storage_id');
|
||||
|
||||
$tempstore = $this->prophesize(SharedTempStore::class);
|
||||
$tempstore->get('my_storage_id')->willReturn(['section_storage' => 'this_is_not_an_entity']);
|
||||
|
||||
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
|
||||
$tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
|
||||
|
||||
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
|
||||
|
||||
$this->setExpectedException(\UnexpectedValueException::class, 'The entry with storage type "my_storage_type" and ID "my_storage_id" is invalid');
|
||||
$repository->get($section_storage->reveal());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageDefinition;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class OverridesSectionStorageTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The plugin.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
|
||||
$this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
|
||||
|
||||
$definition = new SectionStorageDefinition([
|
||||
'id' => 'overrides',
|
||||
'class' => OverridesSectionStorage::class,
|
||||
]);
|
||||
$this->plugin = new OverridesSectionStorage([], 'overrides', $definition, $this->entityTypeManager->reveal(), $this->entityFieldManager->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::extractIdFromRoute
|
||||
*
|
||||
* @dataProvider providerTestExtractIdFromRoute
|
||||
*/
|
||||
public function testExtractIdFromRoute($expected, $value, array $defaults) {
|
||||
$result = $this->plugin->extractIdFromRoute($value, [], 'the_parameter_name', $defaults);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for ::testExtractIdFromRoute().
|
||||
*/
|
||||
public function providerTestExtractIdFromRoute() {
|
||||
$data = [];
|
||||
$data['with value, with layout'] = [
|
||||
'my_entity_type.entity_with_layout',
|
||||
'my_entity_type.entity_with_layout',
|
||||
[],
|
||||
];
|
||||
$data['with value, without layout'] = [
|
||||
NULL,
|
||||
'my_entity_type',
|
||||
[],
|
||||
];
|
||||
$data['empty value, populated defaults'] = [
|
||||
'my_entity_type.entity_with_layout',
|
||||
'',
|
||||
[
|
||||
'entity_type_id' => 'my_entity_type',
|
||||
'my_entity_type' => 'entity_with_layout',
|
||||
],
|
||||
];
|
||||
$data['empty value, empty defaults'] = [
|
||||
NULL,
|
||||
'',
|
||||
[],
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getSectionListFromId
|
||||
*
|
||||
* @dataProvider providerTestGetSectionListFromId
|
||||
*/
|
||||
public function testGetSectionListFromId($success, $expected_entity_type_id, $id) {
|
||||
$defaults['the_parameter_name'] = $id;
|
||||
|
||||
if ($expected_entity_type_id) {
|
||||
$entity_storage = $this->prophesize(EntityStorageInterface::class);
|
||||
|
||||
$entity_without_layout = $this->prophesize(FieldableEntityInterface::class);
|
||||
$entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE);
|
||||
$entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled();
|
||||
$entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal());
|
||||
|
||||
$entity_with_layout = $this->prophesize(FieldableEntityInterface::class);
|
||||
$entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE);
|
||||
$entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value');
|
||||
$entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal());
|
||||
|
||||
$this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal());
|
||||
}
|
||||
else {
|
||||
$this->entityTypeManager->getStorage(Argument::any())->shouldNotBeCalled();
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
$this->setExpectedException(\InvalidArgumentException::class);
|
||||
}
|
||||
|
||||
$result = $this->plugin->getSectionListFromId($id);
|
||||
if ($success) {
|
||||
$this->assertEquals('the_return_value', $result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for ::testGetSectionListFromId().
|
||||
*/
|
||||
public function providerTestGetSectionListFromId() {
|
||||
$data = [];
|
||||
$data['with value, with layout'] = [
|
||||
TRUE,
|
||||
'my_entity_type',
|
||||
'my_entity_type.entity_with_layout',
|
||||
];
|
||||
$data['with value, without layout'] = [
|
||||
FALSE,
|
||||
'my_entity_type',
|
||||
'my_entity_type.entity_without_layout',
|
||||
];
|
||||
$data['empty value, empty defaults'] = [
|
||||
FALSE,
|
||||
NULL,
|
||||
'',
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::buildRoutes
|
||||
* @covers ::hasIntegerId
|
||||
* @covers ::getEntityTypes
|
||||
*/
|
||||
public function testBuildRoutes() {
|
||||
$entity_types = [];
|
||||
|
||||
$not_fieldable = $this->prophesize(EntityTypeInterface::class);
|
||||
$not_fieldable->entityClassImplements(FieldableEntityInterface::class)->willReturn(FALSE);
|
||||
$entity_types['not_fieldable'] = $not_fieldable->reveal();
|
||||
|
||||
$no_view_builder = $this->prophesize(EntityTypeInterface::class);
|
||||
$no_view_builder->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$no_view_builder->hasViewBuilderClass()->willReturn(FALSE);
|
||||
$entity_types['no_view_builder'] = $no_view_builder->reveal();
|
||||
|
||||
$no_canonical_link = $this->prophesize(EntityTypeInterface::class);
|
||||
$no_canonical_link->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$no_canonical_link->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$no_canonical_link->hasLinkTemplate('canonical')->willReturn(FALSE);
|
||||
$entity_types['no_canonical_link'] = $no_canonical_link->reveal();
|
||||
$this->entityFieldManager->getFieldStorageDefinitions('no_canonical_link')->shouldNotBeCalled();
|
||||
|
||||
$with_string_id = $this->prophesize(EntityTypeInterface::class);
|
||||
$with_string_id->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$with_string_id->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$with_string_id->hasLinkTemplate('canonical')->willReturn(TRUE);
|
||||
$with_string_id->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
|
||||
$with_string_id->id()->willReturn('with_string_id');
|
||||
$with_string_id->getKey('id')->willReturn('id');
|
||||
$entity_types['with_string_id'] = $with_string_id->reveal();
|
||||
$string_id = $this->prophesize(FieldStorageDefinitionInterface::class);
|
||||
$string_id->getType()->willReturn('string');
|
||||
$this->entityFieldManager->getFieldStorageDefinitions('with_string_id')->willReturn(['id' => $string_id->reveal()]);
|
||||
|
||||
$with_integer_id = $this->prophesize(EntityTypeInterface::class);
|
||||
$with_integer_id->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
|
||||
$with_integer_id->hasViewBuilderClass()->willReturn(TRUE);
|
||||
$with_integer_id->hasLinkTemplate('canonical')->willReturn(TRUE);
|
||||
$with_integer_id->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
|
||||
$with_integer_id->id()->willReturn('with_integer_id');
|
||||
$with_integer_id->getKey('id')->willReturn('id');
|
||||
$entity_types['with_integer_id'] = $with_integer_id->reveal();
|
||||
$integer_id = $this->prophesize(FieldStorageDefinitionInterface::class);
|
||||
$integer_id->getType()->willReturn('integer');
|
||||
$this->entityFieldManager->getFieldStorageDefinitions('with_integer_id')->willReturn(['id' => $integer_id->reveal()]);
|
||||
|
||||
$this->entityTypeManager->getDefinitions()->willReturn($entity_types);
|
||||
|
||||
$expected = [
|
||||
'layout_builder.overrides.with_string_id.view' => new Route(
|
||||
'/entity/{entity}/layout',
|
||||
[
|
||||
'entity_type_id' => 'with_string_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'is_rebuilding' => FALSE,
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
|
||||
'_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_string_id' => ['type' => 'entity:with_string_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_string_id.save' => new Route(
|
||||
'/entity/{entity}/layout/save',
|
||||
[
|
||||
'entity_type_id' => 'with_string_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_string_id' => ['type' => 'entity:with_string_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_string_id.cancel' => new Route(
|
||||
'/entity/{entity}/layout/cancel',
|
||||
[
|
||||
'entity_type_id' => 'with_string_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_string_id' => ['type' => 'entity:with_string_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_string_id.revert' => new Route(
|
||||
'/entity/{entity}/layout/revert',
|
||||
[
|
||||
'entity_type_id' => 'with_string_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_string_id' => ['type' => 'entity:with_string_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_integer_id.view' => new Route(
|
||||
'/entity/{entity}/layout',
|
||||
[
|
||||
'entity_type_id' => 'with_integer_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'is_rebuilding' => FALSE,
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
|
||||
'_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
'with_integer_id' => '\d+',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_integer_id' => ['type' => 'entity:with_integer_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_integer_id.save' => new Route(
|
||||
'/entity/{entity}/layout/save',
|
||||
[
|
||||
'entity_type_id' => 'with_integer_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
'with_integer_id' => '\d+',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_integer_id' => ['type' => 'entity:with_integer_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_integer_id.cancel' => new Route(
|
||||
'/entity/{entity}/layout/cancel',
|
||||
[
|
||||
'entity_type_id' => 'with_integer_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
'with_integer_id' => '\d+',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_integer_id' => ['type' => 'entity:with_integer_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
'layout_builder.overrides.with_integer_id.revert' => new Route(
|
||||
'/entity/{entity}/layout/revert',
|
||||
[
|
||||
'entity_type_id' => 'with_integer_id',
|
||||
'section_storage_type' => 'overrides',
|
||||
'section_storage' => '',
|
||||
'_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
|
||||
],
|
||||
[
|
||||
'_has_layout_section' => 'true',
|
||||
'with_integer_id' => '\d+',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
'section_storage' => ['layout_builder_tempstore' => TRUE],
|
||||
'with_integer_id' => ['type' => 'entity:with_integer_id'],
|
||||
],
|
||||
'_layout_builder' => TRUE,
|
||||
]
|
||||
),
|
||||
];
|
||||
|
||||
$collection = new RouteCollection();
|
||||
$this->plugin->buildRoutes($collection);
|
||||
$this->assertEquals($expected, $collection->all());
|
||||
$this->assertSame(array_keys($expected), array_keys($collection->all()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Layout\LayoutInterface;
|
||||
use Drupal\Core\Layout\LayoutPluginManagerInterface;
|
||||
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
|
||||
use Drupal\layout_builder\LayoutBuilderEvents;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\SectionComponent
|
||||
* @group layout_builder
|
||||
*/
|
||||
class SectionComponentTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArray() {
|
||||
$existing_block = $this->prophesize(BlockPluginInterface::class);
|
||||
$existing_block->getPluginId()->willReturn('block_plugin_id');
|
||||
|
||||
$block_manager = $this->prophesize(BlockManagerInterface::class);
|
||||
$block_manager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($existing_block->reveal());
|
||||
|
||||
// Imitate an event subscriber by setting a resulting build on the event.
|
||||
$event_dispatcher = $this->prophesize(EventDispatcherInterface::class);
|
||||
$event_dispatcher
|
||||
->dispatch(LayoutBuilderEvents::SECTION_COMPONENT_BUILD_RENDER_ARRAY, Argument::type(SectionComponentBuildRenderArrayEvent::class))
|
||||
->shouldBeCalled()
|
||||
->will(function ($args) {
|
||||
/** @var \Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent $event */
|
||||
$event = $args[1];
|
||||
$event->setBuild(['#markup' => $event->getPlugin()->getPluginId()]);
|
||||
return;
|
||||
});
|
||||
|
||||
$layout_plugin = $this->prophesize(LayoutInterface::class);
|
||||
$layout_plugin->build(Argument::type('array'))->willReturnArgument(0);
|
||||
|
||||
$layout_manager = $this->prophesize(LayoutPluginManagerInterface::class);
|
||||
$layout_manager->createInstance('layout_onecol', [])->willReturn($layout_plugin->reveal());
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('plugin.manager.block', $block_manager->reveal());
|
||||
$container->set('event_dispatcher', $event_dispatcher->reveal());
|
||||
$container->set('plugin.manager.core.layout', $layout_manager->reveal());
|
||||
\Drupal::setContainer($container);
|
||||
|
||||
$expected = [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
'#markup' => 'block_plugin_id',
|
||||
];
|
||||
|
||||
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
|
||||
$result = $component->toRenderArray();
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
|
||||
use Drupal\Component\Plugin\Exception\PluginException;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Block\BlockManagerInterface;
|
||||
use Drupal\Core\Block\BlockPluginInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\Layout\LayoutDefinition;
|
||||
use Drupal\Core\Layout\LayoutInterface;
|
||||
use Drupal\Core\Layout\LayoutPluginManagerInterface;
|
||||
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
|
||||
use Drupal\Core\Plugin\Context\ContextRepositoryInterface;
|
||||
use Drupal\Core\Plugin\ContextAwarePluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray;
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Prophecy\Argument;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Section
|
||||
* @group layout_builder
|
||||
*/
|
||||
class SectionRenderTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $account;
|
||||
|
||||
/**
|
||||
* The block plugin manager.
|
||||
*
|
||||
* @var \Drupal\Core\Block\BlockManagerInterface
|
||||
*/
|
||||
protected $blockManager;
|
||||
|
||||
/**
|
||||
* The plugin context handler.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\Context\ContextHandlerInterface
|
||||
*/
|
||||
protected $contextHandler;
|
||||
|
||||
/**
|
||||
* The context manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\Context\ContextRepositoryInterface
|
||||
*/
|
||||
protected $contextRepository;
|
||||
|
||||
/**
|
||||
* The event dispatcher.
|
||||
*
|
||||
* @var \Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher
|
||||
*/
|
||||
protected $eventDispatcher;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$layout_plugin_manager = $this->prophesize(LayoutPluginManagerInterface::class);
|
||||
$this->blockManager = $this->prophesize(BlockManagerInterface::class);
|
||||
$this->contextHandler = $this->prophesize(ContextHandlerInterface::class);
|
||||
$this->contextRepository = $this->prophesize(ContextRepositoryInterface::class);
|
||||
// @todo Refactor this into some better tests in https://www.drupal.org/node/2942605.
|
||||
$this->eventDispatcher = (new \ReflectionClass(ContainerAwareEventDispatcher::class))->newInstanceWithoutConstructor();
|
||||
|
||||
$this->account = $this->prophesize(AccountInterface::class);
|
||||
$subscriber = new BlockComponentRenderArray($this->account->reveal());
|
||||
$this->eventDispatcher->addSubscriber($subscriber);
|
||||
|
||||
$layout = $this->prophesize(LayoutInterface::class);
|
||||
$layout->getPluginDefinition()->willReturn(new LayoutDefinition([]));
|
||||
$layout->build(Argument::type('array'))->willReturnArgument(0);
|
||||
$layout_plugin_manager->createInstance('layout_onecol', [])->willReturn($layout->reveal());
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('current_user', $this->account->reveal());
|
||||
$container->set('plugin.manager.block', $this->blockManager->reveal());
|
||||
$container->set('plugin.manager.core.layout', $layout_plugin_manager->reveal());
|
||||
$container->set('context.handler', $this->contextHandler->reveal());
|
||||
$container->set('context.repository', $this->contextRepository->reveal());
|
||||
$container->set('event_dispatcher', $this->eventDispatcher);
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArray() {
|
||||
$block_content = ['#markup' => 'The block content.'];
|
||||
$render_array = [
|
||||
'#theme' => 'block',
|
||||
'#weight' => 0,
|
||||
'#configuration' => [],
|
||||
'#plugin_id' => 'block_plugin_id',
|
||||
'#base_plugin_id' => 'block_plugin_id',
|
||||
'#derivative_plugin_id' => NULL,
|
||||
'content' => $block_content,
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
|
||||
|
||||
$access_result = AccessResult::allowed();
|
||||
$block->access($this->account->reveal(), TRUE)->willReturn($access_result);
|
||||
$block->build()->willReturn($block_content);
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn([]);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
$block->getPluginId()->willReturn('block_plugin_id');
|
||||
$block->getBaseId()->willReturn('block_plugin_id');
|
||||
$block->getDerivativeId()->willReturn(NULL);
|
||||
$block->getConfiguration()->willReturn([]);
|
||||
|
||||
$section = [
|
||||
new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']),
|
||||
];
|
||||
$expected = [
|
||||
'content' => [
|
||||
'some_uuid' => $render_array,
|
||||
],
|
||||
];
|
||||
$result = (new Section('layout_onecol', [], $section))->toRenderArray();
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArrayAccessDenied() {
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
|
||||
|
||||
$access_result = AccessResult::forbidden();
|
||||
$block->access($this->account->reveal(), TRUE)->willReturn($access_result);
|
||||
$block->build()->shouldNotBeCalled();
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn([]);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
|
||||
$section = [
|
||||
new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']),
|
||||
];
|
||||
$expected = [
|
||||
'content' => [
|
||||
'some_uuid' => [
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$result = (new Section('layout_onecol', [], $section))->toRenderArray();
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArrayPreview() {
|
||||
$block_content = ['#markup' => 'The block content.'];
|
||||
$render_array = [
|
||||
'#theme' => 'block',
|
||||
'#weight' => 0,
|
||||
'#configuration' => [],
|
||||
'#plugin_id' => 'block_plugin_id',
|
||||
'#base_plugin_id' => 'block_plugin_id',
|
||||
'#derivative_plugin_id' => NULL,
|
||||
'content' => $block_content,
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => 0,
|
||||
],
|
||||
];
|
||||
$block = $this->prophesize(BlockPluginInterface::class);
|
||||
$this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
|
||||
|
||||
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
|
||||
$block->build()->willReturn($block_content);
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn([]);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
$block->getConfiguration()->willReturn([]);
|
||||
$block->getPluginId()->willReturn('block_plugin_id');
|
||||
$block->getBaseId()->willReturn('block_plugin_id');
|
||||
$block->getDerivativeId()->willReturn(NULL);
|
||||
|
||||
$section = [
|
||||
new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']),
|
||||
];
|
||||
$expected = [
|
||||
'content' => [
|
||||
'some_uuid' => $render_array,
|
||||
],
|
||||
];
|
||||
$result = (new Section('layout_onecol', [], $section))->toRenderArray([], TRUE);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArrayEmpty() {
|
||||
$section = [];
|
||||
$expected = [];
|
||||
$result = (new Section('layout_onecol', [], $section))->toRenderArray();
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testContextAwareBlock() {
|
||||
$render_array = [
|
||||
'#theme' => 'block',
|
||||
'#weight' => 0,
|
||||
'#configuration' => [],
|
||||
'#plugin_id' => 'block_plugin_id',
|
||||
'#base_plugin_id' => 'block_plugin_id',
|
||||
'#derivative_plugin_id' => NULL,
|
||||
'content' => [],
|
||||
'#cache' => [
|
||||
'contexts' => [],
|
||||
'tags' => [],
|
||||
'max-age' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
$block = $this->prophesize(BlockPluginInterface::class)->willImplement(ContextAwarePluginInterface::class);
|
||||
$this->blockManager->createInstance('block_plugin_id', ['id' => 'block_plugin_id'])->willReturn($block->reveal());
|
||||
|
||||
$access_result = AccessResult::allowed();
|
||||
$block->access($this->account->reveal(), TRUE)->willReturn($access_result);
|
||||
$block->build()->willReturn([]);
|
||||
$block->getCacheContexts()->willReturn([]);
|
||||
$block->getCacheTags()->willReturn([]);
|
||||
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
|
||||
$block->getContextMapping()->willReturn([]);
|
||||
$block->getPluginId()->willReturn('block_plugin_id');
|
||||
$block->getBaseId()->willReturn('block_plugin_id');
|
||||
$block->getDerivativeId()->willReturn(NULL);
|
||||
$block->getConfiguration()->willReturn([]);
|
||||
|
||||
$section = [
|
||||
new SectionComponent('some_uuid', 'content', ['id' => 'block_plugin_id']),
|
||||
];
|
||||
$expected = [
|
||||
'content' => [
|
||||
'some_uuid' => $render_array,
|
||||
],
|
||||
];
|
||||
$result = (new Section('layout_onecol', [], $section))->toRenderArray();
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::toRenderArray
|
||||
*/
|
||||
public function testToRenderArrayMissingPluginId() {
|
||||
$this->setExpectedException(PluginException::class, 'No plugin ID specified for component with "some_uuid" UUID');
|
||||
(new Section('layout_onecol', [], [new SectionComponent('some_uuid', 'content')]))->toRenderArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\Component\Plugin\Factory\FactoryInterface;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\layout_builder\SectionListInterface;
|
||||
use Drupal\layout_builder\SectionStorage\SectionStorageManager;
|
||||
use Drupal\layout_builder\SectionStorageInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\SectionStorage\SectionStorageManager
|
||||
*
|
||||
* @group layout_builder
|
||||
*/
|
||||
class SectionStorageManagerTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The section storage manager.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManager
|
||||
*/
|
||||
protected $manager;
|
||||
|
||||
/**
|
||||
* The plugin.
|
||||
*
|
||||
* @var \Drupal\layout_builder\SectionStorageInterface
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$cache = $this->prophesize(CacheBackendInterface::class);
|
||||
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
|
||||
$this->manager = new SectionStorageManager(new \ArrayObject(), $cache->reveal(), $module_handler->reveal());
|
||||
|
||||
$this->plugin = $this->prophesize(SectionStorageInterface::class);
|
||||
|
||||
$factory = $this->prophesize(FactoryInterface::class);
|
||||
$factory->createInstance('the_plugin_id', [])->willReturn($this->plugin->reveal());
|
||||
$reflection_property = new \ReflectionProperty($this->manager, 'factory');
|
||||
$reflection_property->setAccessible(TRUE);
|
||||
$reflection_property->setValue($this->manager, $factory->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadEmpty
|
||||
*/
|
||||
public function testLoadEmpty() {
|
||||
$result = $this->manager->loadEmpty('the_plugin_id');
|
||||
$this->assertInstanceOf(SectionStorageInterface::class, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadFromStorageId
|
||||
*/
|
||||
public function testLoadFromStorageId() {
|
||||
$section_list = $this->prophesize(SectionListInterface::class);
|
||||
$this->plugin->setSectionList($section_list->reveal())->will(function () {
|
||||
return $this;
|
||||
});
|
||||
$this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal());
|
||||
|
||||
$result = $this->manager->loadFromStorageId('the_plugin_id', 'the_storage_id');
|
||||
$this->assertInstanceOf(SectionStorageInterface::class, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadFromRoute
|
||||
*/
|
||||
public function testLoadFromRoute() {
|
||||
$section_list = $this->prophesize(SectionListInterface::class);
|
||||
$this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', [])->willReturn('the_storage_id');
|
||||
$this->plugin->getSectionListFromId('the_storage_id')->willReturn($section_list->reveal());
|
||||
$this->plugin->setSectionList($section_list->reveal())->will(function () {
|
||||
return $this;
|
||||
});
|
||||
|
||||
$result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', []);
|
||||
$this->assertInstanceOf(SectionStorageInterface::class, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::loadFromRoute
|
||||
*/
|
||||
public function testLoadFromRouteNull() {
|
||||
$this->plugin->extractIdFromRoute('the_value', [], 'the_parameter_name', ['_route' => 'the_route_name'])->willReturn(NULL);
|
||||
|
||||
$result = $this->manager->loadFromRoute('the_plugin_id', 'the_value', [], 'the_parameter_name', ['_route' => 'the_route_name']);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\layout_builder\Unit;
|
||||
|
||||
use Drupal\layout_builder\Section;
|
||||
use Drupal\layout_builder\SectionComponent;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\layout_builder\Section
|
||||
* @group layout_builder
|
||||
*/
|
||||
class SectionTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The section object to test.
|
||||
*
|
||||
* @var \Drupal\layout_builder\Section
|
||||
*/
|
||||
protected $section;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->section = new Section('layout_onecol', [], [
|
||||
new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']),
|
||||
(new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3),
|
||||
(new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::setComponent
|
||||
* @covers ::getComponents
|
||||
*/
|
||||
public function testGetComponents() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3),
|
||||
'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2),
|
||||
];
|
||||
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getComponent
|
||||
*/
|
||||
public function testGetComponentInvalidUuid() {
|
||||
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid UUID "invalid-uuid"');
|
||||
$this->section->getComponent('invalid-uuid');
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::getComponent
|
||||
*/
|
||||
public function testGetComponent() {
|
||||
$expected = new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']);
|
||||
|
||||
$this->assertEquals($expected, $this->section->getComponent('existing-uuid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::removeComponent
|
||||
* @covers ::getComponentsByRegion
|
||||
*/
|
||||
public function testRemoveComponent() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3),
|
||||
];
|
||||
|
||||
$this->section->removeComponent('first-uuid');
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::appendComponent
|
||||
* @covers ::getNextHighestWeight
|
||||
* @covers ::getComponentsByRegion
|
||||
*/
|
||||
public function testAppendComponent() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3),
|
||||
'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2),
|
||||
'new-uuid' => (new SectionComponent('new-uuid', 'some-region', []))->setWeight(1),
|
||||
];
|
||||
|
||||
$this->section->appendComponent(new SectionComponent('new-uuid', 'some-region'));
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertAfterComponent
|
||||
*/
|
||||
public function testInsertAfterComponent() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4),
|
||||
'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2),
|
||||
'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(3),
|
||||
];
|
||||
|
||||
$this->section->insertAfterComponent('first-uuid', new SectionComponent('new-uuid', 'ordered-region'));
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertAfterComponent
|
||||
*/
|
||||
public function testInsertAfterComponentValidUuidRegionMismatch() {
|
||||
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "existing-uuid"');
|
||||
$this->section->insertAfterComponent('existing-uuid', new SectionComponent('new-uuid', 'ordered-region'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertAfterComponent
|
||||
*/
|
||||
public function testInsertAfterComponentInvalidUuid() {
|
||||
$this->setExpectedException(\InvalidArgumentException::class, 'Invalid preceding UUID "invalid-uuid"');
|
||||
$this->section->insertAfterComponent('invalid-uuid', new SectionComponent('new-uuid', 'ordered-region'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertComponent
|
||||
* @covers ::getComponentsByRegion
|
||||
*/
|
||||
public function testInsertComponent() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(4),
|
||||
'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(3),
|
||||
'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(2),
|
||||
];
|
||||
|
||||
$this->section->insertComponent(0, new SectionComponent('new-uuid', 'ordered-region'));
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertComponent
|
||||
*/
|
||||
public function testInsertComponentAppend() {
|
||||
$expected = [
|
||||
'existing-uuid' => (new SectionComponent('existing-uuid', 'some-region', ['id' => 'existing-block-id']))->setWeight(0),
|
||||
'second-uuid' => (new SectionComponent('second-uuid', 'ordered-region', ['id' => 'second-block-id']))->setWeight(3),
|
||||
'first-uuid' => (new SectionComponent('first-uuid', 'ordered-region', ['id' => 'first-block-id']))->setWeight(2),
|
||||
'new-uuid' => (new SectionComponent('new-uuid', 'ordered-region', []))->setWeight(4),
|
||||
];
|
||||
|
||||
$this->section->insertComponent(2, new SectionComponent('new-uuid', 'ordered-region'));
|
||||
$this->assertComponents($expected, $this->section);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::insertComponent
|
||||
*/
|
||||
public function testInsertComponentInvalidDelta() {
|
||||
$this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "7" for the "new-uuid" component');
|
||||
$this->section->insertComponent(7, new SectionComponent('new-uuid', 'ordered-region'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the section has the expected components.
|
||||
*
|
||||
* @param \Drupal\layout_builder\SectionComponent[] $expected
|
||||
* The expected sections.
|
||||
* @param \Drupal\layout_builder\Section $section
|
||||
* The section storage to check.
|
||||
*/
|
||||
protected function assertComponents(array $expected, Section $section) {
|
||||
$result = $section->getComponents();
|
||||
$this->assertEquals($expected, $result);
|
||||
$this->assertSame(array_keys($expected), array_keys($result));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user