first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
.menu-enabled {
width: 70px;
}
.menu-label {
font-weight: bold;
}

View File

@@ -0,0 +1,19 @@
---
label: 'Configure menu settings for a content type'
related:
- menu_ui.menu_item_add
- menu_ui.menu_operations
- core.menus
---
{% set content_types_text %}{% trans %}Content types{% endtrans %}{% endset %}
{% set content_types_link = render_var(help_route_link(content_types_text, 'entity.node_type.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}For an existing content type, configure the available menus that will be shown as options on content editing screens; links to content items of this type can be added to these menus during editing.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; {{ content_types_link }}.{% endtrans %}</li>
<li>{% trans %}Locate the content type you want to configure, and click <em>Edit</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Under <em>Menu settings</em>, check the menus that you want to be available when editing a content item of this type.{% endtrans %}</li>
<li>{% trans %}Optionally, select the <em>Default parent item</em>, to put links to content items under a default location in the menu structure.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,34 @@
---
label: Adding a link to a menu
related:
- menu_ui.content_type_configuration
- menu_ui.menu_operations
- core.menus
---
{% set structure_menu_text %}{% trans %}Menus{% endtrans %}{% endset %}
{% set structure_menu_link = render_var(help_route_link(structure_menu_text, 'entity.menu.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Add a link to a menu. Note that you can also add a link to a menu from the content edit page if menu settings have been configured for the content type.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate to <em>Structure</em> &gt; {{ structure_menu_link }}.{% endtrans %}</li>
<li>{% trans %}Locate the desired menu and click <em>Add link</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Enter the <em>Menu link title</em> to be displayed.{% endtrans %}</li>
<li>{% trans %}Enter the <em>Link</em>, one of the following:{% endtrans %}
<ul>
<li>{% trans %}An internal path, such as <em>/node/add</em>{% endtrans %}</li>
<li>{% trans %}A full external URL{% endtrans %}</li>
<li>{% trans %}Start typing the title of a content item and select it when the full title comes up{% endtrans %}</li>
<li>{% trans %}<em>&lt;nolink&gt;</em> to display the <em>Menu link title</em> as plain text without a link{% endtrans %}</li>
<li>{% trans %}<em>&lt;front&gt;</em> to link to the front page of your site{% endtrans %}</li>
</ul>
</li>
<li>{% trans %}Make sure that <em>Enabled</em> is checked; if not, the menu link will not be displayed.{% endtrans %}</li>
<li>{% trans %}Optionally, enter a <em>Description</em>, which will be displayed when a user hovers over the link.{% endtrans %}</li>
<li>{% trans %}Optionally, check <em>Show as expanded</em> to automatically show the children of this link (if any) when this link is shown.{% endtrans %}</li>
<li>{% trans %}Optionally, select the <em>Parent link</em>, if this menu link should be a child of another menu link.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be returned to the <em>Add link</em> page to add another link.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate to <em>Structure</em> &gt; {{ structure_menu_link }}.{% endtrans %}</li>
<li>{% trans %}Locate the menu you just added a link to and click <em>Edit</em> in the <em>Operations</em> list.{% endtrans %}</li>
<li>{% trans %}Verify that the order of links is correct. If it is not, drag menu links until the order is correct, and click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,19 @@
---
label: 'Creating a menu'
related:
- menu_ui.content_type_configuration
- core.menus
---
{% set structure_menu_text %}{% trans %}Menus{% endtrans %}{% endset %}
{% set structure_menu_link = render_var(help_route_link(structure_menu_text, 'entity.menu.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Create a new menu.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate <em>Structure</em> &gt; {{ structure_menu_link }}.{% endtrans %}</li>
<li>{% trans %}Click <em>Add menu</em>.{% endtrans %}</li>
<li>{% trans %}Enter the title for the menu, which is used as the default block title if the menu is displayed as a block. If desired, also edit the machine name of the menu, which is by default derived from the title.{% endtrans %}</li>
<li>{% trans %}Enter an administrative summary, which is displayed on the <em>Menus</em> page.{% endtrans %}</li>
<li>{% trans %}If your site has more than one language, choose the language for the menu.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>. You will be on the menu editing page, ready to add links to the menu if the core Custom Menu Links module is installed; see related topics for further tasks.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,18 @@
---
label: Reordering and disabling menu links
related:
- menu_ui.menu_item_add
- menu_ui.menu_operations
- core.menus
---
{% set structure_menu_text %}{% trans %}Menus{% endtrans %}{% endset %}
{% set structure_menu_link = render_var(help_route_link(structure_menu_text, 'entity.menu.collection')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Disable menu links or change the order and hierarchy of menu links.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administration menu, navigate to <em>Structure</em> &gt; {{ structure_menu_link }}.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit menu</em> for the menu that you want to edit.{% endtrans %}</li>
<li>{% trans %}Drag menu links into a new order, or check/uncheck <em>Enabled</em> to enable or disable menu links.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em> to save your changes.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,70 @@
/**
* @file
* Menu UI admin behaviors.
*/
(function ($, Drupal) {
/**
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.menuUiChangeParentItems = {
attach(context, settings) {
const menu = once('menu-parent', '#edit-menu');
if (menu.length) {
const $menu = $(menu);
// Update the list of available parent menu items to match the initial
// available menus.
Drupal.menuUiUpdateParentList();
// Update list of available parent menu items.
$menu.on('change', 'input', Drupal.menuUiUpdateParentList);
}
},
};
/**
* Function to set the options of the menu parent item dropdown.
*/
Drupal.menuUiUpdateParentList = function () {
const $menu = $('#edit-menu');
const values = [];
$menu.find('input:checked').each(function () {
// Get the names of all checked menus.
values.push(Drupal.checkPlain(this.value));
});
$.ajax({
url: `${window.location.protocol}//${window.location.host}${Drupal.url(
'admin/structure/menu/parents',
)}`,
type: 'POST',
data: { 'menus[]': values },
dataType: 'json',
success(options) {
const $select = $('#edit-menu-parent');
// Save key of last selected element.
const selected = $select[0].value;
// Remove all existing options from dropdown.
$select.children().remove();
// Add new options to dropdown. Keep a count of options for testing later.
let totalOptions = 0;
Object.keys(options || {}).forEach((machineName) => {
const selectContents = document.createElement('option');
selectContents.selected = machineName === selected;
selectContents.value = machineName;
selectContents.textContent = options[machineName];
$select.append(selectContents);
totalOptions++;
});
// Hide the parent options if there are no options for it.
$select
.closest('div')
.toggle(totalOptions > 0)
.attr('hidden', totalOptions === 0);
},
});
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,13 @@
name: Menu UI
type: module
description: 'Provides a user interface for managing menus.'
package: Core
# version: VERSION
configure: entity.menu.collection
dependencies:
- drupal:menu_link_content
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

96
core/modules/menu_ui/menu_ui.js Executable file
View File

@@ -0,0 +1,96 @@
/**
* @file
* Menu UI behaviors.
*/
(function ($, Drupal) {
/**
* Set a summary on the menu link form.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Find the form and call `drupalSetSummary` on it.
*/
Drupal.behaviors.menuUiDetailsSummaries = {
attach(context) {
$(context)
.find('.menu-link-form')
.drupalSetSummary((context) => {
const $context = $(context);
if (
$context.find('.js-form-item-menu-enabled input:checked').length
) {
return Drupal.checkPlain(
$context.find('.js-form-item-menu-title input')[0].value,
);
}
return Drupal.t('Not in menu');
});
},
};
/**
* Automatically fill in a menu link title, if possible.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches change and keyup behavior for automatically filling out menu
* link titles.
*/
Drupal.behaviors.menuUiLinkAutomaticTitle = {
attach(context) {
const $context = $(context);
$context.find('.menu-link-form').each(function () {
const $this = $(this);
// Try to find menu settings widget elements as well as a 'title' field
// in the form, but play nicely with user permissions and form
// alterations.
const $checkbox = $this.find('.js-form-item-menu-enabled input');
const $linkTitle = $context.find('.js-form-item-menu-title input');
const $title = $this
.closest('form')
.find('.js-form-item-title-0-value input');
// Bail out if we do not have all required fields.
if (!($checkbox.length && $linkTitle.length && $title.length)) {
return;
}
// If there is a link title already, mark it as overridden. The user
// expects that toggling the checkbox twice will take over the node's
// title.
if ($checkbox[0].checked && $linkTitle[0].value.length) {
$linkTitle.data('menuLinkAutomaticTitleOverridden', true);
}
// Whenever the value is changed manually, disable this behavior.
$linkTitle.on('keyup', () => {
$linkTitle.data('menuLinkAutomaticTitleOverridden', true);
});
// Global trigger on checkbox (do not fill-in a value when disabled).
$checkbox.on('change', () => {
if ($checkbox[0].checked) {
if (!$linkTitle.data('menuLinkAutomaticTitleOverridden')) {
$linkTitle[0].value = $title[0].value;
}
} else {
$linkTitle[0].value = '';
$linkTitle.removeData('menuLinkAutomaticTitleOverridden');
}
$checkbox.closest('.vertical-tabs-pane').trigger('summaryUpdated');
$checkbox.trigger('formUpdated');
});
// Take over any title change.
$title.on('keyup', () => {
if (
!$linkTitle.data('menuLinkAutomaticTitleOverridden') &&
$checkbox[0].checked
) {
$linkTitle[0].value = $title[0].value;
$linkTitle.trigger('formUpdated');
}
});
});
},
};
})(jQuery, Drupal);

View File

@@ -0,0 +1,22 @@
drupal.menu_ui:
version: VERSION
js:
menu_ui.js: {}
dependencies:
- core/jquery
- core/drupal
- core/drupal.form
drupal.menu_ui.admin:
version: VERSION
js:
menu_ui.admin.js: {}
dependencies:
- core/jquery
- core/drupal
drupal.menu_ui.adminforms:
version: VERSION
css:
theme:
css/menu_ui.admin.css: {}

View File

@@ -0,0 +1,12 @@
entity.menu.add_link_form:
route_name: entity.menu.add_link_form
title: 'Add link'
class: \Drupal\menu_ui\Plugin\Menu\LocalAction\MenuLinkAdd
appears_on:
- entity.menu.edit_form
entity.menu.add_form:
route_name: entity.menu.add_form
title: 'Add menu'
appears_on:
- entity.menu.collection

View File

@@ -0,0 +1,4 @@
entity.menu.edit_form:
title: 'Edit menu'
route_name: 'entity.menu.edit_form'
group: menu

View File

@@ -0,0 +1,5 @@
entity.menu.collection:
title: Menus
description: 'Manage menus and menu links.'
route_name: entity.menu.collection
parent: system.admin_structure

View File

@@ -0,0 +1,9 @@
entity.menu.edit_form:
title: 'Edit menu'
route_name: entity.menu.edit_form
base_route: entity.menu.edit_form
entity.menu.collection:
title: 'List'
route_name: entity.menu.collection
base_route: entity.menu.collection

View File

@@ -0,0 +1,492 @@
<?php
/**
* @file
* Allows administrators to customize the site's navigation menus.
*
* A menu (in this context) is a hierarchical collection of links, generally
* used for navigation.
*/
use Drupal\block\BlockInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\NodeTypeInterface;
use Drupal\system\Entity\Menu;
use Drupal\node\NodeInterface;
use Drupal\system\MenuInterface;
/**
* Implements hook_help().
*/
function menu_ui_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.menu_ui':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Menu UI module provides an interface for managing menus. A menu is a hierarchical collection of links, which can be within or external to the site, generally used for navigation. For more information, see the <a href=":menu">online documentation for the Menu UI module</a>.', [':menu' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/menu-ui-module']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Managing menus') . '</dt>';
$output .= '<dd>' . t('Users with the <em>Administer menus and menu links</em> permission can add, edit, and delete custom menus on the <a href=":menu">Menus page</a>. Custom menus can be special site menus, menus of external links, or any combination of internal and external links. You may create an unlimited number of additional menus, each of which will automatically have an associated block (if you have the <a href=":block_help">Block module</a> installed). By selecting <em>Edit menu</em>, you can add, edit, or delete links for a given menu. The links listing page provides a drag-and-drop interface for controlling the order of links, and creating a hierarchy within the menu.', [':block_help' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('help.page', ['name' => 'block'])->toString() : '#', ':menu' => Url::fromRoute('entity.menu.collection')->toString()]) . '</dd>';
$output .= '<dt>' . t('Displaying menus') . '</dt>';
$output .= '<dd>' . t('If you have the Block module installed, then each menu that you create is rendered in a block that you enable and position on the <a href=":blocks">Block layout page</a>. In some <a href=":themes">themes</a>, the main menu and possibly the secondary menu will be output automatically; you may be able to disable this behavior on the <a href=":themes">theme\'s settings page</a>.', [':blocks' => (\Drupal::moduleHandler()->moduleExists('block')) ? Url::fromRoute('block.admin_display')->toString() : '#', ':themes' => Url::fromRoute('system.themes_page')->toString(), ':theme_settings' => Url::fromRoute('system.theme_settings')->toString()]) . '</dd>';
$output .= '</dl>';
return $output;
}
if ($route_name == 'entity.menu.add_form' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
return '<p>' . t('You can enable the newly-created block for this menu on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
}
elseif ($route_name == 'entity.menu.collection' && \Drupal::moduleHandler()->moduleExists('block') && \Drupal::currentUser()->hasPermission('administer blocks')) {
return '<p>' . t('Each menu has a corresponding block that is managed on the <a href=":blocks">Block layout page</a>.', [':blocks' => Url::fromRoute('block.admin_display')->toString()]) . '</p>';
}
}
/**
* Implements hook_entity_type_build().
*/
function menu_ui_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
$entity_types['menu']
->setFormClass('add', 'Drupal\menu_ui\MenuForm')
->setFormClass('edit', 'Drupal\menu_ui\MenuForm')
->setFormClass('delete', 'Drupal\menu_ui\Form\MenuDeleteForm')
->setListBuilderClass('Drupal\menu_ui\MenuListBuilder')
->setLinkTemplate('add-form', '/admin/structure/menu/add')
->setLinkTemplate('delete-form', '/admin/structure/menu/manage/{menu}/delete')
->setLinkTemplate('edit-form', '/admin/structure/menu/manage/{menu}')
->setLinkTemplate('add-link-form', '/admin/structure/menu/manage/{menu}/add')
->setLinkTemplate('collection', '/admin/structure/menu');
if (isset($entity_types['node'])) {
$entity_types['node']->addConstraint('MenuSettings', []);
}
}
/**
* Implements hook_block_view_BASE_BLOCK_ID_alter() for 'system_menu_block'.
*/
function menu_ui_block_view_system_menu_block_alter(array &$build, BlockPluginInterface $block) {
if ($block->getBaseId() == 'system_menu_block') {
$menu_name = $block->getDerivativeId();
$build['#contextual_links']['menu'] = [
'route_parameters' => ['menu' => $menu_name],
];
}
}
/**
* Helper function to create or update a menu link for a node.
*
* @param \Drupal\node\NodeInterface $node
* Node entity.
* @param array $values
* Values for the menu link.
*/
function _menu_ui_node_save(NodeInterface $node, array $values) {
/** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
if (!empty($values['entity_id'])) {
$entity = MenuLinkContent::load($values['entity_id']);
if ($entity->isTranslatable() && $node->isTranslatable()) {
if (!$entity->hasTranslation($node->language()->getId())) {
$entity = $entity->addTranslation($node->language()->getId(), $entity->toArray());
}
else {
$entity = $entity->getTranslation($node->language()->getId());
}
}
else {
// Ensure the entity matches the node language.
$entity = $entity->getUntranslated();
$entity->set($entity->getEntityType()->getKey('langcode'), $node->language()->getId());
}
}
else {
// Create a new menu_link_content entity.
$entity = MenuLinkContent::create([
'link' => ['uri' => 'entity:node/' . $node->id()],
'langcode' => $node->language()->getId(),
]);
$entity->enabled->value = 1;
}
$entity->title->value = trim($values['title']);
$entity->description->value = trim($values['description']);
$entity->menu_name->value = $values['menu_name'];
$entity->parent->value = $values['parent'];
$entity->weight->value = $values['weight'] ?? 0;
$entity->isDefaultRevision($node->isDefaultRevision());
$entity->save();
}
/**
* Returns the definition for a menu link for the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node entity.
*
* @return array
* An array that contains default values for the menu link form.
*/
function menu_ui_get_menu_link_defaults(NodeInterface $node) {
// Prepare the definition for the edit form.
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = $node->type->entity;
$menu_name = strtok($node_type->getThirdPartySetting('menu_ui', 'parent', 'main:'), ':');
$defaults = FALSE;
if ($node->id()) {
$id = FALSE;
// Give priority to the default menu
$type_menus = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']);
if (in_array($menu_name, $type_menus)) {
$query = \Drupal::entityQuery('menu_link_content')
->accessCheck(TRUE)
->condition('link.uri', 'entity:node/' . $node->id())
->condition('menu_name', $menu_name)
->sort('id', 'ASC')
->range(0, 1);
$result = $query->execute();
$id = (!empty($result)) ? reset($result) : FALSE;
}
// Check all allowed menus if a link does not exist in the default menu.
if (!$id && !empty($type_menus)) {
$query = \Drupal::entityQuery('menu_link_content')
->accessCheck(TRUE)
->condition('link.uri', 'entity:node/' . $node->id())
->condition('menu_name', array_values($type_menus), 'IN')
->sort('id', 'ASC')
->range(0, 1);
$result = $query->execute();
$id = (!empty($result)) ? reset($result) : FALSE;
}
if ($id) {
$menu_link = MenuLinkContent::load($id);
$menu_link = \Drupal::service('entity.repository')->getTranslationFromContext($menu_link);
$defaults = [
'entity_id' => $menu_link->id(),
'id' => $menu_link->getPluginId(),
'title' => $menu_link->getTitle(),
'title_max_length' => $menu_link->getFieldDefinitions()['title']->getSetting('max_length'),
'description' => $menu_link->getDescription(),
'description_max_length' => $menu_link->getFieldDefinitions()['description']->getSetting('max_length'),
'menu_name' => $menu_link->getMenuName(),
'parent' => $menu_link->getParentId(),
'weight' => $menu_link->getWeight(),
];
}
}
if (!$defaults) {
// Get the default max_length of a menu link title from the base field
// definition.
$field_definitions = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('menu_link_content');
$max_length = $field_definitions['title']->getSetting('max_length');
$description_max_length = $field_definitions['description']->getSetting('max_length');
$defaults = [
'entity_id' => 0,
'id' => '',
'title' => '',
'title_max_length' => $max_length,
'description' => '',
'description_max_length' => $description_max_length,
'menu_name' => $menu_name,
'parent' => '',
'weight' => 0,
];
}
return $defaults;
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm.
*
* Adds menu item fields to the node form.
*
* @see menu_ui_form_node_form_submit()
*/
function menu_ui_form_node_form_alter(&$form, FormStateInterface $form_state) {
// Generate a list of possible parents (not including this link or descendants).
// @todo This must be handled in a #process handler.
$node = $form_state->getFormObject()->getEntity();
$defaults = menu_ui_get_menu_link_defaults($node);
/** @var \Drupal\node\NodeTypeInterface $node_type */
$node_type = $node->type->entity;
/** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
$menu_parent_selector = \Drupal::service('menu.parent_form_selector');
$type_menus_ids = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']);
if (empty($type_menus_ids)) {
return;
}
/** @var \Drupal\system\MenuInterface[] $type_menus */
$type_menus = Menu::loadMultiple($type_menus_ids);
$available_menus = [];
foreach ($type_menus as $menu) {
$available_menus[$menu->id()] = $menu->label();
}
if ($defaults['id']) {
$default = $defaults['menu_name'] . ':' . $defaults['parent'];
}
else {
$default = $node_type->getThirdPartySetting('menu_ui', 'parent', 'main:');
}
$parent_element = $menu_parent_selector->parentSelectElement($default, $defaults['id'], $available_menus);
// If no possible parent menu items were found, there is nothing to display.
if (empty($parent_element)) {
return;
}
$form['menu'] = [
'#type' => 'details',
'#title' => t('Menu settings'),
'#access' => \Drupal::currentUser()->hasPermission('administer menu'),
'#open' => (bool) $defaults['id'],
'#group' => 'advanced',
'#attached' => [
'library' => ['menu_ui/drupal.menu_ui'],
],
'#tree' => TRUE,
'#weight' => -2,
'#attributes' => ['class' => ['menu-link-form']],
];
$form['menu']['enabled'] = [
'#type' => 'checkbox',
'#title' => t('Provide a menu link'),
'#default_value' => (int) (bool) $defaults['id'],
];
$form['menu']['link'] = [
'#type' => 'container',
'#parents' => ['menu'],
'#states' => [
'invisible' => [
'input[name="menu[enabled]"]' => ['checked' => FALSE],
],
],
];
// Populate the element with the link data.
foreach (['id', 'entity_id'] as $key) {
$form['menu']['link'][$key] = ['#type' => 'value', '#value' => $defaults[$key]];
}
$form['menu']['link']['title'] = [
'#type' => 'textfield',
'#title' => t('Menu link title'),
'#default_value' => $defaults['title'],
'#maxlength' => $defaults['title_max_length'],
];
$form['menu']['link']['description'] = [
'#type' => 'textfield',
'#title' => t('Description'),
'#default_value' => $defaults['description'],
'#description' => t('Shown when hovering over the menu link.'),
'#maxlength' => $defaults['description_max_length'],
];
$form['menu']['link']['menu_parent'] = $parent_element;
$form['menu']['link']['menu_parent']['#title'] = t('Parent link');
$form['menu']['link']['menu_parent']['#attributes']['class'][] = 'menu-parent-select';
$form['menu']['link']['weight'] = [
'#type' => 'number',
'#title' => t('Weight'),
'#default_value' => $defaults['weight'],
'#description' => t('Menu links with lower weights are displayed before links with higher weights.'),
];
foreach (array_keys($form['actions']) as $action) {
if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
$form['actions'][$action]['#submit'][] = 'menu_ui_form_node_form_submit';
}
}
$form['#entity_builders'][] = 'menu_ui_node_builder';
}
/**
* Entity form builder to add the menu information to the node.
*/
function menu_ui_node_builder($entity_type, NodeInterface $entity, &$form, FormStateInterface $form_state) {
$entity->menu = $form_state->getValue('menu');
}
/**
* Form submission handler for menu item field on the node form.
*
* @see menu_ui_form_node_form_alter()
*/
function menu_ui_form_node_form_submit($form, FormStateInterface $form_state) {
$node = $form_state->getFormObject()->getEntity();
if (!$form_state->isValueEmpty('menu')) {
$values = $form_state->getValue('menu');
if (empty($values['enabled'])) {
if ($values['entity_id']) {
$entity = MenuLinkContent::load($values['entity_id']);
$entity->delete();
}
}
else {
// In case the menu title was left empty, fall back to the node title.
if (empty(trim($values['title']))) {
$values['title'] = $node->label();
}
// Decompose the selected menu parent option into 'menu_name' and 'parent',
// if the form used the default parent selection widget.
if (!empty($values['menu_parent'])) {
[$menu_name, $parent] = explode(':', $values['menu_parent'], 2);
$values['menu_name'] = $menu_name;
$values['parent'] = $parent;
}
_menu_ui_node_save($node, $values);
}
}
}
/**
* Implements hook_form_FORM_ID_alter() for \Drupal\node\NodeTypeForm.
*
* Adds menu options to the node type form.
*
* @see NodeTypeForm::form()
* @see menu_ui_form_node_type_form_builder()
*/
function menu_ui_form_node_type_form_alter(&$form, FormStateInterface $form_state) {
/** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
$menu_parent_selector = \Drupal::service('menu.parent_form_selector');
$menu_options = array_map(function (MenuInterface $menu) {
return $menu->label();
}, Menu::loadMultiple());
asort($menu_options);
/** @var \Drupal\node\NodeTypeInterface $type */
$type = $form_state->getFormObject()->getEntity();
$form['menu'] = [
'#type' => 'details',
'#title' => t('Menu settings'),
'#attached' => [
'library' => ['menu_ui/drupal.menu_ui.admin'],
],
'#group' => 'additional_settings',
];
$form['menu']['menu_options'] = [
'#type' => 'checkboxes',
'#title' => t('Available menus'),
'#default_value' => $type->getThirdPartySetting('menu_ui', 'available_menus', ['main']),
'#options' => $menu_options,
'#description' => t('Content of this type can be placed in the selected menus.'),
];
// @todo See if we can avoid pre-loading all options by changing the form or
// using a #process callback. https://www.drupal.org/node/2310319
// To avoid an 'illegal option' error after saving the form we have to load
// all available menu parents. Otherwise, it is not possible to dynamically
// add options to the list using ajax.
$options_cacheability = new CacheableMetadata();
$options = $menu_parent_selector->getParentSelectOptions('', NULL, $options_cacheability);
$form['menu']['menu_parent'] = [
'#type' => 'select',
'#title' => t('Default parent link'),
'#default_value' => $type->getThirdPartySetting('menu_ui', 'parent', 'main:'),
'#options' => $options,
'#description' => t('Choose the menu link to be the default parent for a new link in the content authoring form.'),
'#attributes' => ['class' => ['menu-title-select']],
];
$options_cacheability->applyTo($form['menu']['menu_parent']);
$form['#validate'][] = 'menu_ui_form_node_type_form_validate';
$form['#entity_builders'][] = 'menu_ui_form_node_type_form_builder';
}
/**
* Validate handler for forms with menu options.
*
* @see menu_ui_form_node_type_form_alter()
*/
function menu_ui_form_node_type_form_validate(&$form, FormStateInterface $form_state) {
$available_menus = array_filter($form_state->getValue('menu_options'));
// If there is at least one menu allowed, the selected item should be in
// one of them.
if (count($available_menus)) {
$menu_item_id_parts = explode(':', $form_state->getValue('menu_parent'));
if (!in_array($menu_item_id_parts[0], $available_menus)) {
$form_state->setErrorByName('menu_parent', t('The selected menu link is not under one of the selected menus.'));
}
}
else {
$form_state->setValue('menu_parent', '');
}
}
/**
* Entity builder for the node type form with menu options.
*
* @see menu_ui_form_node_type_form_alter()
*/
function menu_ui_form_node_type_form_builder($entity_type, NodeTypeInterface $type, &$form, FormStateInterface $form_state) {
$type->setThirdPartySetting('menu_ui', 'available_menus', array_values(array_filter($form_state->getValue('menu_options'))));
$type->setThirdPartySetting('menu_ui', 'parent', $form_state->getValue('menu_parent'));
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function menu_ui_preprocess_block(&$variables) {
if ($variables['configuration']['provider'] == 'menu_ui') {
$variables['attributes']['role'] = 'navigation';
}
}
/**
* Implements hook_system_breadcrumb_alter().
*/
function menu_ui_system_breadcrumb_alter(Breadcrumb $breadcrumb, RouteMatchInterface $route_match, array $context) {
// Custom breadcrumb behavior for editing menu links, we append a link to
// the menu in which the link is found.
if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link_plugin')) {
if (($menu_link instanceof MenuLinkInterface)) {
// Add a link to the menu admin screen.
$menu = Menu::load($menu_link->getMenuName());
$breadcrumb->addLink(Link::createFromRoute($menu->label(), 'entity.menu.edit_form', ['menu' => $menu->id()]));
}
}
}
/**
* Implements hook_theme().
*/
function menu_ui_theme(): array {
return [
'menu_link_form' => [
'render element' => 'form',
],
];
}
/**
* Implements hook_entity_operation().
*/
function menu_ui_entity_operation(EntityInterface $entity): array {
$operations = [];
if ($entity instanceof BlockInterface) {
$plugin = $entity->getPlugin();
if ($plugin->getBaseId() === 'system_menu_block') {
$menu = Menu::load($plugin->getDerivativeId());
if ($menu && $menu->access('edit')) {
$operations['menu-edit'] = [
'title' => t('Edit menu'),
'url' => $menu->toUrl('edit-form'),
'weight' => 50,
];
}
}
}
return $operations;
}

View File

@@ -0,0 +1,63 @@
entity.menu.collection:
path: '/admin/structure/menu'
defaults:
_entity_list: 'menu'
_title: 'Menus'
requirements:
_permission: 'administer menu'
menu_ui.parent_options_js:
path: '/admin/structure/menu/parents'
defaults:
_controller: '\Drupal\menu_ui\Controller\MenuController::getParentOptions'
requirements:
_permission: 'administer menu'
menu_ui.link_edit:
path: '/admin/structure/menu/link/{menu_link_plugin}/edit'
defaults:
_form: '\Drupal\menu_ui\Form\MenuLinkEditForm'
_title: 'Edit menu link'
options:
parameters:
menu_link_plugin:
type: menu_link_plugin
requirements:
_permission: 'administer menu'
menu_ui.link_reset:
path: '/admin/structure/menu/link/{menu_link_plugin}/reset'
defaults:
_form: '\Drupal\menu_ui\Form\MenuLinkResetForm'
_title: 'Reset menu link'
options:
parameters:
menu_link_plugin:
type: menu_link_plugin
requirements:
_permission: 'administer menu'
_custom_access: '\Drupal\menu_ui\Form\MenuLinkResetForm::linkIsResettable'
entity.menu.add_form:
path: '/admin/structure/menu/add'
defaults:
_entity_form: 'menu.add'
_title: 'Add menu'
requirements:
_entity_create_access: 'menu'
entity.menu.edit_form:
path: '/admin/structure/menu/manage/{menu}'
defaults:
_entity_form: 'menu.edit'
_title_callback: '\Drupal\menu_ui\Controller\MenuController::menuTitle'
requirements:
_entity_access: 'menu.update'
entity.menu.delete_form:
path: '/admin/structure/menu/manage/{menu}/delete'
defaults:
_entity_form: 'menu.delete'
_title: 'Delete menu'
requirements:
_entity_access: 'menu.delete'

View File

@@ -0,0 +1,4 @@
services:
menu_ui.menu_tree_manipulators:
class: Drupal\menu_ui\Menu\MenuUiMenuTreeManipulators
Drupal\menu_ui\Menu\MenuUiMenuTreeManipulators: '@menu_ui.menu_tree_manipulators'

View File

@@ -0,0 +1,17 @@
# The menu migration is in the system module and the menu_links migration is in the menu_link_content module.
id: menu_settings
label: Menu UI configuration
migration_tags:
- Drupal 6
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- menu_override_parent_selector
source_module: menu
process:
override_parent_selector: menu_override_parent_selector
destination:
plugin: config
config_name: menu_ui.settings

View File

@@ -0,0 +1,5 @@
finished:
6:
menu: menu_ui
7:
menu: menu_ui

View File

@@ -0,0 +1,70 @@
<?php
namespace Drupal\menu_ui\Controller;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\system\MenuInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
/**
* Returns responses for Menu routes.
*/
class MenuController extends ControllerBase {
/**
* The menu parent form service.
*
* @var \Drupal\Core\Menu\MenuParentFormSelectorInterface
*/
protected $menuParentSelector;
/**
* Creates a new MenuController object.
*
* @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_form
* The menu parent form service.
*/
public function __construct(MenuParentFormSelectorInterface $menu_parent_form) {
$this->menuParentSelector = $menu_parent_form;
}
/**
* Gets all the available menus and menu items as a JavaScript array.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request of the page.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The available menu and menu items.
*/
public function getParentOptions(Request $request) {
$available_menus = [];
if ($menus = $request->request->all('menus')) {
foreach ($menus as $menu) {
$available_menus[$menu] = $menu;
}
}
// @todo Update this to use the optional $cacheability parameter, so that
// a cacheable JSON response can be sent.
$options = $this->menuParentSelector->getParentSelectOptions('', $available_menus);
return new JsonResponse($options);
}
/**
* Route title callback.
*
* @param \Drupal\system\MenuInterface $menu
* The menu entity.
*
* @return array
* The menu label as a render array.
*/
public function menuTitle(MenuInterface $menu) {
return ['#markup' => $menu->label(), '#allowed_tags' => Xss::getHtmlTagList()];
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Drupal\menu_ui\Form;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a confirmation form for deletion of a custom menu.
*
* @internal
*/
class MenuDeleteForm extends EntityDeleteForm {
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* Constructs a new MenuDeleteForm.
*
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(MenuLinkManagerInterface $menu_link_manager, Connection $connection) {
$this->menuLinkManager = $menu_link_manager;
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.menu.link'),
$container->get('database')
);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
$caption = '';
$num_links = $this->menuLinkManager->countMenuLinks($this->entity->id());
if ($num_links) {
$caption .= '<p>' . $this->formatPlural($num_links, '<strong>Warning:</strong> There is currently 1 menu link in %title. It will be deleted (system-defined links will be reset).', '<strong>Warning:</strong> There are currently @count menu links in %title. They will be deleted (system-defined links will be reset).', ['%title' => $this->entity->label()]) . '</p>';
}
$caption .= '<p>' . $this->t('This action cannot be undone.') . '</p>';
return $caption;
}
/**
* {@inheritdoc}
*/
protected function logDeletionMessage() {
$this->logger('menu')->notice('Deleted custom menu %title and all its menu links.', ['%title' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Locked menus may not be deleted.
if ($this->entity->isLocked()) {
return;
}
// Delete all links to the overview page for this menu.
// @todo Add a more generic helper function to the menu link plugin
// manager to remove links to an entity or other ID used as a route
// parameter that is being removed. Also, consider moving this to
// menu_ui.module as part of a generic response to entity deletion.
// https://www.drupal.org/node/2310329
$menu_links = $this->menuLinkManager->loadLinksByRoute('entity.menu.edit_form', ['menu' => $this->entity->id()], TRUE);
foreach ($menu_links as $id => $link) {
$this->menuLinkManager->removeDefinition($id);
}
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\menu_ui\Form;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a generic edit form for all menu link plugin types.
*
* The menu link plugin defines which class defines the corresponding form.
*
* @internal
*
* @see \Drupal\Core\Menu\MenuLinkInterface::getFormClass()
*/
class MenuLinkEditForm extends FormBase {
/**
* The class resolver.
*
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
*/
protected $classResolver;
/**
* Constructs a MenuLinkEditForm object.
*
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
*/
public function __construct(ClassResolverInterface $class_resolver) {
$this->classResolver = $class_resolver;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('class_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'menu_link_edit';
}
/**
* {@inheritdoc}
*
* @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\Core\Menu\MenuLinkInterface $menu_link_plugin
* The plugin instance to use for this form.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?MenuLinkInterface $menu_link_plugin = NULL) {
$form['menu_link_id'] = [
'#type' => 'value',
'#value' => $menu_link_plugin->getPluginId(),
];
$class_name = $menu_link_plugin->getFormClass();
$form['#plugin_form'] = $this->classResolver->getInstanceFromDefinition($class_name);
$form['#plugin_form']->setMenuLinkInstance($menu_link_plugin);
$form += $form['#plugin_form']->buildConfigurationForm($form, $form_state);
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#button_type' => 'primary',
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$form['#plugin_form']->validateConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$link = $form['#plugin_form']->submitConfigurationForm($form, $form_state);
$this->messenger()->addStatus($this->t('The menu link has been saved.'));
$form_state->setRedirect(
'entity.menu.edit_form',
['menu' => $link->getMenuName()]
);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Drupal\menu_ui\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a confirmation form for resetting a single modified menu link.
*
* @internal
*/
class MenuLinkResetForm extends ConfirmFormBase {
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The menu link.
*
* @var \Drupal\Core\Menu\MenuLinkInterface
*/
protected $link;
/**
* Constructs a MenuLinkResetForm object.
*
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
*/
public function __construct(MenuLinkManagerInterface $menu_link_manager) {
$this->menuLinkManager = $menu_link_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.menu.link')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'menu_link_reset_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to reset the link %item to its default values?', ['%item' => $this->link->getTitle()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.menu.edit_form', [
'menu' => $this->link->getMenuName(),
]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Any customizations will be lost. This action cannot be undone.');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Reset');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?MenuLinkInterface $menu_link_plugin = NULL) {
$this->link = $menu_link_plugin;
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->link = $this->menuLinkManager->resetLink($this->link->getPluginId());
$this->messenger()->addStatus($this->t('The menu link was reset to its default settings.'));
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* Checks access based on whether the link can be reset.
*
* @param \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin
* The menu link plugin being checked.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function linkIsResettable(MenuLinkInterface $menu_link_plugin) {
return AccessResult::allowedIf($menu_link_plugin->isResettable())->setCacheMaxAge(0);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\menu_ui\Menu;
use Drupal\Core\Access\AccessResult;
/**
* Provides menu tree manipulators to be used when managing menu links.
*/
class MenuUiMenuTreeManipulators {
/**
* Grants access to a menu tree when used in the menu management form.
*
* This manipulator allows access to menu links with inaccessible routes.
*
* Example use cases:
* - A login menu link, using the `user.login` route, is not accessible to a
* logged-in user, but the site builder still needs to configure the menu
* link.
* - A site builder wants to create a menu item for a Views page that has not
* been created. In this case, there is no access to the route because it
* does not exist.
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
*
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
*
* @internal
* This menu tree manipulator is intended for use only in the context of
* MenuForm because the user permissions to administer links is already
* checked. Don't use this manipulator in other places.
*
* @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
* @see \Drupal\menu_ui\MenuForm
*/
public function checkAccess(array $tree): array {
foreach ($tree as $element) {
$element->access = AccessResult::allowed();
if ($element->subtree) {
$element->subtree = $this->checkAccess($element->subtree);
}
}
return $tree;
}
}

View File

@@ -0,0 +1,529 @@
<?php
namespace Drupal\menu_ui;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Link;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkTreeElement;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\Core\Utility\LinkGeneratorInterface;
use Drupal\menu_link_content\MenuLinkContentStorageInterface;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\system\MenuStorage;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Base form for menu edit forms.
*
* @internal
*/
class MenuForm extends EntityForm {
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The menu tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuTree;
/**
* The link generator.
*
* @var \Drupal\Core\Utility\LinkGeneratorInterface
*/
protected $linkGenerator;
/**
* The menu_link_content storage handler.
*
* @var \Drupal\menu_link_content\MenuLinkContentStorageInterface
*/
protected $menuLinkContentStorage;
/**
* The overview tree form.
*
* @var array
*/
protected $overviewTreeForm = ['#tree' => TRUE];
/**
* Constructs a MenuForm object.
*
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
* The menu link manager.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
* The menu tree service.
* @param \Drupal\Core\Utility\LinkGeneratorInterface $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, MenuLinkContentStorageInterface $menu_link_content_storage) {
$this->menuLinkManager = $menu_link_manager;
$this->menuTree = $menu_tree;
$this->linkGenerator = $link_generator;
$this->menuLinkContentStorage = $menu_link_content_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.menu.link'),
$container->get('menu.link_tree'),
$container->get('link_generator'),
$container->get('entity_type.manager')->getStorage('menu_link_content')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$menu = $this->entity;
if ($this->operation == 'edit') {
$form['#title'] = $this->t('Edit menu %label', ['%label' => $menu->label()]);
}
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Title'),
'#default_value' => $menu->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#title' => $this->t('Menu name'),
'#default_value' => $menu->id(),
'#maxlength' => MenuStorage::MAX_ID_LENGTH,
'#description' => $this->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
'#machine_name' => [
'exists' => [$this, 'menuNameExists'],
'source' => ['label'],
'replace_pattern' => '[^a-z0-9-]+',
'replace' => '-',
],
// A menu's machine name cannot be changed.
'#disabled' => !$menu->isNew() || $menu->isLocked(),
];
$form['description'] = [
'#type' => 'textfield',
'#title' => $this->t('Administrative summary'),
'#maxlength' => 512,
'#default_value' => $menu->getDescription(),
];
$form['langcode'] = [
'#type' => 'language_select',
'#title' => $this->t('Menu language'),
'#languages' => LanguageInterface::STATE_ALL,
'#default_value' => $menu->language()->getId(),
];
// Add menu links administration form for existing menus.
if (!$menu->isNew() || $menu->isLocked()) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow handling the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
// @see self::submitOverviewForm()
$form_state->set('menu_overview_form_parents', ['links']);
$form['links'] = [];
$form['links'] = $this->buildOverviewForm($form['links'], $form_state);
}
return parent::form($form, $form_state);
}
/**
* Returns whether a menu name already exists.
*
* @param string $value
* The name of the menu.
*
* @return bool
* Returns TRUE if the menu already exists, FALSE otherwise.
*/
public function menuNameExists($value) {
// Check first to see if a menu with this ID exists.
if ($this->entityTypeManager->getStorage('menu')->getQuery()->condition('id', $value)->range(0, 1)->count()->execute()) {
return TRUE;
}
// Check for a link assigned to this menu.
return $this->menuLinkManager->menuNameInUse($value);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$menu = $this->entity;
$status = $menu->save();
$edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString();
if ($status == SAVED_UPDATED) {
$this->messenger()->addStatus($this->t('Menu %label has been updated.', ['%label' => $menu->label()]));
$this->logger('menu')->notice('Menu %label has been updated.', ['%label' => $menu->label(), 'link' => $edit_link]);
}
else {
$this->messenger()->addStatus($this->t('Menu %label has been added.', ['%label' => $menu->label()]));
$this->logger('menu')->notice('Menu %label has been added.', ['%label' => $menu->label(), 'link' => $edit_link]);
}
$form_state->setRedirectUrl($this->entity->toUrl('edit-form'));
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
parent::submitForm($form, $form_state);
if (!$this->entity->isNew() || $this->entity->isLocked()) {
$this->submitOverviewForm($form, $form_state);
}
}
/**
* Form constructor to edit an entire menu tree at once.
*
* Shows for one menu the menu links accessible to the current user and
* relevant operations.
*
* This form constructor can be integrated as a section into another form. It
* relies on the following keys in $form_state:
* - menu: A menu entity.
* - menu_overview_form_parents: An array containing the parent keys to this
* form.
* Forms integrating this section should call menu_overview_form_submit() from
* their form submit handler.
*/
protected function buildOverviewForm(array &$form, FormStateInterface $form_state) {
// Ensure that menu_overview_form_submit() knows the parents of this form
// section.
if (!$form_state->has('menu_overview_form_parents')) {
$form_state->set('menu_overview_form_parents', []);
}
$form['#attached']['library'][] = 'menu_ui/drupal.menu_ui.adminforms';
$tree = $this->menuTree->load($this->entity->id(), new MenuTreeParameters());
// We indicate that a menu administrator is running the menu access check.
$this->getRequest()->attributes->set('_menu_admin', TRUE);
$manipulators = [
// Use a dedicated menu tree access check manipulator as users editing
// this form, granted with 'administer menu' permission, should be able to
// access menu links with inaccessible routes. The default menu tree
// manipulator only allows the access to menu links with accessible routes.
// @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkAccess()
// @see \Drupal\menu_ui\Menu\MenuUiMenuTreeManipulators::checkAccess()
['callable' => 'menu_ui.menu_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
$tree = $this->menuTree->transform($tree, $manipulators);
$this->getRequest()->attributes->set('_menu_admin', FALSE);
// Determine the delta; the number of weights to be made available.
$count = function (array $tree) {
$sum = function ($carry, MenuLinkTreeElement $item) {
return $carry + $item->count();
};
return array_reduce($tree, $sum);
};
$delta = max($count($tree), 50);
$form['links'] = [
'#type' => 'table',
'#theme' => 'table__menu_overview',
'#header' => [
$this->t('Menu link'),
[
'data' => $this->t('Enabled'),
'class' => ['checkbox'],
],
$this->t('Weight'),
[
'data' => $this->t('Operations'),
'colspan' => 3,
],
],
'#attributes' => [
'id' => 'menu-overview',
],
'#tabledrag' => [
[
'action' => 'match',
'relationship' => 'parent',
'group' => 'menu-parent',
'subgroup' => 'menu-parent',
'source' => 'menu-id',
'hidden' => TRUE,
'limit' => $this->menuTree->maxDepth() - 1,
],
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'menu-weight',
],
],
];
$form['links']['#empty'] = $this->t('There are no menu links yet. <a href=":url">Add link</a>.', [
':url' => Url::fromRoute('entity.menu.add_link_form', ['menu' => $this->entity->id()], [
'query' => ['destination' => $this->entity->toUrl('edit-form')->toString()],
])->toString(),
]);
$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) {
if (isset($links[$id]['#item'])) {
$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'];
// TableDrag: Mark the table row as draggable.
$form['links'][$id]['#attributes'] = $element['#attributes'];
$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.
$form['links'][$id]['#weight'] = $element['#item']->link->getWeight();
// Add special classes to be used for tabledrag.js.
$element['parent']['#attributes']['class'] = ['menu-parent'];
$element['weight']['#attributes']['class'] = ['menu-weight'];
$element['id']['#attributes']['class'] = ['menu-id'];
$form['links'][$id]['title'] = [
[
'#theme' => 'indentation',
'#size' => $element['#item']->depth - 1,
],
$element['title'],
];
$form['links'][$id]['enabled'] = $element['enabled'];
$form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled'];
// 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.
$form['links'][$id]['operations'] = $element['operations'];
$form['links'][$id]['id'] = $element['id'];
$form['links'][$id]['parent'] = $element['parent'];
}
}
return $form;
}
/**
* Recursive helper function for buildOverviewForm().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load().
* @param int $delta
* The default number of menu items used in the menu weight selector is 50.
*
* @return array
* The overview tree form.
*/
protected function buildOverviewTreeForm($tree, $delta) {
$form = &$this->overviewTreeForm;
$tree_access_cacheability = new CacheableMetadata();
foreach ($tree as $element) {
$tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
// Only render accessible links.
if (!$element->access->isAllowed()) {
continue;
}
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($link) {
$id = 'menu_plugin_id:' . $link->getPluginId();
$form[$id]['#item'] = $element;
$form[$id]['#attributes'] = $link->isEnabled() ? ['class' => ['menu-enabled']] : ['class' => ['menu-disabled']];
$form[$id]['title'] = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toRenderable();
if (!$link->isEnabled()) {
$form[$id]['title']['#suffix'] = ' (' . $this->t('disabled') . ')';
}
// @todo Remove this in https://www.drupal.org/node/2568785.
elseif ($id === 'menu_plugin_id:user.logout') {
$form[$id]['title']['#suffix'] = ' (' . $this->t('<q>Log in</q> for anonymous users') . ')';
}
// @todo Remove this in https://www.drupal.org/node/2568785.
elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') {
$form[$id]['title']['#suffix'] = ' (' . $this->t('logged in users only') . ')';
}
$form[$id]['enabled'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable @title menu link', ['@title' => $link->getTitle()]),
'#title_display' => 'invisible',
'#default_value' => $link->isEnabled(),
];
$form[$id]['weight'] = [
'#type' => 'weight',
'#delta' => $delta,
'#default_value' => $link->getWeight(),
'#title' => $this->t('Weight for @title', ['@title' => $link->getTitle()]),
'#title_display' => 'invisible',
];
$form[$id]['id'] = [
'#type' => 'hidden',
'#value' => $link->getPluginId(),
];
$form[$id]['parent'] = [
'#type' => 'hidden',
'#default_value' => $link->getParent(),
];
$operations = $link->getOperations();
if ($element->depth < $this->menuTree->maxDepth()) {
$add_link_url = Url::fromRoute(
'entity.menu.add_link_form',
['menu' => $this->entity->id()],
['query' => ['parent' => $link->getPluginId()]]
);
$operations += [
'add-child' => [
'title' => $this->t('Add child'),
'weight' => 20,
'url' => $add_link_url,
],
];
uasort($operations, [SortArray::class, 'sortByWeightElement']);
}
foreach ($operations as $key => $operation) {
if (!isset($operations[$key]['query'])) {
// Bring the user back to the menu overview.
$operations[$key]['query'] = $this->getDestinationArray();
}
}
$form[$id]['operations'] = [
'#type' => 'operations',
'#links' => $operations,
];
}
if ($element->subtree) {
$this->buildOverviewTreeForm($element->subtree, $delta);
}
}
$tree_access_cacheability
->merge(CacheableMetadata::createFromRenderArray($form))
->applyTo($form);
return $form;
}
/**
* Submit handler for the menu overview form.
*
* This function takes great care in saving parent items first, then items
* underneath them. Saving items in the incorrect order can break the tree.
*/
protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) {
// Form API supports constructing and validating self-contained sections
// within forms, but does not allow to handle the form section's submission
// equally separated yet. Therefore, we use a $form_state key to point to
// the parents of the form section.
$parents = $form_state->get('menu_overview_form_parents');
$input = NestedArray::getValue($form_state->getUserInput(), $parents);
$form = &NestedArray::getValue($complete_form, $parents);
// When dealing with saving menu items, the order in which these items are
// saved is critical. If a changed child item is saved before its parent,
// the child item could be saved with an invalid path past its immediate
// parent. To prevent this, save items in the form in the same order they
// are sent, ensuring parents are saved first, then their children.
// See https://www.drupal.org/node/181126#comment-632270.
$order = is_array($input) ? array_flip(array_keys($input)) : [];
// Update our original form with the new order.
$form = array_intersect_key(array_merge($order, $form), $form);
$fields = ['weight', 'parent', 'enabled'];
$form_links = $form['links'];
foreach (Element::children($form_links) as $id) {
if (isset($form_links[$id]['#item'])) {
$element = $form_links[$id];
$updated_values = [];
// Update any fields that have changed in this menu item.
foreach ($fields as $field) {
if (isset($element[$field]['#value']) && $element[$field]['#value'] != $element[$field]['#default_value']) {
$updated_values[$field] = $element[$field]['#value'];
}
}
if ($updated_values) {
// Use the ID from the actual plugin instance since the hidden value
// in the form could be tampered with.
$this->menuLinkManager->updateDefinition($element['#item']->link->getPluginId(), $updated_values);
}
}
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Drupal\menu_ui;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
* Defines a class to build a listing of menu entities.
*
* @see \Drupal\system\Entity\Menu
* @see menu_entity_info()
*/
class MenuListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
protected function getEntityIds() {
$query = $this
->getStorage()
->getQuery()
->sort('label', 'ASC');
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['title'] = t('Title');
$header['description'] = [
'data' => t('Description'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
];
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['title'] = [
'data' => $entity->label(),
'class' => ['menu-label'],
];
$row['description']['data'] = ['#markup' => $entity->getDescription()];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
if (isset($operations['edit'])) {
$operations['edit']['title'] = t('Edit menu');
$operations['add'] = [
'title' => t('Add link'),
'weight' => 20,
'url' => $entity->toUrl('add-link-form'),
'query' => [
'destination' => $entity->toUrl('edit-form')->toString(),
],
];
}
if (isset($operations['delete'])) {
$operations['delete']['title'] = t('Delete menu');
}
return $operations;
}
/**
* {@inheritdoc}
*/
protected function ensureDestination(Url $url) {
// We don't want to add the destination URL here, as it means we get
// redirected back to the list-builder after adding/deleting menu links from
// a menu.
return $url;
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$build['#attached']['library'][] = "menu_ui/drupal.menu_ui.adminforms";
return $build;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Drupal\menu_ui\Plugin\Menu\LocalAction;
use Drupal\Core\Menu\LocalActionDefault;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Modifies the 'Add link' local action to add a destination.
*/
class MenuLinkAdd extends LocalActionDefault {
/**
* The redirect destination.
*
* @var \Drupal\Core\Routing\RedirectDestinationInterface
*/
private $redirectDestination;
/**
* Constructs a MenuLinkAdd object.
*
* @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\Routing\RouteProviderInterface $route_provider
* The route provider to load routes by name.
* @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
* The redirect destination.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, RedirectDestinationInterface $redirect_destination) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider);
$this->redirectDestination = $redirect_destination;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider'),
$container->get('redirect.destination')
);
}
/**
* {@inheritdoc}
*/
public function getOptions(RouteMatchInterface $route_match) {
$options = parent::getOptions($route_match);
// Append the current path as destination to the query string.
$options['query']['destination'] = $this->redirectDestination->get();
return $options;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Drupal\menu_ui\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Validation constraint for changing the menu settings in pending revisions.
*/
#[Constraint(
id: 'MenuSettings',
label: new TranslatableMarkup('Menu settings.', [], ['context' => 'Validation'])
)]
class MenuSettingsConstraint extends SymfonyConstraint {
public $message = 'You can only change the menu settings for the <em>published</em> version of this content.';
public $messageWeight = 'You can only change the menu link weight for the <em>published</em> version of this content.';
public $messageParent = 'You can only change the parent menu link for the <em>published</em> version of this content.';
public $messageRemove = 'You can only remove the menu link in the <em>published</em> version of this content.';
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Drupal\menu_ui\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Constraint validator for changing the menu settings in pending revisions.
*/
class MenuSettingsConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint) {
if (isset($entity) && !$entity->isNew() && !$entity->isDefaultRevision()) {
$defaults = menu_ui_get_menu_link_defaults($entity);
// If the menu UI entity builder is not present and the menu property has
// not been set, do not attempt to validate the menu settings since they
// are not being modified.
if (!$values = $entity->menu) {
return;
}
if (trim($values['title']) && !empty($values['menu_parent'])) {
[$menu_name, $parent] = explode(':', $values['menu_parent'], 2);
$values['menu_name'] = $menu_name;
$values['parent'] = $parent;
}
// Handle the case when the menu link is deleted in a pending revision.
if (empty($values['enabled']) && $defaults['entity_id']) {
$this->context->buildViolation($constraint->messageRemove)
->atPath('menu')
->setInvalidValue($entity)
->addViolation();
}
// Handle all the other non-revisionable menu link changes in a pending
// revision.
elseif ($defaults['entity_id']) {
if ($defaults['entity_id'] && ($values['menu_name'] != $defaults['menu_name'])) {
$this->context->buildViolation($constraint->messageParent)
->atPath('menu.menu_parent')
->setInvalidValue($entity)
->addViolation();
}
elseif (isset($values['parent']) && ($values['parent'] != $defaults['parent'])) {
$this->context->buildViolation($constraint->messageParent)
->atPath('menu.menu_parent')
->setInvalidValue($entity)
->addViolation();
}
elseif (($values['weight'] != $defaults['weight'])) {
$this->context->buildViolation($constraint->messageWeight)
->atPath('menu.weight')
->setInvalidValue($entity)
->addViolation();
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{#
/**
* @file
* Default theme implementation for menu_link_form.
*
* Two-column template for the menu link add/edit form.
*
* This template will be used when a menu link form specifies
* 'menu_link_form' as its #theme callback. Otherwise, by default,
* menu_link add/edit forms will be themed by form.html.twig.
*
* Available variables:
* - form: The menu link add/edit form.
*
* @ingroup themeable
*/
#}
{{ form }}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for menu_ui.
*
* @group menu_ui
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\Core\Url;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\system\Functional\Cache\PageCacheTagsTestBase;
use Drupal\system\Entity\Menu;
/**
* Tests the Menu and Menu Link entities' cache tags.
*
* @group menu_ui
*/
class MenuCacheTagsTest extends PageCacheTagsTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['menu_ui', 'block', 'test_page_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests cache tags presence and invalidation of the Menu entity.
*
* Tests the following cache tags:
* - "menu:<menu ID>"
*/
public function testMenuBlock(): void {
$url = Url::fromRoute('test_page_test.test_page');
// Create a Llama menu, add a link to it and place the corresponding block.
$menu = Menu::create([
'id' => 'llama',
'label' => 'Llama',
'description' => 'Description text',
]);
$menu->save();
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
// Move a link into the new menu.
$menu_link = $menu_link_manager->updateDefinition('test_page_test.test_page', ['menu_name' => 'llama', 'parent' => '']);
$block = $this->drupalPlaceBlock('system_menu_block:llama', ['label' => 'Llama', 'provider' => 'system', 'region' => 'footer']);
// Prime the page cache.
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit, but also the presence of the correct cache tags.
$expected_tags = [
'http_response',
'rendered',
'block_view',
'config:block_list',
'config:block.block.' . $block->id(),
'config:system.menu.llama',
// The cache contexts associated with the (in)accessible menu links are
// bubbled.
'config:user.role.anonymous',
];
$this->verifyPageCache($url, 'HIT', $expected_tags);
// Verify that after modifying the menu, there is a cache miss.
$menu->set('label', 'Awesome llama');
$menu->save();
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT');
// Verify that after modifying the menu link weight, there is a cache miss.
$menu_link_manager->updateDefinition('test_page_test.test_page', ['weight' => -10]);
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT');
// Verify that after adding a menu link, there is a cache miss.
$menu_link_2 = MenuLinkContent::create([
'id' => '',
'parent' => '',
'title' => 'Alpaca',
'menu_name' => 'llama',
'link' => [
['uri' => 'internal:/'],
],
'bundle' => 'menu_name',
]);
$menu_link_2->save();
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT');
// Verify that after resetting the first menu link, there is a cache miss.
$this->assertTrue($menu_link->isResettable(), 'First link can be reset');
$menu_link = $menu_link_manager->resetLink($menu_link->getPluginId());
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT', $expected_tags);
// Verify that after deleting the menu, there is a cache miss.
$menu->delete();
$this->verifyPageCache($url, 'MISS');
// Verify a cache hit.
$this->verifyPageCache($url, 'HIT', ['config:block_list', 'config:user.role.anonymous', 'http_response', 'rendered']);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Reorder menu items.
*
* @group menu_ui
*/
class MenuLinkReorderTest extends BrowserTestBase {
/**
* An administrator user.
*
* @var \Drupal\user\UserInterface
*/
protected $administrator;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['menu_ui', 'test_page_test', 'node', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests creating, editing, deleting menu links via node form widget.
*/
public function testDefaultMenuLinkReorder(): void {
// Add the main menu block.
$this->drupalPlaceBlock('system_menu_block:main');
// Assert that the Home link is available.
$this->drupalGet('test-page');
$this->assertSession()->linkExists('Home');
// The administrator user that can re-order menu links.
$this->administrator = $this->drupalCreateUser([
'administer site configuration',
'access administration pages',
'administer menu',
]);
$this->drupalLogin($this->administrator);
// Change the weight of the link to a non default value.
$edit = [
'links[menu_plugin_id:test_page_test.front_page][weight]' => -10,
];
$this->drupalGet('admin/structure/menu/manage/main');
$this->submitForm($edit, 'Save');
// The link is still there.
$this->drupalGet('test-page');
$this->assertSession()->linkExists('Home');
// Clear all caches.
$this->drupalGet('admin/config/development/performance');
$this->submitForm([], 'Clear all caches');
// Clearing all caches should not affect the state of the menu link.
$this->drupalGet('test-page');
$this->assertSession()->linkExists('Home');
}
}

View File

@@ -0,0 +1,243 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
/**
* Tests Menu UI and Content Moderation integration.
*
* @group menu_ui
*/
class MenuUiContentModerationTest extends BrowserTestBase {
use ContentModerationTestTrait;
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'block',
'content_moderation',
'node',
'menu_ui',
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:main');
// Create a 'page' content type.
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
$workflow->save();
}
/**
* Tests that node drafts can not modify the menu settings.
*/
public function testMenuUiWithPendingRevisions(): void {
$editor = $this->drupalCreateUser([
'administer nodes',
'administer menu',
'create page content',
'edit any page content',
'use editorial transition create_new_draft',
'use editorial transition publish',
'view latest version',
'view any unpublished content',
]);
$this->drupalLogin($editor);
// Create a node.
$node = $this->drupalCreateNode();
// Publish the node with no changes.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
// Create a pending revision with no changes.
$edit = ['moderation_state[0][state]' => 'draft'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
// Add a menu link and save a new default (published) revision.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => 'Test menu link',
'moderation_state[0][state]' => 'published',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test menu link');
// Try to change the menu link weight and save a new non-default (draft)
// revision.
$edit = [
'menu[weight]' => 1,
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check that the menu settings were not applied.
$this->assertSession()->pageTextContains('You can only change the menu link weight for the published version of this content.');
// Try to change the menu link parent and save a new non-default (draft)
// revision.
$edit = [
'menu[menu_parent]' => 'main:test_page_test.front_page',
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check that the menu settings were not applied.
$this->assertSession()->pageTextContains('You can only change the parent menu link for the published version of this content.');
// Try to delete the menu link and save a new non-default (draft) revision.
$edit = [
'menu[enabled]' => 0,
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Check that the menu settings were not applied.
$this->assertSession()->pageTextContains('You can only remove the menu link in the published version of this content.');
$this->assertSession()->linkExists('Test menu link');
// Try to change the menu link title and description and save a new
// non-default (draft) revision.
$edit = [
'menu[title]' => 'Test menu link draft',
'menu[description]' => 'Test menu link description',
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
// Ensure the content was not immediately published.
$this->assertSession()->linkExists('Test menu link');
// Publish the node and ensure the new link text was published.
$edit = [
'moderation_state[0][state]' => 'published',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('Test menu link draft');
// Try to save a new non-default (draft) revision without any changes and
// check that the error message is not shown.
$edit = ['moderation_state[0][state]' => 'draft'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Create a node.
$node = $this->drupalCreateNode();
// Publish the node with no changes.
$edit = ['moderation_state[0][state]' => 'published'];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
// Add a menu link and save and create a new non-default (draft) revision
// and ensure it's not immediately published.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => 'Second test menu link',
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
$this->assertSession()->linkNotExists('Second test menu link');
// Publish the content and ensure the new menu link shows up.
$edit = [
'moderation_state[0][state]' => 'published',
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("Page {$node->label()} has been updated.");
$this->assertSession()->linkExists('Second test menu link');
}
/**
* Tests that unpublished content can be selected through the menu UI.
*/
public function testMenuUiWithUnpublishedContent(): void {
$editor_with_unpublished_content_access = $this->drupalCreateUser([
'administer nodes',
'administer menu',
'create page content',
'use editorial transition create_new_draft',
'view any unpublished content',
]);
$this->drupalLogin($editor_with_unpublished_content_access);
// Create a node.
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'moderation_state[0][state]' => 'draft',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
// Assert that the unpublished node can be selected as a parent menu link
// for users with access to the node.
$node = $this->drupalGetNodeByTitle($node_title);
$this->assertTrue($node->access('view', $editor_with_unpublished_content_access));
$this->assertEquals($edit['title[0][value]'], $node->getTitle());
$this->drupalGet('node/add/page');
$link_id = menu_ui_get_menu_link_defaults($node)['entity_id'];
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $link */
$link = MenuLinkContent::load($link_id);
$this->assertSession()->optionExists('edit-menu-menu-parent', 'main:' . $link->getPluginId());
// Assert that the unpublished node cannot be selected as a parent menu link
// for users without access to the node.
$editor_without_unpublished_content_access = $this->drupalCreateUser([
'administer nodes',
'administer menu',
'create page content',
'use editorial transition create_new_draft',
]);
$this->drupalLogin($editor_without_unpublished_content_access);
$this->assertFalse($node->access('view', $editor_without_unpublished_content_access));
$this->drupalGet('node/add/page');
$this->assertSession()->optionNotExists('edit-menu-menu-parent', 'main:' . $link->getPluginId());
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\language\Traits\LanguageTestTrait;
/**
* Tests Menu UI and Content Translation integration for content entities.
*
* @group menu_ui
*/
class MenuUiContentTranslationTest extends BrowserTestBase {
use LanguageTestTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'block',
'language',
'content_translation',
'menu_ui',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Place menu block and local tasks block.
$this->drupalPlaceBlock('system_menu_block:main');
$this->drupalPlaceBlock('local_tasks_block');
// Create a 'page' content type.
$this->drupalCreateContentType([
'type' => 'page',
'name' => 'Basic page',
'display_submitted' => FALSE,
]);
// Add a second language.
static::createLanguageFromLangcode('de');
// Create an account and login.
$user = $this->drupalCreateUser([
'administer site configuration',
'administer nodes',
'create page content',
'edit any page content',
'delete any page content',
'administer content translation',
'translate any entity',
'create content translations',
'administer languages',
'administer content types',
'administer menu',
]);
$this->drupalLogin($user);
// Enable translation for page nodes and menu link content.
static::enableBundleTranslation('node', 'page');
static::enableBundleTranslation('menu_link_content', 'menu_link_content');
}
/**
* Gets a content entity object by title.
*
* @param string $entity_type_id
* Id of content entity type of content entity to load.
* @param string $title
* Title of content entity to load.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* First found content entity with given title.
*/
protected function getContentEntityByTitle($entity_type_id, $title) {
$entity_type_manager = $this->container->get('entity_type.manager');
$storage = $entity_type_manager->getStorage($entity_type_id);
$storage->resetCache();
$entities = $storage->loadByProperties([
'title' => $title,
]);
return reset($entities);
}
/**
* Provides test data sets for testChangeContentToPseudoLanguage().
*
* @return array
* Data sets to test keyed by data set label.
*/
public static function provideChangeContentToPseudoLanguageData() {
return [
'und' => ['langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED],
'zxx' => ['langcode' => LanguageInterface::LANGCODE_NOT_APPLICABLE],
];
}
/**
* Tests changing content with menu link from language to pseudo language.
*
* @param string $langcode
* Language code of pseudo-language to change content language to.
* Either \Drupal\Core\LanguageInterface::LANGCODE_NOT_SPECIFIED or
* \Drupal\Core\LanguageInterface::LANGCODE_NOT_APPLICABLE.
*
* @dataProvider provideChangeContentToPseudoLanguageData
*/
public function testChangeContentToPseudoLanguage($langcode): void {
$node_title = 'Test node';
$menu_link_title_en = 'Test menu link EN';
$menu_link_title_pseudo = 'Test menu link PSEUDO';
// Create a page node in English.
$this->drupalGet('node/add/page');
$this->assertSession()->statusCodeEquals(200);
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => $menu_link_title_en,
];
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Assert that node exists and node language is English.
$node = $this->getContentEntityByTitle('node', $node_title);
$this->assertTrue(is_object($node));
$this->assertTrue($node->language()->getId() == 'en');
// Assert that menu link exists and menu link language is English.
$menu_link = $this->getContentEntityByTitle('menu_link_content', $menu_link_title_en);
$this->assertTrue(is_object($menu_link));
$this->assertTrue($menu_link->language()->getId() == 'en');
$this->assertTrue($menu_link->hasTranslation('en'));
// Assert that menu link is visible with initial title.
$this->assertSession()->linkExists($menu_link_title_en);
// Change language of page node and title of its menu link.
$this->clickLink('Edit');
$edit = [
'langcode[0][value]' => $langcode,
'menu[title]' => $menu_link_title_pseudo,
];
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Assert that node exists and node language is target language.
$node = $this->getContentEntityByTitle('node', $node_title);
$this->assertTrue(is_object($node));
$this->assertTrue($node->language()->getId() == $langcode);
// Assert that menu link exists and menu link language is target language.
$menu_link = $this->getContentEntityByTitle('menu_link_content', $menu_link_title_pseudo);
$this->assertTrue(is_object($menu_link));
$this->assertTrue($menu_link->language()->getId() == $langcode);
$this->assertFalse($menu_link->hasTranslation('en'));
// Assert that menu link is visible with updated title.
$this->assertSession()->linkExists($menu_link_title_pseudo);
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\menu_ui\Traits\MenuUiTrait;
/**
* Tests for menu_ui language settings.
*
* Create menu and menu links in non-English language, and edit language
* settings.
*
* @group menu_ui
*/
class MenuUiLanguageTest extends BrowserTestBase {
use MenuUiTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'language',
'menu_link_content',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'administer menu',
]));
// Add some custom languages.
foreach (['aa', 'bb', 'cc', 'cs'] as $language_code) {
ConfigurableLanguage::create([
'id' => $language_code,
'label' => $this->randomMachineName(),
])->save();
}
}
/**
* Tests menu language settings and the defaults for menu link items.
*/
public function testMenuLanguage(): void {
// Create a test menu to test the various language-related settings.
// Machine name has to be lowercase.
$menu_name = $this->randomMachineName(16);
$label = $this->randomString();
$edit = [
'id' => $menu_name,
'description' => '',
'label' => $label,
'langcode' => 'aa',
];
$this->drupalGet('admin/structure/menu/add');
$this->submitForm($edit, 'Save');
ContentLanguageSettings::loadByEntityTypeBundle('menu_link_content', 'menu_link_content')
->setDefaultLangcode('bb')
->setLanguageAlterable(TRUE)
->save();
// Check menu language.
$this->assertTrue($this->assertSession()->optionExists('edit-langcode', $edit['langcode'])->isSelected());
// Test menu link language.
$link_path = '/';
// Add a menu link.
$link_title = $this->randomString();
$edit = [
'title[0][value]' => $link_title,
'link[0][uri]' => $link_path,
];
$this->drupalGet("admin/structure/menu/manage/{$menu_name}/add");
$this->submitForm($edit, 'Save');
// Check the link was added with the correct menu link default language.
$menu_links = \Drupal::entityTypeManager()->getStorage('menu_link_content')->loadByProperties(['title' => $link_title]);
$menu_link = reset($menu_links);
$this->assertMenuLink([
'menu_name' => $menu_name,
'route_name' => '<front>',
'langcode' => 'bb',
], $menu_link->getPluginId());
// Edit menu link default, changing it to cc.
ContentLanguageSettings::loadByEntityTypeBundle('menu_link_content', 'menu_link_content')
->setDefaultLangcode('cc')
->setLanguageAlterable(TRUE)
->save();
// Add a menu link.
$link_title = $this->randomString();
$edit = [
'title[0][value]' => $link_title,
'link[0][uri]' => $link_path,
];
$this->drupalGet("admin/structure/menu/manage/{$menu_name}/add");
$this->submitForm($edit, 'Save');
// Check the link was added with the correct new menu link default language.
$menu_links = \Drupal::entityTypeManager()->getStorage('menu_link_content')->loadByProperties(['title' => $link_title]);
$menu_link = reset($menu_links);
$this->assertMenuLink([
'menu_name' => $menu_name,
'route_name' => '<front>',
'langcode' => 'cc',
], $menu_link->getPluginId());
// Now change the language of the new link to 'bb'.
$edit = [
'langcode[0][value]' => 'bb',
];
$this->drupalGet('admin/structure/menu/item/' . $menu_link->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertMenuLink([
'menu_name' => $menu_name,
'route_name' => '<front>',
'langcode' => 'bb',
], $menu_link->getPluginId());
// Saving menu link items ends up on the edit menu page. To check the menu
// link has the correct language default on edit, go to the menu link edit
// page first.
$this->drupalGet('admin/structure/menu/item/' . $menu_link->id() . '/edit');
// Check that the language selector has the correct default value.
$this->assertTrue($this->assertSession()->optionExists('edit-langcode-0-value', 'bb')->isSelected());
// Edit menu to hide the language select on menu link item add.
ContentLanguageSettings::loadByEntityTypeBundle('menu_link_content', 'menu_link_content')
->setDefaultLangcode('cc')
->setLanguageAlterable(FALSE)
->save();
// Check that the language selector is not available on menu link add page.
$this->drupalGet("admin/structure/menu/manage/$menu_name/add");
$this->assertSession()->fieldNotExists('edit-langcode-0-value');
}
}

View File

@@ -0,0 +1,466 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\system\Entity\Menu;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\content_translation\Traits\ContentTranslationTestTrait;
/**
* Add, edit, and delete a node with menu link.
*
* @group menu_ui
*/
class MenuUiNodeTest extends BrowserTestBase {
use ContentTranslationTestTrait;
/**
* An editor user.
*
* @var \Drupal\user\UserInterface
*/
protected $editor;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'menu_ui',
'test_page_test',
'node',
'block',
'locale',
'language',
'content_translation',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_menu_block:main');
$this->drupalPlaceBlock('page_title_block');
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->editor = $this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer menu',
'create page content',
'edit any page content',
'delete any page content',
'create content translations',
'update content translations',
'delete content translations',
'translate any entity',
]);
$this->drupalLogin($this->editor);
}
/**
* Tests creating, editing, deleting menu links via node form widget.
*/
public function testMenuNodeFormWidget(): void {
// Verify that cacheability metadata is bubbled from the menu link tree
// access checking that is performed when determining the "default parent
// item" options in menu_ui_form_node_type_form_alter(). The "log out" link
// adds the "user.roles:authenticated" cache context.
$this->drupalGet('admin/structure/types/manage/page');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Contexts', 'user.roles:authenticated');
// Assert the description of "Available menus" checkboxes field.
$this->assertSession()->pageTextContains('Content of this type can be placed in the selected menus.');
// Verify that the menu link title has the correct maxlength.
$title_max_length = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('menu_link_content')['title']->getSetting('max_length');
$this->drupalGet('node/add/page');
$this->assertSession()->responseMatches('/<input .* id="edit-menu-title" .* maxlength="' . $title_max_length . '" .* \/>/');
// Verify that the menu link description has the correct maxlength.
$description_max_length = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions('menu_link_content')['description']->getSetting('max_length');
$this->drupalGet('node/add/page');
$this->assertSession()->responseMatches('/<input .* id="edit-menu-description" .* maxlength="' . $description_max_length . '" .* \/>/');
// Disable the default main menu, so that no menus are enabled.
$edit = [
'menu_options[main]' => FALSE,
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
// Verify that no menu settings are displayed and nodes can be created.
$this->drupalGet('node/add/page');
$this->assertSession()->pageTextContains('Create Basic page');
$this->assertSession()->pageTextNotContains('Menu settings');
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'body[0][value]' => $this->randomString(),
];
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($node_title);
$this->assertEquals($edit['title[0][value]'], $node->getTitle());
// Test that we cannot set a menu item from a menu that is not set as
// available.
$edit = [
'menu_options[tools]' => 1,
'menu_parent' => 'main:',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The selected menu link is not under one of the selected menus.');
$this->assertSession()->pageTextNotContains("The content type Basic page has been updated.");
// Enable Tools menu as available menu.
$edit = [
'menu_options[main]' => 1,
'menu_options[tools]' => 1,
'menu_parent' => 'main:',
];
$this->drupalGet('admin/structure/types/manage/page');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains("The content type Basic page has been updated.");
// Test that we can preview a node that will create a menu item.
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => 'Test preview',
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Preview');
// Create a node.
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'body[0][value]' => $this->randomString(),
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($node_title);
// Assert that there is no link for the node.
$this->drupalGet('test-page');
$this->assertSession()->linkNotExists($node_title);
// Edit the node, enable the menu link setting, but skip the link title.
$edit = [
'menu[enabled]' => 1,
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Assert that there is a link for the node.
$this->drupalGet('test-page');
$this->assertSession()->linkExists($node_title);
// Make sure the menu links only appear when the node is published.
// These buttons just appear for 'administer nodes' users.
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer nodes',
'administer menu',
'create page content',
'edit any page content',
]);
$this->drupalLogin($admin_user);
// Assert that the link does not exist if unpublished.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'status[value]' => FALSE,
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->drupalGet('test-page');
$this->assertSession()->linkNotExists($node_title, 'Found no menu link with the node unpublished');
// Assert that the link exists if published.
$edit['status[value]'] = TRUE;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->drupalGet('test-page');
$this->assertSession()->linkExists($node_title, 0, 'Found a menu link with the node published');
// Log back in as normal user.
$this->drupalLogin($this->editor);
// Edit the node and create a menu link.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'menu[weight]' => 17,
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Assert that the link exists.
$this->drupalGet('test-page');
$this->assertSession()->linkExists($node_title);
// Check if menu weight is 17.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->fieldValueEquals('edit-menu-weight', 17);
// Verify that the menu link title field has correct maxlength in node edit
// form.
$this->assertSession()->responseMatches('/<input .* id="edit-menu-title" .* maxlength="' . $title_max_length . '" .* \/>/');
// Verify that the menu link description field has correct maxlength in
// node add form.
$this->assertSession()->responseMatches('/<input .* id="edit-menu-description" .* maxlength="' . $description_max_length . '" .* \/>/');
// Disable the menu link, then edit the node--the link should stay disabled.
$link_id = menu_ui_get_menu_link_defaults($node)['entity_id'];
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $link */
$link = MenuLinkContent::load($link_id);
$link->set('enabled', FALSE);
$link->save();
$this->drupalGet($node->toUrl('edit-form'));
$this->submitForm($edit, 'Save');
$link = MenuLinkContent::load($link_id);
$this->assertFalse($link->isEnabled(), 'Saving a node with a disabled menu link keeps the menu link disabled.');
// Edit the node and remove the menu link.
$edit = [
'menu[enabled]' => FALSE,
];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
// Assert that there is no link for the node.
$this->drupalGet('test-page');
$this->assertSession()->linkNotExists($node_title);
// Add a menu link to the Administration menu.
$item = MenuLinkContent::create([
'link' => [['uri' => 'entity:node/' . $node->id()]],
'title' => $this->randomMachineName(16),
'menu_name' => 'admin',
]);
$item->save();
// Assert that disabled Administration menu is not shown on the
// node/$nid/edit page.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->pageTextContains('Provide a menu link');
// Assert that the link is still in the Administration menu after save.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$link = MenuLinkContent::load($item->id());
$this->assertInstanceOf(MenuLinkContent::class, $link);
// Move the menu link back to the Tools menu.
$item->menu_name->value = 'tools';
$item->save();
// Create a second node.
$child_node = $this->drupalCreateNode(['type' => 'article']);
// Assign a menu link to the second node, being a child of the first one.
$child_item = MenuLinkContent::create([
'link' => [['uri' => 'entity:node/' . $child_node->id()]],
'title' => $this->randomMachineName(16),
'parent' => $item->getPluginId(),
'menu_name' => $item->getMenuName(),
]);
$child_item->save();
// Edit the first node.
$this->drupalGet('node/' . $node->id() . '/edit');
// Assert that it is not possible to set the parent of the first node to itself or the second node.
$this->assertSession()->optionNotExists('edit-menu-menu-parent', 'tools:' . $item->getPluginId());
$this->assertSession()->optionNotExists('edit-menu-menu-parent', 'tools:' . $child_item->getPluginId());
// Assert that disallowed Administration menu is not available in options.
$this->assertSession()->optionNotExists('edit-menu-menu-parent', 'admin:');
}
/**
* Testing correct loading and saving of menu links via node form widget in a multilingual environment.
*/
public function testMultilingualMenuNodeFormWidget(): void {
// Setup languages.
$langcodes = ['de'];
foreach ($langcodes as $langcode) {
static::createLanguageFromLangcode($langcode);
}
array_unshift($langcodes, \Drupal::languageManager()->getDefaultLanguage()->getId());
$config = \Drupal::service('config.factory')->getEditable('language.negotiation');
// Ensure path prefix is used to determine the language.
$config->set('url.source', 'path_prefix');
// Ensure that there's a path prefix set for english as well.
$config->set('url.prefixes.' . $langcodes[0], $langcodes[0]);
$config->save();
$languages = [];
foreach ($langcodes as $langcode) {
$languages[$langcode] = ConfigurableLanguage::load($langcode);
}
// Enable translation for pages and menu link content..
$this->enableContentTranslation('node', 'page');
$this->enableContentTranslation('menu_link_content', 'menu_link_content');
$this->rebuildContainer();
// Create a node.
$node_title = $this->randomMachineName(8);
$node = Node::create([
'type' => 'page',
'title' => $node_title,
'body' => $this->randomMachineName(16),
'uid' => $this->editor->id(),
'status' => 1,
'langcode' => $langcodes[0],
]);
$node->save();
// Create translation.
$translated_node_title = $this->randomMachineName(8);
$node->addTranslation($langcodes[1], ['title' => $translated_node_title, 'body' => $this->randomMachineName(16), 'status' => 1]);
$node->save();
// Edit the node and create a menu link.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'menu[weight]' => 17,
];
$options = ['language' => $languages[$langcodes[0]]];
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->submitForm($edit, 'Save (this translation)');
// Edit the node in a different language and translate the menu link.
$edit = [
'menu[enabled]' => 1,
'menu[title]' => $translated_node_title,
'menu[weight]' => 17,
];
$options = ['language' => $languages[$langcodes[1]]];
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->submitForm($edit, 'Save (this translation)');
// Assert that the original link exists in the frontend.
$this->drupalGet('node/' . $node->id(), ['language' => $languages[$langcodes[0]]]);
$this->assertSession()->linkExists($node_title);
// Assert that the translated link exists in the frontend.
$this->drupalGet('node/' . $node->id(), ['language' => $languages[$langcodes[1]]]);
$this->assertSession()->linkExists($translated_node_title);
// Revisit the edit page in original language, check the loaded menu item title and save.
$options = ['language' => $languages[$langcodes[0]]];
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertSession()->fieldValueEquals('edit-menu-title', $node_title);
$this->submitForm([], 'Save (this translation)');
// Revisit the edit page of the translation and check the loaded menu item title.
$options = ['language' => $languages[$langcodes[1]]];
$url = $node->toUrl('edit-form', $options);
$this->drupalGet($url);
$this->assertSession()->fieldValueEquals('edit-menu-title', $translated_node_title);
}
/**
* Tests creating menu links via node form widget for nodes with grants.
*/
public function testMenuNodeWithGrantsFormWidget(): void {
\Drupal::service('module_installer')->install(['node_access_test']);
node_access_rebuild();
$this->assertTrue(\Drupal::moduleHandler()->hasImplementations('node_grants'));
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer nodes',
'administer menu',
'create page content',
'edit any page content',
]);
$this->drupalLogin($admin_user);
$node_title = $this->randomMachineName();
$edit = [
'title[0][value]' => $node_title,
'menu[enabled]' => 1,
'menu[title]' => $node_title,
'status[value]' => 0,
];
$this->drupalGet('node/add/page');
$this->submitForm($edit, 'Save');
$node = $this->drupalGetNodeByTitle($node_title);
$this->assertTrue($node->access('view', $admin_user));
$this->drupalGet('node/add/page');
$link_id = menu_ui_get_menu_link_defaults($node)['entity_id'];
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $link */
$link = MenuLinkContent::load($link_id);
$this->assertSession()->optionExists('edit-menu-menu-parent', 'main:' . $link->getPluginId());
// Assert that the unpublished node cannot be selected as a parent menu link
// for users without access to the node.
$admin_user_without_content_access = $this->drupalCreateUser([
'access administration pages',
'administer content types',
'administer nodes',
'administer menu',
'create page content',
'edit any page content',
]);
$this->drupalLogin($admin_user_without_content_access);
$this->assertFalse($node->access('view', $admin_user_without_content_access));
$this->drupalGet('node/add/page');
$this->assertSession()->optionNotExists('edit-menu-menu-parent', 'main:' . $link->getPluginId());
}
/**
* Tests main menu links are prioritized when editing nodes.
*
* @see menu_ui_get_menu_link_defaults()
*/
public function testMainMenuIsPrioritized(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer menu',
'edit any page content',
]));
$menu_name = $this->randomMachineName();
$mainLinkTitle = $this->randomMachineName();
$nonMainLinkTitle = $this->randomMachineName();
Menu::create(['id' => $menu_name, 'label' => $menu_name])->save();
$nodeType = NodeType::load('page');
$nodeType->setThirdPartySetting('menu_ui', 'available_menus', [$menu_name, 'main'])->save();
$node = Node::create([
'type' => 'page',
'title' => $this->randomMachineName(),
'uid' => $this->rootUser->id(),
'status' => 1,
]);
$node->save();
MenuLinkContent::create([
'link' => [['uri' => 'entity:node/' . $node->id()]],
'title' => $nonMainLinkTitle,
'menu_name' => $menu_name,
])->save();
MenuLinkContent::create([
'link' => [['uri' => 'entity:node/' . $node->id()]],
'title' => $mainLinkTitle,
'menu_name' => 'main',
])->save();
$this->drupalGet('node/' . $node->id() . '/edit');
$element = $this->assertSession()->elementExists('css', 'input[name="menu[title]"]');
$this->assertEquals($mainLinkTitle, $element->getValue());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\system\Entity\Menu;
/**
* Tests that uninstalling menu does not remove custom menus.
*
* @group menu_ui
*/
class MenuUninstallTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['menu_ui'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests Menu uninstall.
*/
public function testMenuUninstall(): void {
\Drupal::service('module_installer')->uninstall(['menu_ui']);
\Drupal::entityTypeManager()->getStorage('menu')->resetCache(['admin']);
$this->assertNotEmpty(Menu::load('admin'), 'The \'admin\' menu still exists after uninstalling Menu UI module.');
}
}

View File

@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\system\Entity\Menu;
use Drupal\system\MenuStorage;
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
use Drupal\Tests\menu_ui\Traits\MenuUiTrait;
/**
* Tests custom menu and menu links operations using the UI.
*
* @group menu_ui
*/
class MenuUiJavascriptTest extends WebDriverTestBase {
use ContextualLinkClickTrait;
use MenuUiTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'contextual',
'menu_link_content',
'menu_ui',
'test_page_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests the contextual links on a menu block.
*/
public function testBlockContextualLinks(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer menu',
'access contextual links',
'administer blocks',
]));
$menu = $this->addCustomMenu();
$block = $this->drupalPlaceBlock('system_menu_block:' . $menu->id(), [
'label' => 'Custom menu',
'provider' => 'system',
]);
$this->addMenuLink('', '/', $menu->id());
$this->drupalGet('test-page');
// Click on 'Configure block' contextual link.
$this->clickContextualLink("#block-{$block->id()}", 'Configure block');
// Check that we're on block configuration form.
$this->assertNotEmpty($this->getSession()->getPage()->findLink('Remove block'));
$this->drupalGet('test-page');
// Click on 'Edit menu' contextual link.
$this->clickContextualLink("#block-{$block->id()}", 'Edit menu');
// Check that we're on block configuration form.
$this->assertSession()->pageTextContains("Machine name: {$menu->id()}");
}
/**
* Creates a custom menu.
*
* @return \Drupal\system\Entity\Menu
* The custom menu that has been created.
*/
protected function addCustomMenu() {
// Try adding a menu using a menu_name that is too long.
$label = $this->randomMachineName(16);
$menu_id = $this->randomMachineName(MenuStorage::MAX_ID_LENGTH + 1);
$this->drupalGet('admin/structure/menu/add');
$page = $this->getSession()->getPage();
// Type the label to activate the machine name field and the edit button.
$page->fillField('Title', $label);
// Wait for the machine name widget to be activated.
$this->assertSession()->waitForElementVisible('css', 'button[type=button].link:contains(Edit)');
// Activate the machine name text field.
$page->pressButton('Edit');
// Try to fill a text longer than the allowed limit.
$page->fillField('Menu name', $menu_id);
$page->pressButton('Save');
// Check that the menu was saved with the ID truncated to the max length.
$menu = Menu::load(substr($menu_id, 0, MenuStorage::MAX_ID_LENGTH));
$this->assertEquals($label, $menu->label());
// Check that the menu was added.
$this->drupalGet('admin/structure/menu');
$this->assertSession()->pageTextContains($label);
// Confirm that the custom menu block is available.
$this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
$this->clickLink('Place block');
// Wait for the modal dialog to be loaded.
$this->assertSession()->waitForElement('css', "div[aria-describedby=drupal-modal]");
// Check that the block is available to be placed.
$this->assertSession()->pageTextContains($label);
return $menu;
}
/**
* Adds a menu link using the UI.
*
* @param string $parent
* Optional parent menu link id.
* @param string $path
* The path to enter on the form. Defaults to the front page.
* @param string $menu_id
* Menu ID. Defaults to 'tools'.
* @param bool $expanded
* Whether or not this menu link is expanded. Setting this to TRUE should
* test whether it works when we do the authenticatedUser tests. Defaults
* to FALSE.
* @param string $weight
* Menu weight. Defaults to 0.
*
* @return \Drupal\menu_link_content\Entity\MenuLinkContent
* A menu link entity.
*/
protected function addMenuLink($parent = '', $path = '/', $menu_id = 'tools', $expanded = FALSE, $weight = '0') {
// View add menu link page.
$this->drupalGet("admin/structure/menu/manage/$menu_id/add");
$title = '!link_' . $this->randomMachineName(16);
$edit = [
'link[0][uri]' => $path,
'title[0][value]' => $title,
'description[0][value]' => '',
'enabled[value]' => 1,
'expanded[value]' => $expanded,
'menu_parent' => $menu_id . ':' . $parent,
'weight[0][value]' => $weight,
];
// Add menu link.
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('The menu link has been saved.');
$storage = $this->container->get('entity_type.manager')->getStorage('menu_link_content');
$menu_links = $storage->loadByProperties(['title' => $title]);
$menu_link = reset($menu_links);
// Check that the stored menu link meeting the expectations.
$this->assertNotNull($menu_link);
$this->assertMenuLink([
'menu_name' => $menu_id,
'children' => [],
'parent' => $parent,
], $menu_link->getPluginId());
return $menu_link;
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\Entity\Menu;
use Drupal\block\Entity\Block;
use Drupal\system\MenuInterface;
use Drupal\Tests\user\Traits\UserCreationTrait;
/**
* Tests SystemMenuBlock.
*
* @group menu_ui
*/
class MenuBlockTest extends KernelTestBase {
use UserCreationTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'system',
'block',
'menu_ui',
'user',
];
/**
* The menu for testing.
*
* @var \Drupal\system\MenuInterface
*/
protected MenuInterface $menu;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->setUpCurrentUser([], ['administer menu']);
// Add a new custom menu.
$menu_name = 'mock';
$label = $this->randomMachineName(16);
$this->menu = Menu::create([
'id' => $menu_name,
'label' => $label,
'description' => 'Description text',
]);
$this->menu->save();
}
/**
* Tests the editing links for SystemMenuBlock.
*/
public function testOperationLinks(): void {
$block = Block::create([
'plugin' => 'system_menu_block:' . $this->menu->id(),
'region' => 'footer',
'id' => 'machine_name',
'theme' => 'stark',
]);
// Test when user does have "administer menu" permission.
$this->assertEquals([
'menu-edit' => [
'title' => 'Edit menu',
'url' => $this->menu->toUrl('edit-form'),
'weight' => 50,
],
], menu_ui_entity_operation($block));
$this->setUpCurrentUser();
// Test when user doesn't have "administer menu" permission.
$this->assertEmpty(menu_ui_entity_operation($block));
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\system\Entity\Menu;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests menu settings when creating and editing content types.
*
* @group menu_ui
*/
class MenuUiNodeTypeTest extends KernelTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'menu_ui',
'node',
'system',
'text',
'user',
];
/**
* Asserts that the available menu names are sorted alphabetically by label.
*
* @param \Drupal\node\Entity\NodeType $node_type
* The node type under test.
*/
private function assertMenuNamesAreSorted(NodeType $node_type): void {
// The available menus should be sorted by label, not machine name.
$expected_options = [
'b' => 'X',
'c' => 'Y',
'a' => 'Z',
];
$form = $this->container->get('entity.form_builder')
->getForm($node_type, $node_type->isNew() ? 'add' : 'edit');
$this->assertSame($expected_options, $form['menu']['menu_options']['#options']);
}
/**
* Tests node type-specific settings for Menu UI.
*/
public function testContentTypeMenuSettings(): void {
$this->installConfig(['node']);
Menu::create(['id' => 'a', 'label' => 'Z'])->save();
Menu::create(['id' => 'b', 'label' => 'X'])->save();
Menu::create(['id' => 'c', 'label' => 'Y'])->save();
$this->assertMenuNamesAreSorted(NodeType::create());
$this->assertMenuNamesAreSorted($this->createContentType());
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Kernel\Migrate;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of menu_ui settings.
*
* @group menu_ui
*/
class MigrateMenuSettingsTest extends MigrateDrupal7TestBase {
protected static $modules = ['menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['menu_ui']);
$this->executeMigration('menu_settings');
}
public function testMigration(): void {
$this->assertTrue(\Drupal::config('menu_ui.settings')->get('override_parent_selector'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\menu_ui\Traits;
/**
* Provides common methods for Menu UI module tests.
*/
trait MenuUiTrait {
/**
* Asserts that a menu fetched from the database matches an expected one.
*
* @param array $expected_item
* Array containing properties to check.
* @param int $menu_plugin_id
* Menu item id.
*/
protected function assertMenuLink(array $expected_item, $menu_plugin_id) {
// Retrieve the menu link.
/** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */
$menu_link_manager = \Drupal::service('plugin.manager.menu.link');
$menu_link_manager->resetDefinitions();
// Reset the static load cache.
\Drupal::entityTypeManager()->getStorage('menu_link_content')->resetCache();
$definition = $menu_link_manager->getDefinition($menu_plugin_id);
$entity = NULL;
// Pull the path from the menu link content.
if (str_starts_with($menu_plugin_id, 'menu_link_content')) {
[, $uuid] = explode(':', $menu_plugin_id, 2);
/** @var \Drupal\menu_link_content\Entity\MenuLinkContent $entity */
$entity = \Drupal::service('entity.repository')
->loadEntityByUuid('menu_link_content', $uuid);
}
if (isset($expected_item['children'])) {
$child_ids = array_values($menu_link_manager->getChildIds($menu_plugin_id));
sort($expected_item['children']);
if ($child_ids) {
sort($child_ids);
}
$this->assertSame($expected_item['children'], $child_ids);
unset($expected_item['children']);
}
if (isset($expected_item['parents'])) {
$parent_ids = array_values($menu_link_manager->getParentIds($menu_plugin_id));
$this->assertSame($expected_item['parents'], $parent_ids);
unset($expected_item['parents']);
}
if (isset($expected_item['langcode']) && $entity) {
$this->assertEquals($expected_item['langcode'], $entity->langcode->value);
unset($expected_item['langcode']);
}
if (isset($expected_item['enabled']) && $entity) {
$this->assertEquals($expected_item['enabled'], $entity->enabled->value);
unset($expected_item['enabled']);
}
foreach ($expected_item as $key => $value) {
$this->assertNotNull($definition[$key]);
$this->assertSame($value, $definition[$key]);
}
}
}