From e465d1768f34cd1ae6ec3b3cb0a21ff9d3d6d40c Mon Sep 17 00:00:00 2001 From: sunguangming Date: Thu, 30 May 2024 14:17:45 +0800 Subject: [PATCH] * Code for task #119474. --- module/common/model.php | 4 +- module/execution/control.php | 5 +- module/execution/js/taskkanban.ui.js | 2 +- module/execution/model.php | 2 +- module/execution/ui/taskkanban.html.php | 5 +- module/kanban/model.php | 99 ++++++++++++++----- module/kanban/tao.php | 32 ++++++ .../kanban/test/lib/kanban.unittest.class.php | 2 +- .../kanban/test/model/getexecutionkanban.php | 28 +++--- 9 files changed, 132 insertions(+), 47 deletions(-) diff --git a/module/common/model.php b/module/common/model.php index abd9aebe00..187e4bade4 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -2412,7 +2412,7 @@ class commonModel extends model { if(!empty($actionData['url']) && is_array($actionData['url'])) { - $module = $actionData['url']['module'] == 'story' ? $data->type : $actionData['url']['module']; + $module = ($actionData['url']['module'] == 'story' && $moduleName == 'story') ? $data->type : $actionData['url']['module']; $method = $actionData['url']['method']; $params = $actionData['url']['params']; if(!common::hasPriv($module, $method)) return false; @@ -2420,7 +2420,7 @@ class commonModel extends model } else if(!empty($actionData['data-url']) && is_array($actionData['data-url'])) { - $module = $actionData['data-url']['module'] == 'story' ? $data->type : $actionData['data-url']['module']; + $module = ($actionData['data-url']['module'] == 'story' && $moduleName == 'story') ? $data->type : $actionData['data-url']['module']; $method = $actionData['data-url']['method']; $params = $actionData['data-url']['params']; if(!common::hasPriv($module, $method)) return false; diff --git a/module/execution/control.php b/module/execution/control.php index d6e9336139..aa8924a19b 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -1749,11 +1749,11 @@ class execution extends control /* Get kanban data. */ $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy; - $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); if(empty($kanbanGroup)) { $this->kanban->createExecutionLane($executionID, $browseType); - $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); } /* Show lanes of the attribute: no story and bug in request, no bug in design. */ @@ -1769,6 +1769,7 @@ class execution extends control $this->view->browseType = $browseType; $this->view->features = $features; $this->view->kanbanGroup = $kanbanGroup; + $this->view->links = $links; $this->view->groupBy = $groupBy; $this->display(); diff --git a/module/execution/js/taskkanban.ui.js b/module/execution/js/taskkanban.ui.js index 59514a2f3f..7723d2c7ae 100644 --- a/module/execution/js/taskkanban.ui.js +++ b/module/execution/js/taskkanban.ui.js @@ -233,7 +233,7 @@ window.getItem = function(info) info.item.title = info.item.title.replaceAll(searchValue, "" + searchValue + ""); info.item.title = {html: info.item.title}; } - info.item.titleUrl = info.laneInfo.type == 'story' ? $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`) : $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`); + info.item.titleUrl = ['story', 'epic', 'requirement', 'parentStory'].includes(info.laneInfo.type) ? $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`) : $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`); info.item.titleAttrs = {'data-toggle': 'modal', 'data-size': 'lg', 'title': info.item.title, 'class': 'card-title clip'}; info.item.content = {html: content}; diff --git a/module/execution/model.php b/module/execution/model.php index cf4cf100ac..5f74324099 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -2891,7 +2891,7 @@ class executionModel extends model if($storyType == 'requirement' and $execution->model == 'ipd') $this->dao->update(TABLE_STORY)->set('status')->eq('developing')->where('id')->eq($storyID)->exec(); } - if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID, $storyType); + if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID); return true; } diff --git a/module/execution/ui/taskkanban.html.php b/module/execution/ui/taskkanban.html.php index 525b13e5bf..75abfdb543 100644 --- a/module/execution/ui/taskkanban.html.php +++ b/module/execution/ui/taskkanban.html.php @@ -230,7 +230,10 @@ div ( set::key('kanban'), set::items($kanbanGroup), - set::height('calc(100vh - 120px)') + set::height('calc(100vh - 120px)'), + set::links($links), + set::selectable(true), + set::showLinkOnSelected(true) ) ); diff --git a/module/kanban/model.php b/module/kanban/model.php index e8902a1837..44df81465c 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1381,8 +1381,6 @@ class kanbanModel extends model ->fetchgroup('lane', 'column'); /* Get group objects. */ - if($browseType == 'all' || $browseType == 'epic') $objectGroup['epic'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic'); - if($browseType == 'all' || $browseType == 'requirement') $objectGroup['requirement'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement'); if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); @@ -1454,31 +1452,73 @@ class kanbanModel extends model ->fetchGroup('lane', 'id'); /* Get group objects. */ - if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); - if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); - if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); + if($browseType == 'all' || $browseType == 'epic') $objectGroup['epic'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic'); + if($browseType == 'all' || $browseType == 'requirement') $objectGroup['requirement'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement'); + if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); /* Get objects cards menus. */ $menus = array(); - $menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array(); - $menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array(); - $menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array(); + $menus['epic'] = $browseType == 'all' || $browseType == 'epic' ? $this->getKanbanCardMenu($executionID, $objectGroup['epic'], 'epic') : array(); + $menus['requirement'] = $browseType == 'all' || $browseType == 'requirement' ? $this->getKanbanCardMenu($executionID, $objectGroup['requirement'], 'requirement') : array(); + $menus['parentStory'] = $browseType == 'all' || $browseType == 'parentStory' ? $this->getKanbanCardMenu($executionID, $objectGroup['parentStory'], 'parentStory') : array(); + $menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array(); + $menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array(); + $menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array(); - /* Build kanban group data. */ - $kanbanGroup = array(); + /* 获取看板连线的fromKanbanID. */ + $fromKanbanID = ''; foreach($lanes as $lane) { - list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus); - - $kanbanGroup[$lane->type]['id'] = $lane->id; - $kanbanGroup[$lane->type]['key'] = 'group' . $lane->id; - $kanbanGroup[$lane->type]['data'] = array(); - $kanbanGroup[$lane->type]['data']['lanes'] = array($laneData); - $kanbanGroup[$lane->type]['data']['cols'] = $columnData; - $kanbanGroup[$lane->type]['data']['items'] = $cardsData; + if($lane->type == 'parentStory') $fromKanbanID = 'group' . $lane->id; } - return array_values($kanbanGroup); + /* Build kanban group data. */ + $kanbanGroup = array(); + $links = array(); + foreach($lanes as $lane) + { + $laneType = $lane->type; + list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus); + + $kanbanID = 'group' . $lane->id; + + $kanbanGroup[$laneType]['id'] = $lane->id; + $kanbanGroup[$laneType]['key'] = $kanbanID; + $kanbanGroup[$laneType]['data'] = array(); + $kanbanGroup[$laneType]['data']['lanes'] = array($laneData); + $kanbanGroup[$laneType]['data']['cols'] = $columnData; + $kanbanGroup[$laneType]['data']['items'] = $cardsData; + + foreach($cardsData as $columnCard) + { + foreach($columnCard as $cards) + { + foreach($cards as $card) + { + if($card['parent'] == 0) continue; + /* 获取看板卡片的连线关系,业需、用需和父需求共用同一组看板列,所以fromKanban和toKanban是一样的。 */ + $link = array('from' => $card['parent'], 'to' => $card['id'], 'fromKanban' => $fromKanbanID, 'toKanban' => $kanbanID); + if(in_array($laneType, array('epic', 'requirement'))) $link['toKanban'] = $fromKanbanID; + + $links[] = $link; + } + } + } + } + + foreach(array('requirement', 'epic') as $laneType) + { + if(!isset($kanbanGroup[$laneType])) continue; + + array_unshift($kanbanGroup['parentStory']['data']['lanes'], $kanbanGroup[$laneType]['data']['lanes'][0]); + $kanbanGroup['parentStory']['data']['items'] += $kanbanGroup[$laneType]['data']['items']; + unset($kanbanGroup[$laneType]); + } + + return array(array_values($kanbanGroup), $links); } /** @@ -1592,6 +1632,7 @@ class kanbanModel extends model $cardData['column'] = $column->id; $cardData['pri'] = zget($object, 'pri', 0); $cardData['group'] = $laneType; + $cardData['parent'] = zget($object, 'originParent', 0); $cardData['status'] = zget($object, 'status', ''); $cardData['estimate'] = zget($object, 'estimate', 0); $cardData['assignedTo'] = $object->assignedTo; @@ -2334,8 +2375,7 @@ class kanbanModel extends model if($URSRLanes) { - $parentStoryLane = $this->dao->select('*')->from(TABLE_KANBANLANE)->where('id')->eq($parentStoryLaneID)->fetch(); - $columnIDList = $this->dao->select('id')->from(TABLE_KANBANCOLUMN)->where('deleted')->eq(0)->andWhere('archived')->eq(0)->andWhere('`group`')->eq($parentStoryLane->group)->fetchPairs(); + $columnIDList = $this->dao->select('`column`')->from(TABLE_KANBANCELL)->where('lane')->eq($parentStoryLaneID)->fetchPairs(); foreach($URSRLanes as $type => $laneID) { foreach($columnIDList as $columnID) @@ -2674,7 +2714,7 @@ class kanbanModel extends model * @access public * @return void */ - public function updateLane(int $executionID, string $laneType, int $cardID = 0) + public function updateLane(int $executionID, string $laneType = '', int $cardID = 0) { $execution = $this->loadModel('execution')->getByID($executionID); if($execution->type == 'kanban') @@ -2686,18 +2726,18 @@ class kanbanModel extends model ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.kanban')->eq($executionID) ->andWhere('t2.execution')->eq($executionID) - ->andWhere('t2.type')->eq($laneType) + ->beginIF($laneType)->andWhere('t2.type')->eq($laneType)->fi() ->beginIF(!empty($cardID))->andWhere('t3.cards')->like("%,$cardID,%")->fi() ->orderBy('t1.`order` asc, t2.`order` asc') ->fetchAll('id'); - if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 1); + if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 2); } else { $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) ->where('execution')->eq($executionID) - ->andWhere('type')->eq($laneType) + ->beginIF($laneType)->andWhere('type')->eq($laneType)->fi() ->fetchAll('id'); } @@ -3523,6 +3563,15 @@ class kanbanModel extends model $menus = array(); switch ($objecType) { + case 'epic': + $menus = $this->kanbanTao->getURSRCardMenu($executionID, $objects); + break; + case 'requirement': + $menus = $this->kanbanTao->getURSRCardMenu($executionID, $objects); + break; + case 'parentStory': + $menus = $this->kanbanTao->getURSRCardMenu($executionID, $objects); + break; case 'story': $menus = $this->kanbanTao->getStoryCardMenu($executionID, $objects); break; diff --git a/module/kanban/tao.php b/module/kanban/tao.php index 69dd797b66..5298b9f579 100644 --- a/module/kanban/tao.php +++ b/module/kanban/tao.php @@ -964,6 +964,38 @@ class kanbanTao extends kanbanModel return $cardPairs; } + /** + * 获取父需求类型的卡片操作菜单。 + * Get menu of story card. + * + * @param int $executionID + * @param array $objects + * @access public + * @return array + */ + protected function getURSRCardMenu(int $executionID, array $objects): array + { + $execution = $this->loadModel('execution')->getByID($executionID); + + $menus = array(); + $objects = $this->loadModel('story')->mergeReviewer($objects); + foreach($objects as $story) + { + $menu = array(); + + if(common::hasPriv($story->type, 'edit') and $this->story->isClickable($story, 'edit')) $menu[] = array('label' => $this->lang->story->edit, 'icon' => 'edit', 'url' => helper::createLink($story->type, 'edit', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'change') and $this->story->isClickable($story, 'change')) $menu[] = array('label' => $this->lang->story->change, 'icon' => 'alter', 'url' => helper::createLink($story->type, 'change', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'review') and $this->story->isClickable($story, 'review')) $menu[] = array('label' => $this->lang->story->review, 'icon' => 'search', 'url' => helper::createLink($story->type, 'review', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'activate') and $this->story->isClickable($story, 'activate')) $menu[] = array('label' => $this->lang->story->activate, 'icon' => 'magic', 'url' => helper::createLink($story->type, 'activate', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv('execution', 'unlinkStory') && $execution->hasProduct) $menu[] = array('label' => $this->lang->execution->unlinkStory, 'icon' => 'unlink', 'url' => helper::createLink('execution', 'unlinkStory', "executionID=$executionID&storyID=$story->story&confirm=no&from=taskkanban")); + if(common::hasPriv($story->type, 'delete')) $menu[] = array('label' => $this->lang->story->delete, 'icon' => 'trash', 'url' => helper::createLink($story->type, 'delete', "storyID=$story->id&confirm=no&from=taskkanban")); + + $menus[$story->id] = $menu; + } + + return $menus; + } + /** * 获取需求类型的卡片操作菜单。 * Get menu of story card. diff --git a/module/kanban/test/lib/kanban.unittest.class.php b/module/kanban/test/lib/kanban.unittest.class.php index 3be2d716ec..282f4d3ad7 100644 --- a/module/kanban/test/lib/kanban.unittest.class.php +++ b/module/kanban/test/lib/kanban.unittest.class.php @@ -583,7 +583,7 @@ class kanbanTest if(empty($objects)) { $this->objectModel->createExecutionLane($executionID, $browseType, $groupBy); - $objects = $this->objectModel->getExecutionKanban($executionID, $browseType, $groupBy); + list($objects, $links) = $this->objectModel->getExecutionKanban($executionID, $browseType, $groupBy); } if(dao::isError()) return dao::getError(); diff --git a/module/kanban/test/model/getexecutionkanban.php b/module/kanban/test/model/getexecutionkanban.php index 5505826c36..24d7e941df 100755 --- a/module/kanban/test/model/getexecutionkanban.php +++ b/module/kanban/test/model/getexecutionkanban.php @@ -20,23 +20,23 @@ title=测试 kanbanModel->getExecutionKanban(); timeout=0 cid=1 -- 测试获取execution2的执行看板信息 @columns:2, lanes:2, cards:65 +- 测试获取execution2的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution2 story的执行看板信息 @columns:1, lanes:1, cards:0 +- 测试获取execution2 story的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution2 task的执行看板信息 @columns:1, lanes:1, cards:65 +- 测试获取execution2 task的执行看板信息 @columns:0, lanes:0, cards:0 - 测试获取execution2 bug的执行看板信息 @columns:10, lanes:1, cards:0 -- 测试获取execution3的执行看板信息 @columns:2, lanes:2, cards:53 +- 测试获取execution3的执行看板信息 @columns:0, lanes:0, cards:0 - 测试获取execution3 story pri的执行看板信息 @columns:0, lanes:1, cards:0 - 测试获取execution3 story category的执行看板信息 @columns:0, lanes:1, cards:0 -- 测试获取execution4的执行看板信息 @columns:2, lanes:2, cards:53 +- 测试获取execution4的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution4 task module的执行看板信息 @columns:2, lanes:1, cards:107 +- 测试获取execution4 task module的执行看板信息 @columns:2, lanes:1, cards:109 - 测试获取execution4 story source的执行看板信息 @columns:0, lanes:1, cards:0 @@ -48,13 +48,13 @@ $groupByList = array('pri', 'category', 'module', 'source', 'assignedTo', 's $kanban = new kanbanTest(); -r($kanban->getExecutionKanbanTest($executionIDList[0])) && p() && e('columns:2, lanes:2, cards:65'); // 测试获取execution2的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[0])) && p() && e('columns:1, lanes:1, cards:0'); // 测试获取execution2 story的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[1])) && p() && e('columns:1, lanes:1, cards:65'); // 测试获取execution2 task的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[2])) && p() && e('columns:10, lanes:1, cards:0'); // 测试获取execution2 bug的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[1])) && p() && e('columns:2, lanes:2, cards:53'); // 测试获取execution3的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[0])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2 story的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[1])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2 task的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[2])) && p() && e('columns:10, lanes:1, cards:0'); // 测试获取execution2 bug的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[1])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution3的执行看板信息 r($kanban->getExecutionKanbanTest($executionIDList[1], $browseTypeList[0], $groupByList[0])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution3 story pri的执行看板信息 r($kanban->getExecutionKanbanTest($executionIDList[1], $browseTypeList[0], $groupByList[1])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution3 story category的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2])) && p() && e('columns:2, lanes:2, cards:53'); // 测试获取execution4的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[1], $groupByList[2])) && p() && e('columns:2, lanes:1, cards:107'); // 测试获取execution4 task module的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[0], $groupByList[3])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution4 story source的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution4的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[1], $groupByList[2])) && p() && e('columns:2, lanes:1, cards:109'); // 测试获取execution4 task module的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[0], $groupByList[3])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution4 story source的执行看板信息 \ No newline at end of file