/* Init variables */ const apps = { /** @type {Record} */ map: {}, /** @type {Record} */ openedMap: {}, defaultCode: '', zIndex: 10, frameContent: null, theme: null, oldPages: new Set(oldPages) }; const DEBUG = config.debug; function getUrlID(url) { const info = $.parseLink(url); return info.moduleName ? `${info.moduleName}-${info.methodName}` : ''; } function showLog(code, name, moreTitles, trace, moreInfos) { const titles = ['%c HOME ']; const titleColors = ['color:#fff;font-weight:bold;background:#009688']; if(code) { titles.push(`%c ${code} `); titleColors.push('background:rgba(0,150,136,0.2);color:#009688;'); } if(name) { titles.push(`%c ${name} `); titleColors.push('color:#009688;font-weight:bold;'); } if(!Array.isArray(moreTitles)) moreTitles = [moreTitles]; if(typeof moreTitles[0] === 'string' && (moreTitles[0].startsWith('success:') || moreTitles[0].startsWith('error:'))) { const message = moreTitles.shift(); const [type, content] = message.split(':', 2); titles.push(`%c ${content} `); titleColors.push(`color:${type === 'error' ? '#f56c6c' : '#67c23a'};`); } if(trace || moreInfos) { console.groupCollapsed(titles.join(''), ...titleColors, ...moreTitles); if(trace) console.trace(trace); if(moreInfos) { if($.isPlainObject(moreInfos)) Object.keys(moreInfos).forEach((infoName) => console.log(infoName, moreInfos[infoName])); else console.log(moreInfos); } console.groupEnd(); } else { console.log(titles.join(''), ...titleColors, ...moreTitles); } } function triggerAppEvent(code, event, args, options) { const app = apps.openedMap[code]; if(!app) return; event = event.includes('.') ? event : `${event}.apps`; if(DEBUG && (!options || options.silent !== true)) showLog(code, 'Event', event, {args}); if(!Array.isArray(args)) args = [args]; if(app.$app) app.$app.trigger(event, args); try { const iframeWindow = app.iframe && app.iframe.contentWindow; if(iframeWindow) return iframeWindow.$(iframeWindow.document).trigger(event, args); } catch(e){} } function isOldPage(url) { if(typeof url !== 'object') url = $.parseLink(url); return apps.oldPages.has(url.moduleName.toLowerCase()) || apps.oldPages.has(`${url.moduleName}-${url.methodName}`.toLowerCase()); } /** * 提取纯文本标签(去除 HTML 标签) * @param {string} html * @returns {string} */ function extractLabelText(html) { return html.replace(/<[^>]*>/g, '').trim(); } /** * 获取所有应用列表。 * @returns {Array<{id: string, label: string, active: boolean}>} */ function getApps() { const lastAppCode = getLastAppCode(); return Array.from(allAppsItemsMap.values()).map(app => ({ id: app.code, label: extractLabelText(window.appsItems.find(item => item.code === app.code)?.title), active: lastAppCode === app.code })); } /** * 获取所有可见的应用列表。 * @returns {Array<{id: string, label: string, active: boolean}>} */ function getVisibleApps() { const $mainNav = $('#menuMainNav'); return getApps().filter(app => { const $menuItem = $mainNav.find(`li[data-app="${app.id}"]`); return $menuItem.length > 0 && !$menuItem.is('[data-hidden="1"]'); }); } /** * 获取所有已打开的应用列表。 * @returns {Array<{id: string, label: string, active: boolean}>} */ function getOpenedApps() { const lastAppCode = getLastAppCode(); return Object.values(apps.openedMap).map(app => ({ id: app.code, label: extractLabelText(window.appsItems.find(item => item.code === app.code)?.title), active: app.code === lastAppCode })); } /** * 触发 onChangeApp 回调函数。 */ function changeApp() { const callback = window.zinCallbacks.onChangeApp; if(typeof callback === 'function') { try { callback(getVisibleApps(), getOpenedApps()); } catch (e) { console.error('[ZIN] onChangeApp callback error:', e); } } } /** * Open app * @param {string} url * @param {string|object} [code] * @param {boolean|object} [options] * @returns {ZentaoOpenedApp|undefined} */ function openApp(url, code, options) { if(!apps.frameContent) { setTimeout(() => {openApp(url, code, options);}, 100); return; } const loadOptions = {}; if(typeof code === 'object') $.extend(loadOptions, code); else if(code) loadOptions.code = code; if(typeof options === 'boolean') loadOptions.forceReload = options; else if(typeof options === 'object') $.extend(loadOptions, options); options = loadOptions; code = options.code; let forceReload = options.forceReload !== false; delete options.forceReload; delete options.code; if(!code) { if(apps.map[url]) { code = url; url = ''; } else if(url) { url = url.replace('http:', window.location.protocol).replace('https:', window.location.protocol); code = getAppCode(url); } if(!code) return openApp('my'); } const app = apps.map[code]; if(!app) { if(DEBUG) showLog(code, 'APP NOT FOUND!', url, options); return zui.Modal.alert(appNotFound); } let openedApp = apps.openedMap[code]; if($.apps.workspace && $.apps.workspace !== code) { url = url || (openedApp ? openedApp.currentUrl : app.url); if(url) window.open(url, '_blank'); return; } /* Create iframe for app */ if(!openedApp) { if(!url) url = app.url; openedApp = $.extend({opened: true, url: url, zIndex: ++apps.zIndex, currentUrl: url}, app); forceReload = false; apps.openedMap[code] = openedApp; const $iframe = $([ '' ].join(' ')); const iframe = $iframe[0]; openedApp.iframe = iframe; openedApp.$app = $('
') .append($iframe) .appendTo('#apps'); const finishLoad = () => { $iframe.removeClass('loading').addClass('in'); setTimeout(() => {openedApp.$app.removeClass('loading');}, 300); }; if(app.external) { $iframe.on('ready.app', () =>{openApp(url, code, options);}); } else { const writeToDoc = () => { iframe.contentDocument.open(); const html = apps.frameContent.replace('window.defaultAppUrl = ""', `window.defaultAppUrl = "${url}"`); iframe.contentDocument.write(html); iframe.contentDocument.close(); }; if(!iframe.contentDocument.body.children.length) setTimeout(() => writeToDoc(), 500); } iframe.onload = iframe.onreadystatechange = function(e) { try { iframe.contentWindow.$(iframe.contentDocument).one('pageload.app pagecaheload.app', finishLoad); setTimeout(finishLoad, 10000); } catch(e){finishLoad()} triggerAppEvent(openedApp.code, 'loadapp', [openedApp, e]); }; openedApp.$app.show().css('z-index', openedApp.zIndex); } if(!url) url = openedApp.currentUrl; /* Set tab cookie */ $.cookie.set('tab', code, {expires: config.cookieLife, path: config.webRoot}); /* Highlight on left menu */ if(!openedApp.workspace) { const $menuNav = $('#menuMainNav,#menuMoreNav'); const $lastItem = $menuNav.find('li>a.active'); if($lastItem.data('app') !== code) { $lastItem.removeClass('active'); $menuNav.find('li[data-app="' + code + '"]>a').addClass('active'); openedApp.$app.trigger('showapp', openedApp); } } /* Show and load app */ const isSameUrl = $.parseLink(openedApp.currentUrl).url === $.parseLink(url).url; const needLoad = !isSameUrl || forceReload !== false; if(needLoad) { const openFromHidden = openedApp.zIndex < apps.zIndex; reloadApp(code, url, openFromHidden ? $.extend({loadingIndicatorDelay: '0s'}, options) : options); openedApp.$app.toggleClass('open-from-hidden', openFromHidden); } else { updateApp(code, url, openedApp.currentTitle, 'show'); } $('body').attr('data-app', code); openedApp.zIndex = ++apps.zIndex; openedApp.$app.show().css('z-index', openedApp.zIndex); openedApp.getPageInfo = () => { const getPageInfo = openedApp.iframe.contentWindow.getPageInfo; return getPageInfo ? getPageInfo() : null; }; openedApp.getPerfData = () => { const getPerfData = openedApp.iframe.contentWindow.getPerfData; return getPerfData ? getPerfData() : null; }; /* Update on app tabs bar */ const $tabs = $('#appTabs'); let $tabItem = $('#appTab-' + code); if(!$tabItem.length) { if (app.text === undefined) return false; const $link= $('') .attr('data-app', code) .addClass('show-in-app') .append($('').text(app.text)); $tabItem = $('') .attr({'data-app': code, id: 'appTab-' + code}) .append($link) .appendTo($tabs); openedApp.$bar = $tabItem; } const $lastTab = $tabs.find('li>a.active'); if($lastTab.data('app') !== code) { $lastTab.removeClass('active').parent().removeClass('active'); $tabs.find('li[data-app="' + code + '"]>a').addClass('active').parent().addClass('active'); } if(DEBUG) showLog(code, 'Open', getUrlID(url), openedApp, {options, forceReload, needLoad}); triggerAppEvent(code, 'openapp', [openedApp, {load: needLoad}]); changeApp(); const spaceType = openedApp.workspace ? openedApp.workspace.type : ''; if(spaceType !== $('#menu').attr('data-space')) updateSpaceMenu(openedApp.workspace); return openedApp; } /** * Show app * @param {string} code */ function showApp(code) { return openApp('', code, false); } /** * Reload app * @param {string} code * @param {string} url * @param {object} options */ function reloadApp(code, url, options) { const app = apps.openedMap[code]; if(!app) return; if(url === true) url = app.url; else if(!url) url = app.currentUrl; const iframe = app.iframe; try { if(app.external) iframe.src = url; else if(iframe.contentWindow.loadPage) iframe.contentWindow.loadPage(url, $.extend({updateHeading: true}, options)); else if(DEBUG) showLog(code, 'Reload', 'error:Cannot load page when iframe is not ready.', {options}); else console.error('[APPS]', 'Reload failed: Cannot load page when iframe is not ready.'); } catch(error) { iframe.src = url; } app.currentUrl = url; changeApp(); } function updateApp(code, url, title, type) { const app = apps.openedMap[code]; if(!app || app.external) return; const state = typeof code === 'object' ? code : {code: code, url: url, title: title, type: type}; const prevState = window.history.state; const urlInfo = $.parseLink(state.url); state.prev = prevState; state.index = prevState ? prevState.index + 1 : 1; state.path = urlInfo.methodName ? `${urlInfo.moduleName}-${urlInfo.methodName}` : ''; if(title) { document.title = title; app.currentTitle = title; } if(prevState && prevState.code === code && prevState.url === url) return prevState; app.currentUrl = url; const urlParts = url.split('#'); const hash = urlParts.length > 1 ? urlParts[1] : ''; const browserUrl = getAppCode(urlParts[0]) !== code ? `${urlParts[0]}#${hash.length ? `${hash}&` : ''}app=${code}` : url; window.history.pushState(state, title, browserUrl); zui.store.session.set('lastOpenApp', {code, url}); triggerAppEvent(code, 'updateapp', [code, url, title, type]); if(DEBUG) showLog(code, 'Update', title || getUrlID(url), state, {url, title, type}); return state; } /** * Get last opened app * @returns {object} The opened app info object */ function getLastApp() { let lastShowIndex = 0; let lastApp = null; $.each(apps.openedMap, function(_code, app) { if(lastShowIndex < app.zIndex && !app.closed) { lastShowIndex = app.zIndex; lastApp = app; } }); return lastApp; } /** Get last opened app code. */ function getLastAppCode() { const lastApp = getLastApp(); return lastApp ? lastApp.code : null; } /** * Close app * @param {string} code * @returns {ZentaoOpenedApp|undefined|false} */ function closeApp(code) { code = code || getLastAppCode(); const app = apps.openedMap[code]; if(!app) return; const iframe = app.iframe; if(iframe) { if(iframe && iframe.contentDocument && iframe.contentWindow && iframe.contentWindow.onCloseApp) { var result = iframe.contentWindow.onCloseApp(); if(result === false) return false; } } $('#appTabs a.active[data-app="' + code + '"]').parent().remove(); app.closed = true; app.$app.trigger('hideapp', app); app.$app.remove(); app.$bar.remove(); hideApp(code); delete apps.openedMap[code]; triggerAppEvent(code, 'closeapp', app); return app; } /** * Hide app * @param {string} code * @returns {ZentaoOpenedApp|undefined} */ function hideApp(code) { code = code || getLastAppCode(); const app = apps.openedMap[code]; if(!app) return; $('#menuNav a.active[data-app="' + code + '"]').removeClass('active'); if(!app.closed) triggerAppEvent(code, 'hideapp', app); app.$app.hide(); /* Active last app */ const lastApp = getLastApp(true) || getLastApp(); showApp(lastApp ? lastApp.code : apps.defaultCode); return app; } /** * Get app code from url * @param {String} urlOrModuleName Url string * @return {String} */ function getAppCode(urlOrModuleName, defaultCode) { var code = navGroup[urlOrModuleName]; if(code) return code; var link = $.parseLink(urlOrModuleName); if(!link.moduleName || link.isOnlyBody || (link.moduleName === 'index' && link.methodName === 'index')) return ''; if(link.hash && link.hash.indexOf('app=') === 0) return link.hash.substr(4); /* Handling special situations */ var moduleName = link.moduleName; var methodName = link.methodName; var moduleMethodLower = (moduleName + '-' + methodName).toLowerCase(); if(moduleMethodLower === 'index-index') return 'my'; if(moduleMethodLower === 'search-buildindex') return 'admin'; if(moduleName === 'tutorial' && methodName === 'wizard') { moduleName = link.vars[0][1]; methodName = link.vars[1][1]; } var methodLowerCase = methodName.toLowerCase(); if(moduleName === 'doc') { if(link.prj) return 'project'; if((link.params.from || link.params.$3) == 'product') { if(['objectlibs', 'showfiles', 'browse', 'view', 'edit', 'delete', 'create'].includes(methodLowerCase)) return 'product'; } return 'doc'; } if(['caselib', 'testreport', 'testsuite', 'testtask', 'testcase', 'bug', 'qa'].includes(moduleName)) { return link.prj ? 'project' : 'qa'; } if(moduleName === 'report') { if(['usereport', 'editreport', 'deletereport', 'custom'].includes(methodLowerCase) && link.params.from) return 'system'; if(methodName == 'projectsummary') return 'project'; return 'bi'; } if(moduleName === 'story' && vision === 'lite') return 'project' if(moduleName === 'testcase' && methodLowerCase === 'zerocase') { return link.params.from == 'project' ? 'project' : 'qa'; } if(moduleName === 'execution' && methodLowerCase === 'all') { return (link.params.from || link.params.$3) == 'project' ? 'project' : 'execution'; } if(moduleName === 'issue' || moduleName === 'risk' || moduleName === 'opportunity' || moduleName === 'pssp' || moduleName === 'auditplan' || moduleName === 'meeting' || moduleName === 'nc') { if(link.params.$2 == 'my' || link.params.from == 'my') return 'my'; if(link.params.$2 == 'project' || link.params.from == 'project') return 'project'; if(link.params.$2 == 'execution' || link.params.from == 'execution') return 'execution'; } if(moduleName === 'product') { if(methodLowerCase === 'create' && (link.params.programID || link.params.$1)) return 'program'; if(methodLowerCase === 'edit' && (link.params.programID || link.params.$4)) return 'program'; if(methodLowerCase === 'batchedit') return 'program'; var moduleGroup = link.params.moduleGroup ? link.params.moduleGroup : link.params.$2; if(methodLowerCase === 'showerrornone' && moduleGroup) return moduleGroup; } if(moduleName === 'stakeholder') { if(methodLowerCase === 'create' && (link.params.programID || link.params.$1)) return 'program'; } if(moduleName === 'user') { if(['todo', 'todocalendar', 'effortcalendar', 'effort', 'task', 'todo', 'story', 'bug', 'testtask', 'testcase', 'execution', 'dynamic', 'profile', 'view', 'issue', 'risk'].includes(methodLowerCase)) return 'system'; } if(moduleName === 'my') { if(['team'].includes(methodLowerCase)) return 'system'; } if(moduleName === 'company') if(methodLowerCase == 'browse') return 'admin'; if(moduleName === 'opportunity' || moduleName === 'risk' || moduleName == 'trainplan') if(methodLowerCase == 'view') return 'project'; if(moduleName === 'tree') { if(methodLowerCase === 'browse') { var viewType = link.params.view || link.params.$2; if(['bug', 'case', 'caselib'].includes(viewType)) return link.params.$5 === 'project' ? 'project' : 'qa'; if(viewType === 'doc' && (link.params.from === 'product' || link.params.$5 == 'product')) return 'product'; if(viewType === 'doc' && (link.params.from === 'project' || link.params.$5 == 'project')) return 'project'; if(viewType === 'doc') return 'doc'; if(viewType === 'story') return 'product'; if(viewType === 'host') return 'admin'; if(viewType === 'ticket') return 'feedback'; } if(methodLowerCase === 'browsetask') return 'execution'; if(methodLowerCase === 'browsegroup') return 'bi'; } if(['ai', 'zai', 'aiteammate', 'aitask'].includes(moduleName)) return 'aiapp'; code = navGroup[moduleName] || moduleName || urlOrModuleName; return apps.map[code] ? code : defaultCode; } /** * Search history and go back to specified path. * * @param {string} target Back target, can be app name or module-method path. * @param {string} url Fallback url. * @param {object} startState Start state. * @returns {void} */ function goBack(target, url, startState) { const currentState = window.history.state; const preState = currentState && currentState.prev; if(DEBUG) showLog(null, 'Go back', target, null, {url, startState, currentState, preState}); if(target && startState && currentState && preState) { startState = startState.prev || (currentState && currentState.prev); if($.apps.openedMap[target]) { let state = startState; while(state && state.code !== target) state = state.prev; if(state && state.code === target) { if(state.index === preState.index) { if(url) return openApp(url, target, false); return window.history.back(); } return openApp(state.url, state.code, false); } } else { const pathSet = new Set(target.toLowerCase().split(',')); let state = startState; while(state && state.path && !pathSet.has(state.path.toLowerCase())) state = state.prev; if(state && pathSet.has(state.path.toLowerCase())) { return openApp(state.url, state.code, false); } } } if(url) return openApp(url); if(target && !target.endsWith('APP')) { if($.apps.openedMap[target]) return openApp(target); if(target.includes('-')) { const parts = target.split(',').shift().split('-'); return openApp($.createLink(parts[0], parts[1])); } } window.history.back(); } /** * Logout current user. * * @param {string} url */ function logout(url) { if(!url) url = getLastApp().currentUrl.replace(/^https?:\/\/[^/]+/, ''); $.get($.createLink('user', 'logout', 'referer=' + btoa(url)), function(data) { let load = $.createLink('user', 'login'); try { data = JSON.parse(data); if(data.load != 'login') load = data.load; } catch (error) {} location.href = load; }); } /** * Toggle left menu * @param {boolean} [toggle] * @returns {boolean} */ function toggleMenu(toggle) { var $body = $('body'); if (toggle === undefined) toggle = $body.hasClass('hide-menu'); $body.toggleClass('hide-menu', !toggle).toggleClass('show-menu', !!toggle); $.cookie.set('hideMenu', String(!toggle), {expires: config.cookieLife, path: config.webRoot}); return toggle; } /** * Refresh more menu in #menuNav * @return {void} */ function refreshMenu() { const $mainNav = $('#menuMainNav'); const $list = $('#menuMoreList'); const $menuNav = $('#menuNav'); const hasSpace = $('body').hasClass('has-space'); const $menuItems = $mainNav.children(hasSpace ? '.is-space' : '.is-original'); const itemHeight = $menuItems.filter((_, ele) => { const $ele = $(ele); return $ele.css('display') !== 'none' && !$ele.hasClass('divider'); }).first().outerHeight(); const maxHeight = $('#menu').outerHeight() - ($('body').hasClass('has-space') ? (($('#spaceHeading').outerHeight() || 0) + 8) : 0) - 24 - $('#menuToggleNav').outerHeight(); const dividerHeight = 13; let showMoreMenu = false; let currentHeight = 0; let $firstHiddenItem = null; const moreBtnHeight = itemHeight + dividerHeight; $menuItems.each(function() { let $item = $(this); let isDivider = $item.hasClass('divider'); let height = isDivider ? dividerHeight : itemHeight; if(showMoreMenu || (currentHeight + height) > maxHeight) { if(!$firstHiddenItem) $firstHiddenItem = $item; $item.addClass('hidden'); if(!showMoreMenu) { showMoreMenu = true; $list.empty(); } $list.append($item.clone().toggleClass('menu-item', !isDivider).removeClass('hidden')); } else { currentHeight += height; $item.removeClass('hidden'); } }); if(showMoreMenu && $firstHiddenItem && (currentHeight + moreBtnHeight) > maxHeight) { const $prevItem = $firstHiddenItem.prev(); if($prevItem.length) { $prevItem.addClass('hidden'); let isDivider = $prevItem.hasClass('divider'); $list.prepend($prevItem.clone().toggleClass('menu-item', !isDivider).removeClass('hidden')); } } let moreMenuHeight = 16; let nextNoDivider = true; $list.children().each(function() { const $item = $(this); const isDivider = $item.hasClass('divider'); if(isDivider && nextNoDivider) { $item.addClass('hidden'); return; } nextNoDivider = isDivider; moreMenuHeight += isDivider ? dividerHeight : itemHeight; }); nextNoDivider = true; let $lastItem = null; $menuItems.each(function() { const $item = $(this); if($item.hasClass('hidden')) return; const isDivider = $item.hasClass('divider'); if(isDivider && nextNoDivider) { $item.addClass('hidden'); return; } nextNoDivider = isDivider; $lastItem = $item; }); if($lastItem && $lastItem.hasClass('divider')) $lastItem.addClass('hidden'); $menuNav.toggleClass('show-more-nav', showMoreMenu); if(showMoreMenu && !$list.data('listened-click')) { $list.data('listened-click', true).on('click', function() { $list.addClass('hidden'); setTimeout(function(){$list.removeClass('hidden')}, 200); }); } } /** Init apps menu list. */ function initAppsMenu(items) { if($.cookie.get('workspace')) { $('#menu').addClass('waiting-workspace'); $(document).on('openapp', () => { if($.cookie.get('workspace') === getLastAppCode()) return; $('#menu').removeClass('waiting-workspace'); }); } apps.map.help = { code: 'help', icon: 'icon-help', url: manualUrl || $helpLink.attr('href'), external: true, text: manualText || $helpLink.text(), appUrl: config.webRoot + '#app=help' }; const $menuMainNav = $('#menuMainNav').empty(); (items || appsItems).forEach(function(item) { const oldItem = apps.map[item.code]; if(item === 'divider') return $menuMainNav.append('
  • '); if(oldItem !== item && oldItem) item = $.extend({}, oldItem, item, {active: oldItem.active}); item.external = item.external || item.url && item.url.includes('://'); const $link= $('') .attr('data-app', item.notApp ? undefined : item.code) .attr('href', item.url || '#') .attr('target', item.notApp ? '_blank' : undefined) .addClass('rounded' + (item.notApp ? '' : ' show-in-app')) .html(item.title, false); item.icon = item.icon || ($link.find('.icon').attr('class') || '').replace('icon ', ''); item.text = $link.text().trim(); $link.html('', false); $link.find('.text').toggleClass('font-brand', ['devops', 'bi', 'safe'].includes(item.code)).attr('title', item.text).text(item.text); apps.map[item.code] = item; $('
  • ') .attr({'data-app': item.code, 'data-hint': item.text}) .append($link) .appendTo($menuMainNav); const hasLongText = $link.find('.text').width() > ($link.outerWidth() - 24); $link.toggleClass('has-long-text', hasLongText).find('.text').attr('title', hasLongText ? item.text : null); if(!apps.defaultCode) apps.defaultCode = item.code; }); /* 隐藏的App依然可以通过输入URL的形式打开。 */ allAppsItems.forEach(function(item) { if(item.code && !apps.map[item.code]) apps.map[item.code] = item; }); const lastApp = getLastApp(); if(lastApp) $menuMainNav.find('li[data-app="' + lastApp.code + '"]>a').addClass('active'); apps.map.search = { opened: false, code: 'search', group: 'search', icon: 'icon-search', methodName: 'index', moduleName: 'search', text: langData.search, title: ' ' + langData.search, url: '/index.php?m=search&f=index', vars: '' }; $('#appTabs').find('.nav-item').each(function() { const $item = $(this); const code = $item.data('app'); const app = apps.map[code]; if(!app) return; $item.find('.text').text(app.text); }); const selectLangCallback = window.zinCallbacks.onSelectLang; if(typeof selectLangCallback === 'function') { try { selectLangCallback(window.config.clientLang); } catch (e) { console.error('[ZIN] onSelectLang callback error:', e); } } const selectVisionCallback = window.zinCallbacks.onSelectVision; if(typeof selectVisionCallback === 'function') { try { selectVisionCallback(window.getVisions()); } catch (e) { console.error('[ZIN] onSelectVision callback error:', e); } } refreshMenu(); } /** Update apps menu. */ function updateAppsMenu(includeAppsBar) { loadCurrentPage( { selector: (includeAppsBar ? '#menuMoreBtn>*,#appsToolbar>*,#visionSwitcher>*,' : '') + '#appsItemsData,#configJS,#searchItemsData,#userVisionData', complete: () => initAppsMenu(window.appsItems), }); } function changeAppsLang(lang) { $('html').attr('lang', lang); zui.i18n.setCode(lang); $.each(apps.openedMap, function(_code, app) { if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.changeAppLang) { app.iframe.contentWindow.changeAppLang(lang); } }); if(!$.apps.workspace) updateAppsMenu(true); } function changeAppsTheme(theme) { changeAppTheme(theme); $.each(apps.openedMap, function(_code, app) { if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.changeAppTheme) { app.iframe.contentWindow.changeAppTheme(theme); } }); apps.theme = theme; $.get($.createLink('index', 'app'), html => { apps.frameContent = html; apps.theme = null; }); } function updateUserToolbar() { $.each(apps.openedMap, function(_code, app) { if(app.iframe && app.iframe.contentWindow && app.iframe.contentWindow.loadPage) { app.iframe.contentWindow.loadPage({selector: '#toolbar', partial: true, target: '#toolbar'}); } }); } initAppsMenu(); /* Refresh more menu on window resize */ $(window).on('resize', refreshMenu); setTimeout(refreshMenu, 500); /* Bind event for menut-toggle */ $(document).on('click', '.menu-toggle', () => toggleMenu()); toggleMenu(!$('body').hasClass('hide-menu')); /** * Get current menu nav data. * * @returns {Array<{name: string; order: number;}>} */ function getMenuNavData() { const data = []; const $nav = $('#menuMainNav'); const workspace = $.apps.workspace; $nav.children($.apps.workspace ? '.is-space' : '.is-original').each(function(index, element) { const $elm = $(element); if($elm.hasClass('rsh-more')) return; const menuItem = {}; menuItem.name = $elm.is('.divider') ? 'divider' : $elm.data(workspace ? 'name' : 'app'); menuItem.order = index * 5; if(typeof $elm.data('hidden') != 'undefined') menuItem.hidden = true; data.push(menuItem); }); return data; } /** * Save menu nav to server. */ function saveMenuNavToServer() { const url = $.createLink('custom', 'ajaxSetMenu'); const data = getMenuNavData(); let menu = 'nav'; if($.apps.workspace) { const currentApp = getLastApp(); if(currentApp.workspace.type === $.apps.workspace) menu = currentApp.workspace.menuGroup; } $.ajaxSubmit({url, data: {menu: menu, items: JSON.stringify(data)}}); } /** * Restore menu nav to server. */ function restoreMenuNavToServer() { let menu = 'nav'; if($.apps.workspace) { const currentApp = getLastApp(); if(currentApp.workspace.type === $.apps.workspace) menu = currentApp.workspace.menuGroup; } const url = $.createLink('custom', 'ajaxRestoreMenu'); $.ajaxSubmit({url, data: {menu: menu}}); top.location.reload(); } /** * Generate menu nav items to be added. * * @param {Cash} $item * @param {(item: string) => void} onClick click handler of menu item. * @returns {Array<{icon: string; text: string; onClick: () => void;}>} */ function generateAddMenuNavItems($item) { const onClick = addMenuToMainNavCb($item); const items = []; const $li = $item.closest('li'); if(canAddDivider($li)) items.push({ icon: 'icon-minus', text: langData.divider, onClick: () => { onClick('divider'); saveMenuNavToServer(); } }); const data = getMenuNavData(); const allAppCodeSet = new Set(allAppsItemsMap.keys()); for(const {name} of data) { if(name === 'divider') continue; allAppCodeSet.delete(name); } if(allAppCodeSet.size === 0) return items; for(const name of allAppCodeSet) { const [icon, title] = getAppItemIconAndTitle(name); items.push({ icon, text: title, onClick: () => { onClick(name); saveMenuNavToServer(); } }); } return items; } function addMenuToSpaceNavCb($li, info) { return (name) => { if(name === 'divider') { $li.after('
  • '); refreshMenu(); return } const fromName = $li.attr('data-name'); const item = info.itemMap.get(name); if(!item) return; if(name === 'divider') { const index = info.items.findIndex(item => item.code === fromName); info.items.splice(index, 0, {type: 'divider'}); $li.after('
  • '); } else { const oldIndex = info.items.findIndex(item => item.code === name); info.items.splice(oldIndex, 1); const newIndex = info.items.findIndex(item => item.code === fromName); info.items.splice(newIndex, 0, item); item.hidden = false; updateSpaceMenu(info); } }; } function generateAddSpaceNavItems($ele) { const currentApp = getLastApp(); const info = currentApp.workspace; const $li = $ele.closest('li'); if(!info || !$li.length) return []; const handleItemClick = (name) => { const fromName = $li.attr('data-name'); if(name === 'divider') { const index = info.items.findIndex(item => item.code === fromName); info.items.splice(index, 0, {type: 'divider'}); $li.after('
  • '); } else { const item = info.itemMap.get(name); if(!item) return; const oldIndex = info.items.findIndex(item => item.code === name); info.items.splice(oldIndex, 1); const newIndex = info.items.findIndex(item => item.code === fromName); info.items.splice(newIndex + 1, 0, item); item.hidden = false; updateSpaceMenu(info); } saveMenuNavToServer(); }; const items = []; if(canAddDivider($li)) items.push({ icon: 'icon-minus', text: langData.divider, onClick: () => handleItemClick('divider') }); info.items.forEach((item) => { if(!item.hidden) return; items.push({ icon: `icon-app-empty ${item.icon || `icon-${item.code}`}`, text: item.text, onClick: () => handleItemClick(item.code) }); }); return items; } function updateSpaceMenu(info) { const spaceType = info ? info.type : ''; const currentApp = getLastApp(); const currentCode = currentApp.code; const hasSpaceNav = spaceType === currentCode; const $menuMainNav = $('#menuMainNav'); const $menu = $('#menu'); $.apps.workspace = spaceType; currentApp.workspace = info; $menu.attr('data-space', spaceType); $('body').toggleClass('has-space', hasSpaceNav); $menuMainNav.children('.is-space').remove(); if(!hasSpaceNav) { refreshMenu(); if(info !== undefined) $menu.removeClass('waiting-workspace'); return; } const $spaceHeading = $('#spaceHeading').toggleClass('has-dropmenu', !!info.dropmenu); if(info.dropmenu) { const $dropmenu = $('
    '); $spaceHeading.find('.text').empty().append($dropmenu); const options = zui.evalValue(info.dropmenu, ['_element', $dropmenu[0]], ['_$dropmenu', $dropmenu]); zui.create('dropmenu', $dropmenu[0], $.extend({icon: info.icon || spaceType, display: zui.jsx`${info.name}`}, options, {leadingAngle: false})); } else { if(info.name) $spaceHeading.find('.text').text(info.name).attr('title', info.name); } $spaceHeading.find('.icon').attr('class', `icon icon-${info.icon || spaceType}`); info.items.forEach(function(item) { item.code = item['data-id']; if(item.code === 'more') return; if(item === 'divider' || item.type === 'divider') return $menuMainNav.append('
  • '); const $link= $('') .attr('data-app', currentCode) .attr('href', item.url || '#') .attr('target', item.notApp ? '_blank' : undefined) .toggleClass('active', item.code === info.active) .addClass('rounded' + (item.notApp ? '' : ' open-in-app')); item.icon = item.icon || `icon-${item.code}`; $link.html('', false); $link.find('.text').toggleClass('font-brand', ['devops', 'bi', 'safe'].includes(item.code)).attr('title', item.text).text(item.text); $('
  • ') .attr({'data-app': currentCode, 'data-hint': item.text, 'data-name': item.code}) .attr('data-hidden', item.hidden ? 1 : null) .css('display', item.hidden ? 'none' : 'block') .append($link) .appendTo($menuMainNav); }); refreshMenu(); $menu.removeClass('waiting-workspace'); } $(document).on('contextmenu', '#menuMainNav .divider', function(event) { const $divider = $(this); const $nav = $divider.closest('.nav'); const isMoving = $nav.is('[z-use-sortable]'); let items = []; if(isMoving) { items.push({ text: langData.save, onClick: () => { $divider.closest('.nav').zui().destroy(); saveMenuNavToServer(); } }); } else { items.push({ text: langData.sort, onClick: () => { const sortable = new zui.Sortable('#menuMainNav', { animation: 150, ghostClass: 'bg-primary-pale', onSort: () => saveMenuNavToServer() }); } }); } items.push({ text: langData.hide, onClick: () => { const $li = $divider.closest('li'); $li.remove(); if(!$.apps.workspace) refreshMenu(); saveMenuNavToServer(); } }); const toAddedItems = $.apps.workspace ? generateAddSpaceNavItems($divider) : generateAddMenuNavItems($divider); items.push(toAddedItems.length ? {text: langData.add, items: toAddedItems} : {text: langData.add, disabled: true}); items.push({text: langData.restore, onClick: () => restoreMenuNavToServer()}); items = items.filter(Boolean); if(!items.length) return; if(apps.openedMenu) apps.openedMenu.hide(); apps.openedMenu = zui.ContextMenu.show({ element: $divider[0], placement: 'right-start', items: items, event: event, onClickItem: (info) => info.event.preventDefault(), onHide: () => apps.openedMenu = null, }); event.preventDefault(); }); /* Bind events for app trigger */ $(document).on('click', '.open-in-app,.show-in-app', function(e) { const $link = $(this); if($link.is('[data-modal],[data-toggle],.iframe,.not-in-app')) return; const url = $link.attr('href') || $link.data('url'); if(url && url.includes('onlybody=yes')) return; const code = $link.data('app'); if($.apps.openedMap[code]) showApp(code); else openApp(url, $link.data('app'), !$link.hasClass('show-in-app')); e.preventDefault(); }).on('contextmenu', '.open-in-app,.show-in-app', function(event) { if(isTutorialMode) return; const $btn = $(this); const code = $btn.data('app'); if(!code) return; const app = apps.openedMap[code]; const inTabs = !!$btn.closest('#appTabs').length; const inWorkspace = $.apps.workspace === code; let items = [(inWorkspace && !inTabs) ? null : {text: langData.open, disabled: app && getLastAppCode() === code, onClick: () => showApp(code)}]; if(app && (!inWorkspace || inTabs || $btn.hasClass('active'))) items.push({text: langData.reload, onClick: () => reloadApp(code)}); if($btn.closest('#menuMainNav').length !== 0) { const $nav = $btn.closest('.nav'); const isMoving = $nav.is('[z-use-sortable]'); if(isMoving) { items.push({ text: langData.save, onClick: () => { $btn.closest('.nav').zui().destroy(); saveMenuNavToServer(); } }); } else { items.push({ text: langData.sort, onClick: () => { const sortable = new zui.Sortable('#menuMainNav', { animation: 150, ghostClass: 'bg-primary-pale', onSort: () => saveMenuNavToServer() }); } }); } const hideDisabled = code === 'my' || $btn.is('.active'); items.push({ text: langData.hide, onClick: hideDisabled ? null : () => { const $li = $btn.closest('li'); $li.hide().attr('data-hidden', '1'); if($.apps.workspace) { const info = app.workspace; const name = $li.attr('data-name'); const item = info.items.find(x => x.code === name); if(item) item.hidden = true; } else { closeApp(code); refreshMenu(); changeApp(); } saveMenuNavToServer(); }, disabled: hideDisabled, }); const toAddedItems = inWorkspace ? generateAddSpaceNavItems($btn) : generateAddMenuNavItems($btn); if(toAddedItems.length) { items.push(toAddedItems.length ? {text: langData.add, items: toAddedItems} : {text: langData.add, disabled: true}); } if(!inWorkspace && app && code !== 'my') items.push({text: langData.close, onClick: () => closeApp(code)}); items.push({text: langData.restore, onClick: () => restoreMenuNavToServer()}); } else { if(!inWorkspace && app && code !== 'my') items.push({text: langData.close, onClick: () => closeApp(code)}); } items = items.filter(Boolean); if(!items.length) return; if(apps.openedMenu) apps.openedMenu.hide(); 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}}); event.preventDefault(); }); $(window).on('popstate', function(event) { const state = event.state; if(DEBUG) showLog(state ? state.code : null, 'Popstate', state ? state.url : null, state); if(state) openApp(state.url, state.code, false); }); $.get($.createLink('index', 'app'), html => { apps.frameContent = html; /* Open default app */ let url = defaultOpen || ''; let code = ''; if(location.hash.indexOf('#app=') === 0) { const params = $.parseUrlParams(location.hash.substring(1)); code = params.app; } else if(url.includes(' ')) { const parts = url.split(' '); url = parts[0]; code = parts[1]; } else if(url && $.apps[url]) { code = url; url = ''; } if(!code && defaultOpen) { const lastOpenApp = zui.store.session.get('lastOpenApp'); if(lastOpenApp && (lastOpenApp.url === url || lastOpenApp.url.endsWith(url) || $.parseLink(lastOpenApp.url).url === $.parseLink(url).url)) code = lastOpenApp.code; } if(!url) { if(!code) url = apps.defaultCode; else {url = code; code = '';} } openApp(url, code); }); $.apps = $.extend(apps, { openedApps: apps.openedMap, openApp: openApp, open: openApp, reloadApp: reloadApp, showApp: showApp, updateApp: updateApp, updateUrl: function(appCode, url, title) {return updateApp(appCode, url, title)}, getLastApp: getLastApp, goBack: goBack, logout: logout, isOldPage: isOldPage, getAppCode: getAppCode, updateAppsMenu: updateAppsMenu, changeAppsLang: changeAppsLang, changeAppsTheme: changeAppsTheme, updateUserToolbar: updateUserToolbar, closeApp: closeApp, toggleMenu: toggleMenu, triggerAppEvent: triggerAppEvent, getApps: getApps, getVisibleApps: getVisibleApps, getOpenedApps: getOpenedApps, updateSpaceMenu: updateSpaceMenu, }); window.notifyMessage = function(data) { if(!window.Notification) return; var notify = null; var message = data; if(typeof data.text == 'string') message = data.text; if(Notification.permission == "granted") { notify = new Notification("", {body:message, tag:'zentao', data:data}); } else if(Notification.permission != "denied") { Notification.requestPermission().then(function(permission) { notify = new Notification("", {body:message, tag:'zentao', data:data}); }); } if(!notify) return; notify.onclick = function() { window.focus(); if(typeof notify.data.url == 'string' && notify.data.url) window.location.href = notify.data.url; notify.close(); } setTimeout(function(){notify.close();}, 3000); } window.browserNotify = function() { let windowBlur = false; setInterval(function() { if(window.Notification && Notification.permission == 'default') Notification.requestPermission(); if(window.Notification && Notification.permission == 'granted') { window.onblur = function(){windowBlur = true;} window.onfocus = function(){windowBlur = false;} $('iframe').each(function() { let iframeID = $(this).attr('id'); let $this = document.getElementById(iframeID); if(document.all) { $this.onblur = function(){windowBlur = true;} $this.onfocus = function(){windowBlur = false;} } else { $this.contentWindow.onblur = function(){windowBlur = true;} $this.contentWindow.onfocus = function(){windowBlur = false;} } }); } $.get($.createLink('message', 'ajaxGetMessage', "windowBlur=" + (windowBlur ? '1' : '0')), function(data) { if(!data) return; if(typeof data == 'string') data = JSON.parse(data); for(i in data.messages) { let message = data.messages[i]; if(windowBlur) { if(typeof message.text == 'string') notifyMessage(message); } else { if(!$(message).hasClass('browser-message-content')) continue; zui.Messager.show( { content: {html: message}, placement: 'bottom-right', time: 0, icon: 'envelope-o', className: 'bg-secondary-50 text-secondary-600 messager-notice w-96' }); } } let unreadCount = parseInt(data.newCount); let showCount = data.showCount != '0'; let dotStyle = 'padding: 2px;'; let rightStyle = showCount ? 'right: -10px;' : 'right: -2px;'; if(!showCount) dotStyle += 'width: 5px; height: 5px;'; if(unreadCount < 10 && showCount) rightStyle = 'right: -5px;'; if(unreadCount > 99) unreadCount = '99+'; dotStyle += showCount ? 'top: -3px; aspect-ratio: 0;' : 'top: -2px; aspect-ratio: 1 / 1;'; dotStyle += rightStyle; let dotHtml = '' + (showCount ? unreadCount : '') + ''; $('#apps .app-container').each(function() { let $iframeMessageBar = $(this).find('iframe').contents().find('#messageBar'); if($iframeMessageBar.length > 0) { $iframeMessageBar.find('.label-dot.danger').remove(); if(unreadCount) $iframeMessageBar.append(dotHtml); } let $oldPage = $(this).find('iframe').contents().find('#oldPage'); if($oldPage.length > 0) { $iframeMessageBar = $oldPage.find('iframe').contents().find('#messageBar'); if($iframeMessageBar.length == 0) return; $iframeMessageBar.find('.label-dot.danger').remove(); if(unreadCount) $iframeMessageBar.append(dotHtml); } }); }); }, pollTime * 1000); }; window.clickMessage = function(obj) { let $obj = $(obj); let url = $obj.attr('data-url').replace(/\?onlybody=yes/g, '').replace(/\&onlybody=yes/g, ''); let messageID = $obj.parent().attr('data-id'); openApp(url); $.get($.createLink('message', 'ajaxMarkRead', 'id=' + messageID)); $(obj).closest('.alert.messager').find('.alert-close').trigger('click'); } window.ping = function() { setInterval(function(){$.get($.createLink('misc', 'ping'));}, pollTime * 1000); } window.startCron = function(restart) { if(typeof(restart) == 'undefined') restart = 0; $.ajax({type:"GET", timeout:100, url:$.createLink('cron', 'ajaxExec', 'restart=' + restart)}); } turnon ? browserNotify() : ping(); if(runnable) startCron(); if(scoreNotice) zui.Messager.show({ content: {html: scoreNotice}, placement: 'bottom-right', time: 0, icon: 'diamond', className: 'bg-secondary-50 text-secondary-600 score-notice'}); /* Handle clicking outside. */ $(document).on('click', e => { if($(e.target).attr('id') != 'bizLink' && !$(e.target).parents('#bizLink').length) { $('#upgradeContent').hide(); $('#bizLink').removeClass('active'); } }); const allAppsItemsMap = new Map(); $(document).ready( function() { for(const item of allAppsItems) { if(item === 'divider') continue; allAppsItemsMap.set(item.code, item); } } ); /** * Get icon and title of app item. * * @param {string} name app name * @returns {[string, string]} */ function getAppItemIconAndTitle(name) { if(!allAppsItemsMap.has(name)) return[]; const item = allAppsItemsMap.get(name); const str = item.title; const regex = /class=["']icon (\S*)["']\>\<\/i\>\s(\S*)/; const matches = str.match(regex); if(matches) { const icon = matches[1]; const text = matches[2].trim(); return [icon, text]; } return []; } /** * Add menu item to #mainNav callback, used by generateAddMenuNavItems. * * @param {Cash} $li menu item li * @returns {(name: string) => void} */ function addMenuToMainNavCb($ele) { return (name) => { const $li = $ele.closest('li'); if(name === 'divider') { $li.after('
  • '); refreshMenu(); return } let item = allAppsItemsMap.get(name); const oldItem = apps.map[item.code]; if(oldItem !== item && oldItem) item = $.extend({}, oldItem, item, {active: false}); item.external = item.external || item.url && item.url.includes('://'); const $link= $('') .attr('data-app', item.notApp ? undefined : item.code) .attr('href', item.url || '#') .attr('target', item.notApp ? '_blank' : undefined) .addClass('rounded' + (item.notApp ? '' : ' show-in-app')) .html(item.title, false); item.icon = item.icon || ($link.find('.icon').attr('class') || '').replace('icon ', ''); item.text = $link.text().trim(); $link.html('' + item.text + '', false); if(['devops', 'bi', 'safe'].includes(item.code)) $link.find('.text').addClass('font-brand'); apps.map[item.code] = item; $('
  • ') .attr({'data-app': item.code, 'data-hint': item.text}) .append($link) .insertAfter($li); refreshMenu(); if(!apps.defaultCode) apps.defaultCode = item.code; }; } /** * Check whether current element can add a divider. * @param {Cash} $item * @returns {boolean} */ function canAddDivider($item) { $item = $item.closest('li'); if($item.is('.divider')) return false; if($item.next().not('.hidden').is('.divider')) return false; if($item.is(':last-child')) return false; return true; }