AdminLTEBundle icon indicating copy to clipboard operation
AdminLTEBundle copied to clipboard

Breadcrumbs that do not appear in main menu (KNP Menu)

Open aun408 opened this issue 6 years ago • 14 comments

Hi, I am just wonder if there is any existing feature that will allow non left navigation menu item to be displayed in BreadcrumbMenu.

For example, if I have a User Management module.

View in Left Navigation Menu as below: User Management

View in Breadcrumb Menu will be as below: Home -> User

Now my User Management page consist of a list of users. When I click on the specific user, it will redirected to the user page for editing purposes. What I want to achieve is at this specific page to display the Breadcrumb Menu as below:

Home -> User -> Edit

Or at least stay as Home -> User, for now it only display Home.

Your comment or feedback is very much appreciated.

aun408 avatar Oct 17 '19 10:10 aun408

Its up to you what you display in the breadcrumb: https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/breadcrumbs.md

You don't have to use the same source for SidebarMenuEvent::class and BreadcrumbMenuEvent::class

kevinpapst avatar Oct 18 '19 09:10 kevinpapst

Same question here, and with the doc I don't understand either..

In my case it is also with a user CRUD, sidebar display only : list and create, but I want to display on the breadcrumb the show status too.

I've subscribed to the BreadcrumbMenuEvent => 'onSetupNavbar', but I don't have any navbar, only left menu (sidebar). And my function onSetupNavbar is never triggered..

Do you have any exemple/demo (with files) on knp mode ?

My code:

<?php

namespace App\EventSubscriber;

use KevinPapst\AdminLTEBundle\Event\BreadcrumbMenuEvent;
use KevinPapst\AdminLTEBundle\Event\KnpMenuEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class KnpMenuBuilderSubscriber implements EventSubscriberInterface
{
    /** @var ContainerInterface */
    private $container;

    function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KnpMenuEvent::class => ['onSetupMenu', 100],
            BreadcrumbMenuEvent::class => ['onSetupNavbar', 100],
        ];
    }

    public function onSetupNavbar(BreadcrumbMenuEvent $event){

        $items = $event->getItems();
        $request = $event->getRequest();
        $active = $event->getActive();

    }

    public function onSetupMenu(KnpMenuEvent $event)
    {
        $menu = $event->getMenu();

        /* ========== DASHBOARD ========== */
        $menu->addChild('dashboard', [
            'route' => 'dashboard',
            'label' => 'Dashboard',
            'childOptions' => $event->getChildOptions()
        ])->setLabelAttribute('icon', 'fa fa-columns');
        

        /* ========== CLIENTS ========== */
        $client_menu = $menu->addChild('client', [
            'label' => 'Clients',
            'childOptions' => $event->getChildOptions()
        ])->setLabelAttribute('icon', 'fa fa-users');

        /* --- CLIENTS/Create --- */
        $client_menu->addChild('client_new', [
            'route' => 'client_new',
            'label' => 'Create',
            'childOptions' => $event->getChildOptions()
        ])->setLabelAttribute('icon', 'fa fa-user-plus');

        /* --- CLIENTS/List --- */
        $client_menu->addChild('client_index', [
            'route' => 'client_index',
            'label' => 'List',
            'childOptions' => $event->getChildOptions()
        ])->setLabelAttribute('icon', 'fa fa-users');
    }
}

My admin_lte.yaml :

admin_lte:
    knp_menu:
        enable: true
        main_menu: adminlte_main
        breadcrumb_menu: true

My services.yaml :

[...]
services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
[...]

cavasinf avatar Jan 28 '20 09:01 cavasinf

The demo app https://github.com/kevinpapst/AdminLTEBundle-Demo has a KNP mode as well.

Simply switch this to true: https://github.com/kevinpapst/AdminLTEBundle-Demo/blob/master/config/packages/admint_lte.yaml#L41

kevinpapst avatar Jan 28 '20 10:01 kevinpapst

@kevinpapst Thanks for the fast anwser !! I've upadted my question (a lot, I didn't think you will respond that fast).

