Merge branch 'main' of zentao/zentaopms (#2969)

This commit is contained in:
王怡栋
2024-09-03 10:34:07 +08:00
committed by Gitfox
21 changed files with 1111 additions and 101 deletions
+1
View File
@@ -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());}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace zin;
class graph extends wg
{
protected static array $defineProps = array(
'type?: string', // 'TreeGraph' or 'Graph', default is 'TreeGraph'
'responsive?: bool=false',
'width?: number|string="100%"',
'height?: number|string="500"',
);
protected function build(): zui
{
list($type, $width, $height, $responsive) = $this->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()),
);
}
}
+211
View File
@@ -0,0 +1,211 @@
/**
* @type {string[]}
*/
const allMainNavbarItemNames = [];
/**
* @type {Map<string, any>}
*/
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 = $('<a></a>')
.attr('href', item.url)
.attr('data-id', item['data-id'])
.attr('data-app', item['data-app'])
.append(`<span class="text">${item.text}</span>`);
if(item.badge)
{
$a.append(`<span class="${item.badge.class}">${item.badge.text}</span>`);
}
const $navItem = $('<li class="nav-item item"></li>');
$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});
}
+19 -4
View File
@@ -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);
}
}
+271
View File
@@ -0,0 +1,271 @@
/**
* @type {string[]}
*/
const allNavbarItemNames = [];
/**
* @type {Map<string, any>}
*/
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('<li class="nav-divider item divider"></li>');
const item = allNavbarItemMap.get(name);
const $a = $('<a></a>')
.attr('href', item.url)
.attr('target', item.target)
.attr('data-id', item['data-id'])
.append(`<span class="text">${item.text}</span>`);
if(item.class) $a.attr('class', item.class);
const $navItem = $('<li class="nav-item item"></li>');
$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;
}
+44 -7
View File
@@ -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()
)
);
+19
View File
@@ -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'];
+1
View File
@@ -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";
+41 -16
View File
@@ -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;
}
/**
+38
View File
@@ -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&section=customMenu&key=$menu");
}
$this->send(array('result' => 'success'));
}
}
+23 -53
View File
@@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
<?php declare(strict_types=1);
/**
* The model file of custom module of ZenTaoPMS.
*
@@ -226,7 +225,7 @@ class customModel extends model
else
{
$isFirst = false;
if(!isset($menu[$item->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);
}
/**
+1
View File
@@ -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();
+5 -2
View File
@@ -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;}
+346 -3
View File
@@ -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('<li class="divider"></li>');
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= $('<a data-pos="menu"></a>')
.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('<i class="icon ' + item.icon + '"></i><span class="text">' + item.text + '</span>', false);
if(['devops', 'bi', 'safe'].includes(item.code)) $link.find('.text').addClass('font-brand');
apps.map[item.code] = item;
$('<li class="hint-right"></li>')
.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;
}
+9 -3
View File
@@ -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';
+9 -3
View File
@@ -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';
+9 -3
View File
@@ -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';
+9 -3
View File
@@ -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 = '现在升级';
+9 -3
View File
@@ -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 = '現在升級';
+1
View File
@@ -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']);
+19 -1
View File
@@ -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;