Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -0,0 +1,66 @@
core.entity_view_display.*.*.*.third_party.layout_builder:
type: mapping
label: 'Per-view-mode Layout Builder settings'
mapping:
enabled:
type: boolean
label: 'Whether the Layout Builder is enabled for this display'
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'
inline_block:
type: block_settings
label: 'Inline block'
mapping:
view_mode:
type: string
lable: 'View mode'
block_revision_id:
type: integer
label: 'Block revision ID'
block_serialized:
type: string
label: 'Serialized block'
block.settings.inline_block:*:
type: inline_block

View file

@ -0,0 +1,87 @@
.new-section {
background-color: #f7f7f7;
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;
}
.new-section__link,
.new-block__link {
color: #787878;
border-bottom: none;
padding-left: 24px;
background: url(../../../misc/icons/787878/plus.svg) transparent top left / 16px 16px no-repeat;
}
.new-section__link:hover,
.new-section__link:active,
.new-section__link:focus,
.new-block__link:hover,
.new-block__link:active,
.new-block__link:focus {
border-bottom-style: none;
color: #000;
}
.layout-section {
margin-bottom: 1.5em;
}
.layout-section .ui-sortable-helper {
background-color: #fff;
outline: 2px solid #f7f7f7;
}
.layout-section .ui-state-drop {
background-color: #ffd;
outline: 2px dashed #fedb60;
margin: 20px;
padding: 30px;
}
.layout-section .layout-builder--layout__region {
outline: 2px dashed #2f91da;
}
.layout-section .layout-builder--layout__region .new-block {
background-color: #eff6fc;
padding: 1.5em 0;
text-align: center;
}
.layout-section .layout-builder--layout__region .block {
padding: 1.5em;
}
.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;
}

View file

