/** * Update column name. * * @param int $columnID * @param string $name * @param string $color * @access public * @return void */ function updateColumnName(columnID, name, color) { $('.kanban-col[data-id="' + columnID + '"] > div.title > span:first').text(name).attr('title', name).css('color', color); } /** * Change view. * * @param string $view * @access public * @return void */ function changeView(view) { var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + view); location.href = link; } /** * Render user avatar * * @param array $user * @param string $objectType * @param int $objectID * @param int $size * @param string $objectStatus * @access public * @return void */ function renderUserAvatar(user, objectType, objectID, size, objectStatus) { var avatarSizeClass = 'avatar-' + (size || 'sm'); var $noPrivAndNoAssigned = $('
'); if(objectType == 'task') { if(!priv.canAssignTask && !user) return $noPrivAndNoAssigned; var link = createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true); } if(objectType == 'story') { if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned; var link = createLink('story', 'assignto', 'id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true); } if(objectType == 'bug') { if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned; var link = createLink('bug', 'assignto', 'id=' + objectID + '&kanbanGroup=default&from=taskkanban', '', true); } if(!user) return objectStatus == 'closed' ? '' : $(''); if(typeof user === 'string') user = {account: user}; if(!user.avatar && window.userList && window.userList[user.account]) user = window.userList[user.account]; var $noPrivAvatar = $('
').avatar({user: user}); if(objectType == 'task' && !priv.canAssignTask) return $noPrivAvatar; if(objectType == 'story' && !priv.canAssignStory) return $noPrivAvatar; if(objectType == 'bug' && !priv.canAssignBug) return $noPrivAvatar; var realname = user.realname ? user.realname : user.account; var title = user.title ? user.title : realname; return objectStatus == 'closed' ? '' : $('').avatar({user: user}); } /** * Render deadline * @param {String|Date} deadline Deadline @param {string} status * @returns {JQuery} */ function renderDeadline(deadline, status) { if(deadline == '0000-00-00') return; var date = $.zui.createDate(deadline); var now = new Date(); now.setHours(0); now.setMinutes(0); now.setSeconds(0); now.setMilliseconds(0); var isEarlyThanToday = date.getTime() < now.getTime(); var deadlineDate = $.zui.formatDate(date, 'MM-dd'); var statusList = ['doing','pause']; var textColor = isEarlyThanToday && typeof(status) != 'undefined' && statusList.indexOf(status) != -1 ? 'text-red' : 'text-muted'; return $('').text(deadlineLang + ' ' + deadlineDate).addClass(textColor); } /** * Render estStarted * * @param {String|Date} estStarted EstStarted * @param {string} status * @access public * @return void */ function renderEstStarted(estStarted, status) { if(estStarted == '0000-00-00') return; var date = $.zui.createDate(estStarted); var now = new Date(); now.setHours(0); now.setMinutes(0); now.setSeconds(0); now.setMilliseconds(0); var isEarlyThanToday = date.getTime() < now.getTime(); var estStartedDate = $.zui.formatDate(date, 'MM-dd'); var textColor = isEarlyThanToday && typeof(status) != 'undefined' && status == 'wait' ? 'text-red' : 'text-muted'; return $('').text(estStartedLang + ' ' + estStartedDate).addClass(textColor); } /** * Render story item * @param {Object} item Story item object * @param {JQuery} $item Kanban item element * @param {Object} col Column object * @returns {JQuery} $item Kanban item element */ function renderStoryItem(item, $item, col) { if(groupBy == 'story' && item.id == '0') { $('.storyCell').css('width', '100%'); $parentItem = $item[0] == undefined ? $('.storyCell') : $item.parent(); $parentItem.addClass('text-center storyCell'); $parentItem.css('line-height', ($parentItem.parent().height() - 20) + 'px'); $item.replaceWith('' + item.title + ''); return; } var scaleSize = window.kanbanScaleSize; if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); if(scaleSize <= 3) { var $title = $item.find('.title'); if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') .attr('href', $.createLink('execution', 'storyView', 'storyID=' + item.id, '', true)); $title.appendTo($item); } var title = searchValue != '' ? "" + item.title.replaceAll(searchValue, "" + searchValue + "") + "": "" + item.title + ""; $title.attr('title', item.title).find('.text').replaceWith(title); } if(scaleSize <= 2) { var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; var priHtml = '' + item.pri + ''; var hoursHtml = (item.estimate && scaleSize <= 1) ? ('' + item.estimate + hourUnit +'') : ''; var avatarHtml = renderUserAvatar(item.assignedTo, 'story', item.id, '', col.type); var $infos = $item.find('.infos'); if(!$infos.length) $infos = $('
'); $infos.html([idHtml, priHtml, hoursHtml].join('')); $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); if(scaleSize <= 1) $infos.appendTo($item); else if(scaleSize === 2) $infos.prependTo($item); else $infos.prependTo($item.find('.title')); } else if(scaleSize === 4) { $item.html(renderUserAvatar(item.assignedTo, 'story', item.id, 'md')); } if(scaleSize <= 1) { var $actions = $item.find('.actions'); if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } if($.cookie('isFullScreen') == 1) hideAllAction(); return $item.attr('data-type', 'story').addClass('kanban-item-story'); } /** * Render bug item * @param {Object} item Bug item object * @param {JQuery} $item Kanban item element * @param {Object} col Column object * @returns {JQuery} $item Kanban item element */ function renderBugItem(item, $item, col) { var scaleSize = window.kanbanScaleSize; if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); if(scaleSize <= 3) { var $title = $item.find('.title'); if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '') .attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)); $title.appendTo($item); } var title = searchValue != '' ? "" + item.title.replaceAll(searchValue, "" + searchValue + "") + "": "" + item.title + ""; $title.attr('title', item.title).find('.text').replaceWith(title); } if(scaleSize <= 2) { var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; var severityHtml = scaleSize <= 1 ? ('') : ''; var priHtml = '' + item.pri + ''; var avatarHtml = renderUserAvatar(item.assignedTo, 'bug', item.id, '', col.type); var $infos = $item.find('.infos'); if(!$infos.length) $infos = $('
'); $infos.html([idHtml, severityHtml, priHtml].join('')); if(item.deadline && scaleSize <= 1) $infos.append(renderDeadline(item.deadline)); $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); if(scaleSize <= 1) $infos.appendTo($item); else if(scaleSize === 2) $infos.prependTo($item); else $infos.prependTo($item.find('.title')); } else if(scaleSize === 4) { $item.html(renderUserAvatar(item.assignedTo, 'bug', item.id, 'md')); } if(scaleSize <= 1) { var $actions = $item.find('.actions'); if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } if($.cookie('isFullScreen') == 1) hideAllAction(); return $item.attr('data-type', 'bug').addClass('kanban-item-bug'); } /** * Render task item * @param {Object} item Task item object * @param {JQuery} $item Kanban item element * @param {Object} col Column object * @returns {JQuery} $item Kanban item element */ function renderTaskItem(item, $item, col) { var scaleSize = window.kanbanScaleSize; if($item.attr('data-scale-size') !== scaleSize) $item.empty().attr('data-scale-size', scaleSize); if(scaleSize <= 3) { var $title = $item.find('.title'); if(!$title.length) { $title = $('' + (scaleSize <= 1 ? ' ' : '') + '').attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)); $title.appendTo($item); } var name = searchValue != '' ? "" + item.name.replaceAll(searchValue, "" + searchValue + "") + "": "" + item.name + ""; $title.attr('title', item.name).find('.text').replaceWith(name); } if(scaleSize <= 2) { var priHtml = '' + item.pri + ''; var hoursHtml = scaleSize <= 1 && item.status != 'wait' ? ('' + taskLang.leftAB + ' ' + item.left + 'h') : ('' + taskLang.estimateAB + ' ' + item.estimate + 'h'); var avatarHtml = ''; if(item.assignedTo == '' && item.mode == 'multi') avatarHtml = renderUserAvatar({title: item.teamMembers, realname: teamWords}, 'task', item.id, '', col.type); else avatarHtml = renderUserAvatar(item.assignedTo, 'task', item.id, '', col.type); var $infos = $item.find('.infos'); if(!$infos.length) $infos = $('
'); $infos.html([priHtml, hoursHtml].join('')); if(item.deadline && scaleSize <= 1 && (item.status == 'doing' || item.status == 'pause')) $infos.append(renderDeadline(item.deadline, item.status)); if(item.estStarted && scaleSize <= 1 && item.status == 'wait') $infos.append(renderEstStarted(item.estStarted, item.status)); $infos[scaleSize <= 1 ? 'append' : 'prepend'](avatarHtml); if(scaleSize <= 1) $infos.appendTo($item); else if(scaleSize === 2) $infos.prependTo($item); else $infos.prependTo($item.find('.title')); } else if(scaleSize === 4) { $item.html(renderUserAvatar(item.assignedTo, 'task', item.id, 'md')); } if(scaleSize <= 1) { var $actions = $item.find('.actions'); if(!$actions.length && item.menus && item.menus.length) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } $item.attr('data-type', 'task').addClass('kanban-item-task'); if($.cookie('isFullScreen') == 1) hideAllAction(); return $item; } /* Add column renderer */ addColumnRenderer('story', renderStoryItem); addColumnRenderer('bug', renderBugItem); addColumnRenderer('task', renderTaskItem); /** * Render column count * @param {JQuery} $count Kanban count element * @param {number} count Column cards count * @param {number} col Column object * @param {Object} kanban Kanban intance */ function renderColumnCount($count, count, col) { if(groupBy == 'story' && col.type == 'story') { var orderButton = '' + "" + '' + ''; $count.parent().next().html(orderButton); $count.parent().next().addClass('createButton'); $count.hide(); return; } var text = count + '/' + (col.limit < 0 ? '' : col.limit); $count.html(text + ''); if(col.limit != -1 && col.limit < count) { $count.parents('.title').parent('.kanban-header-col').css('background-color', '#F6A1A1'); $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 200); $count.css('color', '#E33030'); if(!$count.parent().find('.error').length) $count.parent().find('.include-last').after(""); } else { $count.parents('.title').parent('.kanban-header-col').css('background-color', 'transparent'); $count.parents('.title').find('.text').css('max-width', $count.parents('.title').width() - 120); $count.css('color', '#8B91A2'); $count.parent().find('.error').remove(); } } /** * Alert to link product. * * @access public * @return void */ function tips() { bootbox.alert(needLinkProducts); } /** * Render header column * @param {JQuery} $col Header column element * @param {Object} col Header column object * @param {JQuery} $header Header element * @param {Object} kanban Kanban object */ function renderHeaderCol($col, col, $header, kanban) { if(col.asParent) $col = $col.children('.kanban-header-col'); if($col.children('.actions').context != undefined || (groupBy == 'story' && col.type == 'story')) return; var $actions = $('
'); var printStoryButton = printTaskButton = printBugButton = false; if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlan) printStoryButton = true; if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true; if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true; if(col.type === 'backlog' || col.type === 'wait' || col.type == 'unconfirmed') { var tips = productID ? '' : 'onclick="tips()"'; $actions.append([ '', '', '' ].join('')); } if(priv.canSetWIP || priv.canEditName) { $actions.append([ '', '', '' ].join('')); } $actions.appendTo($col); } /** * Render lane name * @param {JQuery} $name Name element * @param {Object} lane Lane object * @param {JQuery} $kanban $kanban element * @param {Object} columns Kanban columns * @param {Object} kanban Kanban object */ function renderLaneName($name, lane, $kanban, columns, kanban) { if(groupBy == 'story') { $name.hide(); return; } if(lane.id != 'story' && lane.id != 'task' && lane.id != 'bug') return false; if(!$name.children('.actions').length && (priv.canSetLane || priv.canMoveLane)) { $([ '
', '', '', '', '
' ].join('')).appendTo($name); } } /** * Updata kanban data * @param {string} kanbanID Kanban id * @param {Object} data Kanban data */ function updateKanban(kanbanID, data) { var $kanban = $('#kanban-' + kanbanID); if(!$kanban.length) return; if(data == null) { $kanban.hide(); return false; } $kanban.show(); $kanban.data('zui.kanban').render(data); resetKanbanHeight(); return true; } /** * Create kanban in page * @param {string} kanbanID Kanban id * @param {Object} data Kanban data * @param {Object} options Kanban options */ function createKanban(kanbanID, data, options) { var $kanban = $('#kanban-' + kanbanID); if($kanban.length) return updateKanban(kanbanID, data); $kanban = $('
').appendTo('#kanbans'); $kanban.kanban($.extend({data: data, calcColHeight: calcColHeight, displayCards: typeof window.displayCards === 'number' ? window.displayCards : 2}, options)); } /** * Hide all actions. * * @access public * @return void */ function hideAllAction() { $('.actions').hide(); $(".title, .avatar.iframe").attr("disabled", true).css("pointer-events", "none"); } /** * Display the kanban in full screen. * * @access public * @return void */ function fullScreen() { $('#kanbans .kanban-header').addClass('headerTop'); var element = document.getElementById('kanbanContainer'); var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; if(requestMethod) { var afterEnterFullscreen = function() { $('#kanbanContainer').addClass('scrollbar-hover'); hideAllAction(); $.cookie('isFullScreen', 1); } var whenFailEnterFullscreen = function() { exitFullScreen(); } try { var result = requestMethod.call(element); if(result && (typeof result.then === 'function' || result instanceof window.Promise)) { result.then(afterEnterFullscreen).catch(whenFailEnterFullscreen); } else { afterEnterFullscreen(); } } catch (error) { whenFailEnterFullscreen(error); } } } /** * Exit full screen. * * @access public * @return void */ function exitFullScreen() { $('#kanbans .kanban-header').removeClass('headerTop'); $('#kanbanContainer').removeClass('scrollbar-hover'); $('.actions').show(); $(".title, .avatar.iframe").attr("disabled", false).css("pointer-events", "auto"); $.cookie('isFullScreen', 0); } document.addEventListener('fullscreenchange', function (e) { if(!document.fullscreenElement) exitFullScreen(); }); document.addEventListener('webkitfullscreenchange', function (e) { if(!document.webkitFullscreenElement) exitFullScreen(); }); document.addEventListener('mozfullscreenchange', function (e) { if(!document.mozFullScreenElement) exitFullScreen(); }); document.addEventListener('msfullscreenChange', function (e) { if(!document.msfullscreenElement) exitFullScreen(); }); /* Define drag and drop rules */ if(!window.kanbanDropRules) { window.kanbanDropRules = { story: { backlog: ['ready'], ready: ['backlog'], tested: ['verified'], verified: ['tested', 'released'], released: ['verified', 'closed'], closed: ['released'], }, bug: { 'unconfirmed': ['confirmed', 'fixing', 'fixed'], 'confirmed': ['fixing', 'fixed'], 'fixing': ['fixed'], 'fixed': ['testing', 'tested', 'fixing'], 'testing': ['tested', 'closed', 'fixing'], 'tested': ['closed', 'fixing'], 'closed': ['fixing'], }, task: { 'wait': ['developing', 'developed', 'canceled'], 'developing': ['developed', 'pause', 'canceled'], 'developed': ['developing', 'closed'], 'pause': ['developing'], 'canceled': ['developing', 'closed'], 'closed': ['developing'], } } } /* * Find drop columns * @param {JQuery} $element Drag element * @param {JQuery} $root Dnd root element */ function findDropColumns($element, $root) { var $col = $element.closest('.kanban-col'); var col = $col.data(); var kanbanID = $root.data('id'); var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[kanbanID] : null; $.zui.ContextMenu.hide(); if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])'); var colRules = kanbanRules[col.type]; var lane = $col.closest('.kanban-lane').data('lane'); return $root.find('.kanban-lane-col').filter(function() { if(!colRules) return false; if(colRules === true) return true; if($.cookie('isFullScreen') == 1) return false; var $newCol = $(this); var newCol = $newCol.data(); if(newCol.id === col.id) return false; var $newLane = $newCol.closest('.kanban-lane'); var newLane = $newLane.data('lane'); var canDropHere = colRules.indexOf(newCol.type) > -1 && newLane.id === lane.id; if(canDropHere) $newCol.addClass('can-drop-here'); return canDropHere; }); } /** * Change card's type by changing column. * * @param int $cardID * @param int $fromColID * @param int $toColID * @param int $fromLaneID * @param int $toLaneID * @param string $cardType * @param string $fromColType * @param string $toColType * @access public * @return void */ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType) { var objectID = cardID; var showIframe = false; var moveCard = false; /* Task lane. */ if(cardType == 'task') { if(toColType == 'developed') { if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask) { var link = createLink('task', 'finish', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } } else if(toColType == 'pause') { if(fromColType == 'developing' && priv.canPauseTask) { var link = createLink('task', 'pause', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } } else if(toColType == 'developing') { if((fromColType == 'canceled' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask) { var link = createLink('task', 'activate', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } if(fromColType == 'pause' && priv.canActivateTask) { var link = createLink('task', 'restart', 'taskID=' + objectID + '&from=' + 'taskkanban', '', true); showIframe = true; } if(fromColType == 'wait' && priv.canStartTask) { var link = createLink('task', 'start', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } } else if(toColType == 'canceled') { if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask) { var link = createLink('task', 'cancel', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } } else if(toColType == 'closed') { if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) { var link = createLink('task', 'close', 'taskID=' + objectID + '&extra=from=' + 'taskkanban', '', true); showIframe = true; } } } /* Bug lane. */ if(cardType == 'bug') { if(toColType == 'confirmed') { if(fromColType == 'unconfirmed' && priv.canConfirmBug) { var link = createLink('bug', 'confirmBug', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true); showIframe = true; } } else if(toColType == 'fixing') { if(fromColType == 'confirmed' || fromColType == 'unconfirmed') moveCard = true; if((fromColType == 'closed' || fromColType == 'fixed' || fromColType == 'testing' || fromColType == 'tested') && priv.canActivateBug) { var link = createLink('bug', 'activate', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true); showIframe = true; } } else if(toColType == 'fixed') { if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') { var link = createLink('bug', 'resolve', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true); showIframe = true; } } else if(toColType == 'testing') { if(fromColType == 'fixed') moveCard = true; } else if(toColType == 'tested') { if(fromColType == 'fixed' || fromColType == 'testing') moveCard = true; } else if(toColType == 'closed') { if(fromColType == 'testing' || fromColType == 'tested') { var link = createLink('bug', 'close', 'bugID=' + objectID + '&extra=&from=taskkanban', '', true); showIframe = true; } } if(moveCard) { var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); $.get(link, function(data) { if(data) { kanbanGroup = $.parseJSON(data); if(groupBy == 'default') { updateKanban('bug', kanbanGroup.bug); } else { updateKanban(browseType, kanbanGroup[groupBy]); } } }) } } /* Story lane. */ if(cardType == 'story') { if(toColType == 'closed' && priv.canCloseStory) { var link = createLink('story', 'close', 'storyID=' + objectID + '&from=taskkanban', '', true); showIframe = true; } else { if(toColType == 'ready') { $.get(createLink('story', 'ajaxGetInfo', "storyID=" + cardID), function(data) { if(data) { data = $.parseJSON(data); if(data.status == 'draft' || data.status == 'changing' || data.status == 'reviewing') { bootbox.alert(executionLang.storyDragError); } else { ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID); } } }); } else { ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID); } } } if(showIframe) { var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); modalTrigger.show(); } } /** * AJAX: move card. * * @param int $objectID * @param int $fromColID * @param int $toColID * @param int $fromLaneID * @param int $toLaneID * @access public * @return void */ function ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID) { var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy); $.get(link, function(data) { if(data) { kanbanGroup = $.parseJSON(data); if(groupBy == 'default') { updateKanban('story', kanbanGroup.story); } else { updateKanban(browseType, kanbanGroup[groupBy]); } } }); } /** * Handle drop task. * * @param object $element * @param object $event * @param object $kanban * @access public * @return void */ function handleDropTask($element, event, kanban) { if(!event.target) return; var $card = $element; var $oldCol = $card.closest('.kanban-col'); var $newCol = $(event.target).closest('.kanban-col'); var oldCol = $oldCol.data(); var newCol = $newCol.data(); var oldLane = $oldCol.closest('.kanban-lane').data('lane'); var newLane = $newCol.closest('.kanban-lane').data('lane'); var cardType = $card.find('.kanban-card').data('type'); if(!oldCol || !newCol || !newLane || !oldLane) return false; if(oldCol.id === newCol.id && newLane.id === oldLane.id) return false; var cardID = $card.data().id; var fromColType = $oldCol.data('type'); var toColType = $newCol.data('type'); changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType); } var kanbanActionHandlers = { dropItem: handleDropTask }; /** * Handle kanban action. * * @param string $action * @param object $element * @param object $event * @param object $kanban * @access public * @return void */ function handleKanbanAction(action, $element, event, kanban) { $('.kanban').attr('data-action-enabled', action); var handler = kanbanActionHandlers[action]; if(handler) handler($element, event, kanban); } /** * Handle finish drop task * @param {Object} event Event object * @returns {void} */ function handleFinishDrop(event) { $('#kanbans').find('.can-drop-here').removeClass('can-drop-here'); } /** * Create column menu * @returns {Object[]} */ function createColumnMenu(options) { var $col = options.$trigger.closest('.kanban-col'); var col = $col.data('col'); var kanbanID = options.kanban; var items = []; if(priv.canEditName) items.push({label: executionLang.editName, url: $.createLink('kanban', 'setColumn', 'col=' + col.id + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}}) if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.id + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}}) //if(priv.canSortCards) items.push({label: executionLang.sortColumn, items: ['按ID倒序', '按ID顺序'], className: 'iframe', onClick: handleSortColCards}) return items; } /** * Create column create button menu * @returns {Object[]} */ function createColumnCreateMenu(options) { var $col = options.$trigger.closest('.kanban-col'); var col = $col.data('col'); var items = []; if(col.laneType == 'story') { if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}}); if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStory, url: $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); if(priv.canLinkStoryByPlan) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal'}}); } else if(col.laneType == 'bug') { if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=0&moduleID=0&extra=executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}}); if(priv.canBatchCreateBug) { if(productNum > 1) items.push({label: bugLang.batchCreate, url: '#batchCreateBug', 'attrs' : {'data-toggle': 'modal'}}); else items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); } } else { if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '80%'}}); if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); if(priv.canImportBug && canImportBug) items.push({label: executionLang.importBug, url: $.createLink('execution', 'importBug', 'executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-width': '90%'}}); } return items; } /** * Create lane menu * @returns {Object[]} */ function createLaneMenu(options) { var $lane = options.$trigger.closest('.kanban-lane'); var $kanban = $lane.closest('.kanban'); var lane = $lane.data('lane'); var kanbanID = options.kanban; var upTargetKanban = $kanban.prev('.kanban').length ? $kanban.prev('.kanban').data('id') : ''; var downTargetKanban = $kanban.next('.kanban').length ? $kanban.next('.kanban').data('id') : ''; var items = []; if(priv.canSetLane) items.push({label: kanbanLang.setLane, icon: 'edit', url: $.createLink('kanban', 'setLane', 'lane=' + lane.laneID + '&executionID=' + executionID + '&from=execution'), className: 'iframe'}); if(priv.canMoveLane) items.push( {label: kanbanLang.moveUp, icon: 'arrow-up', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + upTargetKanban), className: 'iframe', disabled: !$kanban.prev('.kanban').length}, {label: kanbanLang.moveDown, icon: 'arrow-down', url: $.createLink('kanban', 'laneMove', 'executionID=' + executionID + '¤tLane=' + lane.id + '&targetLane=' + downTargetKanban), className: 'iframe', disabled: !$kanban.next('.kanban').length} ); var bounds = options.$trigger[0].getBoundingClientRect(); items.$options = {x: bounds.right, y: bounds.top}; return items; } /** * Create story menu * @returns {Object[]} */ function createStoryMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var story = $card.data('item'); var items = []; $.each(story.menus, function() { var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; if(this.size) item.attrs['data-width'] = this.size; if(this.icon == 'unlink' || this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}}; items.push(item); }); return items; } /** * Create bug menu * @returns {Object[]} */ function createBugMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var bug = $card.data('item'); var items = []; $.each(bug.menus, function() { var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; if(this.size) item.attrs['data-width'] = this.size; if(this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}}; items.push(item); }); return items; } /** * Create task menu * @returns {Object[]} */ function createTaskMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var task = $card.data('item'); var items = []; $.each(task.menus, function() { var item = {label: this.label, icon: this.icon, url: this.url, attrs: {'data-toggle': 'modal', 'data-type': 'iframe'}}; if(this.size) item.attrs['data-width'] = this.size; if(this.icon == 'trash') item = {label: this.label, icon: this.icon, url: this.url, attrs: {'target': 'hiddenwin'}}; items.push(item); }); return items; } /** Resize kanban container size */ function resizeKanbanContainer() { var $container = $('#kanbanContainer'); var maxHeight = window.innerHeight - 98 - 15; if($.cookie('isFullScreen') == 1) maxHeight = window.innerHeight - 15; $container.children('.panel-body').css('max-height', maxHeight); } /* Define menu creators */ window.menuCreators = { column: createColumnMenu, columnCreate: createColumnCreateMenu, lane: createLaneMenu, story: createStoryMenu, bug: createBugMenu, task: createTaskMenu, }; /* Set kanban affix container */ window.kanbanAffixContainer = '#kanbanContainer>.panel-body'; /* Overload kanban default options */ $.extend($.fn.kanban.Constructor.DEFAULTS, { onRender: function() { var maxWidth = 0; $('#kanbans .kanban-board').each(function() { maxWidth = Math.max(maxWidth, $(this).outerWidth()); }); $('#kanbans').css('min-width', maxWidth); } }); /** Get card height */ function getCardHeight() { return [59, 59, 62, 62, 47][window.kanbanScaleSize]; } /** Change kanban scale size */ function changeKanbanScaleSize(newScaleSize) { var newScaleSize = Math.max(1, Math.min(4, newScaleSize)); if(newScaleSize === window.kanbanScaleSize) return; window.kanbanScaleSize = newScaleSize; $.zui.store.set('executionKanbanScaleSize', newScaleSize); $('#kanbanScaleSize').text(newScaleSize); $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', newScaleSize >= 4 ? 'disabled' : null); $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', newScaleSize <= 1 ? 'disabled' : null); $('#kanbans').children('.kanban').each(function() { var kanban = $(this).data('zui.kanban'); if(!kanban) return; kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); }); resetKanbanHeight(); return newScaleSize; } /** Affix kanban board header */ window.affixKanbanHeader = function($kanbanBoard, affixed) { var $header = $kanbanBoard.children('.kanban-header'); var $headerCols = $header.children('.kanban-header-cols'); var headerStyle = {width: '', left: 0}; var headerColsStyle = {width: '', marginLeft: ''}; if(affixed) { var $kanban = $('#kanbanContainer'); var kanbanBounding = $kanban[0].getBoundingClientRect(); var kanbanBoardBounding = $kanbanBoard[0].getBoundingClientRect(); var laneNameWidth = +$headerCols.css('left').replace('px', ''); headerStyle.width = kanbanBounding.width; headerStyle.left = kanbanBounding.left; headerColsStyle.width = kanbanBoardBounding.width - laneNameWidth; headerColsStyle.marginLeft = kanbanBoardBounding.left - kanbanBounding.left; } $header.css(headerStyle); $headerCols.css(headerColsStyle); $kanbanBoard.toggleClass('kanban-affixed', !!affixed); $kanbanBoard.css('padding-top', affixed ? $header.outerHeight() : ''); } /** * Handle sort cards. * * @param object event * @access public * @return void */ function handleSortCards(event) { if(groupBy != 'default' || searchValue != '') return; var newLaneID = event.element.closest('.kanban-lane').data('id'); var newColID = event.element.closest('.kanban-col').data('id'); var cards = event.element.closest('.kanban-lane-items').data('cards'); var orders = cards.map(function(card){return card.id}); var fromID = String(event.element.data('id')); var toID = String(event.target.data('id')); orders.splice(orders.indexOf(fromID), 1); orders.splice(orders.indexOf(toID) + (event.insert === 'before' ? 0 : 1), 0, fromID); var url = createLink('kanban', 'sortCard', 'kanbanID=' + executionID + '&laneID=' + newLaneID + '&columnID=' + newColID + '&cards=' + orders.join(',')); $.getJSON(url, function(response) { if(response.result === 'fail') { if(typeof response.message === 'string' && response.message.length) { bootbox.alert(response.message); } setTimeout(function(){return location.reload()}, 3000); } else { $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution' + '&searchValue=' + searchValue + '&orderBy=' + orderBy), function(data) { if(data && lastUpdateData !== data) { lastUpdateData = data; kanbanGroup = $.parseJSON(data); var kanbanLane = ''; for(var i in kanbanList) { if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane); } } }); } }); } /* Example code: */ $(function() { $.cookie('isFullScreen', 0); window.kanbanScaleSize = +$.zui.store.get('executionKanbanScaleSize', 1); $('#kanbanScaleSize').text(window.kanbanScaleSize); $('#kanbanScaleControl .btn[data-type="+"]').attr('disabled', window.kanbanScaleSize >= 4 ? 'disabled' : null); $('#kanbanScaleControl .btn[data-type="-"]').attr('disabled', window.kanbanScaleSize <= 1 ? 'disabled' : null); changeOrder = false; /* Common options */  var commonOptions = { maxColHeight: 'auto', minColWidth: typeof window.minColWidth === 'number' ? window.minColWidth : defaultMinColWidth, maxColWidth: typeof window.maxColWidth === 'number' ? window.maxColWidth : defaultMaxColWidth, cardHeight: getCardHeight(), showCount: true, showZeroCount: true, fluidBoardWidth: fluidBoard, cardsPerRow: window.kanbanScaleSize, virtualize: true, onAction: handleKanbanAction, virtualRenderOptions: {container: '#kanbanContainer>.panel-body,#kanbanContainer'}, virtualCardList: true, droppable: { target: findDropColumns, finish: handleFinishDrop }, onRenderHeaderCol: renderHeaderCol, onRenderLaneName: renderLaneName, onRenderCount: renderColumnCount, sortable: handleSortCards, }; /* Create kanban */ if(groupBy == 'default') { var kanbanLane = ''; for(var i in kanbanList) { if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; if(browseType == kanbanList[i] || browseType == 'all') createKanban(kanbanList[i], kanbanLane, commonOptions); } } else { /* Create kanban by group. */ createKanban(browseType, kanbanGroup[groupBy], commonOptions); } /* Init iframe modals */ $(document).on('click', '#kanbans .iframe,.contextmenu-menu .iframe', function(event) { var $link = $(this); if($link.data('zui.modaltrigger')) return; $link.modalTrigger({show: true}); event.preventDefault(); }); /* Init contextmenu */ $('#kanbans').on('click', '[data-contextmenu]', function(event) { var $trigger = $(this); var menuType = $trigger.data('contextmenu'); var menuCreator = window.menuCreators[menuType]; if(!menuCreator) return; var options = $.extend({event: event, $trigger: $trigger}, $trigger.data()); var items = menuCreator(options); if(!items || !items.length) return; $.zui.ContextMenu.show(items, items.$options || {event: event}); }); /* Make kanbanScaleControl works */ $('#kanbanScaleControl').on('click', '.btn', function() { changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1)); }); /* Resize kanban container on window resize */ resizeKanbanContainer(); $(window).on('resize', resizeKanbanContainer); /* Hide contextmenu when page scroll */ $(window).on('scroll', function() { $.zui.ContextMenu.hide(); }); $('#toStoryButton').on('click', function() { var planID = $('#plan').val(); if(planID) { var param = "¶m=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy; location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=taskKanban&extra=' + param); } }); $('#product').change(function() { var product = $('#product').val(); if(product) { var link = createLink('bug', 'batchCreate', 'productID=' + product + '&branch=&executionID=' + executionID, '', true); $('#batchCreateBugButton').attr('href', link); } }); document.addEventListener('scroll', function() { $('.storyColumn').parent().removeClass('open'); }, true); $('#type_chosen .chosen-single span').prepend(''); $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': '); /* Ajax update kanban. */ lastUpdateData = ''; setInterval(function() { $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=' + searchValue + '&orderBy=' + orderBy), function(data) { if(lastUpdateData == '') lastUpdateData = data; if(data && lastUpdateData !== data) { lastUpdateData = data; kanbanGroup = $.parseJSON(data); if(groupBy == 'default') { var kanbanLane = ''; for(var i in kanbanList) { if(kanbanList[i] == 'story') kanbanLane = kanbanGroup.story; if(kanbanList[i] == 'bug') kanbanLane = kanbanGroup.bug; if(kanbanList[i] == 'task') kanbanLane = kanbanGroup.task; if(browseType == kanbanList[i] || browseType == 'all') updateKanban(kanbanList[i], kanbanLane); } } else { updateKanban(browseType, kanbanGroup[groupBy]); } } }); }, 10000); resetKanbanHeight(); var kanbanMinColWidth = typeof window.minColWidth === 'number' ? window.minColWidth : defaultMinColWidth; if(kanbanMinColWidth < 190) { var miniColWidth = kanbanMinColWidth * 0.2; $('.kanban-header-col>.title>span:not(.text)').hide(); $('.kanban-header-col>.title > span.text').css('max-width', miniColWidth + 'px'); } $('[data-toggle="tooltip"]').tooltip(); }); $('#type').change(function() { var type = $('#type').val(); if(type != 'all') { $('.c-group').show(); $.get(createLink('execution', 'ajaxGetGroup', 'type=' + type), function(data) { $('#group_chosen').remove(); $('#group').replaceWith(data); $('#group').chosen(); }) } var link = createLink('execution', 'taskKanban', "executionID=" + executionID + '&type=' + type); location.href = link; }); $('.c-group').change(function() { $('.c-group').show(); var type = $('#type').val(); var group = $('#group').val(); var link = createLink('execution', 'taskKanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); location.href = link; }); /** Calculate column height */ function calcColHeight(col, lane, colCards, colHeight, kanban) { var options = kanban.options; if(!options.displayCards) return colHeight; var displayCards = +(options.displayCards || 2); if (typeof displayCards !== 'number' || displayCards < 2) displayCards = 2; return (displayCards * (options.cardHeight + options.cardSpace) + options.cardSpace); } /* Hide contextmenu when page scroll */ $('.panel-body').scroll(function() { $.zui.ContextMenu.hide(); }); /** * Reset kanban height according to window height. * * @access public * @return void */ function resetKanbanHeight() { var laneCount = $('.kanban-lane').length; if(laneCount > 1) return; var windowHeight = $(window).height(); var headerHeight = $('#mainHeader').outerHeight(); var mainPadding = $('#main').css('padding-top'); var menuHeight = $('#mainMenu').height(); var panelBorder = $('.panel').css('border-top-width'); var bodyPadding = $('.panel-body').css('padding-top'); var columnHeight = $('.kanban-header').outerHeight(); var height = windowHeight - headerHeight - (parseInt(mainPadding) * 2) - menuHeight - (parseInt(panelBorder) * 2) - (parseInt(bodyPadding) * 2) - columnHeight; $('.kanban-lane').css('height', height -2); } $(document).on('click', '.dropdown-menu', function() { $.zui.ContextMenu.hide(); }); /** * Toggle kanban search box. * * @access public * @return void */ function toggleSearchBox() { $('#searchBox').toggle(); if($('#searchBox').css('display') == 'block') { $(".querybox-toggle").css("color", "#0c64eb"); } else { $(".querybox-toggle").css("color", "#3c495c"); $('#taskKanbanSearchInput').attr('value', ''); searchCards(''); } } /** * Search kanban cards. * * @param string value * @param string order * * @access public * @return void */ function searchCards(value, order = '') { searchValue = value; orderBy = order == '' ? orderBy : order; if(order != '') changeOrder = true; $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=execution&searchValue=' + value + '&orderBy=' + orderBy), function(data) { lastUpdateData = data; var kanbanData = $.parseJSON(data); var hideAll = true; if(groupBy == 'default') { var kanbanLane = ''; for(var i in kanbanList) { if(kanbanList[i] == 'story') kanbanLane = kanbanData.story; if(kanbanList[i] == 'bug') kanbanLane = kanbanData.bug; if(kanbanList[i] == 'task') kanbanLane = kanbanData.task; if(browseType == kanbanList[i] || browseType == 'all') hideAll = !updateKanban(kanbanList[i], kanbanLane) && hideAll; } } else { hideAll = !updateKanban(browseType, kanbanData[groupBy]) && hideAll; } if(hideAll) { $("#emptyBox").removeClass('hidden'); $("#kanbanContainer .panel-body").addClass('hidden'); } else { $("#emptyBox").addClass('hidden'); $("#kanbanContainer .panel-body").removeClass('hidden'); } }); }