diff --git a/core/modules/menu/lib/Drupal/menu/MenuFormController.php b/core/modules/menu/lib/Drupal/menu/MenuFormController.php index 4eeab27..579f50f 100644 --- a/core/modules/menu/lib/Drupal/menu/MenuFormController.php +++ b/core/modules/menu/lib/Drupal/menu/MenuFormController.php @@ -7,18 +7,50 @@ namespace Drupal\menu; +use Drupal\Core\Entity\EntityControllerInterface; use Drupal\Core\Entity\EntityFormController; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Language\Language; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Base form controller for menu edit forms. */ -class MenuFormController extends EntityFormController { +class MenuFormController extends EntityFormController implements EntityControllerInterface { + + /** + * The module handler to invoke hooks on. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info, $operation = NULL) { + return new static( + $operation, + $container->get('module_handler') + ); + } + + /** + * Constructs a new EntityListController object. + * + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke hooks on. + */ + public function __construct($operation, ModuleHandlerInterface $module_handler) { + parent::__construct($operation); + + $this->moduleHandler = $module_handler; + } /** * Overrides Drupal\Core\Entity\EntityFormController::form(). */ public function form(array $form, array &$form_state) { - $form = parent::form($form, $form_state); $menu = $this->entity; $system_menus = menu_list_system_menus(); $form_state['menu'] = &$menu; @@ -35,6 +67,7 @@ public function form(array $form, array &$form_state) { '#default_value' => $menu->id(), '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI, '#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), + '#field_prefix' => $menu->isNew() ? 'menu-' : '', '#machine_name' => array( 'exists' => 'menu_edit_menu_name_exists', 'source' => array('label'), @@ -51,6 +84,29 @@ public function form(array $form, array &$form_state) { '#default_value' => $menu->description, ); + $form['langcode'] = array( + '#type' => 'language_select', + '#title' => t('Menu language'), + '#languages' => Language::STATE_ALL, + '#default_value' => $menu->langcode, + ); + // Unlike the menu langcode, the default language configuration for menu + // links only works with language module installed. + if ($this->moduleHandler->moduleExists('language')) { + $form['default_menu_links_language'] = array( + '#type' => 'details', + '#title' => t('Menu links language'), + ); + $form['default_menu_links_language']['default_language'] = array( + '#type' => 'language_configuration', + '#entity_information' => array( + 'entity_type' => 'menu_link', + 'bundle' => $menu->id(), + ), + '#default_value' => language_get_default_configuration('menu_link', $menu->id()), + ); + } + // Add menu links administration form for existing menus. if (!$menu->isNew() || isset($system_menus[$menu->id()])) { // Form API supports constructing and validating self-contained sections @@ -63,7 +119,7 @@ public function form(array $form, array &$form_state) { $form['links'] = menu_overview_form($form['links'], $form_state); } - return $form; + return parent::form($form, $form_state); } /** @@ -76,10 +132,46 @@ protected function actions(array $form, array &$form_state) { $system_menus = menu_list_system_menus(); $actions['delete']['#access'] = !$menu->isNew() && !isset($system_menus[$menu->id()]); + // Add the language configuration submit handler. This is needed because the + // submit button has custom submit handlers. + if ($this->moduleHandler->moduleExists('language')) { + array_unshift($actions['submit']['#submit'],'language_configuration_element_submit'); + array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit')); + } + // We cannot leverage the regular submit handler definition because we have + // button-specific ones here. Hence we need to explicitly set it for the + // submit action, otherwise it would be ignored. + if ($this->moduleHandler->moduleExists('translation_entity')) { + array_unshift($actions['submit']['#submit'], 'translation_entity_language_configuration_element_submit'); + } return $actions; } /** + * {@inheritdoc} + */ + public function validate(array $form, array &$form_state) { + if ($this->entity->isNew()) { + // The machine name is validated automatically, we only need to add the + // 'menu-' prefix here. + $form_state['values']['id'] = 'menu-' . $form_state['values']['id']; + } + } + + /** + * Submit handler to update the bundle for the default language configuration. + */ + public function languageConfigurationSubmit(array &$form, array &$form_state) { + // Since the machine name is not known yet, and it can be changed anytime, + // we have to also update the bundle property for the default language + // configuration in order to have the correct bundle value. + $form_state['language']['default_language']['bundle'] = $form_state['values']['id']; + // Clear cache so new menus (bundles) show on the language settings admin + // page. + entity_info_cache_clear(); + } + + /** * Overrides Drupal\Core\Entity\EntityFormController::save(). */ public function save(array $form, array &$form_state) { @@ -90,11 +182,6 @@ public function save(array $form, array &$form_state) { menu_overview_form_submit($form, $form_state); } - if ($menu->isNew()) { - // Add 'menu-' to the menu name to help avoid name-space conflicts. - $menu->set('id', 'menu-' . $menu->id()); - } - $status = $menu->save(); $uri = $menu->uri(); diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuLanguageTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuLanguageTest.php new file mode 100644 index 0000000..0a8c245 --- /dev/null +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuLanguageTest.php @@ -0,0 +1,162 @@ + 'Menu language', + 'description' => 'Create menu and menu links in non-English language, and edit language settings.', + 'group' => 'Menu', + ); + } + + function setUp() { + parent::setUp(); + + // Create user. + $this->admin_user = $this->drupalCreateUser(array('access administration pages', 'administer menu')); + $this->drupalLogin($this->admin_user); + + // Add some custom languages. + foreach (array('aa', 'bb', 'cc') as $language_code) { + $language = new Language(array( + 'langcode' => $language_code, + 'name' => $this->randomName(), + )); + language_save($language); + } + } + + /** + * Tests menu language settings and the defaults for menu link items. + */ + function testMenuLanguage() { + // Create a test menu to test the various language-related settings. + // Machine name has to be lowercase. + $menu_name = drupal_strtolower($this->randomName(16)); + $label = $this->randomString(); + $edit = array( + 'id' => $menu_name, + 'description' => '', + 'label' => $label, + 'langcode' => 'aa', + 'default_language[langcode]' => 'bb', + 'default_language[language_show]' => TRUE, + ); + $this->drupalPost('admin/structure/menu/add', $edit, t('Save')); + + // Check that the language settings were saved. + // The menu name should have been prefixed. + $menu_name = 'menu-' . $menu_name; + $this->assertEqual(entity_load('menu', $menu_name)->langcode, $edit['langcode']); + $language_settings = language_get_default_configuration('menu_link', $menu_name); + $this->assertEqual($language_settings['langcode'], 'bb'); + $this->assertEqual($language_settings['language_show'], TRUE); + + // Check menu language and item language configuration. + $this->assertOptionSelected('edit-langcode', $edit['langcode'], 'The menu language was correctly selected.'); + $this->assertOptionSelected('edit-default-language-langcode', $edit['default_language[langcode]'], 'The menu link default language was correctly selected.'); + $this->assertFieldChecked('edit-default-language-language-show'); + + // Test menu link language. + $link_path = ''; + + // Add a menu link. + $link_title = $this->randomString(); + $edit = array( + 'link_title' => $link_title, + 'link_path' => $link_path, + ); + $this->drupalPost("admin/structure/menu/manage/$menu_name/add", $edit, t('Save')); + // Check the link was added with the correct menu link default language. + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $link_title)); + $menu_link = reset($menu_links); + $this->assertMenuLink($menu_link->id(), array( + 'menu_name' => $menu_name, + 'link_path' => $link_path, + 'langcode' => 'bb', + )); + + // Edit menu link default, changing it to cc. + $edit = array( + 'default_language[langcode]' => 'cc', + ); + $this->drupalPost("admin/structure/menu/manage/$menu_name", $edit, t('Save')); + + // Check cc is the menu link default. + $this->assertOptionSelected('edit-default-language-langcode', $edit['default_language[langcode]'], 'The menu link default language was correctly selected.'); + + // Add a menu link. + $link_title = $this->randomString(); + $edit = array( + 'link_title' => $link_title, + 'link_path' => $link_path, + ); + $this->drupalPost("admin/structure/menu/manage/$menu_name/add", $edit, t('Save')); + // Check the link was added with the correct new menu link default language. + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $link_title)); + $menu_link = reset($menu_links); + $this->assertMenuLink($menu_link->id(), array( + 'menu_name' => $menu_name, + 'link_path' => $link_path, + 'langcode' => 'cc', + )); + + // Now change the language of the new link to 'bb'. + $edit = array( + 'langcode' => 'bb', + ); + $this->drupalPost('admin/structure/menu/item/' . $menu_link->id() . '/edit', $edit, t('Save')); + $this->assertMenuLink($menu_link->id(), array( + 'menu_name' => $menu_name, + 'link_path' => $link_path, + 'langcode' => 'bb', + )); + + // Saving menu link items ends up on the edit menu page. To check the menu + // link has the correct language default on edit, go to the menu link edit + // page first. + $this->drupalGet('admin/structure/menu/item/' . $menu_link->id() . '/edit'); + // Check that the language selector has the correct default value. + $this->assertOptionSelected('edit-langcode', 'bb', 'The menu link language was correctly selected.'); + + // Edit menu to hide the language select on menu link item add. + $edit = array( + 'default_language[language_show]' => FALSE, + ); + $this->drupalPost("admin/structure/menu/manage/$menu_name", $edit, t('Save')); + $this->assertNoFieldChecked('edit-default-language-language-show'); + + // Check that the language settings were saved. + $language_settings = language_get_default_configuration('menu_link', $menu_name); + $this->assertEqual($language_settings['langcode'], 'cc'); + $this->assertEqual($language_settings['language_show'], FALSE); + + // Check that the language selector is not available on menu link add page. + $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); + $this->assertNoField('edit-langcode', 'The language selector field was hidden the page'); + } + +} diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index 46e6825..b914837 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -7,16 +7,17 @@ namespace Drupal\menu\Tests; -use Drupal\simpletest\WebTestBase; - -class MenuTest extends WebTestBase { +/** + * Defines a test class for testing menu and menu link functionality. + */ +class MenuTest extends MenuWebTestBase { /** * Modules to enable. * * @var array */ - public static $modules = array('menu', 'block', 'test_page_test', 'contextual'); + public static $modules = array('block', 'test_page_test', 'contextual'); protected $big_user; protected $std_user; @@ -210,9 +211,33 @@ function doMenuTests($menu_name) { $item1 = $this->addMenuLink(0, 'node/' . $node1->nid, $menu_name); $item2 = $this->addMenuLink($item1['mlid'], 'node/' . $node2->nid, $menu_name, FALSE); $item3 = $this->addMenuLink($item2['mlid'], 'node/' . $node3->nid, $menu_name); - $this->assertMenuLink($item1['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => 0)); - $this->assertMenuLink($item2['mlid'], array('depth' => 2, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => $item2['mlid'], 'p3' => 0)); - $this->assertMenuLink($item3['mlid'], array('depth' => 3, 'has_children' => 0, 'p1' => $item1['mlid'], 'p2' => $item2['mlid'], 'p3' => $item3['mlid'], 'p4' => 0)); + $this->assertMenuLink($item1['mlid'], array( + 'depth' => 1, + 'has_children' => 1, + 'p1' => $item1['mlid'], + 'p2' => 0, + // We assert the language code here to make sure that the language + // selection element degrades gracefully without Language module. + 'langcode' => 'en', + )); + $this->assertMenuLink($item2['mlid'], array( + 'depth' => 2, 'has_children' => 1, + 'p1' => $item1['mlid'], + 'p2' => $item2['mlid'], + 'p3' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item3['mlid'], array( + 'depth' => 3, + 'has_children' => 0, + 'p1' => $item1['mlid'], + 'p2' => $item2['mlid'], + 'p3' => $item3['mlid'], + 'p4' => 0, + // See above. + 'langcode' => 'en', + )); // Verify menu links. $this->verifyMenuLink($item1, $node1); @@ -222,8 +247,23 @@ function doMenuTests($menu_name) { // Add more menu links. $item4 = $this->addMenuLink(0, 'node/' . $node4->nid, $menu_name); $item5 = $this->addMenuLink($item4['mlid'], 'node/' . $node5->nid, $menu_name); - $this->assertMenuLink($item4['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => 0)); - $this->assertMenuLink($item5['mlid'], array('depth' => 2, 'has_children' => 0, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => 0)); + $this->assertMenuLink($item4['mlid'], array( + 'depth' => 1, + 'has_children' => 1, + 'p1' => $item4['mlid'], + 'p2' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item5['mlid'], array( + 'depth' => 2, + 'has_children' => 0, + 'p1' => $item4['mlid'], + 'p2' => $item5['mlid'], + 'p3' => 0, + // See above. + 'langcode' => 'en', + )); // Modify menu links. $this->modifyMenuLink($item1); @@ -235,11 +275,52 @@ function doMenuTests($menu_name) { // Move link and verify that descendants are updated. $this->moveMenuLink($item2, $item5['mlid'], $menu_name); - $this->assertMenuLink($item1['mlid'], array('depth' => 1, 'has_children' => 0, 'p1' => $item1['mlid'], 'p2' => 0)); - $this->assertMenuLink($item4['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => 0)); - $this->assertMenuLink($item5['mlid'], array('depth' => 2, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => 0)); - $this->assertMenuLink($item2['mlid'], array('depth' => 3, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => $item2['mlid'], 'p4' => 0)); - $this->assertMenuLink($item3['mlid'], array('depth' => 4, 'has_children' => 0, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => $item2['mlid'], 'p4' => $item3['mlid'], 'p5' => 0)); + $this->assertMenuLink($item1['mlid'], array( + 'depth' => 1, + 'has_children' => 0, + 'p1' => $item1['mlid'], + 'p2' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item4['mlid'], array( + 'depth' => 1, + 'has_children' => 1, + 'p1' => $item4['mlid'], + 'p2' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item5['mlid'], array( + 'depth' => 2, + 'has_children' => 1, + 'p1' => $item4['mlid'], + 'p2' => $item5['mlid'], + 'p3' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item2['mlid'], array( + 'depth' => 3, + 'has_children' => 1, + 'p1' => $item4['mlid'], + 'p2' => $item5['mlid'], + 'p3' => $item2['mlid'], + 'p4' => 0, + // See above. + 'langcode' => 'en', + )); + $this->assertMenuLink($item3['mlid'], array( + 'depth' => 4, + 'has_children' => 0, + 'p1' => $item4['mlid'], + 'p2' => $item5['mlid'], + 'p3' => $item2['mlid'], + 'p4' => $item3['mlid'], + 'p5' => 0, + // See above. + 'langcode' => 'en', + )); // Add 102 menu links with increasing weights, then make sure the last-added // item's weight doesn't get changed because of the old hardcoded delta=50 @@ -395,7 +476,7 @@ function addMenuLink($plid = 0, $link = '', $menu_name = 'tools', $expand $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $title)); $menu_link = reset($menu_links); - $this->assertTrue('Menu link was found in database.'); + $this->assertTrue($menu_link, 'Menu link was found in database.'); $this->assertMenuLink($menu_link->id(), array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid)); return $menu_link; @@ -578,30 +659,6 @@ function enableMenuLink($item) { } /** - * Fetch the menu item from the database and compare it to the specified - * array. - * - * @param $mlid - * Menu item id. - * @param $item - * Array containing properties to verify. - */ - function assertMenuLink($mlid, array $expected_item) { - // Retrieve menu link. - $item = entity_load('menu_link', $mlid); - $options = $item->options; - if (!empty($options['query'])) { - $item['link_path'] .= '?' . drupal_http_build_query($options['query']); - } - if (!empty($options['fragment'])) { - $item['link_path'] .= '#' . $options['fragment']; - } - foreach ($expected_item as $key => $value) { - $this->assertEqual($item[$key], $value, format_string('Parameter %key had expected value.', array('%key' => $key))); - } - } - - /** * Get standard menu link. */ private function getStandardMenuLink() { diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuWebTestBase.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuWebTestBase.php new file mode 100644 index 0000000..e1c21a0 --- /dev/null +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuWebTestBase.php @@ -0,0 +1,47 @@ +options; + if (!empty($options['query'])) { + $item['link_path'] .= '?' . \Drupal::urlGenerator()->httpBuildQuery($options['query']); + } + if (!empty($options['fragment'])) { + $item['link_path'] .= '#' . $options['fragment']; + } + foreach ($expected_item as $key => $value) { + $this->assertEqual($item[$key], $value); + } + } + +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php index 9aa76d4..fa0d4c5 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php @@ -7,10 +7,13 @@ namespace Drupal\menu_link; +use Drupal\Core\Entity\EntityControllerInterface; use Drupal\Core\Entity\EntityFormController; use Drupal\Core\Language\Language; -use Drupal\Core\Entity\EntityControllerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Path\AliasManagerInterface; +use Drupal\Core\Routing\UrlGenerator; +use Drupal\menu_link\MenuLinkStorageControllerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,6 +22,13 @@ class MenuLinkFormController extends EntityFormController implements EntityControllerInterface { /** + * The menu link storage controller. + * + * @var \Drupal\menu_link\MenuLinkStorageControllerInterface + */ + protected $menuLinkStorageController; + + /** * The path alias manager. * * @var \Drupal\Core\Path\AliasManagerInterface @@ -26,6 +36,20 @@ class MenuLinkFormController extends EntityFormController implements EntityContr protected $pathAliasManager; /** + * The URL generator. + * + * @var \Drupal\Core\Routing\UrlGenerator + */ + protected $urlGenerator; + + /** + * The module handler + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * Constructs a new MenuLinkFormController object. * * @param string $operation @@ -33,10 +57,12 @@ class MenuLinkFormController extends EntityFormController implements EntityContr * @param \Drupal\Core\Path\AliasManagerInterface $path_alias_manager * The path alias manager. */ - public function __construct($operation, AliasManagerInterface $path_alias_manager) { + public function __construct($operation, MenuLinkStorageControllerInterface $menu_link_storage_controller, AliasManagerInterface $path_alias_manager, UrlGenerator $url_generator, ModuleHandlerInterface $module_handler) { parent::__construct($operation); - + $this->menuLinkStorageController = $menu_link_storage_controller; $this->pathAliasManager = $path_alias_manager; + $this->urlGenerator = $url_generator; + $this->moduleHandler = $module_handler; } /** @@ -45,7 +71,10 @@ public function __construct($operation, AliasManagerInterface $path_alias_manage public static function createInstance(ContainerInterface $container, $entity_type, array $entity_info, $operation = NULL) { return new static( $operation, - $container->get('path.alias_manager.cached') + $container->get('plugin.manager.entity')->getStorageController('menu_link'), + $container->get('path.alias_manager.cached'), + $container->get('url_generator'), + $container->get('module_handler') ); } @@ -86,7 +115,7 @@ public function form(array $form, array &$form_state) { // $base_path. $path = $menu_link->link_path; if (isset($menu_link->options['query'])) { - $path .= '?' . drupal_http_build_query($menu_link->options['query']); + $path .= '?' . $this->urlGenerator->httpBuildQuery($menu_link->options['query']); } if (isset($menu_link->options['fragment'])) { $path .= '#' . $menu_link->options['fragment']; @@ -145,8 +174,7 @@ public function form(array $form, array &$form_state) { ); // Get number of items in menu so the weight selector is sized appropriately. - $delta = \Drupal::entityManager() - ->getStorageController('menu_link')->countMenuLinks($menu_link->menu_name); + $delta = $this->menuLinkStorageController->countMenuLinks($menu_link->menu_name); $form['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), @@ -156,11 +184,28 @@ public function form(array $form, array &$form_state) { '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'), ); + // Language module allows to configure the menu link language independently + // of the menu language. It also allows to optionally show the language + // selector on the menu link form so that the language of each menu link can + // be configured individually. + if ($this->moduleHandler->moduleExists('language')) { + $language_configuration = language_get_default_configuration('menu_link', $menu_link->bundle()); + $default_langcode = ($menu_link->isNew() ? $language_configuration['langcode'] : $menu_link->langcode); + $language_show = $language_configuration['language_show']; + } + // Without Language module menu links inherit the menu language and no + // language selector is shown. + else { + $default_langcode = ($menu_link->isNew() ? entity_load('menu', $menu_link->menu_name)->langcode : $menu_link->langcode); + $language_show = FALSE; + } + $form['langcode'] = array( '#type' => 'language_select', '#title' => t('Language'), '#languages' => Language::STATE_ALL, - '#default_value' => $menu_link->langcode, + '#default_value' => $default_langcode, + '#access' => $language_show, ); return parent::form($form, $form_state, $menu_link); diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php index f32844f..c3cd2fe 100644 --- a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php +++ b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php @@ -36,6 +36,7 @@ * static_cache = FALSE, * base_table = "menu_links", * uri_callback = "menu_link_uri", + * translatable = TRUE, * entity_keys = { * "id" = "mlid", * "label" = "link_title",