Jump to content

Custom Admin Menus


Robin S
 Share

Recommended Posts

This module lets you add some custom menu items to the main admin menu, and you can set the dropdown links dynamically in a hook if needed.

Sidenote: the module config uses some repeatable/sortable rows for the child link settings, similar to the ProFields Table interface. The data gets saved as JSON in a hidden textarea field. Might be interesting to other module developers?

Custom Admin Menus

Adds up to three custom menu items with optional dropdowns to the main admin menu.

The menu items can link to admin pages, front-end pages, or pages on external websites.

The links can be set to open in a new browser tab, and child links in the dropdown can be given an icon.

Requires ProcessWire v3.0.178 or newer and AdminThemeUikit.

Screenshots

Example of menu items

cam-1

Module config for the menus

cam-2

Link list shown when parent menu item is not given a URL

cam-3

Advanced

Setting child menu items dynamically

If needed you can set the child menu items dynamically using a hook.

Example:

$wire->addHookAfter('CustomAdminMenus::getMenuChildren', function(HookEvent $event) {
    // The menu number is the first argument
    $menu_number = $event->arguments(0);
    if($menu_number === 1) {
        $colours = $event->wire()->pages->findRaw('template=colour', ['title', 'url', 'page_icon']);
        $children = [];
        foreach($colours as $colour) {
            // Each child item should be an array with the following keys
            $children[] = [
                'icon' => $colour['page_icon'],
                'label' => $colour['title'],
                'url' => $colour['url'],
                'newtab' => false,
            ];
        }
        $event->return = $children;
    }
});

Create multiple levels of flyout menus

It's also possible to create multiple levels of flyout submenus using a hook.

cam-4

For each level a submenu can be defined in a "children" item. Example:

$wire->addHookAfter('CustomAdminMenus::getMenuChildren', function(HookEvent $event) {
    // The menu number is the first argument
    $menu_number = $event->arguments(0);
    if($menu_number === 1) {
        $children = [
            [
                'icon' => 'adjust',
                'label' => 'One',
                'url' => '/one/',
                'newtab' => false,
            ],
            [
                'icon' => 'anchor',
                'label' => 'Two',
                'url' => '/two/',
                'newtab' => false,
                'children' => [
                    [
                        'icon' => 'child',
                        'label' => 'Red',
                        'url' => '/red/',
                        'newtab' => false,
                    ],
                    [
                        'icon' => 'bullhorn',
                        'label' => 'Green',
                        'url' => '/green/',
                        'newtab' => false,
                        'children' => [
                            [
                                'icon' => 'wifi',
                                'label' => 'Small',
                                'url' => '/small/',
                                'newtab' => true,
                            ],
                            [
                                'icon' => 'codepen',
                                'label' => 'Medium',
                                'url' => '/medium/',
                                'newtab' => false,
                            ],
                            [
                                'icon' => 'cogs',
                                'label' => 'Large',
                                'url' => '/large/',
                                'newtab' => false,
                            ],
                        ]
                    ],
                    [
                        'icon' => 'futbol-o',
                        'label' => 'Blue',
                        'url' => '/blue/',
                        'newtab' => true,
                    ],
                ]
            ],
            [
                'icon' => 'hand-o-left',
                'label' => 'Three',
                'url' => '/three/',
                'newtab' => false,
            ],
        ];
        $event->return = $children;
    }
});

Showing/hiding menus according to user role

You can determine which menu items can be seen by a role by checking the user's role in the hook.

For example, if a user has or lacks a role you could include different child menu items in the hook return value. Or if you want to conditionally hide a custom menu altogether you can set the return value to false. Example:

$wire->addHookAfter('CustomAdminMenus::getMenuChildren', function(HookEvent $event) {
    // The menu number is the first argument
    $menu_number = $event->arguments(0);
    $user = $event->wire()->user;
    // For custom menu number 1...
    if($menu_number === 1) {
        // ...if user does not have some particular role...
        if(!$user->hasRole('foo')) {
            // ...do not show the menu
            $event->return = false;
        }
    }
});
  • Like 14
  • Thanks 4
Link to comment
Share on other sites

Ooh, this looks great! Thanks Robin. I'm still on .165 at the moment so will have to upgrade. ?

