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 @@
+
+
+
+
+
+
+
+
+
+
+ 1execution->kanbanCardsUnit; ?>
+
+
+
+
+
+
';
+ ?>
+ execution = $executionID;
+ $canCreateTask = common::hasPriv('task', 'create', $checkObject);
+ $canBatchCreateTask = common::hasPriv('task', 'batchCreate', $checkObject);
+ $canCreateBug = common::hasPriv('bug', 'create');
+ $canBatchCreateBug = common::hasPriv('bug', 'batchCreate');
+ $canCreateStory = ($productID and common::hasPriv('story', 'create'));
+ $canBatchCreateStory = ($productID and common::hasPriv('story', 'batchCreate'));
+ $canLinkStory = ($productID and common::hasPriv('execution', 'linkStory'));
+ $canLinkStoryByPlane = ($productID and common::hasPriv('execution', 'importplanstories'));
+ $hasStoryButton = ($canCreateStory or $canBatchCreateStory or $canLinkStory or $canLinkStoryByPlane);
+ $hasTaskButton = ($canCreateTask or $canBatchCreateTask);
+ $hasBugButton = ($canCreateBug or $canBatchCreateBug);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ execution->linkStory, "id='toStoryButton'", 'btn btn-primary');?>
+
+
+
+
+
+
+
+
+
+
+
+ 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)