diff --git a/db/update15.9.sql b/db/update15.9.sql
index c2e2acd946..64c41110e2 100644
--- a/db/update15.9.sql
+++ b/db/update15.9.sql
@@ -104,3 +104,5 @@ ADD `feedbackBy` varchar(100) NOT NULL AFTER `version`,
ADD `notifyEmail` varchar(100) NOT NULL AFTER `feedbackBy`;
ALTER TABLE `zt_product` ADD `reviewer` varchar(255) NOT NULL AFTER `whitelist`;
+
+ALTER TABLE `zt_kanbanlane` ADD `lastEditedTime` datetime NOT NULL AFTER `order`;
diff --git a/db/zentao.sql b/db/zentao.sql
index 8eb979c646..8dfa427632 100644
--- a/db/zentao.sql
+++ b/db/zentao.sql
@@ -648,6 +648,7 @@ CREATE TABLE IF NOT EXISTS `zt_kanbanlane` (
`name` varchar(255) NOT NULL DEFAULT '',
`color` char(30) NOT NULL,
`order` smallint(6) NOT NULL DEFAULT '0',
+ `lastEditedDate` datetime NOT NULL,
`deleted` enum('0','1') NOT NULL default '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/module/bug/model.php b/module/bug/model.php
index c9bf0d5c6f..b6041ce6c0 100644
--- a/module/bug/model.php
+++ b/module/bug/model.php
@@ -99,6 +99,8 @@ class bugModel extends model
$this->file->saveUpload('bug', $bugID);
empty($bug->case) ? $this->loadModel('score')->create('bug', 'create', $bugID) : $this->loadModel('score')->create('bug', 'createFormCase', $bug->case);
+ if($bug->execution) $this->loadModel('kanban')->updateLane($bug->execution, 'bug');
+
/* Callback the callable method to process the related data for object that is transfered to bug. */
if($from && is_callable(array($this, $this->config->bug->fromObjects[$from]['callback']))) call_user_func(array($this, $this->config->bug->fromObjects[$from]['callback']), $bugID);
@@ -251,6 +253,7 @@ class bugModel extends model
$bugID = $this->dao->lastInsertID();
$this->executeHooks($bugID);
+ if($bug->execution) $this->loadModel('kanban')->updateLane($bug->execution, 'bug');
/* When the bug is created by uploading the image, add the image to the file of the bug. */
$this->loadModel('score')->create('bug', 'create', $bugID);
@@ -681,6 +684,8 @@ class bugModel extends model
if(!empty($bug->resolvedBy)) $this->loadModel('score')->create('bug', 'resolve', $bugID);
$this->file->updateObjectID($this->post->uid, $bugID, 'bug');
+ if($bug->execution and $bug->status != $oldBug->status) $this->loadModel('kanban')->updateLane($bug->execution, 'bug');
+
return common::createChanges($oldBug, $bug);
}
}
@@ -919,6 +924,7 @@ class bugModel extends model
if(!dao::isError())
{
$this->loadModel('score')->create('bug', 'confirmBug', $oldBug);
+ if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug');
return common::createChanges($oldBug, $bug);
}
}
@@ -1031,6 +1037,7 @@ class bugModel extends model
if(!dao::isError())
{
$this->loadModel('score')->create('bug', 'resolve', $oldBug);
+ if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug');
/* Link bug to build and release. */
$this->linkBugToBuild($bugID, $bug->resolvedBuild);
@@ -1193,6 +1200,7 @@ class bugModel extends model
$this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec();
$this->executeHooks($bugID);
+ if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug');
$changes[$bugID] = common::createChanges($oldBug, $bug);
}
@@ -1250,6 +1258,7 @@ class bugModel extends model
}
}
+ if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug');
$bug->activatedCount += 1;
return common::createChanges($oldBug, $bug);
}
@@ -1278,6 +1287,7 @@ class bugModel extends model
->get();
$this->dao->update(TABLE_BUG)->data($bug)->autoCheck()->where('id')->eq((int)$bugID)->exec();
+ if($oldBug->execution) $this->loadModel('kanban')->updateLane($oldBug->execution, 'bug');
return common::createChanges($oldBug, $bug);
}
diff --git a/module/bug/view/create.html.php b/module/bug/view/create.html.php
index f49d8dd535..d88a84117d 100644
--- a/module/bug/view/create.html.php
+++ b/module/bug/view/create.html.php
@@ -26,6 +26,7 @@ js::set('isStepsTemplate', $isStepsTemplate);
js::set('oldProjectID', $projectID);
js::set('blockID', $blockID);
js::set('moduleID', $moduleID);
+a(inlink('view', "bugid=1"));
?>
diff --git a/module/execution/control.php b/module/execution/control.php
index 841142d36b..b9a4427e6a 100644
--- a/module/execution/control.php
+++ b/module/execution/control.php
@@ -3319,6 +3319,32 @@ class execution extends control
die(html::select("group", $groups, $group, 'class="form-control chosen" data-max_drop_width="215"'));
}
+ /**
+ * Ajax update kanban.
+ *
+ * @param int $executionID
+ * @param string $enterTime
+ * @param string $browseType
+ * @param string $groupBy
+ * @access public
+ * @return void
+ */
+ public function ajaxUpdateKanban($executionID = 0, $enterTime = '', $browseType = '', $groupBy = '')
+ {
+ $enterTime = date('Y-m-d H:i:s', $enterTime);
+ $lastEditedTime = $this->dao->select("max(lastEditedTime) as lastEditedTime")->from(TABLE_KANBANLANE)->where('execution')->eq($executionID)->fetch('lastEditedTime');
+
+ if($lastEditedTime > $enterTime)
+ {
+ $kanbanGroup = $this->loadModel('kanban')->getExecutionKanban($executionID, $browseType, $groupBy);
+ die(json_encode($kanbanGroup));
+ }
+ else
+ {
+ die('');
+ }
+ }
+
/**
* AJAX: Update the execution name.
*
diff --git a/module/execution/js/kanban.js b/module/execution/js/kanban.js
index e43e4e874e..8ce3c9e1f3 100644
--- a/module/execution/js/kanban.js
+++ b/module/execution/js/kanban.js
@@ -466,8 +466,64 @@ function findDropColumns($element, $root)
*/
function changeCardColType(card, fromColType, toColType, kanbanID)
{
- /* TODO: Post data to server on change card type 将变更卡片类型操作提交到服务器 */
- console.log('TODO: Post data to server on change card type 将变更卡片类型操作提交到服务器', {card, fromColType, toColType, kanbanID});
+ if(typeof card == 'undefined') return false;
+ var objectID = card.id;
+ var showIframe = false;
+
+ 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;
+ }
+ }
+ }
+
+ if(showIframe)
+ {
+ var modalTrigger = new $.zui.ModalTrigger({type: 'iframe', width: '80%', url: link});
+ modalTrigger.show();
+ }
/*
// TODO: The server must return a updated kanban data 服务器返回更新后的看板数据
@@ -771,6 +827,34 @@ $(function()
$('#type_chosen .chosen-single span').prepend('');
$('#group_chosen .chosen-single span').prepend(kanbanLang.laneGroup + ': ');
+
+ /* Ajax update kanban. */
+ setInterval(function()
+ {
+ $.get(createLink('execution', 'ajaxUpdateKanban', "executionID=" + executionID + "&entertime=" + entertime + "&browseType=" + browseType + "&groupBy=" + groupBy), function(data)
+ {
+ if(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()
diff --git a/module/execution/view/kanban.html.php b/module/execution/view/kanban.html.php
index 48213f32ed..534de79bca 100644
--- a/module/execution/view/kanban.html.php
+++ b/module/execution/view/kanban.html.php
@@ -137,7 +137,13 @@ js::set('priv',
'canLinkStoryByPlane' => $canLinkStoryByPlane,
'canAssignTask' => common::hasPriv('task', 'assignto'),
'canAssignStory' => common::hasPriv('story', 'assignto'),
- 'canAssignBug' => common::hasPriv('bug', 'assignto')
+ 'canAssignBug' => common::hasPriv('bug', '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')
)
);
?>
@@ -152,4 +158,5 @@ js::set('priv',
task->deadlineAB);?>
task->noAssigned);?>
+
diff --git a/module/kanban/model.php b/module/kanban/model.php
index b02817e3c4..b9df8bd351 100644
--- a/module/kanban/model.php
+++ b/module/kanban/model.php
@@ -337,6 +337,24 @@ class kanbanModel extends model
}
}
+ /**
+ * Update kanban lane.
+ *
+ * @param int $executionID
+ * @param string $laneType
+ * @access public
+ * @return void
+ */
+ public function updateLane($executionID, $laneType)
+ {
+ $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE)
+ ->where('execution')->eq($executionID)
+ ->andWhere('type')->eq($laneType)
+ ->fetchAll('id');
+
+ foreach($lanes as $lane) $this->updateCards($lane);
+ }
+
/**
* Update column cards.
*
@@ -448,6 +466,8 @@ class kanbanModel extends model
{
$this->dao->update(TABLE_KANBANCOLUMN)->set('cards')->eq($cards)->where('lane')->eq($lane->id)->andWhere('type')->eq($colType)->exec();
}
+
+ $this->dao->update(TABLE_KANBANLANE)->set('lastEditedTime')->eq(helper::now())->where('id')->eq($lane->id)->exec();
}
/**
diff --git a/module/story/model.php b/module/story/model.php
index a3cbcd947d..57768e318c 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -280,6 +280,7 @@ class storyModel extends model
{
$this->linkStory($executionID, $this->post->product, $storyID);
if($this->config->systemMode == 'new' and $executionID != $this->session->project) $this->linkStory($this->session->project, $this->post->product, $storyID);
+ $this->loadModel('kanban')->updateLane($executionID, 'story');
}
if(is_array($this->post->URS))
@@ -1816,6 +1817,8 @@ class storyModel extends model
$tasks[] = $taskID;
$this->action->create('task', $taskID, 'Opened', '');
}
+
+ $this->loadModel('kanban')->updateLane($executionID, 'task');
return $tasks;
}
diff --git a/module/task/model.php b/module/task/model.php
index 3ce29cb28d..597708c727 100644
--- a/module/task/model.php
+++ b/module/task/model.php
@@ -138,6 +138,8 @@ class taskModel extends model
$this->dao->insert(TABLE_TASKSPEC)->data($taskSpec)->autoCheck()->exec();
if(dao::isError()) return false;
+ $this->loadModel('kanban')->updateLane($executionID, 'task');
+
if($this->post->story) $this->loadModel('story')->setStage($this->post->story);
if($this->post->selectTestStory)
{
@@ -408,7 +410,12 @@ class taskModel extends model
$mails[$i]->actionID = $actionID;
}
- if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchCreate');
+ if(!dao::isError())
+ {
+ $this->loadModel('score')->create('ajax', 'batchCreate');
+ $this->loadModel('kanban')->updateLane($executionID, 'task');
+ }
+
if($parentID > 0 && !empty($taskID))
{
$oldParentTask = $this->dao->select('*')->from(TABLE_TASK)->where('id')->eq((int)$parentID)->fetch();
@@ -1008,6 +1015,7 @@ class taskModel extends model
if($this->post->story != false) $this->loadModel('story')->setStage($this->post->story);
if($task->status == 'done') $this->loadModel('score')->create('task', 'finish', $taskID);
if($task->status == 'closed') $this->loadModel('score')->create('task', 'close', $taskID);
+ if($task->status != $oldTask->status) $this->loadModel('kanban')->updateLane($task->execution, 'task');
$this->loadModel('action');
$changed = $task->parent != $oldTask->parent;
@@ -1286,6 +1294,7 @@ class taskModel extends model
if($task->status == 'done') $this->loadModel('score')->create('task', 'finish', $taskID);
if($task->status == 'closed') $this->loadModel('score')->create('task', 'close', $taskID);
+ if($task->status != $oldTask->status) $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
$allChanges[$taskID] = common::createChanges($oldTask, $task);
}
else
@@ -1461,6 +1470,7 @@ class taskModel extends model
$this->computeBeginAndEnd($oldTask->parent);
}
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
+ $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
@@ -1719,7 +1729,11 @@ class taskModel extends model
if($oldTask->parent > 0) $this->updateParentStatus($taskID);
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
- if($task->status == 'done' && !dao::isError()) $this->loadModel('score')->create('task', 'finish', $taskID);
+ if($task->status == 'done' && !dao::isError())
+ {
+ $this->loadModel('score')->create('task', 'finish', $taskID);
+ $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
+ }
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
@@ -1778,6 +1792,8 @@ class taskModel extends model
if($oldTask->parent > 0) $this->updateParentStatus($taskID);
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
$this->loadModel('score')->create('task', 'close', $taskID);
+ $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
+
return common::createChanges($oldTask, $task);
}
}
@@ -1816,6 +1832,7 @@ class taskModel extends model
$this->dao->update(TABLE_TASK)->set('assignedTo=openedBy')->where('parent')->eq((int)$taskID)->exec();
}
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
+ $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
if(!dao::isError()) return common::createChanges($oldTask, $task);
}
@@ -1881,6 +1898,7 @@ class taskModel extends model
$this->computeWorkingHours($taskID);
}
if($oldTask->story) $this->loadModel('story')->setStage($oldTask->story);
+ $this->loadModel('kanban')->updateLane($oldTask->execution, 'task');
if(!dao::isError()) return common::createChanges($oldTask, $task);
}