Out of interest is there a technical reason why it's limited to three menus? Also, do you think there will ever be support for the navJSON type links? I.e. Main > drop down item > third-level item.

Link to comment
Share on other sites

Just installed this and it is really good! Thanks @Robin S, I am abandoning my Process class from the other thread. ?

menu.png.9de375c85478b2c6ab88365585761507.png

One suggestion. Can you dynamically set permissions? If so, it might be nice to restrict the appearance of the menus to a permission. It doesn't really matter for my use case but thought it might be worth adding if it's easy enough.

  • Like 2
Link to comment
Share on other sites

On 9/7/2021 at 9:49 PM, DrQuincy said:

Out of interest is there a technical reason why it's limited to three menus?

No, but there needed to be some limit set and I can't personally imagine needing more than three custom menus. If anyone using the module finds they need more then I'd consider increasing the limit.

On 9/7/2021 at 9:49 PM, DrQuincy said:

Also, do you think there will ever be support for the navJSON type links? I.e. Main > drop down item > third-level item.

Not actual navJSON ajax-loaded menus because those require a Process module. But in the latest version I've allowed for the possibility to define multiple levels of submenus using a hook. See the updated readme.

16 hours ago, DrQuincy said:

Can you dynamically set permissions? If so, it might be nice to restrict the appearance of the menus to a permission.

In the hook you can check if the user has or lacks a role and return different child item data accordingly. And in the latest version if you return false from the hook then the menu is not shown at all. See the updated readme.

20 hours ago, DrQuincy said:

P.S. One “gotcha” that caught me out was the items did not appear in the mobile nav until I logged in and out again.

Should be fixed in the latest version.

  • Like 2
Link to comment
Share on other sites

  • 4 months later...

Nice work @Robin S!

One Issue I've come across that I'd be really keen on a solution for is to support $config->urls in the URL field.

For example, I have a 'Site Settings' page in all my sites where the client can edit the global company details like phone, email, address etc. I'd like to add a link in the top admin bar to this page so it's easy to find (some sites have a lot pf pages in the tree)!

I can do it once the site is launched with an absolute URL but I'm struggling with a relative URL that I can add to my Framework site. This is because the correct relative link depends on the page the 'Settings Page' was accessed from:

This would work from another edit page:

../../admin/page/edit/?id=1016

However, it would need to be this from the page tree:

../admin/page/edit/?id=1016

One fix would be to support $config->urls in {} e.g. 

{$urls->admin}page/edit/?id=1016

As per these docs: https://processwire.com/api/ref/paths/

Hope this helps and thanks for the module!

Link to comment
Share on other sites

6 hours ago, prestoav said:

I can do it once the site is launched with an absolute URL but I'm struggling with a relative URL that I can add to my Framework site.

Personally I would use the admin page name in the URL but not the scheme/domain. So a relative URL but relative to the site root.

In your example it looks like the admin page name is "admin" so the URL would be:

/admin/page/edit/?id=1016

If you want to get the admin URL dynamically from $config for some reason then you just need to use the CustomAdminMenus::getMenuChildren hook method instead of entering your URLs as plain text in the admin.

Link to comment
Share on other sites

@Robin S Thanks for getting back to me.

Your initial suggestion does work on live sites in a domain root but, sadly, not in a development environment where different sites are housed on sub folders.

For example, if the site is at www.mydomain.com then the resulting link is www.mydomain.com/admin/page/edit/?id=1016 and this works from all admin pages. However, if the site is at localhost:8888/dev_site_1/ then the link created is localhost:8888/admin/page/edit/?id=1016 (i.e. the 'dev_site_1' is missing) and the resulting link does't work.

I'll investigate the hook method of course but many devs might find this useful to work in their framework sites.

Thanks again for the good work.

Link to comment
Share on other sites

10 minutes ago, prestoav said:

Your initial suggestion does work on live sites in a domain root but, sadly, not in a development environment where different sites are housed on sub folders.

As a general thing to make your life as a developer easier I suggest looking into virtual hosts for local development. That way your local site will be at something like mysite.dev and you can seamlessly move from local to remote without any URL issues. You can use virtual hosts with any LAMP/WAMP/etc, and in particular Laragon will give you an automatic virtual host for every site without needing any configuration.

  • Like 1
