window.getLane = function(lane) { /* 看板只有一个泳道时,铺满全屏。 */ if(laneCount < 2) lane.minHeight = window.innerHeight - 235; } $('#kanbanList').on('enterFullscreen', () => { $('#kanbanList > div').css('height', '100%'); $('.card-heading a').each(function() { const $this = $(this); const newDiv = $('
').addClass($this.attr('class')).html($this.html()); $this.data('relatedDiv', newDiv).before(newDiv).hide(); }); }); $('#kanbanList').on('exitFullscreen', () => { $('#kanbanList > div').css('height', 'calc(100vh - 120px)'); $('.card-heading a').each(function() { const $this = $(this); const relatedDiv = $this.data('relatedDiv'); if(relatedDiv) { relatedDiv.remove(); $this.show().removeData('relatedDiv'); } }); }); /* * 构造看板泳道上的操作按钮。 * Build action buttons on the kanban lane. */ window.getLaneActions = function(lane) { if(typeof lane.actionList == 'undefined') return []; let actions = []; if(lane.actionList.includes('editLaneName')) actions.push({text: kanbanLang.editLaneName, key: 'editLaneName', icon: 'edit', url: $.createLink('kanban', 'editLaneName', 'id=' + lane.id), 'data-toggle': 'modal'}); if(lane.actionList.includes('editLaneColor')) actions.push({text: kanbanLang.editLaneColor, key: 'editLaneColor', icon: 'color', url: $.createLink('kanban', 'editLaneColor', 'id=' + lane.id), 'data-toggle': 'modal'}); if(lane.actionList.includes('sortLane')) actions.push({text: kanbanLang.sortLane, key: 'sortLane', icon: 'move', url: 'javascript:;', 'data-on': 'click', 'data-call': 'sortItems', 'data-params': 'event', 'data-type': 'lane', 'data-id': lane.id, 'data-region': lane.region}); if(lane.actionList.includes('deleteLane')) actions.push({text: kanbanLang.deleteLane, key: 'deleteLane', icon: 'trash', url: $.createLink('kanban', 'deleteLane', 'regionID=' + lane.region + '&id=' + lane.id), 'data-confirm': laneLang.confirmDelete, 'innerClass': 'ajax-submit'}); return [{ type: 'dropdown', icon: 'ellipsis-v', caret: false, items: actions }]; } window.getCol = function(col) { /* 计算WIP。*/ if(kanban.showWIP != '1') return false; const limit = col.limit == -1 ? "" : col.limit; const cards = col.cards; col.subtitleClass = 'ml-1'; let wip = `(${cards} / ${limit})`; if(col.limit != -1 && cards > col.limit) { col.subtitleClass += ' text-danger'; wip += ' '; } col.subtitle = {html: wip}; } window.getColActions = function(col) { if(!canModify) return []; let actionList = []; /* 父列不需要创建卡片相关的操作按钮。 */ if(col.parent != '-1') actionList.push( { type: 'dropdown', icon: 'expand-alt text-primary', caret: false, items: buildColCardActions(col), } ); actionList.push( { type: 'dropdown', icon: 'ellipsis-v', caret: false, items: buildColActions(col), } ); return actionList; } /* * 构造看板上的创建卡片相关操作按钮。 * Build create card related action buttons on the kanban. */ window.buildColCardActions = function(col) { let actions = []; if(col.actionList.includes('createCard')) actions.push({text: kanbanLang.createCard, url: $.createLink('kanban', 'createCard', `kanbanID=${kanbanID}®inID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal'}); if(col.actionList.includes('batchCreateCard')) actions.push({text: kanbanLang.batchCreateCard, url: $.createLink('kanban', 'batchCreateCard', `kanbanID=${kanbanID}®inID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object.indexOf('cards') != -1) actions.push({text: kanbanLang.importCard, url: $.createLink('kanban', 'importCard', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object && kanban.object != 'cards' && vision != 'lite') { actions.push({type: 'divider'}); let items = []; if(kanban.object.indexOf('plans') != -1) items.push({text: kanbanLang.importPlan, url: $.createLink('kanban', 'importPlan', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object.indexOf('releases') != -1) items.push({text: kanbanLang.importRelease, url: $.createLink('kanban', 'importRelease', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object.indexOf('executions') != -1) items.push({text: kanbanLang.importExecution, url: $.createLink('kanban', 'importExecution', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object.indexOf('builds') != -1) items.push({text: kanbanLang.importBuild, url: $.createLink('kanban', 'importBuild', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-size': 'lg'}); if(kanban.object.indexOf('tickets') != -1) items.push({text: kanbanLang.importTicket, url: $.createLink('kanban', 'importTicket', `kanbanID=${kanbanID}®ionID=${col.region}&groupID=${col.group}&columnID=${col.id}`), 'data-toggle': 'modal', 'data-type': 'iframe'}); actions.push( { text: kanbanLang.importAB, caret: true, items: items } ); } return actions; } /* * 构造看板上的创建列、拆分列等操作按钮。 * Build create column, split column and other action buttons on the kanban. */ window.buildColActions = function(col) { if(!col.actionList) return []; let actions = []; if(col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'edit'}); if(col.actionList.includes('setWIP')) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'alert'}); if(col.actionList.includes('sortColumn')) actions.push({text: kanbanLang.sortColumn, url: 'javascript:;', 'icon': 'move', 'data-on': 'click', 'data-call': 'sortItems', 'data-params': 'event', 'data-type': 'column', 'data-id': col.id, 'data-region': col.region}); actions.push({type: 'divider'}); if(col.actionList.includes('splitColumn')) actions.push({text: kanbanLang.splitColumn, url: $.createLink('kanban', 'splitColumn', `columnID=${col.id}`), 'data-toggle': 'modal', 'icon': 'col-split'}); if(col.actionList.includes('createColumn')) actions.push({text: kanbanLang.createColumnOnLeft, url: $.createLink('kanban', 'createColumn', `columnID=${col.id}&position=left`), 'data-toggle': 'modal', 'icon': 'col-add-left'}); if(col.actionList.includes('createColumn')) actions.push({text: kanbanLang.createColumnOnRight, url: $.createLink('kanban', 'createColumn', `columnID=${col.id}&position=right`), 'data-toggle': 'modal', 'icon': 'col-add-right'}); actions.push({type: 'divider'}); if(col.actionList.includes('archiveColumn')) actions.push({text: kanbanLang.archiveColumn, url: $.createLink('kanban', 'archiveColumn', `columnID=${col.id}`), 'icon': 'card-archive', 'data-confirm': columnLang.confirmArchive, 'innerClass': 'ajax-submit'}); if(col.actionList.includes('deleteColumn')) actions.push({text: kanbanLang.deleteColumn, url: $.createLink('kanban', 'deleteColumn', `columnID=${col.id}`), 'icon': 'trash', 'innerClass': 'ajax-submit', 'data-confirm': columnLang.confirmDelete}); return actions; } /** * 渲染卡片内容。 * Render card content. */ window.getItem = function(info) { info.item.titleAttrs = {}; if(info.item.fromType == 'execution') { renderExecutionItem(info); } else if(info.item.fromType == 'release') { renderReleaseItem(info); } else if(info.item.fromType == 'build') { renderBuildItem(info); } else if(info.item.fromType == 'productplan') { renderProductplanItem(info); } else if(info.item.fromType == 'ticket') { renderTicketItem(info); } else { renderGeneralItem(info); } if(info.item.color && info.item.color != '#fff') appendCardClass(info.item, 'color-' + info.item.color.replace('#', '')); appendCardClass(info.item, getCardReminderClass(info.item)); info.item.titleAttrs.class = 'card-title clip'; } function appendCardClass(item, className) { if(!className) return; item.className = item.className ? `${item.className} ${className}` : className; } function getCardReminderClass(item) { if(item.progress == 100) return ''; const deadline = normalizeCardDeadline(item.deadline || item.end); if(!deadline) return ''; if(deadline < today) return 'card-remind-overdue'; const diffDays = Math.floor((new Date(`${deadline}T00:00:00`) - new Date(`${today}T00:00:00`)) / 86400000); if(expireDays > 0 && diffDays >= 0 && diffDays < expireDays) return 'card-remind-soon'; return ''; } function normalizeCardDeadline(date) { if(!date || date === '0000-00-00' || date < '1970-01-01') return ''; return date.slice(0, 10); } window.renderGeneralItem = function(info) { let begin = info.item.begin; let end = info.item.end; let beginAndEnd = ''; if(begin < '1970-01-01' && end > '1970-01-01') { beginAndEnd = end + ' ' + cardLang.deadlineAB; } else if(end < '1970-01-01' && begin > '1970-01-01') { beginAndEnd = begin + ' ' + cardLang.beginAB; } else if(begin > '1970-01-01' && end > '1970-01-01') { beginAndEnd = formatDate(begin) + ' ~ ' + formatDate(end); } const avatar = renderAvatar(info.item.avatarList); const content = `
${info.item.pri} ${beginAndEnd}
${avatar}
`; info.item.titleUrl = $.createLink('kanban', 'viewCard', `id=${info.item.id}`); info.item.titleAttrs = {'data-toggle': 'modal', 'data-size' : 'lg', 'title' : info.item.title}; info.item.content = {html: content}; if(kanban.performable == 1) { info.item.footer = {html: "
' + info.item.progress + '%' + '
'}; } } window.renderExecutionItem = function(info) { info.item.icon = 'run'; info.item.titleUrl = canViewExecution ? $.createLink('execution', 'task', `id=${info.item.fromID}`) : null; info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title}; if(info.item.delay) { info.item.suffix = executionLang.delayed; info.item.suffixClass = 'label danger rounded-xl'; } if(info.item.deleted == '0') { statusBox = '' + executionLang.statusList[info.item.objectStatus] + ''; } else { statusBox = '' + executionLang.deleted + ''; } const date = '' + info.item.end.slice(5) + ' ' + cardLang.deadlineAB + ''; info.item.content = {html: statusBox + date} if(kanban.performable == 1) { info.item.footer = {html: "
' + info.item.progress + '%' + '
'}; } } window.renderReleaseItem = function(info) { info.item.icon = 'publish'; info.item.titleUrl = canViewRelease ? $.createLink('release', 'view', `id=${info.item.fromID}`) : ''; info.item.titleAttrs = {'class': 'card-title clip', 'title' : info.item.title}; if(info.item.deleted == '0') { statusBox = '' + releaseLang.statusList[info.item.objectStatus] + ''; } else { statusBox = '' + releaseLang.deleted + ''; } const date = '' + info.item.date + ''; info.item.content = {html: statusBox + date} } window.renderBuildItem = function(info) { info.item.icon = 'ver'; info.item.titleUrl = canViewBuild ? $.createLink('build', 'view', `id=${info.item.fromID}`) : ''; info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title, 'data-app': 'project'}; const date = '' + info.item.date + ''; info.item.content = {html: date} } window.renderProductplanItem = function(info) { info.item.icon = 'delay'; info.item.titleUrl = canViewPlan ? $.createLink('productplan', 'view', `id=${info.item.fromID}`) : ''; info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title}; if(info.item.deleted == '0') { statusBox = '' + productplanLang.statusList[info.item.objectStatus] + ''; } else { statusBox = '' + productplanLang.deleted + ''; } const today = zui.formatDate(new Date(), 'yyyy-MM-dd'); const labelType = (info.item.begin <= today && info.item.end >= today) ? 'danger' : 'ghost'; info.item.begin = info.item.begin == futureDate ? productplanLang.future : info.item.begin.slice(5); info.item.end = info.item.end == futureDate ? productplanLang.future : info.item.end.slice(5); const date = '' + info.item.begin + ' ' + productplanLang.to + ' ' + info.item.end + ''; info.item.content = {html: `
${info.item.desc}
`}; info.item.contentClass = 'text-gray clip mr-2'; info.item.footer = {html: statusBox + date} } window.renderTicketItem = function(info) { info.item.icon = 'ticket'; info.item.titleUrl = canViewTicket ? $.createLink('ticket', 'view', `id=${info.item.fromID}`) : ''; info.item.titleAttrs = {'class': 'card-title clip', 'title': info.item.title}; let statusBox = ''; if(info.item.deleted == '0') { statusBox = '' + ticketLang.statusList[info.item.objectStatus] + ''; } else { statusBox = '' + ticketLang.deleted + ''; } const date = info.item.deadline && info.item.deadline != '0000-00-00' ? '' + info.item.deadline.slice(5) + ' ' + cardLang.deadlineAB + '' : ''; let avatar = ''; if(info.item.assignedTo && typeof userList[info.item.assignedTo] != 'undefined') { const user = userList[info.item.assignedTo]; const assignedTo = user.realname; const userAvatar = user.avatar ? "" : info.item.assignedTo.substr(0, 1).toUpperCase(); avatar = "'; } let assignees = '
'; let count = 0; for(let avatar of avatarList) { if(count > 2) break; assignees += `
${avatar}
`; count ++; } if(count > 2) assignees += '
...
'; assignees += '
'; return assignees; } window.getItemActions = function(item) { if(!canModify) return []; return [{ type: 'dropdown', icon: 'ellipsis-v', caret: false, items: buildCardActions(item), }]; } window.buildCardActions = function(item) { let actions = []; if(item.actionList.includes('editCard') && item.fromType == '') actions.push({text: kanbanLang.editCard, url: $.createLink('kanban', 'editCard', `id=${item.id}`), 'data-toggle': 'modal', 'icon': 'edit'}); if(item.actionList.includes('deleteCard')) actions.push({text: item.fromType == '' ? kanbanLang.deleteCard : kanbanLang.removeCard, url: $.createLink('kanban', 'deleteCard', `id=${item.id}`), 'data-confirm': item.fromType == '' ? cardLang.confirmDelete : cardLang.confirmRemove, 'innerClass': 'ajax-submit', 'icon': 'trash'}); if(kanban.performable == 1 && item.fromType == '') { if(item.status == 'done') { if(item.actionList.includes('activateCard')) actions.push({text: kanbanLang.activateCard, url: $.createLink('kanban', 'activateCard', `id=${item.id}`), 'icon': 'magic', 'data-toggle': 'modal'}); } else { if(item.actionList.includes('finishCard')) actions.push({text: kanbanLang.finishCard, url: $.createLink('kanban', 'finishCard', `id=${item.id}`), 'icon': 'checked', 'innerClass': 'ajax-submit'}); } } if(kanban.archived == '1' && item.actionList.includes('archiveCard')) actions.push({text: kanbanLang.archiveCard, url: $.createLink('kanban', 'archiveCard', `id=${item.id}`), 'icon': 'card-archive', 'data-confirm': cardLang.confirmArchive, 'innerClass': 'ajax-submit'}); /* Append divider. */ const editCardAction = (item.actionList.includes('editCard') && item.fromType == '') ? true : false; const deleteCardAction = item.actionList.includes('deleteCard'); const archiveCardAction = (item.actionList.includes('archiveCard') && kanban.archived == '1') ? true : false; const performable = kanban.performable == 1 ? true : false; const moveCardAction = item.actionList.includes('moveCard'); const setCardColorAction = item.actionList.includes('setCardColor'); const basicActions = (editCardAction || deleteCardAction || archiveCardAction) ? true : false; const otherActions = (moveCardAction || setCardColorAction) ? true : false; if((performable || basicActions) && otherActions) actions.push({type: 'divider'}); if(item.actionList.includes('moveCard')) actions.push({text: kanbanLang.moveCard, url: $.createLink('kanban', 'moveCard', `cardID=${item.id}&fromColID=0&toColID=0&fromLaneID=0&toLaneID=0&kanbanID=${kanbanID}&showModal=true`), 'data-toggle': 'modal', 'icon': 'move', 'data-size': 'sm'}); if(item.actionList.includes('setCardColor')) { actions.push({text: kanbanLang.cardColor, 'icon': 'color', items: buildColorItems(item)}); } return actions; } window.buildColorItems = function(card) { let items = []; for (let index in colorList) { let color = colorList[index]; let icon = (card.color == color) || (!card.color && color == '#fff') ? "" : ''; items.push({text: {html: `
` + cardLang.colorList[color] + ' ' + icon + '
'}}); } return items; } window.changCardColor = function(cardID, color) { color = color.replace('#', ''); $.ajaxSubmit({url: $.createLink('kanban', 'setCardColor', 'cardID=' + cardID + '&color=' + color + '&kanbanID=' + kanbanID)}); } window.canDrop = function(dragInfo, dropInfo) { if(!dragInfo || $.getFullscreenElement()) return false; const column = this.getCol(dropInfo.col); const lane = this.getLane(dropInfo.lane); if(!column || !lane) return false; return true; } window.onDrop = function(changes, dropInfo) { if(!dropInfo || !canMoveCard) return false; const item = dropInfo['drag']['item']; const toColID = dropInfo['drop']['col'] const toLaneID = dropInfo['drop']['lane'] if(item.col == toColID && item.lane == toLaneID) { let sortList = ''; for(let i = 0; i < dropInfo['data']['list'].length; i++) sortList += dropInfo['data']['list'][i] + ','; url = $.createLink('kanban', 'sortCard', `kanbanID=${kanbanID}&laneID=${toLaneID}&columnID=${toColID}&cards=${sortList}`); } else { url = $.createLink('kanban', 'moveCard', `cardID=${item.id}&fromColID=${item.column}&toColID=${toColID}&fromLaneID=${item.lane}&toLaneID=${toLaneID}&kanbanID=${kanbanID}`); } $.ajaxSubmit({url}); } window.clickRegionMenu = function(event) { $('.regionMenu li').removeClass('active'); $(event.target).closest('li').addClass('active'); const regionID = $(event.target).closest('li').data('region'); const url = $.createLink('kanban', 'view', 'kanbanID=' + kanbanID + '®ionID=' + regionID); loadPartial(url, '#kanbanList'); } function formatDate(inputDate) { const date = new Date(inputDate); const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`; return formattedDate; } window.loadMore = function(type, regionID) { const method = 'viewArchived' + type; const selector = '#archived' + type + 's'; const link = $.createLink('kanban', method, 'regionID=' + regionID); $(selector).load(link, function() { const height = $(window).height() - $('#header').height(); $(selector + ' .panel').css('height', height); }); } window.sortItems = function(event) { const title = $(event.target).closest('a').text(); const objectType = $(event.target).closest('a').data('type'); const objectID = $(event.target).closest('a').data('id'); const regionID = $(event.target).closest('a').data('region'); const url = $.createLink('kanban', 'ajaxGetSortItems', 'objectType=' + objectType + '&objectID=' + objectID); $.getJSON(url, function(items) { zui.Modal.sortView({ title, items, }).then(orders => { if(orders) { if(objectType == 'region') link = $.createLink('kanban', 'sortRegion', 'regions=' + orders); if(objectType == 'column') link = $.createLink('kanban', 'sortColumn', 'regionID=' + regionID + '&columns=' + orders); if(objectType == 'lane') link = $.createLink('kanban', 'sortLane', 'regionID=' + regionID + '&lanes=' + orders); $.getJSON(link, function() { loadCurrentPage(); }); } }); }); }