@ -0,0 +1,54 @@
(($, { ajax, behaviors }) => {
behaviors.layoutBuilder = {
attach(context) {
$(context)
.find('.layout-builder--layout__region')
.sortable({
items: '> .draggable',
connectWith: '.layout-builder--layout__region',
placeholder: 'ui-state-drop',
/**
* 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);

View file

@ -0,0 +1,35 @@
/**
* 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',
placeholder: 'ui-state-drop',
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);

View file

@ -0,0 +1,11 @@
name: 'Layout Builder'
type: module
description: 'Provides layout building utility.'
package: Core (Experimental)
version: VERSION
core: 8.x
dependencies:
- drupal:layout_discovery
- drupal:contextual
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
- drupal:field_ui

View file

@ -0,0 +1,138 @@
<?php
/**
* @file
* Contains install and update functions for Layout Builder.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Section;
/**
* Implements hook_install().
*/
function layout_builder_install() {
$display_changed = FALSE;
$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
->enableLayoutBuilder()
->appendSection(new Section($field_layout['id'], $field_layout['settings']))
->save();
$display_changed = TRUE;
}
}
// 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.
if ($display_changed) {
Cache::invalidateTags(['rendered']);
}
}
/**
* Enable Layout Builder for existing entity displays.
*/
function layout_builder_update_8601(&$sandbox) {
$config_factory = \Drupal::configFactory();
if (!isset($sandbox['count'])) {
$sandbox['ids'] = $config_factory->listAll('core.entity_view_display.');
$sandbox['count'] = count($sandbox['ids']);
}
$ids = array_splice($sandbox['ids'], 0, 50);
foreach ($ids as $id) {
$display = $config_factory->getEditable($id);
if ($display->get('third_party_settings.layout_builder')) {
$display
->set('third_party_settings.layout_builder.enabled', TRUE)
->save();
}
}
$sandbox['#finished'] = empty($sandbox['ids']) ? 1 : ($sandbox['count'] - count($sandbox['ids'])) / $sandbox['count'];
}
/**
* Implements hook_schema().
*/
function layout_builder_schema() {
$schema['inline_block_usage'] = [
'description' => 'Track where a block_content entity is used.',
'fields' => [
'block_content_id' => [
'description' => 'The block_content entity ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'layout_entity_type' => [
'description' => 'The entity type of the parent entity.',
'type' => 'varchar_ascii',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'not null' => FALSE,
'default' => '',
],
'layout_entity_id' => [
'description' => 'The ID of the parent entity.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => FALSE,
'default' => 0,
],
],
'primary key' => ['block_content_id'],
'indexes' => [
'type_id' => ['layout_entity_type', 'layout_entity_id'],
],
];
return $schema;
}
/**
* Create the 'inline_block_usage' table.
*/
function layout_builder_update_8602() {
$inline_block_usage = [
'description' => 'Track where a block_content entity is used.',
'fields' => [
'block_content_id' => [
'description' => 'The block_content entity ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
],
'layout_entity_type' => [
'description' => 'The entity type of the parent entity.',
'type' => 'varchar_ascii',
'length' => EntityTypeInterface::ID_MAX_LENGTH,
'not null' => FALSE,
'default' => '',
],
'layout_entity_id' => [
'description' => 'The ID of the parent entity.',
'type' => 'varchar_ascii',
'length' => 128,
'not null' => FALSE,
'default' => 0,
],
],
'primary key' => ['block_content_id'],
'indexes' => [
'type_id' => ['layout_entity_type', 'layout_entity_id'],
],
];
Database::getConnection()->schema()->createTable('inline_block_usage', $inline_block_usage);
}

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,2 @@
layout_builder_ui:
deriver: '\Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver'

View file

@ -0,0 +1,204 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder.
*/
use Drupal\Core\Entity\EntityInterface;
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;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
/**
* 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();
}
/**
* Implements hook_entity_view_alter().
*
* ExtraFieldBlock block plugins add placeholders for each extra field which is
* configured to be displayed. Those placeholders are replaced by this hook.
* Modules that implement hook_entity_extra_field_info() use their
* implementations of hook_entity_view_alter() to add the rendered output of
* the extra fields they provide, so we cannot get the rendered output of extra
* fields before this point in the view process.
* layout_builder_module_implements_alter() moves this implementation of
* hook_entity_view_alter() to the end of the list.
*
* @see \Drupal\layout_builder\Plugin\Block\ExtraFieldBlock::build()
* @see layout_builder_module_implements_alter()
*/
function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
if ($display instanceof LayoutEntityDisplayInterface) {
/** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
$field_manager = \Drupal::service('entity_field.manager');
$extra_fields = $field_manager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
if (!empty($extra_fields['display'])) {
foreach ($extra_fields['display'] as $field_name => $extra_field) {
// If the extra field is not set replace with an empty array to avoid
// the placeholder text from being rendered.
$replacement = isset($build[$field_name]) ? $build[$field_name] : [];
ExtraFieldBlock::replaceFieldPlaceholder($build, $replacement, $field_name);
// After the rendered field in $build has been copied over to the
// ExtraFieldBlock block we must remove it from its original location or
// else it will be rendered twice.
unset($build[$field_name]);
}
}
}
}
/**
* Implements hook_builder_module_implements_alter().
*/
function layout_builder_module_implements_alter(&$implementations, $hook) {
if ($hook === 'entity_view_alter') {
// Ensure that this module's implementation of hook_entity_view_alter() runs
// last so that other modules that use this hook to render extra fields will
// run before it.
$group = $implementations['layout_builder'];
unset($implementations['layout_builder']);
$implementations['layout_builder'] = $group;
}
}
/**
* Implements hook_entity_presave().
*/
function layout_builder_entity_presave(EntityInterface $entity) {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->handlePreSave($entity);
}
}
/**
* Implements hook_entity_delete().
*/
function layout_builder_entity_delete(EntityInterface $entity) {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->handleEntityDelete($entity);
}
}
/**
* Implements hook_cron().
*/
function layout_builder_cron() {
if (\Drupal::moduleHandler()->moduleExists('block_content')) {
/** @var \Drupal\layout_builder\InlineBlockEntityOperations $entity_operations */
$entity_operations = \Drupal::classResolver(InlineBlockEntityOperations::class);
$entity_operations->removeUnused();
}
}
/**
* Implements hook_plugin_filter_TYPE_alter().
*/
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
// @todo Determine the 'inline_block' blocks should be allowed outside
// of layout_builder https://www.drupal.org/node/2979142.
if ($consumer !== 'layout_builder') {
foreach ($definitions as $id => $definition) {
if ($definition['id'] === 'inline_block') {
unset($definitions[$id]);
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function layout_builder_block_content_access(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\block_content\BlockContentInterface $entity */
if ($operation === 'view' || $entity->isReusable() || empty(\Drupal::service('inline_block.usage')->getUsage($entity->id()))) {
// If the operation is 'view' or this is reusable block or if this is
// non-reusable that isn't used by this module then don't alter the access.
return AccessResult::neutral();
}
if ($account->hasPermission('configure any layout')) {
return AccessResult::allowed();
}
return AccessResult::forbidden();
}

View file

@ -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

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Post update functions for Layout Builder.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* 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'];
}
/**
* Ensure all extra fields are properly stored on entity view displays.
*
* Previously
* \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::setComponent()
* was not correctly setting the configuration for extra fields. This function
* calls setComponent() for all extra field components to ensure the updated
* logic is invoked on all extra fields to correct the settings.
*/
function layout_builder_post_update_add_extra_fields(&$sandbox = NULL) {
$entity_field_manager = \Drupal::service('entity_field.manager');
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (LayoutEntityDisplayInterface $display) use ($entity_field_manager) {
if (!$display->isLayoutBuilderEnabled()) {
return FALSE;
}
$extra_fields = $entity_field_manager->getExtraFields($display->getTargetEntityTypeId(), $display->getTargetBundle());
$components = $display->getComponents();
// Sort the components to avoid them being reordered by setComponent().
uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
$result = FALSE;
foreach ($components as $name => $component) {
if (isset($extra_fields['display'][$name])) {
$display->setComponent($name, $component);
$result = TRUE;
}
}
return $result;
});
}

View file

@ -0,0 +1,126 @@
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
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'
_layout_builder_access: 'view'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE

View file

@ -0,0 +1,48 @@
services:
layout_builder.tempstore_repository:
class: Drupal\layout_builder\LayoutTempstoreRepository
arguments: ['@tempstore.shared']
access_check.entity.layout_builder_access:
class: Drupal\layout_builder\Access\LayoutBuilderAccessCheck
tags:
- { name: access_check, applies_to: _layout_builder_access }
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 }
logger.channel.layout_builder:
parent: logger.channel_base
arguments: ['layout_builder']
inline_block.usage:
class: Drupal\layout_builder\InlineBlockUsage
arguments: ['@database']

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access check for the Layout Builder defaults.
*
* @internal
*/
class LayoutBuilderAccessCheck implements AccessInterface {
/**
* Checks routing access to the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(SectionStorageInterface $section_storage, AccountInterface $account, Route $route) {
$operation = $route->getRequirement('_layout_builder_access');
$access = $section_storage->access($operation, $account, TRUE);
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($section_storage);
}
return $access;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
/**
* Accessible class to allow access for inline blocks in the Layout Builder.
*
* @internal
*/
class LayoutPreviewAccessAllowed implements AccessibleInterface {
/**
* {@inheritdoc}
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'view') {
return $return_as_object ? AccessResult::allowed() : TRUE;
}
// The layout builder preview should only need 'view' access.
return $return_as_object ? AccessResult::forbidden() : FALSE;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
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());
}
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
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;
use StringTranslationTrait;
/**
* 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['#title'] = $this->t('Choose a block');
$build['#type'] = 'container';
$build['#attributes']['class'][] = 'block-categories';
// @todo Explicitly cast delta to an integer, remove this in
// https://www.drupal.org/project/drupal/issues/2984509.
$delta = (int) $delta;
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
'section_storage' => $section_storage,
'delta' => $delta,
'region' => $region,
]);
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;
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
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 = [];
$definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', [], ['section_storage' => $section_storage]);
foreach ($definitions 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;
}
}

View file

@ -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', 'new-section__link'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
],
]
),
],
'#type' => 'container',
'#attributes' => [
'class' => ['new-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', 'new-block__link'],
'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' => ['new-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());
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
/**
* 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.
*
* @todo Refactor this interface in https://www.drupal.org/node/2985362.
*/
interface DefaultsSectionStorageInterface extends SectionStorageInterface, ThirdPartySettingsInterface, LayoutBuilderEnabledInterface {
/**
* 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);
}

View file

@ -0,0 +1,401 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\EntityContext;
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;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
// Set $entityFieldManager before calling the parent constructor because the
// constructor will call init() which then calls setComponent() which needs
// $entityFieldManager.
$this->entityFieldManager = \Drupal::service('entity_field.manager');
parent::__construct($values, $entity_type);
}
/**
* {@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 isLayoutBuilderEnabled() {
return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->setOverridable(FALSE);
$this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
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');
}
else {
$this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
}
}
$already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
$set_enabled = $this->isLayoutBuilderEnabled();
if ($already_enabled !== $set_enabled) {
if ($set_enabled) {
// Loop through all existing field-based components and add them as
// section-based components.
$components = $this->getComponents();
// Sort the components by weight.
uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
foreach ($components as $name => $component) {
$this->setComponent($name, $component);
}
}
else {
// When being disabled, remove all existing section data.
while (count($this) > 0) {
$this->removeSection(0);
}
}
}
}
/**
* Removes a layout section field if it is no longer needed.
*
* Because the field is shared across all view modes, the field will only be
* removed if no other view modes are using it.
*
* @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 removeSectionField($entity_type_id, $bundle, $field_name) {
$query = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->getQuery()
->condition('targetEntityType', $this->getTargetEntityTypeId())
->condition('bundle', $this->getTargetBundle())
->condition('mode', $this->getMode(), '<>')
->condition('third_party_settings.layout_builder.allow_custom', TRUE);
$enabled = (bool) $query->count()->execute();
if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
$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}
*/
public function createCopy($mode) {
// Disable Layout Builder and remove any sections copied from the original.
return parent::createCopy($mode)
->setSections([])
->disableLayoutBuilder();
}
/**
* {@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);
if (!$this->isLayoutBuilderEnabled()) {
return $build_list;
}
/** @var \Drupal\Core\Entity\EntityInterface $entity */
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();
$label = new TranslatableMarkup('@entity being viewed', [
'@entity' => $entity->getEntityType()->getSingularLabel(),
]);
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
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;
}
/**
* {@inheritdoc}
*/
public function setComponent($name, array $options = []) {
parent::setComponent($name, $options);
// Only continue if Layout Builder is enabled.
if (!$this->isLayoutBuilderEnabled()) {
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);
$extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
$is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
$configuration = [
'label_display' => '0',
'context_mapping' => ['entity' => 'layout_builder.entity'],
];
if ($is_view_configurable_non_extra_field) {
$configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
$keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
$configuration['formatter'] = array_intersect_key($options, $keys);
}
else {
$configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
}
$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);
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\layout_builder\Section;
/**
* 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'];
$sections = array_map([Section::class, 'fromArray'], $sections);
}
}
return parent::mapFromStorageRecords($records);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Drupal\layout_builder\Entity;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\layout_builder\LayoutBuilderEnabledInterface;
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.
*
* @todo Refactor this interface in https://www.drupal.org/node/2985362.
*/
interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface, LayoutBuilderEnabledInterface {
/**
* 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);
}

View file

@ -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;
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
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;
}
// Set block access dependency even if we are not checking access on
// this level. The block itself may render another
// RefinableDependentAccessInterface object and need to pass on this value.
if ($block instanceof RefinableDependentAccessInterface) {
$contexts = $event->getContexts();
if (isset($contexts['layout_builder.entity'])) {
if ($entity = $contexts['layout_builder.entity']->getContextValue()) {
if ($event->inPreview()) {
// If previewing in Layout Builder allow access.
$block->setAccessDependency(new LayoutPreviewAccessAllowed());
}
else {
$block->setAccessDependency($entity);
}
}
}
}
// 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);
}
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\BlockContentEvents;
use Drupal\block_content\BlockContentInterface;
use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\layout_builder\InlineBlockUsage;
use Drupal\layout_builder\LayoutEntityHelperTrait;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* An event subscriber that returns an access dependency for inline blocks.
*
* When used within the layout builder the access dependency for inline blocks
* will be explicitly set but if access is evaluated outside of the layout
* builder then the dependency may not have been set.
*
* A known example of when the access dependency will not have been set is when
* determining 'view' or 'download' access to a file entity that is attached
* to a content block via a field that is using the private file system. The
* file access handler will evaluate access on the content block without setting
* the dependency.
*
* @internal
*
* @see \Drupal\file\FileAccessControlHandler::checkAccess()
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
*/
class SetInlineBlockDependency implements EventSubscriberInterface {
use LayoutEntityHelperTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* The inline block usage service.
*
* @var \Drupal\layout_builder\InlineBlockUsage
*/
protected $usage;
/**
* Constructs SetInlineBlockDependency object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
* @param \Drupal\layout_builder\InlineBlockUsage $usage
* The inline block usage service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, InlineBlockUsage $usage) {
$this->entityTypeManager = $entity_type_manager;
$this->database = $database;
$this->usage = $usage;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY => 'onGetDependency',
];
}
/**
* Handles the BlockContentEvents::INLINE_BLOCK_GET_DEPENDENCY event.
*
* @param \Drupal\block_content\Event\BlockContentGetDependencyEvent $event
* The event.
*/
public function onGetDependency(BlockContentGetDependencyEvent $event) {
if ($dependency = $this->getInlineBlockDependency($event->getBlockContentEntity())) {
$event->setAccessDependency($dependency);
}
}
/**
* Get the access dependency of an inline block.
*
* If the block is used in an entity that entity will be returned as the
* dependency.
*
* For revisionable entities the entity will only be returned if it is used in
* the latest revision of the entity. For inline blocks that are not used in
* the latest revision but are used in a previous revision the entity will not
* be returned because calling
* \Drupal\Core\Access\AccessibleInterface::access() will only check access on
* the latest revision. Therefore if the previous revision of the entity was
* returned as the dependency access would be granted to inline block
* regardless of whether the user has access to the revision in which the
* inline block was used.
*
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content entity.
*
* @return \Drupal\Core\Entity\EntityInterface|null
* Returns the layout dependency.
*
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
protected function getInlineBlockDependency(BlockContentInterface $block_content) {
$layout_entity_info = $this->usage->getUsage($block_content->id());
if (empty($layout_entity_info)) {
// If the block does not have usage information then we cannot set a
// dependency. It may be used by another module besides layout builder.
return NULL;
}
/** @var \Drupal\layout_builder\InlineBlockUsage $usage */
$layout_entity_storage = $this->entityTypeManager->getStorage($layout_entity_info->layout_entity_type);
$layout_entity = $layout_entity_storage->load($layout_entity_info->layout_entity_id);
if ($this->isLayoutCompatibleEntity($layout_entity)) {
if ($this->isBlockRevisionUsedInEntity($layout_entity, $block_content)) {
return $layout_entity;
}
}
return NULL;
}
/**
* Determines if a block content revision is used in an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $layout_entity
* The layout entity.
* @param \Drupal\block_content\BlockContentInterface $block_content
* The block content revision.
*
* @return bool
* TRUE if the block content revision is used as an inline block in the
* layout entity.
*/
protected function isBlockRevisionUsedInEntity(EntityInterface $layout_entity, BlockContentInterface $block_content) {
$sections_blocks_revision_ids = $this->getInlineBlockRevisionIdsInSections($this->getEntitySections($layout_entity));
return in_array($block_content->getRevisionId(), $sections_blocks_revision_ids);
}
}

View file

@ -0,0 +1,58 @@
<?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 getEntity() {
$entity = parent::getEntity();
// Ensure the entity is updated with the latest value.
$entity->set($this->getName(), $this->getValue());
return $entity;
}
}

View file

@ -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->get('layout_builder__component')) {
$component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]);
$section_storage->getSection($delta)->appendComponent($component);
$form_state->set('layout_builder__component', $component);
}
return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
}