Link to comment
Share on other sites

3 minutes ago, Robin S said:

As a general thing to make your life as a developer easier I suggest looking into virtual hosts for local development. That way your local site will be at something like mysite.dev and you can seamlessly move from local to remote without any URL issues. You can use virtual hosts with any LAMP/WAMP/etc, and in particular Laragon will give you an automatic virtual host for every site without needing any configuration.

I'll certainly look into that.

Link to comment
Share on other sites

  • 2 months later...
On 4/14/2022 at 8:06 PM, taotoo said:

This module looks great! Should it work with the Reno theme, or are they incompatible?

Thanks.

It looks like the legacy Default and Reno themes don't call AdminThemeFramework::getPrimaryNavArray() so the hooks added by this module don't have any effect. Therefore AdminThemeUikit is a requirement - I've updated the module install requirements and readme to reflect this.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

  • 1 year later...

@bernhard, thanks for the heads-up. The new tab option was working for child items but not for the top level menu items. Fixed in v0.1.5 and in this version I've also added an option to define an icon for the top-level items - these are only visible in the sidebar menu so probably not seen all that often but still good to have control over. 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

  • 1 year later...

Good day, @Robin S! Thanks for the module! I've used it with a great pleasure on my single-language sites. But now I need to set things up for the ML site. How would you suggest to do that?

I think I would need both language-dependant labels AND urls. Is it better to handle this with hooks, or there could be a way to do it from the UI?

Link to comment
Share on other sites

On 12/7/2024 at 11:55 PM, Ivan Gretsky said:

I think I would need both language-dependant labels AND urls. Is it better to handle this with hooks, or there could be a way to do it from the UI?

Unfortunately I don't think this module can be easily adapted to support multi-language menus, because to get the repeater-like interface for variable numbers of child items working I needed to come up with a custom way to store that data in JSON format rather than use the core way of storing individual inputfield data. So multi-language support would mean creating a new way of storing multi-language data that's separate from PW's way and I don't want to try to reinvent that wheel.

But you could hook some of the same methods that the module does to add your own custom menus to the admin. Example:

$wire->addHookBefore('ProcessController::execute', function(HookEvent $event) {
	// Prevent admin menus from being cached
	$this->wire()->session->removeFor('AdminThemeUikit', 'prnav');
	$this->wire()->session->removeFor('AdminThemeUikit', 'sidenav');
});

$wire->addHookAfter('AdminThemeFramework::getPrimaryNavArray', function(HookEvent $event) {
	$items = $event->return;
	$user = $event->wire()->user;

	$data = [
		[
			'label' => [
				'default' => 'Shirts',
				'french' => 'Chemises',
			],
			'url' => [
				'default' => '/shirts/',
				'french' => '/chemises/',
			],
			'children' => [
				[
					'label' => [
						'default' => 'Small',
						'french' => 'Petit',
					],
					'url' => [
						'default' => '/shirts/small/',
						'french' => '/chemises/petit/',
					],
					'icon' => 'smile-o',
				],
				[
					'label' => [
						'default' => 'Medium',
						'french' => 'Moyen',
					],
					'url' => [
						'default' => '/shirts/medium/',
						'french' => '/chemises/moyen/',
					],
					'icon' => 'thumbs-o-up',
				],
			],
		],
	];

	foreach($data as $item) {
		$menu = [
			'id' => 0,
			'parent_id' => 0,
			'name' => '',
			'title' => $item['label'][$user->language->name],
			'url' => $item['url'][$user->language->name],
			'icon' => '',
			'children' => [],
			'navJSON' => '',
		];
		foreach($item['children'] as $child) {
			$menu['children'][] = [
				'id' => 0,
				'parent_id' => 0,
				'name' => '',
				'title' => $child['label'][$user->language->name],
				'url' => $child['url'][$user->language->name],
				'icon' => $child['icon'],
				'children' => [],
				'navJSON' => '',
			];
		}
		$items[] = $menu;
	}

	$event->return = $items;
});

image.png.d27134e9fc93cffa11d67bd9868cd958.png

  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...