index.ui.js 54 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681
  1. /* Init variables */
  2. const apps =
  3. {
  4. /** @type {Record<string, ZentaoApp>} */
  5. map: {},
  6. /** @type {Record<string, ZentaoOpenedApp>} */
  7. openedMap: {},
  8. defaultCode: '',
  9. zIndex: 10,
  10. frameContent: null,
  11. theme: null,
  12. oldPages: new Set(oldPages)
  13. };
  14. const DEBUG = config.debug;
  15. function getUrlID(url)
  16. {
  17. const info = $.parseLink(url);
  18. return info.moduleName ? `${info.moduleName}-${info.methodName}` : '';
  19. }
  20. function showLog(code, name, moreTitles, trace, moreInfos)
  21. {
  22. const titles = ['%c HOME '];
  23. const titleColors = ['color:#fff;font-weight:bold;background:#009688'];
  24. if(code)
  25. {
  26. titles.push(`%c ${code} `);
  27. titleColors.push('background:rgba(0,150,136,0.2);color:#009688;');
  28. }
  29. if(name)
  30. {
  31. titles.push(`%c ${name} `);
  32. titleColors.push('color:#009688;font-weight:bold;');
  33. }
  34. if(!Array.isArray(moreTitles)) moreTitles = [moreTitles];
  35. if(typeof moreTitles[0] === 'string' && (moreTitles[0].startsWith('success:') || moreTitles[0].startsWith('error:')))
  36. {
  37. const message = moreTitles.shift();
  38. const [type, content] = message.split(':', 2);
  39. titles.push(`%c ${content} `);
  40. titleColors.push(`color:${type === 'error' ? '#f56c6c' : '#67c23a'};`);
  41. }
  42. if(trace || moreInfos)
  43. {
  44. console.groupCollapsed(titles.join(''), ...titleColors, ...moreTitles);
  45. if(trace) console.trace(trace);
  46. if(moreInfos)
  47. {
  48. if($.isPlainObject(moreInfos)) Object.keys(moreInfos).forEach((infoName) => console.log(infoName, moreInfos[infoName]));
  49. else console.log(moreInfos);
  50. }
  51. console.groupEnd();
  52. }
  53. else
  54. {
  55. console.log(titles.join(''), ...titleColors, ...moreTitles);
  56. }
  57. }
  58. function triggerAppEvent(code, event, args, options)
  59. {
  60. const app = apps.openedMap[code];
  61. if(!app) return;
  62. event = event.includes('.') ? event : `${event}.apps`;
  63. if(DEBUG && (!options || options.silent !== true)) showLog(code, 'Event', event, {args});
  64. if(!Array.isArray(args)) args = [args];
  65. if(app.$app) app.$app.trigger(event, args);
  66. try
  67. {
  68. const iframeWindow = app.iframe && app.iframe.contentWindow;
  69. if(iframeWindow) return iframeWindow.$(iframeWindow.document).trigger(event, args);
  70. }
  71. catch(e){}
  72. }
  73. function isOldPage(url)
  74. {
  75. if(typeof url !== 'object') url = $.parseLink(url);
  76. return apps.oldPages.has(url.moduleName.toLowerCase()) || apps.oldPages.has(`${url.moduleName}-${url.methodName}`.toLowerCase());
  77. }
  78. /**
  79. * 提取纯文本标签(去除 HTML 标签)
  80. * @param {string} html
  81. * @returns {string}
  82. */
  83. function extractLabelText(html)
  84. {
  85. return html.replace(/<[^>]*>/g, '').trim();
  86. }
  87. /**
  88. * 获取所有应用列表。
  89. * @returns {Array<{id: string, label: string, active: boolean}>}
  90. */
  91. function getApps()
  92. {
  93. const lastAppCode = getLastAppCode();
  94. return Array.from(allAppsItemsMap.values()).map(app => ({
  95. id: app.code,
  96. label: extractLabelText(window.appsItems.find(item => item.code === app.code)?.title),
  97. active: lastAppCode === app.code
  98. }));
  99. }
  100. /**
  101. * 获取所有可见的应用列表。
  102. * @returns {Array<{id: string, label: string, active: boolean}>}
  103. */
  104. function getVisibleApps()
  105. {
  106. const $mainNav = $('#menuMainNav');
  107. return getApps().filter(app => {
  108. const $menuItem = $mainNav.find(`li[data-app="${app.id}"]`);
  109. return $menuItem.length > 0 && !$menuItem.is('[data-hidden="1"]');
  110. });
  111. }
  112. /**
  113. * 获取所有已打开的应用列表。
  114. * @returns {Array<{id: string, label: string, active: boolean}>}
  115. */
  116. function getOpenedApps()
  117. {
  118. const lastAppCode = getLastAppCode();
  119. return Object.values(apps.openedMap).map(app => ({
  120. id: app.code,
  121. label: extractLabelText(window.appsItems.find(item => item.code === app.code)?.title),
  122. active: app.code === lastAppCode
  123. }));
  124. }
  125. /**
  126. * 触发 onChangeApp 回调函数。
  127. */
  128. function changeApp()
  129. {
  130. const callback = window.zinCallbacks.onChangeApp;
  131. if(typeof callback === 'function')
  132. {
  133. try { callback(getVisibleApps(), getOpenedApps()); } catch (e) { console.error('[ZIN] onChangeApp callback error:', e); }
  134. }
  135. }
  136. /**
  137. * Open app
  138. * @param {string} url
  139. * @param {string|object} [code]
  140. * @param {boolean|object} [options]
  141. * @returns {ZentaoOpenedApp|undefined}
  142. */
  143. function openApp(url, code, options)
  144. {
  145. if(!apps.frameContent)
  146. {
  147. setTimeout(() => {openApp(url, code, options);}, 100);
  148. return;
  149. }
  150. const loadOptions = {};
  151. if(typeof code === 'object') $.extend(loadOptions, code);
  152. else if(code) loadOptions.code = code;
  153. if(typeof options === 'boolean') loadOptions.forceReload = options;
  154. else if(typeof options === 'object') $.extend(loadOptions, options);
  155. options = loadOptions;
  156. code = options.code;
  157. let forceReload = options.forceReload !== false;
  158. delete options.forceReload;
  159. delete options.code;
  160. if(!code)
  161. {
  162. if(apps.map[url])
  163. {
  164. code = url;
  165. url = '';
  166. }
  167. else if(url)
  168. {
  169. url = url.replace('http:', window.location.protocol).replace('https:', window.location.protocol);
  170. code = getAppCode(url);
  171. }
  172. if(!code) return openApp('my');
  173. }
  174. const app = apps.map[code];
  175. if(!app)
  176. {
  177. if(DEBUG) showLog(code, 'APP NOT FOUND!', url, options);
  178. return zui.Modal.alert(appNotFound);
  179. }
  180. let openedApp = apps.openedMap[code];
  181. if($.apps.workspace && $.apps.workspace !== code)
  182. {
  183. url = url || (openedApp ? openedApp.currentUrl : app.url);
  184. if(url) window.open(url, '_blank');
  185. return;
  186. }
  187. /* Create iframe for app */
  188. if(!openedApp)
  189. {
  190. if(!url) url = app.url;
  191. openedApp = $.extend({opened: true, url: url, zIndex: ++apps.zIndex, currentUrl: url}, app);
  192. forceReload = false;
  193. apps.openedMap[code] = openedApp;
  194. const $iframe =
  195. $([
  196. '<iframe',
  197. 'id="appIframe-' + code + '"',
  198. 'name="app-' + code + '"',
  199. 'class="fade"',
  200. 'allowfullscreen="true"',
  201. app.external ? 'src="' + url + '"' : '',
  202. 'frameborder="no"',
  203. 'allowtransparency="true"',
  204. 'scrolling="auto"',
  205. '/>'
  206. ].join(' '));
  207. const iframe = $iframe[0];
  208. openedApp.iframe = iframe;
  209. openedApp.$app = $('<div class="app-container loading" id="app-' + code + '"></div>')
  210. .append($iframe)
  211. .appendTo('#apps');
  212. const finishLoad = () =>
  213. {
  214. $iframe.removeClass('loading').addClass('in');
  215. setTimeout(() => {openedApp.$app.removeClass('loading');}, 300);
  216. };
  217. if(app.external)
  218. {
  219. $iframe.on('ready.app', () =>{openApp(url, code, options);});
  220. }
  221. else
  222. {
  223. const writeToDoc = () => {
  224. iframe.contentDocument.open();
  225. const html = apps.frameContent.replace('window.defaultAppUrl = ""', `window.defaultAppUrl = "${url}"`);
  226. iframe.contentDocument.write(html);
  227. iframe.contentDocument.close();
  228. };
  229. if(!iframe.contentDocument.body.children.length) setTimeout(() => writeToDoc(), 500);
  230. }
  231. iframe.onload = iframe.onreadystatechange = function(e)
  232. {
  233. try
  234. {
  235. iframe.contentWindow.$(iframe.contentDocument).one('pageload.app pagecaheload.app', finishLoad);
  236. setTimeout(finishLoad, 10000);
  237. }
  238. catch(e){finishLoad()}
  239. triggerAppEvent(openedApp.code, 'loadapp', [openedApp, e]);
  240. };
  241. openedApp.$app.show().css('z-index', openedApp.zIndex);
  242. }
  243. if(!url) url = openedApp.currentUrl;
  244. /* Set tab cookie */
  245. $.cookie.set('tab', code, {expires: config.cookieLife, path: config.webRoot});
  246. /* Highlight on left menu */
  247. if(!openedApp.workspace)
  248. {
  249. const $menuNav = $('#menuMainNav,#menuMoreNav');
  250. const $lastItem = $menuNav.find('li>a.active');
  251. if($lastItem.data('app') !== code)
  252. {
  253. $lastItem.removeClass('active');
  254. $menuNav.find('li[data-app="' + code + '"]>a').addClass('active');
  255. openedApp.$app.trigger('showapp', openedApp);
  256. }
  257. }
  258. /* Show and load app */
  259. const isSameUrl = $.parseLink(openedApp.currentUrl).url === $.parseLink(url).url;
  260. const needLoad = !isSameUrl || forceReload !== false;
  261. if(needLoad)
  262. {
  263. const openFromHidden = openedApp.zIndex < apps.zIndex;
  264. reloadApp(code, url, openFromHidden ? $.extend({loadingIndicatorDelay: '0s'}, options) : options);
  265. openedApp.$app.toggleClass('open-from-hidden', openFromHidden);
  266. }
  267. else
  268. {
  269. updateApp(code, url, openedApp.currentTitle, 'show');
  270. }
  271. $('body').attr('data-app', code);
  272. openedApp.zIndex = ++apps.zIndex;
  273. openedApp.$app.show().css('z-index', openedApp.zIndex);
  274. openedApp.getPageInfo = () => {
  275. const getPageInfo = openedApp.iframe.contentWindow.getPageInfo;
  276. return getPageInfo ? getPageInfo() : null;
  277. };
  278. openedApp.getPerfData = () => {
  279. const getPerfData = openedApp.iframe.contentWindow.getPerfData;
  280. return getPerfData ? getPerfData() : null;
  281. };
  282. /* Update on app tabs bar */
  283. const $tabs = $('#appTabs');
  284. let $tabItem = $('#appTab-' + code);
  285. if(!$tabItem.length)
  286. {
  287. if (app.text === undefined) return false;
  288. const $link= $('<a data-pos="tab"></a>')
  289. .attr('data-app', code)
  290. .addClass('show-in-app')
  291. .append($('<span class="text"></span>').text(app.text));
  292. $tabItem = $('<li class="nav-item"></li>')
  293. .attr({'data-app': code, id: 'appTab-' + code})
  294. .append($link)
  295. .appendTo($tabs);
  296. openedApp.$bar = $tabItem;
  297. }
  298. const $lastTab = $tabs.find('li>a.active');
  299. if($lastTab.data('app') !== code)
  300. {
  301. $lastTab.removeClass('active').parent().removeClass('active');
  302. $tabs.find('li[data-app="' + code + '"]>a').addClass('active').parent().addClass('active');
  303. }
  304. if(DEBUG) showLog(code, 'Open', getUrlID(url), openedApp, {options, forceReload, needLoad});
  305. triggerAppEvent(code, 'openapp', [openedApp, {load: needLoad}]);
  306. changeApp();
  307. const spaceType = openedApp.workspace ? openedApp.workspace.type : '';
  308. if(spaceType !== $('#menu').attr('data-space')) updateSpaceMenu(openedApp.workspace);
  309. return openedApp;
  310. }
  311. /**
  312. * Show app
  313. * @param {string} code
  314. */
  315. function showApp(code)
  316. {
  317. return openApp('', code, false);
  318. }
  319. /**
  320. * Reload app
  321. * @param {string} code
  322. * @param {string} url
  323. * @param {object} options
  324. */
  325. function reloadApp(code, url, options)
  326. {
  327. const app = apps.openedMap[code];
  328. if(!app) return;
  329. if(url === true) url = app.url;
  330. else if(!url) url = app.currentUrl;
  331. const iframe = app.iframe;
  332. try
  333. {
  334. if(app.external) iframe.src = url;
  335. else if(iframe.contentWindow.loadPage) iframe.contentWindow.loadPage(url, $.extend({updateHeading: true}, options));
  336. else if(DEBUG) showLog(code, 'Reload', 'error:Cannot load page when iframe is not ready.', {options});
  337. else console.error('[APPS]', 'Reload failed: Cannot load page when iframe is not ready.');
  338. }
  339. catch(error)
  340. {
  341. iframe.src = url;
  342. }
  343. app.currentUrl = url;
  344. changeApp();
  345. }
  346. function updateApp(code, url, title, type)
  347. {
  348. const app = apps.openedMap[code];
  349. if(!app || app.external) return;
  350. const state = typeof code === 'object' ? code : {code: code, url: url, title: title, type: type};
  351. const prevState = window.history.state;
  352. const urlInfo = $.parseLink(state.url);
  353. state.prev = prevState;
  354. state.index = prevState ? prevState.index + 1 : 1;
  355. state.path = urlInfo.methodName ? `${urlInfo.moduleName}-${urlInfo.methodName}` : '';
  356. if(title)
  357. {
  358. document.title = title;
  359. app.currentTitle = title;
  360. }
  361. if(prevState && prevState.code === code && prevState.url === url) return prevState;
  362. app.currentUrl = url;
  363. const urlParts = url.split('#');
  364. const hash = urlParts.length > 1 ? urlParts[1] : '';
  365. const browserUrl = getAppCode(urlParts[0]) !== code ? `${urlParts[0]}#${hash.length ? `${hash}&` : ''}app=${code}` : url;
  366. window.history.pushState(state, title, browserUrl);
  367. zui.store.session.set('lastOpenApp', {code, url});
  368. triggerAppEvent(code, 'updateapp', [code, url, title, type]);
  369. if(DEBUG) showLog(code, 'Update', title || getUrlID(url), state, {url, title, type});
  370. return state;
  371. }
  372. /**
  373. * Get last opened app
  374. * @returns {object} The opened app info object
  375. */
  376. function getLastApp()
  377. {
  378. let lastShowIndex = 0;
  379. let lastApp = null;
  380. $.each(apps.openedMap, function(_code, app)
  381. {
  382. if(lastShowIndex < app.zIndex && !app.closed)
  383. {
  384. lastShowIndex = app.zIndex;
  385. lastApp = app;
  386. }
  387. });
  388. return lastApp;
  389. }
  390. /** Get last opened app code. */
  391. function getLastAppCode()
  392. {
  393. const lastApp = getLastApp();
  394. return lastApp ? lastApp.code : null;
  395. }
  396. /**
  397. * Close app
  398. * @param {string} code
  399. * @returns {ZentaoOpenedApp|undefined|false}
  400. */
  401. function closeApp(code)
  402. {
  403. code = code || getLastAppCode();
  404. const app = apps.openedMap[code];
  405. if(!app) return;
  406. const iframe = app.iframe;
  407. if(iframe)
  408. {
  409. if(iframe && iframe.contentDocument && iframe.contentWindow && iframe.contentWindow.onCloseApp)
  410. {
  411. var result = iframe.contentWindow.onCloseApp();
  412. if(result === false) return false;
  413. }
  414. }
  415. $('#appTabs a.active[data-app="' + code + '"]').parent().remove();
  416. app.closed = true;
  417. app.$app.trigger('hideapp', app);
  418. app.$app.remove();
  419. app.$bar.remove();
  420. hideApp(code);
  421. delete apps.openedMap[code];
  422. triggerAppEvent(code, 'closeapp', app);
  423. return app;
  424. }
  425. /**
  426. * Hide app
  427. * @param {string} code
  428. * @returns {ZentaoOpenedApp|undefined}
  429. */
  430. function hideApp(code)
  431. {
  432. code = code || getLastAppCode();
  433. const app = apps.openedMap[code];
  434. if(!app) return;
  435. $('#menuNav a.active[data-app="' + code + '"]').removeClass('active');
  436. if(!app.closed) triggerAppEvent(code, 'hideapp', app);
  437. app.$app.hide();
  438. /* Active last app */
  439. const lastApp = getLastApp(true) || getLastApp();
  440. showApp(lastApp ? lastApp.code : apps.defaultCode);
  441. return app;
  442. }
  443. /**
  444. * Get app code from url
  445. * @param {String} urlOrModuleName Url string
  446. * @return {String}
  447. */
  448. function getAppCode(urlOrModuleName, defaultCode)
  449. {
  450. var code = navGroup[urlOrModuleName];
  451. if(code) return code;
  452. var link = $.parseLink(urlOrModuleName);
  453. if(!link.moduleName || link.isOnlyBody || (link.moduleName === 'index' && link.methodName === 'index')) return '';
  454. if(link.hash && link.hash.indexOf('app=') === 0) return link.hash.substr(4);
  455. /* Handling special situations */
  456. var moduleName = link.moduleName;
  457. var methodName = link.methodName;
  458. var moduleMethodLower = (moduleName + '-' + methodName).toLowerCase();
  459. if(moduleMethodLower === 'index-index') return 'my';
  460. if(moduleMethodLower === 'search-buildindex') return 'admin';
  461. if(moduleName === 'tutorial' && methodName === 'wizard')
  462. {
  463. moduleName = link.vars[0][1];
  464. methodName = link.vars[1][1];
  465. }
  466. var methodLowerCase = methodName.toLowerCase();
  467. if(moduleName === 'doc')
  468. {
  469. if(link.prj) return 'project';
  470. if((link.params.from || link.params.$3) == 'product')
  471. {
  472. if(['objectlibs', 'showfiles', 'browse', 'view', 'edit', 'delete', 'create'].includes(methodLowerCase)) return 'product';
  473. }
  474. return 'doc';
  475. }
  476. if(['caselib', 'testreport', 'testsuite', 'testtask', 'testcase', 'bug', 'qa'].includes(moduleName))
  477. {
  478. return link.prj ? 'project' : 'qa';
  479. }
  480. if(moduleName === 'report')
  481. {
  482. if(['usereport', 'editreport', 'deletereport', 'custom'].includes(methodLowerCase) && link.params.from) return 'system';
  483. if(methodName == 'projectsummary') return 'project';
  484. return 'bi';
  485. }
  486. if(moduleName === 'story' && vision === 'lite') return 'project'
  487. if(moduleName === 'testcase' && methodLowerCase === 'zerocase')
  488. {
  489. return link.params.from == 'project' ? 'project' : 'qa';
  490. }
  491. if(moduleName === 'execution' && methodLowerCase === 'all')
  492. {
  493. return (link.params.from || link.params.$3) == 'project' ? 'project' : 'execution';
  494. }
  495. if(moduleName === 'issue' || moduleName === 'risk' || moduleName === 'opportunity' || moduleName === 'pssp' || moduleName === 'auditplan' || moduleName === 'meeting' || moduleName === 'nc')
  496. {
  497. if(link.params.$2 == 'my' || link.params.from == 'my') return 'my';
  498. if(link.params.$2 == 'project' || link.params.from == 'project') return 'project';
  499. if(link.params.$2 == 'execution' || link.params.from == 'execution') return 'execution';
  500. }
  501. if(moduleName === 'product')
  502. {
  503. if(methodLowerCase === 'create' && (link.params.programID || link.params.$1)) return 'program';
  504. if(methodLowerCase === 'edit' && (link.params.programID || link.params.$4)) return 'program';
  505. if(methodLowerCase === 'batchedit') return 'program';
  506. var moduleGroup = link.params.moduleGroup ? link.params.moduleGroup : link.params.$2;
  507. if(methodLowerCase === 'showerrornone' && moduleGroup) return moduleGroup;
  508. }
  509. if(moduleName === 'stakeholder')
  510. {
  511. if(methodLowerCase === 'create' && (link.params.programID || link.params.$1)) return 'program';
  512. }
  513. if(moduleName === 'user')
  514. {
  515. if(['todo', 'todocalendar', 'effortcalendar', 'effort', 'task', 'todo', 'story', 'bug', 'testtask', 'testcase', 'execution', 'dynamic', 'profile', 'view', 'issue', 'risk'].includes(methodLowerCase)) return 'system';
  516. }
  517. if(moduleName === 'my')
  518. {
  519. if(['team'].includes(methodLowerCase)) return 'system';
  520. }
  521. if(moduleName === 'company') if(methodLowerCase == 'browse') return 'admin';
  522. if(moduleName === 'opportunity' || moduleName === 'risk' || moduleName == 'trainplan') if(methodLowerCase == 'view') return 'project';
  523. if(moduleName === 'tree')
  524. {
  525. if(methodLowerCase === 'browse')
  526. {
  527. var viewType = link.params.view || link.params.$2;
  528. if(['bug', 'case', 'caselib'].includes(viewType)) return link.params.$5 === 'project' ? 'project' : 'qa';
  529. if(viewType === 'doc' && (link.params.from === 'product' || link.params.$5 == 'product')) return 'product';
  530. if(viewType === 'doc' && (link.params.from === 'project' || link.params.$5 == 'project')) return 'project';
  531. if(viewType === 'doc') return 'doc';
  532. if(viewType === 'story') return 'product';
  533. if(viewType === 'host') return 'admin';
  534. if(viewType === 'ticket') return 'feedback';
  535. }
  536. if(methodLowerCase === 'browsetask') return 'execution';
  537. if(methodLowerCase === 'browsegroup') return 'bi';
  538. }
  539. if(['ai', 'zai', 'aiteammate', 'aitask'].includes(moduleName)) return 'aiapp';
  540. code = navGroup[moduleName] || moduleName || urlOrModuleName;
  541. return apps.map[code] ? code : defaultCode;
  542. }
  543. /**
  544. * Search history and go back to specified path.
  545. *
  546. * @param {string} target Back target, can be app name or module-method path.
  547. * @param {string} url Fallback url.
  548. * @param {object} startState Start state.
  549. * @returns {void}
  550. */
  551. function goBack(target, url, startState)
  552. {
  553. const currentState = window.history.state;
  554. const preState = currentState && currentState.prev;
  555. if(DEBUG) showLog(null, 'Go back', target, null, {url, startState, currentState, preState});
  556. if(target && startState && currentState && preState)
  557. {
  558. startState = startState.prev || (currentState && currentState.prev);
  559. if($.apps.openedMap[target])
  560. {
  561. let state = startState;
  562. while(state && state.code !== target) state = state.prev;
  563. if(state && state.code === target)
  564. {
  565. if(state.index === preState.index)
  566. {
  567. if(url) return openApp(url, target, false);
  568. return window.history.back();
  569. }
  570. return openApp(state.url, state.code, false);
  571. }
  572. }
  573. else
  574. {
  575. const pathSet = new Set(target.toLowerCase().split(','));
  576. let state = startState;
  577. while(state && state.path && !pathSet.has(state.path.toLowerCase())) state = state.prev;
  578. if(state && pathSet.has(state.path.toLowerCase()))
  579. {
  580. return openApp(state.url, state.code, false);
  581. }
  582. }
  583. }
  584. if(url) return openApp(url);
  585. if(target && !target.endsWith('APP'))
  586. {
  587. if($.apps.openedMap[target]) return openApp(target);
  588. if(target.includes('-'))
  589. {
  590. const parts = target.split(',').shift().split('-');
  591. return openApp($.createLink(parts[0], parts[1]));
  592. }
  593. }
  594. window.history.back();
  595. }
  596. /**
  597. * Logout current user.
  598. *
  599. * @param {string} url
  600. */
  601. function logout(url)
  602. {
  603. if(!url) url = getLastApp().currentUrl.replace(/^https?:\/\/[^/]+/, '');
  604. $.get($.createLink('user', 'logout', 'referer=' + btoa(url)), function(data)
  605. {
  606. let load = $.createLink('user', 'login');
  607. try
  608. {
  609. data = JSON.parse(data);
  610. if(data.load != 'login') load = data.load;
  611. }
  612. catch (error) {}
  613. location.href = load;
  614. });
  615. }
  616. /**
  617. * Toggle left menu
  618. * @param {boolean} [toggle]
  619. * @returns {boolean}
  620. */
  621. function toggleMenu(toggle)
  622. {
  623. var $body = $('body');
  624. if (toggle === undefined) toggle = $body.hasClass('hide-menu');
  625. $body.toggleClass('hide-menu', !toggle).toggleClass('show-menu', !!toggle);
  626. $.cookie.set('hideMenu', String(!toggle), {expires: config.cookieLife, path: config.webRoot});
  627. return toggle;
  628. }
  629. /**
  630. * Refresh more menu in #menuNav
  631. * @return {void}
  632. */
  633. function refreshMenu()
  634. {
  635. const $mainNav = $('#menuMainNav');
  636. const $list = $('#menuMoreList');
  637. const $menuNav = $('#menuNav');
  638. const hasSpace = $('body').hasClass('has-space');
  639. const $menuItems = $mainNav.children(hasSpace ? '.is-space' : '.is-original');
  640. const itemHeight = $menuItems.filter((_, ele) => {
  641. const $ele = $(ele);
  642. return $ele.css('display') !== 'none' && !$ele.hasClass('divider');
  643. }).first().outerHeight();
  644. const maxHeight = $('#menu').outerHeight() - ($('body').hasClass('has-space') ? (($('#spaceHeading').outerHeight() || 0) + 8) : 0) - 24 - $('#menuToggleNav').outerHeight();
  645. const dividerHeight = 13;
  646. let showMoreMenu = false;
  647. let currentHeight = 0;
  648. let $firstHiddenItem = null;
  649. const moreBtnHeight = itemHeight + dividerHeight;
  650. $menuItems.each(function()
  651. {
  652. let $item = $(this);
  653. let isDivider = $item.hasClass('divider');
  654. let height = isDivider ? dividerHeight : itemHeight;
  655. if(showMoreMenu || (currentHeight + height) > maxHeight)
  656. {
  657. if(!$firstHiddenItem) $firstHiddenItem = $item;
  658. $item.addClass('hidden');
  659. if(!showMoreMenu)
  660. {
  661. showMoreMenu = true;
  662. $list.empty();
  663. }
  664. $list.append($item.clone().toggleClass('menu-item', !isDivider).removeClass('hidden'));
  665. }
  666. else
  667. {
  668. currentHeight += height;
  669. $item.removeClass('hidden');
  670. }
  671. });
  672. if(showMoreMenu && $firstHiddenItem && (currentHeight + moreBtnHeight) > maxHeight)
  673. {
  674. const $prevItem = $firstHiddenItem.prev();
  675. if($prevItem.length)
  676. {
  677. $prevItem.addClass('hidden');
  678. let isDivider = $prevItem.hasClass('divider');
  679. $list.prepend($prevItem.clone().toggleClass('menu-item', !isDivider).removeClass('hidden'));
  680. }
  681. }
  682. let moreMenuHeight = 16;
  683. let nextNoDivider = true;
  684. $list.children().each(function()
  685. {
  686. const $item = $(this);
  687. const isDivider = $item.hasClass('divider');
  688. if(isDivider && nextNoDivider)
  689. {
  690. $item.addClass('hidden');
  691. return;
  692. }
  693. nextNoDivider = isDivider;
  694. moreMenuHeight += isDivider ? dividerHeight : itemHeight;
  695. });
  696. nextNoDivider = true;
  697. let $lastItem = null;
  698. $menuItems.each(function()
  699. {
  700. const $item = $(this);
  701. if($item.hasClass('hidden')) return;
  702. const isDivider = $item.hasClass('divider');
  703. if(isDivider && nextNoDivider)
  704. {
  705. $item.addClass('hidden');
  706. return;
  707. }
  708. nextNoDivider = isDivider;
  709. $lastItem = $item;
  710. });
  711. if($lastItem && $lastItem.hasClass('divider')) $lastItem.addClass('hidden');
  712. $menuNav.toggleClass('show-more-nav', showMoreMenu);
  713. if(showMoreMenu && !$list.data('listened-click'))
  714. {
  715. $list.data('listened-click', true).on('click', function()
  716. {
  717. $list.addClass('hidden');
  718. setTimeout(function(){$list.removeClass('hidden')}, 200);
  719. });
  720. }
  721. }
  722. /** Init apps menu list. */
  723. function initAppsMenu(items)
  724. {
  725. if($.cookie.get('workspace'))
  726. {
  727. $('#menu').addClass('waiting-workspace');
  728. $(document).on('openapp', () =>
  729. {
  730. if($.cookie.get('workspace') === getLastAppCode()) return;
  731. $('#menu').removeClass('waiting-workspace');
  732. });
  733. }
  734. apps.map.help =
  735. {
  736. code: 'help',
  737. icon: 'icon-help',
  738. url: manualUrl || $helpLink.attr('href'),
  739. external: true,
  740. text: manualText || $helpLink.text(),
  741. appUrl: config.webRoot + '#app=help'
  742. };
  743. const $menuMainNav = $('#menuMainNav').empty();
  744. (items || appsItems).forEach(function(item)
  745. {
  746. const oldItem = apps.map[item.code];
  747. if(item === 'divider') return $menuMainNav.append('<li class="divider is-original"></li>');
  748. if(oldItem !== item && oldItem) item = $.extend({}, oldItem, item, {active: oldItem.active});
  749. item.external = item.external || item.url && item.url.includes('://');
  750. const $link= $('<a data-pos="menu"></a>')
  751. .attr('data-app', item.notApp ? undefined : item.code)
  752. .attr('href', item.url || '#')
  753. .attr('target', item.notApp ? '_blank' : undefined)
  754. .addClass('rounded' + (item.notApp ? '' : ' show-in-app'))
  755. .html(item.title, false);
  756. item.icon = item.icon || ($link.find('.icon').attr('class') || '').replace('icon ', '');
  757. item.text = $link.text().trim();
  758. $link.html('<i class="icon icon-app-empty ' + item.icon + '"></i><span class="text"></span>', false);
  759. $link.find('.text').toggleClass('font-brand', ['devops', 'bi', 'safe'].includes(item.code)).attr('title', item.text).text(item.text);
  760. apps.map[item.code] = item;
  761. $('<li class="hint-right is-original"></li>')
  762. .attr({'data-app': item.code, 'data-hint': item.text})
  763. .append($link)
  764. .appendTo($menuMainNav);
  765. const hasLongText = $link.find('.text').width() > ($link.outerWidth() - 24);
  766. $link.toggleClass('has-long-text', hasLongText).find('.text').attr('title', hasLongText ? item.text : null);
  767. if(!apps.defaultCode) apps.defaultCode = item.code;
  768. });
  769. /* 隐藏的App依然可以通过输入URL的形式打开。 */
  770. allAppsItems.forEach(function(item)
  771. {
  772. if(item.code && !apps.map[item.code]) apps.map[item.code] = item;
  773. });
  774. const lastApp = getLastApp();
  775. if(lastApp) $menuMainNav.find('li[data-app="' + lastApp.code + '"]>a').addClass('active');
  776. apps.map.search =
  777. {
  778. opened: false,
  779. code: 'search',
  780. group: 'search',
  781. icon: 'icon-search',
  782. methodName: 'index',
  783. moduleName: 'search',
  784. text: langData.search,
  785. title: '<i class="icon icon-search"></i> ' + langData.search,
  786. url: '/index.php?m=search&f=index',
  787. vars: ''
  788. };
  789. $('#appTabs').find('.nav-item').each(function()
  790. {
  791. const $item = $(this);
  792. const code = $item.data('app');
  793. const app = apps.map[code];
  794. if(!app) return;
  795. $item.find('.text').text(app.text);
  796. });
  797. const selectLangCallback = window.zinCallbacks.onSelectLang;
  798. if(typeof selectLangCallback === 'function')
  799. {
  800. try { selectLangCallback(window.config.clientLang); } catch (e) { console.error('[ZIN] onSelectLang callback error:', e); }
  801. }
  802. const selectVisionCallback = window.zinCallbacks.onSelectVision;
  803. if(typeof selectVisionCallback === 'function')
  804. {
  805. try { selectVisionCallback(window.getVisions()); } catch (e) { console.error('[ZIN] onSelectVision callback error:', e); }
  806. }
  807. refreshMenu();
  808. }
  809. /** Update apps menu. */
  810. function updateAppsMenu(includeAppsBar)
  811. {
  812. loadCurrentPage(
  813. {
  814. selector: (includeAppsBar ? '#menuMoreBtn>*,#appsToolbar>*,#visionSwitcher>*,' : '') + '#appsItemsData,#configJS,#searchItemsData,#userVisionData',
  815. complete: () => initAppsMenu(window.appsItems),
  816. });
  817. }
  818. function changeAppsLang(lang)
  819. {
  820. $('html').attr('lang', lang);
  821. zui.i18n.setCode(lang);
  822. $.each(apps.openedMap, function(_code, app)
  823. {
  824. if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.changeAppLang)
  825. {
  826. app.iframe.contentWindow.changeAppLang(lang);
  827. }
  828. });
  829. if(!$.apps.workspace) updateAppsMenu(true);
  830. }
  831. function changeAppsTheme(theme)
  832. {
  833. changeAppTheme(theme);
  834. $.each(apps.openedMap, function(_code, app)
  835. {
  836. if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.changeAppTheme)
  837. {
  838. app.iframe.contentWindow.changeAppTheme(theme);
  839. }
  840. });
  841. apps.theme = theme;
  842. $.get($.createLink('index', 'app'), html =>
  843. {
  844. apps.frameContent = html;
  845. apps.theme = null;
  846. });
  847. }
  848. function updateUserToolbar()
  849. {
  850. $.each(apps.openedMap, function(_code, app)
  851. {
  852. if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.loadPage)
  853. {
  854. app.iframe.contentWindow.loadPage({selector: '#toolbar', partial: true, target: '#toolbar'});
  855. }
  856. });
  857. }
  858. initAppsMenu();
  859. /* Refresh more menu on window resize */
  860. $(window).on('resize', refreshMenu);
  861. setTimeout(refreshMenu, 500);
  862. /* Bind event for menut-toggle */
  863. $(document).on('click', '.menu-toggle', () => toggleMenu());
  864. toggleMenu(!$('body').hasClass('hide-menu'));
  865. /**
  866. * Get current menu nav data.
  867. *
  868. * @returns {Array<{name: string; order: number;}>}
  869. */
  870. function getMenuNavData()
  871. {
  872. const data = [];
  873. const $nav = $('#menuMainNav');
  874. const workspace = $.apps.workspace;
  875. $nav.children($.apps.workspace ? '.is-space' : '.is-original').each(function(index, element)
  876. {
  877. const $elm = $(element);
  878. if($elm.hasClass('rsh-more')) return;
  879. const menuItem = {};
  880. menuItem.name = $elm.is('.divider') ? 'divider' : $elm.data(workspace ? 'name' : 'app');
  881. menuItem.order = index * 5;
  882. if(typeof $elm.data('hidden') != 'undefined') menuItem.hidden = true;
  883. data.push(menuItem);
  884. });
  885. return data;
  886. }
  887. /**
  888. * Save menu nav to server.
  889. */
  890. function saveMenuNavToServer()
  891. {
  892. const url = $.createLink('custom', 'ajaxSetMenu');
  893. const data = getMenuNavData();
  894. let menu = 'nav';
  895. if($.apps.workspace)
  896. {
  897. const currentApp = getLastApp();
  898. if(currentApp.workspace.type === $.apps.workspace) menu = currentApp.workspace.menuGroup;
  899. }
  900. $.ajaxSubmit({url, data: {menu: menu, items: JSON.stringify(data)}});
  901. }
  902. /**
  903. * Restore menu nav to server.
  904. */
  905. function restoreMenuNavToServer()
  906. {
  907. let menu = 'nav';
  908. if($.apps.workspace)
  909. {
  910. const currentApp = getLastApp();
  911. if(currentApp.workspace.type === $.apps.workspace) menu = currentApp.workspace.menuGroup;
  912. }
  913. const url = $.createLink('custom', 'ajaxRestoreMenu');
  914. $.ajaxSubmit({url, data: {menu: menu}});
  915. top.location.reload();
  916. }
  917. /**
  918. * Generate menu nav items to be added.
  919. *
  920. * @param {Cash} $item
  921. * @param {(item: string) => void} onClick click handler of menu item.
  922. * @returns {Array<{icon: string; text: string; onClick: () => void;}>}
  923. */
  924. function generateAddMenuNavItems($item)
  925. {
  926. const onClick = addMenuToMainNavCb($item);
  927. const items = [];
  928. const $li = $item.closest('li');
  929. if(canAddDivider($li)) items.push({
  930. icon: 'icon-minus',
  931. text: langData.divider,
  932. onClick: () => {
  933. onClick('divider');
  934. saveMenuNavToServer();
  935. }
  936. });
  937. const data = getMenuNavData();
  938. const allAppCodeSet = new Set(allAppsItemsMap.keys());
  939. for(const {name} of data)
  940. {
  941. if(name === 'divider') continue;
  942. allAppCodeSet.delete(name);
  943. }
  944. if(allAppCodeSet.size === 0) return items;
  945. for(const name of allAppCodeSet)
  946. {
  947. const [icon, title] = getAppItemIconAndTitle(name);
  948. items.push({
  949. icon,
  950. text: title,
  951. onClick: () => {
  952. onClick(name);
  953. saveMenuNavToServer();
  954. }
  955. });
  956. }
  957. return items;
  958. }
  959. function addMenuToSpaceNavCb($li, info)
  960. {
  961. return (name) => {
  962. if(name === 'divider')
  963. {
  964. $li.after('<li class="divider"></li>');
  965. refreshMenu();
  966. return
  967. }
  968. const fromName = $li.attr('data-name');
  969. const item = info.itemMap.get(name);
  970. if(!item) return;
  971. if(name === 'divider')
  972. {
  973. const index = info.items.findIndex(item => item.code === fromName);
  974. info.items.splice(index, 0, {type: 'divider'});
  975. $li.after('<li class="divider is-space"></li>');
  976. }
  977. else
  978. {
  979. const oldIndex = info.items.findIndex(item => item.code === name);
  980. info.items.splice(oldIndex, 1);
  981. const newIndex = info.items.findIndex(item => item.code === fromName);
  982. info.items.splice(newIndex, 0, item);
  983. item.hidden = false;
  984. updateSpaceMenu(info);
  985. }
  986. };
  987. }
  988. function generateAddSpaceNavItems($ele)
  989. {
  990. const currentApp = getLastApp();
  991. const info = currentApp.workspace;
  992. const $li = $ele.closest('li');
  993. if(!info || !$li.length) return [];
  994. const handleItemClick = (name) => {
  995. const fromName = $li.attr('data-name');
  996. if(name === 'divider')
  997. {
  998. const index = info.items.findIndex(item => item.code === fromName);
  999. info.items.splice(index, 0, {type: 'divider'});
  1000. $li.after('<li class="divider is-space"></li>');
  1001. }
  1002. else
  1003. {
  1004. const item = info.itemMap.get(name);
  1005. if(!item) return;
  1006. const oldIndex = info.items.findIndex(item => item.code === name);
  1007. info.items.splice(oldIndex, 1);
  1008. const newIndex = info.items.findIndex(item => item.code === fromName);
  1009. info.items.splice(newIndex + 1, 0, item);
  1010. item.hidden = false;
  1011. updateSpaceMenu(info);
  1012. }
  1013. saveMenuNavToServer();
  1014. };
  1015. const items = [];
  1016. if(canAddDivider($li)) items.push({
  1017. icon: 'icon-minus',
  1018. text: langData.divider,
  1019. onClick: () => handleItemClick('divider')
  1020. });
  1021. info.items.forEach((item) => {
  1022. if(!item.hidden) return;
  1023. items.push({
  1024. icon: `icon-app-empty ${item.icon || `icon-${item.code}`}`,
  1025. text: item.text,
  1026. onClick: () => handleItemClick(item.code)
  1027. });
  1028. });
  1029. return items;
  1030. }
  1031. function updateSpaceMenu(info)
  1032. {
  1033. const spaceType = info ? info.type : '';
  1034. const currentApp = getLastApp();
  1035. const currentCode = currentApp.code;
  1036. const hasSpaceNav = spaceType === currentCode;
  1037. const $menuMainNav = $('#menuMainNav');
  1038. const $menu = $('#menu');
  1039. $.apps.workspace = spaceType;
  1040. currentApp.workspace = info;
  1041. $menu.attr('data-space', spaceType);
  1042. $('body').toggleClass('has-space', hasSpaceNav);
  1043. $menuMainNav.children('.is-space').remove();
  1044. if(!hasSpaceNav)
  1045. {
  1046. refreshMenu();
  1047. if(info !== undefined) $menu.removeClass('waiting-workspace');
  1048. return;
  1049. }
  1050. const $spaceHeading = $('#spaceHeading').toggleClass('has-dropmenu', !!info.dropmenu);
  1051. if(info.dropmenu)
  1052. {
  1053. const $dropmenu = $('<div id="spaceDropmenu"></div>');
  1054. $spaceHeading.find('.text').empty().append($dropmenu);
  1055. const options = zui.evalValue(info.dropmenu, ['_element', $dropmenu[0]], ['_$dropmenu', $dropmenu]);
  1056. zui.create('dropmenu', $dropmenu[0], $.extend({icon: info.icon || spaceType, display: zui.jsx`<span class="text" title=${info.name}>${info.name}</span>`}, options, {leadingAngle: false}));
  1057. }
  1058. else
  1059. {
  1060. if(info.name) $spaceHeading.find('.text').text(info.name).attr('title', info.name);
  1061. }
  1062. $spaceHeading.find('.icon').attr('class', `icon icon-${info.icon || spaceType}`);
  1063. info.items.forEach(function(item)
  1064. {
  1065. item.code = item['data-id'];
  1066. if(item.code === 'more') return;
  1067. if(item === 'divider' || item.type === 'divider') return $menuMainNav.append('<li class="divider is-space"></li>');
  1068. const $link= $('<a data-pos="menu"></a>')
  1069. .attr('data-app', currentCode)
  1070. .attr('href', item.url || '#')
  1071. .attr('target', item.notApp ? '_blank' : undefined)
  1072. .toggleClass('active', item.code === info.active)
  1073. .addClass('rounded' + (item.notApp ? '' : ' open-in-app'));
  1074. item.icon = item.icon || `icon-${item.code}`;
  1075. $link.html('<i class="icon icon-app-empty ' + item.icon + '"></i><span class="text"></span>', false);
  1076. $link.find('.text').toggleClass('font-brand', ['devops', 'bi', 'safe'].includes(item.code)).attr('title', item.text).text(item.text);
  1077. $('<li class="hint-right is-space"></li>')
  1078. .attr({'data-app': currentCode, 'data-hint': item.text, 'data-name': item.code})
  1079. .attr('data-hidden', item.hidden ? 1 : null)
  1080. .css('display', item.hidden ? 'none' : 'block')
  1081. .append($link)
  1082. .appendTo($menuMainNav);
  1083. });
  1084. refreshMenu();
  1085. $menu.removeClass('waiting-workspace');
  1086. }
  1087. $(document).on('contextmenu', '#menuMainNav .divider', function(event)
  1088. {
  1089. const $divider = $(this);
  1090. const $nav = $divider.closest('.nav');
  1091. const isMoving = $nav.is('[z-use-sortable]');
  1092. let items = [];
  1093. if(isMoving)
  1094. {
  1095. items.push({
  1096. text: langData.save,
  1097. onClick: () => {
  1098. $divider.closest('.nav').zui().destroy();
  1099. saveMenuNavToServer();
  1100. }
  1101. });
  1102. }
  1103. else
  1104. {
  1105. items.push({
  1106. text: langData.sort,
  1107. onClick: () => {
  1108. const sortable = new zui.Sortable('#menuMainNav', {
  1109. animation: 150,
  1110. ghostClass: 'bg-primary-pale',
  1111. onSort: () => saveMenuNavToServer()
  1112. });
  1113. }
  1114. });
  1115. }
  1116. items.push({
  1117. text: langData.hide,
  1118. onClick: () => {
  1119. const $li = $divider.closest('li');
  1120. $li.remove();
  1121. if(!$.apps.workspace) refreshMenu();
  1122. saveMenuNavToServer();
  1123. }
  1124. });
  1125. const toAddedItems = $.apps.workspace ? generateAddSpaceNavItems($divider) : generateAddMenuNavItems($divider);
  1126. items.push(toAddedItems.length ? {text: langData.add, items: toAddedItems} : {text: langData.add, disabled: true});
  1127. items.push({text: langData.restore, onClick: () => restoreMenuNavToServer()});
  1128. items = items.filter(Boolean);
  1129. if(!items.length) return;
  1130. if(apps.openedMenu) apps.openedMenu.hide();
  1131. apps.openedMenu = zui.ContextMenu.show({
  1132. element: $divider[0],
  1133. placement: 'right-start',
  1134. items: items,
  1135. event: event,
  1136. onClickItem: (info) => info.event.preventDefault(),
  1137. onHide: () => apps.openedMenu = null,
  1138. });
  1139. event.preventDefault();
  1140. });
  1141. /* Bind events for app trigger */
  1142. $(document).on('click', '.open-in-app,.show-in-app', function(e)
  1143. {
  1144. const $link = $(this);
  1145. if($link.is('[data-modal],[data-toggle],.iframe,.not-in-app')) return;
  1146. const url = $link.attr('href') || $link.data('url');
  1147. if(url && url.includes('onlybody=yes')) return;
  1148. const code = $link.data('app');
  1149. if($.apps.openedMap[code]) showApp(code);
  1150. else openApp(url, $link.data('app'), !$link.hasClass('show-in-app'));
  1151. e.preventDefault();
  1152. }).on('contextmenu', '.open-in-app,.show-in-app', function(event)
  1153. {
  1154. if(isTutorialMode) return;
  1155. const $btn = $(this);
  1156. const code = $btn.data('app');
  1157. if(!code) return;
  1158. const app = apps.openedMap[code];
  1159. const inTabs = !!$btn.closest('#appTabs').length;
  1160. const inWorkspace = $.apps.workspace === code;
  1161. let items = [(inWorkspace && !inTabs) ? null : {text: langData.open, disabled: app && getLastAppCode() === code, onClick: () => showApp(code)}];
  1162. if(app && (!inWorkspace || inTabs || $btn.hasClass('active'))) items.push({text: langData.reload, onClick: () => reloadApp(code)});
  1163. if($btn.closest('#menuMainNav').length !== 0)
  1164. {
  1165. const $nav = $btn.closest('.nav');
  1166. const isMoving = $nav.is('[z-use-sortable]');
  1167. if(isMoving)
  1168. {
  1169. items.push({
  1170. text: langData.save,
  1171. onClick: () => {
  1172. $btn.closest('.nav').zui().destroy();
  1173. saveMenuNavToServer();
  1174. }
  1175. });
  1176. }
  1177. else
  1178. {
  1179. items.push({
  1180. text: langData.sort,
  1181. onClick: () => {
  1182. const sortable = new zui.Sortable('#menuMainNav', {
  1183. animation: 150,
  1184. ghostClass: 'bg-primary-pale',
  1185. onSort: () => saveMenuNavToServer()
  1186. });
  1187. }
  1188. });
  1189. }
  1190. const hideDisabled = code === 'my' || $btn.is('.active');
  1191. items.push({
  1192. text: langData.hide,
  1193. onClick: hideDisabled ? null : () => {
  1194. const $li = $btn.closest('li');
  1195. $li.hide().attr('data-hidden', '1');
  1196. if($.apps.workspace)
  1197. {
  1198. const info = app.workspace;
  1199. const name = $li.attr('data-name');
  1200. const item = info.items.find(x => x.code === name);
  1201. if(item) item.hidden = true;
  1202. }
  1203. else
  1204. {
  1205. closeApp(code);
  1206. refreshMenu();
  1207. changeApp();
  1208. }
  1209. saveMenuNavToServer();
  1210. },
  1211. disabled: hideDisabled,
  1212. });
  1213. const toAddedItems = inWorkspace ? generateAddSpaceNavItems($btn) : generateAddMenuNavItems($btn);
  1214. if(toAddedItems.length)
  1215. {
  1216. items.push(toAddedItems.length ? {text: langData.add, items: toAddedItems} : {text: langData.add, disabled: true});
  1217. }
  1218. if(!inWorkspace && app && code !== 'my') items.push({text: langData.close, onClick: () => closeApp(code)});
  1219. items.push({text: langData.restore, onClick: () => restoreMenuNavToServer()});
  1220. }
  1221. else
  1222. {
  1223. if(!inWorkspace && app && code !== 'my') items.push({text: langData.close, onClick: () => closeApp(code)});
  1224. }
  1225. items = items.filter(Boolean);
  1226. if(!items.length) return;
  1227. if(apps.openedMenu) apps.openedMenu.hide();
  1228. apps.openedMenu = zui.ContextMenu.show({element: $btn[0], placement: $btn.closest('#appTabs').length ? 'top-start' : 'right-start', items: items.filter(Boolean), event: event, onClickItem: function(info){info.event.preventDefault();}, onHide: () => {apps.openedMenu = null}});
  1229. event.preventDefault();
  1230. });
  1231. $(window).on('popstate', function(event)
  1232. {
  1233. const state = event.state;
  1234. if(DEBUG) showLog(state ? state.code : null, 'Popstate', state ? state.url : null, state);
  1235. if(state) openApp(state.url, state.code, false);
  1236. });
  1237. $.get($.createLink('index', 'app'), html =>
  1238. {
  1239. apps.frameContent = html;
  1240. /* Open default app */
  1241. let url = defaultOpen || '';
  1242. let code = '';
  1243. if(location.hash.indexOf('#app=') === 0)
  1244. {
  1245. const params = $.parseUrlParams(location.hash.substring(1));
  1246. code = params.app;
  1247. }
  1248. else if(url.includes(' '))
  1249. {
  1250. const parts = url.split(' ');
  1251. url = parts[0];
  1252. code = parts[1];
  1253. }
  1254. else if(url && $.apps[url])
  1255. {
  1256. code = url;
  1257. url = '';
  1258. }
  1259. if(!code && defaultOpen)
  1260. {
  1261. const lastOpenApp = zui.store.session.get('lastOpenApp');
  1262. if(lastOpenApp && (lastOpenApp.url === url || lastOpenApp.url.endsWith(url) || $.parseLink(lastOpenApp.url).url === $.parseLink(url).url)) code = lastOpenApp.code;
  1263. }
  1264. if(!url)
  1265. {
  1266. if(!code) url = apps.defaultCode;
  1267. else {url = code; code = '';}
  1268. }
  1269. openApp(url, code);
  1270. });
  1271. $.apps = $.extend(apps,
  1272. {
  1273. openedApps: apps.openedMap,
  1274. openApp: openApp,
  1275. open: openApp,
  1276. reloadApp: reloadApp,
  1277. showApp: showApp,
  1278. updateApp: updateApp,
  1279. updateUrl: function(appCode, url, title) {return updateApp(appCode, url, title)},
  1280. getLastApp: getLastApp,
  1281. goBack: goBack,
  1282. logout: logout,
  1283. isOldPage: isOldPage,
  1284. getAppCode: getAppCode,
  1285. updateAppsMenu: updateAppsMenu,
  1286. changeAppsLang: changeAppsLang,
  1287. changeAppsTheme: changeAppsTheme,
  1288. updateUserToolbar: updateUserToolbar,
  1289. closeApp: closeApp,
  1290. toggleMenu: toggleMenu,
  1291. triggerAppEvent: triggerAppEvent,
  1292. getApps: getApps,
  1293. getVisibleApps: getVisibleApps,
  1294. getOpenedApps: getOpenedApps,
  1295. updateSpaceMenu: updateSpaceMenu,
  1296. });
  1297. window.notifyMessage = function(data)
  1298. {
  1299. if(!window.Notification) return;
  1300. var notify = null;
  1301. var message = data;
  1302. if(typeof data.text == 'string') message = data.text;
  1303. if(Notification.permission == "granted")
  1304. {
  1305. notify = new Notification("", {body:message, tag:'zentao', data:data});
  1306. }
  1307. else if(Notification.permission != "denied")
  1308. {
  1309. Notification.requestPermission().then(function(permission)
  1310. {
  1311. notify = new Notification("", {body:message, tag:'zentao', data:data});
  1312. });
  1313. }
  1314. if(!notify) return;
  1315. notify.onclick = function()
  1316. {
  1317. window.focus();
  1318. if(typeof notify.data.url == 'string' && notify.data.url) window.location.href = notify.data.url;
  1319. notify.close();
  1320. }
  1321. setTimeout(function(){notify.close();}, 3000);
  1322. }
  1323. window.browserNotify = function()
  1324. {
  1325. let windowBlur = false;
  1326. setInterval(function()
  1327. {
  1328. if(window.Notification && Notification.permission == 'default') Notification.requestPermission();
  1329. if(window.Notification && Notification.permission == 'granted')
  1330. {
  1331. window.onblur = function(){windowBlur = true;}
  1332. window.onfocus = function(){windowBlur = false;}
  1333. $('iframe').each(function()
  1334. {
  1335. let iframeID = $(this).attr('id');
  1336. let $this = document.getElementById(iframeID);
  1337. if(document.all)
  1338. {
  1339. $this.onblur = function(){windowBlur = true;}
  1340. $this.onfocus = function(){windowBlur = false;}
  1341. }
  1342. else
  1343. {
  1344. $this.contentWindow.onblur = function(){windowBlur = true;}
  1345. $this.contentWindow.onfocus = function(){windowBlur = false;}
  1346. }
  1347. });
  1348. }
  1349. $.get($.createLink('message', 'ajaxGetMessage', "windowBlur=" + (windowBlur ? '1' : '0')), function(data)
  1350. {
  1351. if(!data) return;
  1352. if(typeof data == 'string') data = JSON.parse(data);
  1353. for(i in data.messages)
  1354. {
  1355. let message = data.messages[i];
  1356. if(windowBlur)
  1357. {
  1358. if(typeof message.text == 'string') notifyMessage(message);
  1359. }
  1360. else
  1361. {
  1362. if(!$(message).hasClass('browser-message-content')) continue;
  1363. zui.Messager.show(
  1364. {
  1365. content: {html: message},
  1366. placement: 'bottom-right',
  1367. time: 0,
  1368. icon: 'envelope-o',
  1369. className: 'bg-secondary-50 text-secondary-600 messager-notice w-96'
  1370. });
  1371. }
  1372. }
  1373. let unreadCount = parseInt(data.newCount);
  1374. let showCount = data.showCount != '0';
  1375. let dotStyle = 'padding: 2px;';
  1376. let rightStyle = showCount ? 'right: -10px;' : 'right: -2px;';
  1377. if(!showCount) dotStyle += 'width: 5px; height: 5px;';
  1378. if(unreadCount < 10 && showCount) rightStyle = 'right: -5px;';
  1379. if(unreadCount > 99) unreadCount = '99+';
  1380. dotStyle += showCount ? 'top: -3px; aspect-ratio: 0;' : 'top: -2px; aspect-ratio: 1 / 1;';
  1381. dotStyle += rightStyle;
  1382. let dotHtml = '<span class="label danger label-dot absolute' + (showCount ? ' rounded-sm' : '') + '" style="' + dotStyle + '">' + (showCount ? unreadCount : '') + '</span>';
  1383. $('#apps .app-container').each(function()
  1384. {
  1385. let $iframeMessageBar = $(this).find('iframe').contents().find('#messageBar');
  1386. if($iframeMessageBar.length > 0)
  1387. {
  1388. $iframeMessageBar.find('.label-dot.danger').remove();
  1389. if(unreadCount) $iframeMessageBar.append(dotHtml);
  1390. }
  1391. let $oldPage = $(this).find('iframe').contents().find('#oldPage');
  1392. if($oldPage.length > 0)
  1393. {
  1394. $iframeMessageBar = $oldPage.find('iframe').contents().find('#messageBar');
  1395. if($iframeMessageBar.length == 0) return;
  1396. $iframeMessageBar.find('.label-dot.danger').remove();
  1397. if(unreadCount) $iframeMessageBar.append(dotHtml);
  1398. }
  1399. });
  1400. });
  1401. }, pollTime * 1000);
  1402. };
  1403. window.clickMessage = function(obj)
  1404. {
  1405. let $obj = $(obj);
  1406. let url = $obj.attr('data-url').replace(/\?onlybody=yes/g, '').replace(/\&onlybody=yes/g, '');
  1407. let messageID = $obj.parent().attr('data-id');
  1408. openApp(url);
  1409. $.get($.createLink('message', 'ajaxMarkRead', 'id=' + messageID));
  1410. $(obj).closest('.alert.messager').find('.alert-close').trigger('click');
  1411. }
  1412. window.ping = function()
  1413. {
  1414. setInterval(function(){$.get($.createLink('misc', 'ping'));}, pollTime * 1000);
  1415. }
  1416. window.startCron = function(restart)
  1417. {
  1418. if(typeof(restart) == 'undefined') restart = 0;
  1419. $.ajax({type:"GET", timeout:100, url:$.createLink('cron', 'ajaxExec', 'restart=' + restart)});
  1420. }
  1421. turnon ? browserNotify() : ping();
  1422. if(runnable) startCron();
  1423. if(scoreNotice) zui.Messager.show({ content: {html: scoreNotice}, placement: 'bottom-right', time: 0, icon: 'diamond', className: 'bg-secondary-50 text-secondary-600 score-notice'});
  1424. /* Handle clicking outside. */
  1425. $(document).on('click', e =>
  1426. {
  1427. if($(e.target).attr('id') != 'bizLink' && !$(e.target).parents('#bizLink').length)
  1428. {
  1429. $('#upgradeContent').hide();
  1430. $('#bizLink').removeClass('active');
  1431. }
  1432. });
  1433. const allAppsItemsMap = new Map();
  1434. $(document).ready(
  1435. function()
  1436. {
  1437. for(const item of allAppsItems)
  1438. {
  1439. if(item === 'divider') continue;
  1440. allAppsItemsMap.set(item.code, item);
  1441. }
  1442. }
  1443. );
  1444. /**
  1445. * Get icon and title of app item.
  1446. *
  1447. * @param {string} name app name
  1448. * @returns {[string, string]}
  1449. */
  1450. function getAppItemIconAndTitle(name)
  1451. {
  1452. if(!allAppsItemsMap.has(name)) return[];
  1453. const item = allAppsItemsMap.get(name);
  1454. const str = item.title;
  1455. const regex = /class=["']icon (\S*)["']\>\<\/i\>\s(\S*)/;
  1456. const matches = str.match(regex);
  1457. if(matches)
  1458. {
  1459. const icon = matches[1];
  1460. const text = matches[2].trim();
  1461. return [icon, text];
  1462. }
  1463. return [];
  1464. }
  1465. /**
  1466. * Add menu item to #mainNav callback, used by generateAddMenuNavItems.
  1467. *
  1468. * @param {Cash} $li menu item li
  1469. * @returns {(name: string) => void}
  1470. */
  1471. function addMenuToMainNavCb($ele) {
  1472. return (name) => {
  1473. const $li = $ele.closest('li');
  1474. if(name === 'divider')
  1475. {
  1476. $li.after('<li class="divider"></li>');
  1477. refreshMenu();
  1478. return
  1479. }
  1480. let item = allAppsItemsMap.get(name);
  1481. const oldItem = apps.map[item.code];
  1482. if(oldItem !== item && oldItem) item = $.extend({}, oldItem, item, {active: false});
  1483. item.external = item.external || item.url && item.url.includes('://');
  1484. const $link= $('<a data-pos="menu"></a>')
  1485. .attr('data-app', item.notApp ? undefined : item.code)
  1486. .attr('href', item.url || '#')
  1487. .attr('target', item.notApp ? '_blank' : undefined)
  1488. .addClass('rounded' + (item.notApp ? '' : ' show-in-app'))
  1489. .html(item.title, false);
  1490. item.icon = item.icon || ($link.find('.icon').attr('class') || '').replace('icon ', '');
  1491. item.text = $link.text().trim();
  1492. $link.html('<i class="icon ' + item.icon + '"></i><span class="text">' + item.text + '</span>', false);
  1493. if(['devops', 'bi', 'safe'].includes(item.code)) $link.find('.text').addClass('font-brand');
  1494. apps.map[item.code] = item;
  1495. $('<li class="hint-right is-original"></li>')
  1496. .attr({'data-app': item.code, 'data-hint': item.text})
  1497. .append($link)
  1498. .insertAfter($li);
  1499. refreshMenu();
  1500. if(!apps.defaultCode) apps.defaultCode = item.code;
  1501. };
  1502. }
  1503. /**
  1504. * Check whether current element can add a divider.
  1505. * @param {Cash} $item
  1506. * @returns {boolean}
  1507. */
  1508. function canAddDivider($item)
  1509. {
  1510. $item = $item.closest('li');
  1511. if($item.is('.divider')) return false;
  1512. if($item.next().not('.hidden').is('.divider')) return false;
  1513. if($item.is(':last-child')) return false;
  1514. return true;
  1515. }