diff --git a/modules/backend/assets/css/october.css b/modules/backend/assets/css/october.css index e11e70041..5703da3d0 100644 --- a/modules/backend/assets/css/october.css +++ b/modules/backend/assets/css/october.css @@ -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} diff --git a/modules/backend/assets/less/layout/mainmenu.less b/modules/backend/assets/less/layout/mainmenu.less index 5d3492c26..f4b8c37cb 100644 --- a/modules/backend/assets/less/layout/mainmenu.less +++ b/modules/backend/assets/less/layout/mainmenu.less @@ -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; diff --git a/modules/backend/classes/NavigationManager.php b/modules/backend/classes/NavigationManager.php index 6d54d9550..f88baf6c9 100644 --- a/modules/backend/classes/NavigationManager.php +++ b/modules/backend/classes/NavigationManager.php @@ -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. diff --git a/modules/backend/classes/QuickActionItem.php b/modules/backend/classes/QuickActionItem.php new file mode 100644 index 000000000..bf30cadc1 --- /dev/null +++ b/modules/backend/classes/QuickActionItem.php @@ -0,0 +1,105 @@ +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; + } +} diff --git a/modules/backend/layouts/_mainmenu.htm b/modules/backend/layouts/_mainmenu.htm index 0cf31ac6d..3327218e4 100644 --- a/modules/backend/layouts/_mainmenu.htm +++ b/modules/backend/layouts/_mainmenu.htm @@ -49,15 +49,24 @@