clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

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