diff --git a/module/gitlab/control.php b/module/gitlab/control.php
index 2d6917d7e2..363d551703 100644
--- a/module/gitlab/control.php
+++ b/module/gitlab/control.php
@@ -486,7 +486,7 @@ class gitlab extends control
}
/**
- * Browse gitlab project.
+ * Browse gitlab user.
*
* @param int $gitlabID
* @access public
@@ -501,7 +501,7 @@ class gitlab extends control
}
/**
- * Creat a gitlab project.
+ * Creat a gitlab user.
*
* @param int $gitlabID
* @access public
diff --git a/module/gitlab/model.php b/module/gitlab/model.php
index 5581bf454e..df57a31ebd 100644
--- a/module/gitlab/model.php
+++ b/module/gitlab/model.php
@@ -2186,4 +2186,17 @@ class gitlabModel extends model
return false;
}
+ /**
+ * Get products which scm is GitLab by projects.
+ *
+ * @param array $projectIDs
+ * @return array
+ */
+ public function getProductsByProjects($projectIDs)
+ {
+ return $this->dao->select('path,product')->from(TABLE_REPO)->where('deleted')->eq('0')
+ ->andWhere('SCM')->eq('Gitlab')
+ ->andWhere('path')->in($projectIDs)
+ ->fetchPairs('path', 'product');
+ }
}
diff --git a/module/mr/control.php b/module/mr/control.php
index 86a7db6f1a..40196a980d 100644
--- a/module/mr/control.php
+++ b/module/mr/control.php
@@ -389,6 +389,333 @@ class mr extends control
return $this->send($this->mr->reopen($MR));
}
+ /**
+ * link MR list.
+ *
+ * @param int $MRID
+ * @param string $type
+ * @param string $orderBy
+ * @param string $link
+ * @param string $param
+ * @param int $recTotal
+ * @param int $recPerPage
+ * @param int $pageID
+ * @return void
+ */
+ public function link($MRID, $type = 'story', $orderBy = 'id_desc', $link = 'false', $param = '', $recTotal = 0, $recPerPage = 100, $pageID = 1)
+ {
+ $this->app->loadLang('productplan');
+ $this->app->loadLang('bug');
+ $this->app->loadLang('task');
+
+ $MR = $this->mr->getByID($MRID);
+ $products = $this->loadModel('gitlab')->getProductsByProjects(array($MR->targetProject, $MR->sourceProject));
+ $product = $this->loadModel('product')->getById(array_shift($products));
+
+ /* Load pager. */
+ $this->app->loadClass('pager', $static = true);
+ $storyPager = new pager(0, $recPerPage, $type == 'story' ? $pageID : 1);
+ $bugPager = new pager(0, $recPerPage, $type == 'bug' ? $pageID : 1);
+ $taskPager = new pager(0, $recPerPage, $type == 'task' ? $pageID : 1);
+
+ $stories = $this->mr->getLinkList($MRID, $product->id, 'story', $orderBy, $storyPager);
+ $bugs = $this->mr->getLinkList($MRID, $product->id, 'bug', $orderBy, $bugPager);
+ $tasks = $this->mr->getLinkList($MRID, $product->id, 'task', $orderBy, $taskPager);
+
+ $this->view->MR = $MR;
+ $this->view->canBeChanged = true;
+ $this->view->modulePairs = $this->loadModel('tree')->getOptionMenu($product->id, 'story');
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->stories = $stories;
+ $this->view->summary = $this->product->summary($stories);
+ $this->view->bugs = $bugs;
+ $this->view->tasks = $tasks;
+ $this->view->products = $products;
+ $this->view->product = $product;
+ $this->view->storyPager = $storyPager;
+ $this->view->bugPager = $bugPager;
+ $this->view->taskPager = $taskPager;
+ $this->view->type = $type;
+ $this->view->orderBy = $orderBy;
+ $this->view->link = $link;
+ $this->view->param = $param;
+ $this->display();
+ }
+
+ /**
+ * Link story to mr.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $browseType
+ * @param int $param
+ * @param string $orderBy
+ * @param int $recTotal
+ * @param int $recPerPage
+ * @param int $pageID
+ * @access public
+ * @return void
+ */
+ public function linkStory($MRID, $productID = 0, $browseType = '', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1)
+ {
+ if(!empty($_POST['stories']))
+ {
+ $this->mr->link($MRID, $productID, 'story');
+ die(js::locate(inlink('link', "MRID=$MRID&type=story&orderBy=$orderBy"), 'parent'));
+ }
+
+ $this->loadModel('story');
+ $this->app->loadLang('productplan');
+
+ $product = $this->loadModel('product')->getById($productID);
+ $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'story');
+
+ /* Build search form. */
+ $storyStatusList = $this->lang->story->statusList;
+ unset($storyStatusList['closed']);
+ $queryID = ($browseType == 'bySearch') ? (int) $param : 0;
+
+ unset($this->config->product->search['fields']['product']);
+ $this->config->product->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=story&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
+ $this->config->product->search['queryID'] = $queryID;
+ $this->config->product->search['style'] = 'simple';
+ $this->config->product->search['params']['product']['values'] = array($product) + array('all' => $this->lang->product->allProductsOfProject);
+ $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($productID => $productID));
+ $this->config->product->search['params']['module']['values'] = $modules;
+ $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList);
+
+ if($this->session->currentProductType == 'normal')
+ {
+ unset($this->config->product->search['fields']['branch']);
+ unset($this->config->product->search['params']['branch']);
+ }
+ else
+ {
+ $this->config->product->search['fields']['branch'] = $this->lang->product->branch;
+ $branches = array('' => '') + $this->loadModel('branch')->getPairs($productID, 'noempty');
+ $this->config->product->search['params']['branch']['values'] = $branches;
+ }
+ $this->loadModel('search')->setSearchParams($this->config->product->search);
+
+ $MR = $this->mr->getByID($MRID);
+ $relatedStories = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'story');
+
+ $linkedStories = $this->mr->getLinkList($MRID, $product->id, '', 'story');
+ if($browseType == 'bySearch')
+ {
+ $allStories = $this->story->getBySearch($productID, 0, $queryID, 'id', '', 'story', array_keys($linkedStories), null);
+ }
+ else
+ {
+ $allStories = $this->story->getProductStories($productID, 0, $moduleID = '0', $status = 'draft,active,changed', 'story', 'id_desc', $hasParent = false, array_keys($linkedStories), null);
+ }
+
+ $this->view->modules = $modules;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->allStories = $allStories;
+ $this->view->relatedStories = $relatedStories;
+ $this->view->product = $product;
+ $this->view->MRID = $MRID;
+ $this->view->browseType = $browseType;
+ $this->view->param = $param;
+ $this->view->orderBy = $orderBy;
+ $this->display();
+ }
+
+ /**
+ * Link bug to mr.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $browseType
+ * @param int $param
+ * @param string $orderBy
+ * @param int $recTotal
+ * @param int $recPerPage
+ * @param int $pageID
+ * @access public
+ * @return void
+ */
+ public function linkBug($MRID, $productID = 0, $browseType = '', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1)
+ {
+ if(!empty($_POST['bugs']))
+ {
+ $this->mr->link($MRID, $productID, 'bug');
+ die(js::locate(inlink('link', "MRID=$MRID&type=bug&orderBy=$orderBy"), 'parent'));
+ }
+
+ $this->loadModel('bug');
+ $this->app->loadLang('productplan');
+ $queryID = ($browseType == 'bysearch') ? (int)$param : 0;
+
+ $product = $this->loadModel('product')->getById($productID);
+ $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'bug');
+
+ /* Build search form. */
+ $this->config->bug->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=bug&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
+ $this->config->bug->search['queryID'] = $queryID;
+ $this->config->bug->search['style'] = 'simple';
+ $this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getForProducts(array($productID => $productID));
+ $this->config->bug->search['params']['module']['values'] = $modules;
+ $this->config->bug->search['params']['project']['values'] = $this->product->getExecutionPairsByProduct($productID);
+ $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = '');
+ $this->config->bug->search['params']['resolvedBuild']['values'] = $this->loadModel('build')->getProductBuildPairs($productID, $branch = 0, $params = '');
+
+ unset($this->config->bug->search['fields']['product']);
+ if($this->session->currentProductType == 'normal')
+ {
+ unset($this->config->bug->search['fields']['branch']);
+ unset($this->config->bug->search['params']['branch']);
+ }
+ else
+ {
+ $this->config->bug->search['fields']['branch'] = $this->lang->product->branch;
+ $branches = array('' => '') + $this->loadModel('branch')->getPairs($productID, 'noempty');
+ $this->config->bug->search['params']['branch']['values'] = $branches;
+ }
+ $this->loadModel('search')->setSearchParams($this->config->bug->search);
+
+ $MR = $this->mr->getByID($MRID);
+ $relatedBugs = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'bug');
+
+ $linkedBugs = $this->mr->getLinkList($MRID, $product->id, '','bug');
+ if($browseType == 'bySearch')
+ {
+ $allBugs = $this->bug->getBySearch($productID, 0, $queryID, 'id_desc', array_keys($linkedBugs), null);
+ }
+ else
+ {
+ $allBugs = $this->bug->getActiveBugs($productID, 0, '0', array_keys($linkedBugs), null);
+ }
+
+ $this->view->modules = $modules;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->allBugs = $allBugs;
+ $this->view->relatedBugs = $relatedBugs;
+ $this->view->product = $product;
+ $this->view->MRID = $MRID;
+ $this->view->browseType = $browseType;
+ $this->view->param = $param;
+ $this->view->orderBy = $orderBy;
+ $this->display();
+ }
+
+ /**
+ * Link task to mr.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $browseType
+ * @param int $param
+ * @param string $orderBy
+ * @param int $recTotal
+ * @param int $recPerPage
+ * @param int $pageID
+ * @access public
+ * @return void
+ */
+ public function linkTask($MRID, $productID = 0, $browseType = 'unclosed', $param = 0, $orderBy = 'id_desc', $recTotal = 0, $recPerPage = 100, $pageID = 1)
+ {
+ if(!empty($_POST['tasks']))
+ {
+ $this->mr->link($MRID, $productID, 'task');
+ die(js::locate(inlink('link', "MRID=$MRID&type=task&orderBy=$orderBy"), 'parent'));
+ }
+
+ $this->loadModel('execution');
+ $this->loadModel('product');
+ $this->app->loadLang('task');
+
+ /* Set browse type. */
+ $browseType = strtolower($browseType);
+ $queryID = ($browseType == 'bysearch') ? (int)$param : 0;
+
+ $product = $this->loadModel('product')->getById($productID);
+ $modules = $this->loadModel('tree')->getOptionMenu($productID, $viewType = 'task');
+
+ /* Build search form. */
+ $this->config->execution->search['actionURL'] = $this->createLink('mr', 'link', "$MRID=$MRID&type=task&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
+ $this->config->execution->search['queryID'] = $queryID;
+ $this->config->execution->search['params']['module']['values'] = $modules;
+ $this->loadModel('search')->setSearchParams($this->config->execution->search);
+
+ $MR = $this->mr->getByID($MRID);
+ $relatedTasks = $this->mr->getCommitedLink($MR->gitlabID, $MR->targetProject, $MR->mriid, 'task');
+ $linkedTasks = $this->mr->getLinkList($MRID, $product->id, '','task');
+
+ /* Get executions by product. */
+ $productExecutions = $this->product->getExecutionPairsByProduct($productID);
+ $productExecutionIDs = array_filter(array_keys($productExecutions));
+ $this->config->execution->search['params']['execution']['values'] = array_filter($productExecutions);
+
+ /* Get tasks by executions. */
+ $allTasks = array();
+ foreach($productExecutionIDs as $productExecutionID)
+ {
+ $tasks = $this->execution->getTasks(0, $productExecutionID, array(), $browseType, $queryID, 0, $orderBy, null);
+ $allTasks = array_merge($tasks, $allTasks);
+ }
+ /* Filter linked tasks. */
+ $linkedTaskIDs = array_keys($linkedTasks);
+ foreach($allTasks as $key => $task)
+ {
+ if(in_array($task->id, $linkedTaskIDs)) unset($allTasks[$key]);
+ }
+
+ $this->view->modules = $modules;
+ $this->view->users = $this->loadModel('user')->getPairs('noletter');
+ $this->view->allTasks = $allTasks;
+ $this->view->relatedTasks = $relatedTasks;
+ $this->view->product = $product;
+ $this->view->MRID = $MRID;
+ $this->view->browseType = $browseType;
+ $this->view->param = $param;
+ $this->view->orderBy = $orderBy;
+ $this->display();
+ }
+
+ /**
+ * UnLink an mr link.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $type
+ * @param int $linkID
+ * @param string $confirm
+ * @access public
+ * @return mix
+ */
+ public function unlink($MRID, $productID, $type, $linkID, $confirm = 'no')
+ {
+ $this->app->loadLang('productplan');
+
+ if($confirm == 'no')
+ {
+ die(js::confirm($this->lang->productplan->confirmUnlinkStory, $this->createLink('mr', 'unlink', "MRID=$MRID&productID=$productID&linkID=$linkID&type=$type&confirm=yes")));
+ }
+ else
+ {
+ $this->mr->unlink($MRID, $productID, $type, $linkID);
+
+ /* if ajax request, send result. */
+ if($this->server->ajax)
+ {
+ if(dao::isError())
+ {
+ $response['result'] = 'fail';
+ $response['message'] = dao::getError();
+ }
+ else
+ {
+ $response['result'] = 'success';
+ $response['message'] = '';
+ }
+ return $this->send($response);
+ }
+ die(js::reload('parent'));
+ }
+ }
+
/**
* Add a Bug for this review.
*
diff --git a/module/mr/css/link.css b/module/mr/css/link.css
new file mode 100644
index 0000000000..3a10e0993b
--- /dev/null
+++ b/module/mr/css/link.css
@@ -0,0 +1,25 @@
+#storyList th {border: none;}
+#bugList th {border: none;}
+#taskList th {border: none;}
+.hide-side .col-main {width: 100%;}
+.hide-side .col-side .main-side {width: 0; display: none;}
+
+.dropdown-menu.with-search {padding: 0; min-width: 150px; overflow: hidden; max-height: 302px;}
+.dropdown-menu > .menu-search .input-group {width: 100%;}
+.dropdown-menu > .menu-search .input-group-addon {position: absolute; right: 10px; top: 0; z-index: 10; background: none; border: none; color: #666;}
+.dropdown-menu > .menu-search .form-control {border: none !important; box-shadow: none !important; border-top: 1px solid #ddd !important;}
+.dropdown-list {display: block; padding: 0; max-height: 270px; overflow-y: auto;}
+.dropdown-list > li > a {display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.53846154; color: #141414; white-space: nowrap;}
+.dropdown-list > li > a:hover,
+.dropdown-list > li > a:focus {color: #1a4f85; text-decoration: none; background-color: #ddd;}
+
+.checkbox.btn {margin-top: 0px;}
+
+.linkBox #queryBox .search-form .form-actions {padding-bottom: 5px;}
+.linkBox .table-header {padding: 8px 15px; border-bottom: 1px solid #cbd0db;}
+#unlinkStoryList, #unlinkBugList, #unlinkTaskList{border-top: 1px solid #cbd0db;}
+.fixed-footer .text {color: #fff;}
+ol, ul {padding-left: 20px;}
+#tabsNav .tab-pane>.main-table {border-radius: 0;}
+.body-modal #mainMenu>.btn-toolbar {width: auto;}
+.body-modal #mainContent {min-height: 240px;}
diff --git a/module/mr/js/link.js b/module/mr/js/link.js
new file mode 100644
index 0000000000..343413fbf9
--- /dev/null
+++ b/module/mr/js/link.js
@@ -0,0 +1,86 @@
+function showLink(productID, type, orderBy, param)
+{
+ if(type == 'story') method = 'linkStory';
+ else if(type == 'bug') method = 'linkBug';
+ else if(type == 'task') method = 'linkTask';
+
+ loadURL(createLink('mr', method, 'MRID=' + MRID + '&productID=' + productID + (typeof(param) == 'undefined' ? '' : param) + (typeof(orderBy) == 'undefined' ? '' : "&orderBy=" + orderBy)), type);
+
+ $('.actions').find("a[href*='" + type + "']").addClass('hidden');
+}
+
+/**
+ * Load URL.
+ *
+ * @param string $url
+ * @param string $type
+ * @access public
+ * @return void
+ */
+function loadURL(url, type)
+{
+ $.get(url, function(data)
+ {
+ var $pane = $(type == 'story' ? '#stories' : (type == 'bug' ? '#bugs' : '#tasks'));
+ $pane.find('.main-table').hide();
+ var $linkBox = $pane.find('.linkBox').html(data).removeClass('hidden');
+ $linkBox.html(data).removeClass('hidden');
+ $linkBox.find('[data-ride="table"]').table();
+ $linkBox.find('[data-ride="pager"]').pager();
+ $linkBox.find('[data-ride="pager"] li a.pager-item').click(function()
+ {
+ loadURL($(this).attr('href'), type);
+ return false;
+ });
+ $linkBox.find('[data-ride="pager"] .pager-size-menu a[data-size]').off('click');
+ $linkBox.find('[data-ride="pager"] .pager-size-menu a[data-size]').click(function()
+ {
+ line = $linkBox.find('[data-ride="pager"]').attr('data-link-creator');
+ line = line.replace('{recPerPage}', $(this).attr('data-size')).replace('{page}', $linkBox.find('[data-ride="pager"]').attr('data-page'));
+ $.cookie($linkBox.find('[data-ride="pager"]').attr('data-page-cookie'), $(this).attr('data-size'), {expires:config.cookieLife, path:config.webRoot});
+ loadURL(line, type);
+ return false;
+ });
+ $.toggleQueryBox(true, $linkBox.find('#queryBox'));
+ });
+}
+
+$(function()
+{
+ if(link == 'true') showLink(productID, type, orderBy, param);
+ var infoShowed = false;
+ $('.nav.nav-tabs a[data-toggle="tab"]').on('shown.zui.tab', function(e)
+ {
+ var href = $(e.target).attr('href');
+ var tabPane = $(href + '.tab-pane');
+ if(tabPane.size() == 0) return;
+ var formID = tabPane.find('.linkBox').find('form:last');
+ if(formID.size() == 0) formID = tabPane.find('form:last');
+ if(href == '#planInfo' && !infoShowed)
+ {
+ $('#planInfo img').each(function()
+ {
+ var $tr = $('#planInfo .detail-content .table-data tbody tr:first');
+ width = $tr.width() - $tr.find('th').width();
+ if($(this).parent().prop('tagName').toLowerCase() == 'a') $(this).unwrap();
+ setImageSize($(this), width, 0);
+ });
+
+ infoShowed = true;
+ }
+ });
+
+ $('#storyList').on('sort.sortable', function(e, data)
+ {
+ var list = '';
+ for(i = 0; i < data.list.length; i++) list += $(data.list[i].item).attr('data-id') + ',';
+ $.post(createLink('productplan', 'ajaxStorySort', 'productID=' + productID), {'stories' : list, 'orderBy' : orderBy, 'pageID' : storyPageID, 'recPerPage' : storyRecPerPage, 'recTotal' : storyRecTotal}, function()
+ {
+ var $target = $(data.element[0]);
+ $target.hide();
+ $target.fadeIn(1000);
+ order = 'order_asc';
+ history.pushState({}, 0, createLink('productplan', 'view', "productID=" + productID + '&type=story&orderBy=' + order));
+ });
+ });
+});
diff --git a/module/mr/lang/en.php b/module/mr/lang/en.php
index eeb7a6dc78..b2706bcaa3 100644
--- a/module/mr/lang/en.php
+++ b/module/mr/lang/en.php
@@ -12,6 +12,7 @@ $lang->mr->source = 'source';
$lang->mr->target = 'target';
$lang->mr->viewDiff = 'View diff';
$lang->mr->viewInGitlab = 'View in GitLab';
+$lang->mr->link = 'Link stories,bugs,tasks';
$lang->mr->id = 'ID';
$lang->mr->mriid = "raw MR ID";
@@ -141,3 +142,10 @@ $lang->mr->commandDocument = <<< EOD
EOD;
$lang->mr->noChanges = "Currently there are no changes in this merge request's source branch. Please push new commits or use a different branch.";
+
+$lang->mr->linkTask = "Link task";
+$lang->mr->unlinkTask = "Remove task";
+$lang->mr->linkedTasks = 'Task';
+$lang->mr->unlinkedTasks = 'Task not linked';
+$lang->mr->confirmUnlinkTask = "Are you sure to remove this task?";
+$lang->mr->taskSummary = "There are %s tasks on this page";
\ No newline at end of file
diff --git a/module/mr/lang/zh-cn.php b/module/mr/lang/zh-cn.php
index 5a971d6bd4..5345936511 100644
--- a/module/mr/lang/zh-cn.php
+++ b/module/mr/lang/zh-cn.php
@@ -12,6 +12,7 @@ $lang->mr->source = '源项目分支';
$lang->mr->target = '目标项目分支';
$lang->mr->viewDiff = '比对代码';
$lang->mr->viewInGitlab = '在GitLab查看';
+$lang->mr->link = '关联需求、bug、任务';
$lang->mr->id = 'ID';
$lang->mr->mriid = "MR原始ID";
@@ -141,3 +142,10 @@ $lang->mr->commandDocument = <<< EOD
EOD;
$lang->mr->noChanges = "目前在这个合并请求的源分支中没有变化,请推送新的提交或使用不同的分支。";
+
+$lang->mr->linkTask = "关联任务";
+$lang->mr->unlinkTask = "移除任务";
+$lang->mr->linkedTasks = '任务';
+$lang->mr->unlinkedTasks = '未关联任务';
+$lang->mr->confirmUnlinkTask = "您确认移除该任务吗?";
+$lang->mr->taskSummary = "本页共 %s 个任务";
diff --git a/module/mr/model.php b/module/mr/model.php
index 6787228352..4b5d5ba981 100644
--- a/module/mr/model.php
+++ b/module/mr/model.php
@@ -697,6 +697,21 @@ class mrModel extends model
return json_decode(commonModel::http($url));
}
+ /**
+ * Get diff commits of MR from GitLab API.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $MRID
+ * @access public
+ * @return object
+ */
+ public function apiGetDiffCommits($gitlabID, $projectID, $MRID)
+ {
+ $url = sprintf($this->gitlab->getApiRoot($gitlabID), "/projects/$projectID/merge_requests/$MRID/commits");
+ return json_decode(commonModel::http($url));
+ }
+
/**
* Reject or Approve this MR.
*
@@ -972,4 +987,130 @@ class mrModel extends model
return $this->dao->select('*')->from(TABLE_BUG)->where('entry')->eq($entry)->orderby('id_desc')->fetch();
}
+ /**
+ * Get mr link list.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $type
+ * @param string $orderBy
+ * @param object $pager
+ * @access public
+ * @return array
+ */
+ public function getLinkList($MRID, $productID, $type, $orderBy, $pager = null)
+ {
+ $linkIDs = $this->dao->select('BID')->from(TABLE_RELATION)
+ ->where('product')->eq($productID)
+ ->andWhere('relation')->eq('interrated')
+ ->andWhere('AType')->eq('mr')
+ ->andWhere('AID')->eq($MRID)
+ ->andWhere('BType')->eq($type)
+ ->fetchPairs('BID');
+
+ $links = array();
+ if($type == 'story' and !empty($linkIDs))
+ {
+ $orderBy = str_replace('name_', 'title_', $orderBy);
+ $links = $this->dao->select('t1.*, t2.spec, t2.verify, t3.name as productTitle')
+ ->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_STORYSPEC)->alias('t2')->on('t1.id=t2.story')
+ ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product=t3.id')
+ ->where('t1.deleted')->eq(0)
+ ->andWhere('t1.version=t2.version')
+ ->andWhere('t1.id')->in($linkIDs)
+ ->orderBy($orderBy)
+ ->page($pager)
+ ->fetchAll('id');
+ }
+ if($type == 'bug' and !empty($linkIDs))
+ {
+ $orderBy = str_replace('name_', 'title_', $orderBy);
+ $links = $this->dao->select('*')->from(TABLE_BUG)
+ ->where('deleted')->eq(0)
+ ->andWhere('id')->in($linkIDs)
+ ->orderBy($orderBy)
+ ->page($pager)
+ ->fetchAll('id');
+ }
+ if($type == 'task' and !empty($linkIDs))
+ {
+ $orderBy = str_replace('title_', 'name_', $orderBy);
+ $links = $this->dao->select('*')->from(TABLE_TASK)
+ ->where('deleted')->eq(0)
+ ->andWhere('id')->in($linkIDs)
+ ->orderBy($orderBy)
+ ->page($pager)
+ ->fetchAll('id');
+ }
+
+ return $links;
+ }
+
+ /**
+ * Create an mr link.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $type
+ * @access public
+ * @return void
+ */
+ public function link($MRID, $productID, $type)
+ {
+ if($type == 'story') $links = $this->post->stories;
+ if($type == 'bug') $links = $this->post->bugs;
+ if($type == 'task') $links = $this->post->tasks;
+
+ foreach($links as $linkID)
+ {
+ $relation = new stdclass;
+ $relation->product = $productID;
+ $relation->AType = 'mr';
+ $relation->AID = $MRID;
+ $relation->relation = 'interrated';
+ $relation->BType = $type;
+ $relation->BID = $linkID;
+
+ $this->dao->replace(TABLE_RELATION)->data($relation)->exec();
+ }
+ }
+
+ /**
+ * unLink an mr link.
+ *
+ * @param int $MRID
+ * @param int $productID
+ * @param string $type
+ * @param int $linkID
+ * @access public
+ * @return void
+ */
+ public function unlink($MRID, $productID, $type, $linkID)
+ {
+ return $this->dao->delete()->from(TABLE_RELATION)->where('product')->eq($productID)->andWhere('AType')->eq('mr')->andWhere('AID')->eq($MRID)->andWhere('BType')->eq($type)->andWhere('BID')->eq($linkID)->exec();
+ }
+
+ /**
+ * Get links by mr commites.
+ *
+ * @param int $gitlabID
+ * @param int $projectID
+ * @param int $MRID
+ * @param string $type
+ * @access public
+ * @return array
+ */
+ public function getCommitedLink($gitlabID, $projectID, $MRID, $type)
+ {
+ $DiffCommits = $this->apiGetDiffCommits($gitlabID, $projectID, $MRID);
+
+ $commits = array();
+ foreach($DiffCommits as $DiffCommit)
+ {
+ $commits[] = substr($DiffCommit->id, 0, 10);
+ }
+
+ return $this->dao->select('objectID')->from(TABLE_ACTION)->where('objectType')->eq($type)->andWhere('extra')->in($commits)->fetchPairs('objectID');
+ }
}
diff --git a/module/mr/view/browse.html.php b/module/mr/view/browse.html.php
index 06733d6f26..10e9b3cd4f 100644
--- a/module/mr/view/browse.html.php
+++ b/module/mr/view/browse.html.php
@@ -79,6 +79,7 @@
common::printLink('mr', 'view', "mr={$MR->id}", '', '', "title='{$lang->mr->view}' class='btn btn-info'");
common::printLink('mr', 'edit', "mr={$MR->id}", '', '', "title='{$lang->mr->edit}' class='btn btn-info'");
common::printLink('mr', 'diff', "mr={$MR->id}", '', '', "title='{$lang->mr->viewDiff}' class='btn btn-info'");
+ common::printLink('mr', 'link', "mr={$MR->id}", '', '', "title='{$lang->mr->link}' class='btn btn-info'");
common::printLink('mr', 'delete', "mr={$MR->id}", '', 'hiddenwin', "title='{$lang->mr->delete}' class='btn btn-info'");
?>
diff --git a/module/mr/view/link.html.php b/module/mr/view/link.html.php
new file mode 100644
index 0000000000..0bfb3dfe9e
--- /dev/null
+++ b/module/mr/view/link.html.php
@@ -0,0 +1,270 @@
+
+
+
+
+productplan->confirmUnlinkStory)?>
+productplan->confirmUnlinkBug)?>
+mr->confirmUnlinkTask)?>
+id);?>
+id);?>
+pageID);?>
+recPerPage);?>
+recTotal);?>
+
+
+
+
+
+
'>
+
+
+ id, \"story\")", ' ' . $lang->productplan->linkStory, '', "class='btn btn-primary'");?>
+
+
+
+
+
'>
+
+ id, \"bug\")", ' ' . $lang->productplan->linkBug, '', "class='btn btn-primary'");?>
+
+
+
+
+
'>
+
+ id, \"task\")", ' ' . $lang->mr->linkTask, '', "class='btn btn-primary'");?>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module/mr/view/linkbug.html.php b/module/mr/view/linkbug.html.php
new file mode 100644
index 0000000000..c2d1d60efd
--- /dev/null
+++ b/module/mr/view/linkbug.html.php
@@ -0,0 +1,78 @@
+
+ * @package mr
+ * @version $Id: linkbug.html.php$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
diff --git a/module/mr/view/linkstory.html.php b/module/mr/view/linkstory.html.php
new file mode 100644
index 0000000000..d26560de18
--- /dev/null
+++ b/module/mr/view/linkstory.html.php
@@ -0,0 +1,91 @@
+
+
+
+
diff --git a/module/mr/view/linktask.html.php b/module/mr/view/linktask.html.php
new file mode 100644
index 0000000000..824dc3479e
--- /dev/null
+++ b/module/mr/view/linktask.html.php
@@ -0,0 +1,78 @@
+
+ * @package mr
+ * @version $Id: linktask.html.php$
+ * @link http://www.zentao.net
+ */
+?>
+
+
+
diff --git a/module/repo/model.php b/module/repo/model.php
index 4f6637891b..184c9eb814 100644
--- a/module/repo/model.php
+++ b/module/repo/model.php
@@ -1821,4 +1821,18 @@ class repoModel extends model
$this->loadModel('git')->updateCommit($repo, $commentGroup, false);
}
}
+
+ /**
+ * Get products which scm is GitLab by projects.
+ *
+ * @param array $projectIDs
+ * @return array
+ */
+ public function getGitlabProductsByProjects($projectIDs)
+ {
+ return $this->dao->select('path,product')->from(TABLE_REPO)->where('deleted')->eq('0')
+ ->andWhere('SCM')->eq('Gitlab')
+ ->andWhere('path')->in($projectIDs)
+ ->fetchPairs('path', 'product');
+ }
}