I just added my admin_lte.yaml and services.yaml. As you can see, knp_menu is already enabled.

I should have said that before, but the knp_menu and breadcrumb work well together. But if I've a link that not in the sidebar menu (knp_menu), the breadcrumb doesn't work anymore.

cavasinf avatar Jan 28 '20 10:01 cavasinf

Thats because you set it to true which means "re-use the main menu for the breadcrumb, see https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/views/Breadcrumb/knp-breadcrumb.html.twig#L5

Switch breadcrumb_menu: true to breadcrumb_menu: my_breadcrumb_menu, see https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/knp_menu.md#enabling-breadcrumb-support

kevinpapst avatar Jan 28 '20 10:01 kevinpapst

Ok I get it for the alias_menu option, but what I don't understand, is why do I need to make a complete menu builder for that case ? I don't want to remake the wheel ..

There no way to 'autoWire' all controllers links to the breadcrumb ? Do I need to defined manually all links that the breadcrumb can pass through ?

cavasinf avatar Jan 28 '20 11:01 cavasinf

Maybe I don't understand your use case. Maybe you just want to overwrite the breadcrumb menu template. Maybe you want to add/use a feature that does not yet exist.

If you want to auto-wire all routes (that allow GET), then create a new MenuBuilder that does that. Or create a new Annotation and to it during compile time, Symfony has so many options.

Please go ahead and sent in a PR if you have a good idea how to make that simpler for everyone!

kevinpapst avatar Jan 28 '20 11:01 kevinpapst

Yeah I'll try to find a way to do what I want.. Looking for the 'cookie' way for fast dev. Going later on Menu builder if I'm satisfied

There is the case I try to do :

[sidebar] (treeview)
Clients
╚>Create (/client/create)
╚> List (/client)
Others data
╚> ...

image

[breadcrumb]

  • (/client/create) OK image
  • (/client) OK image
  • (/client/1) NOT ok nothing is displayed.

I'm on a detailled view of a specific user, url is like : /client/1 I want it to be like 'Clients > Detail' or 'Clients > Show'

cavasinf avatar Jan 28 '20 12:01 cavasinf

Yeah, not sure about that. As I said: never used it like this by myself. So in order to find an answer I would have to debug as well, but as I don't need it (and have no project that uses KNP menu) ... all I can say is: sorry, but I don't think its possible out of the box.

But I would appreciate if you would share your results, might it be documentation or a PR, so it becomes easier for the next one.

Just an idea: only create a clients menu entry, then add a "create" button somewhere on that initial page and write the breadcrumb code manually on each page.

kevinpapst avatar Jan 28 '20 12:01 kevinpapst

Sadly there is no way to get the Request with KnpMenuEvent..

Trying to work around a voter with KnpMenu/Matcher .

Code looks like that today :


public static function getSubscribedEvents(): array
    {
        return [
            KnpMenuEvent::class => ['onSetupMenu', 100],
        ];
    }

    public function onSetupMenu(KnpMenuEvent $event,$eventName, EventDispatcherInterface $dispatcher)
    {

        $currentURL = $_SERVER['REQUEST_URI'];
        $matcherUri = new Matcher(new UriVoter($currentURL));
        $rendererUri = new ListRenderer($matcherUri);
        $htmlUri = $rendererUri->render($menu);

        $matcherRoute = new Matcher(new RouteVoter());
        $rendererRoute = new ListRenderer($matcherRoute);
        $htmlRoute = $rendererRoute->render($menu);

        $items = $menu->getChildren();

        $matcher = new Matcher(new RegexVoter($currentURL));
        $renderer = new ListRenderer($matcher);
        $html = $renderer->render($menu);
}

Doesn't work, even for normal link that should .. I Try to continue tomorrow.

cavasinf avatar Jan 28 '20 16:01 cavasinf

You get the request like this:

    public function __construct(RequestStack $requestStack)
    {
        $this->request = $requestStack->getCurrentRequest();
    }

kevinpapst avatar Jan 28 '20 16:01 kevinpapst

@kevinpapst Did it ....

