Issue #2880152 by amateescu, blazey, plach, larowlan, vijaycs85: Convert custom menu links to be revisionable

merge-requests/1119/head
Francesco Placella 2019-03-11 14:38:31 +01:00
parent 1e0ad95a0b
commit 5ad8f598e5
19 changed files with 597 additions and 32 deletions

View File

@ -29,6 +29,16 @@ function menu_link_content_help($route_name, RouteMatchInterface $route_match) {
} }
} }
/**
* Implements hook_entity_type_alter().
*/
function menu_link_content_entity_type_alter(array &$entity_types) {
// @todo Moderation is disabled for custom menu links until when we have an UI
// for them.
// @see https://www.drupal.org/project/drupal/issues/2350939
$entity_types['menu_link_content']->setHandlerClass('moderation', '');
}
/** /**
* Implements hook_menu_delete(). * Implements hook_menu_delete().
*/ */

View File

@ -0,0 +1,103 @@
<?php
/**
* @file
* Post update functions for the Menu link content module.
*/
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Update custom menu links to be revisionable.
*/
function menu_link_content_post_update_make_menu_link_content_revisionable(&$sandbox) {
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
/** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
$last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
$entity_type = $definition_update_manager->getEntityType('menu_link_content');
$field_storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions('menu_link_content');
// Update the entity type definition.
$entity_keys = $entity_type->getKeys();
$entity_keys['revision'] = 'revision_id';
$entity_keys['revision_translation_affected'] = 'revision_translation_affected';
$entity_type->set('entity_keys', $entity_keys);
$entity_type->set('revision_table', 'menu_link_content_revision');
$entity_type->set('revision_data_table', 'menu_link_content_field_revision');
$revision_metadata_keys = [
'revision_default' => 'revision_default',
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log_message',
];
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// Update the field storage definitions and add the new ones required by a
// revisionable entity type.
$field_storage_definitions['langcode']->setRevisionable(TRUE);
$field_storage_definitions['title']->setRevisionable(TRUE);
$field_storage_definitions['description']->setRevisionable(TRUE);
$field_storage_definitions['link']->setRevisionable(TRUE);
$field_storage_definitions['external']->setRevisionable(TRUE);
$field_storage_definitions['enabled']->setRevisionable(TRUE);
$field_storage_definitions['changed']->setRevisionable(TRUE);
$field_storage_definitions['revision_id'] = BaseFieldDefinition::create('integer')
->setName('revision_id')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision ID'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$field_storage_definitions['revision_default'] = BaseFieldDefinition::create('boolean')
->setName('revision_default')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Default revision'))
->setDescription(new TranslatableMarkup('A flag indicating whether this was a default revision when it was saved.'))
->setStorageRequired(TRUE)
->setInternal(TRUE)
->setTranslatable(FALSE)
->setRevisionable(TRUE);
$field_storage_definitions['revision_translation_affected'] = BaseFieldDefinition::create('boolean')
->setName('revision_translation_affected')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision translation affected'))
->setDescription(new TranslatableMarkup('Indicates if the last edit of a translation belongs to current revision.'))
->setReadOnly(TRUE)
->setRevisionable(TRUE)
->setTranslatable(TRUE);
$field_storage_definitions['revision_created'] = BaseFieldDefinition::create('created')
->setName('revision_created')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision create time'))
->setDescription(new TranslatableMarkup('The time that the current revision was created.'))
->setRevisionable(TRUE);
$field_storage_definitions['revision_user'] = BaseFieldDefinition::create('entity_reference')
->setName('revision_user')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision user'))
->setDescription(new TranslatableMarkup('The user ID of the author of the current revision.'))
->setSetting('target_type', 'user')
->setRevisionable(TRUE);
$field_storage_definitions['revision_log_message'] = BaseFieldDefinition::create('string_long')
->setName('revision_log_message')
->setTargetEntityTypeId('menu_link_content')
->setTargetBundle(NULL)
->setLabel(new TranslatableMarkup('Revision log message'))
->setDescription(new TranslatableMarkup('Briefly describe the changes you have made.'))
->setRevisionable(TRUE)
->setDefaultValue('');
$definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);
return t('Custom menu links have been converted to be revisionable.');
}

View File

@ -2,9 +2,7 @@
namespace Drupal\menu_link_content\Entity; namespace Drupal\menu_link_content\Entity;
use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\BaseFieldDefinition;
@ -28,7 +26,7 @@ use Drupal\menu_link_content\MenuLinkContentInterface;
* plural = "@count custom menu links", * plural = "@count custom menu links",
* ), * ),
* handlers = { * handlers = {
* "storage" = "Drupal\Core\Entity\Sql\SqlContentEntityStorage", * "storage" = "\Drupal\menu_link_content\MenuLinkContentStorage",
* "storage_schema" = "Drupal\menu_link_content\MenuLinkContentStorageSchema", * "storage_schema" = "Drupal\menu_link_content\MenuLinkContentStorageSchema",
* "access" = "Drupal\menu_link_content\MenuLinkContentAccessControlHandler", * "access" = "Drupal\menu_link_content\MenuLinkContentAccessControlHandler",
* "form" = { * "form" = {
@ -39,26 +37,34 @@ use Drupal\menu_link_content\MenuLinkContentInterface;
* admin_permission = "administer menu", * admin_permission = "administer menu",
* base_table = "menu_link_content", * base_table = "menu_link_content",
* data_table = "menu_link_content_data", * data_table = "menu_link_content_data",
* revision_table = "menu_link_content_revision",
* revision_data_table = "menu_link_content_field_revision",
* translatable = TRUE, * translatable = TRUE,
* entity_keys = { * entity_keys = {
* "id" = "id", * "id" = "id",
* "revision" = "revision_id",
* "label" = "title", * "label" = "title",
* "langcode" = "langcode", * "langcode" = "langcode",
* "uuid" = "uuid", * "uuid" = "uuid",
* "bundle" = "bundle", * "bundle" = "bundle",
* "published" = "enabled", * "published" = "enabled",
* }, * },
* revision_metadata_keys = {
* "revision_user" = "revision_user",
* "revision_created" = "revision_created",
* "revision_log_message" = "revision_log_message",
* },
* links = { * links = {
* "canonical" = "/admin/structure/menu/item/{menu_link_content}/edit", * "canonical" = "/admin/structure/menu/item/{menu_link_content}/edit",
* "edit-form" = "/admin/structure/menu/item/{menu_link_content}/edit", * "edit-form" = "/admin/structure/menu/item/{menu_link_content}/edit",
* "delete-form" = "/admin/structure/menu/item/{menu_link_content}/delete", * "delete-form" = "/admin/structure/menu/item/{menu_link_content}/delete",
* } * },
* constraints = {
* "MenuTreeHierarchy" = {}
* },
* ) * )
*/ */
class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterface { class MenuLinkContent extends EditorialContentEntityBase implements MenuLinkContentInterface {
use EntityChangedTrait;
use EntityPublishedTrait;
/** /**
* A flag for whether this entity is wrapped in a plugin instance. * A flag for whether this entity is wrapped in a plugin instance.
@ -203,6 +209,11 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
public function postSave(EntityStorageInterface $storage, $update = TRUE) { public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update); parent::postSave($storage, $update);
// Don't update the menu tree if a pending revision was saved.
if (!$this->isDefaultRevision()) {
return;
}
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link'); $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
@ -277,6 +288,7 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
->setDescription(t('The text to be used for this link in the menu.')) ->setDescription(t('The text to be used for this link in the menu.'))
->setRequired(TRUE) ->setRequired(TRUE)
->setTranslatable(TRUE) ->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255) ->setSetting('max_length', 255)
->setDisplayOptions('view', [ ->setDisplayOptions('view', [
'label' => 'hidden', 'label' => 'hidden',
@ -293,6 +305,7 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
->setLabel(t('Description')) ->setLabel(t('Description'))
->setDescription(t('Shown when hovering over the menu link.')) ->setDescription(t('Shown when hovering over the menu link.'))
->setTranslatable(TRUE) ->setTranslatable(TRUE)
->setRevisionable(TRUE)
->setSetting('max_length', 255) ->setSetting('max_length', 255)
->setDisplayOptions('view', [ ->setDisplayOptions('view', [
'label' => 'hidden', 'label' => 'hidden',
@ -313,6 +326,7 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['link'] = BaseFieldDefinition::create('link') $fields['link'] = BaseFieldDefinition::create('link')
->setLabel(t('Link')) ->setLabel(t('Link'))
->setDescription(t('The location this menu link points to.')) ->setDescription(t('The location this menu link points to.'))
->setRevisionable(TRUE)
->setRequired(TRUE) ->setRequired(TRUE)
->setSettings([ ->setSettings([
'link_type' => LinkItemInterface::LINK_GENERIC, 'link_type' => LinkItemInterface::LINK_GENERIC,
@ -326,7 +340,8 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['external'] = BaseFieldDefinition::create('boolean') $fields['external'] = BaseFieldDefinition::create('boolean')
->setLabel(t('External')) ->setLabel(t('External'))
->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).')) ->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).'))
->setDefaultValue(FALSE); ->setDefaultValue(FALSE)
->setRevisionable(TRUE);
$fields['rediscover'] = BaseFieldDefinition::create('boolean') $fields['rediscover'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Indicates whether the menu link should be rediscovered')) ->setLabel(t('Indicates whether the menu link should be rediscovered'))
@ -365,7 +380,6 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['enabled']->setLabel(t('Enabled')); $fields['enabled']->setLabel(t('Enabled'));
$fields['enabled']->setDescription(t('A flag for whether the link should be enabled in menus or hidden.')); $fields['enabled']->setDescription(t('A flag for whether the link should be enabled in menus or hidden.'));
$fields['enabled']->setTranslatable(FALSE); $fields['enabled']->setTranslatable(FALSE);
$fields['enabled']->setRevisionable(FALSE);
$fields['enabled']->setDisplayOptions('view', [ $fields['enabled']->setDisplayOptions('view', [
'label' => 'hidden', 'label' => 'hidden',
'type' => 'boolean', 'type' => 'boolean',
@ -383,7 +397,14 @@ class MenuLinkContent extends ContentEntityBase implements MenuLinkContentInterf
$fields['changed'] = BaseFieldDefinition::create('changed') $fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed')) ->setLabel(t('Changed'))
->setDescription(t('The time that the menu link was last edited.')) ->setDescription(t('The time that the menu link was last edited.'))
->setTranslatable(TRUE); ->setTranslatable(TRUE)
->setRevisionable(TRUE);
// @todo Keep this field hidden until we have a revision UI for menu links.
// @see https://www.drupal.org/project/drupal/issues/2350939
$fields['revision_log_message']->setDisplayOptions('form', [
'region' => 'hidden',
]);
return $fields; return $fields;
} }

View File

@ -127,19 +127,11 @@ class MenuLinkContentForm extends ContentEntityForm {
public function save(array $form, FormStateInterface $form_state) { public function save(array $form, FormStateInterface $form_state) {
// The entity is rebuilt in parent::submit(). // The entity is rebuilt in parent::submit().
$menu_link = $this->entity; $menu_link = $this->entity;
$saved = $menu_link->save(); $menu_link->save();
if ($saved) { $this->messenger()->addStatus($this->t('The menu link has been saved.'));
$this->messenger()->addStatus($this->t('The menu link has been saved.'));
$form_state->setRedirect( $form_state->setRedirectUrl($menu_link->toUrl('canonical'));
'entity.menu_link_content.canonical',
['menu_link_content' => $menu_link->id()]
);
}
else {
$this->messenger()->addError($this->t('There was an error saving the menu link.'));
$form_state->setRebuild();
}
} }
} }

View File

@ -5,11 +5,12 @@ namespace Drupal\menu_link_content;
use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
/** /**
* Defines an interface for custom menu links. * Defines an interface for custom menu links.
*/ */
interface MenuLinkContentInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface { interface MenuLinkContentInterface extends ContentEntityInterface, EntityChangedInterface, EntityPublishedInterface, RevisionLogInterface {
/** /**
* Flags this instance as being wrapped in a menu link plugin instance. * Flags this instance as being wrapped in a menu link plugin instance.

View File

@ -0,0 +1,44 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
/**
* Storage handler for menu_link_content entities.
*/
class MenuLinkContentStorage extends SqlContentEntityStorage implements MenuLinkContentStorageInterface {
/**
* {@inheritdoc}
*/
public function getMenuLinkIdsWithPendingRevisions() {
$table_mapping = $this->getTableMapping();
$id_field = $table_mapping->getColumnNames($this->entityType->getKey('id'))['value'];
$revision_field = $table_mapping->getColumnNames($this->entityType->getKey('revision'))['value'];
$rta_field = $table_mapping->getColumnNames($this->entityType->getKey('revision_translation_affected'))['value'];
$langcode_field = $table_mapping->getColumnNames($this->entityType->getKey('langcode'))['value'];
$revision_default_field = $table_mapping->getColumnNames($this->entityType->getRevisionMetadataKey('revision_default'))['value'];
$query = $this->database->select($this->getRevisionDataTable(), 'mlfr');
$query->fields('mlfr', [$id_field]);
$query->addExpression("MAX(mlfr.$revision_field)", $revision_field);
$query->join($this->getRevisionTable(), 'mlr', "mlfr.$revision_field = mlr.$revision_field AND mlr.$revision_default_field = 0");
$inner_select = $this->database->select($this->getRevisionDataTable(), 't');
$inner_select->condition("t.$rta_field", '1');
$inner_select->fields('t', [$id_field, $langcode_field]);
$inner_select->addExpression("MAX(t.$revision_field)", $revision_field);
$inner_select
->groupBy("t.$id_field")
->groupBy("t.$langcode_field");
$query->join($inner_select, 'mr', "mlfr.$revision_field = mr.$revision_field AND mlfr.$langcode_field = mr.$langcode_field");
$query->groupBy("mlfr.$id_field");
return $query->execute()->fetchAllKeyed(1, 0);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Drupal\menu_link_content;
use Drupal\Core\Entity\ContentEntityStorageInterface;
/**
* Defines an interface for menu_link_content entity storage classes.
*/
interface MenuLinkContentStorageInterface extends ContentEntityStorageInterface {
/**
* Gets a list of menu link IDs with pending revisions.
*
* @return int[]
* An array of menu link IDs which have pending revisions, keyed by their
* revision IDs.
*
* @internal
*/
public function getMenuLinkIdsWithPendingRevisions();
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\menu_link_content\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\CompositeConstraintBase;
/**
* Validation constraint for changing the menu hierarchy in pending revisions.
*
* @Constraint(
* id = "MenuTreeHierarchy",
* label = @Translation("Menu tree hierarchy.", context = "Validation"),
* )
*/
class MenuTreeHierarchyConstraint extends CompositeConstraintBase {
/**
* The default violation message.
*
* @var string
*/
public $message = 'You can only change the hierarchy for the <em>published</em> version of this menu link.';
/**
* {@inheritdoc}
*/
public function coversFields() {
return ['parent', 'weight'];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Drupal\menu_link_content\Plugin\Validation\Constraint;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing menu link parents in pending revisions.
*/
class MenuTreeHierarchyConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
private $entityTypeManager;
/**
* Creates a new MenuTreeHierarchyConstraintValidator instance.
*
* @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) {
return new static(
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
if ($entity && !$entity->isNew() && !$entity->isDefaultRevision()) {
$original = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
// Ensure that empty items do not affect the comparison checks below.
// @todo Remove this filtering when
// https://www.drupal.org/project/drupal/issues/3039031 is fixed.
$entity->parent->filterEmptyItems();
if (($entity->parent->isEmpty() !== $original->parent->isEmpty()) || !$entity->parent->equals($original->parent)) {
$this->context->buildViolation($constraint->message)
->atPath('menu_parent')
->addViolation();
}
if (!$entity->weight->equals($original->weight)) {
$this->context->buildViolation($constraint->message)
->atPath('weight')
->addViolation();
}
}
}
}

View File

@ -120,6 +120,11 @@ abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
'value' => 1, 'value' => 1,
], ],
], ],
'revision_id' => [
[
'value' => 1,
],
],
'title' => [ 'title' => [
[ [
'value' => 'Llama Gabilondo', 'value' => 'Llama Gabilondo',
@ -191,6 +196,16 @@ abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
], ],
], ],
'parent' => [], 'parent' => [],
'revision_created' => [
$this->formatExpectedTimestampItemValues((int) $this->entity->getRevisionCreationTime()),
],
'revision_user' => [],
'revision_log_message' => [],
'revision_translation_affected' => [
[
'value' => TRUE,
],
],
]; ];
} }

View File

@ -59,6 +59,49 @@ class MenuLinkContentUpdateTest extends UpdatePathTestBase {
$this->assertTrue($menu_link->isPublished()); $this->assertTrue($menu_link->isPublished());
} }
/**
* Tests the conversion of custom menu links to be revisionable.
*
* @see menu_link_content_post_update_make_menu_link_content_revisionable()
*/
public function testConversionToRevisionable() {
$entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
$this->assertFalse($entity_type->isRevisionable());
$this->runUpdates();
$entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
$this->assertTrue($entity_type->isRevisionable());
// Log in as user 1.
$account = User::load(1);
$account->passRaw = 'drupal';
$this->drupalLogin($account);
// Make sure our custom menu link exists.
$assert_session = $this->assertSession();
$this->drupalGet('admin/structure/menu/item/1/edit');
$assert_session->checkboxChecked('edit-enabled-value');
// Check that custom menu links can be created, saved and then loaded.
$storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */
$menu_link = $storage->create([
'menu_name' => 'main',
'link' => 'route:user.page',
'title' => 'Pineapple',
]);
$menu_link->save();
$storage->resetCache();
$menu_link = $storage->loadRevision($menu_link->getRevisionId());
$this->assertEquals('main', $menu_link->getMenuName());
$this->assertEquals('Pineapple', $menu_link->label());
$this->assertEquals('route:user.page', $menu_link->link->uri);
$this->assertTrue($menu_link->isPublished());
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -18,7 +18,7 @@ class MenuLinkContentDeriverTest extends KernelTestBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static $modules = ['menu_link_content', 'link', 'system', 'menu_link_content_dynamic_route']; public static $modules = ['menu_link_content', 'link', 'system', 'menu_link_content_dynamic_route', 'user'];
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -26,6 +26,7 @@ class MenuLinkContentDeriverTest extends KernelTestBase {
protected function setUp() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content'); $this->installEntitySchema('menu_link_content');
} }

View File

@ -310,4 +310,114 @@ class MenuLinksTest extends KernelTestBase {
$this->assertEqual(count($menu_links), 0); $this->assertEqual(count($menu_links), 0);
} }
/**
* Tests handling of pending revisions.
*
* @coversDefaultClass \Drupal\menu_link_content\Plugin\Validation\Constraint\MenuTreeHierarchyConstraintValidator
*/
public function testPendingRevisions() {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
// Add new menu items in a hierarchy.
$default_root_1_title = $this->randomMachineName(8);
$root_1 = $storage->create([
'title' => $default_root_1_title,
'link' => [['uri' => 'internal:/#root_1']],
'menu_name' => 'menu_test',
]);
$root_1->save();
$default_child1_title = $this->randomMachineName(8);
$child1 = $storage->create([
'title' => $default_child1_title,
'link' => [['uri' => 'internal:/#child1']],
'menu_name' => 'menu_test',
'parent' => 'menu_link_content:' . $root_1->uuid(),
]);
$child1->save();
$default_child2_title = $this->randomMachineName(8);
$child2 = $storage->create([
'title' => $default_child2_title,
'link' => [['uri' => 'internal:/#child2']],
'menu_name' => 'menu_test',
'parent' => 'menu_link_content:' . $child1->uuid(),
]);
$child2->save();
$default_root_2_title = $this->randomMachineName(8);
$root_2 = $storage->create([
'title' => $default_root_2_title,
'link' => [['uri' => 'internal:/#root_2']],
'menu_name' => 'menu_test',
]);
$root_2->save();
// Check that changing the title and the link in a pending revision is
// allowed.
$pending_child1_title = $this->randomMachineName(8);
$child1_pending_revision = $storage->createRevision($child1, FALSE);
$child1_pending_revision->set('title', $pending_child1_title);
$child1_pending_revision->set('link', [['uri' => 'internal:/#test']]);
$violations = $child1_pending_revision->validate();
$this->assertEmpty($violations);
$child1_pending_revision->save();
$storage->resetCache();
$child1_pending_revision = $storage->loadRevision($child1_pending_revision->getRevisionId());
$this->assertFalse($child1_pending_revision->isDefaultRevision());
$this->assertEquals($pending_child1_title, $child1_pending_revision->getTitle());
$this->assertEquals('/#test', $child1_pending_revision->getUrlObject()->toString());
// Check that saving a pending revision does not affect the menu tree.
$menu_tree = \Drupal::menuTree()->load('menu_test', new MenuTreeParameters());
$parent_link = reset($menu_tree);
$this->assertEquals($default_root_1_title, $parent_link->link->getTitle());
$this->assertEquals('/#root_1', $parent_link->link->getUrlObject()->toString());
$child1_link = reset($parent_link->subtree);
$this->assertEquals($default_child1_title, $child1_link->link->getTitle());
$this->assertEquals('/#child1', $child1_link->link->getUrlObject()->toString());
$child2_link = reset($child1_link->subtree);
$this->assertEquals($default_child2_title, $child2_link->link->getTitle());
$this->assertEquals('/#child2', $child2_link->link->getUrlObject()->toString());
// Check that changing the parent in a pending revision is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('parent', $child1->id());
$violations = $child2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the <em>published</em> version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
// Check that changing the weight in a pending revision is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('weight', 500);
$violations = $child2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the <em>published</em> version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('weight', $violations[0]->getPropertyPath());
// Check that changing both the parent and the weight in a pending revision
// is not allowed.
$child2_pending_revision = $storage->createRevision($child2, FALSE);
$child2_pending_revision->set('parent', $child1->id());
$child2_pending_revision->set('weight', 500);
$violations = $child2_pending_revision->validate();
$this->assertCount(2, $violations);
$this->assertEquals('You can only change the hierarchy for the <em>published</em> version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('You can only change the hierarchy for the <em>published</em> version of this menu link.', $violations[1]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
$this->assertEquals('weight', $violations[1]->getPropertyPath());
// Check that changing the parent of a term which didn't have a parent
// initially is not allowed in a pending revision.
$root_2_pending_revision = $storage->createRevision($root_2, FALSE);
$root_2_pending_revision->set('parent', $root_1->id());
$violations = $root_2_pending_revision->validate();
$this->assertCount(1, $violations);
$this->assertEquals('You can only change the hierarchy for the <em>published</em> version of this menu link.', $violations[0]->getMessage());
$this->assertEquals('menu_parent', $violations[0]->getPropertyPath());
}
} }

View File

@ -18,7 +18,7 @@ class PathAliasMenuLinkContentTest extends KernelTestBase {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static $modules = ['menu_link_content', 'system', 'link', 'test_page_test']; public static $modules = ['menu_link_content', 'system', 'link', 'test_page_test', 'user'];
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -26,6 +26,7 @@ class PathAliasMenuLinkContentTest extends KernelTestBase {
protected function setUp() { protected function setUp() {
parent::setUp(); parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content'); $this->installEntitySchema('menu_link_content');
// Ensure that the weight of module_link_content is higher than system. // Ensure that the weight of module_link_content is higher than system.

View File

@ -15,6 +15,8 @@ use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Element; use Drupal\Core\Render\Element;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface; use Drupal\Core\Utility\LinkGeneratorInterface;
use Drupal\menu_link_content\MenuLinkContentStorageInterface;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
@ -45,6 +47,13 @@ class MenuForm extends EntityForm {
*/ */
protected $linkGenerator; protected $linkGenerator;
/**
* The menu_link_content storage handler.
*
* @var \Drupal\menu_link_content\MenuLinkContentStorageInterface
*/
protected $menuLinkContentStorage;
/** /**
* The overview tree form. * The overview tree form.
* *
@ -61,11 +70,14 @@ class MenuForm extends EntityForm {
* The menu tree service. * The menu tree service.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
* The link generator. * The link generator.
* @param \Drupal\menu_link_content\MenuLinkContentStorageInterface $menu_link_content_storage
* The menu link content storage handler.
*/ */
public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator) { public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator, MenuLinkContentStorageInterface $menu_link_content_storage) {
$this->menuLinkManager = $menu_link_manager; $this->menuLinkManager = $menu_link_manager;
$this->menuTree = $menu_tree; $this->menuTree = $menu_tree;
$this->linkGenerator = $link_generator; $this->linkGenerator = $link_generator;
$this->menuLinkContentStorage = $menu_link_content_storage;
} }
/** /**
@ -75,7 +87,8 @@ class MenuForm extends EntityForm {
return new static( return new static(
$container->get('plugin.manager.menu.link'), $container->get('plugin.manager.menu.link'),
$container->get('menu.link_tree'), $container->get('menu.link_tree'),
$container->get('link_generator') $container->get('link_generator'),
$container->get('entity_type.manager')->getStorage('menu_link_content')
); );
} }
@ -273,16 +286,52 @@ class MenuForm extends EntityForm {
]), ]),
]); ]);
$links = $this->buildOverviewTreeForm($tree, $delta); $links = $this->buildOverviewTreeForm($tree, $delta);
// Get the menu links which have pending revisions, and disable the
// tabledrag if there are any.
$edited_ids = array_filter(array_map(function ($element) {
return is_array($element) && isset($element['#item']) && $element['#item']->link instanceof MenuLinkContent ? $element['#item']->link->getMetaData()['entity_id'] : NULL;
}, $links));
$pending_menu_link_ids = array_intersect($this->menuLinkContentStorage->getMenuLinkIdsWithPendingRevisions(), $edited_ids);
if ($pending_menu_link_ids) {
$form['help'] = [
'#type' => 'container',
'message' => [
'#markup' => $this->formatPlural(
count($pending_menu_link_ids),
'%capital_name contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.',
'%capital_name contains @count menu links with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.',
[
'%capital_name' => $this->entity->label(),
]
),
],
'#attributes' => ['class' => ['messages', 'messages--warning']],
'#weight' => -10,
];
unset($form['links']['#tabledrag']);
unset($form['links']['#header'][2]);
}
foreach (Element::children($links) as $id) { foreach (Element::children($links) as $id) {
if (isset($links[$id]['#item'])) { if (isset($links[$id]['#item'])) {
$element = $links[$id]; $element = $links[$id];
$is_pending_menu_link = isset($element['#item']->link->getMetaData()['entity_id'])
&& in_array($element['#item']->link->getMetaData()['entity_id'], $pending_menu_link_ids);
$form['links'][$id]['#item'] = $element['#item']; $form['links'][$id]['#item'] = $element['#item'];
// TableDrag: Mark the table row as draggable. // TableDrag: Mark the table row as draggable.
$form['links'][$id]['#attributes'] = $element['#attributes']; $form['links'][$id]['#attributes'] = $element['#attributes'];
$form['links'][$id]['#attributes']['class'][] = 'draggable'; $form['links'][$id]['#attributes']['class'][] = 'draggable';
if ($is_pending_menu_link) {
$form['links'][$id]['#attributes']['class'][] = 'color-warning';
$form['links'][$id]['#attributes']['class'][] = 'menu-link-content--pending-revision';
}
// TableDrag: Sort the table row according to its existing/configured weight. // TableDrag: Sort the table row according to its existing/configured weight.
$form['links'][$id]['#weight'] = $element['#item']->link->getWeight(); $form['links'][$id]['#weight'] = $element['#item']->link->getWeight();
@ -301,7 +350,14 @@ class MenuForm extends EntityForm {
$form['links'][$id]['enabled'] = $element['enabled']; $form['links'][$id]['enabled'] = $element['enabled'];
$form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled']; $form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled'];
$form['links'][$id]['weight'] = $element['weight']; // Disallow changing the publishing status of a pending revision.
if ($is_pending_menu_link) {
$form['links'][$id]['enabled']['#access'] = FALSE;
}
if (!$pending_menu_link_ids) {
$form['links'][$id]['weight'] = $element['weight'];
}
// Operations (dropbutton) column. // Operations (dropbutton) column.
$form['links'][$id]['operations'] = $element['operations']; $form['links'][$id]['operations'] = $element['operations'];
@ -463,7 +519,7 @@ class MenuForm extends EntityForm {
$updated_values = []; $updated_values = [];
// Update any fields that have changed in this menu item. // Update any fields that have changed in this menu item.
foreach ($fields as $field) { foreach ($fields as $field) {
if ($element[$field]['#value'] != $element[$field]['#default_value']) { if (isset($element[$field]['#value']) && $element[$field]['#value'] != $element[$field]['#default_value']) {
$updated_values[$field] = $element[$field]['#value']; $updated_values[$field] = $element[$field]['#value'];
} }
} }

View File

@ -987,4 +987,48 @@ class MenuUiTest extends BrowserTestBase {
$block->save(); $block->save();
} }
/**
* Test that menu links with pending revisions can not be re-parented.
*/
public function testMenuUiWithPendingRevisions() {
$this->drupalLogin($this->adminUser);
$assert_session = $this->assertSession();
// Add four menu links in two separate menus.
$menu_1 = $this->addCustomMenu();
$root_1 = $this->addMenuLink('', '/', $menu_1->id());
$this->addMenuLink($root_1->getPluginId(), '/', $menu_1->id());
$menu_2 = $this->addCustomMenu();
$root_2 = $this->addMenuLink('', '/', $menu_2->id());
$child_2 = $this->addMenuLink($root_2->getPluginId(), '/', $menu_2->id());
$this->drupalGet('admin/structure/menu/manage/' . $menu_2->id());
$assert_session->pageTextNotContains($menu_2->label() . ' contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.');
$this->drupalGet('admin/structure/menu/manage/' . $menu_1->id());
$assert_session->pageTextNotContains($menu_1->label() . ' contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.');
// Create a pending revision for one of the menu links and check that it can
// no longer be re-parented in the UI. We can not create pending revisions
// through the UI yet so we have to use API calls.
\Drupal::entityTypeManager()->getStorage('menu_link_content')->createRevision($child_2, FALSE)->save();
$this->drupalGet('admin/structure/menu/manage/' . $menu_2->id());
$assert_session->pageTextContains($menu_2->label() . ' contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.');
// Check that the 'Enabled' checkbox is hidden for a pending revision.
$this->assertNotEmpty($this->cssSelect('input[name="links[menu_plugin_id:' . $root_2->getPluginId() . '][enabled]"]'), 'The publishing status of a default revision can be changed.');
$this->assertEmpty($this->cssSelect('input[name="links[menu_plugin_id:' . $child_2->getPluginId() . '][enabled]"]'), 'The publishing status of a pending revision can not be changed.');
$this->drupalGet('admin/structure/menu/manage/' . $menu_1->id());
$assert_session->pageTextNotContains($menu_1->label() . ' contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.');
// Check that the menu overview form can be saved without errors when there
// are pending revisions.
$this->drupalPostForm('admin/structure/menu/manage/' . $menu_2->id(), [], 'Save');
$errors = $this->xpath('//div[contains(@class, "messages--error")]');
$this->assertFalse($errors, 'Menu overview form saved without errors.');
}
} }

View File

@ -239,7 +239,7 @@ EOS;
*/ */
public function testEnableModulesFixedList() { public function testEnableModulesFixedList() {
// Install system module. // Install system module.
$this->container->get('module_installer')->install(['system', 'menu_link_content']); $this->container->get('module_installer')->install(['system', 'user', 'menu_link_content']);
$entity_manager = \Drupal::entityManager(); $entity_manager = \Drupal::entityManager();
// entity_test is loaded via $modules; its entity type should exist. // entity_test is loaded via $modules; its entity type should exist.

View File

@ -130,6 +130,8 @@ class DbDumpTest extends KernelTestBase {
'key_value_expire', 'key_value_expire',
'menu_link_content', 'menu_link_content',
'menu_link_content_data', 'menu_link_content_data',
'menu_link_content_revision',
'menu_link_content_field_revision',
'sequences', 'sequences',
'sessions', 'sessions',
'url_alias', 'url_alias',

View File

@ -41,6 +41,7 @@ class MenuLinkTreeTest extends KernelTestBase {
'menu_link_content', 'menu_link_content',
'field', 'field',
'link', 'link',
'user',
]; ];
/** /**
@ -49,6 +50,7 @@ class MenuLinkTreeTest extends KernelTestBase {
protected function setUp() { protected function setUp() {
parent::setUp(); parent::setUp();
\Drupal::service('router.builder')->rebuild(); \Drupal::service('router.builder')->rebuild();
$this->installEntitySchema('user');
$this->installEntitySchema('menu_link_content'); $this->installEntitySchema('menu_link_content');
$this->linkTree = $this->container->get('menu.link_tree'); $this->linkTree = $this->container->get('menu.link_tree');