Merge branch 'edit_menu_navbar'

pull/4701/head
Isaac Connor 2026-03-09 17:00:26 -04:00
commit 6a33a8ccb0
10 changed files with 671 additions and 144 deletions

View File

@ -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

81
db/zm_update-1.39.3.sql Normal file
View File

@ -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;

68
web/includes/MenuItem.php Normal file
View File

@ -0,0 +1,68 @@
<?php
namespace ZM;
require_once('database.php');
require_once('Object.php');
class MenuItem extends ZM_Object {
protected static $table = 'Menu_Items';
protected $defaults = array(
'Id' => 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'});
}
}

View File

@ -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
?>

View File

@ -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 '<i class="fa '.htmlspecialchars($icon).'"></i>';
} else if ($iconType == 'image') {
return '<img src="'.htmlspecialchars($icon).'" alt="" style="width:24px;height:24px;object-fit:contain;"/>';
}
return '<i class="material-icons">'.htmlspecialchars($icon).'</i>';
}
// 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 = '
<li id="' . $id . '" class="menu-item '.$activeClass.'">
<a href="' . $href . '" class="' . $classNameForTag_A . '">
<span class="menu-icon"><i class="material-icons">' . $icon . '</i></span>
<span class="menu-title">'.$itemName.'</span>
<span class="menu-icon">'.renderMenuIcon($icon, $iconType).'</span>
<span class="menu-title">'.htmlspecialchars($itemName).'</span>
</a>
</li>'.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 = '
<li class="menu-header"><span> GENERAL </span></li> ' .
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 '<ul class="nav navbar-nav align-self-start justify-content-center">';
echo getConsoleHTML();
echo getOptionsHTML();
echo getLogHTML();
echo getDevicesHTML();
echo getIntelGpuHTML();
echo getGroupsHTML($view);
echo getFilterHTML($view);
echo getCycleHTML($view);
echo getMontageHTML($view);
echo getMontageReviewHTML($view);
echo getSnapshotsHTML($view);
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo getMapHTML($view);
echo getAdditionalLinksHTML($view);
echo renderMenuItems($forLeftBar = false);
echo getHeaderFlipHTML();
echo '</ul></div><div id="accountstatus">';
echo '<ul class="nav navbar-nav justify-content-end align-self-start flex-grow-1">';
@ -669,21 +746,7 @@ function getCollapsedNavBarHTML($running, $user, $bandwidth_options, $view, $ski
<?php
if ( $user and $user->Username() ) {
echo '<ul class="navbar-nav">';
echo getConsoleHTML();
echo getOptionsHTML();
echo getLogHTML();
echo getDevicesHTML();
echo getIntelGpuHTML();
echo getGroupsHTML($view);
echo getFilterHTML($view);
echo getCycleHTML($view);
echo getMontageHTML($view);
echo getMontageReviewHTML($view);
echo getSnapshotsHTML($view);
echo getReportsHTML($view);
echo getRprtEvntAuditHTML($view);
echo getMapHTML($view);
echo getAdditionalLinksHTML($view);
echo renderMenuItems($forLeftBar = false);
echo '</ul>';
}
?>
@ -963,23 +1026,26 @@ function getNavBrandHTML() {
}
// Returns the html representing the Console menu item
function getConsoleHTML($forLeftBar = false) {
function getConsoleHTML($forLeftBar = false, $customLabel = null) {
global $user;
$result = '';
$label = $customLabel !== null ? $customLabel : 'Console';
$skipTranslate = $customLabel !== null;
if (count($user->viewableMonitorIds()) or !ZM\Monitor::find_one()) {
if ($forLeftBar) {
$result .= buildMenuItem(
$viewItemName = 'console',
$id = 'getConsoleHTML',
$itemName = 'Console',
$itemName = $label,
$href = '?view=console',
$icon = 'dashboard',
$classNameForTag_A = '',
$subMenu = ''
$subMenu = '',
$skipTranslate
);
} else {
$result .= '<li id="getConsoleHTML" class="nav-item"><a class="nav-link" href="?view=console">'.translate('Console').'</a></li>'.PHP_EOL;
$result .= '<li id="getConsoleHTML" class="nav-item"><a class="nav-link" href="?view=console">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@ -987,9 +1053,10 @@ function getConsoleHTML($forLeftBar = false) {
}
// Returns the html representing the Options menu item
function getOptionsHTML($forLeftBar = false) {
function getOptionsHTML($forLeftBar = false, $customLabel = null) {
global $zmMenu;
$result = '';
$label = $customLabel !== null ? $customLabel : translate('Options');
// Sorting order of the "Options" submenu items. If a submenu item is in the DB but is not here, it will be automatically added to the end of the list.
$categoryDisplayOrder = [
@ -1026,7 +1093,8 @@ function getOptionsHTML($forLeftBar = false) {
'privacy',
'MQTT',
'telemetry',
'version'
'version',
'menu'
]);
$zmMenu::buildSubMenuOptions($categoryDisplayOrder);
@ -1055,16 +1123,23 @@ function getOptionsHTML($forLeftBar = false) {
</div>
';
$optIcon = 'settings';
$optIconType = 'material';
if (isset($menuIconOverride)) {
$optIcon = $menuIconOverride['icon'];
$optIconType = $menuIconOverride['iconType'];
}
$result .= '
<li id="getOptionsHTML" class="menu-item sub-menu '.($view == "options" ? ' open' : '').'">
<a href="#<!--?view='.$view_.'&amp;tab=system-->">
<span class="menu-icon"><i class="material-icons">settings</i></span>
<span class="menu-title">'.translate('Options').'</span>
<span class="menu-icon">'.renderMenuIcon($optIcon, $optIconType).'</span>
<span class="menu-title">'.htmlspecialchars($label).'</span>
</a>
' . $subMenuOptions . '
</li>'.PHP_EOL;
} else {
$result .= '<li id="getOptionsHTML" class="nav-item"><a class="nav-link" href="?view=options">'.translate('Options').'</a></li>'.PHP_EOL;
$result .= '<li id="getOptionsHTML" class="nav-item"><a class="nav-link" href="?view=options">'.getNavbarIcon().htmlspecialchars($label).'</a></li>'.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 .= '<li id="getLogHTML" class="nav-item"><a class="nav-link '.$class.'" href="?view=log">'.translate('Log').'</a></li>'.PHP_EOL;
$result .= '<li id="getLogHTML" class="nav-item"><a class="nav-link '.$class.'" href="?view=log">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getDevicesHTML" class="nav-item"><a class="nav-link" href="?view=devices">'.translate('Devices').'</a></li>'.PHP_EOL;
$result .= '<li id="getDevicesHTML" class="nav-item"><a class="nav-link" href="?view=devices">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getIntelGpuHTML" class="nav-item"><a class="nav-link" href="?view=intelgpu">Intel GPU</a></li>'.PHP_EOL;
$result .= '<li id="getIntelGpuHTML" class="nav-item"><a class="nav-link" href="?view=intelgpu">'.getNavbarIcon().htmlspecialchars($label).'</a></li>'.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 .= '<li id="getGroupsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=groups">'. translate('Groups') .'</a></li>'.PHP_EOL;
$result .= '<li id="getGroupsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=groups">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getFilterHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=filter">'.translate('Filters').'</a></li>'.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&amp;cycle=true',
//$icon = 'cyclone',
$icon = 'repeat',
$classNameForTag_A = '',
$subMenu = ''
);
} else {
$result .= '<li id="getCycleHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=watch&amp;cycle=true">' .translate('Cycle'). '</a></li>'.PHP_EOL;
}
$result .= '<li id="getFilterHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=filter">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getMontageHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=montage">' .translate('Montage'). '</a></li>'.PHP_EOL;
$result .= '<li id="getMontageHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=montage">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getMontageReviewHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=montagereview' .$live. '">'.translate('MontageReview').'</a></li>'.PHP_EOL;
$result .= '<li id="getMontageReviewHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=montagereview' .$live. '">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getSnapshotsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=snapshots">' .translate('Snapshots'). '</a></li>'.PHP_EOL;
$result .= '<li id="getSnapshotsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=snapshots">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getEventsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=events">' .translate('Events'). '</a></li>'.PHP_EOL;
$result .= '<li id="getEventsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=events">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getReportsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=reports">'.translate('Reports').'</a></li>'.PHP_EOL;
$result .= '<li id="getReportsHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=reports">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getRprtEvntAuditHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=report_event_audit">'.translate('ReportEventAudit').'</a></li>'.PHP_EOL;
$result .= '<li id="getRprtEvntAuditHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=report_event_audit">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.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 .= '<li id="getMapHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=map">'.translate('Map').'</a></li>'.PHP_EOL;
$result .= '<li id="getMapHTML" class="nav-item"><a class="nav-link'.$class.'" href="?view=map">'.getNavbarIcon().htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}

View File

@ -0,0 +1,107 @@
<?php
require_once('includes/MenuItem.php');
$menuItems = ZM\MenuItem::find([], ['order' => 'SortOrder ASC']);
$canEdit = canEdit('System');
?>
<form name="menuItemsForm" method="post" action="?" enctype="multipart/form-data">
<input type="hidden" name="view" value="options"/>
<input type="hidden" name="tab" value="menu"/>
<input type="hidden" name="action" value="menuitems"/>
<div id="options">
<div class="row pb-2">
<div class="col">
<div id="contentButtons">
<?php if ($canEdit) { ?>
<button type="submit" class="btn btn-primary"><?php echo translate('Save') ?></button>
<button type="button" id="sortMenuBtn" data-on-click-this="sortMenuItems">
<i class="material-icons" title="<?php echo translate('Click and drag rows to change order') ?>">swap_vert</i>
<span class="text"><?php echo translate('Sort') ?></span>
</button>
<button type="submit" name="action" value="resetmenu" class="btn btn-warning"
onclick="return confirm('<?php echo addslashes(translate('Reset menu items to defaults?')) ?>');"
><?php echo translate('Reset') ?></button>
<?php } ?>
</div>
</div>
</div>
<div class="wrapper-scroll-table">
<div class="row">
<div class="col">
<table class="table table-striped" id="menuItemsTable">
<thead>
<tr>
<th class="text-left"><?php echo translate('Enabled') ?></th>
<th class="text-left"><?php echo translate('Name') ?></th>
<th class="text-left"><?php echo translate('Custom Label') ?></th>
<th class="text-left"><?php echo translate('Icon') ?></th>
</tr>
</thead>
<tbody id="menuItemsBody">
<?php foreach ($menuItems as $item) {
$id = $item->Id();
$effIcon = $item->effectiveIcon();
$effIconType = $item->effectiveIconType();
$hasCustomIcon = ($item->Icon() !== null && $item->Icon() !== '');
?>
<tr id="menuItem-<?php echo $id ?>">
<td>
<input type="hidden" name="items[<?php echo $id ?>][Id]" value="<?php echo $id ?>"/>
<input type="hidden" name="items[<?php echo $id ?>][SortOrder]" class="sortOrderInput" value="<?php echo $item->SortOrder() ?>"/>
<input type="checkbox" name="items[<?php echo $id ?>][Enabled]" value="1"
<?php echo $item->Enabled() ? 'checked' : '' ?>
<?php echo !$canEdit ? 'disabled' : '' ?>
/>
</td>
<td class="text-left"><?php echo htmlspecialchars(translate($item->MenuKey())) ?></td>
<td>
<input type="text" name="items[<?php echo $id ?>][Label]"
value="<?php echo htmlspecialchars($item->Label() ?? '') ?>"
placeholder="<?php echo htmlspecialchars(translate($item->MenuKey())) ?>"
<?php echo !$canEdit ? 'disabled' : '' ?>
/>
</td>
<td class="text-left">
<div class="d-flex align-items-center" style="gap:6px;">
<span class="menuIconPreview" id="iconPreview-<?php echo $id ?>">
<?php if ($effIconType == 'fontawesome') { ?>
<i class="fa <?php echo htmlspecialchars($effIcon) ?>"></i>
<?php } else if ($effIconType == 'image') { ?>
<img src="<?php echo htmlspecialchars($effIcon) ?>" style="width:24px;height:24px;object-fit:contain;" alt=""/>
<?php } else { ?>
<i class="material-icons"><?php echo htmlspecialchars($effIcon) ?></i>
<?php } ?>
</span>
<select name="items[<?php echo $id ?>][IconType]" class="form-control form-control-sm iconTypeSelect"
data-item-id="<?php echo $id ?>"
style="width:auto;display:inline-block;"
<?php echo !$canEdit ? 'disabled' : '' ?>
>
<option value="material" <?php echo $effIconType == 'material' ? 'selected' : '' ?>>Material</option>
<option value="fontawesome" <?php echo $effIconType == 'fontawesome' ? 'selected' : '' ?>>Font Awesome</option>
<option value="image" <?php echo $effIconType == 'image' ? 'selected' : '' ?>>Image</option>
<option value="none" <?php echo $effIconType == 'none' ? 'selected' : '' ?>>None</option>
</select>
<input type="text" name="items[<?php echo $id ?>][Icon]" class="form-control form-control-sm iconNameInput"
id="iconName-<?php echo $id ?>"
value="<?php echo htmlspecialchars($hasCustomIcon ? $item->Icon() : '') ?>"
placeholder="<?php echo htmlspecialchars($effIcon) ?>"
style="width:140px;<?php echo ($effIconType == 'image' || $effIconType == 'none') ? 'display:none;' : '' ?>"
<?php echo !$canEdit ? 'disabled' : '' ?>
/>
<input type="file" name="items[<?php echo $id ?>][IconFile]" class="form-control-file form-control-sm iconFileInput"
id="iconFile-<?php echo $id ?>"
accept="image/*"
style="width:200px;<?php echo $effIconType != 'image' ? 'display:none;' : '' ?>"
<?php echo !$canEdit ? 'disabled' : '' ?>
/>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>

View File

@ -267,14 +267,14 @@ echo $navbar ?>
<?php if ( ZM_WEB_LIST_THUMBS ) { ?>
<th data-sortable="false" data-field="Thumbnail" class="colThumbnail"><?php echo translate('Thumbnail') ?></th>
<?php } ?>
<th data-sortable="true" data-field="Name" class="colName"><i class="material-icons">videocam</i>&nbsp;<?php echo translate('Name') ?></th>
<th data-sortable="true" data-field="Name" class="colName"><?php echo translate('Name') ?></th>
<th data-sortable="true" data-visible="false" data-field="Manufacturer" class="colName"><?php echo translate('Manufacturer') ?></th>
<th data-sortable="true" data-visible="false" data-field="Model" class="colName"><?php echo translate('Model') ?></th>
<th data-sortable="true" data-field="Function" class="colFunction"><?php echo translate('Function') ?></th>
<?php if ( count($Servers) ) { ?>
<th data-sortable="true" data-field="Server" class="colServer"><?php echo translate('Server') ?></th>
<?php } ?>
<th data-sortable="true" data-field="Source" class="colSource"><i class="material-icons">settings</i>&nbsp;<?php echo translate('Source') ?></th>
<th data-sortable="true" data-field="Source" class="colSource"><?php echo translate('Source') ?></th>
<?php if ( $show_storage_areas ) { ?>
<th data-sortable="true" data-field="Storage" class="colStorage"><?php echo translate('Storage') ?></th>
<?php }
@ -293,13 +293,14 @@ echo $navbar ?>
: array('cnj'=>'and', 'attr'=>'Monitor')
);
parseFilter($filter);
echo '<th data-sortable="true" data-field="'.$i.'Events" class="colEvents"><a '
.(canView('Events') ? 'href="?view='.ZM_WEB_EVENTS_VIEW.'&amp;page=1'.$filter['querystring'].'">' : '')
.$eventCounts[$i]['title']
.'</a></th>'.PHP_EOL;
$eventsLink = canView('Events') ? '?view='.ZM_WEB_EVENTS_VIEW.'&amp;page=1'.$filter['querystring'] : '';
echo '<th data-sortable="true" data-field="'.$i.'Events" class="colEvents"'
.'>'
.htmlspecialchars($eventCounts[$i]['title'])
.'</th>'.PHP_EOL;
} // end foreach eventCounts
?>
<th data-sortable="true" data-field="ZoneCount" class="colZones"><a href="?view=zones"><?php echo translate('Zones') ?></a></th>
<th data-sortable="true" data-field="ZoneCount" class="colZones"><?php echo translate('Zones') ?></th>
<th data-sortable="true" data-visible="false" data-field="Sequence" class="Sequence"><?php echo translate('Sequence') ?></th>
</tr>
</thead>

View File

@ -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('<i class="material-icons">' + headerIcons[field] + '</i>&nbsp;');
}
if (field === 'ZoneCount') {
var text = inner.text();
inner.html('<a href="?view=zones">' + text + '</a>');
}
});
} // end function initPage
function sortMonitors(button) {

View File

@ -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() {

View File

@ -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();