v1.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. <?php
  2. namespace zin;
  3. require_once dirname(__DIR__) . DS . 'heading' . DS . 'v1.php';
  4. require_once dirname(__DIR__) . DS . 'navbar' . DS . 'v1.php';
  5. require_once dirname(__DIR__) . DS . 'toolbar' . DS . 'v1.php';
  6. require_once dirname(__DIR__) . DS . 'useravatar' . DS . 'v1.php';
  7. class header extends wg
  8. {
  9. /**
  10. * @var mixed[]
  11. */
  12. protected static $defineBlocks = array
  13. (
  14. 'heading' => array('map' => 'heading'),
  15. 'headingToolbar' => array('map' => 'toolbar'),
  16. 'dropmenu' => array('map' => 'dropmenu'),
  17. 'navbar' => array('map' => 'nav'),
  18. 'toolbar' => array('map' => 'btn')
  19. );
  20. public static function getPageCSS()
  21. {
  22. return <<<'CSS'
  23. #heading {z-index: 5}
  24. .in-workspace #heading {display: none;}
  25. CSS;
  26. }
  27. protected function buildHeading()
  28. {
  29. if($this->hasBlock('heading')) return $this->block('heading');
  30. $headingToolbar = $this->block('headingToolbar');
  31. $dropmenu = $this->block('dropmenu');
  32. return new heading($headingToolbar, $dropmenu);
  33. }
  34. protected function buildNavbar()
  35. {
  36. $navbar = $this->block('navbar');
  37. if(empty($navbar)) $navbar = new navbar();
  38. return $navbar;
  39. }
  40. protected function buildToolbar()
  41. {
  42. $toolbar = $this->block('toolbar');
  43. if(empty($toolbar))
  44. {
  45. $toolbar = new toolbar
  46. (
  47. setClass('gap-2'),
  48. static::quickAddMenu(),
  49. static::messageBar(),
  50. static::userBar()
  51. );
  52. }
  53. $pageToolbar = data('pageToolbar');
  54. return h::div
  55. (
  56. set::id('toolbar'),
  57. div
  58. (
  59. setID('pageToolbar'),
  60. setClass('toolbar mr-2'),
  61. $pageToolbar ? html($pageToolbar) : null,
  62. static::workspaceEntry()
  63. ),
  64. $toolbar
  65. );
  66. }
  67. /**
  68. * @param string $workspace
  69. */
  70. protected function buildWorkspaceHeader($workspace)
  71. {
  72. return h::header(setID('header'), commonModel::isTutorialMode() ? setStyle('min-width', 'fit-content') : null, $this->buildHeading());
  73. }
  74. /**
  75. * Build.
  76. *
  77. * @access protected
  78. */
  79. protected function build()
  80. {
  81. return h::header
  82. (
  83. setID('header'),
  84. commonModel::isTutorialMode() ? setStyle('min-width', 'fit-content') : null,
  85. h::div
  86. (
  87. setClass('container'),
  88. $this->buildHeading(),
  89. $this->buildNavbar(),
  90. $this->buildToolbar()
  91. )
  92. );
  93. }
  94. public static function userBar()
  95. {
  96. global $lang, $app, $config;
  97. if(!isset($app->user)) return;
  98. $user = $app->user;
  99. $isGuest = $user->account == 'guest';
  100. $items = array();
  101. $modalClass = (isset($config->zin->mode) && $config->zin->mode == 'compatible') ? 'open-in-parent' : null;
  102. if(!$isGuest)
  103. {
  104. $noRole = empty($user->role) || !isset($lang->user->roleList[$user->role]);
  105. $items[] = array
  106. (
  107. 'url' => createLink('my', 'profile', '', '', true),
  108. 'leadingClass' => 'row items-center gap-2 px-2 py-1 text-inherit',
  109. 'icon' => ' hidden',
  110. 'title' => empty($user->realname) ? $user->account : $user->realname,
  111. 'titleClass' => 'text-lg',
  112. 'subtitle' => $noRole ? null : $lang->user->roleList[$user->role],
  113. 'innerClass' => $modalClass,
  114. 'data-toggle' => 'modal',
  115. 'data-size' => 700,
  116. 'data-id' => 'profile',
  117. 'leading' => array('html' => userAvatar(set::user($user))->render(), 'className' => 'center mr-1')
  118. );
  119. $items[] = array('type' => 'divider');
  120. $items[] = array
  121. (
  122. 'url' => createLink('my', 'profile', '', '', true),
  123. 'icon' => 'account',
  124. 'text' => $lang->profile,
  125. 'innerClass' => $modalClass,
  126. 'data-toggle' => 'modal',
  127. 'data-size' => 700,
  128. 'data-id' => 'profile'
  129. );
  130. if(common::hasPriv('my', 'changePassword'))
  131. {
  132. $items[] = array
  133. (
  134. 'url' => createLink('my', 'changepassword', '', '', true),
  135. 'icon' => 'cog-outline',
  136. 'text' => $lang->changePassword,
  137. 'innerClass' => $modalClass,
  138. 'data-toggle' => 'modal',
  139. 'data-size' => 'sm'
  140. );
  141. }
  142. /* 禅道国际版增加站点管理。 */
  143. if(!empty($config->sanplexVersion) && $app->user->admin) $items[] = array('text' => $lang->site, 'url' => createLink('admin', 'subscription'), 'icon' => 'sitemap');
  144. $items[] = array('type' => 'divider');
  145. if($app->config->vision === 'rnd' && !commonModel::isTutorialMode())
  146. {
  147. $items[] = array
  148. (
  149. 'url' => createLink('tutorial', 'start'),
  150. 'icon' => 'guide',
  151. 'text' => $lang->tutorialAB,
  152. 'class' => '800',
  153. 'outerClass' => 'user-tutorial',
  154. 'data-width' => 700,
  155. 'data-class-name' => 'modal-inverse tutorial-start',
  156. 'data-headerless' => true,
  157. 'data-backdrop' => true,
  158. 'data-keyboard' => true,
  159. 'innerClass' => $modalClass,
  160. 'data-toggle' => 'modal'
  161. );
  162. }
  163. }
  164. $helpItems = array();
  165. $manualUrl = ((!empty($config->isINT)) ? $config->manualUrl['int'] : $config->manualUrl['home']) . '&theme=' . $_COOKIE['theme'];
  166. $helpItems[] = array('text' => $lang->manual, 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
  167. $helpItems[] = array('text' => $lang->changeLog, 'url' => createLink('misc', 'changeLog'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
  168. /* 禅道国际版隐藏帮助。 */
  169. if(empty($config->sanplexVersion)) $items[] = array('text' => $lang->manual, 'icon' => 'help', 'url' => $manualUrl, 'attrs' => array('data-app' => 'help'));
  170. $items[] = array('type' => 'divider');
  171. if(!$isGuest && $app->config->vision === 'rnd')
  172. {
  173. $items[] = array
  174. (
  175. 'url' => createLink('my', 'preference', 'showTip=false', '', true),
  176. 'icon' => 'controls',
  177. 'text' => $lang->preference,
  178. 'data-width' => 700,
  179. 'innerClass' => $modalClass,
  180. 'data-toggle' => 'modal'
  181. );
  182. }
  183. $themeItems = array();
  184. foreach($app->lang->themes as $key => $value)
  185. {
  186. $themeItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectTheme(\"$key\")", 'selected' => $app->cookie->theme == $key);
  187. }
  188. $items[] = array
  189. (
  190. 'text' => $lang->theme,
  191. 'icon' => 'theme',
  192. 'key' => 'theme',
  193. 'items' => $themeItems,
  194. 'listProps' => ['getItem' => jsRaw('(item) => {item.selected = item["data-value"] === $.cookie.get("theme"); return item;}')]
  195. );
  196. $langItems = array();
  197. foreach($app->config->langs as $key => $value) $langItems[] = array('text' => $value, 'data-value' => $key, 'url' => "javascript:selectLang(\"$key\")", 'active' => $app->cookie->lang == $key);
  198. $items[] = array('text' => $lang->lang, 'icon' => 'lang', 'items' => $langItems);
  199. $items[] = array('type' => 'divider');
  200. /* Zentao desktop client menu. */
  201. if(isset($config->xxserver->installed) && $config->xuanxuan->turnon)
  202. {
  203. $clientSubMenu = array();
  204. $clientSubMenu[] = array('text' => $lang->downloadClient, 'url' => createLink('misc', 'downloadClient'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
  205. $clientSubMenu[] = array('text' => $lang->clientHelp, 'url' => $lang->clientHelpLink, 'attrs' => array('data-app' => 'help'));
  206. $items[] = array('text' => $lang->clientName, 'icon' => 'desktop', 'items' => $clientSubMenu);
  207. }
  208. $mobileSubMenu[] = array('content' => array('html' => "<img src='{$config->webRoot}static/images/app-qrcode.png' />", 'style' => 'width: 100px; heigth: 100px;'));
  209. /* 禅道国际版隐藏该内容。 */
  210. if(empty($config->sanplexVersion))
  211. {
  212. $items[] = array('icon' => 'mobile', 'text' => $lang->downloadMobile, 'items' => $mobileSubMenu);
  213. $items[] = array('text' => $lang->aboutZenTao, 'icon' => 'about', 'url' => createLink('misc', 'about'), 'data-toggle' => 'modal', 'innerClass' => $modalClass);
  214. $items[] = array('type' => 'html', 'className' => 'menu-item', 'html' => $lang->designedByAIUX);
  215. $items[] = array('type' => 'divider');
  216. }
  217. if($isGuest)
  218. {
  219. $items[] = array('text' => $lang->login, 'url' => createLink('user', 'login'), 'target' => '_top');
  220. }
  221. else
  222. {
  223. $items[] = array('text' => $lang->logout, 'url' => "javascript:$.apps.logout()", 'icon' => 'exit');
  224. }
  225. return dropdown
  226. (
  227. a
  228. (
  229. setClass('w-7 h-7 cursor-pointer'),
  230. userAvatar
  231. (
  232. set::circle(true),
  233. set::size(28),
  234. set::user($user)
  235. ),
  236. set::caret(false)
  237. ),
  238. set::id('userMenu'),
  239. set::placement('bottom-end'),
  240. set::menu(array('style' => array('color' => 'var(--color-fore)'))),
  241. set::strategy('fixed'),
  242. set::arrow(true),
  243. set::items($items)
  244. );
  245. }
  246. public static function messageBar()
  247. {
  248. global $app, $lang, $config;
  249. $app->loadConfig('message');
  250. if(!$config->message->browser->turnon) return null;
  251. $showCount = $config->message->browser->count;
  252. $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;
  253. $dotStyle = commonModel::getDotStyle($showCount != '0', $unreadCount);
  254. if($unreadCount > 99) $unreadCount = '99+';
  255. return dropdown
  256. (
  257. set::arrow(true),
  258. set::placement('bottom-end'),
  259. set::offset(array("alignmentAxis" => -50)),
  260. to::trigger
  261. (
  262. btn
  263. (
  264. setID('messageBar'),
  265. set(array('data-on' => 'click', 'data-call' => 'fetchMessage', 'data-fetcher' => createLink('message', 'ajaxGetDropMenu'))),
  266. setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
  267. set::square(true),
  268. set::caret(false),
  269. set::size('sm'),
  270. icon('bell', set::size('lg')),
  271. $unreadCount ? label(setClass('danger label-dot absolute' . ($showCount ? ' rounded-sm' : '')), set::style($dotStyle), $showCount ? $unreadCount : null) : null
  272. )
  273. ),
  274. to::menu(menu
  275. (
  276. setClass('dropdown-menu not-hide-menu messageDropdownBox'),
  277. set::style(array('padding' => '0')),
  278. div(setID('dropdownMessageMenu'))
  279. ))
  280. );
  281. }
  282. public static function quickAddMenu()
  283. {
  284. global $app, $config, $lang;
  285. /* Initialize the default values. */
  286. $showCreateList = $needPrintDivider = false;
  287. $isCompatible = isset($config->zin->mode) && $config->zin->mode == 'compatible';
  288. $modalClass = $isCompatible ? 'open-in-parent' : null;
  289. /* Get default product id. */
  290. $productID = isset($_SESSION['product']) ? $_SESSION['product'] : 0;
  291. if($productID)
  292. {
  293. $product = $app->dbh->query("SELECT id FROM " . TABLE_PRODUCT . " WHERE `deleted` = '0' and vision = '{$config->vision}' and id = '{$productID}'")->fetch();
  294. if(empty($product)) $productID = 0;
  295. }
  296. if(!$productID and $app->user->view->products)
  297. {
  298. $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();
  299. if($product) $productID = $product->id;
  300. }
  301. if($config->vision == 'lite')
  302. {
  303. $condition = " WHERE `deleted` = '0' AND `vision` = 'lite' AND `model` = 'kanban'";
  304. if(!$app->user->admin) $condition .= " AND `id` " . helper::dbIN($app->user->view->projects);
  305. $object = $app->dbh->query("select id from " . TABLE_PROJECT . $condition . ' LIMIT 1')->fetch();
  306. if(empty($object)) unset($lang->createIcons['story'], $lang->createIcons['task'], $lang->createIcons['execution']);
  307. }
  308. if($config->edition == 'open') unset($lang->createIcons['effort']);
  309. if($config->systemMode == 'light') unset($lang->createIcons['program'], $lang->createIcons['charter']);
  310. if(empty($config->board)) unset($lang->createIcons['board']);
  311. /* Check whether the creation permission is available, and print create buttons. */
  312. $items = array();
  313. foreach($lang->createIcons as $objectType => $objectIcon)
  314. {
  315. $createMethod = 'create';
  316. $module = $objectType == 'kanbanspace' ? 'kanban' : $objectType;
  317. if($objectType == 'effort') $createMethod = 'batchCreate';
  318. if($objectType == 'kanbanspace') $createMethod = 'createSpace';
  319. if($objectType == 'board') $createMethod = 'createBoard';
  320. if(str_contains('|bug|execution|kanbanspace|', "|$objectType|")) $needPrintDivider = true;
  321. if(!common::hasPriv($module, $createMethod)) continue;
  322. if($objectType == 'doc' and !common::hasPriv('doc', 'create')) continue;
  323. /* Determines whether to print a divider. */
  324. if($needPrintDivider and $showCreateList)
  325. {
  326. $items[] = array('type' => 'divider');
  327. $needPrintDivider = false;
  328. }
  329. $showCreateList = true;
  330. $isOnlyBody = false;
  331. $item = array('icon' => $objectIcon, 'text' => $lang->createObjects[$objectType]);
  332. $params = '';
  333. switch($objectType)
  334. {
  335. case 'doc':
  336. $params = '';
  337. $createMethod = 'selectLibType';
  338. $item['innerClass'] = $modalClass;
  339. $item['data-toggle'] = 'modal';
  340. break;
  341. case 'project':
  342. if($config->vision == 'lite')
  343. {
  344. $params = "model=kanban";
  345. }
  346. elseif(!commonModel::isTutorialMode())
  347. {
  348. $params = "programID=0&from=global";
  349. $createMethod = 'createGuide';
  350. $item['innerClass'] = $modalClass;
  351. $item['data-toggle'] = 'modal';
  352. if($isCompatible) $item['data-type'] = 'ajax';
  353. }
  354. else
  355. {
  356. $params = "model=scrum&programID=0&copyProjectID=0&extra=from=global";
  357. }
  358. break;
  359. case 'bug':
  360. $params = "productID=$productID&branch=&extras=from=global";
  361. break;
  362. case 'story':
  363. if(!$productID and $config->vision == 'lite')
  364. {
  365. $module = 'project';
  366. $params = "model=kanban";
  367. }
  368. else
  369. {
  370. $params = "productID=$productID&branch=0&moduleID=0&storyID=0&objectID=0&bugID=0&planID=0&todoID=0&extra=from=global";
  371. if($config->vision == 'lite')
  372. {
  373. $projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0;
  374. $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();
  375. $projectIdList = array();
  376. foreach($projects as $project) $projectIdList[$project->id] = $project->id;
  377. if($projectID and !isset($projectIdList[$projectID])) $projectID = 0;
  378. if(empty($projectID)) $projectID = key($projectIdList);
  379. $params = "productID={$productID}&branch=0&moduleID=0&storyID=0&objectID={$projectID}&bugID=0&planID=0&todoID=0&extra=from=global";
  380. }
  381. }
  382. break;
  383. case 'task':
  384. $params = "executionID=0&storyID=0&moduleID=0&taskID=0&todoID=0&extra=from=global";
  385. break;
  386. case 'testcase':
  387. $params = "productID=$productID&branch=&moduleID=0&from=&param=0&storyID=0&extras=from=global";
  388. break;
  389. case 'execution':
  390. $projectID = isset($_SESSION['project']) ? $_SESSION['project'] : 0;
  391. $params = "projectID={$projectID}&executionID=0&copyExecutionID=0&planID=0&confirm=no&productID=0&extra=from=global";
  392. break;
  393. case 'product':
  394. $params = "programID=&extra=from=global";
  395. break;
  396. case 'program':
  397. $params = "parentProgramID=0&charterID=0&extra=from=global";
  398. break;
  399. case 'kanbanspace':
  400. $isOnlyBody = true;
  401. $item['innerClass'] = $modalClass;
  402. $item['data-toggle'] = 'modal';
  403. $item['data-width'] = '75%';
  404. break;
  405. case 'kanban':
  406. $isOnlyBody = true;
  407. $item['innerClass'] = $modalClass;
  408. $item['data-toggle'] = 'modal';
  409. $item['data-width'] = '75%';
  410. break;
  411. case 'board':
  412. $createMethod = 'createByTemplate';
  413. $params = 'templateID=1';
  414. $isOnlyBody = true;
  415. $item['innerClass'] = $modalClass;
  416. $item['data-toggle'] = 'modal';
  417. $item['data-width'] = '75%';
  418. break;
  419. }
  420. $item['url'] = createLink($module, $createMethod, $params, '', $isOnlyBody);
  421. $items[] = $item;
  422. }
  423. if(!$showCreateList) return '';
  424. return dropdown
  425. (
  426. btn
  427. (
  428. icon('plus-solid-circle', set::size('lg')),
  429. setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
  430. set::square(true),
  431. set::size('sm'),
  432. set::caret(false)
  433. ),
  434. set::id('quickAddMenu'),
  435. set::menu(array('style' => array('color' => 'var(--color-fore)'))),
  436. set::placement('bottom'),
  437. set::strategy('fixed'),
  438. set::arrow(true),
  439. set::items($items)
  440. );
  441. }
  442. public static function workspaceEntry()
  443. {
  444. global $lang;
  445. $workspace = commonModel::getWorkspaceInfo();
  446. if(empty($workspace['type'])) return null;
  447. $opened = $workspace['opened'];
  448. return array(
  449. btn
  450. (
  451. setClass('rounded-full bg-gray bg-opacity-10 text-primary-900 text-opacity-70 ring-0 w-9'),
  452. set::size('sm'),
  453. set::icon($opened ? 'export rotate-90' : 'import rotate-270'),
  454. set::hint($opened ? $lang->exitWorkspace : $lang->enterWorkspace),
  455. on::click()->call($opened ? 'exitWorkspace' : 'enterWorkspace')
  456. ),
  457. html('<div class="divider h-2 self-center" style="margin:0 0 0 0.75rem"></div>')
  458. );
  459. }
  460. }