View file

@ -0,0 +1,245 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
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;
}
}

View file

@ -0,0 +1,197 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
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()));
}
}

View file

@ -0,0 +1,106 @@
<?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\DefaultsSectionStorageInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Disables Layout Builder for a given default.
*/
class LayoutBuilderDisableForm extends ConfirmFormBase {
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
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->setMessenger($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_disable_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to disable Layout Builder?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('All customizations will be removed. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->sectionStorage->getRedirectUrl();
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
if (!$section_storage instanceof DefaultsSectionStorageInterface) {
throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide defaults', $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) {
$this->sectionStorage->disableLayoutBuilder()->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger()->addMessage($this->t('Layout Builder has been disabled.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View file

@ -0,0 +1,201 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityInterface;
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\DefaultsSectionStorageInterface
*/
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);
// Remove the Layout Builder field from the list.
$form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']);
unset($form['fields']['layout_builder__layout']);
$is_enabled = $this->entity->isLayoutBuilderEnabled();
if ($is_enabled) {
// 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(),
'#access' => $is_enabled,
];
$form['layout'] = [
'#type' => 'details',
'#open' => TRUE,
'#title' => $this->t('Layout options'),
'#tree' => TRUE,
];
$form['layout']['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use Layout Builder'),
'#default_value' => $is_enabled,
];
$form['#entity_builders']['layout_builder'] = '::entityFormEntityBuild';
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
if ($this->entity->getMode() === 'default') {
$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(),
'#states' => [
'disabled' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
'invisible' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
],
];
if (!$is_enabled) {
$form['layout']['allow_custom']['#attributes']['disabled'] = 'disabled';
}
// Prevent turning off overrides while any exist.
if ($this->hasOverrides($this->entity)) {
$form['layout']['enabled']['#disabled'] = TRUE;
$form['layout']['enabled']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.');
$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.');
unset($form['layout']['allow_custom']['#states']);
unset($form['#entity_builders']['layout_builder']);
}
}
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();
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// Do not process field values if Layout Builder is or will be enabled.
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity */
$already_enabled = $entity->isLayoutBuilderEnabled();
if ($already_enabled || $set_enabled) {
$form['#fields'] = [];
$form['#extra'] = [];
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
/**
* Entity builder for layout options on the entity view display form.
*/
public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) {
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
$already_enabled = $display->isLayoutBuilderEnabled();
if ($set_enabled) {
$overridable = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
$display->setOverridable($overridable);
if (!$already_enabled) {
$display->enableLayoutBuilder();
}
}
elseif ($already_enabled) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl('disable'));
}
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildFieldRow($field_definition, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildExtraFieldRow($field_id, $extra_field);
}
}

View file

@ -0,0 +1,120 @@
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Ajax\AjaxFormHelperTrait;
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);
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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');
}
}

View file