So, if you have to display a breadcrumb with a link that doesn't require parameters you can add an item into the KnpMenuBuilderSubscriber with a display at false, and it's done.

But if you want to do the same with parameters, like: @Route("/client/{id}", name="client_show", methods={"GET"}) It is not that simple than the first method.

I had to do a custom voter that looking pretty much like the default RouteVoter from KnpMenu, expect I've removed the 'parameters check' part.

And use that item into my menu :

            /* --- CLIENTS/Show --- */
            $client_menu->addChild('client_show', [
                'route' => 'client_show',
                'routeParameters' => ['id' => 0],
                'label' => 'Show',
                'childOptions' => $event->getChildOptions()
            ])->setDisplay(false);

Note

You have three way to hide an item menu :

  • addChild options => 'attributes' => ['class' => 'hidden']
  • addChild(...)->setAttribute('class','hidden');
  • addChild(...)->setDisplay(false); setDisplay being my favorite
$client_menu->addChild('test_hidden', [
   'route' => 'test_hidden',
   'label' => 'Show',
   //'attributes' => ['class' => 'hidden'],
   'childOptions' => $event->getChildOptions()
])->setDisplay(false);//->setAttribute('class','hidden');

Files

I can't do a PR, but I can give you my files

  • [NEW File] RouteNameVoter
<?php
// src/Matcher/Voter/RouteNameVoter.php
namespace App\Matcher\Voter;

use Knp\Menu\ItemInterface;
use Knp\Menu\Matcher\Voter\VoterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Voter based on the route name only (ignore parameters)
 */
class RouteNameVoter implements VoterInterface
{
    /**
     * @var RequestStack|null
     */
    private $requestStack;

    /**
     * @var Request|null
     */
    private $request;

    public function __construct($requestStack = null)
    {
        if ($requestStack instanceof RequestStack) {
            $this->requestStack = $requestStack;
        } elseif ($requestStack instanceof Request) {
            @trigger_error(sprintf('Passing a Request as the first argument for "%s" constructor is deprecated since version 2.3 and won\'t be possible in 3.0. Pass a RequestStack instead.', __CLASS__), E_USER_DEPRECATED);

            // BC layer for the old API of the class
            $this->request = $requestStack;
        } elseif (null !== $requestStack) {
            throw new \InvalidArgumentException('The first argument of %s must be null, a RequestStack or a Request. %s given', __CLASS__, is_object($requestStack) ? get_class($requestStack) :  gettype($requestStack));
        } else {
            @trigger_error(sprintf('Not passing a RequestStack as the first argument for "%s" constructor is deprecated since version 2.3 and won\'t be possible in 3.0.', __CLASS__), E_USER_DEPRECATED);
        }
    }

    public function matchItem(ItemInterface $item)
    {
        if (null !== $this->requestStack) {
            $request = $this->requestStack->getMasterRequest();
        } else {
            $request = $this->request;
        }

        if (null === $request) {
            return null;
        }

        $route = $request->attributes->get('_route');
        if (null === $route) {
            return null;
        }

        $routes = (array) $item->getExtra('routes', []);

        foreach ($routes as $testedRoute) {
            if (\is_string($testedRoute)) {
                $testedRoute = ['route' => $testedRoute];
            }

            if (!\is_array($testedRoute)) {
                throw new \InvalidArgumentException('Routes extra items must be strings or arrays.');
            }

            if ($this->isMatchingRouteName($request, $testedRoute)) {
                return true;
            }
        }

        return null;
    }

    private function isMatchingRouteName(Request $request, array $testedRoute)
    {
        $route = $request->attributes->get('_route');

        if (isset($testedRoute['route'])) {
            if ($route !== $testedRoute['route']) {
                return false;
            }
        } elseif (!empty($testedRoute['pattern'])) {
            if (!\preg_match($testedRoute['pattern'], $route)) {
                return false;
            }
        } else {
            throw new \InvalidArgumentException('Routes extra items must have a "route" or "pattern" key.');
        }

        return true;
    }
}
  • [Example] KnpMenuBuilderSubscriber
