feat: add customizable navbar/sidebar menu via Options > Menu tab

Add Menu_Items table to store per-item enabled state, custom labels,
and sort order. Admins can enable/disable menu items, set custom
labels, and reorder via drag-and-drop in Options > Menu tab.

Refactor hardcoded get*HTML() calls in buildSidebarMenu(),
getNormalNavBarHTML(), and getCollapsedNavBarHTML() to data-driven
renderMenuItems() that reads from DB with fallback for empty table.

Remove deprecated Cycle view from menu (functionality merged into
Watch view). Add reset button to restore default menu configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Isaac ConnorandClaude Opus 4.6 committed 2026-03-09 14:37:14 -04:00
1 parent 2d3e0df07d
commit 4bd56c22d9
8 files changed
+428 -135

No files matched your search

+31
View File
@@ -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
+51
View File
@@ -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;
+31
View File
@@ -0,0 +1,31 @@
<?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,
);
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'});
}
}
+42
View File
@@ -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
?>
+178 -134
View File
@@ -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 = '
<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-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 {
$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 = '
<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 +567,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 +711,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 +991,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -987,9 +1018,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 +1058,8 @@ function getOptionsHTML($forLeftBar = false) {
'privacy',
'MQTT',
'telemetry',
'version'
'version',
'menu'
]);
$zmMenu::buildSubMenuOptions($categoryDisplayOrder);
@@ -1059,12 +1092,12 @@ function getOptionsHTML($forLeftBar = false) {
<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-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">'.htmlspecialchars($label).'</a></li>'.PHP_EOL;
}
}
@@ -1072,9 +1105,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 +1118,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
}
return $result;
}
@@ -1116,22 +1152,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1139,8 +1178,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 +1192,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">'.htmlspecialchars($label).'</a></li>'.PHP_EOL;
}
}
}
@@ -1167,80 +1209,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">'.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">'.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 +1273,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1263,9 +1289,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 +1315,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. '">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1302,8 +1331,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 +1342,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1326,9 +1358,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 +1370,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">'.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 +1396,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1374,8 +1412,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 +1423,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
@@ -1398,8 +1439,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 +1450,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">'.htmlspecialchars($skipTranslate ? $label : translate($label)).'</a></li>'.PHP_EOL;
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
require_once('includes/MenuItem.php');
$menuItems = ZM\MenuItem::find([], ['order' => 'SortOrder ASC']);
$canEdit = canEdit('System');
?>
<form name="menuItemsForm" method="post" action="?">
<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>
</tr>
</thead>
<tbody id="menuItemsBody">
<?php foreach ($menuItems as $item) { ?>
<tr id="menuItem-<?php echo $item->Id() ?>">
<td>
<input type="hidden" name="items[<?php echo $item->Id() ?>][Id]" value="<?php echo $item->Id() ?>"/>
<input type="hidden" name="items[<?php echo $item->Id() ?>][SortOrder]" class="sortOrderInput" value="<?php echo $item->SortOrder() ?>"/>
<input type="checkbox" name="items[<?php echo $item->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 $item->Id() ?>][Label]"
value="<?php echo htmlspecialchars($item->Label() ?? '') ?>"
placeholder="<?php echo htmlspecialchars(translate($item->MenuKey())) ?>"
<?php echo !$canEdit ? 'disabled' : '' ?>
/>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
+27
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,20 @@ function initPage() {
NewServerBtn.prop('disabled', !canEdit.System);
$j('.bootstraptable').bootstrapTable({icons: icons}).show();
// Menu items tab: sortable drag-and-drop
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);
});
}
});
}
}
$j(document).ready(function() {
+3 -1
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();