@ -0,0 +1,267 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\layout_builder\Plugin\Block\InlineBlock;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class for reacting to entity events related to Inline Blocks.
*
* @internal
*/
class InlineBlockEntityOperations implements ContainerInjectionInterface {
use LayoutEntityHelperTrait;
/**
* Inline block usage tracking service.
*
* @var \Drupal\layout_builder\InlineBlockUsage
*/
protected $usage;
/**
* The block content storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentStorage;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new EntityOperations object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager service.
* @param \Drupal\layout_builder\InlineBlockUsage $usage
* Inline block usage tracking service.
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(EntityTypeManagerInterface $entityTypeManager, InlineBlockUsage $usage, Connection $database) {
$this->entityTypeManager = $entityTypeManager;
$this->blockContentStorage = $entityTypeManager->getStorage('block_content');
$this->usage = $usage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('inline_block.usage'),
$container->get('database')
);
}
/**
* Remove all unused inline blocks on save.
*
* Entities that were used in prevision revisions will be removed if not
* saving a new revision.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
protected function removeUnusedForEntityOnSave(EntityInterface $entity) {
// If the entity is new or '$entity->original' is not set then there will
// not be any unused inline blocks to remove.
// If this is a revisionable entity then do not remove inline blocks. They
// could be referenced in previous revisions even if this is not a new
// revision.
if ($entity->isNew() || !isset($entity->original) || $entity instanceof RevisionableInterface) {
return;
}
$sections = $this->getEntitySections($entity);
// If this is a layout override and there are no sections then it is a new
// override.
if ($this->isEntityUsingFieldOverride($entity) && empty($sections)) {
return;
}
// Delete and remove the usage for inline blocks that were removed.
if ($removed_block_ids = $this->getRemovedBlockIds($entity)) {
$this->deleteBlocksAndUsage($removed_block_ids);
}
}
/**
* Gets the IDs of the inline blocks that were removed.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*
* @return int[]
* The block content IDs that were removed.
*/
protected function getRemovedBlockIds(EntityInterface $entity) {
$original_sections = $this->getEntitySections($entity->original);
$current_sections = $this->getEntitySections($entity);
// Avoid un-needed conversion from revision IDs to block content IDs by
// first determining if there are any revisions in the original that are not
// also in the current sections.
$current_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($current_sections);
$original_block_content_revision_ids = $this->getInlineBlockRevisionIdsInSections($original_sections);
if ($unused_original_revision_ids = array_diff($original_block_content_revision_ids, $current_block_content_revision_ids)) {
// If there are any revisions in the original that aren't in the current
// there may some blocks that need to be removed.
$current_block_content_ids = $this->getBlockIdsForRevisionIds($current_block_content_revision_ids);
$unused_original_block_content_ids = $this->getBlockIdsForRevisionIds($unused_original_revision_ids);
return array_diff($unused_original_block_content_ids, $current_block_content_ids);
}
return [];
}
/**
* Handles entity tracking on deleting a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handleEntityDelete(EntityInterface $entity) {
if ($this->isLayoutCompatibleEntity($entity)) {
$this->usage->removeByLayoutEntity($entity);
}
}
/**
* Handles saving a parent entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The parent entity.
*/
public function handlePreSave(EntityInterface $entity) {
if (!$this->isLayoutCompatibleEntity($entity)) {
return;
}
$duplicate_blocks = FALSE;
if ($sections = $this->getEntitySections($entity)) {
if ($this->isEntityUsingFieldOverride($entity)) {
if (!$entity->isNew() && isset($entity->original)) {
if (empty($this->getEntitySections($entity->original))) {
// If there were no sections in the original entity then this is a
// new override from a default and the blocks need to be duplicated.
$duplicate_blocks = TRUE;
}
}
}
$new_revision = FALSE;
if ($entity instanceof RevisionableInterface) {
// If the parent entity will have a new revision create a new revision
// of the block.
// @todo Currently revisions are never created for the parent entity.
// This will be fixed in https://www.drupal.org/node/2937199.
// To work around this always make a revision when the parent entity
// is an instance of RevisionableInterface. After the issue is fixed
// only create a new revision if '$entity->isNewRevision()'.
$new_revision = TRUE;
}
foreach ($this->getInlineBlockComponents($sections) as $component) {
$this->saveInlineBlockComponent($entity, $component, $new_revision, $duplicate_blocks);
}
}
$this->removeUnusedForEntityOnSave($entity);
}
/**
* Gets a block ID for an inline block plugin.
*
* @param \Drupal\layout_builder\Plugin\Block\InlineBlock $block_plugin
* The inline block plugin.
*
* @return int
* The block content ID or null none available.
*/
protected function getPluginBlockId(InlineBlock $block_plugin) {
$configuration = $block_plugin->getConfiguration();
if (!empty($configuration['block_revision_id'])) {
$revision_ids = $this->getBlockIdsForRevisionIds([$configuration['block_revision_id']]);
return array_pop($revision_ids);
}
return NULL;
}
/**
* Delete the inline blocks and the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
protected function deleteBlocksAndUsage(array $block_content_ids) {
foreach ($block_content_ids as $block_content_id) {
if ($block = $this->blockContentStorage->load($block_content_id)) {
$block->delete();
}
}
$this->usage->deleteUsage($block_content_ids);
}
/**
* Removes unused inline blocks.
*
* @param int $limit
* The maximum number of inline blocks to remove.
*/
public function removeUnused($limit = 100) {
$this->deleteBlocksAndUsage($this->usage->getUnused($limit));
}
/**
* Gets blocks IDs for an array of revision IDs.
*
* @param int[] $revision_ids
* The revision IDs.
*
* @return int[]
* The block IDs.
*/
protected function getBlockIdsForRevisionIds(array $revision_ids) {
if ($revision_ids) {
$query = $this->blockContentStorage->getQuery();
$query->condition('revision_id', $revision_ids, 'IN');
$block_ids = $query->execute();
return $block_ids;
}
return [];
}
/**
* Saves an inline block component.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with the layout.
* @param \Drupal\layout_builder\SectionComponent $component
* The section component with an inline block.
* @param bool $new_revision
* Whether a new revision of the block should be created.
* @param bool $duplicate_blocks
* Whether the blocks should be duplicated.
*/
protected function saveInlineBlockComponent(EntityInterface $entity, SectionComponent $component, $new_revision, $duplicate_blocks) {
/** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */
$plugin = $component->getPlugin();
$pre_save_configuration = $plugin->getConfiguration();
$plugin->saveBlockContent($new_revision, $duplicate_blocks);
$post_save_configuration = $plugin->getConfiguration();
if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) {
$this->usage->addUsage($this->getPluginBlockId($plugin), $entity);
}
$component->setConfiguration($post_save_configuration);
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityInterface;
/**
* Service class to track inline block usage.
*
* @internal
*/
class InlineBlockUsage {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Creates an InlineBlockUsage object.
*
* @param \Drupal\Core\Database\Connection $database
* The database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* Adds a usage record.
*
* @param int $block_content_id
* The block content id.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function addUsage($block_content_id, EntityInterface $entity) {
$this->database->merge('inline_block_usage')
->keys([
'block_content_id' => $block_content_id,
'layout_entity_id' => $entity->id(),
'layout_entity_type' => $entity->getEntityTypeId(),
])->execute();
}
/**
* Gets unused inline block IDs.
*
* @param int $limit
* The maximum number of block content entity IDs to return.
*
* @return int[]
* The entity IDs.
*/
public function getUnused($limit = 100) {
$query = $this->database->select('inline_block_usage', 't');
$query->fields('t', ['block_content_id']);
$query->isNull('layout_entity_id');
$query->isNull('layout_entity_type');
return $query->range(0, $limit)->execute()->fetchCol();
}
/**
* Remove usage record by layout entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The layout entity.
*/
public function removeByLayoutEntity(EntityInterface $entity) {
$query = $this->database->update('inline_block_usage')
->fields([
'layout_entity_type' => NULL,
'layout_entity_id' => NULL,
]);
$query->condition('layout_entity_type', $entity->getEntityTypeId());
$query->condition('layout_entity_id', $entity->id());
$query->execute();
}
/**
* Delete the inline blocks' the usage records.
*
* @param int[] $block_content_ids
* The block content entity IDs.
*/
public function deleteUsage(array $block_content_ids) {
if (!empty($block_content_ids)) {
$query = $this->database->delete('inline_block_usage')->condition('block_content_id', $block_content_ids, 'IN');
$query->execute();
}
}
/**
* Gets usage record for inline block by ID.
*
* @param int $block_content_id
* The block content entity ID.
*
* @return object
* The usage record with properties layout_entity_id and layout_entity_type.
*/
public function getUsage($block_content_id) {
$query = $this->database->select('inline_block_usage');
$query->condition('block_content_id', $block_content_id);
$query->fields('inline_block_usage', ['layout_entity_id', 'layout_entity_type']);
$query->range(0, 1);
return $query->execute()->fetchObject();
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\layout_builder;
/**
* Provides methods for enabling and disabling Layout Builder.
*/
interface LayoutBuilderEnabledInterface {
/**
* Determines if Layout Builder is enabled.
*
* @return bool
* TRUE if Layout Builder is enabled, FALSE otherwise.
*/
public function isLayoutBuilderEnabled();
/**
* Enables the Layout Builder.
*
* @return $this
*/
public function enableLayoutBuilder();
/**
* Disables the Layout Builder.
*
* @return $this
*/
public function disableLayoutBuilder();
}

View file

@ -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';
}

View file

@ -0,0 +1,40 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Sets the layout_builder.get_block_dependency_subscriber service definition.
*
* This service is dependent on the block_content module so it must be provided
* dynamically.
*
* @internal
*
* @see \Drupal\layout_builder\EventSubscriber\SetInlineBlockDependency
*/
class LayoutBuilderServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$modules = $container->getParameter('container.modules');
if (isset($modules['block_content'])) {
$definition = new Definition(SetInlineBlockDependency::class);
$definition->setArguments([
new Reference('entity_type.manager'),
new Reference('database'),
new Reference('inline_block.usage'),
]);
$definition->addTag('event_subscriber');
$container->setDefinition('layout_builder.get_block_dependency_subscriber', $definition);
}
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Methods to help with entities using the layout builder.
*
* @internal
*/
trait LayoutEntityHelperTrait {
/**
* Determines if an entity can have a layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check.
*
* @return bool
* TRUE if the entity can have a layout otherwise FALSE.
*/
protected function isLayoutCompatibleEntity(EntityInterface $entity) {
return $entity instanceof LayoutEntityDisplayInterface || $this->isEntityUsingFieldOverride($entity);
}
/**
* Gets revision IDs for layout sections.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return int[]
* The revision IDs.
*/
protected function getInlineBlockRevisionIdsInSections(array $sections) {
$revision_ids = [];
foreach ($this->getInlineBlockComponents($sections) as $component) {
$configuration = $component->getPlugin()->getConfiguration();
if (!empty($configuration['block_revision_id'])) {
$revision_ids[] = $configuration['block_revision_id'];
}
}
return $revision_ids;
}
/**
* Gets the sections for an entity if any.
*
* @todo Replace this method with calls to the SectionStorageManagerInterface
* method for getting sections from an entity in
* https://www.drupal.org/node/2986403.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\layout_builder\Section[]|null
* The entity layout sections if available.
*/
protected function getEntitySections(EntityInterface $entity) {
if ($entity instanceof LayoutEntityDisplayInterface) {
return $entity->getSections();
}
elseif ($this->isEntityUsingFieldOverride($entity)) {
return $entity->get('layout_builder__layout')->getSections();
}
return NULL;
}
/**
* Gets components that have Inline Block plugins.
*
* @param \Drupal\layout_builder\Section[] $sections
* The layout sections.
*
* @return \Drupal\layout_builder\SectionComponent[]
* The components that contain Inline Block plugins.
*/
protected function getInlineBlockComponents(array $sections) {
$inline_block_components = [];
foreach ($sections as $section) {
foreach ($section->getComponents() as $component) {
$plugin = $component->getPlugin();
if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') {
$inline_block_components[] = $component;
}
}
}
return $inline_block_components;
}
/**
* Determines if an entity is using a field for the layout override.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return bool
* TRUE if the entity is using a field for a layout override.
*/
protected function isEntityUsingFieldOverride(EntityInterface $entity) {
return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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\DefaultsSectionStorageInterface
* The defaults section storage.
*
* @todo Determine if this method needs a parameter in
* https://www.drupal.org/project/drupal/issues/2936507.
*/
public function getDefaultSectionStorage();
}

View file

@ -0,0 +1,174 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
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 an extra field from an entity.
*
* This block handles fields that are provided by implementations of
* hook_entity_extra_field_info().
*
* @see \Drupal\layout_builder\Plugin\Block\FieldBlock
* This block plugin handles all other field entities not provided by
* hook_entity_extra_field_info().
*
* @Block(
* id = "extra_field_block",
* deriver = "\Drupal\layout_builder\Plugin\Derivative\ExtraFieldBlockDeriver",
* )
*
* @internal
* Plugin classes are internal.
*/
class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface, ContainerFactoryPluginInterface {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The field name.
*
* @var string
*/
protected $fieldName;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ExtraFieldBlock.
*
* @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\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->entityFieldManager = $entity_field_manager;
// Get field name from the plugin ID.
list (, , , $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
assert(!empty($field_name));
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'label_display' => FALSE,
];
}
/**
* {@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')
);
}
/**
* 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() {
$entity = $this->getEntity();
// Add a placeholder to replace after the entity view is built.
// @see layout_builder_entity_view_alter().
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
if (!isset($extra_fields['display'][$this->fieldName])) {
$build = [];
}
else {
$build = [
'#extra_field_placeholder_field_name' => $this->fieldName,
// Always provide a placeholder. The Layout Builder will NOT invoke
// hook_entity_view_alter() so extra fields will not be added to the
// render array. If the hook is invoked the placeholder will be
// replaced.
// @see ::replaceFieldPlaceholder()
'#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]),
];
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* Replaces all placeholders for a given field.
*
* @param array $build
* The built render array for the elements.
* @param array $built_field
* The render array to replace the placeholder.
* @param string $field_name
* The field name.
*
* @see ::build()
*/
public static function replaceFieldPlaceholder(array &$build, array $built_field, $field_name) {
foreach (Element::children($build) as $child) {
if (isset($build[$child]['#extra_field_placeholder_field_name']) && $build[$child]['#extra_field_placeholder_field_name'] === $field_name) {
$placeholder_cache = CacheableMetadata::createFromRenderArray($build[$child]);
$built_cache = CacheableMetadata::createFromRenderArray($built_field);
$merged_cache = $placeholder_cache->merge($built_cache);
$build[$child] = $built_field;
$merged_cache->applyTo($build);
}
else {
static::replaceFieldPlaceholder($build[$child], $built_field, $field_name);
}
}
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return $this->getEntity()->access('view', $account, TRUE);
}
}

View file

@ -0,0 +1,395 @@
<?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 Psr\Log\LoggerInterface;
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;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* 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.
* @param \Psr\Log\LoggerInterface $logger
* The logger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityFieldManagerInterface $entity_field_manager, FormatterPluginManager $formatter_manager, ModuleHandlerInterface $module_handler, LoggerInterface $logger) {
$this->entityFieldManager = $entity_field_manager;
$this->formatterManager = $formatter_manager;
$this->moduleHandler = $module_handler;
$this->logger = $logger;
// 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'),
$container->get('logger.channel.layout_builder')
);
}
/**
* 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();
try {
$build = $entity->get($this->fieldName)->view($display_settings);
}
catch (\Exception $e) {
$build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
}
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,
]);
}
}

View file

@ -0,0 +1,283 @@
<?php
namespace Drupal\layout_builder\Plugin\Block;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\block_content\Access\RefinableDependentAccessTrait;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines an inline block plugin type.
*
* @Block(
* id = "inline_block",
* admin_label = @Translation("Inline block"),
* category = @Translation("Inline blocks"),
* deriver = "Drupal\layout_builder\Plugin\Derivative\InlineBlockDeriver",
* )
*
* @internal
* Plugin classes are internal.
*/
class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface, RefinableDependentAccessInterface {
use RefinableDependentAccessTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $blockContent;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* Whether a new block is being created.
*
* @var bool
*/
protected $isNew = TRUE;
/**
* Constructs a new InlineBlock.
*
* @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\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
$this->entityDisplayRepository = $entity_display_repository;
if (!empty($this->configuration['block_revision_id']) || !empty($this->configuration['block_serialized'])) {
$this->isNew = FALSE;
}
}
/**
* {@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_display.repository')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'view_mode' => 'full',
'block_revision_id' => NULL,
'block_serialized' => NULL,
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$block = $this->getEntity();
// Add the entity form display in a process callback so that #parents can
// be successfully propagated to field widgets.
$form['block_form'] = [
'#type' => 'container',
'#process' => [[static::class, 'processBlockForm']],
'#block' => $block,
];
$options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
$form['view_mode'] = [
'#type' => 'select',
'#options' => $options,
'#title' => $this->t('View mode'),
'#description' => $this->t('The view mode in which to render the block.'),
'#default_value' => $this->configuration['view_mode'],
'#access' => count($options) > 1,
];
return $form;
}
/**
* Process callback to insert a Custom Block form.
*
* @param array $element
* The containing element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The containing element, with the Custom Block form inserted.
*/
public static function processBlockForm(array $element, FormStateInterface $form_state) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $element['#block'];
EntityFormDisplay::collectRenderDisplay($block, 'edit')->buildForm($block, $element, $form_state);
$element['revision_log']['#access'] = FALSE;
$element['info']['#access'] = FALSE;
return $element;
}
/**
* {@inheritdoc}
*/
public function blockValidate($form, FormStateInterface $form_state) {
$block_form = $form['block_form'];
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$form_display->validateFormValues($block, $block_form, $complete_form_state);
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$form_state->setTemporaryValue('block_form_parents', $block_form['#parents']);
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['view_mode'] = $form_state->getValue('view_mode');
// @todo Remove when https://www.drupal.org/project/drupal/issues/2948549 is closed.
$block_form = NestedArray::getValue($form, $form_state->getTemporaryValue('block_form_parents'));
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = $block_form['#block'];
$form_display = EntityFormDisplay::collectRenderDisplay($block, 'edit');
$complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
$form_display->extractFormValues($block, $block_form, $complete_form_state);
$block->setInfo($this->configuration['label']);
$this->configuration['block_serialized'] = serialize($block);
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($entity = $this->getEntity()) {
return $entity->access('view', $account, TRUE);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
$block = $this->getEntity();
return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']);
}
/**
* Loads or creates the block content entity of the block.
*
* @return \Drupal\block_content\BlockContentInterface
* The block content entity.
*/
protected function getEntity() {
if (!isset($this->blockContent)) {
if (!empty($this->configuration['block_serialized'])) {
$this->blockContent = unserialize($this->configuration['block_serialized']);
}
elseif (!empty($this->configuration['block_revision_id'])) {
$entity = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
$this->blockContent = $entity;
}
else {
$this->blockContent = $this->entityTypeManager->getStorage('block_content')->create([
'type' => $this->getDerivativeId(),
'reusable' => FALSE,
]);
}
if ($this->blockContent instanceof RefinableDependentAccessInterface && $dependee = $this->getAccessDependency()) {
$this->blockContent->setAccessDependency($dependee);
}
}
return $this->blockContent;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
if ($this->isNew) {
// If the Content Block is new then don't provide a default label.
unset($form['label']['#default_value']);
}
$form['label']['#description'] = $this->t('The title of the block as shown to the user.');
return $form;
}
/**
* Saves the block_content entity for this plugin.
*
* @param bool $new_revision
* Whether to create new revision.
* @param bool $duplicate_block
* Whether to duplicate the "block_content" entity.
*/
public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE) {
/** @var \Drupal\block_content\BlockContentInterface $block */
$block = NULL;
if (!empty($this->configuration['block_serialized'])) {
$block = unserialize($this->configuration['block_serialized']);
}
if ($duplicate_block) {
if (empty($block) && !empty($this->configuration['block_revision_id'])) {
$block = $this->entityTypeManager->getStorage('block_content')->loadRevision($this->configuration['block_revision_id']);
}
if ($block) {
$block = $block->createDuplicate();
}
}
if ($block) {
if ($new_revision) {
$block->setNewRevision();
}
$block->save();
$this->configuration['block_revision_id'] = $block->getRevisionId();
$this->configuration['block_serialized'] = NULL;
}
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,115 @@
<?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\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Context\EntityContextDefinition;
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
* 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 ExtraFieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs new FieldBlockDeriver.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
// Only process fieldable entity types.
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
continue;
}
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
foreach ($bundles as $bundle_id => $bundle) {
$extra_fields = $this->entityFieldManager->getExtraFields($entity_type_id, $bundle_id);
// Skip bundles without any extra fields.
if (empty($extra_fields['display'])) {
continue;
}
foreach ($extra_fields['display'] as $extra_field_id => $extra_field) {
$derivative = $base_plugin_definition;
$derivative['category'] = $entity_type->getLabel();
$derivative['admin_label'] = $extra_field['label'];
$context_definition = EntityContextDefinition::fromEntityType($entity_type)
->addConstraint('Bundle', [$bundle_id]);
$derivative['context'] = [
'entity' => $context_definition,
];
$derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle_id . PluginBase::DERIVATIVE_SEPARATOR . $extra_field_id;
$this->derivatives[$derivative_id] = $derivative;
}
}
}
return $this->derivatives;
}
}

