$(function() { $('#products').change(function() { var selectProductID = $('#products').val(); var attr = createLink('story', 'batchCreate', 'productID=' + selectProductID + '&branch=0' + '&moduleID=0&story=0&execution=' + executionID, '', true); $('#batchCreateStoryButton').attr('href',attr); }); }) /** * When execution status change. * * @param stirng executionStatus * @access public * @return void */ function changeStatus(executionStatus) { if(executionStatus == 'wait') { $('.startButton').removeClass('hidden'); $('.putoffButton').removeClass('hidden'); $('.suspendButton').removeClass('hidden'); $('.closeButton').removeClass('hidden'); $('.activateButton').addClass('hidden'); } else if(executionStatus == 'doing') { $('.startButton').addClass('hidden'); $('.putoffButton').removeClass('hidden'); $('.suspendButton').removeClass('hidden'); $('.closeButton').removeClass('hidden'); $('.activateButton').addClass('hidden'); } else if(executionStatus == 'suspended') { $('.startButton').addClass('hidden'); $('.putoffButton').addClass('hidden'); $('.suspendButton').addClass('hidden'); $('.closeButton').removeClass('hidden'); $('.activateButton').removeClass('hidden'); } else if(executionStatus == 'closed') { $('.startButton').addClass('hidden'); $('.putoffButton').addClass('hidden'); $('.suspendButton').addClass('hidden'); $('.closeButton').addClass('hidden'); $('.activateButton').removeClass('hidden'); } } /** * Hide all action. * * @access public * @return void */ function hideAction() { $('.actions').hide(); $('.action').hide(); $('.avatar').removeAttr('data-toggle'); $('.kanban-group-header').hide(); $(".title").attr("disabled", true).css("pointer-events", "none"); $(".avatar").attr("disabled", true).css("pointer-events", "none"); window.sortableDisabled = true; } /** * Display the kanban in full screen. * * @access public * @return void */ function fullScreen() { var element = document.getElementById('kanbanContainer'); var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullscreen; if(requestMethod) { var afterEnterFullscreen = function() { $('#kanbanContainer').addClass('fullscreen') .on('scroll', tryUpdateKanbanAffix); hideAction(); $.cookie('isFullScreen', 1); }; var whenFailEnterFullscreen = function(error) { 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() { $('#kanbanContainer').removeClass('fullscreen') .off('scroll', tryUpdateKanbanAffix); $('.actions').show(); $('.action').show(); $('.avatar').attr('data-toggle', 'modal'); $('.kanban-group-header').show(); $(".title").attr("disabled", false).css("pointer-events", "auto"); $(".avatar").attr("disabled", false).css("pointer-events", "auto"); window.sortableDisabled = false; $.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(); }); /** 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); if(groupBy == 'default') { $('#kanban').children('.region').children("div[id^='kanban']").each(function() { var kanban = $(this).data('zui.kanban'); if(!kanban) return; kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); }); } else { var kanban = $('#kanban' + executionID).data('zui.kanban'); if(!kanban) return; kanban.setOptions({cardsPerRow: newScaleSize, cardHeight: getCardHeight()}); } if(laneCount == 1) resetRegionHeight('open'); return newScaleSize; } /** Get card height */ function getCardHeight() { return [60, 60, 62, 62, 47][window.kanbanScaleSize]; } $('#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', 'kanban', "executionID=" + executionID + '&browseType=' + type); location.href = link; }); $('.c-group').change(function() { $('.c-group').show(); var type = $('#type').val() ? $('#type').val() : browseType; var group = $('#group').val(); var link = createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=id_asc' + '&groupBy=' + group); location.href = link; }); /** * Create lane menu. * * @param object $options * @access public * @return void */ function createLaneMenu(options) { var lane = options.$trigger.closest('.kanban-lane').data('lane'); var privs = lane.actions; if(!privs.length) return []; var items = []; var regionID = lane.$kanbanData.region; var kanbanID = lane.$kanbanData.kanban; if(privs.includes('editLaneName')) items.push({label: kanbanLang.editLaneName, icon: 'edit', url: createLink('kanban', 'editLaneName', 'laneID=' + lane.id + '&executionID=0&from=kanban'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '635px'}}); if(privs.includes('editLaneColor')) items.push({label: kanbanLang.editLaneColor, icon: 'color', url: createLink('kanban', 'editLaneColor', 'laneID=' + lane.id + '&executionID=0&from=kanban'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '635px'}}); if(privs.includes('deleteLane')) items.push({label: kanbanLang.deleteLane, icon: 'trash', url: createLink('kanban', 'deleteLane', 'regionID=' + regionID + '&kanbanID=' + kanbanID + '&lane=' + lane.id), attrs: {'target': 'hiddenwin'}}); var bounds = options.$trigger[0].getBoundingClientRect(); items.$options = {x: bounds.right, y: bounds.top}; return items; } function createColumnMenu(options) { var column = options.$trigger.closest('.kanban-col').data('col'); var privs = column.actions; if(!privs.length) return []; var items = []; if(privs.includes('setColumn')) items.push({label: kanbanLang.editColumn, icon: 'edit', url: createLink('kanban', 'setColumn', 'columnID=' + column.id + '&executionID=' + executionID + '&from=RDKanban', '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal'}}); if(privs.includes('setWIP')) items.push({label: kanbanLang.setWIP, icon: 'alert', url: createLink('kanban', 'setWIP', 'columnID=' + column.id + '&executionID=' + executionID + '&from=RDKanban', '' ,'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width' : '500px'}}); var bounds = options.$trigger[0].getBoundingClientRect(); items.$options = {x: bounds.right, y: bounds.top}; 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 = []; var laneID = col.$kanbanData.lanes[0].id ? col.$kanbanData.lanes[0].id : 0; var regionID = col.$kanbanData.region == undefined ? 0 : col.$kanbanData.region; if(col.type == 'backlog') { if(priv.canCreateStory) items.push({label: storyLang.create, url: $.createLink('story', 'create', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&objectID=' + executionID + '&bugID=0&planID=0&todoID=0&extra=regionID=' + regionID + ',laneID=' + laneID + ',columnID=' + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStory, url: productCount > 1 ? '#batchCreateStory' : $.createLink('story', 'batchcreate', 'productID=' + productID + '&branch=0&moduleID=0&storyID=0&executionID=' + executionID + '&plan=0&type=story&extra=regionID=' + regionID + ',laneID=' + laneID + ',columnID=' + col.id, '', true), className: 'iframe',attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); if(priv.canLinkStory) items.push({label: executionLang.linkStory, url: $.createLink('execution', 'linkStory', 'executionID=' + executionID + '&browseType=¶m=0&recTotal=0&recPerPage=50,&pageID=1&extra=laneID=' + laneID + ',columnID=' + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); if(priv.canLinkStoryByPlan) items.push({label: executionLang.linkStoryByPlan, url: '#linkStoryByPlan', 'attrs' : {'data-toggle': 'modal', 'data-target': '#linkStoryByPlan','data-col' : col.id, 'data-lane' : laneID, 'class' : 'linkStoryByPlanButton'}}); } else if(col.type == 'unconfirmed') { if(priv.canCreateBug) items.push({label: bugLang.create, url: $.createLink('bug', 'create', 'productID=' + productID + '&moduleID=0&extra=regionID=' + regionID + ',laneID=' + laneID + ',columnID=' + col.id + ',executionID=' + executionID, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', '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 + '&branch=&executionID=' + executionID + '&module=0&extra=regionID=' + regionID + ',laneID=' + laneID + ',columnID=' + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); } } else if(col.type == 'wait') { if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID + "&storyID=0&moduleID=0&taskID=0&todoID=0&extra=regionID=" + regionID + ",laneID=" + laneID + ",columnID=" + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID + "&storyID=0&moduleID=0&taskID=0&iframe=0&extra=regionID=" + regionID + ",laneID=" + laneID + ",columnID=" + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); if(priv.canImportBug && vision == 'rnd') items.push({label: executionLang.importBug, url: $.createLink('execution', 'importBug', 'executionID=' + executionID + "&storyID=0&moduleID=0&taskID=0&todoID=0&extra=laneID=" + laneID + ",columnID=" + col.id, '', true), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '90%'}}); } return items; } /** * Update region name. * * @param int $regionID * @param string $name * @access public * @return void */ function updateRegionName(regionID, name) { $('.region[data-id="' + regionID + '"] > .region-header > strong:first').text(name); } /** * Update lane name. * * @param int $laneID * @param string $name * @access public * @return void */ function updateLaneName(laneID, name) { $('.kanban-lane[data-id="' + laneID + '"] > .kanban-lane-name > span').text(name).attr('title', name); } /** * Update lane color. * * @param int $laneID * @param string $color * @access public * @return void */ function updateLaneColor(laneID, color) { $('.kanban-lane[data-id="' + laneID + '"] > .kanban-lane-name').css('background-color', color); } /** * 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); } /** * Hide kanban action */ function hideKanbanAction() { $('.kanban').attr('data-action-enabled', null); $('.contextmenu').removeClass('contextmenu-show'); $('.contextmenu .contextmenu-menu').removeClass('open').removeClass('in'); $('#moreTasks, #moreColumns').animate({right: -400}, 500); $('.storyColumn').parent().removeClass('open'); } /** * Handle finish drop task */ function handleFinishDrop() { $('.kanban').find('.can-drop-here').removeClass('can-drop-here'); } /* Define drag and drop rules */ if(!window.kanbanDropRules) { window.kanbanDropRules = { story: { backlog: ['ready', 'backlog'], ready: ['backlog', 'ready'], tested: ['verified'], verified: ['tested', 'released'], released: ['verified', 'closed'], closed: ['released'], }, bug: { 'unconfirmed': ['unconfirmed', 'confirmed', 'fixing', 'fixed'], 'confirmed': ['confirmed', 'fixing', 'fixed'], 'fixing': ['fixing', 'fixed'], 'fixed': ['fixed', 'testing', 'tested', 'fixing'], 'testing': ['testing', 'tested', 'closed', 'fixing'], 'tested': ['tested', 'closed', 'fixing'], 'closed': ['closed', 'fixing'], }, task: { 'wait': ['wait', 'developing', 'developed', 'canceled'], 'developing': ['developing', 'developed', 'pause', 'canceled'], 'developed': ['developed', 'developing', 'closed'], 'pause': ['pause', 'developing', 'canceled'], 'canceled': ['canceled', 'developing', 'closed'], '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 laneType = $element.closest('.kanban-lane').data().lane.type; var kanbanRules = window.kanbanDropRules ? window.kanbanDropRules[laneType] : null; hideKanbanAction(); if(!kanbanRules) return $root.find('.kanban-lane-col:not([data-type="' + col.type + '"])'); var colRules = kanbanRules[col.type]; var groupID = $col.closest('.kanban-board').data().id; var oldLaneID = $col.closest('.kanban-lane').data().id; 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(); var newGroupID = $newCol.closest('.kanban-board').data().id; var newLaneID = $newCol.closest('.kanban-lane').data().id; var canDropHere = colRules.indexOf(newCol.type) > -1 && newGroupID === groupID; if(groupBy && groupBy != 'default' && canDropHere) canDropHere = oldLaneID === newLaneID; if(canDropHere) $newCol.addClass('can-drop-here'); return canDropHere; }); } /** * 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=' + groupBy, '', true); } if(objectType == 'story') { if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned; var link = createLink('story', 'assignto', 'id=' + objectID + '&kanbanGroup=' + groupBy, '', true); } if(objectType == 'bug') { if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned; var link = createLink('bug', 'assignto', 'id=' + objectID + '&kanbanGroup=' + groupBy, '', true); } if(!user) return objectStatus == 'closed' ? '' : $(''); if(typeof user === 'string') user = {account: user}; if(!user.avatar && window.userList && window.userList[user.account]) user = {avatar: userList[user.account].avatar, account: user.account, realname: userList[user.account].realname}; 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; return objectStatus == 'closed' ? '' : $('').avatar({user: user}).attr('data-toggle', 'modal').attr('data-width', '80%'); } /** * 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 ? ' ' : '') + ''); if(priv.canViewStory) $title = $title.attr('href', $.createLink('execution', 'storyView', 'storyID=' + item.id + '&executionID=' + execution.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '95%'); $title.appendTo($item); } var title = rdSearchValue != '' ? "" + item.title.replaceAll(rdSearchValue, "" + rdSearchValue + "") + "": "" + 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 && (priv.canEditStory || priv.canActivateStory || priv.canChangeStory || priv.canCreateTask || priv.canBatchCreate || priv.canUnlinkStory)) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } if($.cookie('isFullScreen') == 1) hideAction(); 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 ? ' ' : '') + ''); if(priv.canViewBug) $title =$title.attr('href', $.createLink('bug', 'view', 'bugID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%'); $title.appendTo($item); } var title = rdSearchValue != '' ? "" + item.title.replaceAll(rdSearchValue, "" + rdSearchValue + "") + "": "" + 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 && (priv.canEditBug || priv.canResolveBug || priv.canConfirmBug || priv.canCopyBug || priv.canToStoryBug || priv.canDeleteBug)) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } if($.cookie('isFullScreen') == 1) hideAction(); 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 ? ' ' : '') + ''); if(priv.canViewTask) $title = $title.attr('href', $.createLink('task', 'view', 'taskID=' + item.id, '', true)).attr('data-toggle', 'modal').attr('data-width', '80%'); $title.appendTo($item); } var name = rdSearchValue != '' ? "" + item.name.replaceAll(rdSearchValue, "" + rdSearchValue + "") + "": "" + 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 = 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 && (priv.canEditTask || priv.canRecordEstimateTask || priv.canCreateTask || priv.canCancelTask)) { $actions = $([ '
', '', '', '', '
' ].join('')).appendTo($item); } } $item.attr('data-type', 'task').addClass('kanban-item-task'); if($.cookie('isFullScreen') == 1) hideAction(); return $item; } /* Add column renderer */ addColumnRenderer('story', renderStoryItem); addColumnRenderer('bug', renderBugItem); addColumnRenderer('task', renderTaskItem); /** * Render items count of a column. */ function renderCount($count, count, column) { if(groupBy == 'story' && column.type == 'story') { var orderButton = '' + "" + '' + ''; $count.parent().next().html(orderButton); $count.parent().next().addClass('createButton'); $count.remove(); return; } /* Render WIP. */ var limit = !column.limit || column.limit == '-1' ? '' : column.limit; if($count.parent().find('.limit').length) { $count.parent().find('.limit').html(limit); } else { $count.parent().find('.count').before("("); $count.parent().find('.count').after("/" + limit + ")"); } if(column.limit != -1 && column.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 of a column. */ function renderHeaderCol($column, column, $header, kanbanData) { if(groupBy == 'story' && column.type == 'story') return; /* Render group header. */ var privs = kanbanData.actions; var columnPrivs = kanbanData.columns[0].actions; var $actions = $column.children('.actions'); if(column.parent == -1) { $column.append('
'); $actions = $column.children('.actions'); } if(groupBy == 'default' && privs.includes('sortGroup')) { var groups = regions[column.region].groups; if($header.closest('.kanban').data('zui.kanban')) { groups = $header.closest('.kanban').data('zui.kanban').data; } if(groups.length > 1) { $column.closest('.kanban-board').addClass('sort'); $column.closest('.kanban-header').find('.kanban-group-header').remove(); $column.closest('.kanban-header').prepend('
'); } } var printMoreBtn = (columnPrivs.includes('setColumn') || columnPrivs.includes('setWIP')); /* Render more menu. */ if((column.type == 'backlog' || column.type == 'wait' || column.type == 'unconfirmed') && $actions.children('.text-primary').length == 0) { var tips = productID ? '' : 'onclick="tips()"'; $actions.append([ '', '', '' ].join('')); } if(printMoreBtn && $actions.children('.btn').length == 0) { $actions.append(' '); } } /** * Render lane name. * * @param object $lane * @param int lane * @param object $kanban * @param array columns * @param object $kanban * @access public * @return void */ function renderLaneName($lane, lane, $kanban, columns, kanban) { if(groupBy == 'story') { $lane.hide(); return; } if(groupBy != 'default') return; var canEditLaneColor = lane.actions.includes('editLaneColor'); var canEditLaneName = lane.actions.includes('editLaneName'); var canSort = lane.actions.includes('sortLane') && kanban.lanes.length > 1; var canDelete = lane.actions.includes('deleteLane'); $lane.parent().toggleClass('sort', canSort); if(!$lane.children('.actions').length && (canEditLaneColor || canEditLaneName || canDelete)) { $([ '
', '', '', '', '
' ].join('')).appendTo($lane); } } /** * Update a region. * * @param int regionID * @param array regionData * @access public * @return boolean */ function updateRegion(regionID, regionData = []) { if(groupBy != 'default') regionID = executionID; if(!regionID) return false; var $region = $('#kanban'+ regionID).kanban(); if(!$region.length) return false; if(!regionData) regionData = regions[regionID]; var data = groupBy == 'default' ? regionData.groups : regionData; if((data == null || data.length == 0) && rdSearchValue != '') { if(groupBy == 'default') $("div[data-id^=" + regionID + "].region").hide(); if(groupBy != 'default') $("div[data-id^=" + regionID + "].kanban").hide(); return false; } else { if(groupBy == 'default') $("div[data-id^=" + regionID + "].region").show(); if(groupBy != 'default') $("div[data-id^=" + regionID + "].kanban").show(); } $region.data('zui.kanban').render(data); resetRegionHeight('open'); return true; } /** * Handle drop task. * * @param object $element * @param object $event * @param object $kanban * @access public * @return void */ function handleDropTask($element, event, kanban) { if(!event.target || !event.isNew) 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'); var regionID = $card.closest('.region').data().id; changeCardColType(cardID, oldCol.id, newCol.id, oldLane.id, newLane.id, cardType, fromColType, toColType, regionID); } var kanbanActionHandlers = { dropItem: handleDropTask }; /** * Handle kanban action */ function handleKanbanAction(action, $element, event, kanban) { $('.kanban').attr('data-action-enabled', action); var handler = kanbanActionHandlers[action]; if(handler) handler($element, event, kanban); } /** * changeCardColType * * @param int cardID * @param int fromColID * @param int toColID * @param int fromLaneID * @param int toLaneID * @param string cardType * @param string fromColType * @param string toColType * @param int regionID * @access public * @return void */ function changeCardColType(cardID, fromColID, toColID, fromLaneID, toLaneID, cardType, fromColType, toColType, regionID = 0) { var objectID = cardID; var showIframe = false; var moveCard = false; regionID = regionID ? regionID : 0; /* Task lane. */ if(cardType == 'task') { if(toColType == 'developed') { if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask) { var link = createLink('task', 'finish', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } else if(toColType == 'pause') { if(fromColType == 'developing' && priv.canPauseTask) { var link = createLink('task', 'pause', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } else if(toColType == 'developing') { if((fromColType == 'canceled' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask) { var link = createLink('task', 'activate', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } if(fromColType == 'pause' && priv.canActivateTask) { var link = createLink('task', 'restart', 'taskID=' + objectID, '', true); showIframe = true; } if(fromColType == 'wait' && priv.canStartTask) { var link = createLink('task', 'start', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } else if(toColType == 'canceled') { if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask) { var link = createLink('task', 'cancel', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } else if(toColType == 'closed') { if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) { var link = createLink('task', 'close', 'taskID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } if(fromLaneID != toLaneID && fromColID == toColID) shiftCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID); } /* Bug lane. */ if(cardType == 'bug') { if(toColType == 'confirmed') { if(fromColType == 'unconfirmed' && priv.canConfirmBug) { var link = createLink('bug', 'confirmBug', 'bugID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', 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=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } else if(toColType == 'fixed') { if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') { var link = createLink('bug', 'resolve', 'bugID=' + objectID + '&extra=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', 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=fromColID=' + fromColID + ',toColID=' + toColID + ',fromLaneID=' + fromLaneID + ',toLaneID=' + toLaneID + ',regionID=' + regionID, '', true); showIframe = true; } } if(moveCard || (fromLaneID != toLaneID && fromColID == toColID)) shiftCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID); } /* Story lane. */ if(cardType == 'story') { if(toColType == 'closed' && priv.canCloseStory) { var link = createLink('story', 'close', 'storyID=' + objectID, '', 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, regionID); } } }); } else { ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID); } } } 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 * @param int $regionID * @access public * @return void */ function ajaxMoveCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID) { var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID+ '&orderBy=' + orderBy ); $.ajax( { method: 'post', dataType: 'json', url: link, success: function(data) { data = groupBy == 'default' ? data[regionID] : data[groupBy]; updateRegion(regionID, data); }, error: function(xhr, status, error) { showErrorMessager(error || lang.timeout); } }); } /** * Delete a card. * * @param string objectType * @param int objectID * @param int regionID * @access public * @return void */ function deleteCard(objectType, objectID, regionID) { $.zui.ContextMenu.hide(); regionID = regionID ? regionID : 0; setTimeout(function() { var objectLang = objectType + 'Lang'; var result = confirm(window[objectLang].confirmDelete) ? true : false; if(!result) return false; if(!objectID) return false; var url = createLink('kanban', 'deleteObjectCard', 'objectType=' + objectType + '&objectID=' + objectID + '®ionID=' + regionID); return $.ajax( { method: 'post', dataType: 'json', url: url, success: function(data) { data = groupBy == 'default' ? data[regionID] : data[groupBy]; updateRegion(regionID, data); } }); }, 200) } /** * Close modal and update kanban data. * * @param string kanbanData * @param int regionID * @access public * @return void */ function updateKanban(kanbanData, regionID = 0) { setTimeout(function() { $.zui.closeModal(); if(regionID && groupBy == 'default') { updateRegion(regionID, kanbanData[regionID]); } else { $('#kanban').children('.region').children("div[id^='kanban']").each(function() { var regionID = $(this).attr('data-id'); var data = groupBy == 'default' ? kanbanData[regionID] : kanbanData[groupBy] updateRegion(regionID, data); }); } }, 200); resetRegionHeight('open'); } /** * Shift card. * * @param int objectID * @param int fromColID * @param int toColID * @param int fromLaneID * @param int toLaneID * @param int regionID * @access public * @return void */ function shiftCard(objectID, fromColID, toColID, fromLaneID, toLaneID, regionID) { var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&fromColID=' + fromColID + '&toColID=' + toColID + '&fromLaneID=' + fromLaneID + '&toLaneID=' + toLaneID + '&execitionID=' + executionID + '&browseType=' + browseType + '&groupBy=' + groupBy + '®ionID=' + regionID + '&orderBy=' + orderBy ); $.ajax( { method: 'post', dataType: 'json', url: link, success: function(data) { data = groupBy == 'default' ? data[regionID] : data[groupBy]; updateRegion(regionID, data); }, error: function(xhr, status, error) { showErrorMessager(error || lang.timeout); } }); } /** * Process minus button. * * @access public * @return void */ function processMinusBtn() { var columnCount = $('#splitTable .child-column').size(); if(columnCount > 2 && columnCount < 10) { $('#splitTable .btn-plus').show(); $('#splitTable .btn-close').show(); } else if(columnCount <= 2) { $('#splitTable .btn-close').hide(); } else if(columnCount >= 10) { $('#splitTable .btn-plus').hide(); } } /** * Create story menu * @returns {Object[]} */ function createStoryMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var story = $card.data('item'); var items = []; var showAction = story.$col.type == 'backlog' || story.$col.type == 'ready' || story.$col.type == 'developing' || story.$col.type == 'developed' || story.$col.type == 'testing' || story.$col.type == 'tested' || story.$col.type == 'verified' || story.$col.type == 'released'; if(priv.canEditStory) items.push({label: storyLang.edit, icon: 'edit', url: createLink('story', 'edit', 'storyID=' + story.id + '&kanbanGroup=' + groupBy, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '95%'}}); if(priv.canChangeStory && showAction && story.status == 'active') items.push({label: storyLang.change, icon: 'change', url: createLink('story', 'change', 'storyID=' + story.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canCreateTask && showAction && story.status == 'active') items.push({label: executionLang.wbs, icon: 'plus', url: createLink('task', 'create', 'executionID=' + execution.id + '&storyID=' + story.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canBatchCreateTask && showAction && story.status == 'active') items.push({label: executionLang.batchWBS, icon: 'pluses', url: createLink('task', 'batchCreate', 'executionID=' + execution.id + '&storyID=' + story.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canActivateStory && story.$col.type == 'closed') items.push({label: executionLang.activate, icon: 'magic', url: createLink('story', 'activate', 'storyID=' + story.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canUnlinkStory) items.push({label: executionLang.unlinkStory, icon: 'unlink', url: createLink('execution', 'unlinkStory', 'executionID=' + execution.id + '&storyID=' + story.id + '&confirm=no&from=' + '&laneID=' + story.lane + '&columnID=' + story.column, '', 'false'), attrs: {target: 'hiddenwin'}}); if(priv.canDeleteStory) items.push({label: storyLang.delete, icon: 'trash', onClick: function(){deleteCard('story', story.id, story.$lane.region)}}); return items; } /** * Create task menu * @returns {Object[]} */ function createTaskMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var task = $card.data('item'); var items = []; if(priv.canEditTask) items.push({label: taskLang.edit, icon: 'edit', url: createLink('task', 'edit', 'taskID=' + task.id + '&comment=&kanbanGroup=' + groupBy, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '95%'}}); if(priv.canRestartTask && task.$col.type == 'pause') items.push({label: taskLang.restart, icon: 'play', url: createLink('task', 'restart', 'taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canPauseTask && task.$col.type == 'developing') items.push({label: taskLang.pause, icon: 'pause', url: createLink('task', 'pause', 'taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canRecordEstimateTask) items.push({label: executionLang.effort, icon: 'time', url: createLink('task', 'recordEstimate', 'taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canActivateTask && (task.$col.type == 'developed' || task.$col.type == 'canceled' || task.$col.type == 'closed')) items.push({label: executionLang.activate, icon: 'magic', url: createLink('task', 'activate', 'taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canCreateTask) items.push({label: taskLang.copy, icon: 'copy', url: createLink('task', 'create', 'executionID=' + executionID + '&storyID=' + '0' + '&moduleID=' + '0' + '&taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canCancelTask && (task.$col.type == 'wait' || task.$col.type == 'developing' || task.$col.type == 'pause')) items.push({label: taskLang.cancel, icon: 'cancel', url: createLink('task', 'cancel', 'taskID=' + task.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canDeleteTask) items.push({label: taskLang.delete, icon: 'trash', onClick: function(){deleteCard('task', task.id, task.$lane.region)}}); return items; } /** * Create bug menu * @returns {Object[]} */ function createBugMenu(options) { var $card = options.$trigger.closest('.kanban-item'); var bug = $card.data('item'); var items = []; if(priv.canEditBug) items.push({label: bugLang.edit, icon: 'edit', url: createLink('bug', 'edit', 'bugID=' + bug.id + '&comment=&kanbanGroup=' + groupBy, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '95%'}}); if(priv.canResolveBug && (bug.$col.type == 'unconfirmed' || bug.$col.type == 'confirmed' || bug.$col.type == 'fixing')) items.push({label: bugLang.resolve, icon: 'checked', url: createLink('bug', 'resolve', 'bugID=' + bug.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canConfirmBug && (bug.$col.type == 'fixed' || bug.$col.type == 'testing' || bug.$col.type == 'tested')) items.push({label: bugLang.close, icon: 'off', url: createLink('bug', 'close', 'bugID=' + bug.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canConfirmBug && bug.$col.type == 'unconfirmed') items.push({label: bugLang.confirmBug, icon: 'ok', url: createLink('bug', 'confirmbug', 'bugID=' + bug.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canCopyBug) items.push({label: bugLang.copy, icon: 'copy', url: createLink('bug', 'create', 'productID=' + productID + '&branch=&extras=bugID=' + bug.id + ',regionID=' + bug.$lane.region + ',laneID=' + bug.lane + ',columnID=' + bug.column + ',executionID=' + executionID, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canToStoryBug && (bug.$col.type != 'closed')) items.push({label: bugLang.toStory, icon: 'lightbulb', url: createLink('story', 'create', 'product=' + productID + '&branch=' + '0' + '&module=' + '0' + '&story=' + '0' + '&execution=' + '0' + '&bugID=' + bug.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canActivateBug && (bug.$col.type == 'fixed') || bug.$col.type == 'testing' || bug.$col.type == 'tested' || bug.$col.type == 'closed') items.push({label: bugLang.activate, icon: 'magic', url: createLink('bug', 'activate', 'bugID=' + bug.id, '', 'true'), className: 'iframe', attrs: {'data-toggle': 'modal', 'data-width': '80%'}}); if(priv.canDeleteBug) items.push({label: bugLang.delete, icon: 'trash', onClick: function(){deleteCard('bug', bug.id, bug.$lane.region)}}); return items; } /** * Handle sort cards. * * @param object event * @access public * @return void */ function handleSortCards(event) { if(window.sortableDisabled || groupBy != 'default' || rdSearchValue != '') 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=RD' + '&serachValue=' + rdSearchValue + '&orderBy=' + orderBy), function(data) { if(data && lastUpdateData !== data) { lastUpdateData = data; kanbanData = $.parseJSON(data); for(var region in kanbanData) { updateRegion(region, kanbanData[region]); } } }); } }); } /* Define menu creators */ window.menuCreators = { lane: createLaneMenu, column: createColumnMenu, columnCreate: createColumnCreateMenu, task: createTaskMenu, story: createStoryMenu, bug: createBugMenu, }; /** * Init kanban. * * @param object $kanban * @access public * @return void */ function initKanban($kanban) { var id = $kanban.data('id'); var region = regions[id]; $kanban.kanban( { data: groupBy == 'default' ? region.groups : kanbanData[groupBy], maxColHeight: 'auto', calcColHeight: calcColHeight, minColWidth: typeof window.minColWidth === 'number' ? window.minColWidth : defaultMinColWidth, maxColWidth: typeof window.maxColWidth === 'number' ? window.maxColWidth : defaultMaxColWidth, cardHeight: getCardHeight(), fluidBoardWidth: fluidBoard, displayCards: typeof window.displayCards === 'number' ? window.displayCards : 2, createColumnText: kanbanLang.createColumn, addItemText: '', cardsPerRow: window.kanbanScaleSize, onAction: handleKanbanAction, onRenderLaneName: renderLaneName, onRenderHeaderCol: renderHeaderCol, onRenderCount: renderCount, droppable: {target: findDropColumns, finish:handleFinishDrop}, sortable: handleSortCards, virtualize: true, virtualCardList: true, virtualRenderOptions: {container: $(window).add($('#kanbanContainer'))} }); $kanban.on('click', '.action-cancel', hideKanbanAction); $kanban.on('scroll', function() { $.zui.ContextMenu.hide(); }); 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'); } } /** * Init when page ready */ $(function() { changeStatus(execution.status); if($.cookie('isFullScreen') == 1) $.cookie('isFullScreen', 0); changeOrder = false; 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); /* Make kanbanScaleControl works */ $('#kanbanScaleControl').on('click', '.btn', function() { changeKanbanScaleSize(window.kanbanScaleSize + ($(this).data('type') === '+' ? 1 : -1)); }); /* Init first kanban */ $('.kanban').each(function() { initKanban($(this)); }); $('.icon-chevron-double-up,.icon-chevron-double-down').on('click', function() { $(this).toggleClass('icon-chevron-double-up icon-chevron-double-down'); $(this).parents('.region').find('.kanban').toggle(); hideKanbanAction(); resetRegionHeight($(this).hasClass('icon-chevron-double-up') ? 'open' : 'close'); }); $('.region-header').on('click', '.action', hideKanbanAction); $('#TRAction').on('click', '.btn', hideKanbanAction); /* Hide action box when user click document */ $(document).on('click', function(e) { $('.kanban').each(function() { var currentAction = $(this).kanban().attr('data-action-enabled'); var canHideAction = (currentAction === 'headerMore' || currentAction === 'editLaneName') && !$(e.target).closest('.action,.action-box').length; if(canHideAction) hideKanbanAction(); }); }); /* Init contextmenu */ $('#kanban').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}); }); /* Hide contextmenu when page scroll */ $(window).on('scroll', function() { $.zui.ContextMenu.hide(); }); $('#toStoryButton').on('click', function() { var planID = $('#plan').val(); if(planID) { var vars = $('.linkStoryByPlanButton').data('lane') != null ? '&extra=laneID='+ $('.linkStoryByPlanButton').data('lane') + ',columnID=' + $('.linkStoryByPlanButton').data('col') : ''; var param = "¶m=executionID=" + executionID + ",browseType=" + browseType + ",orderBy=id_asc,groupBy=" + groupBy; location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban' + vars + param); $.closeModal(); } }); $('#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).on('click', '#splitTable .btn-plus', function() { var tr = $(this).closest('tr'); tr.after($('#childTpl').html().replace(/key/g, key)); tr.next().find('input[name^=color]').colorPicker(); key++; processMinusBtn(); return false; }); /* Remove a trade detail item. */ $(document).on('click', '#splitTable .btn-close', function() { $(this).closest('tr').remove(); processMinusBtn(); return false; }); /* Modify color's border color. */ $(document).on('mouseout', '.color0', function() { $('.color0 .cardcolor').css('border', '1px solid #b0b0b0'); }); /* Modify default color's border color. */ $(document).on('mouseover', '.color0', function() { $('.color0 .cardcolor').css('border', '1px solid #fff'); }); document.addEventListener('scroll', function() { hideKanbanAction(); }, true); /* Init sortable */ var sortType = ''; var $cards = null; $('#kanban').sortable( { selector: '.region, .kanban-board, .kanban-lane', trigger: '.region.sort > .region-header, .kanban-board.sort > .kanban-header > .kanban-group-header, .kanban-lane.sort > .kanban-lane-name', dropOnMouseleave: true, container: function($ele) { return $ele.parent(); }, targetSelector: function($ele) { /* Sort regions */ if($ele.hasClass('region')) { sortType = 'region'; return $ele.parent().children('.region'); } /* Sort boards */ if($ele.hasClass('kanban-board')) { sortType = 'board'; return $ele.parent().children('.kanban-board'); } /* Sort lanes */ if($ele.hasClass('kanban-lane')) { sortType = 'lane'; $cards = $ele.find('.kanban-item'); return $ele.parent().children('.kanban-lane'); } }, before: function() { return !window.sortableDisabled; }, start: function(e) { if(sortType == 'region') { showRegionIdList = ''; $('.icon-chevron-double-up').each(function() { showRegionIdList += $(this).attr('data-id') + ','; $(this).attr('class', 'icon-chevron-double-down'); }); $('.region').find('.kanban').hide(); hideKanbanAction(); } }, finish: function(e) { if(!e.changed) return; var url = ''; var orders = []; if(sortType == 'region') { e.list.each(function(index, data) { if(data.item.hasClass('region') && data.item.hasClass('sort')) orders.push(data.item.data('id')); }); $('.region').each(function() { if(showRegionIdList.includes($(this).attr('data-id'))) { $(this).find('.icon-chevron-double-down').attr('class', 'icon-chevron-double-up'); $(this).find('.kanban').show(); } }) url = createLink('kanban', 'sortRegion', 'regions=' + orders.join(',')); } if(sortType == 'board') { var regionID = e.element.closest('.region').data('id'); e.list.each(function(index, data) { if(data.item.hasClass('kanban-board') && data.item.hasClass('sort') && regionID == data.item.parent().data().id) orders.push(data.item.data('id')); }); url = createLink('kanban', 'sortGroup', 'region=' + regionID + '&groups=' + orders.join(',')); } if(sortType == 'lane') { var groupID = e.element.closest('.kanban-board').data('id'); e.list.each(function(index, data) { if(data.item.hasClass('kanban-lane') && data.item.hasClass('sort') && groupID == data.item.data().lane.group) orders.push(data.item.data('id')); }); var regionID = e.element.closest('.region').data('id'); url = createLink('kanban', 'sortLane', 'region=' + regionID + '&lanes=' + orders.join(',')); } if(!url) return true; $.getJSON(url, function(response) { if(response.result == 'fail' && response.message.length) { bootbox.alert(response.message); setTimeout(function(){return location.reload()}, 3000); } }); }, always: function(e) { if(sortType == 'lane') $cards.show(); } }); /* Ajax update kanban. */ if(groupBy == 'default') { setInterval(function() { if(rdSearchValue == '') { $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy + '&from=RD&searchValue=' + rdSearchValue + '&orderBy=' + orderBy), function(data) { if(lastUpdateData == '') lastUpdateData = data; if(data && lastUpdateData !== data) { lastUpdateData = data; kanbanData = $.parseJSON(data); for(var region in kanbanData) { updateRegion(region, kanbanData[region]); } } }); } }, 10000); } if(groupBy != 'default') { $('.region').css('padding-bottom', '10px'); $('.region').css('border', '0px'); } resetRegionHeight('open'); }); /** 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); } /** * Reset region height according to window height. * * @param string fold * @access public * @return void */ function resetRegionHeight(fold) { var laneCount = $('.kanban-lane').length; if(laneCount > 1 || $('.region').length > 0) return; var regionHeaderHeight = $('.region-header').outerHeight(); if(fold == 'open') { 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 regionBorder = $('.region').css('border-top-width'); var regionPadding = $('.kanban').css('padding-bottom'); var columnHeight = $('.kanban-header').outerHeight(); var height = windowHeight - (parseInt(mainPadding) * 2) - menuHeight - (parseInt(bodyPadding) * 2) - headerHeight - (parseInt(panelBorder) * 2) - (parseInt(regionBorder) * 2); if(groupBy == 'default') $('.region').css('height', height); $('.kanban-lane').css('height', height - regionHeaderHeight - parseInt(regionPadding) - columnHeight); } else { $('.region').css('height', regionHeaderHeight); } } /** * Toggle RDSearchBox. * * @access public * @return void */ function toggleRDSearchBox() { $('#rdSearchBox').toggle(); if($('#rdSearchBox').css('display') == 'block') { $(".querybox-toggle").css("color", "#0c64eb"); } else { $(".querybox-toggle").css("color", "#3c495c"); $('#rdKanbanSearchInput').attr('value', ''); searchCards(''); } } /** * Search all cards. * * @param string $value * @param string order * * @access public * @return void */ function searchCards(value, order = '') { rdSearchValue = value; orderBy = order == '' ? orderBy : order; if(order != '') changeOrder = true; $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=0&browseType=" + browseType + "&groupBy=" + groupBy + '&from=RD&searchValue=' + rdSearchValue + '&orderBy=' + orderBy), function(data) { lastUpdateData = data; kanbanData = $.parseJSON(data); var hideAll = true; $('#kanban').children('.region').children("div[id^='kanban']").each(function() { var regionID = $(this).attr('data-id'); if(kanbanData != null) data = groupBy == 'default' ? kanbanData[regionID] : kanbanData[groupBy]; if(rdSearchValue != '' && groupBy == 'default' && data.laneCount != 0) $(this).parent().show(); if(rdSearchValue != '' && groupBy == 'default' && data.laneCount == 0) $(this).parent().hide(); $(this).empty(); hideAll = !updateRegion(regionID, data) && hideAll; }); if(hideAll && rdSearchValue != '') { if($('.table-empty-tip').length == 0) $('#kanban').append('

' + kanbancardLang.empty + '

'); } else { $('.table-empty-tip').remove(); } }); }