diff --git a/core/composer.json b/core/composer.json index 503704c..3c2cddf 100644 --- a/core/composer.json +++ b/core/composer.json @@ -112,6 +112,7 @@ "drupal/locale": "self.version", "drupal/minimal": "self.version", "drupal/menu_link_content": "self.version", + "drupal/menu_link": "self.version", "drupal/menu_ui": "self.version", "drupal/migrate": "self.version", "drupal/migrate_drupal": "self.version", diff --git a/core/modules/menu_link/config/schema/menu_link.schema.yml b/core/modules/menu_link/config/schema/menu_link.schema.yml new file mode 100644 index 0000000..54b7a45 --- /dev/null +++ b/core/modules/menu_link/config/schema/menu_link.schema.yml @@ -0,0 +1,26 @@ +field.field_settings.menu_link: + type: mapping + label: 'Menu link settings' + mapping: + available_menus: + type: sequence + label: 'Available menus' + sequence: + - type: string + default_menu_parent: + type: string + label: 'Default menu parent' + +field.value.menu_link: + type: mapping + label: 'Default value' + mapping: + menu_name: + type: string + label: 'Value' + parent: + type: string + label: 'Text format' + weight: + type: integer + label: 'Weight' diff --git a/core/modules/menu_ui/menu_ui.js b/core/modules/menu_link/js/menu_link.js similarity index 96% rename from core/modules/menu_ui/menu_ui.js rename to core/modules/menu_link/js/menu_link.js index 5eb10e0..f26b11d 100644 --- a/core/modules/menu_ui/menu_ui.js +++ b/core/modules/menu_link/js/menu_link.js @@ -15,7 +15,7 @@ * @prop {Drupal~behaviorAttach} attach * Find the form and call `drupalSetSummary` on it. */ - Drupal.behaviors.menuUiDetailsSummaries = { + Drupal.behaviors.menuLinkDetailsSummaries = { attach: function (context) { $(context).find('.menu-link-form').drupalSetSummary(function (context) { var $context = $(context); @@ -38,7 +38,7 @@ * Attaches change and keyup behavior for automatically filling out menu * link titles. */ - Drupal.behaviors.menuUiLinkAutomaticTitle = { + Drupal.behaviors.menuLinkLinkAutomaticTitle = { attach: function (context) { var $context = $(context); $context.find('.menu-link-form').each(function () { diff --git a/core/modules/menu_link/menu_link.info.yml b/core/modules/menu_link/menu_link.info.yml new file mode 100644 index 0000000..2c90e90 --- /dev/null +++ b/core/modules/menu_link/menu_link.info.yml @@ -0,0 +1,8 @@ +name: Menu link +type: module +description: 'Defines a menu link field type' +package: Field types +version: VERSION +core: 8.x +dependencies: + - field diff --git a/core/modules/menu_link/menu_link.libraries.yml b/core/modules/menu_link/menu_link.libraries.yml new file mode 100644 index 0000000..6610a24 --- /dev/null +++ b/core/modules/menu_link/menu_link.libraries.yml @@ -0,0 +1,8 @@ +menu_link.form: + version: VERSION + js: + js/menu_link.js: {} + dependencies: + - core/jquery + - core/drupal + - core/drupal.form diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module new file mode 100644 index 0000000..55e8bec --- /dev/null +++ b/core/modules/menu_link/menu_link.module @@ -0,0 +1,21 @@ +getType() == 'menu_link') { + // We only support posting one menu link at a time. + // @todo - this is a hack and should be managed by the field definition. + // https://www.drupal.org/node/2403703 + $form['field_storage']['cardinality_container']['cardinality']['#default_value'] = 1; + $form['field_storage']['cardinality_container']['#access'] = FALSE; + } +} diff --git a/core/modules/menu_link/src/Plugin/Field/FieldType/MenuLinkItem.php b/core/modules/menu_link/src/Plugin/Field/FieldType/MenuLinkItem.php new file mode 100644 index 0000000..bb6aa9b --- /dev/null +++ b/core/modules/menu_link/src/Plugin/Field/FieldType/MenuLinkItem.php @@ -0,0 +1,283 @@ +menuPluginManager = \Drupal::service('plugin.manager.menu.link'); + $this->menuParentFormSelector = \Drupal::service('menu.parent_form_selector'); + } + + /** + * {@inheritdoc} + */ + public static function defaultFieldSettings() { + $settings = parent::defaultFieldSettings(); + + $settings['available_menus'] = ['main']; + $settings['default_menu_parent'] = 'main:'; + return $settings; + } + + /** + * {@inheritdoc} + */ + public function fieldSettingsForm(array $form, FormStateInterface $form_state) { + $form = parent::fieldSettingsForm($form, $form_state); + + $menu_options = $this->getMenuNames(); + + $form['available_menus'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Available menus'), + '#default_value' => $this->getSetting('available_menus'), + '#options' => $menu_options, + '#description' => $this->t('The menus available to place links in for this kind of entity.'), + '#required' => TRUE, + ]; + + $parent_options = []; + // Make sure the setting is normalized to an associative array. + $available_menus = array_filter($this->getSetting('available_menus')); + $available_menus = array_combine($available_menus, $available_menus); + foreach ($available_menus as $name) { + if (isset($menu_options[$name])) { + $parent_options["$name:"] = $menu_options[$name]; + } + } + $form['default_menu_parent'] = [ + '#type' => 'select', + '#title' => $this->t('Default menu for new links'), + '#default_value' => $this->getSetting('default_menu_parent'), + '#options' => $parent_options, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { + $definitions = []; + + $definitions['menu_name'] = DataDefinition::create('string') + ->setLabel(t('Menu')); + $definitions['title'] = DataDefinition::create('string') + ->setLabel(t('Menu link title')); + $definitions['description'] = DataDefinition::create('string') + ->setLabel(t('Menu link description')); + $definitions['parent'] = DataDefinition::create('string') + ->setLabel(t('Menu link parent')); + $definitions['weight'] = DataDefinition::create('integer') + ->setLabel(t('Menu link weight')); + + return $definitions; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = []; + + $schema['columns']['menu_name'] = [ + 'description' => 'The menu of the link', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ]; + + $schema['columns']['title'] = [ + 'description' => 'The menu link text.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ]; + + $schema['columns']['description'] = [ + 'description' => 'The description of the menu link.', + 'type' => 'blob', + 'size' => 'big', + 'not null' => FALSE, + ]; + + $schema['columns']['parent'] = [ + 'description' => 'The parent of the menu link', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ]; + + $schema['columns']['weight'] = [ + 'description' => 'The weight of the menu link', + 'type' => 'int', + ]; + + return $schema; + } + + /** + * {@inheritdoc} + */ + public function insert() { + parent::insert(); + + $this->doSave(); + } + + /** + * {@inheritdoc} + */ + public function update() { + parent::update(); + + $this->doSave(); + } + + /** + * {@inheritdoc} + */ + public function delete() { + parent::delete(); + + $plugin_id = $this->getMenuPluginId(); + if ($this->menuPluginManager->hasDefinition($plugin_id)) { + $this->menuPluginManager->removeDefinition($plugin_id, FALSE); + } + } + + /** + * Saves the plugin definition. + */ + protected function doSave() { + // We only update the menu link definition when working with the original + // language value of the field, otherwise, we can never properly update or + // remove the menu link. + // @todo - use the FieldTranslationSynchronizer + // https://www.drupal.org/node/2403455 + if ($this->getLangcode() != $this->getEntity()->getUntranslated()->language()->getId()) { + return; + } + $plugin_id = $this->getMenuPluginId(); + + // When the entity is saved via a plugin instance, we should not call the + // menu tree manager to update the definition a second time. + if ($menu_plugin_definition = $this->getMenuPluginDefinition()) { + if (!$this->menuPluginManager->hasDefinition($plugin_id)) { + $this->menuPluginManager->addDefinition($plugin_id, $this->getMenuPluginDefinition()); + } + else { + $this->menuPluginManager->updateDefinition($plugin_id, $this->getMenuPluginDefinition(), FALSE); + } + } + else { + $this->menuPluginManager->removeDefinition($plugin_id, FALSE); + } + } + + /** + * Generates the plugin ID for the associated menu link. + * + * @return string + * The Plugin ID. + */ + public function getMenuPluginId() { + $field_name = $this->definition->getFieldDefinition()->getName(); + $entity_type_id = $this->getEntity()->getEntityTypeId(); + return 'menu_link_field:' . "{$entity_type_id}_{$field_name}_{$this->getEntity()->uuid()}"; + } + + /** + * Generates the plugin definition of the associated menu link. + * + * @return array|FALSE + * The menu plugin definition, otherwise FALSE. + */ + protected function getMenuPluginDefinition() { + $menu_definition = []; + + // If there is no menu name selected, you don't have a valid menu plugin. + if (!$this->values['menu_name']) { + return FALSE; + } + + $entity = $this->getEntity(); + $menu_definition['id'] = $this->getPluginId(); + $menu_definition['title'] = $this->values['title'] ?: $entity->label(); + $menu_definition['title_max_length'] = $this->getFieldDefinition()->getSetting('max_length'); + $menu_definition['description'] = isset($this->values['description']) ? $this->values['description'] : ''; + $menu_definition['menu_name'] = $this->values['menu_name']; + $menu_definition['parent'] = $this->values['parent']; + $menu_definition['weight'] = isset($this->values['weight']) ? $this->values['weight'] : 0; + $menu_definition['class'] = '\Drupal\menu_link\Plugin\Menu\MenuLinkField'; + $menu_definition['form_class'] = '\Drupal\menu_link\Plugin\Menu\Form\MenuLinkFieldForm'; + $menu_definition['metadata']['entity_id'] = $entity->id(); + $menu_definition['metadata']['entity_type_id'] = $entity->getEntityTypeId(); + $menu_definition['metadata']['field_name'] = $this->definition->getFieldDefinition()->getName(); + $menu_definition['metadata']['translatable'] = $entity->getEntityType()->isTranslatable(); + + $url = $entity->urlInfo(); + $menu_definition['route_name'] = $url->getRouteName(); + $menu_definition['route_parameters'] = $url->getRouteParameters(); + + return $menu_definition; + } + + /** + * Returns available menu names. + * + * @return string[] + * Returns menu labels, keyed by menu ID. + */ + protected function getMenuNames() { + if ($custom_menus = Menu::loadMultiple()) { + foreach ($custom_menus as $menu_name => $menu) { + $custom_menus[$menu_name] = $menu->label(); + } + asort($custom_menus); + } + + return $custom_menus; + } + +} diff --git a/core/modules/menu_link/src/Plugin/Field/FieldWidget/MenuLinkWidget.php b/core/modules/menu_link/src/Plugin/Field/FieldWidget/MenuLinkWidget.php new file mode 100644 index 0000000..f36b966 --- /dev/null +++ b/core/modules/menu_link/src/Plugin/Field/FieldWidget/MenuLinkWidget.php @@ -0,0 +1,219 @@ +menuParentSelector = $menu_parent_selector; + $this->account = $account; + $this->menuLinkManager = $menu_link_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('menu.parent_form_selector'), + $container->get('current_user'), + $container->get('plugin.manager.menu.link') + ); + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $default_weight = isset($items[$delta]->weight) ? $items[$delta]->weight : 0; + + $available_menus = array_filter($items->getSetting('available_menus')); + $available_menus = array_combine($available_menus, $available_menus); + $menu_names = array_keys($available_menus); + + if (empty($items[$delta]->menu_name)) { + $default_menu_parent = $items->getSetting('default_menu_parent'); + if (empty($available_menus[rtrim($default_menu_parent, ':')])) { + $default_menu_parent = reset($menu_names) . ':'; + } + } + else { + $menu = $items[$delta]->menu_name; + $parent = !empty($items[$delta]->parent) ? $items[$delta]->parent : ''; + $default_menu_parent = "$menu:$parent"; + } + $default_title = isset($items[$delta]->title) ? $items[$delta]->title : NULL; + $default_description = isset($items[$delta]->description) ? $items[$delta]->description : NULL; + // The widget form may be used to define default values, so make sure the + // form object is an entity form, rather than a configuration form. + $form_object = $form_state->getFormObject(); + $in_translation_form = is_subclass_of($form_object, '\Drupal\Core\Entity\ContentEntityFormInterface') && !$form_object->isDefaultFormLangcode($form_state); + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $items->getParent()->getValue(); + $element['#pre_render'][] = [get_class($this), 'preRenderMenuDetails']; + + $element['#attached']['library'][] = 'menu_link/menu_link.form'; + + $element['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Menu link title'), + '#default_value' => $default_title, + '#attributes' => ['class' => ['menu-link-title']], + ]; + + $element['description'] = [ + '#type' => 'textarea', + '#title' => $this->t('Description'), + '#default_value' => $default_description, + '#rows' => 1, + '#description' => $this->t('Shown when hovering over the menu link.'), + ]; + + $plugin_id = $items[$delta]->getMenuPluginId(); + $has_plugin = $plugin_id && $this->menuLinkManager->hasDefinition($plugin_id); + $element['enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Provide a menu link'), + '#default_value' => (int) (bool) $has_plugin, + '#attributes' => ['class' => ['menu-link-enabled']], + '#multilingual' => FALSE, + '#disabled' => $in_translation_form, + ]; + $element['menu'] = [ + '#type' => 'details', + '#title' => $this->t('Menu settings'), + '#open' => (bool) $has_plugin, + '#tree' => FALSE, + '#weight' => $entity->getEntityTypeId() == 'node' ? -2 : 0, + '#group' => $entity->getEntityTypeId() == 'node' ? 'advanced' : NULL, + '#attributes' => ['class' => ['menu-link-form']], + '#attached' => [ + 'library' => ['menu_ui/drupal.menu_ui'], + ], + ]; + + $plugin_id = $items[$delta]->getMenuPluginId(); + $parent_element = $this->menuParentSelector->parentSelectElement($default_menu_parent, $plugin_id, $available_menus); + + $element['menu_parent'] = $parent_element; + $element['menu_parent']['#title'] = $this->t('Parent item'); + $element['menu_parent']['#attributes']['class'][] = 'menu-parent-select'; + $element['menu_parent']['#multilingual'] = FALSE; + $element['menu_parent']['#disabled'] = $in_translation_form; + + $element['weight'] = [ + '#type' => 'number', + '#title' => $this->t('Weight'), + '#default_value' => $default_weight, + '#description' => $this->t('Menu links with lower weights are displayed before links with higher weights.'), + ]; + + return $element; + } + + /** + * Pre-render callback: Builds a renderable array for the menu link widget. + * + * @param array $element + * A renderable array. + * + * @return array + * A renderable array. + */ + public static function preRenderMenuDetails($element) { + $element['menu']['enabled'] = $element['enabled']; + $element['menu']['title'] = $element['title']; + $element['menu']['description'] = $element['description']; + $element['menu']['menu_parent'] = $element['menu_parent']; + $element['menu']['weight'] = $element['weight']; + + // Hide the elements when enabled is disabled. + foreach (['title', 'description', 'menu_parent', 'weight'] as $form_element) { + $element['menu'][$form_element]['#states'] = [ + 'invisible' => [ + 'input[name="' . $element['menu']['enabled']['#name'] . '"]' => ['checked' => FALSE], + ], + ]; + } + + unset($element['enabled'], $element['title'], $element['description'], $element['menu_parent'], $element['weight']); + + return $element; + } + + /** + * {@inheritdoc} + */ + public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { + parent::extractFormValues($items, $form, $form_state); + + // Extract menu and parent menu link from single select element. + foreach ($items as $delta => $item) { + if (!empty($item->enabled) && !empty($item->menu_parent) && $item->menu_parent != ':') { + list($item->menu_name, $item->parent) = explode(':', $item->menu_parent, 2); + } + else { + $item->title = ''; + $item->description = ''; + $item->menu_name = ''; + $item->parent = ''; + } + unset($item->enabled); + unset($item->menu_parent); + } + } + +} diff --git a/core/modules/menu_link/src/Plugin/Field/MenuLinkItemList.php b/core/modules/menu_link/src/Plugin/Field/MenuLinkItemList.php new file mode 100644 index 0000000..ee68c6d --- /dev/null +++ b/core/modules/menu_link/src/Plugin/Field/MenuLinkItemList.php @@ -0,0 +1,32 @@ +menuLink->getEntity(); + + $form['info'] = [ + '#type' => 'item', + '#title' => $this->t('This link is provided by the %type: @label. The path cannot be edited.', [ + '%type' => $entity->getEntityType()->getLabel(), + '@url' => $entity->url(), + '@label' => $entity->label(), + ]), + ]; + + return $form; + } + +} diff --git a/core/modules/menu_link/src/Plugin/Menu/MenuLinkField.php b/core/modules/menu_link/src/Plugin/Menu/MenuLinkField.php new file mode 100644 index 0000000..5a84504 --- /dev/null +++ b/core/modules/menu_link/src/Plugin/Menu/MenuLinkField.php @@ -0,0 +1,202 @@ +entityTypeManager = $entity_manager; + $this->entityRepository = $entity_repository; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity.repository'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + return $this->getProperty('title'); + } + + /** + * Gets a specific property. + * + * In case the underlying entity is translatable, we watch the translated + * value. + * + * @param string $property + * Gets a specific property from the field, like title or weight. + * + * @return mixed + * The Property. + */ + protected function getProperty($property) { + // We only need to get the property from the actual entity if it may be a + // translation based on the current language context. This can only happen + // if the site is configured to be multilingual. + if (!empty($this->pluginDefinition['metadata']['translatable']) && $this->languageManager->isMultilingual()) { + /** @var \Drupal\Core\TypedData\TranslatableInterface|\Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->entityRepository->getTranslationFromContext($this->getEntity()); + $field_name = $this->getFieldName(); + if ($property_value = $entity->$field_name->$property) { + return $property_value; + } + return $entity->label(); + } + return $this->pluginDefinition[$property]; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->getProperty('description'); + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->getProperty('weight'); + } + + /** + * {@inheritdoc} + */ + public function updateLink(array $new_definition_values, $persist) { + $field_name = $this->getFieldName(); + + $this->pluginDefinition = $new_definition_values + $this->getPluginDefinition(); + if ($persist) { + $updated = []; + foreach ($new_definition_values as $key => $value) { + $field = $this->getEntity()->{$field_name}; + if (isset($field->{$key})) { + $field->{$key} = $value; + $updated[] = $key; + } + } + if ($updated) { + $this->getEntity()->save(); + } + } + + return $this->pluginDefinition; + } + + /** + * Loads the entity the field was attached to. + * + * @return \Drupal\Core\Entity\EntityInterface + * Returns the entity, if exists. + */ + public function getEntity() { + if (empty($this->entity)) { + $entity_type_id = $this->pluginDefinition['metadata']['entity_type_id']; + $entity_id = $this->pluginDefinition['metadata']['entity_id']; + $this->entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function isDeletable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function deleteLink() { + $entity = $this->getEntity(); + $field_name = $this->getFieldName(); + + $entity->$field_name->title = ''; + $entity->$field_name->description = ''; + $entity->$field_name->menu_name = ''; + $entity->$field_name->parent = ''; + $this->entity->save(); + } + + /** + * Returns the field name. + * + * @return string + * The Field name. + */ + protected function getFieldName() { + return $this->getPluginDefinition()['metadata']['field_name']; + } + +} diff --git a/core/modules/menu_link/src/Tests/MenuLinkFieldStandardTest.php b/core/modules/menu_link/src/Tests/MenuLinkFieldStandardTest.php new file mode 100644 index 0000000..f2c0a3d --- /dev/null +++ b/core/modules/menu_link/src/Tests/MenuLinkFieldStandardTest.php @@ -0,0 +1,89 @@ +getPermissions()); + $this->adminUser = $this->drupalCreateUser($perms); + $this->drupalLogin($this->adminUser); + } + + /** + * Tests field CRUD on the node form and field configurations. + */ + public function testLinkEdit() { + // Ensure that the field_menu link got created. + $node = $this->drupalCreateNode(array( + 'type' => 'article', + 'title' => 'Foobar', + 'promote' => 1, + 'status' => 1, + )); + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertNoFieldChecked('edit-field-menu-0-enabled'); + $title = $this->randomString(); + $edit = $this->translatePostValues([ + 'field_menu' => [ + 0 => [ + 'enabled' => TRUE, + 'title' => $title, + ], + ], + ]); + $this->drupalPostForm("node/{$node->id()}/edit", $edit, 'Save and keep published'); + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertFieldChecked('edit-field-menu-0-enabled'); + $this->assertOptionSelected('edit-field-menu-0-menu-parent', 'main:'); + // Enable another menu. + $edit = $this->translatePostValues([ + 'field' => [ + 'settings' => [ + 'available_menus' => [ + 'footer' => TRUE, + 'main' => TRUE, + ], + ], + ], + ]); + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_menu', $edit, 'Save settings'); + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertOptionSelected('edit-field-menu-0-menu-parent', 'main:'); + $edit = $this->translatePostValues([ + 'field_menu' => [ + 0 => [ + 'menu_parent' => 'footer:', + ], + ], + ]); + $this->drupalPostForm("node/{$node->id()}/edit", $edit, 'Save and keep published'); + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertOptionSelected('edit-field-menu-0-menu-parent', 'footer:'); + } + +} diff --git a/core/modules/menu_link/src/Tests/MenuLinkFieldTranslateUITest.php b/core/modules/menu_link/src/Tests/MenuLinkFieldTranslateUITest.php new file mode 100644 index 0000000..e23f067 --- /dev/null +++ b/core/modules/menu_link/src/Tests/MenuLinkFieldTranslateUITest.php @@ -0,0 +1,61 @@ +bundle content"]; + } + + /** + * {@inheritdoc} + */ + protected function getTranslatorPermissions() { + return array_merge(parent::getTranslatorPermissions(), ['administer menu', 'administer nodes', "edit any $this->bundle content"]); + } + + /** + * {@inheritdoc} + */ + protected function getAdministratorPermissions() { + return array_merge(parent::getAdministratorPermissions(), ['administer menu', 'access administration pages', 'administer content types', 'administer node fields', 'access content overview', 'bypass node access', 'administer languages', 'administer themes', 'view the administration theme']); + } + +} diff --git a/core/modules/menu_ui/menu_ui.libraries.yml b/core/modules/menu_ui/menu_ui.libraries.yml index c5f9594..31f34d2 100644 --- a/core/modules/menu_ui/menu_ui.libraries.yml +++ b/core/modules/menu_ui/menu_ui.libraries.yml @@ -1,12 +1,3 @@ -drupal.menu_ui: - version: VERSION - js: - menu_ui.js: {} - dependencies: - - core/jquery - - core/drupal - - core/drupal.form - drupal.menu_ui.admin: version: VERSION js: diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index cffccde..1512063 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -130,344 +130,6 @@ function menu_ui_block_view_system_menu_block_alter(array &$build, BlockPluginIn } /** - * 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()) { - if (!$entity->hasTranslation($node->language()->getId())) { - $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray()); - } - else { - $entity = $entity->getTranslation($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 = isset($values['weight']) ? $values['weight'] : 0; - $entity->save(); -} - -/** - * Implements hook_ENTITY_TYPE_predelete() for node entities. - */ -function menu_ui_node_predelete(EntityInterface $node) { - // Delete all MenuLinkContent links that point to this node. - /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ - $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); - $result = $menu_link_manager->loadLinksByRoute('entity.node.canonical', ['node' => $node->id()]); - - if (!empty($result)) { - foreach ($result as $id => $instance) { - if ($instance->isDeletable() && strpos($id, 'menu_link_content:') === 0) { - $instance->deleteLink(); - } - } - } -} - -/** - * 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') - ->condition('link.uri', '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') - ->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(), - '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::entityManager()->getBaseFieldDefinitions('menu_link_content'); - $max_length = $field_definitions['title']->getSetting('max_length'); - $defaults = [ - 'entity_id' => 0, - 'id' => '', - 'title' => '', - 'title_max_length' => $max_length, - 'description' => '', - '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'); - $menu_names = menu_ui_get_menus(); - $type_menus = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']); - $available_menus = []; - foreach ($type_menus as $menu) { - $available_menus[$menu] = $menu_names[$menu]; - } - 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' => 'textarea', - '#title' => t('Description'), - '#default_value' => $defaults['description'], - '#rows' => 1, - '#description' => t('Shown when hovering over the menu link.'), - ]; - - $form['menu']['link']['menu_parent'] = $parent_element; - $form['menu']['link']['menu_parent']['#title'] = t('Parent item'); - $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 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(); - } - } - elseif (trim($values['title'])) { - // 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'])) { - list($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_submit() - */ -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 = menu_ui_get_menus(); - /** @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('The menus available to place links in for this content type.'), - ]; - // @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 item'), - '#default_value' => $type->getThirdPartySetting('menu_ui', 'parent', 'main:'), - '#options' => $options, - '#description' => t('Choose the menu item 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 item 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')); -} - -/** * Return an associative array of the custom menus names. * * @param bool $all diff --git a/core/modules/menu_ui/menu_ui.routing.yml b/core/modules/menu_ui/menu_ui.routing.yml index 7f94a6d..9e1312f 100644 --- a/core/modules/menu_ui/menu_ui.routing.yml +++ b/core/modules/menu_ui/menu_ui.routing.yml @@ -38,6 +38,19 @@ menu_ui.link_reset: _permission: 'administer menu' _custom_access: '\Drupal\menu_ui\Form\MenuLinkResetForm::linkIsResettable' +menu_ui.link_delete: + path: '/admin/structure/menu/link/{menu_link_plugin}/delete' + defaults: + _form: 'Drupal\menu_ui\Form\MenuLinkDeleteForm' + _title: 'Delete menu link' + options: + parameters: + menu_link_plugin: + type: menu_link_plugin + requirements: + _permission: 'administer menu' + _custom_access: '\Drupal\menu_ui\Form\MenuLinkDeleteForm::linkIsDeletable' + entity.menu.add_form: path: '/admin/structure/menu/add' defaults: diff --git a/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php b/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php new file mode 100644 index 0000000..19e8540 --- /dev/null +++ b/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php @@ -0,0 +1,123 @@ +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_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete the link %item?', array('%item' => $this->link->getTitle())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + if ($this->link) { + return new Url('entity.menu.edit_form', array( + 'menu' => $this->link->getMenuName(), + )); + } + return new Url(''); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('The link will be deleted. This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@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->removeDefinition($this->link->getPluginId()); + drupal_set_message($this->t('The menu link was deleted.')); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + /** + * Checks access based on whether the link can be deleted. + * + * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin + * The menu link plugin being checked. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function linkIsDeletable(MenuLinkInterface $menu_link_plugin) { + return AccessResult::allowedIf($menu_link_plugin->isDeletable())->setCacheable(FALSE); + } + +} diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index 0bee996..042141c 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -399,9 +399,17 @@ protected function buildOverviewTreeForm($tree, $delta) { 'url' => Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $link->getPluginId()]), ]; } - elseif ($delete_link = $link->getDeleteRoute()) { - $operations['delete']['url'] = $delete_link; - $operations['delete']['query'] = $this->getDestinationArray(); + elseif ($link->isDeletable()) { + // Allow for a custom delete link per plugin. + $delete_route = $link->getDeleteRoute(); + if ($delete_route) { + $operations['delete']['url'] = $delete_route; + } + else { + // Fall back to the standard delete link. + $operations['delete']['url'] = Url::fromRoute('menu_ui.link_delete', ['menu_link_plugin' => $link->getPluginId()]); + } + $operations['delete']['query']['destination'] = $this->entity->url(); $operations['delete']['title'] = $this->t('Delete'); } if ($link->isTranslatable()) { diff --git a/core/modules/menu_ui/src/Tests/MenuNodeTest.php b/core/modules/menu_ui/src/Tests/MenuNodeTest.php index 3f1bba0..b92ee51 100644 --- a/core/modules/menu_ui/src/Tests/MenuNodeTest.php +++ b/core/modules/menu_ui/src/Tests/MenuNodeTest.php @@ -2,6 +2,8 @@ namespace Drupal\menu_ui\Tests; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\simpletest\WebTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\menu_link_content\Entity\MenuLinkContent; @@ -26,7 +28,22 @@ class MenuNodeTest extends WebTestBase { * * @var array */ - public static $modules = ['menu_ui', 'test_page_test', 'node', 'block', 'locale', 'language', 'content_translation']; + public static $modules = ['menu_ui', 'test_page_test', 'node', 'block', 'menu_link', 'locale', 'language', 'content_translation']; + + /** + * @var \Drupal\field\Entity\FieldConfig + */ + protected $fieldConfig; + + /** + * @var \Drupal\field\Entity\FieldStorageConfig + */ + protected $fieldStorageConfig; + + /** + * @var \Drupal\field\Entity\FieldStorageConfig + */ + protected $fieldConfigArticle; protected function setUp() { parent::setUp(); @@ -35,6 +52,25 @@ protected function setUp() { $this->drupalPlaceBlock('page_title_block'); $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + $this->fieldStorageConfig = FieldStorageConfig::create(['field_name' => 'menu', 'entity_type' => 'node', 'type' => 'menu_link']); + $this->fieldStorageConfig->save(); + $this->fieldConfig = FieldConfig::create(['entity_type' => 'node', 'bundle' => 'page', 'field_name' => 'menu']); + $this->fieldConfig->save(); + entity_get_form_display('node', 'page', 'default') + ->setComponent('menu', [ + 'type' => 'menu_link_default', + ]) + ->save(); + + $this->fieldConfigArticle = FieldConfig::create(['entity_type' => 'node', 'bundle' => 'article', 'field_name' => 'menu']); + $this->fieldConfigArticle->save(); + entity_get_form_display('node', 'article', 'default') + ->setComponent('menu', [ + 'type' => 'menu_link_default', + ]) + ->save(); $this->editor = $this->drupalCreateUser([ 'access administration pages', @@ -68,15 +104,12 @@ public function testMenuNodeFormWidget() { $this->assertPattern('//', 'Menu link title field has correct maxlength in node add form.'); // Disable the default main menu, so that no menus are enabled. - $edit = [ - 'menu_options[main]' => FALSE, - ]; - $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->fieldConfig->settings['available_menus']['menu'] = FALSE; + $this->fieldConfig->save(); // Verify that no menu settings are displayed and nodes can be created. $this->drupalGet('node/add/page'); $this->assertText(t('Create Basic page')); - $this->assertNoText(t('Menu settings')); $node_title = $this->randomMachineName(); $edit = [ 'title[0][value]' => $node_title, @@ -88,22 +121,15 @@ public function testMenuNodeFormWidget() { // 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->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertText(t('The selected menu item is not under one of the selected menus.')); - $this->assertNoRaw(t('The content type %name has been updated.', ['%name' => 'Basic page'])); + $this->fieldConfig->settings['available_menus']['tools'] = 'tools'; + $this->fieldConfig->settings['parent'] = 'main:'; + $this->fieldConfig->save(); // Enable Tools menu as available menu. - $edit = [ - 'menu_options[main]' => 1, - 'menu_options[tools]' => 1, - 'menu_parent' => 'main:', - ]; - $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); - $this->assertRaw(t('The content type %name has been updated.', ['%name' => 'Basic page'])); + $this->fieldConfig->settings['available_menus']['tools'] = 'tools'; + $this->fieldConfig->settings['available_menus']['main'] = 'main'; + $this->fieldConfig->settings['parent'] = 'main:'; + $this->fieldConfig->save(); // Test that we can preview a node that will create a menu item. $edit = [ @@ -126,13 +152,14 @@ public function testMenuNodeFormWidget() { $this->assertNoLink($node_title); // Edit the node, enable the menu link setting, but skip the link title. + // We still expect to have a link to the entity label of the node. $edit = [ - 'menu[enabled]' => 1, + 'menu[0][enabled]' => 1, ]; $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - // Assert that there is no link for the node. + // Assert that there is a link for the node. $this->drupalGet('test-page'); - $this->assertNoLink($node_title); + $this->assertLink($node->label()); // Use not only the save button, but also the two special buttons: // 'Save and publish' as well as 'Save and keep published'. @@ -166,9 +193,8 @@ public function testMenuNodeFormWidget() { $this->drupalLogin($this->editor); // Edit the node and create a menu link. $edit = [ - 'menu[enabled]' => 1, - 'menu[title]' => $node_title, - 'menu[weight]' => 17, + 'menu[0][enabled]' => 1, + 'menu[0][weight]' => 17, ]; $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Assert that the link exists. @@ -176,9 +202,12 @@ public function testMenuNodeFormWidget() { $this->assertLink($node_title); $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertFieldById('edit-menu-weight', 17, 'Menu weight correct in edit form'); + $this->assertFieldById('edit-menu-0-weight', 17, 'Menu weight correct in edit form'); $this->assertPattern('//', 'Menu link title field has correct maxlength in node edit form.'); + // Ensure that the menu link field is opened up by default. + $this->assertFieldById('edit-menu-0-enabled', 1, 'Provide a menu link checkbox is checked.'); + // 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 */ @@ -191,13 +220,17 @@ public function testMenuNodeFormWidget() { // Edit the node and remove the menu link. $edit = [ - 'menu[enabled]' => FALSE, + 'menu[0][enabled]' => FALSE, ]; $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); // Assert that there is no link for the node. $this->drupalGet('test-page'); $this->assertNoLink($node_title); + // Ensure that the menu link is also no longer stored. + $plugin_id = $node->menu->first()->getMenuPluginId(); + $this->assertFalse(\Drupal::service('plugin.manager.menu.link')->hasDefinition($plugin_id)); + // Add a menu link to the Administration menu. $item = MenuLinkContent::create([ 'link' => [['uri' => 'entity:node/' . $node->id()]], @@ -212,12 +245,13 @@ public function testMenuNodeFormWidget() { $this->assertText('Provide a menu link', 'Link in not allowed menu not shown in node edit form'); // Assert that the link is still in the Administration menu after save. $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - $link = MenuLinkContent::load($item->id()); + $link = \Drupal::service('plugin.manager.menu.link')->getDefinition($node->menu->first()->getMenuPluginId()); $this->assertTrue($link, 'Link in not allowed menu still exists after saving node'); // Move the menu link back to the Tools menu. - $item->menu_name->value = 'tools'; - $item->save(); + $node->menu->menu_name = 'tools'; + $node->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. @@ -231,10 +265,10 @@ public function testMenuNodeFormWidget() { // 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->assertNoOption('edit-menu-menu-parent', 'tools:' . $item->getPluginId()); - $this->assertNoOption('edit-menu-menu-parent', 'tools:' . $child_item->getPluginId()); + $this->assertNoOption('edit-menu-0-menu-parent', 'tools:'. $node->menu->first()->getMenuPluginId()); + $this->assertNoOption('edit-menu-0-menu-parent', 'tools:'. $child_node->menu->first()->getMenuPluginId()); // Assert that unallowed Administration menu is not available in options. - $this->assertNoOption('edit-menu-menu-parent', 'admin:'); + $this->assertNoOption('edit-menu-0-menu-parent', 'admin:'); } /** diff --git a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php index 64c8c15..9fff2be 100644 --- a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php @@ -3,8 +3,9 @@ namespace Drupal\system\Tests\Menu; use Drupal\Core\Url; -use Drupal\node\Entity\NodeType; use Drupal\user\RoleInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; /** * Tests breadcrumbs functionality. @@ -18,7 +19,7 @@ class BreadcrumbTest extends MenuTestBase { * * @var array */ - public static $modules = ['menu_test', 'block']; + public static $modules = ['menu_test', 'block', 'menu_link']; /** * An administrative user. @@ -166,11 +167,19 @@ public function testBreadCrumbs() { // @todo Also test all themes? Manually testing led to the suspicion that // breadcrumbs may differ, possibly due to theme overrides. $menus = ['main', 'tools']; - // Alter node type menu settings. - $node_type = NodeType::load($type); - $node_type->setThirdPartySetting('menu_ui', 'available_menus', $menus); - $node_type->setThirdPartySetting('menu_ui', 'parent', 'tools:'); - $node_type->save(); + // Add a menu field to the node type. + $field_storage_config = FieldStorageConfig::create(['field_name' => 'menu', 'entity_type' => 'node', 'type' => 'menu_link']); + $field_storage_config->save(); + $field_config = FieldConfig::create(['entity_type' => 'node', 'bundle' => $type, 'field_name' => 'menu']); + $field_config->settings['available_menus'] = array_combine($menus, $menus); + $field_config->settings['default_menu_parent'] = 'tools:'; + $field_config->save(); + + entity_get_form_display('node', $type, 'default') + ->setComponent('menu', [ + 'type' => 'menu_link_default', + ]) + ->save(); foreach ($menus as $menu) { // Create a parent node in the current menu. @@ -178,14 +187,11 @@ public function testBreadCrumbs() { $node2 = $this->drupalCreateNode([ 'type' => $type, 'title' => $title, - 'menu' => [ - 'enabled' => 1, - 'title' => 'Parent ' . $title, - 'description' => '', - 'menu_name' => $menu, - 'parent' => '', - ], - ]); + )); + $node2->menu->title = 'Parent ' . $title; + $node2->menu->description = ''; + $node2->menu->menu_name = $menu; + $node2->menu->parent = ''; if ($menu == 'tools') { $parent = $node2; @@ -204,7 +210,7 @@ public function testBreadCrumbs() { $link = reset($menu_links); $edit = [ - 'menu[menu_parent]' => $link->getMenuName() . ':' . $link->getPluginId(), + 'menu[0][menu_parent]' => $link->getMenuName() . ':' . $link->getPluginId(), ]; $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save and keep published')); $expected = [ @@ -212,10 +218,10 @@ public function testBreadCrumbs() { ]; $trail = $home + $expected; $tree = $expected + [ - 'node/' . $parent->id() => $parent->menu['title'], + 'node/' . $parent->id() => $parent->menu->title, ]; $trail += [ - 'node/' . $parent->id() => $parent->menu['title'], + 'node/' . $parent->id() => $parent->menu->title, ]; // Add a taxonomy term/tag to last node, and add a link for that term to the diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml index c94e36e..a68b03c 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml @@ -90,4 +90,9 @@ content: size: 60 placeholder: '' third_party_settings: { } + field_menu: + weight: 32 + settings: { } + third_party_settings: { } + type: menu_link_default hidden: { } diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml index 0b7ffd1..cf0830c 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml @@ -3,10 +3,12 @@ status: true dependencies: config: - field.field.node.page.body + - field.field.node.page.field_menu - node.type.page module: - path - text + - menu_link id: node.page.default targetEntityType: node bundle: page @@ -64,4 +66,9 @@ content: size: 60 placeholder: '' third_party_settings: { } + field_menu: + weight: 32 + settings: { } + third_party_settings: { } + type: menu_link_default hidden: { } diff --git a/core/profiles/standard/config/install/field.field.node.article.field_menu.yml b/core/profiles/standard/config/install/field.field.node.article.field_menu.yml new file mode 100644 index 0000000..178853b --- /dev/null +++ b/core/profiles/standard/config/install/field.field.node.article.field_menu.yml @@ -0,0 +1,24 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_menu + - node.type.article + module: + - menu_link +id: node.article.field_menu +field_name: field_menu +entity_type: node +bundle: article +label: Menu link +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + available_menus: + - main + default_menu_parent: 'main:' +third_party_settings: { } +field_type: menu_link diff --git a/core/profiles/standard/config/install/field.field.node.page.field_menu.yml b/core/profiles/standard/config/install/field.field.node.page.field_menu.yml new file mode 100644 index 0000000..c8445a1 --- /dev/null +++ b/core/profiles/standard/config/install/field.field.node.page.field_menu.yml @@ -0,0 +1,24 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.node.field_menu + - node.type.page + module: + - menu_link +id: node.page.field_menu +field_name: field_menu +entity_type: node +bundle: page +label: Menu link +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: + available_menus: + - main + default_menu_parent: 'main:' +third_party_settings: { } +field_type: menu_link diff --git a/core/profiles/standard/config/install/field.storage.node.field_menu.yml b/core/profiles/standard/config/install/field.storage.node.field_menu.yml new file mode 100644 index 0000000..cb69efc --- /dev/null +++ b/core/profiles/standard/config/install/field.storage.node.field_menu.yml @@ -0,0 +1,17 @@ +langcode: en +status: true +dependencies: + module: + - menu_link + - node +id: node.field_menu +field_name: field_menu +entity_type: node +type: menu_link +settings: { } +module: menu_link +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml index 82af32e..9c77190 100644 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@ -15,6 +15,7 @@ dependencies: - contextual - contact - menu_link_content + - menu_link - datetime - block_content - quickedit diff --git a/core/profiles/standard/tests/src/Functional/StandardTest.php b/core/profiles/standard/tests/src/Functional/StandardTest.php index f449c10..7970b3f 100644 --- a/core/profiles/standard/tests/src/Functional/StandardTest.php +++ b/core/profiles/standard/tests/src/Functional/StandardTest.php @@ -44,6 +44,8 @@ public function testStandard() { 'skip comment approval', 'create article content', 'create page content', + 'edit own page content', + 'administer menu', ]); $this->drupalLogin($this->adminUser); // Configure the block. @@ -100,6 +102,16 @@ public function testStandard() { $this->assertText('Foobar'); $this->assertNoText('Then she picked out two somebodies, Sally and me'); + // Ensure that the field_menu link got created. + $node = $this->drupalCreateNode(array( + 'type' => 'page', + 'title' => 'Foobar', + 'promote' => 1, + 'status' => 1, + )); + $this->drupalGet("node/{$node->id()}/edit"); + $this->assertFieldById('edit-field-menu-0-enabled', FALSE); + // Ensure block body exists. $this->drupalGet('block/add'); $this->assertFieldByName('body[0][value]');