View file

@ -0,0 +1,135 @@
<?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\EntityContextDefinition;
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');
$context_definition = EntityContextDefinition::fromEntityTypeId($entity_type_id)->setLabel($entity_type_labels[$entity_type_id]);
$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;
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides inline block plugin definitions for all custom block types.
*
* @internal
*/
class InlineBlockDeriver extends DeriverBase implements ContainerDeriverInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a BlockContentDeriver object.
*
* @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) {
$this->derivatives = [];
if ($this->entityTypeManager->hasDefinition('block_content_type')) {
$block_content_types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple();
foreach ($block_content_types as $id => $type) {
$this->derivatives[$id] = $base_plugin_definition;
$this->derivatives[$id]['admin_label'] = $type->label();
$this->derivatives[$id]['config_dependencies'][$type->getConfigDependencyKey()][] = $type->getConfigDependencyName();
}
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View file

@ -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');
});
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,361 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
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\EntityContext;
use Drupal\Core\Session\AccountInterface;
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($rel = 'view') {
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.$rel", $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());
$contexts = [];
$contexts['layout_builder.entity'] = EntityContext::fromEntity($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;
}
/**
* {@inheritdoc}
*/
public function setThirdPartySetting($module, $key, $value) {
$this->getDisplay()->setThirdPartySetting($module, $key, $value);
return $this;
}
/**
* {@inheritdoc}
*/
public function isLayoutBuilderEnabled() {
return $this->getDisplay()->isLayoutBuilderEnabled();
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->getDisplay()->enableLayoutBuilder();
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->getDisplay()->disableLayoutBuilder();
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartySetting($module, $key, $default = NULL) {
return $this->getDisplay()->getThirdPartySetting($module, $key, $default);
}
/**
* {@inheritdoc}
*/
public function getThirdPartySettings($module) {
return $this->getDisplay()->getThirdPartySettings($module);
}
/**
* {@inheritdoc}
*/
public function unsetThirdPartySetting($module, $key) {
$this->getDisplay()->unsetThirdPartySetting($module, $key);
return $this;
}
/**
* {@inheritdoc}
*/
public function getThirdPartyProviders() {
return $this->getDisplay()->getThirdPartyProviders();
}
/**
* {@inheritdoc}
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$result = AccessResult::allowedIf($this->isLayoutBuilderEnabled());
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -0,0 +1,243 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Core\Access\AccessResult;
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\EntityContext;
use Drupal\Core\Session\AccountInterface;
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() {
// @todo Expand to work for all view modes in
// https://www.drupal.org/node/2907413.
return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'full');
}
/**
* {@inheritdoc}
*/
public function getRedirectUrl() {
return $this->getEntity()->toUrl('canonical');
}
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl($rel = 'view') {
$entity = $this->getEntity();
$route_parameters[$entity->getEntityTypeId()] = $entity->id();
return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters);
}
/**
* {@inheritdoc}
*/
public function getContexts() {
$entity = $this->getEntity();
$contexts['layout_builder.entity'] = EntityContext::fromEntity($entity);
return $contexts;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getEntity()->label();
}
/**
* {@inheritdoc}
*/
public function save() {
return $this->getEntity()->save();
}
/**
* {@inheritdoc}
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$default_section_storage = $this->getDefaultSectionStorage();
$result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage);
return $return_as_object ? $result : $result->isAllowed();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Drupal\layout_builder\Routing;
use Drupal\Component\Utility\NestedArray;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
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';
$requirements['_layout_builder_access'] = 'view';
// 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);
}
elseif (is_subclass_of($definition->getClass(), DefaultsSectionStorageInterface::class)) {
$disable_defaults = $defaults;
$disable_defaults['_form'] = '\Drupal\layout_builder\Form\LayoutBuilderDisableForm';
$disable_options = $options;
unset($disable_options['_admin_route'], $disable_options['_layout_builder']);
$route = (new Route("$path/disable"))
->setDefaults($disable_defaults)
->setRequirements($requirements)
->setOptions($disable_options);
$collection->add("$route_name_prefix.disable", $route);
}
}
}

View file

@ -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']);
}
}

View file

@ -0,0 +1,359 @@
<?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.
*
* Only use this method if you are implementing custom storage 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()),
];
}
/**
* Creates an object from an array representation of the section.
*
* Only use this method if you are implementing custom storage for sections.
*
* @param array $section
* An array of section data in the format returned by ::toArray().
*
* @return static
* The section object.
*/
public static function fromArray(array $section) {
return new static(
$section['layout_id'],
$section['layout_settings'],
array_map([SectionComponent::class, 'fromArray'], $section['components'])
);
}
}

