Problem/Motivation

See #3196329: [META] Use cases covered by the decoupled menus initiative for background.

For simple, non-dynamic use cases, we would like to provide an endpoint for downloading all menu elements needed to render a complete menu.

Proposed resolution

Add a route that:

  • loads a menu
  • creates a menu tree
  • serializes it as JSON
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

gabesullice created an issue. See original summary.

gabesullice’s picture

I started working on this in the issue fork linked above. Rather than invent our own format, I've been prototyping the endpoint using Linkset draft as the format for a use case #1. It has not been adopted as an RFC, but it has been picked up by the IETF's HTTP API working group and I expect it to be adopted without too many changes. Since this is technically a contrib module, I think working from a draft is fine for the time being. This is actually an nice opportunity to contribute outside the Drupal ecosystem and the internet more broadly.

If we use this draft, we could make a JavaScript library for it that would be useful outside of the Drupal ecosystem, just like the once library 🙂

You can see exactly what the endpoint is expected to look like in the tests. This is what I have so far:

  1. Route: /system/menu/{menu}/linkset
    • {menu} is a parameter for a menu config entity's machine name.
      • Example output

    gabesullice’s picture

    Status: Active » Needs review

    Gonna do a self-review, but would appreciate a first pass from another reviewer too :)

    vulcanr’s picture

    Just did a very superficial review, and looks fine to me - good to see things moving!

    deciphered’s picture

    I have installed the module(s) and done some basic testing.

    Thoughts so far:

    - Not being JSON:API seems inconsistent with the existing decoupled architecture of Drupal, and will require a seperate client for consumption.
    - There doesn't appear to be any way to filter the results, or get additional data.

    I will be working on a prototype of the DruxtMenu node module to add support for the modules to do further testing: https://github.com/druxt/druxt-menu/issues/44

    deciphered’s picture

    I've put together proof of concept support in the DruxtMenu module.

    The data structure was a bit odd, with both the linkset and linkset[#].item[#]['drupal-menu'] providing arrays of objects.

    The linkset being an array makes me think that it's possible to get multiple linksets from a single endpoint, for translation support or similar. But I test with Umami, and this does not seem to be the case:

    - /system/menu/main/linkset only returns the English results
    - /es/system/menu/main/linkset only returns the Spanish results.

    The linkset[#].item[#]['drupal-menu'] seems more unusual, as it implies a single link can have multiple hierarchies.

    The absence of a unique ID per link is also a problem for me. In my case I constructed an ID using the menu name and the hierarchy.

    gabesullice’s picture

    Thanks for trying it out @Deciphered! I really appreciate it!

    Not being JSON:API seems inconsistent with the existing decoupled architecture of Drupal, and will require a seperate client for consumption.

    I don't think we need/should make media type-specific clients. Let's just make code that works with Drupal. A Drupal client can use application/vnd.api+json for resources, application/schema+json for schema, and application/linkset+json for link sets (e.g. menus).

    JSON:API 1.1-RC3 has support for JSON Schema integration via the describedby link. So it's not unprecedented to use mixed formats for mixed purposes with JSON:API (so long as it's all JSON).

    There doesn't appear to be any way to filter the results, or get additional data.

    I think both of these go beyond the scope of this issue. Beyond this issue, I think a JSON-RPC plugin makes the most sense for these kinds of things.

    The data structure was a bit odd, with both the linkset and linkset[#].item[#]['drupal-menu'] providing arrays of objects.

    The format is entirely driven by https://tools.ietf.org/html/draft-ietf-httpapi-linkset-00.

    The linkset being an array makes me think that it's possible to get multiple linksets from a single endpoint, for translation support or similar.

    This initially confused me too. There is no way to represent multiple linksets. From my understanding, the reason for the top array is to support different anchors within a single linkset. The drupal-menu array is required by the draft. Specifically, it says: The value of an extension attribute MUST be represented by an array, even if there only is one value to be represented.

    The absence of a unique ID per link is also a problem for me. In my case I constructed an ID using the menu name and the hierarchy.

    I plan on writing a little JS today to denormalize the linkset representation into something more easily consumable. Mind if I ping you to give it a whirl?

    gabesullice’s picture

    - /system/menu/main/linkset only returns the English results
    - /es/system/menu/main/linkset only returns the Spanish results.

    That's awesome! I didn't intend this, it's just a consequence of using the menu tree loader, I think.

    deciphered’s picture

    Mind if I ping you to give it a whirl?

    Ping away, no need to ask permission (for me, I consent).

    I don't think we need/should make media type-specific clients.

    I also agree that we shouldn't make media type-specific clients. What I'm referring to is the pre-existing abundance of JSON:API node modules that work for the existing JSON:API that is baked into core, no longer being able to consume/transform all the types of available data.

    It potentially means requiring more frontend modules to consume the data, or making/using new client modules that have to support multiple standards.

    nod_’s picture

    some data for the hierarchical/flat discussion:

    From those few examples, the hierarchical construct is not too common, other big CMSs tend to refer to the immediate parent.

    Also the linkset spec is not suited for menu type things, it exists to replace/improve http link headers.

    vulcanr’s picture

    Just to add - Shopify also gives back just a `parent_id`

    gabesullice’s picture

    I don't care very much about the hierarchy idea. We can convert it to a parent/child representation if y'all feel very strongly about it for aesthetic reasons. I agree that it is less shocking, but I want to make sure we understand that it has some real drawbacks in terms of ease of use and response size.

    For example, these two bits of pseudocode contain equivalent information:

    [{
      "href": "/foo",
      "hierarchy": ".000"
    }, {
      "href": "/bar",
      "hierarchy": ".000.000"
    }, {
      "href": "/baz",
      "hierarchy": ".000.000.000"
    }, {
      "href": "/qux",
      "hierarchy": ".000.000.001"
    }]
    
    [{
      "href": "/foo",
      "id": "foo",
      "parent": null,
      "weight": 0
    }, {
      "href": "/bar",
      "id": "bar",
      "parent": "foo",
      "weight": 0
    }, {
      "href": "/baz",
      "id": "baz",
      "parent": "bar",
      "weight": 0
    }, {
      "href": "/qux",
      "id": "qux",
      "parent": "bar",
      "weight": 1
    }]
    

    If you have a parent property, then every object must also have an id because you must have an ID to reference. Therefore, we will have to invent IDs, or expose the link plugin IDs. This is more verbose and I was trying to avoid that because it exposes implementation details, like which modules are installed.

    Using plugin IDs will also encourage clients to hardcode IDs. For example, if we supply the plugin ID user.logout, clients will be tempted to filter and hardcode something against it. I didn't want to promote that pattern since I would like to promote link relations for these kinds of things in the future. In that particular example, the user.logout link is actually the login link if you're not authenticated. That would be confusing!

    Note that while not explicitly an ID, `${drupal-menu.name}:${drupal-menu.hierarchy}` does form a unique value if you need one for some reason, like @Deciphered discovered at the end of #7.

    If you have a parent property, then you must also have a weight property (if you want to be explicit about order).

    Now, some code examples showing how easy the value is to work with:

    Get subtree of "/bar":

    links.filter(link => link.hierarchy.startsWith(bar.hierarchy));
    

    Get direct parent of "/qux":

    const getParent = (link) => link.hierarchy.slice(0, link.hierarchy.lastIndexOf('.'));
    links.find(link => link.hierarchy === getParent(qux));
    

    Get grandparent of "/qux":

    const getParent = (link) => link.hierarchy.slice(0, link.hierarchy.lastIndexOf('.'));
    links.find(link => link.hierarchy === getParent(getParent(qux)));
    

    Get links with max depth 2:

    links.filter(link => (link.hierarchy.split('.').length - 1) === 2);
    

    Is some link a descendant of another link?

    link.hierarchy.startsWith(other.hierarchy);
    
    nod_’s picture

    Thanks for the details, that is what was missing for me. It's the kind of informations I wanted when I said "we need to clear up what it's optimized for". So a hierarchy like this is better than the "usual" parent/id thing because it's smaller in the json response, doesn't expose internal ids, it's easier to figure out the parent/child relationship across any levels (and not just one level at a time like with parent/id), easier to target a whole branch of the menu at once.

    With that kind of arguments I'm happy to not do what the others are doing. This is the kind of thing we really need to explain in our documentation, to make people understand why we didn't go with the usual parent/id thing. We might even make it easy to go back to a parent/id format with a small code snippet that people can use if they really don't like this hierarchy thing.

    I don't mind the fact that there is no stable id that people can (ab)use. They'll find another way and hopefully that doesn't mean they'll look at the actual URL to do this type of thing.

    gabesullice’s picture

    Version: » 1.0.x-dev

    Rebased on top of #3202608: Remove the original implementation code so that we can run tests on Drupal CI.

    gabesullice’s picture

    I think this is ready to go.

    nod_’s picture

    Status: Needs review » Reviewed & tested by the community

    That looks good to me. Should the padding be configurable? to handle menus with more than 1k links on one level?

    • gabesullice committed 73b99f1 on 1.0.x
      Issue #3196342 by gabesullice, Deciphered, nod_, vulcanr, larowlan:...

    gabesullice’s picture

    Status: Reviewed & tested by the community » Fixed
    Related issues: +#3204132: Allow for more than 1000 menu links per menu level

    Thanks for the review @nod_! I created a follow up for that issue.

    Status: Fixed » Closed (fixed)

    Automatically closed - issue fixed for 2 weeks with no activity.