array('map' => 'heading'),
'headingToolbar' => array('map' => 'toolbar'),
'dropmenu' => array('map' => 'dropmenu'),
'navbar' => array('map' => 'nav'),
'toolbar' => array('map' => 'btn')
);
public static function getPageCSS()
{
return <<<'CSS'
#heading {z-index: 5}
.in-workspace #heading {display: none;}
CSS;
}
protected function buildHeading()
{
if($this->hasBlock('heading')) return $this->block('heading');
$headingToolbar = $this->block('headingToolbar');
$dropmenu = $this->block('dropmenu');
return new heading($headingToolbar, $dropmenu);
}
protected function buildNavbar()
{
$navbar = $this->block('navbar');
if(empty($navbar)) $navbar = new navbar();
return $navbar;
}
protected function buildToolbar()
{
$toolbar = $this->block('toolbar');
if(empty($toolbar))
{
$toolbar = new toolbar
(
setClass('gap-2'),
static::quickAddMenu(),
static::messageBar(),
static::userBar()
);
}
$pageToolbar = data('pageToolbar');
return h::div
(
set::id('toolbar'),
div
(
setID('pageToolbar'),
setClass('toolbar mr-2'),
$pageToolbar ? html($pageToolbar) : null,
static::workspaceEntry()
),
$toolbar
);
}
/**
* @param string $workspace
*/
protected function buildWorkspaceHeader($workspace)
{
return h::header(setID('header'), commonModel::isTutorialMode() ? setStyle('min-width', 'fit-content') : null, $this->buildHeading());
}
/**
* Build.
*
* @access protected
*/
protected function build()
{
return h::header
(
setID('header'),
commonModel::isTutorialMode() ? setStyle('min-width', 'fit-content') : null,
h::div
(
setClass('container'),
$this->buildHeading(),
$this->buildNavbar(),
$this->buildToolbar()
)
);
}
public static function userBar()
{
global $lang, $app, $config;
if(!isset($app->user)) return;
$user = $app->user;
$isGuest = $user->account == 'guest';
$items = array();
$modalClass = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
if(!$isGuest)
{
$noRole = empty($user->role) || !isset($lang->user->roleList[$user->role]);
$items[] = array
(
'url' => createLink('my', 'profile', '', '', true),
'leadingClass' => 'row items-center gap-2 px-2 py-1 text-inherit',
'icon' => ' hidden',
'title' => empty($user->realname) ? $user->account : $user->realname,
'titleClass' => 'text-lg',
'subtitle' => $noRole ? null : $lang->user->roleList[$user->role],
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 700,
'data-id' => 'profile',
'leading' => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
);
$items[] = array('type' => 'divider');
$items[] = array
(
'url' => createLink('my', 'profile', '', '', true),
'icon' => 'account',
'text' => $lang->profile,
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 700,
'data-id' => 'profile'
);
if(common::hasPriv('my', 'changePassword'))
{
$items[] = array
(
'url' => createLink('my', 'changepassword', '', '', true),
'icon' => 'cog-outline',
'text' => $lang->changePassword,
'innerClass' => $modalClass,
'data-toggle' => 'modal',
'data-size' => 'sm'
);
}
/* 禅道国际版增加站点管理。 */
if(!empty($config->sanplexVersion) && $app->user->admin) $items[] = array('text' => $lang->site, 'url' => createLink('admin', 'subscription'), 'icon' => 'sitemap');
$items[] = array('type' => 'divider');
if($app->config->vision === 'rnd' && !commonModel::isTutorialMode())
{
$items[] = array
(
'url' => createLink('tutorial', 'start'),
'icon' => 'guide',
'text' => $lang->tutorialAB,
'class' => '800',
'outerClass' => 'user-tutorial',
'data-width' => 700,
'data-class-name' => 'modal-inverse tutorial-start',
'data-headerless' => true,
'data-backdrop' => true,
'data-keyboard' => true,
'innerClass' => $modalClass,
'data-toggle' => 'modal'
);
}
}
$helpItems = array();
$manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme'];
$helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
$helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
/* 禅道国际版隐藏帮助。 */
if(empty($config->sanplexVersion)) $items[] = array('text' => $lang->manual, 'icon' => 'help', 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
$items[] = array('type' => 'divider');
if(!$isGuest && $app->config->vision === 'rnd')
{
$items[] = array
(
'url' => createLink('my', 'preference', 'showTip=false', '', true),
'icon' => 'controls',
'text' => $lang->preference,
'data-width' => 700,
'innerClass' => $modalClass,
'data-toggle' => 'modal'
);
}
$themeItems = array();
foreach($app->lang->themes as $key => $value)
{
$themeItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectTheme(\"$key\")", 'selected' => $app->cookie->theme == $key);
}
$items[] = array
(
'text' => $lang->theme,
'icon' => 'theme',
'key' => 'theme',
'items' => $themeItems,
'listProps' => ['getItem' => jsRaw('(item) => {item.selected = item["data-value"] === $.cookie.get("theme"); return item;}')]
);
$langItems = array();
foreach($app->config->langs as $key => $value) $langItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectLang(\"$key\")", 'active' => $app->cookie->lang == $key);
$items[] = array('text' => $lang->lang, 'icon' => 'lang', 'items' => $langItems);
$items[] = array('type' => 'divider');
/* Zentao desktop client menu. */
if(isset($config->xxserver->installed) && $config->xuanxuan->turnon)
{
$clientSubMenu = array();
$clientSubMenu[] = array('text' => $lang->downloadClient, 'url' => createLink('misc', 'downloadClient'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
$clientSubMenu[] = array('text' => $lang->clientHelp, 'url' => $lang->clientHelpLink, 'attrs' => array('data-app' => 'help'));
$items[] = array('text' => $lang->clientName, 'icon' => 'desktop', 'items' => $clientSubMenu);
}
$mobileSubMenu[] = array('content' => array('html' => "
", 'style' => 'width: 100px; heigth: 100px;'));
/* 禅道国际版隐藏该内容。 */
if(empty($config->sanplexVersion))
{
$items[] = array('icon' => 'mobile', 'text' => $lang->downloadMobile, 'items' => $mobileSubMenu);
$items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
$items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX);
$items[] = array('type' => 'divider');
}
if($isGuest)
{
$items[] = array('text' => $lang->login, 'url' => createLink('user', 'login'), 'target' => '_top');
}
else
{
$items[] = array('text' => $lang->logout, 'url' => "javascript:$.apps.logout()", 'icon' => 'exit');
}
return dropdown
(
a
(
setClass('w-7 h-7 cursor-pointer'),
userAvatar
(
set::circle(true),
set::size(28),
set::user($user)
),
set::caret(false)
),
set::id('userMenu'),
set::placement('bottom-end'),
set::menu(array('style' => array('color' => 'var(--color-fore)'))),
set::strategy('fixed'),
set::arrow(true),
set::items($items)
);
}
public static function messageBar()
{
global $app, $lang, $config;
$app->loadConfig('message');
if(!$config->message->browser->turnon) return null;
$showCount = $config->message->browser->count;
$unreadCount = $app->dbh->query("SELECT COUNT(1) AS `count` FROM " . TABLE_NOTIFY . " WHERE `objectType` = 'message' AND status != 'read' AND action != 0 AND `toList` = ',{$app->user->account},'")->fetch()->count;
$dotStyle = commonModel::getDotStyle($showCount != '0', $unreadCount);
if($unreadCount > 99) $unreadCount = '99+';
return dropdown
(
set::arrow(true),
set::placement('bottom-end'),
set::offset(array("alignmentAxis" => -50)),
to::trigger
(
btn
(
setID('messageBar'),
set(array('data-on' => 'click', 'data-call' => 'fetchMessage', 'data-fetcher' => createLink('message', 'ajaxGetDropMenu'))),
setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
set::square(true),
set::caret(false),
set::size('sm'),
icon('bell', set::size('lg')),
$unreadCount ? label(setClass('danger label-dot absolute' . ($showCount ? ' rounded-sm' : '')), set::style($dotStyle), $showCount ? $unreadCount : null) : null
)
),
to::menu(menu
(
setClass('dropdown-menu not-hide-menu messageDropdownBox'),
set::style(array('padding' => '0')),
div(setID('dropdownMessageMenu'))
))
);
}
public static function quickAddMenu()
{
global $app, $config, $lang;
/* Initialize the default values. */
$showCreateList = $needPrintDivider = false;
$isCompatible = isset($config->zin->mode) && $config->zin->mode == 'compatible';
$modalClass = $isCompatible ? 'open-in-parent' : null;
/* Get default product id. */
$productID = isset($_SESSION['product']) ? $_SESSION['product'] : 0;
if($productID)
{
$product = $app->dbh->query("SELECT id FROM " . TABLE_PRODUCT . " WHERE `deleted` = '0' and vision = '{$config->vision}' and id = '{$productID}'")->fetch();
if(empty($product)) $productID = 0;
}
if(!$productID and $app->user->view->products)
{
$product = $app->dbh->query("SELECT id FROM " . TABLE_PRODUCT . " WHERE `deleted` = '0' and vision = '{$config->vision}' and id " . helper::dbIN($app->user->view->products) . " order by `order` desc limit 1")->fetch();
if($product) $productID = $product->id;
}
if($config->vision == 'lite')
{
$condition = " WHERE `deleted` = '0' AND `vision` = 'lite' AND `model` = 'kanban'";
if(!$app->user->admin) $condition .= " AND `id` " . helper::dbIN($app->user->view->projects);
$object = $app->dbh->query("select id from " . TABLE_PROJECT . $condition . ' LIMIT 1')->fetch();
if(empty($object)) unset($lang->createIcons['story'], $lang->createIcons['task'], $lang->createIcons['execution']);
}
if($config->edition == 'open') unset($lang->createIcons['effort']);
if($config->systemMode == 'light') unset($lang->createIcons['program'], $lang->createIcons['charter']);
if(empty($config->board)) unset($lang->createIcons['board']);
/* Check whether the creation permission is available, and print create buttons. */
$items = array();
foreach($lang->createIcons as $objectType => $objectIcon)
{
$createMethod = 'create';
$module = $objectType == 'kanbanspace' ? 'kanban' : $objectType;
if($objectType == 'effort') $createMethod = 'batchCreate';
if($objectType == 'kanbanspace') $createMethod = 'createSpace';
if($objectType == 'board') $createMethod = 'createBoard';
if(str_contains('|bug|execution|kanbanspace|', "|$objectType|")) $needPrintDivider = true;
if(!common::hasPriv($module, $createMethod)) continue;
if($objectType == 'doc' and !common::hasPriv('doc', 'create')) continue;
/* Determines whether to print a divider. */
if($needPrintDivider and $showCreateList)
{
$items[] = array('type' => 'divider');
$needPrintDivider = false;
}
$showCreateList = true;
$isOnlyBody = false;
$item = array('icon' => $objectIcon, 'text' => $lang->createObjects[$objectType]);
$params = '';
switch($objectType)
{
case 'doc':
$params = '';
$createMethod = 'selectLibType';
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
break;
case 'project':
if($config->vision == 'lite')
{
$params = "model=kanban";
}
elseif(!commonModel::isTutorialMode())
{
$params = "programID=0&from=global";
$createMethod = 'createGuide';
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
if($isCompatible) $item['data-type'] = 'ajax';
}
else
{
$params = "model=scrum&programID=0©ProjectID=0&extra=from=global";
}
break;
case 'bug':
$params = "productID=$productID&branch=&extras=from=global";
break;
case 'story':
if(!$productID and $config->vision == 'lite')
{
$module = 'project';
$params = "model=kanban";
}
else
{
$params = "productID=$productID&branch=0&moduleID=0&storyID=0&objectID=0&bugID=0&planID=0&todoID=0&extra=from=global";
if($config->vision == 'lite')
{
$projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0;
$projects = $app->dbh->query("SELECT t2.id FROM " . TABLE_PROJECTPRODUCT . " AS t1 LEFT JOIN " . TABLE_PROJECT . " AS t2 ON t1.project = t2.id WHERE t1.`product` = '{$productID}' and t2.`type` = 'project' and t2.id " . helper::dbIN($app->user->view->projects) . " ORDER BY `order` desc")->fetchAll();
$projectIdList = array();
foreach($projects as $project) $projectIdList[$project->id] = $project->id;
if($projectID and !isset($projectIdList[$projectID])) $projectID = 0;
if(empty($projectID)) $projectID = key($projectIdList);
$params = "productID={$productID}&branch=0&moduleID=0&storyID=0&objectID={$projectID}&bugID=0&planID=0&todoID=0&extra=from=global";
}
}
break;
case 'task':
$params = "executionID=0&storyID=0&moduleID=0&taskID=0&todoID=0&extra=from=global";
break;
case 'testcase':
$params = "productID=$productID&branch=&moduleID=0&from=¶m=0&storyID=0&extras=from=global";
break;
case 'execution':
$projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0;
$params = "projectID={$projectID}&executionID=0©ExecutionID=0&planID=0&confirm=no&productID=0&extra=from=global";
break;
case 'product':
$params = "programID=&extra=from=global";
break;
case 'program':
$params = "parentProgramID=0&charterID=0&extra=from=global";
break;
case 'kanbanspace':
$isOnlyBody = true;
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
$item['data-width'] = '75%';
break;
case 'kanban':
$isOnlyBody = true;
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
$item['data-width'] = '75%';
break;
case 'board':
$createMethod = 'createByTemplate';
$params = 'templateID=1';
$isOnlyBody = true;
$item['innerClass'] = $modalClass;
$item['data-toggle'] = 'modal';
$item['data-width'] = '75%';
break;
}
$item['url'] = createLink($module, $createMethod, $params, '', $isOnlyBody);
$items[] = $item;
}
if(!$showCreateList) return '';
return dropdown
(
btn
(
icon('plus-solid-circle', set::size('lg')),
setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
set::square(true),
set::size('sm'),
set::caret(false)
),
set::id('quickAddMenu'),
set::menu(array('style' => array('color' => 'var(--color-fore)'))),
set::placement('bottom'),
set::strategy('fixed'),
set::arrow(true),
set::items($items)
);
}
public static function workspaceEntry()
{
global $lang;
$workspace = commonModel::getWorkspaceInfo();
if(empty($workspace['type'])) return null;
$opened = $workspace['opened'];
return array(
btn
(
setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
set::size('sm'),
set::icon($opened ? 'export rotate-90' : 'import rotate-270'),
set::hint($opened ? $lang->exitWorkspace : $lang->enterWorkspace),
on::click()->call($opened ? 'exitWorkspace' : 'enterWorkspace')
),
html('