View file

@ -0,0 +1,330 @@
<?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.
*
* Only use this method if you are implementing custom storage for sections.
*
* @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(),
];
}
/**
* Creates an object from an array representation of the section component.
*
* Only use this method if you are implementing custom storage for sections.
*
* @param array $component
* An array of section component data in the format returned by ::toArray().
*
* @return static
* The section component object.
*/
public static function fromArray(array $component) {
return (new static(
$component['uuid'],
$component['region'],
$component['configuration'],
$component['additional']
))->setWeight($component['weight']);
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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.
}
}
}
}

View file

@ -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);
}

View file

@ -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]);
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace Drupal\layout_builder;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Access\AccessibleInterface;
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, AccessibleInterface {
/**
* 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.
*
* @param string $rel
* (optional) The link relationship type, for example: 'view' or 'disable'.
* Defaults to 'view'.
*
* @return \Drupal\Core\Url
* The URL object.
*/
public function getLayoutBuilderUrl($rel = 'view');
/**
* 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();
}

View file

@ -0,0 +1,44 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Add a layout plugin to an existing entity view display without explicitly
// enabling Layout Builder for this display.
$display = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_view_display.block_content.basic.default')
->execute()
->fetchField();
$display = unserialize($display);
$display['third_party_settings']['layout_builder']['sections'][] = [
'layout_id' => 'layout_onecol',
'layout_settings' => [],
'components' => [
'some-uuid' => [
'uuid' => 'some-uuid',
'region' => 'content',
'configuration' => [
'id' => 'system_powered_by_block',
],
'additional' => [],
'weight' => 0,
],
],
];
$connection->update('config')
->fields([
'data' => serialize($display),
'collection' => '',
'name' => 'core.entity_view_display.block_content.basic.default',
])
->condition('collection', '')
->condition('name', 'core.entity_view_display.block_content.basic.default')
->execute();

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Enable Layout Builder on an existing entity view display.
$display = $connection->select('config')
->fields('config', ['data'])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.article.default')
->execute()
->fetchField();
$display = unserialize($display);
$display['third_party_settings']['layout_builder']['enabled'] = TRUE;
$connection->update('config')
->fields([
'data' => serialize($display),
'collection' => '',
'name' => 'core.entity_view_display.node.article.default',
])
->condition('collection', '')
->condition('name', 'core.entity_view_display.node.article.default')
->execute();

View file

@ -0,0 +1,42 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->merge('key_value')
->fields([
'value' => 'i:8000;',
'name' => 'layout_builder',
'collection' => 'system.schema',
])
->condition('collection', 'system.schema')
->condition('name', 'layout_builder')
->execute();
// 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();

View file

@ -0,0 +1,33 @@
<?php
/**
* @file
* Test fixture.
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// 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();

View file

@ -0,0 +1,6 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,60 @@
<?php
/**
* @file
* Provides hook implementations for Layout Builder tests.
*/
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_test_plugin_filter_block__layout_builder_alter(array &$definitions, array $extra) {
// Explicitly remove the "Help" blocks from the list.
unset($definitions['help_block']);
// Explicitly remove the "Sticky at top of lists field_block".
$disallowed_fields = [
'sticky',
];
// Remove "Changed" field if this is the first section.
if ($extra['delta'] === 0) {
$disallowed_fields[] = 'changed';
}
foreach ($definitions as $plugin_id => $definition) {
// Field block IDs are in the form 'field_block:{entity}:{bundle}:{name}',
// for example 'field_block:node:article:revision_timestamp'.
preg_match('/field_block:.*:.*:(.*)/', $plugin_id, $parts);
if (isset($parts[1]) && in_array($parts[1], $disallowed_fields, TRUE)) {
// Unset any field blocks that match our predefined list.
unset($definitions[$plugin_id]);
}
}
}
/**
* Implements hook_entity_extra_field_info().
*/
function layout_builder_test_entity_extra_field_info() {
$extra['node']['bundle_with_section_field']['display']['layout_builder_test'] = [
'label' => t('Extra label'),
'description' => t('Extra description'),
'weight' => 0,
];
return $extra;
}
/**
* Implements hook_entity_node_view().
*/
function layout_builder_test_node_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if ($display->getComponent('layout_builder_test')) {
$build['layout_builder_test'] = [
'#markup' => 'Extra, Extra read all about it.',
];
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Drupal\layout_builder_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a 'TestAjax' block.
*
* @Block(
* id = "layout_builder_test_testajax",
* admin_label = @Translation("TestAjax"),
* category = @Translation("Test")
* )
*/
class TestAjaxBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$form['ajax_test'] = [
'#type' => 'radios',
'#options' => [
1 => $this->t('Ajax test option 1'),
2 => $this->t('Ajax test option 2'),
],
'#prefix' => '<div id="test-ajax-wrapper">',
'#suffix' => '</div>',
'#title' => $this->t('Time in this ajax test is @time', [
'@time' => time(),
]),
'#ajax' => [
'wrapper' => 'test-ajax-wrapper',
'callback' => [$this, 'ajaxCallback'],
],
];
return $form;
}
/**
* Ajax callback.
*/
public function ajaxCallback($form, $form_state) {
return $form['settings']['ajax_test'];
}
/**
* {@inheritdoc}
*/
public function build() {
$build['content'] = [
'#markup' => $this->t('Every word is like an unnecessary stain on silence and nothingness.'),
];
return $build;
}
}

View file

@ -0,0 +1,177 @@
langcode: en
status: true
dependencies:
module:
- node
- user
id: test_block_view
label: 'Test Block View'
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: some
options:
items_per_page: 5
offset: 0
style:
type: default
row:
type: fields
fields:
title:
id: title
table: node_field_data
field: title
settings:
link_to_entity: true
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters:
status:
value: '1'
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
title: 'Test Block View'
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }
block_1:
display_plugin: block
id: block_1
display_title: Block
position: 1
display_options:
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- 'user.node_grants:view'
- user.permissions
tags: { }

View file

@ -0,0 +1,8 @@
name: 'Layout Builder Views Test'
type: module
description: 'Support module for testing.'
package: Testing
version: VERSION
core: 8.x
dependencies:
- views

View file

@ -0,0 +1,23 @@
/**
* Remove all transitions for testing.
*/
* {
/* CSS transitions. */
-o-transition-property: none !important;
-moz-transition-property: none !important;
-ms-transition-property: none !important;
-webkit-transition-property: none !important;
transition-property: none !important;
/* CSS transforms. */
-o-transform: none !important;
-moz-transform: none !important;
-ms-transform: none !important;
-webkit-transform: none !important;
transform: none !important;
/* CSS animations. */
-webkit-animation: none !important;
-moz-animation: none !important;
-o-animation: none !important;
-ms-animation: none !important;
animation: none !important;
}

View file

@ -0,0 +1,6 @@
name: 'CSS Test fix'
type: module
description: 'Provides CSS fixes for tests.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,5 @@
drupal.css_fix:
version: VERSION
css:
theme:
css/css_fix.theme.css: {}

View file

@ -0,0 +1,16 @@
<?php
/**
* @file
* Module for attaching CSS during tests.
*
* CSS pointer-events properties cause testing errors.
*/
/**
* Implements hook_page_attachments().
*/
function settings_tray_test_css_page_attachments(array &$attachments) {
// Unconditionally attach an asset to the page.
$attachments['#attached']['library'][] = 'settings_tray_test_css/drupal.css_fix';
}

View file

