Skip to main content

Menu

Qore offers a simple API to build your application menu.

Your GlobalsController is responsible to provide application wide data and will be retrieved on page load. This data contains resources, tenants, modules, plugins, the menu and more depending on your application.

You can build your menu via the Menu::make() method:

use Qore\System\Menu\Menu;

class GlobalsController extends Controller
{
public function index(): JsonResponse
{
return response()->json([
// Here
'menu' => $this->menu(),
'tenants' => TenantResource::collection(auth()->user()->tenants),
'resources' => resources(),
'modules' => array_values(app(Modules::class)->all()),
'community' => array_values(app(Plugins::class)->all()),
]);
}

private function menu(): Menu
{
return Menu::make() // ->{...}
}
}

Resources in menu

You can add a menu item for a resource:

$group->addResourceMenuItem(resource(TicketResource::class));

You can also pass a Closure to modify the menu item:

$group->addResourceMenuItem(resource(TicketResource::class), function (MenuItem $item) {
$item->setLabel('All tickets');
});

API

Every MenuItem, MenuGroup and MenuSection is contained within a MenuTab:

->tab('home'/*icon*/, function (MenuTab $tab) {
// Optional description
$tab->setDescription(__('My description'));

// Optional permissions
$tab->setPermissions(['settings management']);
});

MenuItem can exist within a MenuTab or MenuGroup:

// With optional Closure
$tab->addMenuItem(__('Home'), '/home'/*url*/, []/*permissions*/, function (MenuItem $item) {
// Optional icon
$item->setIcon('home');
});

// Without Closure
$tab->addMenuItem(__('Settings'), '/settings/mailing', ['mail management']);

// Resource menu items
$tab->addResourceMenuItem(resource('users'));

MenuGroup can be opened/closed and can contain multiple MenuItem

$tab->addMenuGroup(__('Intern'), function (MenuGroup $group) {
// Optional icon
$group->setIcon('group');

$group->addMenuItem(__('Settings'), '/settings/mailing', ['mail management']);

// ..
$group->addResourceMenuItem(resource('users'));
});

MenuSection is a simple divider item and can exist within a MenuTab:

$tab->addMenuSection(__('Menu section'), ['user management']/*icon*/)

// With optional class overrides
$tab->addMenuSection('Menu section', [], 'text-bold text-primary bg-grey-4 q-pa-sm')

Active / inactive state

Whether a menu item should be active (selected) in the menu is handled by default based on the url of the menu item. However, this could sometimes lead into problems when you have multiple menu items that have or start with the same url.

For this reason, you have 2 methods available on a menu item:

$group->addResourceMenuItem(resource(TicketResource::class), function (MenuItem $item) {
// When this is in the url, the menu item should get the selected state
$item->setActiveWhenIncludes(['resources/tickets'])

// When this is in the url, the menu item should not get the selected state
$item->setInactiveWhenIncludes(['bug']);
});

Multiple menu items for a resource

In some cases, you might need multiple menu items that will all go to the same Resource index page, but with applied filters. For example, you might have a TicketResource, and you want 3 menu items: All tickets, Support tickets and Bug tickets.

You can do the following for this use case:

$group->addResourceMenuItem(resource(TicketResource::class), function (MenuItem $item) {
$item->setLabel('All tickets');

// When the following keywords are in the url, don't make this item active.
$item->setInactiveWhenIncludes(['support', 'bug']);
});

$group->addResourceFilterMenuItem(
(new ScopedMenuResource(resource(TicketResource::class), 'support'))
->setLabel('Support tickets')
->setSearchColumns([
'category' => ['support']
])
// Optionally sort by a column
->setSortColumn('name')
->setSortDirection('descending')
);

$group->addResourceFilterMenuItem(
(new ScopedMenuResource(resource(TicketResource::class), 'bug'))
->setLabel('Bug tickets')
->setSearchColumns([
'category' => ['bug']
])
);

The important part here is the tableScope, which should be a unique key. This tableScope will be added in the URL, and is used to determine which menu item is active, how to deal with IndexTabs and more.

Combining with IndexTabs

When using the TicketResource example again, we also might have 3 IndexTabs for the table: All, Internal, External.

For example:

