diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index 4b5e40f75..1ca0e2440 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -1348,6 +1348,39 @@ 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, + `Icon` varchar(128) DEFAULT NULL, + `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material', + 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..1863941ac --- /dev/null +++ b/db/zm_update-1.39.3.sql @@ -0,0 +1,81 @@ +-- +-- 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, + `Icon` varchar(128) DEFAULT NULL, + `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material', + 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; + +-- +-- Add Icon and IconType columns if they don't exist +-- + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Menu_Items' AND column_name = 'Icon' + ) > 0, +"SELECT 'Column Icon already exists'", +"ALTER TABLE `Menu_Items` ADD `Icon` varchar(128) DEFAULT NULL AFTER `SortOrder`" +)); + +PREPARE stmt FROM @s; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @s = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = DATABASE() + AND table_name = 'Menu_Items' AND column_name = 'IconType' + ) > 0, +"SELECT 'Column IconType already exists'", +"ALTER TABLE `Menu_Items` ADD `IconType` enum('material','fontawesome','image','none') NOT NULL DEFAULT 'material' AFTER `Icon`" +)); + +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..290664ea6 --- /dev/null +++ b/web/includes/MenuItem.php @@ -0,0 +1,68 @@ + null, + 'MenuKey' => '', + 'Enabled' => 1, + 'Label' => null, + 'SortOrder' => 0, + 'Icon' => null, + 'IconType' => 'material', + ); + + // Default material icons for each menu key + public static $defaultIcons = array( + 'Console' => 'dashboard', + 'Montage' => 'live_tv', + 'MontageReview' => 'movie', + 'Events' => 'event', + 'Options' => 'settings', + 'Log' => 'notification_important', + 'Devices' => 'devices_other', + 'IntelGpu' => 'memory', + 'Groups' => 'group', + 'Filters' => 'filter_alt', + 'Snapshots' => 'preview', + 'Reports' => 'report', + 'ReportEventAudit' => 'shield', + 'Map' => 'language', + ); + + public function effectiveIcon() { + if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') { + return $this->{'Icon'}; + } + return isset(self::$defaultIcons[$this->{'MenuKey'}]) ? self::$defaultIcons[$this->{'MenuKey'}] : 'menu'; + } + + public function effectiveIconType() { + if ($this->{'IconType'} == 'none') { + return 'none'; + } + if ($this->{'Icon'} !== null && $this->{'Icon'} !== '') { + return $this->{'IconType'}; + } + return 'material'; + } + + 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..600dfc68f 100644 --- a/web/includes/actions/options.php +++ b/web/includes/actions/options.php @@ -189,5 +189,95 @@ 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; + + $iconType = isset($_REQUEST['items'][$id]['IconType']) ? $_REQUEST['items'][$id]['IconType'] : $item->IconType(); + if (!in_array($iconType, ['material', 'fontawesome', 'image', 'none'])) $iconType = 'material'; + $icon = isset($_REQUEST['items'][$id]['Icon']) ? trim($_REQUEST['items'][$id]['Icon']) : $item->Icon(); + if ($icon === '') $icon = null; + + // Handle image upload + if (isset($_FILES['items']['name'][$id]['IconFile']) + && $_FILES['items']['error'][$id]['IconFile'] == UPLOAD_ERR_OK) { + $uploadDir = ZM_PATH_WEB.'/graphics/menu/'; + if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true); + + $tmpName = $_FILES['items']['tmp_name'][$id]['IconFile']; + $origName = basename($_FILES['items']['name'][$id]['IconFile']); + $ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION)); + $allowedExts = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico']; + if (in_array($ext, $allowedExts)) { + // Validate it's actually an image (except SVG/ICO) + if ($ext == 'svg' || $ext == 'ico' || getimagesize($tmpName) !== false) { + $safeName = 'menu_'.$id.'_'.time().'.'.$ext; + $destPath = $uploadDir.$safeName; + if (move_uploaded_file($tmpName, $destPath)) { + // Remove old uploaded icon if it exists + if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) { + unlink(ZM_PATH_WEB.'/'.$item->Icon()); + } + $icon = 'graphics/menu/'.$safeName; + $iconType = 'image'; + } + } + } + } + + // If user cleared icon, reset to default + if ($iconType != 'image' && ($icon === null || $icon === '')) { + $icon = null; + } + + $item->save([ + 'Enabled' => $enabled, + 'Label' => $label, + 'SortOrder' => $sortOrder, + 'Icon' => $icon, + 'IconType' => $iconType, + ]); + } + } + $redirect = '?view=options&tab=menu'; +} else if ($action == 'resetmenu') { + if (!canEdit('System')) { + ZM\Warning('Need System permission to reset menu items'); + } else { + // Clean up any uploaded icon files + require_once('includes/MenuItem.php'); + $oldItems = ZM\MenuItem::find(); + foreach ($oldItems as $item) { + if ($item->IconType() == 'image' && $item->Icon() && file_exists(ZM_PATH_WEB.'/'.$item->Icon())) { + unlink(ZM_PATH_WEB.'/'.$item->Icon()); + } + } + 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..23c1fb18b 100644 --- a/web/skins/classic/includes/functions.php +++ b/web/skins/classic/includes/functions.php @@ -179,48 +179,139 @@ function getBodyTopHTML() { } } // end function getBodyTopHTML -function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '') { +function renderMenuIcon($icon, $iconType = 'material') { + if ($iconType == 'none') { + return ''; + } else if ($iconType == 'fontawesome') { + return ''; + } else if ($iconType == 'image') { + return ''; + } + return ''.htmlspecialchars($icon).''; +} + +// Returns icon HTML from the current $menuIconOverride, or empty string if not set +function getNavbarIcon() { + global $menuIconOverride; + if (!isset($menuIconOverride)) return ''; + $html = renderMenuIcon($menuIconOverride['icon'], $menuIconOverride['iconType']); + if ($html !== '') $html .= ' '; + return $html; +} + +function buildMenuItem($viewItemName, $id, $itemName, $href, $icon, $classNameForTag_A = '', $subMenu = '', $skipTranslate = false, $iconType = 'material') { global $view; + global $menuIconOverride; + + // Check for icon override from renderMenuItems + if (isset($menuIconOverride)) { + $icon = $menuIconOverride['icon']; + $iconType = $menuIconOverride['iconType']; + } + /* Highlighting the active menu section */ if ($viewItemName == 'watch') { $activeClass = ($view == $viewItemName && (isset($_REQUEST['cycle']) && $_REQUEST['cycle'] == "true")) ? ' active' : ''; } 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 { + global $menuIconOverride; + $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; + + // Set icon override for buildMenuItem/getOptionsHTML to pick up + $effIcon = $item->effectiveIcon(); + $effIconType = $item->effectiveIconType(); + $menuIconOverride = ['icon' => $effIcon, 'iconType' => $effIconType]; + + if (in_array($funcName, $noViewFuncs)) { + $result .= $funcName($forLeftBar, $customLabel); + } else { + $result .= $funcName($view, $forLeftBar, $customLabel); + } + } + $menuIconOverride = null; + } + + $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 +602,7 @@ function getNormalNavBarHTML($running, $user, $bandwidth_options, $view, $skin) // *** Build the navigation bar menu items *** echo '
'; echo '
'; + $optIcon = 'settings'; + $optIconType = 'material'; + if (isset($menuIconOverride)) { + $optIcon = $menuIconOverride['icon']; + $optIconType = $menuIconOverride['iconType']; + } + $result .= ' '.PHP_EOL; } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1072,9 +1147,11 @@ function getOptionsHTML($forLeftBar = false) { } // Returns the html representing the Log menu item -function getLogHTML($forLeftBar = false) { +function getLogHTML($forLeftBar = false, $customLabel = null) { $result = ''; - + $label = $customLabel !== null ? $customLabel : 'Log'; + $skipTranslate = $customLabel !== null; + if ( canView('System') ) { if ( ZM\logToDatabase() > ZM\Logger::NOLOG ) { $logstate = logState(); @@ -1083,18 +1160,19 @@ function getLogHTML($forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'log', $id = 'getLogHTML', - $itemName = 'Log', + $itemName = $label, $href = '?view=log', $icon = 'notification_important', $classNameForTag_A = $class, - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } } - + return $result; } @@ -1116,22 +1194,25 @@ function getLogIconHTML() { } // Returns the html representing the X10 Devices menu item -function getDevicesHTML($forLeftBar = false) { +function getDevicesHTML($forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'Devices'; + $skipTranslate = $customLabel !== null; if ( ZM_OPT_X10 && canView('Devices') ) { if ($forLeftBar) { $result .= buildMenuItem( $viewItemName = 'devices', $id = 'getDevicesHTML', - $itemName = 'Devices', + $itemName = $label, $href = '?view=devices', $icon = 'devices_other', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1139,8 +1220,10 @@ function getDevicesHTML($forLeftBar = false) { } // Returns the html representing the Intel GPU status menu item -function getIntelGpuHTML($forLeftBar = false) { +function getIntelGpuHTML($forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'Intel GPU'; + $skipTranslate = $customLabel !== null; // Only show if intel_gpu_top is available and user can view System if (canView('System')) { @@ -1151,14 +1234,15 @@ function getIntelGpuHTML($forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'intelgpu', $id = 'getIntelGpuHTML', - $itemName = 'Intel GPU', + $itemName = $label, $href = '?view=intelgpu', $icon = 'memory', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } } @@ -1167,80 +1251,63 @@ function getIntelGpuHTML($forLeftBar = false) { } // Returns the html representing the Groups menu item -function getGroupsHTML($view, $forLeftBar = false) { +function getGroupsHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; if ( !canView('Groups') ) return $result; + $label = $customLabel !== null ? $customLabel : 'Groups'; + $skipTranslate = $customLabel !== null; $class = $view == 'groups' ? ' selected' : ''; if ($forLeftBar) { $result .= buildMenuItem( $viewItemName = 'groups', $id = 'getGroupsHTML', - $itemName = 'Groups', + $itemName = $label, $href = '?view=groups', $icon = 'group', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } return $result; } // Returns the html representing the Filter menu item -function getFilterHTML($view, $forLeftBar = false) { +function getFilterHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; if ( !canView('Events') ) return $result; - + $label = $customLabel !== null ? $customLabel : 'Filters'; + $skipTranslate = $customLabel !== null; + $class = $view == 'filter' ? ' selected' : ''; if ($forLeftBar) { $result .= buildMenuItem( $viewItemName = 'filter', $id = 'getFilterHTML', - $itemName = 'Filters', + $itemName = $label, $href = '?view=filter', $icon = 'filter_alt', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; - } - - return $result; -} - -// Returns the html representing the Cycle menu item -function getCycleHTML($view, $forLeftBar = false) { - $result = ''; - - if ( canView('Stream') ) { - $class = $view == 'cycle' ? ' selected' : ''; - if ($forLeftBar) { - $result .= buildMenuItem( - $viewItemName = 'watch', - $id = 'getCycleHTML', - $itemName = 'Cycle', - $href = '?view=watch&cycle=true', - //$icon = 'cyclone', - $icon = 'repeat', - $classNameForTag_A = '', - $subMenu = '' - ); - } else { - $result .= ''.PHP_EOL; - } + $result .= ''.PHP_EOL; } return $result; } // Returns the html representing the Montage menu item -function getMontageHTML($view, $forLeftBar = false) { +function getMontageHTML($view, $forLeftBar = false, $customLabel = null) { global $user; $result = ''; + $label = $customLabel !== null ? $customLabel : 'Montage'; + $skipTranslate = $customLabel !== null; if (canView('Stream') and count($user->viewableMonitorIds())) { $class = $view == 'montage' ? ' selected' : ''; @@ -1248,14 +1315,15 @@ function getMontageHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'montage', $id = 'getMontageHTML', - $itemName = 'Montage', + $itemName = $label, $href = '?view=montage', $icon = 'live_tv', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1263,9 +1331,11 @@ function getMontageHTML($view, $forLeftBar = false) { } // Returns the html representing the MontageReview menu item -function getMontageReviewHTML($view, $forLeftBar = false) { +function getMontageReviewHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; - + $label = $customLabel !== null ? $customLabel : 'MontageReview'; + $skipTranslate = $customLabel !== null; + if ( canView('Events') ) { if ( isset($_REQUEST['filter']['Query']['terms']['attr']) ) { $terms = $_REQUEST['filter']['Query']['terms']; @@ -1287,14 +1357,15 @@ function getMontageReviewHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'montagereview', $id = 'getMontageReviewHTML', - $itemName = 'MontageReview', + $itemName = $label, $href = '?view=montagereview' .$live, $icon = 'movie', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1302,8 +1373,10 @@ function getMontageReviewHTML($view, $forLeftBar = false) { } // Returns the html representing the Montage menu item -function getSnapshotsHTML($view, $forLeftBar = false) { +function getSnapshotsHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'Snapshots'; + $skipTranslate = $customLabel !== null; if (defined('ZM_FEATURES_SNAPSHOTS') and ZM_FEATURES_SNAPSHOTS and canView('Snapshots')) { $class = $view == 'snapshots' ? ' selected' : ''; @@ -1311,14 +1384,15 @@ function getSnapshotsHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'snapshots', $id = 'getSnapshotsHTML', - $itemName = 'Snapshots', + $itemName = $label, $href = '?view=snapshots', $icon = 'preview', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1326,9 +1400,11 @@ function getSnapshotsHTML($view, $forLeftBar = false) { } // Returns the html representing the Events menu item -function getEventsHTML($view, $forLeftBar = false) { +function getEventsHTML($view, $forLeftBar = false, $customLabel = null) { global $user; $result = ''; + $label = $customLabel !== null ? $customLabel : 'Events'; + $skipTranslate = $customLabel !== null; if (canView('Events')) { $class = $view == 'events' ? ' selected' : ''; @@ -1336,22 +1412,25 @@ function getEventsHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'events', $id = 'getEventsHTML', - $itemName = 'Events', + $itemName = $label, $href = '?view=events', $icon = 'event', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } return $result; } -function getReportsHTML($view, $forLeftBar = false) { +function getReportsHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'Reports'; + $skipTranslate = $customLabel !== null; if (canView('Events')) { $class = ($view == 'reports' or $view == 'report') ? ' selected' : ''; @@ -1359,14 +1438,15 @@ function getReportsHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'reports', $id = 'getReportsHTML', - $itemName = 'Reports', + $itemName = $label, $href = '?view=reports', $icon = 'report', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1374,8 +1454,10 @@ function getReportsHTML($view, $forLeftBar = false) { } // Returns the html representing the Audit Events Report menu item -function getRprtEvntAuditHTML($view, $forLeftBar = false) { +function getRprtEvntAuditHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'ReportEventAudit'; + $skipTranslate = $customLabel !== null; if ( canView('Events') ) { $class = $view == 'report_event_audit' ? ' selected' : ''; @@ -1383,14 +1465,15 @@ function getRprtEvntAuditHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'report_event_audit', $id = 'getRprtEvntAuditHTML', - $itemName = 'ReportEventAudit', + $itemName = $label, $href = '?view=report_event_audit', $icon = 'shield', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } @@ -1398,8 +1481,10 @@ function getRprtEvntAuditHTML($view, $forLeftBar = false) { } // Returns the html representing the Audit Events Report menu item -function getMapHTML($view, $forLeftBar = false) { +function getMapHTML($view, $forLeftBar = false, $customLabel = null) { $result = ''; + $label = $customLabel !== null ? $customLabel : 'Map'; + $skipTranslate = $customLabel !== null; if (defined('ZM_OPT_USE_GEOLOCATION') and ZM_OPT_USE_GEOLOCATION) { $class = $view == 'map' ? ' selected' : ''; @@ -1407,14 +1492,15 @@ function getMapHTML($view, $forLeftBar = false) { $result .= buildMenuItem( $viewItemName = 'map', $id = 'getMapHTML', - $itemName = 'Map', + $itemName = $label, $href = '?view=map', $icon = 'language', $classNameForTag_A = '', - $subMenu = '' + $subMenu = '', + $skipTranslate ); } else { - $result .= ''.PHP_EOL; + $result .= ''.PHP_EOL; } } diff --git a/web/skins/classic/views/_options_menu.php b/web/skins/classic/views/_options_menu.php new file mode 100644 index 000000000..292fc70c6 --- /dev/null +++ b/web/skins/classic/views/_options_menu.php @@ -0,0 +1,107 @@ + 'SortOrder ASC']); +$canEdit = canEdit('System'); +?> +
+ + + +
+
+
+
+ + + + + +
+
+
+
+
+
+ + + + + + + + + + +Id(); + $effIcon = $item->effectiveIcon(); + $effIconType = $item->effectiveIconType(); + $hasCustomIcon = ($item->Icon() !== null && $item->Icon() !== ''); +?> + + + + + + + + + +
+
+
+
+
diff --git a/web/skins/classic/views/console.php b/web/skins/classic/views/console.php index 8d8e8285c..0bd0a7661 100644 --- a/web/skins/classic/views/console.php +++ b/web/skins/classic/views/console.php @@ -267,14 +267,14 @@ echo $navbar ?> - videocam  + - settings  + : array('cnj'=>'and', 'attr'=>'Monitor') ); parseFilter($filter); - echo '' : '') - .$eventCounts[$i]['title'] - .''.PHP_EOL; + $eventsLink = canView('Events') ? '?view='.ZM_WEB_EVENTS_VIEW.'&page=1'.$filter['querystring'] : ''; + echo '' + .htmlspecialchars($eventCounts[$i]['title']) + .''.PHP_EOL; } // end foreach eventCounts ?> - + diff --git a/web/skins/classic/views/js/console.js b/web/skins/classic/views/js/console.js index 31c85c658..31aacb77b 100644 --- a/web/skins/classic/views/js/console.js +++ b/web/skins/classic/views/js/console.js @@ -552,6 +552,22 @@ function initPage() { // Make the table visible after initialization table.show(); + + // Add icons to column headers after bootstrap-table init so they don't + // leak into the Columns dropdown (which uses the plain text title). + var headerIcons = {Name: 'videocam', Source: 'settings'}; + table.find('thead th').each(function() { + var field = $j(this).data('field'); + var inner = $j(this).find('.th-inner'); + if (!inner.length) return; + if (headerIcons[field]) { + inner.prepend('' + headerIcons[field] + ' '); + } + if (field === 'ZoneCount') { + var text = inner.text(); + inner.html('' + text + ''); + } + }); } // end function initPage function sortMonitors(button) { diff --git a/web/skins/classic/views/js/options.js b/web/skins/classic/views/js/options.js index 4668e1950..1f982cd4f 100644 --- a/web/skins/classic/views/js/options.js +++ b/web/skins/classic/views/js/options.js @@ -54,6 +54,19 @@ function AddNewRole(el) { window.location.assign(url); } +function sortMenuItems(button) { + if (button.classList.contains('btn-success')) { + $j('#menuItemsBody').sortable('disable'); + // Update hidden sort order fields based on new row positions + $j('#menuItemsBody tr').each(function(index) { + $j(this).find('.sortOrderInput').val((index + 1) * 10); + }); + } else { + $j('#menuItemsBody').sortable('enable'); + } + button.classList.toggle('btn-success'); +} + function initPage() { const NewStorageBtn = $j('#NewStorageBtn'); const NewServerBtn = $j('#NewServerBtn'); @@ -65,6 +78,36 @@ function initPage() { NewServerBtn.prop('disabled', !canEdit.System); $j('.bootstraptable').bootstrapTable({icons: icons}).show(); + + // Menu items tab: sortable drag-and-drop and icon type toggle + if ($j('#menuItemsBody').length) { + $j('#menuItemsBody').sortable({ + disabled: true, + axis: 'y', + cursor: 'move', + update: function() { + $j('#menuItemsBody tr').each(function(index) { + $j(this).find('.sortOrderInput').val((index + 1) * 10); + }); + } + }); + + // Toggle between text input and file input based on icon type + $j('.iconTypeSelect').on('change', function() { + const id = $j(this).data('item-id'); + const type = $j(this).val(); + if (type === 'image') { + $j('#iconName-' + id).hide(); + $j('#iconFile-' + id).show(); + } else if (type === 'none') { + $j('#iconName-' + id).hide(); + $j('#iconFile-' + id).hide(); + } else { + $j('#iconName-' + id).show(); + $j('#iconFile-' + id).hide(); + } + }); + } } $j(document).ready(function() { diff --git a/web/skins/classic/views/options.php b/web/skins/classic/views/options.php index 910de6086..5f63c3afe 100644 --- a/web/skins/classic/views/options.php +++ b/web/skins/classic/views/options.php @@ -141,7 +141,9 @@ foreach (array_map('basename', glob('skins/'.$skin.'/css/*', GLOB_ONLYDIR)) as $ include('_options_roles.php'); } else if ($tab == 'API') { include('_options_api.php'); -} // $tab == API +} else if ($tab == 'menu') { + include('_options_menu.php'); +} // $tab == API/menu else { $config = array(); $configCats = array();