Add support for defining quick actions in the Backend's main nav (#5344)

Plugins now have the ability to define quick actions through a "registerQuickActions" method, which follows the same configuration as the "registerNavigation" method. It is still recommended and preferred that most plugin functionality be defined in their own main menu items, but this will allow a plugin to easily define a shortcut (or remove one).
This commit is contained in:
Ben Thomson 2020-11-10 12:53:17 +08:00 committed by GitHub
parent f18769e282
commit 50816a9556
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 336 additions and 30 deletions

View File

@ -679,9 +679,10 @@ nav#layout-mainmenu .toolbar-item:before {left:-12px}
nav#layout-mainmenu .toolbar-item:after {right:-12px}
nav#layout-mainmenu .toolbar-item.scroll-active-before:before {color:#fff}
nav#layout-mainmenu .toolbar-item.scroll-active-after:after {color:#fff}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview {margin:0 0 0 21px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview i {font-size:20px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-preview a {position:relative;padding:0 10px;top:-1px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action {margin:0}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action:first-child {margin-left:21px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action i {font-size:20px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-quick-action a {position:relative;padding:0 10px;top:-1px}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account {margin-right:0}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account >a {padding:0 15px 0 10px;font-size:13px;position:relative}
nav#layout-mainmenu ul.mainmenu-toolbar li.mainmenu-account.highlight >a {z-index:600}
@ -706,8 +707,8 @@ nav#layout-mainmenu ul li .mainmenu-accountmenu li:first-child a:active:after {c
nav#layout-mainmenu ul li .mainmenu-accountmenu li.divider {height:1px;width:100%;background-color:#e0e0e0}
nav#layout-mainmenu.navbar-mode-inline,
nav#layout-mainmenu.navbar-mode-inline_no_icons {height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-preview a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-preview a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-quick-action a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-quick-action a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-toolbar li.mainmenu-account >a,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-toolbar li.mainmenu-account >a {height:60px;line-height:60px}
nav#layout-mainmenu.navbar-mode-inline ul li .mainmenu-accountmenu,
@ -730,7 +731,7 @@ nav#layout-mainmenu.navbar-mode-inline ul.mainmenu-nav li:last-child,
nav#layout-mainmenu.navbar-mode-inline_no_icons ul.mainmenu-nav li:last-child {margin-right:0}
nav#layout-mainmenu.navbar-mode-inline_no_icons .nav-icon {display:none !important}
nav#layout-mainmenu.navbar-mode-tile {height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-preview a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-quick-action a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-toolbar li.mainmenu-account >a {height:78px;line-height:78px}
nav#layout-mainmenu.navbar-mode-tile ul li .mainmenu-accountmenu {top:88px}
nav#layout-mainmenu.navbar-mode-tile ul.mainmenu-nav li a {position:relative;width:65px;height:65px}
@ -749,14 +750,14 @@ nav#layout-mainmenu .menu-toggle .menu-toggle-title {margin-left:10px}
nav#layout-mainmenu .menu-toggle:hover .menu-toggle-icon {opacity:1}
body.mainmenu-open nav#layout-mainmenu .menu-toggle-icon {opacity:1}
nav#layout-mainmenu.navbar-mode-collapse {padding-left:0;height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-preview a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-quick-action a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px}
nav#layout-mainmenu.navbar-mode-collapse ul li .mainmenu-accountmenu {top:55px}
nav#layout-mainmenu.navbar-mode-collapse ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0}
nav#layout-mainmenu.navbar-mode-collapse ul li .mainmenu-accountmenu:after {right:13px}
nav#layout-mainmenu.navbar-mode-collapse ul.nav {display:none}
nav#layout-mainmenu.navbar-mode-collapse .menu-toggle {display:inline-block;color:#fff !important}
@media (max-width:769px) {nav#layout-mainmenu.navbar {padding-left:0;height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-preview a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu {top:55px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0 }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu:after {right:13px }nav#layout-mainmenu.navbar ul.nav {display:none }nav#layout-mainmenu.navbar .menu-toggle {display:inline-block;color:#fff !important }}
@media (max-width:769px) {nav#layout-mainmenu.navbar {padding-left:0;height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-quick-action a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {height:45px;line-height:45px }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu {top:55px }nav#layout-mainmenu.navbar ul.mainmenu-toolbar li.mainmenu-account >a {padding-right:0 }nav#layout-mainmenu.navbar ul li .mainmenu-accountmenu:after {right:13px }nav#layout-mainmenu.navbar ul.nav {display:none }nav#layout-mainmenu.navbar .menu-toggle {display:inline-block;color:#fff !important }}
.mainmenu-collapsed {position:absolute;height:100%;top:0;left:0;margin:0;background:#000}
.mainmenu-collapsed >div {display:block;height:100%}
.mainmenu-collapsed >div ul.mainmenu-nav li a {position:relative;width:65px;height:65px}

View File

@ -47,7 +47,7 @@ body.mainmenu-open {
height: @height;
ul.mainmenu-toolbar {
li.mainmenu-preview {
li.mainmenu-quick-action {
a {
height: @height;
line-height: @height;
@ -191,8 +191,12 @@ nav#layout-mainmenu {
//
ul.mainmenu-toolbar {
li.mainmenu-preview {
margin: 0 0 0 21px;
li.mainmenu-quick-action {
margin: 0;
&:first-child {
margin-left: 21px;
}
i {
font-size: 20px;

View File

@ -28,6 +28,11 @@ class NavigationManager
*/
protected $items;
/**
* @var QuickActionItem[] List of registered quick actions.
*/
protected $quickActions;
protected $contextSidenavPartials = [];
protected $contextOwner;
@ -54,6 +59,9 @@ class NavigationManager
*/
protected function loadItems()
{
$this->items = [];
$this->quickActions = [];
/*
* Load module items
*/
@ -68,11 +76,18 @@ class NavigationManager
foreach ($plugins as $id => $plugin) {
$items = $plugin->registerNavigation();
if (!is_array($items)) {
$quickActions = $plugin->registerQuickActions();
if (!is_array($items) && !is_array($quickActions)) {
continue;
}
$this->registerMenuItems($id, $items);
if (is_array($items)) {
$this->registerMenuItems($id, $items);
}
if (is_array($quickActions)) {
$this->registerQuickActions($id, $quickActions);
}
}
/**
@ -91,17 +106,21 @@ class NavigationManager
Event::fire('backend.menu.extendItems', [$this]);
/*
* Sort menu items
* Sort menu items and quick actions
*/
uasort($this->items, static function ($a, $b) {
return $a->order - $b->order;
});
uasort($this->quickActions, static function ($a, $b) {
return $a->order - $b->order;
});
/*
* Filter items user lacks permission for
* Filter items and quick actions that the user lacks permission for
*/
$user = BackendAuth::getUser();
$this->items = $this->filterItemPermissions($user, $this->items);
$this->quickActions = $this->filterItemPermissions($user, $this->quickActions);
foreach ($this->items as $item) {
if (!$item->sideMenu || !count($item->sideMenu)) {
@ -183,10 +202,6 @@ class NavigationManager
*/
public function registerMenuItems($owner, array $definitions)
{
if (!$this->items) {
$this->items = [];
}
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
@ -319,7 +334,7 @@ class NavigationManager
$this->items[$itemKey]->addSideMenuItem($item);
return true;
}
/**
* Remove multiple side menu items
*
@ -361,10 +376,14 @@ class NavigationManager
*/
public function listMainMenuItems()
{
if ($this->items === null) {
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->items === null) {
return [];
}
foreach ($this->items as $item) {
if ($item->badge) {
$item->counter = (string) $item->badge;
@ -444,6 +463,137 @@ class NavigationManager
return $items;
}
/**
* Registers quick actions in the main navigation.
*
* Quick actions are single purpose links displayed to the left of the user menu in the
* backend main navigation.
*
* The argument is an array of the quick action items. The array keys represent the
* quick action item codes, specific for the plugin/module. Each element in the
* array should be an associative array with the following keys:
* - label - specifies the action label localization string key, used as a tooltip, required.
* - icon - an icon name from the Font Awesome icon collection, required if iconSvg is unspecified.
* - iconSvg - a custom SVG icon to use for the icon, required if icon is unspecified.
* - url - the back-end relative URL the quick action item should point to, required.
* - permissions - an array of permissions the back-end user should have, optional.
* The item will be displayed if the user has any of the specified permissions.
* - order - a position of the item in the menu, optional.
*
* @param string $owner Specifies the quick action items owner plugin or module in the format Author.Plugin.
* @param array $definitions An array of the quick action item definitions.
* @return void
* @throws SystemException If the validation of the quick action configuration fails
*/
public function registerQuickActions($owner, array $definitions)
{
$validator = Validator::make($definitions, [
'*.label' => 'required',
'*.icon' => 'required_without:*.iconSvg',
'*.url' => 'required'
]);
if ($validator->fails()) {
$errorMessage = 'Invalid quick action item detected in ' . $owner . '. Contact the plugin author to fix (' . $validator->errors()->first() . ')';
if (Config::get('app.debug', false)) {
throw new SystemException($errorMessage);
}
Log::error($errorMessage);
}
$this->addQuickActionItems($owner, $definitions);
}
/**
* Dynamically add an array of quick action items
*
* @param string $owner
* @param array $definitions
* @return void
*/
public function addQuickActionItems($owner, array $definitions)
{
foreach ($definitions as $code => $definition) {
$this->addQuickActionItem($owner, $code, $definition);
}
}
/**
* Dynamically add a single quick action item
*
* @param string $owner
* @param string $code
* @param array $definition
* @return void
*/
public function addQuickActionItem($owner, $code, array $definition)
{
$itemKey = $this->makeItemKey($owner, $code);
if (isset($this->quickActions[$itemKey])) {
$definition = array_merge((array) $this->quickActions[$itemKey], $definition);
}
$item = array_merge($definition, [
'code' => $code,
'owner' => $owner
]);
$this->quickActions[$itemKey] = QuickActionItem::createFromArray($item);
}
/**
* Gets the instance of a specified quick action item.
*
* @param string $owner
* @param string $code
* @return QuickActionItem
* @throws SystemException
*/
public function getQuickActionItem(string $owner, string $code)
{
$itemKey = $this->makeItemKey($owner, $code);
if (!array_key_exists($itemKey, $this->quickActions)) {
throw new SystemException('No quick action item found with key ' . $itemKey);
}
return $this->quickActions[$itemKey];
}
/**
* Removes a single quick action item
*
* @param $owner
* @param $code
* @return void
*/
public function removeQuickActionItem($owner, $code)
{
$itemKey = $this->makeItemKey($owner, $code);
unset($this->quickActions[$itemKey]);
}
/**
* Returns a list of quick action items.
*
* @return array
* @throws SystemException
*/
public function listQuickActionItems()
{
if ($this->items === null && $this->quickActions === null) {
$this->loadItems();
}
if ($this->quickActions === null) {
return [];
}
return $this->quickActions;
}
/**
* Sets the navigation context.
* The function sets the navigation owner, main menu item code and the side menu item code.

View File

@ -0,0 +1,105 @@
<?php namespace Backend\Classes;
/**
* Class QuickActionItem
*
* @package Backend\Classes
*/
class QuickActionItem
{
/**
* @var string
*/
public $code;
/**
* @var string
*/
public $owner;
/**
* @var string
*/
public $label;
/**
* @var null|string
*/
public $icon;
/**
* @var null|string
*/
public $iconSvg;
/**
* @var string
*/
public $url;
/**
* @var int
*/
public $order = -1;
/**
* @var array
*/
public $attributes = [];
/**
* @var array
*/
public $permissions = [];
/**
* @param null|string|int $attribute
* @param null|string|array $value
*/
public function addAttribute($attribute, $value)
{
$this->attributes[$attribute] = $value;
}
public function removeAttribute($attribute)
{
unset($this->attributes[$attribute]);
}
/**
* @param string $permission
* @param array $definition
*/
public function addPermission(string $permission, array $definition)
{
$this->permissions[$permission] = $definition;
}
/**
* @param string $permission
* @return void
*/
public function removePermission(string $permission)
{
unset($this->permissions[$permission]);
}
/**
* @param array $data
* @return static
*/
public static function createFromArray(array $data)
{
$instance = new static();
$instance->code = $data['code'];
$instance->owner = $data['owner'];
$instance->label = $data['label'];
$instance->url = $data['url'];
$instance->icon = $data['icon'] ?? null;
$instance->iconSvg = $data['iconSvg'] ?? null;
$instance->attributes = $data['attributes'] ?? $instance->attributes;
$instance->permissions = $data['permissions'] ?? $instance->permissions;
$instance->order = $data['order'] ?? $instance->order;
return $instance;
}
}

View File

@ -49,15 +49,24 @@
</div>
<div class="toolbar-item toolbar-item-account">
<ul class="mainmenu-toolbar">
<li class="mainmenu-preview with-tooltip">
<a
href="<?= Url::to('/') ?>"
target="_blank"
rel="noopener noreferrer"
title="<?= e(trans('backend::lang.tooltips.preview_website')) ?>">
<i class="icon-crosshairs"></i>
</a>
</li>
<?php foreach (BackendMenu::listQuickActionItems() as $item): ?>
<li class="mainmenu-quick-action with-tooltip">
<a
href="<?= $item->url ?>"
title="<?= e(trans($item->label)) ?>"
<?= Html::attributes($item->attributes) ?>
>
<?php if ($item->iconSvg): ?>
<img
src="<?= Url::asset($item->iconSvg) ?>"
class="svg-icon" loading="lazy" width="20" height="20" />
<?php endif ?>
<i class="<?= $item->iconSvg ? 'svg-replace' : null ?> <?= $item->icon ?>"></i>
</a>
</li>
<?php endforeach ?>
<li class="mainmenu-account with-tooltip">
<a
href="javascript:;" onclick="$.oc.layout.toggleAccountMenu(this)"

View File

@ -1,6 +1,7 @@
<?php namespace Cms;
use App;
use Url;
use Lang;
use File;
use Event;
@ -171,6 +172,19 @@ class ServiceProvider extends ModuleServiceProvider
]
]
]);
$manager->registerQuickActions('October.Cms', [
'preview' => [
'label' => 'backend::lang.tooltips.preview_website',
'icon' => 'icon-crosshairs',
'url' => Url::to('/'),
'order' => 10,
'attributes' => [
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
],
]);
});
}

View File

@ -120,6 +120,29 @@ class PluginBase extends ServiceProviderBase
}
}
/**
* Registers back-end quick actions for this plugin.
*
* @return array
*/
public function registerQuickActions()
{
$configuration = $this->getConfigurationFromYaml();
if (array_key_exists('quickActions', $configuration)) {
$quickActions = $configuration['quickActions'];
if (is_array($quickActions)) {
array_walk_recursive($quickActions, function (&$item, $key) {
if ($key === 'url') {
$item = Backend::url($item);
}
});
}
return $quickActions;
}
}
/**
* Registers any back-end permissions used by this plugin.
*