Recently, in one of our projects with Drupal 10, we faced an interesting challenge: implementing two-level "local tasks" for a specific functionality of our module. Despite the number of documentation related to local tasks in Drupal, setting up two levels of these tasks proved challenging, as we couldn't get them to display in the way we needed. However, after exhaustive research, we found an example in an existing module that helped us solve the problem.
Exploring the Problem
The need was to add a main "local task" and three associated subtasks that would show up when viewing or editing a node. Initially, the main obstacle was finding the right way to implement two levels of local tasks.
The Solution: Inspiration from Contributed Modules
During our search among existing contributed modules, we found inspiration. Drupal.org has very good documentation, and even module issues can often provide clues on how to solve certain problems or needs. Fortunately, the Drupal community is extensive, and there is a high probability that someone has faced the problem before.
The documentation on multi-level "local tasks" already exists on Drupal.org, you can view it at "Providing module-defined local tasks", but we had trouble understanding how to implement it properly.
We also looked for examples in the Examples module that could help us.
In this scenario, it was the webform_node
module that inspired us. This module manages multiple levels of local tasks as it provides a webform content type that defines certain tasks to view the number of submissions made to the webform and allows you to manage them, and this was the starting point of our solution. Once the module was installed and running we could investigate and understand what and how we needed to implement it.
The Solution: Implementation
Instead of providing a detailed explanation of each aspect, as there is very good documentation on routes and "local tasks", we will explain the part that was "problematic" for us.
We defined our routes in the mymodule.routing.yml
file as follows:
# Definition of the main route.
mymodule.node.main:
# Sets the URL that triggers this route, with a placeholder for a specific node.
path: '/node/{node}/mymodule/main'
defaults:
# Specifies the page title for this route.
_title: 'Main route'
# Indicates the form that should be used when accessing this route.
_form: 'Drupal\mymodule\Form\MyModuleForm'
# Conditions that must be met to access this route.
requirements:
# The route requires the user to have this specific permission.
_permission: 'administer site configuration'
# Uses a controller method to check custom access.
_custom_access: '\Drupal\mymodule\Controller\MymoduleNodeController::access'
# Ensures that the {node} placeholder is an integer.
node: \d+
options:
# Indicates that this is an admin route, which affects its display in certain backend areas.
_admin_route: TRUE
parameters:
node:
# Establishes that the {node} parameter must represent an entity of type node.
type: entity:node
We also defined the remaining routes, verifying that each included the necessary controllers to handle their specific functions. So far, nothing new.
The Key: Two Levels in Local Task
The real challenge was configuring the local tasks with hierarchy using the mymodule.links.task.yml
file. Here we discovered that the key to enabling a second level lies in the correct implementation of the parent_id
attribute.
In the following example, a main task is defined that points to the "mymodule.node.main
" route named "mymodule.task_main
", and its base route is entity.node.canonical
since we wanted it to be displayed alongside the rest of the node's tasks. You can see that the "mymodule.node.main
" route appears in the definition of two tasks, as it is both a child and the main task and that both the "mymodule.node.main
" route and the others have the main task "mymodule.task_main
" as their parent.
# Define the main task
mymodule.task_main:
# The title of the task that will be visible in the user interface.
title: 'Main task'
# Specifies the name of the route to which the task is associated.
route_name: mymodule.node.main
# Determines the base route where this task will appear in the task set.
base_route: entity.node.canonical
# Controls the order in which the tasks appear; a smaller value appears further to the left.
weight: 10
# Define a subtask under the main task already defined.
mymodule.subtask_main:
# Visible title for the subtask.
title: 'Main subtask'
# Name of the route that the subtask uses, can be the same as the main task.
route_name: mymodule.node.main
# Specifies that this task is a child of the ID of the main task, thus defining the second level of hierarchy.
parent_id: mymodule.task_main
mymodule.subtask_secondary:
title: 'Subtask secondary'
route_name: mymodule.node.secondary
parent_id: mymodule.task_main
mymodule.subtask_other:
title: 'Other subtask'
route_name: mymodule.node.subtask_other
parent_id: mymodule.task_main
By defining the parent_id
, we established the necessary hierarchical relationship for each subtask so that Drupal could understand and display these tasks correctly under the main task "mymodule.task_main".
Conclusion
The ability of Drupal to be extensible and highly customizable is one of its great strengths, and thanks to its community and open source, it is possible to find examples and solutions for almost any challenge. Through the review of contributed modules and a basic understanding of how to integrate parent_id
in the links.task.yml
, we successfully implemented our two-level "local tasks" system.
In addition to resolving the current challenge, this exercise significantly enhanced our knowledge of Drupal's possibilities and the importance of an active and collaborative community. When faced with a similar problem, consider that solutions may often be conveniently located in a click away within an existing module.