diff --git a/config/zentaopms.php b/config/zentaopms.php index 8508db61cd..5acf07c756 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -228,6 +228,8 @@ define('TABLE_RELATION', '`' . $config->db->prefix . 'relation`'); define('TABLE_REPOHISTORY', '`' . $config->db->prefix . 'repohistory`'); define('TABLE_REPOFILES', '`' . $config->db->prefix . 'repofiles`'); define('TABLE_REPOBRANCH', '`' . $config->db->prefix . 'repobranch`'); +define('TABLE_KANBANLANE', '`' . $config->db->prefix . 'kanbanlane`'); +define('TABLE_KANBANCOLUMN', '`' . $config->db->prefix . 'kanbancolumn`'); if(!defined('TABLE_LANG')) define('TABLE_LANG', '`' . $config->db->prefix . 'lang`'); if(!defined('TABLE_PROJECTSPEC')) define('TABLE_PROJECTSPEC', '`' . $config->db->prefix . 'projectspec`'); diff --git a/db/zentao.sql b/db/zentao.sql index 72235445df..925ff437a2 100644 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -595,6 +595,31 @@ CREATE TABLE IF NOT EXISTS `zt_job` ( `deleted` enum('0','1') NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_kanbanlane`; +CREATE TABLE IF NOT EXISTS `zt_kanbanlane` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `execution` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `order` smallint(6) NOT NULL DEFAULT '0', + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; +-- DROP TABLE IF EXISTS `zt_kanbancolumn`; +CREATE TABLE IF NOT EXISTS `zt_kanbancolumn` ( + `id` int(8) NOT NULL AUTO_INCREMENT, + `lane` mediumint(8) NOT NULL DEFAULT '0', + `parent` mediumint(8) NOT NULL DEFAULT '0', + `type` char(30) NOT NULL, + `name` varchar(255) NOT NULL DEFAULT '', + `color` char(30) NOT NULL, + `limit` smallint(6) NOT NULL DEFAULT '-1', + `order` mediumint(8) NOT NULL DEFAULT '0', + `cards` text NULL, + `deleted` enum('0','1') NOT NULL default '0', + PRIMARY KEY (`id`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8; -- DROP TABLE IF EXISTS `zt_lang`; CREATE TABLE IF NOT EXISTS `zt_lang` ( `id` mediumint(8) unsigned NOT NULL auto_increment, diff --git a/module/bug/model.php b/module/bug/model.php index 24718b5a6b..8cfa5e97a6 100644 --- a/module/bug/model.php +++ b/module/bug/model.php @@ -1575,7 +1575,9 @@ class bugModel extends model ->beginIF($type == 'noclosed')->andWhere('status')->ne('closed')->fi() ->beginIF($build)->andWhere("CONCAT(',', openedBuild, ',') like '%,$build,%'")->fi() ->beginIF($excludeBugs)->andWhere('id')->notIN($excludeBugs)->fi() - ->orderBy($orderBy)->page($pager)->fetchAll(); + ->orderBy($orderBy) + ->page($pager) + ->fetchAll('id'); } $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'bug'); diff --git a/module/common/lang/common.php b/module/common/lang/common.php index 57a3c16b1c..137b14fb8e 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -75,6 +75,7 @@ $lang->file = new stdclass(); $lang->misc = new stdclass(); $lang->acl = new stdclass(); $lang->curd = new stdclass(); +$lang->kanban = new stdclass(); $lang->projectbuild = new stdclass(); $lang->projectrelease = new stdclass(); diff --git a/module/execution/control.php b/module/execution/control.php index 3b71572c4a..f819837e60 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1825,26 +1825,27 @@ class execution extends control * Kanban. * * @param int $executionID - * @param string $type - * @param string $orderBy + * @param string $objectType + * @param string $groupBy * @access public * @return void */ - public function kanban($executionID, $type = 'story', $orderBy = 'order_asc') + public function kanban($executionID, $objectType = 'all', $groupBy = 'id_desc') { /* Save to session. */ $uri = $this->app->getURI(true); - $this->app->session->set('taskList', $uri, 'execution'); - $this->app->session->set('bugList', $uri, 'qa'); + $this->app->session->set('taskList', $uri, 'execution'); + $this->app->session->set('bugList', $uri, 'qa'); /* Compatibility IE8*/ if(strpos($this->server->http_user_agent, 'MSIE 8.0') !== false) header("X-UA-Compatible: IE=EmulateIE7"); + $lanes = $this->loadModel('kanban')->getLanesByExecution($executionID, $objectType); $this->execution->setMenu($executionID); $execution = $this->loadModel('execution')->getById($executionID); - $tasks = $this->execution->getKanbanTasks($executionID, "id"); - $bugs = $this->loadModel('bug')->getExecutionBugs($executionID); - $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 0, $orderBy); + $tasks = $this->execution->getKanbanTasks($executionID, "id"); + $bugs = $this->loadModel('bug')->getExecutionBugs($executionID); + $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 0, $orderBy); /* Determines whether an object is editable. */ $canBeChanged = common::canModify('execution', $execution); diff --git a/module/execution/model.php b/module/execution/model.php index a1ebf3927e..fa7539ccf2 100644 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -379,6 +379,8 @@ class executionModel extends model $creatorExists = false; $teamMembers = array(); + $this->loadModel('kanban')->addKanbanLanes($executionID); + /* Save order. */ $this->dao->update(TABLE_EXECUTION)->set('`order`')->eq($executionID * 5)->where('id')->eq($executionID)->exec(); $this->file->updateObjectID($this->post->uid, $executionID, 'execution'); @@ -3154,7 +3156,7 @@ class executionModel extends model */ public function getKanbanTasks($executionID, $orderBy = 'status_asc, id_desc', $pager = null) { - $tasks = $this->dao->select('t1.*, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName') + $tasks = $this->dao->select('t1.*, t1.id as id, t2.id AS storyID, t2.title AS storyTitle, t2.version AS latestStoryVersion, t2.status AS storyStatus, t3.realname AS assignedToRealName') ->from(TABLE_TASK)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_USER)->alias('t3')->on('t1.assignedTo = t3.account') @@ -3163,7 +3165,7 @@ class executionModel extends model ->andWhere('t1.parent')->ge(0) ->orderBy($orderBy) ->page($pager) - ->fetchAll(); + ->fetchAll('id'); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'task'); diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php new file mode 100644 index 0000000000..f61ea60e52 --- /dev/null +++ b/module/kanban/lang/zh-cn.php @@ -0,0 +1,35 @@ +kanban = new stdClass(); + +$lang->kanban->storyColumn = array(); +$lang->kanban->storyColumn['backlog'] = 'Backlog'; +$lang->kanban->storyColumn['ready'] = '准备好'; +$lang->kanban->storyColumn['develop'] = '开发'; +$lang->kanban->storyColumn['developing'] = '进行中'; +$lang->kanban->storyColumn['developed'] = '完成'; +$lang->kanban->storyColumn['test'] = '测试'; +$lang->kanban->storyColumn['testing'] = '进行中'; +$lang->kanban->storyColumn['tested'] = '完成'; +$lang->kanban->storyColumn['verified'] = '已验收'; +$lang->kanban->storyColumn['released'] = '已发布'; +$lang->kanban->storyColumn['closed'] = '已关闭'; + +$lang->kanban->bugColumn = array(); +$lang->kanban->bugColumn['unconfirmed'] = '待确认'; +$lang->kanban->bugColumn['confirmed'] = '已确认'; +$lang->kanban->bugColumn['resolving'] = '解决中'; +$lang->kanban->bugColumn['fixing'] = '进行中'; +$lang->kanban->bugColumn['fixed'] = '完成'; +$lang->kanban->bugColumn['test'] = '测试'; +$lang->kanban->bugColumn['testing'] = '进行中'; +$lang->kanban->bugColumn['tested'] = '完成'; +$lang->kanban->bugColumn['closed'] = '已关闭'; + +$lang->kanban->taskColumn = array(); +$lang->kanban->taskColumn['wait'] = '未开始'; +$lang->kanban->taskColumn['develop'] = '开发'; +$lang->kanban->taskColumn['developing'] = '研发中'; +$lang->kanban->taskColumn['developed'] = '研发完毕'; +$lang->kanban->taskColumn['pause'] = '已暂停'; +$lang->kanban->taskColumn['canceled'] = '已取消'; +$lang->kanban->taskColumn['closed'] = '已关闭'; diff --git a/module/kanban/model.php b/module/kanban/model.php new file mode 100644 index 0000000000..d709f69563 --- /dev/null +++ b/module/kanban/model.php @@ -0,0 +1,223 @@ + + * @package kanban + * @version $Id: model.php 5118 2021-10-22 10:18:41Z $ + * @link https://www.zentao.net + */ +?> +dao->select('*')->from(TABLE_KANBANLANE) + ->where('execution')->eq($executionID) + ->andWhere('deleted')->eq(0) + ->beginIF($objectType != 'all')->andWhere('type')->eq($objectType) + ->fetchAll('id'); + } + + /** + * Add execution Kanban lanes. + * + * @param int|array $executionIdList + * @access public + * @return void + */ + public function addKanbanLanes($executionIdList) + { + if(is_numeric($executionIdList)) $executionIdList = (array)$executionIdList; + if(!is_array($executionIdList) or empty($executionIdList)) return; + + /* Set lane Information. */ + global $lang; + $storyLane = clone $bugLane = clone $taskLane = new stdClass(); + $storyLane->type = 'story'; + $storyLane->name = $lang->SRCommon; + $storyLane->color = '#7ec5ff'; + $storyLane->order = '5'; + + $bugLane->type = 'bug'; + $bugLane->name = $lang->bug->common; + $bugLane->color = '#ba55d3'; + $bugLane->order = '10'; + + $taskLane->type = 'task'; + $taskLane->name = $lang->task->common; + $taskLane->color = '#4169e1'; + $taskLane->order = '15'; + + /* Get stories, bugs and tasks. */ + $stories = $this->dao->select('t1.*,t2.project as project')->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story') + ->where('t1.deleted')->eq(0) + ->andWhere('t1.parent')->ne('-1') + ->andWhere('t2.project')->in($executionIdList) + ->fetchGroup('project', 'id'); + + $bugs = $this->dao->select('*')->from(TABLE_BUG) + ->where('deleted')->eq(0) + ->andWhere('execution')->in($executionIdList) + ->fetchGroup('execution', 'id'); + + $tasks = $this->dao->select('*')->from(TABLE_TASK) + ->where('deleted')->eq(0) + ->andWhere('parent')->ne('-1') + ->andWhere('execution')->in($executionIdList) + ->fetchGroup('execution', 'id'); + + foreach($executionIdList as $executionID) + { + $storyLaneID = 0; + $bugLaneID = 0; + $taskLaneID = 0; + $developColumn = 0; + $testColumn = 0; + $resolvingColumn = 0; + + $lanes[$executionID] = new stdClass(); + $storyLane->execution = $bugLane->execution = $taskLane->execution = $executionID; + + $this->dao->insert(TABLE_KANBANLANE)->data($storyLane)->exec(); + $storyLaneID = $this->dao->lastInsertId(); + + $this->dao->insert(TABLE_KANBANLANE)->data($bugLane)->exec(); + $bugLaneID = $this->dao->lastInsertId(); + + $this->dao->insert(TABLE_KANBANLANE)->data($taskLane)->exec(); + $taskLaneID = $this->dao->lastInsertId(); + + $executionStories = empty($stories[$executionID]) ? array() : $stories[$executionID]; + foreach($this->lang->kanban->storyColumn as $type => $name) + { + $data = new stdClass(); + $data->lane = $storyLaneID; + $data->name = $name; + $data->type = $type; + $data->cards = ''; + if(strpos(',developing,developed,', $type) !== false) $data->parent = $developColumn; + if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumn; + if(strpos(',ready,develop,test,', $type) === false) + { + foreach($executionStories as $storyID => $story) + { + if($type == 'backlog' and $story->status == 'active' and $story->stage == 'projected') + { + $data->cards .= $storyID . ','; + unset($executionStories[$storyID]); + } + + if($type == 'closed' and $story->status == 'closed' and $story->stage == 'closed') + { + $data->cards .= $storyID . ','; + unset($executionStories[$storyID]); + } + + if($story->stage == $type and strpos(',backlog,closed,', $type) === false and $story->status == 'active') + { + $data->cards .= $storyID . ','; + unset($executionStories[$storyID]); + } + } + if(!empty($data->cards)) $data->cards = ',' . $data->cards; + } + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($type == 'develop') $developColumn = $this->dao->lastInsertId(); + if($type == 'test') $testColumn = $this->dao->lastInsertId(); + } + + $executionBugs = empty($bugs[$executionID]) ? array() : $bugs[$executionID]; + foreach($this->lang->kanban->bugColumn as $type => $name) + { + $data = new stdClass(); + $data->lane = $bugLaneID; + $data->name = $name; + $data->type = $type; + $data->cards = ''; + if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumn; + if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumn; + if(strpos(',resolving,fixing,test,testing,tested,', $type) === false) + { + foreach($executionBugs as $bugID => $bug) + { + if($type == 'unconfirmed' and $bug->status == 'active' and $bug->confirmed == 0) + { + $data->cards .= $bugID . ','; + unset($executionBugs[$bugID]); + } + + if($type == 'confirmed' and $bug->status == 'active' and $bug->confirmed == 1) + { + $data->cards .= $bugID . ','; + unset($executionBugs[$bugID]); + } + + if($type == 'fixed' and $bug->status == 'resolved') + { + $data->cards .= $bugID . ','; + unset($executionBugs[$bugID]); + } + + if($type == 'closed' and $bug->status == 'closed') + { + $data->cards .= $bugID . ','; + unset($executionBugs[$bugID]); + } + } + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($type == 'resolving') $resolvingColumn = $this->dao->lastInsertId(); + if($type == 'test') $testColumn = $this->dao->lastInsertId(); + } + } + + $executionTasks = empty($tasks[$executionID]) ? array() : $tasks[$executionID]; + foreach($this->lang->kanban->taskColumn as $type => $name) + { + $data = new stdClass(); + $data->lane = $taskLaneID; + $data->name = $name; + $data->type = $type; + $data->cards = ''; + if(strpos(',developing,developed,', $type) !== false) $data->parent = $developColumn; + if(strpos(',develop,', $type) === false) + { + foreach($executionTasks as $taskID => $task) + { + if($type == 'developing' and $task->status == 'doing') + { + $data->cards .= $taskID . ','; + unset($executionTasks[$taskID]); + } + + if($type == 'developed' and $task->status == 'done') + { + $data->cards .= $taskID . ','; + unset($executionTasks[$taskID]); + } + + if(strpos(',wait,pause,canceled,closed,', $type) !== false and $task->status == $type) + { + $data->cards .= $taskID . ','; + unset($executionTasks[$taskID]); + } + } + $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); + if($type == 'develop') $developColumn = $this->dao->lastInsertId(); + } + } + } + } +} diff --git a/module/upgrade/model.php b/module/upgrade/model.php index 48da796885..bb4db3301a 100644 --- a/module/upgrade/model.php +++ b/module/upgrade/model.php @@ -725,6 +725,11 @@ class upgradeModel extends model $this->saveLogs('Execute 15_6'); $this->execSQL($this->getUpgradeFile('15.6')); $this->appendExec('15_6'); + case '15_7': + $this->saveLogs('Execute 15_7'); + $this->execSQL($this->getUpgradeFile('15.7')); + $this->addKanbanLanes(); + $this->appendExec('15_7'); } $this->deletePatch(); @@ -5321,4 +5326,22 @@ class upgradeModel extends model $this->dao->update(TABLE_CONFIG)->set('`value`')->eq(trim($data->value, ','))->where('id')->eq($data->id)->exec(); return true; } + + /** + * Add single execution Kanban lanes. + * + * @access public + * @return bool + */ + public function addKanbanLanes() + { + $executionIdList = $this->dao->select('id')->from(TABLE_EXECUTION)->where('type')->in('sprint,stage')->fetchPairs(); + + if(empty($executionIdList)) return true; + + /* Create lanes. */ + $lanes = $this->loadModel('kanban')->addKanbanLanes($executionIdList); + + return true; + } }