public function indexTabs(): IndexTabCollection
{
return (new IndexTabCollection(
new AllTicketsIndexTab($this->indexQuery()),
new InternalTickets($this->indexQuery()),
new ExternalTickets($this->indexQuery())
));
}
class AllTicketsIndexTab extends IndexTab
{
protected Builder $baseQuery;

public function __construct(Builder $baseQuery)
{
$this->baseQuery = $baseQuery;

if ($this->tableScope()) {
$this->baseQuery->where('category', $this->tableScope());
}
}

public function query(): Builder
{
return $this->baseQuery;
}
}
class ExternalTickets extends AllTicketsIndexTab
{
// ..

public function query(): Builder
{
return $this->baseQuery->where('is_internal', false);
}
}
class InternalTickets extends AllTicketsIndexTab
{
// ..

public function query(): Builder
{
return $this->baseQuery->where('is_internal', true);
}
}

As you can see in the AllTicketsIndexTab, the tableScope will be utilized in order to take the menu item into account. Every other tab extends this class, so this scope will be appended to all executed queries.

Custom indexQuery

In the examples above, the data that will be shown in the tables is either filtered using ->setSearchColumns or using IndexTabs.

If necessary, there is also a third option to filter data, which is to use the table_scope from the request:

public function indexQuery()
{
return parent::indexQuery()
->when(request()->get('table_scope') === TicketType::BUG->value, function($query) {
$query->where('category', TicketType::BUG->value);
});
}

Full example

The following menu ships with skeleton by default:

private function menu(): Menu
{
return Menu::make()
->tab('home', function (MenuTab $tab) {
// HOME
$tab->addMenuItem(__('Home'), '/home', [], function (MenuItem $item) {
$item->setIcon('home');
});

// INTERNAL
$tab->addMenuGroup(__('Intern'), function (MenuGroup $group) {
$group->setIcon('group');

$group->addResourceMenuItem(resource('users'));

$group->addMenuItem(__('Permissions'), '/permissions', [
'manage permissions',
'apply permissions'
]);
});

// CRM
if (module_is_active('qore/crm')) {
$tab->addMenuGroup(__('CRM'), function (MenuGroup $group) {
$group->setIcon('card_travel');

$group->addResourceMenuItem(resource(Organization::class));
$group->addResourceMenuItem(resource(Contact::class));
});
}
})
->tab('settings', function (MenuTab $tab) {
$tab->setDescription(__('Settings'));

$tab->setPermissions(['settings management', 'module management']);

// APPLICATION SETTINGS
$tab->addMenuGroup(__('Application'), function (MenuGroup $group) {
$group->setIcon('settings_applications');

$group->addResourceMenuItem(resource(Tenant::class));
$group->addMenuItem(__('Company preferences'), '/company-preferences', ['settings management']);
$group->addMenuItem(__('Authentication'), '/settings/authentication', ['settings management']);
$group->addMenuItem(__('Variables'), '/company-variables', ['settings management']);
$group->addMenuItem(__('Serial Numbers'), '/resources/serial_numbers', ['settings management']);
});

// MODULES
$tab->addMenuGroup(__('Modules'), function (MenuGroup $group) {
$group->setIcon('view_module');

$group->addMenuItem(__('List'), '/modules', config('qore.permissions.module.index'));
});

// PLUGINS
$tab->addMenuGroup(__('Plugins'), function (MenuGroup $group) {
$group->setIcon('extension');

$group->addMenuItem(__('List'), '/community', config('qore.permissions.plugin.index'));
});

// MAILING
$tab->addMenuGroup(__('Mailing'), function (MenuGroup $group) {
$group->setIcon('forward_to_inbox');

$group->addResourceMenuItem(resource(MailTemplate::class));
$group->addResourceMenuItem(resource(MailMessage::class));
$group->addResourceMenuItem(resource(MailFooter::class));
$group->addResourceMenuItem(resource(TwigTemplate::class));
$group->addMenuItem(__('Settings'), '/settings/mailing', ['mail management']);
});
})
->tab('o_account_tree', function (MenuTab $tab) {
$tab->setDescription(__('Additional'));

$tab->addMenuGroup('Demo', function (MenuGroup $group) {
$group->setIcon('travel_explore');

$group->addMenuItem(__('Query builder'), '/demo');
});
});
}