I have a number of custom actions that a user must be able to perform from a node display page for a particular content type ("artefacts"). Some actions are adding a node of another type linked to the current artefact via a prepopulated ER field (Add Picture, Add Link, Add Event), others should invoke views pages with set paths and a contextual filter set to the current node (Order Pictures, Order Links). Currently I have implemented those as a bunch of Display Suite fields displaying action icons linked to predefined paths, like this:
<a href="/orderphotos/[node:nid]?destination=/node/[node:nid]"><img data-align="right" src="/themes/anw/styles/icons/order_photos.png" /></a>
<a href="/node/add/events?edit[field_venue]=[node:nid]&amp;edit[field_coordinates][0][lat]=[node:field_coordinates:lat]&amp;edit[field_coordinates][0][lng]=[node:field_coordinates:lng]&amp;edit[field_street_address]=[node:field_street_address]&amp;edit[field_city]=[node:field_city:target_id]&amp;edit[field_country]=[node:field_artefact_country:target_id]"><img data-align="right" src="/themes/anw/styles/icons/add_event.png" /></a>
It works fine but creates unseemly icon clutter in the corner of every artefact page I would like to get rid of. I would prefer a single "action" icon in the corner invoking a menu of actions accessible to the current user on the current artefact - exactly what contextual links are supposed to do.
I have read every documentation page on contextual links I could lay my hands on (starting with https://www.drupal.org/docs/8/api/menu-api/providing-module-defined-cont...) and (as usual with most Drupal documentation) the only thing I fully understand is that I want to kill myself, now.
The first way to add contextual links is supposed to be via a hook_contextual_links_view_alter which is from D7 but seems to be still present in D8. I defined one in my module but it just never gets called at all. I guess I must add that I have implemented other hooks in my module like hook_cron and hook_views_data_alter and those work for me, so I think I understand the basics of hooks and how to define them.
Then I decided to go the mymodule.links.contextual.yml way. But no matter what I put there, nothing ever gets added to any contextual menu on any block on my pages (and there are no messages in the log).
The docs say: "The key concept for contextual links is the group". Ah, the group! Wait, what is "the group"? Am I supposed to put my module name there?
Then I apparently need to define routes for my actions. What am I supposed to put into the "defaults" section when I already have working paths for all of my actions (/node/add and /orderphotos which is a view)?
Could any kind soul who managed to read this amusing post through the end try to unconfuse the poor newb?

Comments

marassa’s picture

I finally made some progress with hook_contextual_links_view_alter() - it turned out that the hook simply was not called for recently displayed pages even after I repeatedly Cleared All Caches. Seems like sets of contextual links for a given page once generated are cached somewhere not flushable with Clear All Caches and are not regenerated until the cache expires. Has anybody experienced anything similar and has any idea of how to defeat it?
And one more question: no matter how I try, I cannot get read of the contextual links' 'pencil' icons popping up on hovering over every block, even when there are no links accessible to the current user. The pencil appears on hover and when clicked, nothing is displayed if the current user is not allowed to configure blocks.
I have found a number of code samples and a whole contrib module to hide contextual links for D7, all utilizing unset($element['#links']). I tried that and the links themselves are indeed cleared but the pencil remains. Is there any way to kill the pencil in D8 on the rendering stage, short of killing it via css/js on the client?

sayco’s picture

Hi, any progress with adding contextual links?
I've stuck on adding cl to my custom module block, unfortunately with no luck so far.

However, what i rly want is to answer for your particular question (hope that it will help other users, when they get in to similar problem).

Seems like sets of contextual links for a given page once generated are cached somewhere not flushable with Clear All Caches and are not regenerated until the cache expires. Has anybody experienced anything similar and has any idea of how to defeat it?

I don't know why it was chose to cache this way, but all that stuff are stored in your browser storage, cache etc. especially in sessionStorage.
In my browser (FF) you can find it at developer tools (F12) -> (data/storage -> not sure about translation cause my native language is Polish)->sessionStorage

marassa’s picture

Hi, yes I finally managed to make them work for me by coding them into my.module. Here's my complete code - hopefully it helps but of course it's highly dependent on my data structures etc:

/**
 * Implements hook_contextual_links_view_alter().
 */
function tanw_contextual_links_view_alter(&$element, $items) {
  // This hook is called for EVERY set of contextual links
  // on a page.  We first want to check the $element to make
  // sure we are adding a link to the correct list of contextual
  // links.
  if (isset($element['#contextual_links']['node'])
         && $element['#contextual_links']['node']['metadata']['ds_view_mode'] == "default"
         && in_array($element['#contextual_links']['node']['metadata']['ds_bundle'], ['artefacts','artists','cities','countries','events'])) {

    $entity = entity_load('node', $element['#contextual_links']['node']['route_parameters']['node']);
    if ($element['#contextual_links']['node']['metadata']['ds_bundle'] == "artefacts") {
      $city = entity_load('node', $entity->field_city->target_id);
      $country = entity_load('node', $entity->field_artefact_country->target_id);

      $element['#links']['add_event'] = array(
        'title' => t('Add event'),
        'url' => Url::fromRoute('node.add', array(
          'node_type' => 'events',
          'edit[field_venue_s_]'            => $entity->id(),
          'edit[field_coordinates][0][lat]' => $entity->field_coordinates->lat,
          'edit[field_coordinates][0][lng]' => $entity->field_coordinates->lng,
          'edit[field_street_address]'      => $entity->field_street_address->value,
          'edit[field_city]'                => $entity->field_city->target_id,
          'edit[field_country]'             => $entity->field_artefact_country->target_id,
        ))
      );
      $element['#links']['add_picture'] = array(
        'title' => t('Add picture'),
        'url' => Url::fromRoute('node.add', array(
          'node_type' => 'pictures',
          'edit[field_linked_to]' => $entity->id(),
          'storage_path' => $country->title->value . '/' . $city->title->value,
        ))
      );
      if(\Drupal::currentUser()->id() == 1 || ((\Drupal::currentUser()->id() === $entity->getOwnerId()) && !$entity->isPublished())) { 
        $element['#links']['manage_pictures'] = array(
          'title' => t('Manage pictures'),
          'url' => Url::fromRoute('view.photo_slideshow.page_1', $element['#contextual_links']['node']['route_parameters']),
        );
      }
    }  
    $element['#links']['add_link'] = array(
      'title' => t('Add link'),
      'url' => Url::fromRoute('node.add', array(
        'node_type' => 'links',
        'edit[field_linked_to]' => $entity->id(),
      ))
    );
    if(\Drupal::currentUser()->id() == 1 || ((\Drupal::currentUser()->id() === $entity->getOwnerId()) && !$entity->isPublished())) {
      $element['#links']['manage_links'] = array(
        'title' => t('Manage links'),
        'url' => Url::fromRoute('view.manage_links.page_1', $element['#contextual_links']['node']['route_parameters']),
      );
    }
    else unset($element['#links']['entitynodeedit-form']);
  } 
  else {
    $element['#access'] = false;
    if (isset($element['#contextual_links']['node'])
         && $element['#contextual_links']['node']['metadata']['ds_view_mode'] != "default"
         && \Drupal::currentUser()->id() != 1) unset($element['#links']['entitynodeedit-form']);
  }
}

For the life of me I can't remember how I fixed the caching issue, but I know that it works now!

sayco’s picture

Thank you for your code sample. Your example is a little bit different as it adds contextuals to node.
However i managed to add contextual link to my plug-in block, another (yet quite simple) way.

It was more then enough to create proper mymodule.links.contextual.yml

mymodule.contextual:
  title: 'Configure weather forecast'
  route_name: mymodule.admin_form
  group: mymodule

Then all what's next to do was add proper hook in mymodule.module file

/**
 * Implements hook_block_view_BASE_BLOCK_ID_alter().
 */
function mymodule_block_view_my_block_id_alter(array &$build, \Drupal\Core\Block\BlockPluginInterface $block) {
  $build['#contextual_links']['mymodule_group_name_from_links.contextual'] = [
    'route_parameters' => [], //empty array as i don't pass any arguments to my route
  ];
}

Now everything works fine and as it was expected.
Btw. The thing about clearing cache in browser is 100% confirmed :-)

Leagnus’s picture

sayco, what link the code above adds?

sayco’s picture

Leagnus, the code above adds new link to the list of contextual links (read more about contextual links) to blocks. In my case I add new contextual link to block provided by my weather forecast module, which allows you to go directly to the weather service configuration page:

As a reminder, this is how contextual links look like