diff --git a/db/update16.1.sql b/db/update16.1.sql index f759fffb73..30870b5441 100644 --- a/db/update16.1.sql +++ b/db/update16.1.sql @@ -13,3 +13,5 @@ CREATE TABLE `zt_kanbancardgroup` ( ALTER TABLE `zt_kanban` ADD `displayCards` smallint(6) NOT NULL AFTER `order`; ALTER TABLE `zt_kanbancard` DROP COLUMN `lane`; ALTER TABLE `zt_kanbancard` DROP COLUMN `column`; + +UPDATE `zt_grouppriv` SET `method` = 'taskKanban' WHERE `module` = 'execution' AND `method` = 'kanban'; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 95959733ca..547d96b935 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -301,7 +301,7 @@ if($config->systemMode == 'new') $lang->execution->homeMenu->executionkanban = a $lang->execution->menu = new stdclass(); $lang->execution->menu->task = array('link' => "{$lang->task->common}|execution|task|executionID=%s", 'subModule' => 'task,tree', 'alias' => 'importtask,importbug'); -$lang->execution->menu->kanban = array('link' => "$lang->executionKanban|execution|kanban|executionID=%s"); +$lang->execution->menu->kanban = array('link' => "$lang->executionKanban|execution|taskkanban|executionID=%s"); $lang->execution->menu->burn = array('link' => "$lang->burn|execution|burn|executionID=%s"); $lang->execution->menu->view = array('link' => "$lang->view|execution|grouptask|executionID=%s", 'alias' => 'grouptask,tree,taskeffort,gantt,calendar,relation,maintainrelation'); $lang->execution->menu->story = array('link' => "$lang->SRCommon|execution|story|executionID=%s", 'subModule' => 'story', 'alias' => 'batchcreate,linkstory,storykanban'); diff --git a/module/execution/control.php b/module/execution/control.php index 3d18fcf079..407ab663c3 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1392,6 +1392,15 @@ class execution extends control } } + if(!empty($projectID) and $project->model == 'kanban') + { + $execution = $this->execution->getById($executionID); + $this->loadModel('kanban')->createRDKanban($execution); + + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('kanban', "executionID=$executionID"))); + } + if(!empty($planID)) { return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=no"))); @@ -1950,6 +1959,21 @@ class execution extends control /** * Kanban. * + * @param int $executionID + * @param string $browseType + * @param string $orderBy + * @param string $group + * @access public + * @return void + */ + public function kanban($executionID, $browseType = 'all', $orderBy = 'id_asc', $group = 'all') + { + $this->display(); + } + + /** + * Task kanban. + * * @param int $executionID * @param string $browseType story|bug|task|all * @param string $orderBy @@ -1957,7 +1981,7 @@ class execution extends control * @access public * @return void */ - public function kanban($executionID, $browseType = '', $orderBy = 'order_asc', $groupBy = '') + public function taskKanban($executionID, $browseType = '', $orderBy = 'order_asc', $groupBy = '') { if(empty($browseType)) $browseType = $this->session->kanbanType ? $this->session->kanbanType : 'all'; if(empty($groupBy) and $browseType != 'all') $groupBy = $this->session->{'kanbanGroupBy' . $browseType} ? $this->session->{'kanbanGroupBy' . $browseType} : 'default'; diff --git a/module/execution/css/taskkanban.css b/module/execution/css/taskkanban.css new file mode 100644 index 0000000000..372183c4b4 --- /dev/null +++ b/module/execution/css/taskkanban.css @@ -0,0 +1,58 @@ +#main {padding-bottom: 10px;} +#main > .container {max-width: 1960px!important; padding: 0 10px} + +.kanban + .kanban {margin-top: 15px} +.kanban-card > .title {display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis} +.kanban-card > .infos {position: relative; margin-top: 5px} +.kanban-card > .infos > .info + .info {margin-left: 8px} +.kanban-card > .infos > .info-id, +.kanban-card > .infos > .info-deadline, +.kanban-card > .infos > .info-estimate {font-size: 12px; position: relative; top: 2px} +.kanban-card > .infos > .label-pri {min-width: 16px; line-height: 14px; height: 16px; padding: 0} +.kanban-card > .infos > .label-severity {transform: scale(.75)} +.kanban-card > .infos > .avatar {position: absolute; right: 0; top: 0} +.kanban-card > .actions {position: absolute; top: 4px; right: 4px; opacity: 0;} +.kanban-card:hover > .actions {opacity: 1;} +.kanban-card > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.kanban-card > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} + +.kanban-header-col > .actions {padding-top: 22px} +.kanban-header-parent-col .kanban-header-col > .actions {padding-top: 6px; padding-right: 2px;} +.kanban-header-col > .actions > a {display: block; float: left; width: 20px; height: 20px; line-height: 20px; text-align: center; border-radius: 4px; opacity: .7;} +.kanban-header-col > .actions > a:hover {background-color: rgba(0,0,0,.075); opacity: 1;} + +.kanban-lane-name > .actions {position: absolute; top: 0; left: 0; right: 0; opacity: 0;} +.kanban-lane-name:hover > .actions {opacity: 1;} +.kanban-lane-name > .actions > a {display: block; width: 20px; height: 20px; line-height: 20px; text-align: center; opacity: .7; color: #fff;} +.kanban-lane-name > .actions > a:hover {background-color: rgba(0,0,0,.2); opacity: 1;} + +.kanban-affixed .kanban-header-col > .title > .text, +.kanban-affixed .kanban-header-col > .title > .count {color: #fff!important;} + +#kanbanContainer {margin: 0;} +#kanbanContainer > .panel-body {overflow: auto;} +#kanbans .kanban {min-height: initial;} +#kanbans .kanban-lane {min-height: 150px; margin-top: 2px;} +#kanbans .kanban-affixed > .kanban-header {top: 100px;} +#kanbans .kanban-card[data-scale-size="4"] {padding: 8px;} +#kanbans .kanban-card[data-scale-size="3"] {padding: 3px 4px;} +#kanbans .kanban-card[data-scale-size="3"] > .title {white-space: normal; max-height: 100%;} +#kanbans .kanban-card[data-scale-size="2"] {padding: 3px 4px; overflow: hidden;} +#kanbans .kanban-card[data-scale-size="2"] > .title {display: inline; line-height: 18px; white-space: normal; max-height: 100%; word-break: break-all;} +#kanbans .kanban-card[data-scale-size="2"] > .infos {display: inline-block; height: 18px; vertical-align: top; margin: 0;} +#kanbans .kanban-card[data-scale-size="2"] > .infos > .info-pri {transform: scale(.8); vertical-align: top;} +#kanbans .kanban-card[data-scale-size="2"] > .infos > .avatar {display: inline-block; transform: scale(.8); position: relative; top: -2px; vertical-align: top; margin-right: -1px;} + +#kanbanScaleControl {width: 100px;} +#kanbanScaleControl .input-group-addon {background: #fff; padding: 5px;} +#kanbanScaleControl > .input-group-btn:first-child > .btn {border-radius: 16px 0 0 16px; border-right-color: transparent;} +#kanbanScaleControl > .input-group-btn:last-child > .btn {border-radius: 0 16px 16px 0; border-left-color: transparent;} +#kanbanScaleControl > .input-group-btn > .btn:hover {border-color: #b8bfce;} + +#type_chosen .icon-kanban {padding-right: 10px; font-size: 15px;} + +.dropdown-menu a {text-align: left;} +.scrollbar-hover {max-height: 2000px; overflow: scroll !important;} +.c-type {width: 150px !important; overflow: unset;} +.c-group {width: 190px !important; overflow: unset;} +.c-type {margin-left: 0px !important;} diff --git a/module/execution/js/taskkanban.js b/module/execution/js/taskkanban.js new file mode 100644 index 0000000000..daf1724e18 --- /dev/null +++ b/module/execution/js/taskkanban.js @@ -0,0 +1,1070 @@ +function changeView(view) +{ + var link = createLink('execution', 'kanban', "executionID=" + executionID + '&type=' + view); + location.href = link; +} + +/** + * Render user avatar + * @param {String|{account: string, avatar: string}} user User account or user object + * @returns {string} + */ +function renderUserAvatar(user, objectType, objectID, size) +{ + 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, '', true); + } + if(objectType == 'story') + { + if(!priv.canAssignStory && !user) return $noPrivAndNoAssigned; + var link = createLink('story', 'assignto', 'id=' + objectID, '', true); + } + if(objectType == 'bug') + { + if(!priv.canAssignBug && !user) return $noPrivAndNoAssigned; + var link = createLink('bug', 'assignto', 'id=' + objectID, '', true); + } + + if(!user) return $(''); + + 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; + + return $('').avatar({user: user}); +} + +/** + * Render deadline + * @param {String|Date} deadline Deadline + * @returns {JQuery} + */ +function renderDeadline(deadline) +{ + 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'); + + return $('').text(deadlineLang + ' ' + deadlineDate).addClass(isEarlyThanToday ? 'text-red' : 'text-muted'); +} + +/** + * 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) +{ + 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('story', 'view', 'storyID=' + item.id, '', true)); + $title.appendTo($item); + } + $title.attr('title', item.title).find('.text').text(item.title); + } + + if(scaleSize <= 2) + { + var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; + var priHtml = '' + item.pri + ''; + var hoursHtml = (item.estimate && scaleSize <= 1) ? ('' + item.estimate + 'h') : ''; + var avatarHtml = renderUserAvatar(item.assignedTo, 'story', item.id); + 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.length) + { + $actions = $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + } + + 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); + } + $title.attr('title', item.title).find('.text').text(item.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); + + 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.length) + { + $actions = $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + } + + 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); + } + $title.attr('title', item.name).find('.text').text(item.name); + } + + if(scaleSize <= 2) + { + var idHtml = scaleSize <= 1 ? ('#' + item.id + '') : ''; + var priHtml = '' + item.pri + ''; + var hoursHtml = (item.estimate && scaleSize <= 1) ? ('' + item.estimate + 'h') : ''; + var avatarHtml = renderUserAvatar(item.assignedTo, 'task', item.id); + + var $infos = $item.find('.infos'); + if(!$infos.length) $infos = $('
'); + $infos.html([idHtml, priHtml, hoursHtml].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, 'task', item.id, 'md')); + } + + if(scaleSize <= 1) + { + var $actions = $item.find('.actions'); + if(!$actions.length && item.menus.length) + { + $actions = $([ + '
', + '', + '', + '', + '
' + ].join('')).appendTo($item); + } + } + + $item.attr('data-type', 'task').addClass('kanban-item-task'); + + 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) +{ + var text = count + '/' + (col.limit < 0 ? '' : col.limit); + $count.html(text + ''); +} + +/** + * 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'); + var $actions = $('
'); + var printStoryButton = printTaskButton = printBugButton = false; + if(priv.canCreateStory || priv.canBatchCreateStory || priv.canLinkStory || priv.canLinkStoryByPlane) printStoryButton = true; + if(priv.canCreateTask || priv.canBatchCreateTask) printTaskButton = true; + if(priv.canCreateBug || priv.canBatchCreateBug) printBugButton = true; + + if((col.type === 'backlog' && printStoryButton) || (col.type === 'wait' && printTaskButton) || (col.type == 'unconfirmed' && printBugButton)) + { + $actions.append([ + '', + '', + '' + ].join('')); + } + + $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(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; + + $kanban.data('zui.kanban').render(data); +} + +/** + * 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}, options)); +} + +function fullScreen() +{ + var element = document.getElementById('kanbanContainer'); + var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; + if(requestMethod) + { + var afterEnterFullscreen = function() + { + $('#kanbanContainer').addClass('scrollbar-hover'); + $('.actions').hide(); + $('#kanbanContainer a.iframe').each(function() + { + if($(this).hasClass('iframe')) + { + var href = $(this).attr('href'); + $(this).removeClass('iframe'); + $(this).attr('href', 'javascript:void(0)'); + $(this).attr('href-bak', href); + } + }) + $.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() +{ + $('#kanbanContainer').removeClass('scrollbar-hover'); + $('.actions').show(); + $('#kanbanContainer a').each(function() + { + var hrefBak = $(this).attr('href-bak'); + if(hrefBak) + { + $(this).addClass('iframe'); + $(this).attr('href', hrefBak); + } + }) + $.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'], + }, + 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', 'closed'], + 'developing': ['developed', 'pause'], + 'developed': ['canceled', 'closed'], + 'pause': ['developing'], + 'canceled': ['developing'], + '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; + + 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; + + 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 column type for a card + * @param {Object} card Card object + * @param {String} fromColType The column type before change + * @param {String} toColType The column type after change + * @param {String} kanbanID Kanban ID + */ +function changeCardColType(card, fromColType, toColType, kanbanID) +{ + if(typeof card == 'undefined') return false; + var objectID = card.id; + var showIframe = false; + var moveCard = false; + + /* Task lane. */ + if(kanbanID == 'task') + { + if(toColType == 'developed') + { + if((fromColType == 'developing' || fromColType == 'wait') && priv.canFinishTask) + { + var link = createLink('task', 'finish', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'pause') + { + if(fromColType == 'developing' && priv.canPauseTask) + { + var link = createLink('task', 'pause', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'developing') + { + if((fromColType == 'pause' || fromColType == 'cancel' || fromColType == 'closed' || fromColType == 'developed') && priv.canActivateTask) + { + var link = createLink('task', 'activate', 'taskID=' + objectID, '', true); + showIframe = true; + } + if(fromColType == 'wait' && priv.canStartTask) + { + var link = createLink('task', 'start', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'canceled') + { + if((fromColType == 'developing' || fromColType == 'wait' || fromColType == 'pause') && priv.canCancelTask) + { + var link = createLink('task', 'cancel', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + else if(toColType == 'closed') + { + if((fromColType == 'developed' || fromColType == 'canceled') && priv.canCloseTask) + { + var link = createLink('task', 'close', 'taskID=' + objectID, '', true); + showIframe = true; + } + } + } + + /* Bug lane. */ + if(kanbanID == 'bug') + { + if(toColType == 'confirmed') + { + if(fromColType == 'unconfirmed' && priv.canConfirmBug) + { + var link = createLink('bug', 'confirmBug', 'bugID=' + objectID, '', 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, '', true); + showIframe = true; + } + } + else if(toColType == 'fixed') + { + if(fromColType == 'fixing' || fromColType == 'confirmed' || fromColType == 'unconfirmed') + { + var link = createLink('bug', 'resolve', 'bugID=' + objectID, '', 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, '', true); + showIframe = true; + } + } + + if(moveCard) + { + var colID = card.$col.columnID; + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&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(kanbanID == 'story') + { + if(toColType == 'ready' || toColType == 'backlog') + { + var colID = card.$col.columnID; + var link = createLink('kanban', 'ajaxMoveCard', 'cardID=' + objectID + '&colID=' + colID + '&toColType=' + toColType + '&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]); + } + } + }) + } + } + + if(showIframe) + { + var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link}); + modalTrigger.show(); + } +} + +/** + * Handle finish drop task + * @param {Object} event Event object + * @returns {void} + */ +function handleFinishDrop(event) +{ + var $card = $(event.element); // The drag card + var $dragCol = $card.closest('.kanban-lane-col'); + var $dropCol = $(event.target); + + /* Get d-n-d(drag and drop) infos */ + var card = $card.data('item'); + var fromColType = $dragCol.data('type'); + var toColType = $dropCol.data('type'); + var kanbanID = $card.closest('.kanban').data('id'); + + changeCardColType(card, fromColType, toColType, kanbanID); + + $('#kanbans').find('.can-drop-here').removeClass('can-drop-here'); +} + +/** Handle sort cards in column */ +function handleSortColCards() +{ + /* TODO: handle sort cards from column contextmenu */ + return false; +} + +/** + * 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.columnID + '&executionID=' + executionID + '&from=execution'), className: 'iframe', attrs: {'data-width': '500px'}}) + if(priv.canSetWIP) items.push({label: executionLang.setWIP, url: $.createLink('kanban', 'setWIP', 'col=' + col.columnID + '&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, '', true), className: 'iframe'}); + if(priv.canBatchCreateStory) items.push({label: executionLang.batchCreateStroy, 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.canLinkStoryByPlane) 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'}); + if(priv.canBatchCreateBug) items.push({label: bugLang.batchCreate, url: $.createLink('bug', 'batchcreate', 'productID=' + productID + '&moduleID=0&executionID=' + executionID, '', true), className: 'iframe'}); + } + else + { + if(priv.canCreateTask) items.push({label: taskLang.create, url: $.createLink('task', 'create', 'executionID=' + executionID, '', true), className: 'iframe'}); + if(priv.canBatchCreateTask) items.push({label: taskLang.batchCreate, url: $.createLink('task', 'batchcreate', 'executionID=' + executionID, '', true), className: 'iframe'}); + } + 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') 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; + + 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; + + 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()}); + }); + + return newScaleSize; +} + +/* 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); + + /* Common options */  + var commonOptions = + { + maxColHeight: 'auto', + minColWidth: 240, + maxColWidth: 240, + cardHeight: getCardHeight(), + showCount: true, + showZeroCount: true, + fluidBoardWidth: false, + cardsPerRow: window.kanbanScaleSize, + virtualize: true, + virtualRenderOptions: {container: '#kanbanContainer>.panel-body'}, + droppable: + { + target: findDropColumns, + finish: handleFinishDrop, + mouseButton: 'left' + }, + onRenderHeaderCol: renderHeaderCol, + onRenderLaneName: renderLaneName, + onRenderCount: renderColumnCount + }; + + /* 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) + { + location.href = createLink('execution', 'importPlanStories', 'executionID=' + executionID + '&planID=' + planID + '&productID=0&fromMethod=kanban'); + } + }); + + $('#type_chosen .chosen-single span').prepend(''); + $('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': '); + + /* Ajax update kanban. */ + var lastUpdateData; + setInterval(function() + { + $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(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); +}); + +$('#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 + '&type=' + type); + location.href = link; +}); + +$('.c-group').change(function() +{ + $('.c-group').show(); + + var type = $('#type').val(); + var group = $('#group').val(); + var link = createLink('execution', 'kanban', 'executionID=' + executionID + '&type=' + type + '&orderBy=order_asc' + '&groupBy=' + group); + location.href = link; +}); diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php index 6006065bfc..40578c1152 100644 --- a/module/execution/lang/en.php +++ b/module/execution/lang/en.php @@ -247,6 +247,8 @@ $lang->execution->iteration = 'Iterations'; $lang->execution->iterationInfo = '%s Iterations'; $lang->execution->viewAll = 'View All'; $lang->execution->testreport = 'Test Report'; +$lang->execution->taskKanban = 'Task Kanban'; +$lang->execution->RDKanban = 'Research & Development Kanban'; /* Group browsing. */ $lang->execution->allTasks = 'All'; diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php index eed1188d4c..f33c35aeac 100644 --- a/module/execution/lang/zh-cn.php +++ b/module/execution/lang/zh-cn.php @@ -247,6 +247,8 @@ $lang->execution->iteration = '版本迭代'; $lang->execution->iterationInfo = '迭代%s次'; $lang->execution->viewAll = '查看所有'; $lang->execution->testreport = '测试报告'; +$lang->execution->taskKanban = '任务看板'; +$lang->execution->RDKanban = '研发看板'; /* 分组浏览。*/ $lang->execution->allTasks = '所有'; diff --git a/module/execution/model.php b/module/execution/model.php index e6663c158b..81a958accc 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -381,7 +381,7 @@ class executionModel extends model $creatorExists = false; $teamMembers = array(); - $this->loadModel('kanban')->createExecutionLane($executionID); + if(isset($project) and $project->model != 'kanban') $this->loadModel('kanban')->createExecutionLane($executionID); /* Save order. */ $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec(); diff --git a/module/execution/view/all.html.php b/module/execution/view/all.html.php index 4c46d38b2f..3652b0b19f 100644 --- a/module/execution/view/all.html.php +++ b/module/execution/view/all.html.php @@ -122,7 +122,8 @@ '>execution->typeList[$execution->type]?> children) ? $execution->name : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'"); + $executionLink = $execution->projectModel == 'kanban' ? html::a($this->createLink('execution', 'kanban', 'executionID=' . $execution->id), $execution->name, '', "class='text-ellipsis'") : html::a($this->createLink('execution', 'task', 'execution=' . $execution->id), $execution->name, '', "class='text-ellipsis'"); + echo !empty($execution->children) ? $execution->name : $executionLink; if(isset($execution->delay)) echo "{$lang->execution->delayed} "; ?> children)):?> diff --git a/module/execution/view/taskkanban.html.php b/module/execution/view/taskkanban.html.php new file mode 100644 index 0000000000..28fb732228 --- /dev/null +++ b/module/execution/view/taskkanban.html.php @@ -0,0 +1,175 @@ + + + + +
+ +
+
+
+
+
+ + + + + + + + + common::hasPriv('kanban', 'setColumn'), + 'canSetWIP' => common::hasPriv('kanban', 'setWIP'), + 'canSetLane' => common::hasPriv('kanban', 'setLane'), + 'canMoveLane' => common::hasPriv('kanban', 'laneMove'), + 'canSortCards' => common::hasPriv('kanban', 'cardsSort'), + 'canCreateTask' => $canCreateTask, + 'canBatchCreateTask' => $canBatchCreateTask, + 'canCreateBug' => $canCreateBug, + 'canBatchCreateBug' => $canBatchCreateBug, + 'canCreateStory' => $canCreateStory, + 'canBatchCreateStory' => $canBatchCreateStory, + 'canLinkStory' => $canLinkStory, + 'canLinkStoryByPlane' => $canLinkStoryByPlane, + 'canAssignTask' => common::hasPriv('task', 'assignto'), + 'canAssignStory' => common::hasPriv('story', 'assignto'), + 'canFinishTask' => common::hasPriv('task', 'finish'), + 'canPauseTask' => common::hasPriv('task', 'pause'), + 'canCancelTask' => common::hasPriv('task', 'cancel'), + 'canCloseTask' => common::hasPriv('task', 'close'), + 'canActivateTask' => common::hasPriv('task', 'activate'), + 'canStartTask' => common::hasPriv('task', 'start'), + 'canAssignBug' => common::hasPriv('bug', 'assignto'), + 'canConfirmBug' => common::hasPriv('bug', 'confirmBug'), + 'canActivateBug' => common::hasPriv('bug', 'activate') + ) +); +?> +execution);?> +story);?> +task);?> +bug);?> +execution->editName);?> +execution->setWIP);?> +execution->sortColumn);?> +kanban);?> +task->deadlineAB);?> +task->noAssigned);?> + + + diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index bd9da48739..9a66f4c96d 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -759,7 +759,7 @@ $lang->resource->execution->linkStory = 'linkStory'; $lang->resource->execution->unlinkStory = 'unlinkStory'; $lang->resource->execution->batchUnlinkStory = 'batchUnlinkStory'; $lang->resource->execution->updateOrder = 'updateOrder'; -$lang->resource->execution->kanban = 'kanban'; +$lang->resource->execution->taskKanban = 'taskKanban'; $lang->resource->execution->printKanban = 'printKanbanAction'; $lang->resource->execution->tree = 'treeAction'; $lang->resource->execution->treeTask = 'treeOnlyTask'; @@ -775,6 +775,7 @@ $lang->resource->execution->addWhitelist = 'addWhitelist'; $lang->resource->execution->unbindWhitelist = 'unbindWhitelist'; $lang->resource->execution->storyEstimate = 'storyEstimate'; $lang->resource->execution->executionkanban = 'kanbanAction'; +$lang->resource->execution->kanban = 'RDKanban'; //if($config->systemMode == 'classic') $lang->resource->project->list = 'list'; //$lang->execution->methodOrder[0] = 'index'; @@ -816,7 +817,7 @@ $lang->execution->methodOrder[170] = 'linkStory'; $lang->execution->methodOrder[175] = 'unlinkStory'; $lang->execution->methodOrder[180] = 'batchUnlinkStory'; $lang->execution->methodOrder[185] = 'updateOrder'; -$lang->execution->methodOrder[190] = 'kanban'; +$lang->execution->methodOrder[190] = 'taskKanban'; $lang->execution->methodOrder[195] = 'printKanban'; $lang->execution->methodOrder[200] = 'kanbanHideCols'; $lang->execution->methodOrder[205] = 'kanbanColsColor'; @@ -832,6 +833,7 @@ $lang->execution->methodOrder[250] = 'addWhitelist'; $lang->execution->methodOrder[255] = 'unbindWhitelist'; $lang->execution->methodOrder[260] = 'storyEstimate'; $lang->execution->methodOrder[265] = 'executionkanban'; +$lang->execution->methodOrder[270] = 'kanban'; /* Task. */ $lang->resource->task = new stdclass(); diff --git a/module/kanban/model.php b/module/kanban/model.php index e3edc42ed7..3e0c29fa7b 100644 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1503,6 +1503,124 @@ class kanbanModel extends model } } + /** + * Create a default RD kanban. + * + * @param object $execution + * @access public + * @return void + */ + public function createRDKanban($execution) + { + $regionID = $this->createRDRegion($execution); + if(dao::isError()) return false; + + $groupID = $this->createGroup($execution->id, $regionID); + if(dao::isError()) return false; + + $this->createRDLane($execution->id, $regionID); + if(dao::isError()) return false; + } + + /** + * Create a default RD region. + * + * @param object $execution + * + * @access public + * @return int|bool + */ + public function createRDRegion($execution) + { + $region = new stdclass(); + $region->name = $this->lang->kanbanregion->default; + $region->kanban = $execution->id; + $region->createdBy = $this->app->user->account; + $region->createdDate = helper::today(); + $region->order = 1; + + $this->dao->insert(TABLE_KANBANREGION)->data($region) + ->check('name', 'unique', "kanban={$execution->id} AND deleted='0'") + ->autoCheck() + ->exec(); + + if(dao::isError()) return false; + return $this->dao->lastInsertId(); + } + + /** + * Create default RD lanes. + * + * @param int $executionID + * @param int $regionID + * + * @access public + * @return bool + */ + public function createRDLane($executionID, $regionID) + { + $laneIndex = 0; + foreach($this->lang->kanban->laneTypeList as $type => $name) + { + $groupID = $this->createGroup($executionID, $regionID); + if(dao::isError()) return false; + + $lane = new stdclass(); + $lane->execution = $executionID; + $lane->type = $type; + $lane->region = $regionID; + $lane->group = $groupID; + $lane->name = $name; + $lane->color = $this->config->kanban->laneColorList[$laneIndex]; + $lane->order = ++ $laneIndex * 5; + + $this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec(); + if(dao::isError()) return false; + + $this->createRDColumn($regionID, $groupID, $this->dao->lastInsertId(), $type); + } + } + + /** + * Create default RD columns. + * + * @param int $regionID + * @param int $groupID + * @param int $laneID + * @param string $laneType + * + * @access public + * @return bool + */ + public function createRDColumn($regionID, $groupID, $laneID, $laneType) + { + $devColumnID = $testColumnID = $resolvingColumnID = 0; + if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn; + if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn; + if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn; + + foreach($columnList as $type => $name) + { + $data = new stdClass(); + $data->lane = $laneID; + $data->name = $name; + $data->color = '#333'; + $data->type = $type; + + if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID; + if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID; + if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID; + if(strpos(',develop,test,resolving,', $type) !== false) $data->parent = -1; + + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if(dao::isError()) return false; + + if($type == 'develop') $devColumnID = $this->dao->lastInsertId(); + if($type == 'test') $testColumnID = $this->dao->lastInsertId(); + if($type == 'resolving') $resolvingColumnID = $this->dao->lastInsertId(); + } + } + /** * Update a region. * diff --git a/module/project/model.php b/module/project/model.php index d47fb02ab0..0895b4bfe6 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -1834,7 +1834,7 @@ class projectModel extends model ->fetchPairs(); } - $executions = $this->dao->select('t1.*,t2.name projectName')->from(TABLE_EXECUTION)->alias('t1') + $executions = $this->dao->select('t1.*,t2.name projectName, t2.model as projectModel')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') ->where('t1.type')->in('sprint,stage') ->beginIF($projectID != 0)->andWhere('t1.project')->eq($projectID)->fi() @@ -1849,7 +1849,7 @@ class projectModel extends model } else { - $executions = $this->dao->select('t2.*,t3.name projectName')->from(TABLE_PROJECTPRODUCT)->alias('t1') + $executions = $this->dao->select('t2.*,t3.name projectName, t3.model as projectModel')->from(TABLE_PROJECTPRODUCT)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project=t2.id') ->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project=t3.id') ->where('t1.product')->eq($productID)