@ -0,0 +1,526 @@
<?php
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\node\Entity\Node;
use Drupal\Tests\BrowserTestBase;
use Drupal\views\Entity\View;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'views',
'layout_builder',
'layout_builder_views_test',
'layout_test',
'block',
'node',
'layout_builder_test',
];
/**
* {@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->linkNotExists('Manage layout');
$assert_session->fieldDisabled('layout[allow_custom]');
$this->drupalPostForm(NULL, ['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
$assert_session->addressEquals("$field_ui_prefix/display-layout/default");
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
// Save the defaults.
$assert_session->linkExists('Save Layout');
$this->clickLink('Save Layout');
$assert_session->addressEquals("$field_ui_prefix/display/default");
// Load the default layouts again after saving to confirm fields are only
// added on new layouts.
$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 only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
// 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->pageTextContains('The layout has been saved.');
$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->pageTextContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$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->pageTextContains('Placeholder for the "Extra label" field');
$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_session->pageTextNotContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
// Assert that overrides cannot be turned off while overrides exist.
$this->drupalGet("$field_ui_prefix/display/default");
$assert_session->checkboxChecked('layout[allow_custom]');
$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');
$assert_session->pageTextContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
// 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->pageTextNotContains('Extra, Extra read all about it.');
$assert_session->pageTextNotContains('Placeholder for the "Extra label" field');
$assert_session->linkExists('Layout');
// Reverting the override returns it to the defaults.
$this->clickLink('Layout');
$assert_session->linkExists('Add Block');
$this->clickLink('Add Block');
$assert_session->linkExists('ID');
$this->clickLink('ID');
$page->pressButton('Add Block');
// The title field is present.
$assert_session->elementExists('css', '.field--name-nid');
$assert_session->pageTextContains('ID');
$assert_session->pageTextContains('1');
$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->elementNotExists('css', '.field--name-nid');
$assert_session->pageTextContains('The first node body');
$assert_session->pageTextContains('Powered by Drupal');
$assert_session->pageTextContains('Placeholder for the "Extra label" field');
// 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 a non-default view mode works as expected.
*/
public function testNonDefaultViewMode() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Allow overrides for the layout.
$this->drupalGet("$field_ui_prefix/display/default");
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$page->checkField('layout[allow_custom]');
$page->pressButton('Save');
$this->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
$this->clickLink('Cancel Layout');
$this->clickLink('Teaser');
// Enabling Layout Builder for the default mode does not affect the teaser.
$assert_session->addressEquals("$field_ui_prefix/display/teaser");
$assert_session->elementNotExists('css', '#layout-builder__layout');
$assert_session->checkboxNotChecked('layout[enabled]');
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$assert_session->linkExists('Manage layout');
$page->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// Enable a disabled view mode.
$page->clickLink('Cancel Layout');
$assert_session->addressEquals("$field_ui_prefix/display/teaser");
$page->clickLink('Default');
$assert_session->addressEquals("$field_ui_prefix/display");
$assert_session->linkNotExists('Full content');
$page->checkField('display_modes_custom[full]');
$page->pressButton('Save');
$assert_session->linkExists('Full content');
$page->clickLink('Full content');
$assert_session->addressEquals("$field_ui_prefix/display/full");
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$assert_session->linkExists('Manage layout');
$page->clickLink('Manage layout');
// Confirm the body field only is shown once.
$assert_session->elementsCount('css', '.field--name-body', 1);
}
/**
* 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->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
$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');
}
/**
* Tests the interaction between full and default view modes.
*
* @see \Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage::getDefaultSectionStorage()
*/
public function testLayoutBuilderUiFullViewMode() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Allow overrides for the layout.
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display-layout/default");
$this->clickLink('Add Block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is the default view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add Block');
$assert_session->pageTextContains('This is the default view mode');
$this->clickLink('Save Layout');
// The default view mode is used for both the node display and layout UI.
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the default view mode');
$this->clickLink('Cancel Layout');
// Enable the full view mode and customize it.
$this->drupalPostForm("$field_ui_prefix/display/default", ['display_modes_custom[full]' => TRUE], 'Save');
$this->drupalPostForm("$field_ui_prefix/display/full", ['layout[enabled]' => TRUE], 'Save');
$this->drupalGet("$field_ui_prefix/display-layout/full");
$this->clickLink('Add Block');
$this->clickLink('Powered by Drupal');
$page->fillField('settings[label]', 'This is the full view mode');
$page->checkField('settings[label_display]');
$page->pressButton('Add Block');
$assert_session->pageTextContains('This is the full view mode');
$this->clickLink('Save Layout');
// The full view mode is now used for both the node display and layout UI.
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the full view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the full view mode');
$this->clickLink('Cancel Layout');
// Disable the full view mode, the default should be used again.
$this->drupalPostForm("$field_ui_prefix/display/default", ['display_modes_custom[full]' => FALSE], 'Save');
$this->drupalGet('node/1');
$assert_session->pageTextContains('This is the default view mode');
$this->drupalGet('node/1/layout');
$assert_session->pageTextContains('This is the default view mode');
$this->clickLink('Cancel Layout');
}
/**
* {@inheritdoc}
*/
public function testLayoutBuilderChooseBlocksAlter() {
// See layout_builder_test_plugin_filter_block__layout_builder_alter().
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
// From the manage display page, go to manage the layout.
$this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display/default', ['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
// Add a new block.
$this->clickLink('Add Block');
// Verify that blocks not modified are present.
$assert_session->linkExists('Powered by Drupal');
$assert_session->linkExists('Default revision');
// Verify that blocks explicitly removed are not present.
$assert_session->linkNotExists('Help');
$assert_session->linkNotExists('Sticky at top of lists');
// Verify that Changed block is not present on first section.
$assert_session->linkNotExists('Changed');
// Go back to Manage layout.
$this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default');
$this->clickLink('Manage layout');
// Add a new section.
$this->clickLink('Add Section', 1);
$assert_session->linkExists('Two column');
$this->clickLink('Two column');
// Add a new block to second section.
$this->clickLink('Add Block', 1);
// Verify that Changed block is present on second section.
$assert_session->linkExists('Changed');
}
/**
* Tests that deleting a View block used in Layout Builder works.
*/
public function testDeletedView() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// Enable overrides.
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
$this->drupalGet('node/1');
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$this->clickLink('Add Block');
$this->clickLink('Test Block View');
$page->pressButton('Add Block');
$assert_session->pageTextContains('Test Block View');
$assert_session->elementExists('css', '.block-views-blocktest-block-view-block-1');
$this->clickLink('Save Layout');
$assert_session->pageTextContains('Test Block View');
$assert_session->elementExists('css', '.block-views-blocktest-block-view-block-1');
View::load('test_block_view')->delete();
$this->drupalGet('node/1');
// Node can be loaded after deleting the View.
$assert_session->pageTextContains(Node::load(1)->getTitle());
$assert_session->pageTextNotContains('Test Block View');
}
/**
* Asserts that a text string only appears once on the page.
*
* @param string $needle
* The string to look for.
*/
protected function assertTextAppearsOnce($needle) {
$this->assertEquals(1, substr_count($this->getSession()->getPage()->getContent(), $needle), "'$needle' only appears once on the page.");
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\Tests\layout_builder\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests functionality of the entity view display with regard to Layout Builder.
*
* @group layout_builder
*/
class LayoutDisplayTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['field_ui', 'layout_builder', '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');
$this->createContentType([
'type' => 'bundle_with_section_field',
]);
$this->createNode(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer display modes',
], 'foobar'));
}
/**
* Tests the interaction between multiple view modes.
*/
public function testMultipleViewModes() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field/display';
// Enable Layout Builder for the default view modes, and overrides.
$this->drupalGet("$field_ui_prefix/default");
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$page->checkField('layout[allow_custom]');
$page->pressButton('Save');
$this->drupalGet('node/1');
$assert_session->pageTextNotContains('Powered by Drupal');
$assert_session->linkExists('Layout');
$this->clickLink('Layout');
$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_session->linkExists('Save Layout');
$this->clickLink('Save Layout');
$assert_session->pageTextContains('Powered by Drupal');
// Add a new view mode.
$this->drupalGet('admin/structure/display-modes/view/add/node');
$page->fillField('label', 'New');
$page->fillField('id', 'new');
$page->pressButton('Save');
// Enable the new view mode.
$this->drupalGet("$field_ui_prefix/default");
$page->checkField('display_modes_custom[new]');
$page->pressButton('Save');
// Enable and disable Layout Builder for the new view mode.
$this->drupalGet("$field_ui_prefix/new");
$page->checkField('layout[enabled]');
$page->pressButton('Save');
$page->uncheckField('layout[enabled]');
$page->pressButton('Save');
$page->pressButton('Confirm');
// The node using the default view mode still contains its overrides.
$this->drupalGet('node/1');
$assert_session->pageTextContains('Powered by Drupal');
}
}

View file

@ -0,0 +1,380 @@
<?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')
->enableLayoutBuilder()
->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,
]);
}
}

View file

@ -0,0 +1,53 @@
<?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 extra fields.
*
* @group layout_builder
* @group legacy
*/
class ExtraFieldUpdatePathTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/layout-builder.php',
__DIR__ . '/../../../fixtures/update/layout-builder-extra.php',
];
}
/**
* Tests the upgrade path for Layout Builder extra fields.
*/
public function testRunUpdates() {
// The default view mode has Layout Builder enabled.
$data = EntityViewDisplay::load('node.article.default')->toArray();
$this->assertArrayHasKey('third_party_settings', $data);
$this->assertArrayNotHasKey('sections', $data['third_party_settings']['layout_builder']);
// The teaser view mode does not have Layout Builder enabled.
$data = EntityViewDisplay::load('node.article.teaser')->toArray();
$this->assertArrayNotHasKey('third_party_settings', $data);
$this->runUpdates();
// The extra links have been added.
$data = EntityViewDisplay::load('node.article.default')->toArray();
$components = $data['third_party_settings']['layout_builder']['sections'][0]->getComponents();
$component = reset($components);
$this->assertSame('extra_field_block:node:article:links', $component->getPluginId());
// No extra links have been added.
$data = EntityViewDisplay::load('node.article.teaser')->toArray();
$this->assertArrayNotHasKey('third_party_settings', $data);
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Drupal\Tests\layout_builder\Functional\Update;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests the upgrade path for enabling Layout Builder.
*
* @see layout_builder_update_8601()
*
* @group layout_builder
* @group legacy
*/
class LayoutBuilderEnableUpdatePathTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
__DIR__ . '/../../../fixtures/update/layout-builder.php',
__DIR__ . '/../../../fixtures/update/layout-builder-enable.php',
];
}
/**
* Tests the upgrade path for enabling Layout Builder.
*/
public function testRunUpdates() {
$assert_session = $this->assertSession();
$expected = [
'sections' => [
[
'layout_id' => 'layout_onecol',
'layout_settings' => [],
'components' => [
'some-uuid' => [
'uuid' => 'some-uuid',
'region' => 'content',
'configuration' => [
'id' => 'system_powered_by_block',
],
'additional' => [],
'weight' => 0,
],
],
],
],
];
$this->assertLayoutBuilderSettings($expected, 'block_content', 'basic', 'default');
$this->assertLayoutBuilderSettings(NULL, 'node', 'page', 'default');
$this->runUpdates();
// The display with existing sections is enabled while the other is not.
$expected['enabled'] = TRUE;
$this->assertLayoutBuilderSettings($expected, 'block_content', 'basic', 'default');
$this->assertLayoutBuilderSettings(NULL, 'node', 'page', 'default');
$this->drupalLogin($this->rootUser);
$this->drupalGet('admin/structure/block/block-content/manage/basic/display');
$assert_session->checkboxChecked('layout[enabled]');
$this->drupalGet('admin/structure/types/manage/page/display');
$assert_session->checkboxNotChecked('layout[enabled]');
}
/**
* Asserts the Layout Builder settings for a given display.
*
* @param mixed $expected
* The expected value.
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle
* The bundle.
* @param string $view_mode
* The view mode.
*/
protected function assertLayoutBuilderSettings($expected, $entity_type_id, $bundle, $view_mode) {
$this->assertEquals($expected, \Drupal::config("core.entity_view_display.$entity_type_id.$bundle.$view_mode")->get('third_party_settings.layout_builder'));
}
}

