diff --git a/lib/zin/func.php b/lib/zin/func.php index 64a82632fe..ef0c7ba69d 100644 --- a/lib/zin/func.php +++ b/lib/zin/func.php @@ -134,6 +134,7 @@ function detailSide(): detailSide {return createWg('detailSide', func_get_args() function detailBody(): detailBody {return createWg('detailBody', func_get_args());} function detailForm(): detailForm {return createWg('detailForm', func_get_args());} function echarts(): echarts {return createWg('echarts', func_get_args());} +function graph(): graph {return createWg('graph', func_get_args());} function popovers(): popovers {return createWg('popovers', func_get_args());} function backBtn(): backBtn {return createWg('backBtn', func_get_args());} function collapseBtn(): collapseBtn {return createWg('collapseBtn', func_get_args());} diff --git a/lib/zin/wg/graph/v1.php b/lib/zin/wg/graph/v1.php new file mode 100644 index 0000000000..44e5eeb367 --- /dev/null +++ b/lib/zin/wg/graph/v1.php @@ -0,0 +1,26 @@ +prop(array('type', 'width', 'height', 'responsive')); + + return zui::graph( + set::_id('zin_graph_' . uniqid()), + set::type($type), + set::responsive($responsive), + set::_style(array('width' => is_int($width) ? "{$width}px" : $width, 'height' => is_int($height) ? "{$height}px" : $height)), + set($this->getRestProps()), + ); + } +} diff --git a/lib/zin/wg/mainnavbar/js/v1.js b/lib/zin/wg/mainnavbar/js/v1.js new file mode 100644 index 0000000000..81d5340200 --- /dev/null +++ b/lib/zin/wg/mainnavbar/js/v1.js @@ -0,0 +1,211 @@ +/** + * @type {string[]} + */ +const allMainNavbarItemNames = []; + +/** + * @type {Map} + */ +const allMainNavbarItemMap = new Map(); + +$(document).ready( + function() + { + allMainNavbarItemNames.length = 0; + allMainNavbarItemMap.clear(); + + if (typeof allMainNavbarItems !== 'undefined') + { + for(const item of allMainNavbarItems) + { + allMainNavbarItemNames.push(item['data-id']); + allMainNavbarItemMap.set(item['data-id'], item); + } + } + } +); + +/** + * Get current main navbar items data. + * + * @returns {Array<{name: string; order: number;}>} + */ +function getCurrentMainNavbarItems() +{ + const items = []; + const $nav = $('#mainNavbar .nav'); + $nav.children().each( + function(index, element) + { + const $elm = $(element); + $a = $elm.find('a'); + items.push( + { + name: $a.attr('data-id'), + order: index * 5 + } + ); + } + ); + return items; +} + +/** + * Generate main menu nav items to be added. + * + * @param {Cash} $item + * @param {(name: string) => void} onClick + * @returns {Array<{text: string; onClick: () => void;}>} + */ +function generateAddMainNavbarItems($item, onClick) +{ + const items = []; + const allMainNavbarItemIDSet = new Set(allMainNavbarItemMap.keys()); + const curMainNavbarItems = getCurrentMainNavbarItems(); + for(const {name} of curMainNavbarItems) + { + allMainNavbarItemIDSet.delete(name); + } + + if(allMainNavbarItemIDSet.size === 0) return items; + for(const name of allMainNavbarItemIDSet) + { + const item = allMainNavbarItemMap.get(name); + items.push( + { + text: item.text, + onClick: () => { + onClick(name), + saveMainNavbarToServer($item); + } + } + ); + } + return items; +} + +/** + * Checks whether the current navbar item can be hidden. + * + * @param {Cash} $item + * @returns {boolean} + */ +function canHideCurrentNavbar($item) +{ + if($item.is('.active')) return false; + + const $navbarActiveItem = $('#navbar .nav .active'); + if($item.attr('href') === $navbarActiveItem.attr('href')) return false; + + return true; +} + +$(document).on( + 'contextmenu', + '#mainNavbar .nav-item > a', + function(event) + { + const $item = $(this); + const $nav = $('#mainNavbar .nav'); + const isMoving = $nav.is('[z-use-sortable]'); + const hideDisabled = !canHideCurrentNavbar($item); + const $li = $item.closest('li'); + const itemsToAdded = generateAddMainNavbarItems($item, (name) => { + const item = allMainNavbarItemMap.get(name); + const $a = $('') + .attr('href', item.url) + .attr('data-id', item['data-id']) + .attr('data-app', item['data-app']) + .append(`${item.text}`); + if(item.badge) + { + $a.append(`${item.badge.text}`); + } + const $navItem = $(''); + $navItem.append($a); + $li.after($navItem); + }); + const items = [ + isMoving + ? { + text: langData.save, + onClick: () => { + $item.closest('.nav').zui().destroy(); + saveMainNavbarToServer($item); + } + } + : { + text: langData.sort, + onClick: () => { + const sortable = new zui.Sortable( + '#mainNavbar .nav', + { + animation: 150, + ghostClass: 'bg-primary-pale', + onSort: () => { + saveMainNavbarToServer($item); + } + } + ); + } + }, + { + text: langData.hide, + disabled: hideDisabled, + onClick: hideDisabled + ? null + : () => { + $li.remove(); + saveMainNavbarToServer($item); + } + }, + itemsToAdded.length === 0 + ? { + text: langData.add, + disabled: true, + } + : { + text: langData.add, + items: itemsToAdded, + }, + { + text: langData.restore, + onClick: () => { + restoreMainNavbarToServer($item, { + onSuccess() { + loadCurrentPage('#mainNavbar'); + } + }); + } + } + + ]; + + zui.ContextMenu.show( + { + hideOthers: true, + element: $item[0], + placement: 'bottom-start', + items: items, + event: event, + onClickItem: (info) => info.event.preventDefault() + } + ); + event.preventDefault(); + } +); + +function saveMainNavbarToServer($item) +{ + const items = getCurrentMainNavbarItems(); + const menu = $item.data('group'); + const url = $.createLink('custom', 'ajaxSetMenu'); + $.ajaxSubmit({url, data: {menu, items: JSON.stringify(items)}}); +} + +function restoreMainNavbarToServer($item, options = {}) +{ + const url = $.createLink('custom', 'ajaxRestoreMenu'); + const menu = $item.data('group'); + $.ajaxSubmit({url, data: {menu}, ...options}); +} \ No newline at end of file diff --git a/lib/zin/wg/mainnavbar/v1.php b/lib/zin/wg/mainnavbar/v1.php index 608aa65afa..58a5bdd26f 100644 --- a/lib/zin/wg/mainnavbar/v1.php +++ b/lib/zin/wg/mainnavbar/v1.php @@ -63,9 +63,19 @@ class mainNavbar extends nav #mainNavbar .main-navbar-left #switcher .icon-angle-right {display: none;} #mainNavbar .main-navbar-left #switcher .caret {color: rgb(var(--color-link-hover-rgb));} #mainNavbar .main-navbar-left #switcher .text {color: rgb(var(--color-primary-500-rgb));} + #mainNavbar .nav[z-use-sortable] > li:hover {cursor: grab !important;} + #mainNavbar .nav[z-use-sortable] > li > a:hover {cursor: grab !important;} CSS; } + public static function getPageJS(): ?string + { + global $lang, $app; + $app->loadLang('index'); + jsVar('langData', $lang->index->dock); + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + protected function created() { global $app; @@ -102,10 +112,12 @@ class mainNavbar extends nav $item = array(); $link = $menuItem['link']; $name = $menuItem['name']; - $item['text'] = $menuItem['text']; - $item['url'] = commonModel::createMenuLink((object)$menuItem, $app->tab); - $item['data-id'] = $name; - $item['data-app'] = $app->tab; + $item['text'] = $menuItem['text']; + $item['url'] = commonModel::createMenuLink((object)$menuItem, $app->tab); + $item['hidden'] = !empty($menuItem['hidden']); + $item['data-id'] = $name; + $item['data-app'] = $app->tab; + $item['data-group'] = $app->tab . '-' . $activeMenu; $active = ''; if($activeItem && $activeItem == $name) @@ -184,6 +196,9 @@ class mainNavbar extends nav } } + jsVar('allMainNavbarItems', $items); + $items = array_filter($items, function($item) { return empty($item['hidden']); }); + $this->setProp('items', $items); } } diff --git a/lib/zin/wg/navbar/js/v1.js b/lib/zin/wg/navbar/js/v1.js new file mode 100644 index 0000000000..60d00dac1d --- /dev/null +++ b/lib/zin/wg/navbar/js/v1.js @@ -0,0 +1,271 @@ +/** + * @type {string[]} + */ +const allNavbarItemNames = []; + +/** + * @type {Map} + */ +const allNavbarItemMap = new Map(); + +$(document).ready( + function() + { + allNavbarItemNames.length = 0; + allNavbarItemMap.clear(); + + for(const item of allNavbarItems) + { + if(item.type === 'divider') + { + allNavbarItemNames.push('divider'); + continue; + } + + const name = item['data-id'] || item.id; + + allNavbarItemNames.push(name); + allNavbarItemMap.set(name, item); + } + } +); + +/** + * Get current navbar items data. + * + * @returns {Array<{name: string; order: number;}>} + */ +function getCurrentNavbarItems() +{ + const items = []; + const $nav = $('#navbar .nav'); + $nav.children().each( + function(index, element) + { + const $elm = $(element) + const $a = $elm.find('a'); + items.push( + { + name: $elm.is('.nav-divider') ? 'divider' : ($a.attr('data-id') || $a.attr('id')), + order: index * 5 + } + ); + } + ); + return items; +} + +/** + * Generate navbar items to be added. + * @param {Cash} $item + * @param {(item: string) => void} onClick click handler of navbar item. + * @returns {Array<{text: string; onClick: () => void;}>} + */ +function generateAddNavbarItems($item, onClick) +{ + const items = canAddDivider($item) + ? [{ + text: langData.divider, + onClick: () => { + onClick('divider'); + saveNavbarToServer(); + } + }] + : []; + + const allNavbarItemIDSet = new Set(allNavbarItemMap.keys()); + const curNavbarItems = getCurrentNavbarItems(); + for(const {name} of curNavbarItems) + { + if(name === 'divider') continue; + allNavbarItemIDSet.delete(name); + } + + if(allNavbarItemIDSet.size === 0) return items; + for(const name of allNavbarItemIDSet) + { + const item = allNavbarItemMap.get(name); + items.push( + { + text: item.text, + onClick: () => { + onClick(name); + saveNavbarToServer(); + } + } + ); + } + return items; +} + +/** + * Checks whether the current navbar item can be hidden. + * + * @param {Cash} $item + * @returns {boolean} + */ +function canHideCurrentNavbar($item) +{ + if($item.is('.nav-divider')) return true; + if($item.is('.active')) return false; + + const app = $.apps.getLastApp(); + const appDefaultUrl = app.url; + const itemUrl = $item.attr('href'); + if(itemUrl === appDefaultUrl) return false; + return true; +} + +/** + * Get menu name by items and app. + * + * @param {string} app + * @returns {string} + */ +function getMenuName(app) +{ + if(typeof projectModel !== 'undefined') + { + return `project-${projectModel}`; + } + + if(isHomeMenu) + { + return `${app}-home`; + } + + if(app == 'admin') + { + return `admin-${adminMenuKey}`; + } + return app; +} + +$(document).on( + 'contextmenu', + '#navbar .nav-item:not(.nav-dropdown) > a, #navbar .nav-divider', + function(event) + { + const $item = $(this); + const $nav = $('#navbar .nav'); + const isMoving = $nav.is('[z-use-sortable]'); + const hideDisabled = !canHideCurrentNavbar($item); + const $li = $item.closest('li'); + const toAddedItems = generateAddNavbarItems($item, (name) => { + if(name === 'divider') return $li.after(''); + + const item = allNavbarItemMap.get(name); + const $a = $('') + .attr('href', item.url) + .attr('target', item.target) + .attr('data-id', item['data-id']) + .append(`${item.text}`); + if(item.class) $a.attr('class', item.class); + const $navItem = $(''); + $navItem.append($a); + $li.after($navItem); + saveNavbarToServer(); + }); + const items = [ + isMoving + ? { + text: langData.save, + onClick: () => { + $item.closest('.nav').zui().destroy(); + saveNavbarToServer(); + } + } + : { + text: langData.sort, + onClick: () => { + const sortable = new zui.Sortable( + '#navbar .nav', + { + animation: 150, + ghostClass: 'bg-primary-pale', + onSort: () => { + saveNavbarToServer(); + } + } + ); + } + }, + { + text: langData.hide, + disabled: hideDisabled, + onClick: hideDisabled + ? null + : () => { + $li.remove(); + saveNavbarToServer(); + } + }, + toAddedItems.length === 0 + ? { + text: langData.add, + disabled: true, + } + : { + text: langData.add, + items: toAddedItems, + }, + { + text: langData.restore, + onClick: () => { + restoreNavbarToServer({ + onSuccess() { + loadCurrentPage('#navbar'); + } + }); + } + } + ]; + zui.ContextMenu.show( + { + hideOthers: true, + element: $item[0], + placement: 'bottom-start', + items: items, + event: event, + onClickItem: (info) => info.event.preventDefault() + } + ); + event.preventDefault(); + } +); + +/** + * Save navbar to server. + */ +function saveNavbarToServer() +{ + const url = $.createLink('custom', 'ajaxSetMenu'); + const items = getCurrentNavbarItems(); + const app = $.apps.getLastApp().code; + const menu = getMenuName(app); + $.ajaxSubmit({url, data: {menu, items: JSON.stringify(items)}}); +} + +/** + * Restore navbar to server. + */ +function restoreNavbarToServer(options = {}) +{ + const url = $.createLink('custom', 'ajaxRestoreMenu'); + const app = $.apps.getLastApp().code; + const menu = getMenuName(app); + $.ajaxSubmit({url, data: {menu}, ...options}); +} + +/** + * Check whether current element can add a divider. + * @param {Cash} $item + * @returns {boolean} + */ +function canAddDivider($item) +{ + $item = $item.closest('li'); + if($item.is('.divider')) return false; + if($item.next().is('.divider')) return false; + return true; +} \ No newline at end of file diff --git a/lib/zin/wg/navbar/v1.php b/lib/zin/wg/navbar/v1.php index 4d15ed409c..87b2646e3d 100644 --- a/lib/zin/wg/navbar/v1.php +++ b/lib/zin/wg/navbar/v1.php @@ -10,6 +10,23 @@ class navbar extends wg 'items?: array' ); + public static function getPageCSS(): ?string + { + return <<<'CSS' + #navbar .nav[z-use-sortable] > li:hover {cursor: grab !important;} + #navbar .nav[z-use-sortable] > li > a:hover {cursor: grab !important;} + #navbar .nav li.nav-divider.divider {border: none; width: 1px; background: currentColor; margin: 0; padding-left: var(--nav-divider-margin); padding-right: var(--nav-divider-margin); box-sizing: content-box; background-clip: content-box;} + CSS; + } + + public static function getPageJS(): ?string + { + global $lang, $app; + $app->loadLang('index'); + jsVar('langData', $lang->index->dock); + return file_get_contents(__DIR__ . DS . 'js' . DS . 'v1.js'); + } + protected function getExecutionMoreItem($executionID) { if(defined('TUTORIAL')) return; @@ -61,6 +78,7 @@ class navbar extends wg 'text' => $lang->more, 'trigger' => 'hover', 'id' => 'navbarMoreMenu', + 'data-id' => 'more', 'menu' => array('style' => array('max-width' => '300px')) ); } @@ -111,11 +129,19 @@ class navbar extends wg if(!empty($items)) return $items; global $app, $lang, $config; - if($app->tab == 'admin') $app->control->loadModel('admin')->setMenu(); + if($app->tab == 'admin') + { + $app->control->loadModel('admin')->setMenu(); + $adminMenuKey = $app->control->loadModel('admin')->getMenuKey(); + jsVar('adminMenuKey', $adminMenuKey); + } + commonModel::replaceMenuLang(); - commonModel::setMainMenu(); + $isHomeMenu = commonModel::setMainMenu(); commonModel::checkMenuVarsReplaced(); + jsVar('isHomeMenu', $isHomeMenu); + $isTutorialMode = commonModel::isTutorialMode(); $currentModule = $app->rawModule; $currentMethod = $app->rawMethod; @@ -123,15 +149,14 @@ class navbar extends wg if($isTutorialMode and defined('WIZARD_MODULE')) $currentModule = WIZARD_MODULE; if($isTutorialMode and defined('WIZARD_METHOD')) $currentMethod = WIZARD_METHOD; - $menu = \customModel::getMainMenu(); $tab = $app->tab; + $menu = \customModel::getMainMenu($isHomeMenu); $activeMenu = ''; $activeMenuID = data('activeMenuID'); $items = array(); $flows = $config->edition != 'open' ? $app->control->loadModel('my')->getFlowPairs() : array(); foreach($menu as $menuItem) { - if(isset($menuItem->hidden) and $menuItem->hidden and (!isset($menuItem->tutorial) or !$menuItem->tutorial)) continue; if(isset($menuItem->class) && strpos($menuItem->class, 'automation-menu')) { if($menuItem->divider) $items[] = array('type' => 'divider'); @@ -147,7 +172,7 @@ class navbar extends wg } if(empty($menuItem->link)) continue; - if($menuItem->divider) $items[] = array('type' => 'divider'); + if($menuItem->divider && empty($menuItem->hidden)) $items[] = array('type' => 'divider'); /* Init the these vars. */ $subModule = isset($menuItem->subModule) ? explode(',', $menuItem->subModule) : array(); @@ -164,6 +189,13 @@ class navbar extends wg $isActive = true; } + if($menuItem->link['module'] == 'project' and $menuItem->link['method'] == 'index') + { + $projectID = str_replace('project=', '', $menuItem->link['vars']); + $projectModel = $app->dbh->query("SELECT `model` FROM " . TABLE_PROJECT . " WHERE `id` = '$projectID'")->fetch(); + if($projectModel) jsVar('projectModel', $projectModel->model); + } + if($menuItem->link['module'] == 'execution' and $menuItem->link['method'] == 'more') { $executionID = $menuItem->link['vars']; @@ -274,7 +306,8 @@ class navbar extends wg 'active' => $isActive, 'target' => $target, 'data-id' => $menuItem->name, - 'data-app' => $dataApp + 'data-app' => $dataApp, + 'hidden' => (isset($menuItem->hidden) && $menuItem->hidden && (!isset($menuItem->tutorial) || !$menuItem->tutorial)) ); } } @@ -286,6 +319,9 @@ class navbar extends wg /* Set active menu to global data, make it accessible to other widgets */ data('activeMenu', $activeMenu); + jsVar('allNavbarItems', $items); + + $items = array_filter($items, function($item) { return empty($item['hidden']); }); return $items; } @@ -297,12 +333,13 @@ class navbar extends wg */ protected function build() { + $items = $this->getItems(); return h::nav ( set::id('navbar'), new nav ( - set::items($this->getItems()), + set::items($items), $this->children() ) ); diff --git a/module/admin/model.php b/module/admin/model.php index dc22478a1d..e4b59a1945 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -111,6 +111,9 @@ class adminModel extends model $menuKey = $this->getMenuKey(); if(empty($menuKey)) return; + $customKey = "admin-$menuKey"; + $customMenu = isset($this->config->customMenu->{$customKey}) ? json_decode($this->config->customMenu->{$customKey}) : array(); + $this->setSwitcher($menuKey); if(isset($this->lang->admin->menuList->$menuKey)) { @@ -146,6 +149,22 @@ class adminModel extends model } } + if($customMenu) + { + $this->lang->admin->menuList->{$menuKey}['menuOrder'] = array(); + $this->lang->admin->menuList->{$menuKey}['dividerMenu'] = ''; + $prev = ''; + foreach($customMenu as $item) + { + $this->lang->admin->menuList->{$menuKey}['menuOrder'][$item->order] = $item->name; + if($prev == 'divider') $this->lang->admin->menuList->{$menuKey}['dividerMenu'] .= ',' . $item->name; + $prev = $item->name; + } + + if($this->lang->admin->menuList->{$menuKey}['dividerMenu']) $this->lang->admin->menuList->{$menuKey}['dividerMenu'] = ',' . trim($this->lang->admin->menuList->{$menuKey}['dividerMenu']) . ','; + ksort($this->lang->admin->menuList->{$menuKey}['menuOrder']); + } + if(isset($this->lang->admin->menuList->{$menuKey}['menuOrder'])) $this->lang->admin->menuOrder = $this->lang->admin->menuList->{$menuKey}['menuOrder']; if(isset($this->lang->admin->menuList->{$menuKey}['dividerMenu'])) $this->lang->admin->dividerMenu = $this->lang->admin->menuList->{$menuKey}['dividerMenu']; if(isset($this->lang->admin->menuList->{$menuKey}['tabMenu'])) $this->lang->admin->tabMenu = $this->lang->admin->menuList->{$menuKey}['tabMenu']; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 74653e4825..ffb94133ea 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -401,6 +401,7 @@ $lang->execution->menuOrder[55] = 'build'; $lang->execution->menuOrder[60] = 'release'; $lang->execution->menuOrder[65] = 'action'; $lang->execution->menuOrder[70] = 'settings'; +$lang->execution->menuOrder[75] = 'more'; $lang->execution->menu->view['subMenu'] = new stdclass(); $lang->execution->menu->view['subMenu']->groupTask = "$lang->groupView|execution|grouptask|executionID=%s"; diff --git a/module/common/model.php b/module/common/model.php index df191a4832..04a49b5854 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -605,12 +605,13 @@ class commonModel extends model * Get main nav items list * * @param string $moduleName + * @param bool $useDefault 是否使用语言项中的默认值 * * @static * @access public * @return array */ - public static function getMainNavList(string $moduleName): array + public static function getMainNavList(string $moduleName, bool $useDefault = false): array { global $lang, $app, $config; @@ -619,7 +620,18 @@ class commonModel extends model /* Ensure user has latest rights set. */ $app->user->rights = $app->control->loadModel('user')->authorize($app->user->account); - $menuOrder = $lang->mainNav->menuOrder; + $menuOrder = array(); + $hasCustomMenu = false; + if(isset($config->customMenu->nav) && !$useDefault) + { + $items = json_decode($config->customMenu->nav); + foreach($items as $item) $menuOrder[$item->order] = $item->name; + $hasCustomMenu = true; + } + else + { + $menuOrder = $lang->mainNav->menuOrder; + } ksort($menuOrder); $items = array(); @@ -628,18 +640,29 @@ class commonModel extends model foreach($menuOrder as $key => $group) { + // 如果有自定义菜单,则直接用自定义后的divider分隔符 + if($hasCustomMenu && $group == 'divider') + { + $items[] = 'divider'; + continue; + } + if($group != 'my' && !empty($app->user->rights['acls']['views']) && !isset($app->user->rights['acls']['views'][$group])) continue; // 后台权限分组中没有给导航视图 if(!isset($lang->mainNav->$group)) continue; $nav = $lang->mainNav->$group; list($title, $currentModule, $currentMethod, $vars) = explode('|', $nav); - /* When last divider is not used in mainNav, use it next menu. */ - $printDivider = ($printDivider or ($lastItem != $key) and strpos($lang->dividerMenu, ",{$group},") !== false) ? true : false; - if($printDivider and !empty($items)) + // 没有自定义过菜单,用默认语言项中的divider分隔符 + if(!$hasCustomMenu) { - $items[] = 'divider'; - $printDivider = false; + /* When last divider is not used in mainNav, use it next menu. */ + $printDivider = ($printDivider or ($lastItem != $key) and strpos($lang->dividerMenu, ",{$group},") !== false) ? true : false; + if($printDivider and !empty($items)) + { + $items[] = 'divider'; + $printDivider = false; + } } $display = false; @@ -707,8 +730,8 @@ class commonModel extends model $items[] = $item; } - /* Fix bug 14574. */ - if(end($items) == 'divider') array_pop($items); + // 如果最后一个是分割线,则删除 + while(!empty($items) && end($items) === 'divider') { array_pop($items); } return $items; } @@ -2039,9 +2062,9 @@ eof; * * @static * @access public - * @return void + * @return bool */ - public static function setMainMenu() + public static function setMainMenu(): bool { global $app, $lang; $tab = $app->tab; @@ -2055,12 +2078,12 @@ eof; $lang->menu = isset($lang->$tab->menu) ? $lang->$tab->menu : array(); $lang->menuOrder = isset($lang->$tab->menuOrder) ? $lang->$tab->menuOrder : array(); - if(!isset($lang->$tab->homeMenu)) return; + if(!isset($lang->$tab->homeMenu)) return false; if($currentModule == $tab and $currentMethod == 'create') { $lang->menu = $lang->$tab->homeMenu; - return; + return true; } /* If the method is in homeMenu, display homeMenu. */ @@ -2073,7 +2096,7 @@ eof; if($method == $currentMethod) { $lang->menu = $lang->$tab->homeMenu; - return; + return true; } $alias = isset($menu['alias']) ? explode(',', strtolower($menu['alias'])) : array(); @@ -2081,15 +2104,17 @@ eof; if(in_array($currentMethod, $alias) && !in_array("{$currentModule}-{$currentMethod}", $exclude)) { $lang->menu = $lang->$tab->homeMenu; - return; + return true; } if(isset($menu['subModule']) and strpos(",{$menu['subModule']},", ",$currentModule,") !== false) { $lang->menu = $lang->$tab->homeMenu; - return; + return true; } } + + return false; } /** diff --git a/module/custom/control.php b/module/custom/control.php index b3befc3ba6..5209e941a9 100644 --- a/module/custom/control.php +++ b/module/custom/control.php @@ -552,4 +552,42 @@ class custom extends control $this->display(); } + + /** + * Ajax set menu + * + * @access public + * @return void + */ + public function ajaxSetMenu() + { + if($_POST) + { + $menu = $this->post->menu; // 导航类型,nav(左侧主导航)|$app(顶部一级导航)|$app-home(项目集、项目的首页导航)|$app-$subMenu(顶部二级导航)|admin-$menuKey(后台导航) + $items = $this->post->items; // 导航项 + $account = $this->app->user->account; + + if($menu && $items) $this->loadModel('setting')->setItem("$account.common.customMenu.$menu@{$this->config->vision}", $items); + } + + $this->send(array('result' => 'success')); + } + + /** + * Ajax restore menu + * + * @access public + * @return void + */ + public function ajaxRestoreMenu() + { + if($_POST) + { + $account = $this->app->user->account; + $menu = $this->post->menu; + $this->loadModel('setting')->deleteItems("owner={$account}&module=common§ion=customMenu&key=$menu"); + } + + $this->send(array('result' => 'success')); + } } diff --git a/module/custom/model.php b/module/custom/model.php index 6f975784b7..186aa02802 100644 --- a/module/custom/model.php +++ b/module/custom/model.php @@ -1,5 +1,4 @@ -order]->divider))$menu[$item->order]->divider = false; + if(!isset($menu[$item->order]->divider)) $menu[$item->order]->divider = false; } } @@ -248,39 +247,17 @@ class customModel extends model global $lang; $customMenuMap = array(); $order = 1; - if($customMenu) + if($customMenu && is_array($customMenu)) { - if(is_string($customMenu)) + $prev = ''; + foreach($customMenu as $customMenuItem) { - $customMenuItems = explode(',', $customMenu); - foreach($customMenuItems as $customMenuItem) - { - $item = new stdclass(); - $item->name = $customMenuItem; - $item->order = $order ++; - $item->hidden = false; - $customMenuMap[$item->name] = $item; - } - foreach($allMenu as $name => $item) - { - if(!isset($customMenuMap[$name])) - { - $item = new stdclass(); - $item->name = $name; - $item->hidden = true; - $item->order = $order ++; - $customMenuMap[$name] = $item; - } - } - } - elseif(is_array($customMenu)) - { - foreach($customMenu as $customMenuItem) - { - if(!isset($customMenuItem->order)) $customMenuItem->order = $order; - $customMenuMap[$customMenuItem->name] = $customMenuItem; - $order ++; - } + $name = $customMenuItem->name; + if(!isset($customMenuItem->order)) $customMenuItem->order = $order; + if($prev == 'divider') $customMenuItem->divider = true; + if($name != 'divider') $customMenuMap[$name] = $customMenuItem; + $prev = $name; + $order ++; } } elseif($module) @@ -387,6 +364,8 @@ class customModel extends model /* Process menu item's order and hidden attirbute. */ $menuItem = static::buildMenuItem($item, $customMenuMap, $name, $label, $itemLink, $isTutorialMode, $subMenu); $menuItem->order = (isset($customMenuMap[$name]) && isset($customMenuMap[$name]->order) ? $customMenuMap[$name]->order : $order ++); + if(!empty($customMenuMap) && !isset($customMenuMap[$name])) $menuItem->hidden = true; // 自定义过滤掉的菜单不显示。 + if(isset($customMenuMap[$name]) && isset($customMenuMap[$name]->divider)) $menuItem->divider = true; if($app->viewType == 'mhtml' && isset($config->custom->moblieHidden[$menuModuleName]) && in_array($name, $config->custom->moblieHidden[$menuModuleName])) $menuItem->hidden = 1; // Hidden menu by config in mobile. while(isset($menu[$menuItem->order])) $menuItem->order ++; $menu[$menuItem->order] = $menuItem; @@ -412,13 +391,6 @@ class customModel extends model */ public static function buildMenuItem(array|string $item, $customMenuMap, string $name = '', string $label = '', string|array $itemLink = '', bool $isTutorialMode = false, array $subMenu = array()): object { - if($item === '-') - { - $menuItem = new stdclass(); - $menuItem->type = 'divider'; - return $menuItem; - } - if(is_array($item) && (isset($item['subMenu']) || isset($item['dropMenu']))) { foreach(array('subMenu', 'dropMenu') as $key) @@ -428,13 +400,6 @@ class customModel extends model { if(isset($subItem->link['module']) && isset($subItem->link['method'])) $subItem->hidden = !common::hasPriv($subItem->link['module'], $subItem->link['method']); } - if(isset($customMenuMap[$name]->$key)) - { - foreach($customMenuMap[$name]->$key as $subItem) - { - if(isset($subItem->hidden) && isset($item[$key][$subItem->name])) $item[$key][$subItem->name]->hidden = $subItem->hidden; - } - } } } @@ -461,10 +426,11 @@ class customModel extends model * Get module menu data, if module is 'main' then return main menu. * * @param string $module + * @param bool $isHomeMenu * @access public * @return array */ - public static function getModuleMenu($module = 'main'): array + public static function getModuleMenu($module = 'main', $isHomeMenu = false): array { global $app, $lang, $config; @@ -474,8 +440,11 @@ class customModel extends model if($module == 'main' and !empty($lang->menu)) $allMenu = $lang->menu; if($module != 'main' and isset($lang->menu->$module) and isset($lang->menu->{$module}['subMenu'])) $allMenu = $lang->menu->{$module}['subMenu']; if($module == 'product' and isset($allMenu->branch)) $allMenu->branch = str_replace('@branch@', $lang->custom->branch, $allMenu->branch); - $flowModule = $config->global->flow . '_' . $module; - $customMenu = isset($config->customMenu->$flowModule) ? $config->customMenu->$flowModule : array(); + + /* 获取自定义过的导航。 */ + $customKey = $isHomeMenu ? $app->tab . '-home' : ($module == 'main' ? $app->tab : $app->tab . '-' . $module); + $customMenu = isset($config->customMenu->{$customKey}) ? $config->customMenu->{$customKey}: array(); + if(!empty($customMenu) && is_string($customMenu) && substr($customMenu, 0, 1) === '[') $customMenu = json_decode($customMenu); if($module == 'my' && empty($config->global->scoreStatus)) unset($allMenu->score); @@ -486,12 +455,13 @@ class customModel extends model * 获取主菜单数据。 * Get main menu data. * + * @param bool $isHomeMenu * @access public * @return array */ - public static function getMainMenu(): array + public static function getMainMenu($isHomeMenu = false): array { - return static::getModuleMenu('main'); + return static::getModuleMenu('main', $isHomeMenu); } /** diff --git a/module/index/control.php b/module/index/control.php index cf1b5357a9..4c94a17983 100644 --- a/module/index/control.php +++ b/module/index/control.php @@ -45,6 +45,7 @@ class index extends control $this->view->showFeatures = $this->indexZen->checkShowFeatures(); $this->view->latestVersionList = $latestVersionList; $this->view->appsItems = commonModel::getMainNavList($this->app->rawModule); + $this->view->allAppsItems = commonModel::getMainNavList($this->app->rawModule, true); $this->view->browserMessage = $this->loadModel('message')->getBrowserMessageConfig(); $this->display(); diff --git a/module/index/css/index.ui.css b/module/index/css/index.ui.css index 3fb0b88819..b4b0fdb637 100644 --- a/module/index/css/index.ui.css +++ b/module/index/css/index.ui.css @@ -11,7 +11,7 @@ #menu {position: fixed; left: 0; top: 0; bottom: 0; background: var(--zt-menu-bg); width: var(--zt-menu-width); color: rgba(var(--color-canvas-rgb), .8); transition: width .1s; user-select: none; z-index: 10;} #menu .nav {flex-direction: column; align-items: stretch;} #menu .nav > li {display: block; height: var(--zt-menu-height, 38px); padding: 4px 8px; transition: padding .2s;} -#menu .nav > .divider {background: var(--color-canvas); opacity: .12; height: 1px; padding: 0; margin: 6px 12px; align-self: stretch} +#menu .nav > .divider {background: var(--color-canvas); opacity: .12; height: 1px; padding: 6px 4px; align-self: stretch; background-clip: content-box; box-sizing: content-box; border: none;} #menu .nav > li > a {color: inherit; display: flex; align-items: center; gap: 4px; padding: 0 6px; height: calc(var(--zt-menu-height, 38px) - 8px); transition: color .2s, background-color .2s; border-radius: var(--radius-md);} #menu .nav > li > a.active, #menu .nav > li > a:hover {background: var(--zt-menu-hover-bg, rgba(var(--color-primary-400-rgb), .4)); color: var(--color-canvas);} @@ -35,7 +35,7 @@ .hide-menu #menu {width: var(--zt-menu-fold-width)!important;} .hide-menu #menu .nav > li {padding: 4px} -.hide-menu #menu .nav > .divider {padding: 0; margin: 6px} +.hide-menu #menu .nav > .divider {padding: 6px;} .hide-menu #menu .nav > li > a {justify-content: center;} .hide-menu #menu .nav > li > a > .text {display: none;} .hide-menu #menu #menuMoreNav .nav > li > a > .text {display: flex;} @@ -83,3 +83,6 @@ #upgradeContent {display: none; position: absolute; bottom: var(--zt-apps-bar-height); right: 10px; width: 370px; height: 322px; background-color: #fff; padding: 5px 0; border: 1px solid rgba(0,0,0,.15); border-color: rgba(0,0,0,.1); opacity: 1; border-radius: 4px; box-shadow: 0 6px 12px rgba(0,0,0,.12), 0 1px 3px rgba(0,0,0,.1);} .version-upgrade {width: 14px; height: 16px; float: left; background-repeat: no-repeat; background-position: center ; background-size: cover; display: block;} #latestVersionList {height: 270px; overflow: auto;} + +#menuMainNav[z-use-sortable] a[data-app]:hover {cursor: grab;} +#menuMainNav[z-use-sortable] li.divider:hover {cursor: grab;} \ No newline at end of file diff --git a/module/index/js/index.ui.js b/module/index/js/index.ui.js index 75803074f0..ce8e634bf6 100644 --- a/module/index/js/index.ui.js +++ b/module/index/js/index.ui.js @@ -819,6 +819,180 @@ setTimeout(refreshMenu, 500); $(document).on('click', '.menu-toggle', () => toggleMenu()); toggleMenu(!$('body').hasClass('hide-menu')); +/** + * Get current menu nav data. + * + * @returns {Array<{name: string; order: number;}>} + */ +function getMenuNavData() +{ + const data = []; + const $nav = $('#menuMainNav'); + $nav.children().each(function(index, element) { + const $elm = $(element); + data.push( + { + name: $elm.is('.divider') ? 'divider' : $elm.data('app'), + order: index * 5 + } + ); + }); + return data; +} + +/** + * Save menu nav to server. + */ +function saveMenuNavToServer() +{ + const url = $.createLink('custom', 'ajaxSetMenu'); + const data = getMenuNavData(); + $.ajaxSubmit({url, data: {menu: 'nav', items: JSON.stringify(data)}}); +} + +/** + * Restore menu nav to server. + */ +function restoreMenuNavToServer() +{ + const url = $.createLink('custom', 'ajaxRestoreMenu'); + $.ajaxSubmit({url, data: {menu: 'nav'}}); +} + +/** + * Generate menu nav items to be added. + * + * @param {Cash} $item + * @param {(item: string) => void} onClick click handler of menu item. + * @returns {Array<{icon: string; text: string; onClick: () => void;}>} + */ +function generateAddMenuNavItems($item, onClick) +{ + const items = canAddDivider($item) + ? [ + { + icon: 'icon-minus', + text: langData.divider, + onClick: () => { + onClick('divider'); + saveMenuNavToServer(); + } + } + ] + : []; + + const data = getMenuNavData(); + const allAppCodeSet = new Set(allAppsItemsMap.keys()); + for(const {name} of data) + { + if(name === 'divider') continue; + allAppCodeSet.delete(name); + } + + if(allAppCodeSet.size === 0) return items; + for(const name of allAppCodeSet) + { + const [icon, title] = getAppItemIconAndTitle(name); + items.push( + { + icon, + text: title, + onClick: () => { + onClick(name); + saveMenuNavToServer(); + } + } + ); + } + return items; +} + +$(document).on('contextmenu', '#menuMainNav .divider', function(event) +{ + const $divider = $(this); + const $nav = $divider.closest('.nav'); + const isMoving = $nav.is('[z-use-sortable]'); + const items = []; + if(isMoving) + { + items.push( + { + text: langData.save, + onClick: () => { + $divider.closest('.nav').zui().destroy(); + saveMenuNavToServer(); + } + } + ); + } + else + { + items.push( + { + text: langData.sort, + onClick: () => { + const sortable = new zui.Sortable( + '#menuMainNav', + { + animation: 150, + ghostClass: 'bg-primary-pale', + onSort: () => { + saveMenuNavToServer(); + } + } + ); + } + } + ); + } + items.push( + { + text: langData.hide, + onClick: () => { + const $li = $divider.closest('li'); + $li.remove(); + refreshMenu(); + saveMenuNavToServer(); + } + } + ); + const toAddedItems = generateAddMenuNavItems($divider, addMenuToMainNavCb($divider)); + items.push( + toAddedItems.length === 0 + ? { + text: langData.add, + disabled: true, + } + : { + text: langData.add, + items: toAddedItems, + } + ); + items.push( + { + text: langData.restore, + onClick: () => { + initAppsMenu(allAppsItems); + refreshMenu(); + restoreMenuNavToServer(); + } + } + ); + + if(apps.openedMenu) apps.openedMenu.hide(); + apps.openedMenu = zui.ContextMenu.show( + { + hideOthers: true, + element: $divider[0], + placement: 'right-start', + items: items, + event: event, + onClickItem: (info) => info.event.preventDefault() + } + ); + event.preventDefault(); +}); + /* Bind events for app trigger */ $(document).on('click', '.open-in-app,.show-in-app', function(e) { @@ -837,11 +1011,88 @@ $(document).on('click', '.open-in-app,.show-in-app', function(e) if(!code) return; const app = apps.openedMap[code]; - const items = [{text: langData.open, disabled: app && getLastAppCode() === code, onClick: function(){showApp(code)}}]; + const items = [{text: langData.open, disabled: app && getLastAppCode() === code, onClick: () => showApp(code)}]; if(app) { - items.push({text: langData.reload, onClick: function(){reloadApp(code)}}); - if(code !== 'my') items.push({text: langData.close, onClick: function(){closeApp(code)}}); + items.push({text: langData.reload, onClick: () => reloadApp(code)}); + if(code !== 'my') items.push({text: langData.close, onClick: () => closeApp(code)}); + } + + if($btn.closest('#menuMainNav').length !== 0) + { + const $nav = $btn.closest('.nav'); + const isMoving = $nav.is('[z-use-sortable]'); + if(isMoving) + { + items.push( + { + text: langData.save, + onClick: () => { + $btn.closest('.nav').zui().destroy(); + saveMenuNavToServer(); + } + } + ); + } + else + { + items.push( + { + text: langData.sort, + onClick: () => { + const sortable = new zui.Sortable( + '#menuMainNav', + { + animation: 150, + ghostClass: 'bg-primary-pale', + onSort: () => { + saveMenuNavToServer(); + } + } + ); + } + } + ); + } + + const hideDisabled = code === 'my' || $btn.is('.active'); + items.push( + { + text: langData.hide, + onClick: hideDisabled + ? null + : () => { + closeApp(code); + const $li = $btn.closest('li'); + $li.remove(); + refreshMenu(); + saveMenuNavToServer(); + }, + disabled: hideDisabled, + } + ); + const toAddedItems = generateAddMenuNavItems($btn, addMenuToMainNavCb($btn.closest('li'))); + items.push( + toAddedItems.length === 0 + ? { + text: langData.add, + disabled: true, + } + : { + text: langData.add, + items: toAddedItems, + } + ); + items.push( + { + text: langData.restore, + onClick: () => { + initAppsMenu(allAppsItems); + refreshMenu(); + restoreMenuNavToServer(); + } + } + ); } if(apps.openedMenu) apps.openedMenu.hide(); @@ -1066,3 +1317,95 @@ $(document).on('click', e => $('#bizLink').removeClass('active'); } }); + +const allAppsItemsMap = new Map(); +$(document).ready( + function() + { + for(const item of allAppsItems) + { + if(item === 'divider') continue; + + allAppsItemsMap.set(item.code, item); + } + } +); + +/** + * Get icon and title of app item. + * + * @param {string} name app name + * @returns {[string, string]} + */ +function getAppItemIconAndTitle(name) +{ + if(!allAppsItemsMap.has(name)) return[]; + const item = allAppsItemsMap.get(name); + const str = item.title; + const regex = /class=["']icon (\S*)["']\>\<\/i\>\s(\S*)/; + const matches = str.match(regex); + + if(matches) + { + const icon = matches[1]; + const text = matches[2].trim(); + return [icon, text]; + } + return []; +} + +/** + * Add menu item to #mainNav callback, used by generateAddMenuNavItems. + * + * @param {Cash} $li menu item li + * @returns {(name: string) => void} + */ +function addMenuToMainNavCb($li) { + return (name) => { + if(name === 'divider') + { + $li.after('
  • '); + refreshMenu(); + return + } + + let item = allAppsItemsMap.get(name); + const oldItem = apps.map[item.code]; + if(oldItem !== item && oldItem) item = $.extend({}, oldItem, item, {active: false}); + item.external = item.external || item.url && item.url.includes('://'); + + const $link= $('') + .attr('data-app', item.notApp ? undefined : item.code) + .attr('href', item.url || '#') + .attr('target', item.notApp ? '_blank' : undefined) + .addClass('rounded' + (item.notApp ? '' : ' show-in-app')) + .html(item.title, false); + + item.icon = item.icon || ($link.find('.icon').attr('class') || '').replace('icon ', ''); + item.text = $link.text().trim(); + $link.html('' + item.text + '', false); + if(['devops', 'bi', 'safe'].includes(item.code)) $link.find('.text').addClass('font-brand'); + apps.map[item.code] = item; + + $('
  • ') + .attr({'data-app': item.code, 'data-hint': item.text}) + .append($link) + .insertAfter($li); + + refreshMenu(); + if(!apps.defaultCode) apps.defaultCode = item.code; + }; +} + +/** + * Check whether current element can add a divider. + * @param {Cash} $item + * @returns {boolean} + */ +function canAddDivider($item) +{ + $item = $item.closest('li'); + if($item.is('.divider')) return false; + if($item.next().is('.divider')) return false; + return true; +} \ No newline at end of file diff --git a/module/index/lang/de.php b/module/index/lang/de.php index a379cf33f2..cf17cb20ee 100644 --- a/module/index/lang/de.php +++ b/module/index/lang/de.php @@ -6,9 +6,15 @@ $lang->index->pleaseInput = 'Please input'; $lang->index->search = 'Search'; $lang->index->dock = new stdClass(); -$lang->index->dock->open = 'Open'; -$lang->index->dock->reload = 'Reload'; -$lang->index->dock->close = 'Close'; +$lang->index->dock->open = 'Open'; +$lang->index->dock->reload = 'Reload'; +$lang->index->dock->close = 'Close'; +$lang->index->dock->sort = 'Sort'; +$lang->index->dock->save = 'Exit sort'; +$lang->index->dock->hide = 'Hide'; +$lang->index->dock->add = 'Add'; +$lang->index->dock->divider = 'Divider'; +$lang->index->dock->restore = 'Restore defaults'; $lang->index->upgradeVersion = 'Upgradable version'; $lang->index->upgradeNow = 'Upgrade now'; diff --git a/module/index/lang/en.php b/module/index/lang/en.php index 3ab80c0ced..8e08d366ca 100644 --- a/module/index/lang/en.php +++ b/module/index/lang/en.php @@ -6,9 +6,15 @@ $lang->index->pleaseInput = 'Enter'; $lang->index->search = 'Search'; $lang->index->dock = new stdClass(); -$lang->index->dock->open = 'Open'; -$lang->index->dock->reload = 'Reload'; -$lang->index->dock->close = 'Close'; +$lang->index->dock->open = 'Open'; +$lang->index->dock->reload = 'Reload'; +$lang->index->dock->close = 'Close'; +$lang->index->dock->sort = 'Sort'; +$lang->index->dock->save = 'Exit sort'; +$lang->index->dock->hide = 'Hide'; +$lang->index->dock->add = 'Add'; +$lang->index->dock->divider = 'Divider'; +$lang->index->dock->restore = 'Restore defaults'; $lang->index->upgradeVersion = 'Upgradable version'; $lang->index->upgradeNow = 'Upgrade now'; diff --git a/module/index/lang/fr.php b/module/index/lang/fr.php index a379cf33f2..cf17cb20ee 100644 --- a/module/index/lang/fr.php +++ b/module/index/lang/fr.php @@ -6,9 +6,15 @@ $lang->index->pleaseInput = 'Please input'; $lang->index->search = 'Search'; $lang->index->dock = new stdClass(); -$lang->index->dock->open = 'Open'; -$lang->index->dock->reload = 'Reload'; -$lang->index->dock->close = 'Close'; +$lang->index->dock->open = 'Open'; +$lang->index->dock->reload = 'Reload'; +$lang->index->dock->close = 'Close'; +$lang->index->dock->sort = 'Sort'; +$lang->index->dock->save = 'Exit sort'; +$lang->index->dock->hide = 'Hide'; +$lang->index->dock->add = 'Add'; +$lang->index->dock->divider = 'Divider'; +$lang->index->dock->restore = 'Restore defaults'; $lang->index->upgradeVersion = 'Upgradable version'; $lang->index->upgradeNow = 'Upgrade now'; diff --git a/module/index/lang/zh-cn.php b/module/index/lang/zh-cn.php index f677680af2..4477d04327 100644 --- a/module/index/lang/zh-cn.php +++ b/module/index/lang/zh-cn.php @@ -6,9 +6,15 @@ $lang->index->pleaseInput = '请输入'; $lang->index->search = '搜索'; $lang->index->dock = new stdClass(); -$lang->index->dock->open = '打开'; -$lang->index->dock->reload = '刷新'; -$lang->index->dock->close = '关闭'; +$lang->index->dock->open = '打开'; +$lang->index->dock->reload = '刷新'; +$lang->index->dock->close = '关闭'; +$lang->index->dock->sort = '排序'; +$lang->index->dock->save = '退出排序'; +$lang->index->dock->hide = '隐藏'; +$lang->index->dock->add = '添加'; +$lang->index->dock->divider = '分割线'; +$lang->index->dock->restore = '恢复默认'; $lang->index->upgradeVersion = '可升级版本'; $lang->index->upgradeNow = '现在升级'; diff --git a/module/index/lang/zh-tw.php b/module/index/lang/zh-tw.php index b138140c1b..9d20ab9547 100644 --- a/module/index/lang/zh-tw.php +++ b/module/index/lang/zh-tw.php @@ -6,9 +6,15 @@ $lang->index->pleaseInput = '請輸入'; $lang->index->search = '搜索'; $lang->index->dock = new stdClass(); -$lang->index->dock->open = '打開'; -$lang->index->dock->reload = '刷新'; -$lang->index->dock->close = '關閉'; +$lang->index->dock->open = '打開'; +$lang->index->dock->reload = '刷新'; +$lang->index->dock->close = '關閉'; +$lang->index->dock->sort = '排序'; +$lang->index->dock->save = '退出排序'; +$lang->index->dock->hide = '隱藏'; +$lang->index->dock->add = '添加'; +$lang->index->dock->divider = '分割線'; +$lang->index->dock->restore = '恢復預設'; $lang->index->upgradeVersion = '可升級版本'; $lang->index->upgradeNow = '現在升級'; diff --git a/module/index/ui/index.html.php b/module/index/ui/index.html.php index d8aa0ebbdd..b1917188a3 100644 --- a/module/index/ui/index.html.php +++ b/module/index/ui/index.html.php @@ -106,6 +106,7 @@ jsVar('vision', $config->vision); jsVar('navGroup', $lang->navGroup); jsVar('oldPages', $config->index->oldPages); jsVar('appsItems', $appsItems); +jsVar('allAppsItems', $allAppsItems); jsVar('defaultOpen', !empty($open) ? $open : ''); jsVar('manualText', $lang->manual); jsVar('manualUrl', ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme']); diff --git a/module/project/tao.php b/module/project/tao.php index 7778505a4d..c963970296 100755 --- a/module/project/tao.php +++ b/module/project/tao.php @@ -976,7 +976,7 @@ class projectTao extends projectModel */ protected function setMenuByModel(string $projectModel): bool { - global $lang; + global $lang, $config; $model = 'scrum'; if(in_array($projectModel, $this->config->project->waterfallList)) { @@ -1000,6 +1000,24 @@ class projectTao extends projectModel if(isset($lang->$model)) { + $key = 'project-' . $model; + if(isset($config->customMenu->$key)) + { + $lang->{$model}->menuOrder = array(); + $lang->{$model}->dividerMenu = ''; + + $items = json_decode($config->customMenu->$key); + $prevItem = ''; + foreach($items as $item) + { + $lang->{$model}->menuOrder[$item->order] = $item->name; + if($prevItem == 'divider') $lang->{$model}->dividerMenu .= $item->name . ','; + $prevItem = $item->name; + } + + if($lang->{$model}->dividerMenu) $lang->{$model}->dividerMenu = ',' . trim($lang->{$model}->dividerMenu, ',') . ','; + } + $lang->project->menu = $lang->{$model}->menu; $lang->project->menuOrder = $lang->{$model}->menuOrder; $lang->project->dividerMenu = $lang->{$model}->dividerMenu;