<?php
//src/EventSubscriber/KnpMenuBuilderSubscriber.php
namespace App\EventSubscriber;

use App\Entity\User;
use App\Matcher\Voter\RouteNameVoter;
use KevinPapst\AdminLTEBundle\Event\KnpMenuEvent;
use Knp\Menu\ItemInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Security;

class KnpMenuBuilderSubscriber implements EventSubscriberInterface
{
    /* @var Security */
    protected $security;
    /* @var Request */
    protected $request;

    public function __construct(RequestStack $requestStack,Security $security)
    {
        $this->security = $security;
        $this->request = $requestStack->getCurrentRequest();
    }

    public static function getSubscribedEvents(): array
    {
        return [
            KnpMenuEvent::class => ['onSetupMenu', 100],
        ];
    }

    public function onSetupMenu(KnpMenuEvent $event)
    {
        $menu = $event->getMenu();

        /* ========== DASHBOARD ========== */
        $menu->addChild('dashboard', [
            'route' => 'dashboard',
            'label' => 'Dashboard',
            'childOptions' => $event->getChildOptions()
        ])->setLabelAttribute('icon', 'fa fa-columns');

        if ($this->security->isGranted(User::CST_Role_User)) {

            /* ========== CLIENTS ========== */
            $client_menu = $menu->addChild('client', [
                'label' => 'Clients',
                'childOptions' => $event->getChildOptions()
            ])->setLabelAttribute('icon', 'fa fa-users');

            /* --- CLIENTS/Create --- */
            $client_menu->addChild('client_new', [
                'route' => 'client_new',
                'label' => 'Create',
                'childOptions' => $event->getChildOptions()
            ])->setLabelAttribute('icon', 'fa fa-user-plus');

            /* --- CLIENTS/List --- */
            $client_menu->addChild('client_index', [
                'route' => 'client_index',
                'label' => 'List',
                'childOptions' => $event->getChildOptions()
            ])->setLabelAttribute('icon', 'fa fa-users');

            /* --- CLIENTS/Show --- */
            $client_menu->addChild('client_show', [
                'route' => 'client_show',
                'routeParameters' => ['id' => 0],
                'label' => 'Show',
                //'attributes' => ['class' => 'hidden'],
                'childOptions' => $event->getChildOptions()
            ])->setDisplay(false);//->setAttribute('class','hidden');



            /* ========== CONSTRUCTION SITES ========== */
            $construction_menu = $menu->addChild('construction_sites', [
                'label' => 'Construction sites',
                'childOptions' => $event->getChildOptions()
            ])->setLabelAttribute('icon', 'fas fa-briefcase');

        }

        // IMPORTANT PART
        $this->activateByRouteName($this->request->attributes->get('_route'),$menu->getChildren());
    }

    /**
     * @param string $routeName
     * @param ItemInterface[] $items
     */
    protected function activateByRouteName($routeName, $items)
    {
        $routeNameVoter = new RouteNameVoter($this->request);

        foreach ($items as $item) {
            if ($item->hasChildren()) {
                $this->activateByRouteName($routeName, $item->getChildren());
            } else {
                // USE the CUSTOM voter
                if ($routeNameVoter->matchItem($item) === true) {
                    $item->setCurrent(true);
                    break;
                }
            }
        }
    }
}

cavasinf avatar Jan 29 '20 09:01 cavasinf

Also, the breadcrumb words with KNP are not using the trans filter from twig template in your knp-breadcrumb.html.twig

<a href="{{ item.uri }}">{{ item.label }}</a> Must be : <a href="{{ item.uri }}">{{ item.label | trans }}</a>

cavasinf avatar Jan 29 '20 10:01 cavasinf

Thanks for sharing, I leave this issue open in case someone else needs a solution.

As said before: I haven't used the KNP Menu. If you think the translation must happen in the template, please send in a PR. Simply open https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/views/Breadcrumb/knp-breadcrumb.html.twig, click edit and GitHub will guide you through the rest.

kevinpapst avatar Jan 30 '20 14:01 kevinpapst