View file

@ -0,0 +1,44 @@
<?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
* @group legacy
*/
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/layout-builder.php',
__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']);
}
}

View file

@ -0,0 +1,97 @@
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Ajax blocks tests.
*
* @group layout_builder
*/
class AjaxBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'block',
'node',
'datetime',
'layout_builder',
'user',
'layout_builder_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$user = $this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]);
$user->save();
$this->drupalLogin($user);
$this->createContentType(['type' => 'bundle_with_section_field']);
}
/**
* Tests configuring a field block for a user field.
*/
public function testAddAjaxBlock() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Start by creating a node.
$node = $this->createNode([
'type' => 'bundle_with_section_field',
'body' => [
[
'value' => 'The node body',
],
],
]);
$node->save();
$this->drupalGet('node/1');
$assert_session->pageTextContains('The node body');
$assert_session->pageTextNotContains('Every word is like an unnecessary stain on silence and nothingness.');
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
// From the manage display page, go to manage the layout.
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
$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->assertWaitOnAjaxRequest();
$assert_session->linkExists('TestAjax');
$this->clickLink('TestAjax');
$assert_session->assertWaitOnAjaxRequest();
// Find the radio buttons.
$name = 'settings[ajax_test]';
/** @var \Behat\Mink\Element\NodeElement[] $radios */
$radios = $this->cssSelect('input[name="' . $name . '"]');
// Click them both a couple of times.
foreach ([1, 2] as $rounds) {
foreach ($radios as $radio) {
$radio->click();
$assert_session->assertWaitOnAjaxRequest();
}
}
// Then add the block.
$page->pressButton('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$block_elements = $this->cssSelect('.block-layout-builder-test-testajax');
// Should be exactly one of these in there.
$this->assertEquals(1, count($block_elements));
$assert_session->pageTextContains('Every word is like an unnecessary stain on silence and nothingness.');
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* @coversDefaultClass \Drupal\layout_builder\Plugin\Block\FieldBlock
*
* @group field
*/
class FieldBlockTest extends WebDriverTestBase {
/**
* {@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');
}
}

View file

@ -0,0 +1,291 @@
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\file\Entity\File;
use Drupal\file\FileInterface;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\file\Functional\FileFieldCreationTrait;
use Drupal\Tests\TestFileCreationTrait;
/**
* Test access to private files in block fields on the Layout Builder.
*
* @group layout_builder
*/
class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
use FileFieldCreationTrait;
use TestFileCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'file',
];
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Update the test node type to not create new revisions by default. This
// allows testing for cases when a new revision is made and when it isn't.
$node_type = NodeType::load('bundle_with_section_field');
$node_type->setNewRevision(FALSE);
$node_type->save();
$field_settings = [
'file_extensions' => 'txt',
'uri_scheme' => 'private',
];
$this->createFileField('field_file', 'block_content', 'basic', $field_settings);
$this->fileSystem = $this->container->get('file_system');
}
/**
* Test access to private files added via inline blocks in the layout builder.
*/
public function testPrivateFiles() {
$assert_session = $this->assertSession();
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
]));
// Enable layout builder and overrides.
$this->drupalPostForm(
static::FIELD_UI_PREFIX . '/display/default',
['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
'Save'
);
$this->drupalLogout();
// Log in as user you can only configure layouts and access content.
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'access content',
]));
$this->drupalGet('node/1/layout');
$file = $this->createPrivateFile('drupal.txt');
$file_real_path = $this->fileSystem->realpath($file->getFileUri());
$this->assertFileExists($file_real_path);
$this->addInlineFileBlockToLayout('The file', $file);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href1 = $this->assertFileAccessibleOnNode($file);
// Remove the inline block with the private file.
$this->drupalGet('node/1/layout');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
$this->drupalGet('node/1');
$assert_session->pageTextNotContains($file->label());
// Try to access file directly after it has been removed. Since a new
// revision was not created for the node the inline block is not in the
// layout of a previous revision of the node.
$this->drupalGet($private_href1);
$assert_session->pageTextContains('You are not authorized to access this page');
$assert_session->pageTextNotContains($this->getFileSecret($file));
$this->assertFileExists($file_real_path);
$file2 = $this->createPrivateFile('2ndFile.txt');
$this->drupalGet('node/1/layout');
$this->addInlineFileBlockToLayout('Number2', $file2);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href2 = $this->assertFileAccessibleOnNode($file2);
$this->createNewNodeRevision(1);
$file3 = $this->createPrivateFile('3rdFile.txt');
$this->drupalGet('node/1/layout');
$this->replaceFileInBlock($file3);
$this->assertSaveLayout();
$this->drupalGet('node/1');
$private_href3 = $this->assertFileAccessibleOnNode($file3);
// $file2 is on a previous revision of the block which is on a previous
// revision of the node. The user does not have access to view the previous
// revision of the node.
$this->drupalGet($private_href2);
$assert_session->pageTextContains('You are not authorized to access this page');
$node = Node::load(1);
$node->setUnpublished();
$node->save();
$this->drupalGet('node/1');
$assert_session->pageTextContains('You are not authorized to access this page');
$this->drupalGet($private_href3);
$assert_session->pageTextNotContains($this->getFileSecret($file3));
$assert_session->pageTextContains('You are not authorized to access this page');
$this->drupalGet('node/2/layout');
$file4 = $this->createPrivateFile('drupal.txt');
$this->addInlineFileBlockToLayout('The file', $file4);
$this->assertSaveLayout();
$this->drupalGet('node/2');
$private_href4 = $this->assertFileAccessibleOnNode($file4);
$this->createNewNodeRevision(2);
// Remove the inline block with the private file.
// The inline block will still be attached to the previous revision of the
// node.
$this->drupalGet('node/2/layout');
$this->removeInlineBlockFromLayout();
$this->assertSaveLayout();
// Ensure that since the user cannot view the previous revision of the node
// they can not view the file which is only used on that revision.
$this->drupalGet($private_href4);
$assert_session->pageTextContains('You are not authorized to access this page');
}
/**
* Replaces the file in the block with another one.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*/
protected function replaceFileInBlock(FileInterface $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Configure');
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Remove');
$assert_session->assertWaitOnAjaxRequest();
$this->attachFileToBlockForm($file);
$page->pressButton('Update');
$this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
}
/**
* Adds an entity block with a file.
*
* @param string $title
* The title field value.
* @param \Drupal\file\Entity\File $file
* The file entity.
*/
protected function addInlineFileBlockToLayout($title, File $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
$this->clickLink('Basic block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);
$this->attachFileToBlockForm($file);
$page->pressButton('Add Block');
$this->assertDialogClosedAndTextVisible($file->label(), static::INLINE_BLOCK_LOCATOR);
}
/**
* Creates a private file.
*
* @param string $file_name
* The file name.
*
* @return \Drupal\Core\Entity\EntityInterface|\Drupal\file\Entity\File
* The file entity.
*/
protected function createPrivateFile($file_name) {
// Create a new file entity.
$file = File::create([
'uid' => 1,
'filename' => $file_name,
'uri' => "private://$file_name",
'filemime' => 'text/plain',
'status' => FILE_STATUS_PERMANENT,
]);
file_put_contents($file->getFileUri(), $this->getFileSecret($file));
$file->save();
return $file;
}
/**
* Asserts a file is accessible on the page.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*
* @return string
* The file href.
*/
protected function assertFileAccessibleOnNode(FileInterface $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$assert_session->linkExists($file->label());
$private_href = $page->findLink($file->label())->getAttribute('href');
$page->clickLink($file->label());
$assert_session->pageTextContains($this->getFileSecret($file));
// Access file directly.
$this->drupalGet($private_href);
$assert_session->pageTextContains($this->getFileSecret($file));
return $private_href;
}
/**
* Gets the text secret for a file.
*
* @param \Drupal\file\FileInterface $file
* The file entity.
*
* @return string
* The text secret.
*/
protected function getFileSecret(FileInterface $file) {
return "The secret in {$file->label()}";
}
/**
* Attaches a file to the block edit form.
*
* @param \Drupal\file\FileInterface $file
* The file to be attached.
*/
protected function attachFileToBlockForm(FileInterface $file) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$page->attachFileToField("files[settings_block_form_field_file_0]", $this->fileSystem->realpath($file->getFileUri()));
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForLink($file->label()));
}
/**
* Create a new revision of the node.
*
* @param int $node_id
* The node id.
*/
protected function createNewNodeRevision($node_id) {
$node = Node::load($node_id);
$node->setTitle('Update node');
$node->setNewRevision();
$node->save();
}
}

Some files were not shown because too many files have changed in this diff Show more