diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 4b5e40f75..976d0c2ad 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -1348,6 +1348,37 @@ CREATE TABLE `Notifications` ( CONSTRAINT `Notifications_ibfk_1` FOREIGN KEY (`UserId`) REFERENCES `Users` (`Id`) ON DELETE CASCADE ) ENGINE=@ZM_MYSQL_ENGINE@; +-- +-- Table structure for table `Menu_Items` +-- + +DROP TABLE IF EXISTS `Menu_Items`; +CREATE TABLE `Menu_Items` ( + `Id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `MenuKey` varchar(32) NOT NULL, + `Enabled` tinyint(1) NOT NULL DEFAULT 1, + `Label` varchar(64) DEFAULT NULL, + `SortOrder` smallint NOT NULL DEFAULT 0, + PRIMARY KEY (`Id`), + UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`) +) ENGINE=@ZM_MYSQL_ENGINE@; + +INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES + ('Console', 1, 10), + ('Montage', 1, 20), + ('MontageReview', 1, 30), + ('Events', 1, 40), + ('Options', 1, 50), + ('Log', 1, 60), + ('Devices', 1, 70), + ('IntelGpu', 1, 80), + ('Groups', 1, 90), + ('Filters', 1, 100), + ('Snapshots', 1, 110), + ('Reports', 1, 120), + ('ReportEventAudit', 1, 130), + ('Map', 1, 140); + source @PKGDATADIR@/db/Object_Types.sql -- We generally don't alter triggers, we drop and re-create them, so let's keep them in a separate file that we can just source in update scripts. source @PKGDATADIR@/db/triggers.sql diff --git a/db/zm_update-1.39.3.sql b/db/zm_update-1.39.3.sql new file mode 100644 index 000000000..14c1b9b51 --- /dev/null +++ b/db/zm_update-1.39.3.sql @@ -0,0 +1,51 @@ +-- +-- Add Menu_Items table for customizable navbar/sidebar menu +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = DATABASE() + AND table_name = 'Menu_Items' + ) > 0, +"SELECT 'Table Menu_Items already exists'", +"CREATE TABLE `Menu_Items` ( + `Id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `MenuKey` varchar(32) NOT NULL, + `Enabled` tinyint(1) NOT NULL DEFAULT 1, + `Label` varchar(64) DEFAULT NULL, + `SortOrder` smallint NOT NULL DEFAULT 0, + PRIMARY KEY (`Id`), + UNIQUE KEY `Menu_Items_MenuKey_idx` (`MenuKey`) +) ENGINE=InnoDB" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- +-- Seed default menu items if table is empty +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM `Menu_Items`) > 0, +"SELECT 'Menu_Items already has data'", +"INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES + ('Console', 1, 10), + ('Montage', 1, 20), + ('MontageReview', 1, 30), + ('Events', 1, 40), + ('Options', 1, 50), + ('Log', 1, 60), + ('Devices', 1, 70), + ('IntelGpu', 1, 80), + ('Groups', 1, 90), + ('Filters', 1, 100), + ('Snapshots', 1, 110), + ('Reports', 1, 120), + ('ReportEventAudit', 1, 130), + ('Map', 1, 140)" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/web/includes/MenuItem.php b/web/includes/MenuItem.php new file mode 100644 index 000000000..2d6ad64ce --- /dev/null +++ b/web/includes/MenuItem.php @@ -0,0 +1,31 @@ + null, + 'MenuKey' => '', + 'Enabled' => 1, + 'Label' => null, + 'SortOrder' => 0, + ); + + public static function find($parameters = array(), $options = array()) { + return ZM_Object::_find(self::class, $parameters, $options); + } + + public static function find_one($parameters = array(), $options = array()) { + return ZM_Object::_find_one(self::class, $parameters, $options); + } + + public function displayLabel() { + if ($this->{'Label'} !== null && $this->{'Label'} !== '') { + return $this->{'Label'}; + } + return translate($this->{'MenuKey'}); + } +} diff --git a/web/includes/actions/options.php b/web/includes/actions/options.php index e90d3ed1c..c072e99d5 100644 --- a/web/includes/actions/options.php +++ b/web/includes/actions/options.php @@ -189,5 +189,47 @@ if ( $action == 'delete' ) { } } } +} else if ($action == 'menuitems') { + if (!canEdit('System')) { + ZM\Warning('Need System permission to edit menu items'); + } else if (isset($_REQUEST['items'])) { + require_once('includes/MenuItem.php'); + $allItems = ZM\MenuItem::find(); + foreach ($allItems as $item) { + $id = $item->Id(); + $enabled = isset($_REQUEST['items'][$id]['Enabled']) ? 1 : 0; + $label = isset($_REQUEST['items'][$id]['Label']) ? trim($_REQUEST['items'][$id]['Label']) : null; + $sortOrder = isset($_REQUEST['items'][$id]['SortOrder']) ? intval($_REQUEST['items'][$id]['SortOrder']) : $item->SortOrder(); + if ($label === '') $label = null; + $item->save([ + 'Enabled' => $enabled, + 'Label' => $label, + 'SortOrder' => $sortOrder, + ]); + } + } + $redirect = '?view=options&tab=menu'; +} else if ($action == 'resetmenu') { + if (!canEdit('System')) { + ZM\Warning('Need System permission to reset menu items'); + } else { + dbQuery('DELETE FROM Menu_Items'); + dbQuery("INSERT INTO `Menu_Items` (`MenuKey`, `Enabled`, `SortOrder`) VALUES + ('Console', 1, 10), + ('Montage', 1, 20), + ('MontageReview', 1, 30), + ('Events', 1, 40), + ('Options', 1, 50), + ('Log', 1, 60), + ('Devices', 1, 70), + ('IntelGpu', 1, 80), + ('Groups', 1, 90), + ('Filters', 1, 100), + ('Snapshots', 1, 110), + ('Reports', 1, 120), + ('ReportEventAudit', 1, 130), + ('Map', 1, 140)"); + } + $redirect = '?view=options&tab=menu'; } // end if object vs action ?> diff --git a/web/skins/classic/includes/functions.php b/web/skins/classic/includes/functions.php index d462eeae1..9c390ddc9 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -179,7 +179,7 @@ function getBodyTopHTML() { } } // end function getBodyTopHTML -function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '') { +function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '', $skipTranslate = false) { global $view; /* Highlighting the active menu section */ if ($viewItemName == 'watch') { @@ -187,40 +187,96 @@ function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameFo } else { $activeClass = $view == $viewItemName ? ' active' : ''; } - $itemName = translate($itemName); + if (!$skipTranslate) $itemName = translate($itemName); $result = ' '.PHP_EOL; return $result; } +function getMenuItemFunctions() { + return [ + 'Console' => 'getConsoleHTML', + 'Montage' => 'getMontageHTML', + 'MontageReview' => 'getMontageReviewHTML', + 'Events' => 'getEventsHTML', + 'Options' => 'getOptionsHTML', + 'Log' => 'getLogHTML', + 'Devices' => 'getDevicesHTML', + 'IntelGpu' => 'getIntelGpuHTML', + 'Groups' => 'getGroupsHTML', + 'Filters' => 'getFilterHTML', + 'Snapshots' => 'getSnapshotsHTML', + 'Reports' => 'getReportsHTML', + 'ReportEventAudit' => 'getRprtEvntAuditHTML', + 'Map' => 'getMapHTML', + ]; +} + +function getMenuItems() { + static $cached = null; + if ($cached === null) { + require_once('includes/MenuItem.php'); + $cached = ZM\MenuItem::find([], ['order' => 'SortOrder ASC']); + } + return $cached; +} + +// Functions that take only ($forLeftBar, $customLabel) - no $view parameter +function getMenuFuncsNoView() { + return ['getConsoleHTML', 'getOptionsHTML', 'getLogHTML', 'getDevicesHTML', 'getIntelGpuHTML']; +} + +function renderMenuItems($forLeftBar = false) { + global $view; + $menuItems = getMenuItems(); + $funcMap = getMenuItemFunctions(); + $result = ''; + + if (empty($menuItems)) { + // Fallback: no DB rows yet, render all in default order + foreach ($funcMap as $key => $funcName) { + $noViewFuncs = getMenuFuncsNoView(); + if (in_array($funcName, $noViewFuncs)) { + $result .= $funcName($forLeftBar); + } else { + $result .= $funcName($view, $forLeftBar); + } + } + } else { + $noViewFuncs = getMenuFuncsNoView(); + foreach ($menuItems as $item) { + if (!$item->Enabled()) continue; + $key = $item->MenuKey(); + if (!isset($funcMap[$key])) continue; + + $funcName = $funcMap[$key]; + $customLabel = ($item->Label() !== null && $item->Label() !== '') ? $item->displayLabel() : null; + + if (in_array($funcName, $noViewFuncs)) { + $result .= $funcName($forLeftBar, $customLabel); + } else { + $result .= $funcName($view, $forLeftBar, $customLabel); + } + } + } + + $result .= getAdditionalLinksHTML($view, $forLeftBar); + return $result; +} + function buildSidebarMenu() { global $view; global $user; if ( $user and $user->Username() ) { $menuForAuthUser = ' ' . - getConsoleHTML($forLeftBar = true) . - getMontageHTML($view, $forLeftBar = true) . - getCycleHTML($view, $forLeftBar = true) . - getMontageReviewHTML($view, $forLeftBar = true) . - getEventsHTML($view, $forLeftBar = true) . - getOptionsHTML($forLeftBar = true) . - getLogHTML($forLeftBar = true) . - getDevicesHTML($forLeftBar = true) . - getIntelGpuHTML($forLeftBar = true) . - getGroupsHTML($view, $forLeftBar = true) . - getFilterHTML($view, $forLeftBar = true) . - getSnapshotsHTML($view, $forLeftBar = true) . - getReportsHTML($view, $forLeftBar = true) . - getRprtEvntAuditHTML($view, $forLeftBar = true) . - getMapHTML($view, $forLeftBar = true) . - getAdditionalLinksHTML($view, $forLeftBar = true) + renderMenuItems($forLeftBar = true) ; } else { // USER IS NOT AUTHORIZED! $menuForAuthUser = ''; @@ -511,21 +567,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin) // *** Build the navigation bar menu items *** echo '
'; echo '