@@ -40,19 +39,19 @@
| release->product;?> |
-
-
- id, "onchange='loadBranches(this.value)' class='form-control chosen'");?>
- type != 'normal') echo html::select('branch', $branches, $branch, "onchange='loadBuilds()' class='form-control chosen control-branch'");?>
-
- |
+ id, "onchange='loadBuilds()' class='form-control chosen'");?> |
|
| release->includedBuild;?> |
build->placeholder->multipleSelect}'");?> |
- |
+
+
+
+
+
+ |
| release->date;?> |
@@ -83,7 +82,6 @@
|
-
|
diff --git a/module/projectrelease/view/view.html.php b/module/projectrelease/view/view.html.php
index cb6267e017..6c1854caac 100644
--- a/module/projectrelease/view/view.html.php
+++ b/module/projectrelease/view/view.html.php
@@ -71,6 +71,7 @@
priAB);?> |
story->title);?> |
+
build->common;?> |
openedByAB);?> |
story->estimateAB);?> |
statusAB);?> |
@@ -98,6 +99,7 @@
echo html::a($storyLink,$story->title, '', "class='preview'");
?>
+
buildName?> |
openedBy);?> |
estimate . $config->hourUnit;?> |
@@ -167,6 +169,7 @@
| bug->title);?> |
bug->status);?> |
+
bug->resolvedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
bug->resolvedByAB);?> |
@@ -191,6 +194,8 @@
processStatus('bug', $bug);?>
|
+ resolvedBuild, '');?>
+
|
openedBy);?> |
openedDate) ? '' : substr($bug->openedDate, 5, 11);?> |
resolvedBy);?> |
@@ -257,6 +262,7 @@
bug->severity;?>>bug->severityAB);?> |
bug->title);?> |
bug->status);?> |
+
bug->openedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
actions;?> |
@@ -295,6 +301,11 @@
title, '', "class='preview'");?> |
processStatus('bug', $bug);?> |
+ openedBuild) as $buildID) $openedBuildName .= zget($builds, $buildID, '') . ' ';
+ ?>
+
|
openedBy);?> |
openedDate?> |
@@ -335,7 +346,7 @@
hasProduct):?>
- | release->product;?> |
+ release->product;?> |
productName;?> |
productType != 'normal'):?>
@@ -362,6 +373,14 @@
?>
+ type != 'normal'):?>
+
+ | release->branch;?> |
+
+ branches as $branchID) echo zget($branches, $branchID, '') . ' ';?>
+ |
+
+
| release->status;?> |
processStatus('release', $release);?> |
diff --git a/module/release/control.php b/module/release/control.php
index 3f8297186f..46712fb037 100644
--- a/module/release/control.php
+++ b/module/release/control.php
@@ -46,7 +46,10 @@ class release extends control
$this->app->loadClass('pager', $static = true);
$pager = new pager($recTotal, $recPerPage, $pageID);
$this->commonAction($productID, $branch);
- $this->session->set('releaseList', $this->app->getURI(true), 'product');
+
+ $uri = $this->app->getURI(true);
+ $this->session->set('releaseList', $uri, 'product');
+ $this->session->set('buildList', $uri);
$this->view->title = $this->view->product->name . $this->lang->colon . $this->lang->release->browse;
$this->view->position[] = $this->lang->release->browse;
@@ -86,27 +89,6 @@ class release extends control
$releasedBuilds = $this->release->getReleasedBuilds($productID, $branch);
foreach($releasedBuilds as $build) unset($builds[$build]);
- /* Get the builds of the linked stories or bugs. */
- $notEmptyBuilds = array();
- $buildList = $this->build->getByList(array_keys($builds));
- foreach($buildList as $build)
- {
- if(!$build->execution && !empty($build->builds))
- {
- $childBuilds = $this->build->getByList($build->builds);
- foreach($childBuilds as $childBuild)
- {
- $childBuild->stories = trim($childBuild->stories, ',');
- $childBuild->bugs = trim($childBuild->bugs, ',');
-
- if($childBuild->stories) $build->stories .= ',' . $childBuild->stories;
- if($childBuild->bugs) $build->bugs .= ',' . $childBuild->bugs;
- }
- }
-
- if(!empty($build->stories) or !empty($build->bugs)) $notEmptyBuilds[$build->id] = $build->id;
- }
-
$this->commonAction($productID, $branch);
$this->view->title = $this->view->product->name . $this->lang->colon . $this->lang->release->create;
$this->view->position[] = $this->lang->release->create;
@@ -114,7 +96,6 @@ class release extends control
$this->view->builds = $builds;
$this->view->users = $this->loadModel('user')->getPairs('noclosed');
$this->view->lastRelease = $this->release->getLast($productID, $branch);
- $this->view->notEmptyBuilds = $notEmptyBuilds;
$this->display();
}
@@ -188,8 +169,10 @@ class release extends control
$release = $this->release->getByID($releaseID, true);
if(!$release) return print(js::error($this->lang->notFound) . js::locate($this->createLink('product', 'index')));
- if($type == 'story') $this->session->set('storyList', $this->app->getURI(true), 'product');
- if($type == 'bug' or $type == 'leftBug') $this->session->set('bugList', $this->app->getURI(true), 'qa');
+ $uri = $this->app->getURI(true);
+ if(!empty($release->build)) $this->session->set('buildList', $uri);
+ if($type == 'story') $this->session->set('storyList', $uri, 'product');
+ if($type == 'bug' or $type == 'leftBug') $this->session->set('bugList', $uri, 'qa');
$this->loadModel('story');
$this->loadModel('bug');
@@ -200,16 +183,18 @@ class release extends control
$sort = common::appendOrder($orderBy);
if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort);
+ $sort .= ',buildID_asc';
$storyPager = new pager($type == 'story' ? $recTotal : 0, $recPerPage, $type == 'story' ? $pageID : 1);
- $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY)
- ->where('id')->in($release->stories)
- ->andWhere('deleted')->eq(0)
+ $stories = $this->dao->select("t1.*,t2.id as buildID, t2.name as buildName, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder")->from(TABLE_STORY)->alias('t1')
+ ->leftJoin(TABLE_BUILD)->alias('t2')->on("FIND_IN_SET(t1.id, t2.stories)")
+ ->where('t1.id')->in($release->stories)
+ ->andWhere('t1.deleted')->eq(0)
->beginIF($type == 'story')->orderBy($sort)->fi()
->page($storyPager)
->fetchAll('id');
- $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story');
+ $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false);
$stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->in(array_keys($stories))->andWhere('branch')->eq($release->branch)->fetchPairs('story', 'stage');
foreach($stages as $storyID => $stage) $stories[$storyID]->stage = $stage;
@@ -255,6 +240,7 @@ class release extends control
$this->view->storyPager = $storyPager;
$this->view->bugPager = $bugPager;
$this->view->leftBugPager = $leftBugPager;
+ $this->view->builds = $this->loadModel('build')->getBuildPairs($release->product, 'all', 'withbranch|hasproject', 0, 'execution', '', false);
if($this->app->getViewType() == 'json')
{
@@ -639,7 +625,7 @@ class release extends control
$this->config->bug->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairsForStory($release->product, $release->branch, 'skipParent|withMainPlan');
$this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($release->product, $release->branch);
- $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs($release->product, $branch = 'all', $params = '');
+ $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs($release->product, $branch = 'all', 'releasetag');
$this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values'];
$searchModules = array();
diff --git a/module/release/css/view.css b/module/release/css/view.css
index c750db6903..acc96a13fd 100644
--- a/module/release/css/view.css
+++ b/module/release/css/view.css
@@ -10,3 +10,4 @@
ol, ul {padding-left: 20px;}
.body-modal #mainContent {min-height: 240px;}
.c-id {width: 110px;}
+td.c-build{white-space: nowrap; overflow:hidden;}
diff --git a/module/release/js/create.js b/module/release/js/create.js
index 1b64abc7c2..e69de29bb2 100644
--- a/module/release/js/create.js
+++ b/module/release/js/create.js
@@ -1,25 +0,0 @@
-$('#submit').click(function()
-{
- var dateFormat = new RegExp(/^\d{4}\-\d{2}\-\d{2}$/);
- var name = $('#name').val();
- var date = $('#date').val();
- var build = $('#build').val();
- var notEmptyBuild = false;
-
- $.each(build, function(index, value)
- {
- if(typeof(notEmptyBuilds[value]) != 'undefined')
- {
- notEmptyBuild = true;
- return false;
- }
- })
-
- if(name && build && notEmptyBuild && dateFormat.test(date))
- {
- var result = confirm(confirmLink) ? true : false;
- $('#sync').val(result);
- }
-});
-
-$('[data-toggle="popover"]').popover();
diff --git a/module/release/lang/de.php b/module/release/lang/de.php
index dfd0244395..0772adf5d3 100644
--- a/module/release/lang/de.php
+++ b/module/release/lang/de.php
@@ -24,7 +24,7 @@ $lang->release->batchUnlinkStory = "Mehrere Storys entfernen";
$lang->release->batchUnlinkBug = "Mehrere Bugs entfernen";
$lang->release->confirmDelete = "Möchten Sie dieses Releas löschen?";
-$lang->release->confirmLink = "Whether to link the stories completed in the version and the bugs solved to the release?";
+$lang->release->syncFromBuilds = "Link the stories completed in the version and the bugs solved to the release";
$lang->release->confirmUnlinkStory = "Möchten Sie diese Story löschen?";
$lang->release->confirmUnlinkBug = "Möchten Sie diesen Bug löschen?";
$lang->release->existBuild = '『Build』『%s』 existiert bereits. Sie können den 『name』 ändern oder ein anderes 『build』 wählen.';
diff --git a/module/release/lang/en.php b/module/release/lang/en.php
index 7b6be960a0..ecdcfab2b6 100644
--- a/module/release/lang/en.php
+++ b/module/release/lang/en.php
@@ -24,7 +24,7 @@ $lang->release->batchUnlinkStory = "Batch Unlink Stories";
$lang->release->batchUnlinkBug = "Batch Unlink Bugs";
$lang->release->confirmDelete = "Do you want to delete this release?";
-$lang->release->confirmLink = "Whether to link the stories completed in the version and the bugs solved to the release?";
+$lang->release->syncFromBuilds = "Link the stories completed in the version and the bugs solved to the release";
$lang->release->confirmUnlinkStory = "Do you want to remove this story?";
$lang->release->confirmUnlinkBug = "Do you want to remove this bug?";
$lang->release->existBuild = '『Build』『%s』existed. You could change『name』or choose a『build』.';
diff --git a/module/release/lang/fr.php b/module/release/lang/fr.php
index 3a7bdc1b1e..e4b46198d1 100644
--- a/module/release/lang/fr.php
+++ b/module/release/lang/fr.php
@@ -24,7 +24,7 @@ $lang->release->batchUnlinkStory = "Retirer Stories par Lot";
$lang->release->batchUnlinkBug = "Retirer Bugs par Lot";
$lang->release->confirmDelete = "Voulez-vous réellement supprimer cette release ?";
-$lang->release->confirmLink = "Whether to link the stories completed in the version and the bugs solved to the release?";
+$lang->release->syncFromBuilds = "Link the stories completed in the version and the bugs solved to the release";
$lang->release->confirmUnlinkStory = "Voulez-vous retirer cette story de la release ?";
$lang->release->confirmUnlinkBug = "Voulez-vous retirer ce bug de la release ?";
$lang->release->existBuild = '『Build』『%s』existant. Vous pouvez changer『name』ou choisir un『build』.';
diff --git a/module/release/lang/zh-cn.php b/module/release/lang/zh-cn.php
index 3e48d6e1b8..9f3af90ca8 100644
--- a/module/release/lang/zh-cn.php
+++ b/module/release/lang/zh-cn.php
@@ -24,7 +24,7 @@ $lang->release->batchUnlinkStory = "批量移除{$lang->SRCommon}";
$lang->release->batchUnlinkBug = "批量移除Bug";
$lang->release->confirmDelete = "您确认删除该发布吗?";
-$lang->release->confirmLink = "是否将版本中完成的{$lang->SRCommon}和已解决的Bug关联到发布下?";
+$lang->release->syncFromBuilds = "将版本中完成的{$lang->SRCommon}和已解决的Bug关联到发布下";
$lang->release->confirmUnlinkStory = "您确认移除该{$lang->SRCommon}吗?";
$lang->release->confirmUnlinkBug = "您确认移除该Bug吗?";
$lang->release->existBuild = '『版本』已经有『%s』这条记录了。您可以更改『发布名称』或者选择一个『版本』。';
diff --git a/module/release/model.php b/module/release/model.php
index 02094bef70..a9b075ac89 100644
--- a/module/release/model.php
+++ b/module/release/model.php
@@ -31,7 +31,21 @@ class releaseModel extends model
->fetch();
if(!$release) return false;
- $release->builds = $this->dao->select('id, filePath, scmPath, name, execution, project')->from(TABLE_BUILD)->where('id')->in($release->build)->fetchAll();
+ $builds = $this->dao->select('id, branch, filePath, scmPath, name, execution, project')->from(TABLE_BUILD)->where('id')->in($release->build)->fetchAll();
+
+ /* Get release's branches by build. */
+ $branches = array();
+ if($release->productType != 'normal')
+ {
+ foreach($builds as $build)
+ {
+ $branchIdList = explode(',', $build->branch);
+ foreach($branchIdList as $branchID) $branches[$branchID] = $branchID;
+ }
+ }
+
+ $release->builds = $builds;
+ $release->branches = $branches;
$this->loadModel('file');
$release = $this->file->replaceImgURL($release, 'desc');
@@ -123,12 +137,13 @@ class releaseModel extends model
$buildIdList = array();
foreach($releases as $release)
{
- if($branch != 'all')
+ if($branch != 'all' and $branch !== '')
{
$inBranch = false;
foreach(explode(',', trim($release->branch, ',')) as $branchID)
{
- if(empty($branchID)) continue;
+ if($branchID === '') continue;
+
if(strpos(",{$branch},", ",{$branchID},") !== false) $inBranch = true;
}
if(!$inBranch) continue;
@@ -221,10 +236,14 @@ class releaseModel extends model
if($linkedBuilds) $builds += $this->dao->select('id,project,branch,builds,stories,bugs')->from(TABLE_BUILD)->where('id')->in($linkedBuilds)->fetchAll('id');
foreach($builds as $build)
{
- $branches[$build->branch] = $build->branch;
+ foreach(explode(',', $build->branch) as $buildBranch)
+ {
+ if(!isset($branches[$buildBranch])) $branches[$buildBranch] = $buildBranch;
+ }
+
$projects[$build->project] = $build->project;
- if($this->post->sync == 'true')
+ if($this->post->sync)
{
$build->stories = trim($build->stories, ',');
$build->bugs = trim($build->bugs, ',');
@@ -232,7 +251,7 @@ class releaseModel extends model
if($build->bugs) $release->bugs .= ',' . $build->bugs;
}
}
- if($this->post->sync == 'true' and $release->bugs)
+ if($this->post->sync and $release->bugs)
{
$releaseBugs = $this->loadModel('bug')->getReleaseBugs(array_keys($builds), $release->product, $release->branch);
$release->bugs = join(',', array_intersect(explode(',', $release->bugs), array_keys($releaseBugs)));
diff --git a/module/release/view/create.html.php b/module/release/view/create.html.php
index 1c41c1a01f..5bc476e931 100644
--- a/module/release/view/create.html.php
+++ b/module/release/view/create.html.php
@@ -12,8 +12,6 @@
?>
-release->confirmLink);?>
-
@@ -36,7 +34,12 @@
| release->includedBuild;?> |
build->placeholder->multipleSelect}'");?> |
- |
+
+
+
+
+
+ |
| release->date;?> |
@@ -67,7 +70,6 @@
|
-
|
diff --git a/module/release/view/view.html.php b/module/release/view/view.html.php
index 00698cf074..80e65b07f0 100644
--- a/module/release/view/view.html.php
+++ b/module/release/view/view.html.php
@@ -73,6 +73,7 @@
pri;?>>priAB);?> |
story->title);?> |
+ build->common;?> |
openedByAB);?> |
story->estimateAB);?> |
statusAB);?> |
@@ -100,6 +101,7 @@
echo html::a($storyLink,$story->title, '', "class='preview'");
?>
+ buildName?> |
openedBy);?> |
estimate . $config->hourUnit;?> |
@@ -169,6 +171,7 @@
| bug->title);?> |
bug->status);?> |
+ bug->resolvedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
bug->resolvedByAB);?> |
@@ -193,6 +196,8 @@
processStatus('bug', $bug);?>
|
+ resolvedBuild, '');?>
+ |
openedBy);?> |
openedDate, 5, 11)?> |
resolvedBy);?> |
@@ -259,6 +264,7 @@
bug->severity;?>>bug->severityAB);?> |
bug->title);?> |
bug->status);?> |
+ bug->openedBuild;?> |
openedByAB);?> |
bug->openedDateAB);?> |
actions;?> |
@@ -297,6 +303,11 @@
title, '', "class='preview'");?> |
processStatus('bug', $bug);?> |
+ openedBuild) as $buildID) $openedBuildName .= zget($builds, $buildID, '') . ' ';
+ ?>
+ |
openedBy);?> |
openedDate?> |
@@ -336,7 +347,7 @@
- | release->product;?> |
+ release->product;?> |
productName;?> |
productType != 'normal'):?>
@@ -367,6 +378,14 @@
?>
+ type != 'normal'):?>
+
+ | release->branch;?> |
+
+ branches as $branchID) echo zget($branches, $branchID, '') . ' ';?>
+ |
+
+
| release->status;?> |
processStatus('release', $release);?> |
diff --git a/module/report/model.php b/module/report/model.php
index 7d4f9d220c..d25d4b0978 100644
--- a/module/report/model.php
+++ b/module/report/model.php
@@ -698,6 +698,7 @@ class reportModel extends model
{
$effort = $this->dao->select('count(*) as count, sum(consumed) as consumed')->from(TABLE_EFFORT)
->where('LEFT(date, 4)')->eq($year)
+ ->andWhere('deleted')->eq(0)
->beginIF($accounts)->andWhere('account')->in($accounts)->fi()
->fetch();
diff --git a/module/report/view/annualdata.html.php b/module/report/view/annualdata.html.php
index 35234b8d7a..68c95a9617 100644
--- a/module/report/view/annualdata.html.php
+++ b/module/report/view/annualdata.html.php
@@ -292,31 +292,39 @@ $(function()
timeline: {
show: yearsData.length && yearsData.length > 1,
axisType: 'category',
- autoPlay: true,
- loop: false,
+ autoPlay: false,
+ loop: true,
playInterval: 1000,
left: "1%",
bottom: "1%",
width: '90%',
data: yearsData,
- symbolSize: [8, 4],
+ currentIndex: yearsData.length - 1,
+ symbolSize: [5, 5],
label: {
- normal: {
- color: '#fff',
- interval: 'auto',
- fontSize: 12,
- lineHeight: 20
- },
+ fontSize: 10,
+ lineHeight: 18,
interval: 'auto',
},
+ emphasis: {
+ label: {
+ fontSize: 10,
+ },
+ },
+ progress: {
+ label: {
+ fontSize: 10,
+ },
+ },
lineStyle: {
width: 5,
},
controlStyle: {
- itemSize: 18
+ itemSize: 18,
},
},
+ tooltip: {},
series: [{
name:'radar;?>',
areaStyle:{color: 'rgb(45, 40, 33)'},
diff --git a/module/story/control.php b/module/story/control.php
index 1f4128f858..6b869ad089 100755
--- a/module/story/control.php
+++ b/module/story/control.php
@@ -128,6 +128,7 @@ class story extends control
$response['result'] = 'success';
setcookie('lastStoryModule', (int)$this->post->module, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false);
+
$storyResult = $this->story->create($objectID, $bugID, $from = isset($fromObjectIDKey) ? $fromObjectIDKey : '', $extra);
if(!$storyResult or dao::isError())
{
@@ -164,7 +165,9 @@ class story extends control
$action = $fromObjectAction;
$extra = $fromObjectID;
}
- $actionID = $this->action->create('story', $storyID, $action, '', $extra);
+ /* Create actions. */
+ $storyIds = $storyResult['ids'];
+ foreach($storyIds as $idItem) $actionID = $this->action->create('story', $idItem, $action, '', $extra);
/* Record submit review action. */
$story = $this->dao->findById((int)$storyID)->from(TABLE_STORY)->fetch();
@@ -295,7 +298,11 @@ class story extends control
}
$users = $this->user->getPairs('pdfirst|noclosed|nodeleted');
+
+ $branchData = explode(',', $branch);
+ $branch = current($branchData);
$moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch === 'all' ? 0 : $branch);
+
if(empty($moduleOptionMenu)) return print(js::locate(helper::createLink('tree', 'browse', "productID=$productID&view=story")));
/* Init vars. */
@@ -437,7 +444,7 @@ class story extends control
$this->view->users = $users;
$this->view->moduleID = $moduleID;
$this->view->moduleOptionMenu = $moduleOptionMenu;
- $this->view->plans = str_replace('2030-01-01', $this->lang->story->undetermined, $this->loadModel('productplan')->getPairsForStory($productID, $branch, 'skipParent|unexpired|noclosed'));
+ $this->view->plans = str_replace('2030-01-01', $this->lang->story->undetermined, $this->loadModel('productplan')->getPairsForStory($productID, $branch == 0 ? '' : $branch, 'skipParent|unexpired|noclosed'));
$this->view->planID = $planID;
$this->view->source = $source;
$this->view->sourceNote = $sourceNote;
@@ -656,7 +663,10 @@ class story extends control
$branches = $product->type != 'normal' ? $this->loadModel('branch')->getPairs($productID, 'active') : array();
}
- $moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch === 'all' ? 0 : $branch);
+ $branchData = explode(',', $branch);
+ $branch = current($branchData);
+
+ $moduleOptionMenu = $this->tree->getOptionMenu($productID, $viewType = 'story', 0, $branch === 'all' ? 0 : $branch);
$moduleOptionMenu['ditto'] = $this->lang->story->ditto;
/* Get reviewers. */
@@ -681,7 +691,7 @@ class story extends control
}
$this->view->titles = $titles;
}
- $plans = $this->loadModel('productplan')->getPairsForStory($productID, ($branch === 'all' or !in_array($branch, array_keys($branches))) ? 0 : $branch, 'skipParent|unexpired|noclosed');
+ $plans = $this->loadModel('productplan')->getPairsForStory($productID, ($branch === 'all' or empty($branch)) ? '' : $branch, 'skipParent|unexpired|noclosed');
$plans['ditto'] = $this->lang->story->ditto;
$priList = (array)$this->lang->story->priList;
@@ -931,6 +941,7 @@ class story extends control
$this->view->title = $this->lang->story->edit . "STORY" . $this->lang->colon . $this->view->story->title;
$this->view->position[] = $this->lang->story->edit;
$this->view->story = $story;
+ $this->view->siblings = empty($story->siblings) ? array() : $this->story->getByList($story->siblings);
$this->view->stories = $stories;
$this->view->users = $users;
$this->view->product = $product;
@@ -939,6 +950,7 @@ class story extends control
$this->view->productStories = $productStories;
$this->view->branchOption = $branchOption;
$this->view->branchTagOption = $branchTagOption;
+ $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id);
$this->view->reviewers = array_keys($reviewerList);
$this->view->reviewedReviewer = $reviewedReviewer;
$this->view->lastReviewer = $this->story->getLastReviewer($story->id);
@@ -1023,6 +1035,16 @@ class story extends control
/* Get edited stories. */
$stories = $this->story->getByList($storyIdList);
+ /* Filter siblings. */
+ $siblings = '';
+ foreach($stories as $id => $story)
+ {
+ if(empty($story->siblings)) continue;
+ $siblings .= "#$id ";
+ unset($stories[$id]);
+ }
+ if(!empty($siblings)) echo js::alert(sprintf($this->lang->story->batchEditTip, $siblings));
+
$this->loadModel('branch');
if($productID and !$executionID)
{
@@ -1251,6 +1273,8 @@ class story extends control
/* Assign. */
$this->view->title = $this->lang->story->change . "STORY" . $this->lang->colon . $this->view->story->title;
+ $this->view->siblings = empty($story->siblings) ? array() : $this->story->getByList($story->siblings);
+ $this->view->branches = $this->loadModel('branch')->getPairs($story->product);
$this->view->users = $this->user->getPairs('pofirst|nodeleted|noclosed', $this->view->story->assignedTo);
$this->view->position[] = $this->lang->story->change;
$this->view->needReview = (($this->app->user->account == $this->view->product->PO or $this->config->story->needReview == 0 or !$this->story->checkForceReview()) and empty($reviewer)) ? "checked='checked'" : "";
@@ -1420,6 +1444,7 @@ class story extends control
$this->view->position = $position;
$this->view->product = $product;
$this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id);
+ $this->view->siblings = !empty($story->siblings) ? $this->story->getByList($story->siblings) : array();
$this->view->plan = $plan;
$this->view->bugs = $bugs;
$this->view->fromBug = $fromBug;
@@ -1640,7 +1665,7 @@ class story extends control
if(!$this->post->storyIdList) return print(js::locate($this->session->storyList, 'parent'));
$storyIdList = $this->post->storyIdList;
$storyIdList = array_unique($storyIdList);
- $actions = $this->story->batchReview($storyIdList, $result, $reason);
+ $this->story->batchReview($storyIdList, $result, $reason);
if(dao::isError()) return print(js::error(dao::getError()));
if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther');
@@ -1877,8 +1902,15 @@ class story extends control
$stories = $this->story->getByList($storyIdList);
$productStoryList = array();
$productList = array();
+ $ignoreSiblings = array();
foreach($stories as $story)
{
+ if(!empty($ignoreSiblings) and isset($ignoreSiblings[$story->id]))
+ {
+ unset($stories[$story->id]);
+ continue;
+ }
+
if($story->parent == -1)
{
$skipStory[] = $story->id;
@@ -1893,6 +1925,11 @@ class story extends control
$storyProduct = isset($productList[$story->product]) ? $productList[$story->product] : $this->product->getByID($story->product);
$branch = $storyProduct->type == 'branch' ? ($story->branch > 0 ? $story->branch : '0') : 'all';
if(!isset($productStoryList[$story->product][$story->branch])) $productStoryList[$story->product][$story->branch] = $this->story->getProductStoryPairs($story->product, $branch, 0, 'all', 'id_desc', 0, '', $story->type);
+
+ if(!empty($story->siblings))
+ {
+ foreach(explode(',', trim($story->siblings, ',')) as $siblingID) $ignoreSiblings[$siblingID] = $siblingID;
+ }
}
if($this->post->comments)
@@ -1910,6 +1947,8 @@ class story extends control
$actionID = $this->action->create('story', $storyID, 'Closed', htmlSpecialString($this->post->comments[$storyID]), ucfirst($this->post->closedReasons[$storyID]) . ($this->post->duplicateStoryIDList[$storyID] ? ':' . (int)$this->post->duplicateStoryIDList[$storyID] : '') . "|$preStatus");
$this->action->logHistory($actionID, $changes);
+
+ if(!empty($stories[$storyID]->siblings)) $this->story->syncSiblings($storyID, $stories[$storyID]->siblings, $changes, 'Closed');
}
}
@@ -2256,12 +2295,27 @@ class story extends control
{
if(!empty($_POST) && isset($_POST['storyIdList']))
{
- $allChanges = $this->story->batchAssignTo();
+ $allChanges = $this->story->batchAssignTo();
if(dao::isError()) return print(js::error(dao::getError()));
+
+ $assignedSiblings = array();
+ $oldStories = $this->story->getByList($this->post->storyIdList);
foreach($allChanges as $storyID => $changes)
{
$actionID = $this->action->create('story', $storyID, 'Assigned', '', $this->post->assignedTo);
$this->action->logHistory($actionID, $changes);
+
+ /* Sync siblings. */
+ if(!empty($oldStories[$storyID]->siblings))
+ {
+ $siblings = $oldStories[$storyID]->siblings;
+ foreach(explode(',', $siblings) as $siblingID)
+ {
+ if(in_array($siblingID, $this->post->storyIdList) or isset($assignedSiblings[$siblingID])) $siblings = str_replace(",$siblingID,", ',', $siblings);
+ }
+ $this->story->syncSiblings($storyID, trim($siblings, ','), $changes, 'Assigned');
+ foreach(explode(',', trim($siblings, ',')) as $assignedID) $assignedSiblings[$assignedID] = $assignedID;
+ }
}
}
if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther');
@@ -2336,7 +2390,7 @@ class story extends control
if($type == 'remove')
{
$result = $this->story->unlinkStory($storyID, $linkedStoryID);
- return print(js::reload('parent'));
+ helprt::end();
}
if($_POST)
@@ -2970,4 +3024,29 @@ class story extends control
return print(html::select('URS[]', $URS, $requirementList, "class='form-control chosen' multiple"));
}
+
+ /**
+ * AJAX: Deleted story sibling.
+ *
+ * @access public
+ * @return void
+ */
+ public function ajaxRelieveSibling()
+ {
+ $siblingID = !empty($_POST['siblingID']) ? $_POST['siblingID'] : 0;
+ $story = $this->story->getByID($siblingID);
+ $siblings = explode(',', trim($story->siblings, ','));
+
+ if(empty($story->siblings)) return $this->send(array('result' => 'fail'));
+
+ /* batchUnset siblingID from siblings.*/
+ $replaceSql = "UPDATE " . TABLE_STORY . " SET siblings = REPLACE(siblings,',$siblingID,', ',') WHERE `product` = $story->product";
+ $this->dbh->exec($replaceSql);
+
+ /* Update siblings to empty by siblingID and if siblings eq ','.*/
+ $this->dao->update(TABLE_STORY)->set('siblings')->eq('')->where('id')->eq($siblingID)->orWhere('siblings')->eq(',')->exec();
+
+ if(!dao::isError()) $this->loadModel('action')->create('story', $siblingID, 'relieved');
+ return $this->send(array('result' => 'success', 'silbingsCount' => count($siblings)-1));
+ }
}
diff --git a/module/story/css/change.css b/module/story/css/change.css
index 3fec429184..147c332d4f 100644
--- a/module/story/css/change.css
+++ b/module/story/css/change.css
@@ -2,4 +2,6 @@
#affectedProjects .table + h6 {border-top: 1px solid #ddd;}
.nav-tabs>li>a {height: 36px;}
.table-form>tbody>tr>th {width: 100px !important;}
-.colorpicker {right: 7px; top: 7px; opacity: 1;}
\ No newline at end of file
+.colorpicker {right: 7px; top: 7px; opacity: 1;}
+#siblingList td, #siblingTitle td {padding-left: 65px;}
+#siblingTitle td {font-weight: 700;}
diff --git a/module/story/css/common.css b/module/story/css/common.css
index 3baf85cc15..b772275ad7 100644
--- a/module/story/css/common.css
+++ b/module/story/css/common.css
@@ -1,3 +1,4 @@
+.icon-exclamation-sign {color: #0075ff;}
.linkbox {height: 180px; overflow-y: auto;}
.tab-pane h6 {margin: 0px; padding: 10px;}
.tab-pane h6, .tab-pane table {border: 1px solid #ddd; border-top: none;}
@@ -5,7 +6,6 @@
.input-group .control-product + .chosen-container-single .chosen-single {border-radius: 2px 0 0 2px; border-right-width: 1px; padding-right: 1px;}
.input-group .control-product + .chosen-container-active .chosen-single {border-right-width: 1px; padding-right: 0;}
-.input-group .control-branch + .chosen-container-single .chosen-single {border-radius: 0 2px 2px 0; border-left-width: 0px;}
#batchCreateForm .input-group, #batchEditForm .input-group,
#batchCreateForm .input-group .form-control, #batchEditForm .input-group .form-control {position: static;}
@@ -17,3 +17,15 @@
.c-source {width: 130px}
.c-note {width: 100px}
.c-category {width: 90px}
+
+.icon-info {color: #ff9800; border-color: #ff9800;}
+.text-active {text-decoration: underline;}
+.text-cancel {text-decoration: underline;color:#838a9d;margin-left:10px;}
+.popover {border:unset;}
+.popover.bottom .arrow{border-bottom-color:#ffffff;}
+.popover-icon {float:left;}
+.popover .content {margin-left:25px;overflow: overlay;}
+.hide {display:none !important;}
+.sibling > a.viewlink {padding:6px;white-space: nowrap;overflow: hidden;display: inline-flex;max-width: 70%;}
+.sibling > span {max-width: 60px;white-space: nowrap;overflow: hidden;}
+.tooltip-inner {max-width: 500px;}
diff --git a/module/story/css/create.css b/module/story/css/create.css
index c9854e8d17..ad21e95319 100644
--- a/module/story/css/create.css
+++ b/module/story/css/create.css
@@ -16,3 +16,7 @@
.source .input-group-addon, .sourceNote .input-group-addon {padding-top: 9px;}
#sourceNoteBox {width: 80px;}
#moduleIdBox .input-group-addon {min-width: 77px;}
+
+.btn.btn-action, .c-actions .btn {color: grey}
+#storyNoticeBranch {font-size: 13px; color: #5e626d;}
+#storyNoticeBranch i {font-size: 14px; color: #0075ff; margin-right: 3px;}
\ No newline at end of file
diff --git a/module/story/css/edit.css b/module/story/css/edit.css
index 7b2e3a76c3..b53c100326 100644
--- a/module/story/css/edit.css
+++ b/module/story/css/edit.css
@@ -5,3 +5,4 @@
.needNotReviewBox {width: 130px;}
.needNotReviewBox span.input-group-addon {border: 1px solid #dcdcdc; border-left-width: 0px; width:130px;}
#moduleIdBox .chosen-container, #planIdBox .chosen-container {min-width: unset;}
+#legendSiblings .form-group {margin-top: 10px;}
diff --git a/module/story/css/view.css b/module/story/css/view.css
index 782054d7ad..5e4b4ddabe 100644
--- a/module/story/css/view.css
+++ b/module/story/css/view.css
@@ -11,5 +11,4 @@
.linkStoryTitle {white-space: nowrap; text-overflow: ellipsis; overflow: hidden;}
.linkStoryTr > th {width: 90px;}
.linkStoryTr > td {padding-left: 0px !important;}
-.removeButton {margin-left: -10px;}
.resolution {white-space: nowrap; overflow: hidden;}
diff --git a/module/story/js/change.js b/module/story/js/change.js
index 3d1d19c8f4..dd48d70c1f 100644
--- a/module/story/js/change.js
+++ b/module/story/js/change.js
@@ -1,5 +1,6 @@
$(function()
{
+ $('[data-toggle="tooltip"]').tooltip();
$('#needNotReview').on('change', function()
{
$('#reviewer').val($(this).is(':checked') ? '' : lastReviewer).attr('disabled', $(this).is(':checked') ? 'disabled' : null).trigger('chosen:updated');
diff --git a/module/story/js/common.js b/module/story/js/common.js
index 5be7092e6e..ad4df2cb43 100644
--- a/module/story/js/common.js
+++ b/module/story/js/common.js
@@ -94,3 +94,49 @@ function loadURS()
$('#URS').chosen();
});
}
+
+$('.sibling').mouseover(function() {
+ $(this).parent('ul').find('a.unlink').addClass('hide');
+ $(this).find('.unlink').removeClass('hide');
+});
+$('.sibling').mouseenter(function() {
+ $('[data-toggle="popover"]').popover('hide');
+});
+$('.sibling').mouseout(function() {
+ $(this).find('.unlink').addClass('hide');
+});
+
+$('[data-toggle="popover"]').each(function(item) {
+ $index = $(this).attr('data-id');
+ $(this).popover({
+ placement: 'bottom',
+ html: true,
+ content: '
' + relievedTip + ' '
+ });
+})
+
+function relieve(index)
+{
+ $.post(relieveURL, {siblingID:index}, function(data){
+ $('[data-id="' + index + '"]').popover('hide');
+
+ if(data.result == 'success')
+ {
+ if(data.silbingsCount != 0) $('[data-id="' + index + '"]').parent('li').remove();
+ if(data.silbingsCount == 0 || index == storyID)
+ {
+ $('[href="#legendSiblings"]').parent('li').next('li').addClass('active');;
+ $('[href="#legendSiblings"]').parent('li').remove();
+ $('#legendSiblings').next('div').addClass('active');
+ $('#legendSiblings').remove();
+ $('#siblingTitle').remove();
+ $('#siblingList').remove();
+ }
+ }
+ }, 'json');
+}
+
+function popoverCancel(index)
+{
+ $('[data-id="' + index + '"]').popover('hide').addClass('hide');
+}
diff --git a/module/story/js/create.js b/module/story/js/create.js
index 868dfd6a20..e16afa22a3 100644
--- a/module/story/js/create.js
+++ b/module/story/js/create.js
@@ -60,6 +60,18 @@ $(function()
});
$(document).on('change', '#module', loadURS);
+
+ if($(".table-form select[id^='branches']").length == $('.switchBranch #branchBox option').length)
+ {
+ $('.table-col .icon-plus').parent().css('pointer-events', 'none')
+ $('.table-col .icon-plus').parent().addClass('disabled')
+ }
+
+ $.get(createLink('product', 'ajaxGetProductById', "productID=" + $('#product').val()), function(data)
+ {
+ $.cookie('branchSourceName', data.branchSourceName)
+ $.cookie('branchName', data.branchName)
+ }, 'json')
});
/**
@@ -108,3 +120,215 @@ function setLane(regionID)
$(window).unload(function(){
if(blockID) window.parent.refreshBlock($('#block' + blockID));
});
+
+/**
+ * Add branch box.
+ *
+ * @param obj $obj
+ * @access public
+ * @return void
+ */
+ function addBranchesBox(obj)
+ {
+ if($(".table-form select[id^='branches']").length == $('.switchBranch #branchBox option').length) return false;
+
+ var selectedVal = [];
+ $(".table-form select[id^='branches']").each(function()
+ {
+ var selectedProduct = $(this).val();
+ if($.inArray(selectedProduct, selectedVal) < 0) selectedVal.push(selectedProduct);
+ });
+
+ var branch = 0;
+ $('.table-form #branches0 option').each(function(){
+ if($.inArray($(this).val(), selectedVal) < 0)
+ {
+ branch = $(this).val();
+ return false;
+ }
+ });
+
+ var item = $('#addBranchesBox').html().replace(/%i%/g, itemIndex);
+ $(obj).closest('tr').after(' ' + item + ' ');
+ $('#branches_i__chosen').remove();
+ $('#branches' + itemIndex).chosen();
+ $('#modules_i__chosen').remove();
+ $('#modules' + itemIndex).chosen();
+ $('#plans_i__chosen').remove();
+ $('#plans' + itemIndex).chosen();
+ $('.addBranchesBox' + itemIndex + ' #planIdBox').css('flex', '0 0 ' + gap + 'px');
+
+ $.ajaxSettings.async = false;
+ loadBranchForSiblings($('#product').val(), branch, itemIndex)
+ loadModuleForSiblings($('#product').val(), branch, itemIndex)
+ loadPlanForSiblings($('#product').val(), branch, itemIndex)
+ $.ajaxSettings.async = true;
+ $('.addBranchesBox' + itemIndex + ' #branchBox .input-group .input-group-addon').html($.cookie('branchSourceName'))
+
+ disableSelectedBranches();
+
+ if($(".table-form select[id^='branches']").length == $('.switchBranch #branchBox option').length)
+ {
+ $('.table-col .icon-plus').parent().css('pointer-events', 'none')
+ $('.table-col .icon-plus').parent().addClass('disabled')
+ }
+
+ itemIndex ++;
+ }
+
+ /**
+ * Delete branch box.
+ *
+ * @param obj $obj
+ * @access public
+ * @return void
+ */
+ function deleteBranchesBox(obj)
+ {
+ $(obj).closest('tr').remove();
+
+ disableSelectedBranches();
+
+ $('.icon-plus').parent().css('pointer-events', 'auto')
+ $('.icon-plus').parent().removeClass('disabled')
+ }
+
+ /**
+ * Make the selected branch non clickable.
+ *
+ * @return void
+ */
+function disableSelectedBranches()
+{
+ $(".table-form select[id^='branches'] option[disabled='disabled']").removeAttr('disabled');
+
+ var selectedVal = [];
+ $(".table-form select[id^='branches']").each(function()
+ {
+ var selectedBranch = $(this).val();
+ if($.inArray(selectedBranch, selectedVal) < 0) selectedVal.push(selectedBranch);
+ })
+
+ $(".table-form select[id^='branches']").each(function()
+ {
+ var selectedBranch = $(this).val();
+ $(this).find('option').each(function()
+ {
+ var optionVal = $(this).attr('value');
+ if(optionVal != selectedBranch && $.inArray(optionVal, selectedVal) >= 0) $(this).attr('disabled', 'disabled');
+ })
+ })
+
+ $(".table-form select[id^=branches]").trigger('chosen:updated');
+}
+
+ /**
+ * Load branch for multi branch or multi platform.
+ *
+ * @param int $branch
+ * @param int $branchIndex
+ * @access public
+ * @return void
+ */
+function loadBranchRelation(branch, branchIndex)
+{
+ var productID = $('#product').val();
+ if(typeof(branch) == 'undefined') branch = 0;
+
+ $.ajaxSettings.async = false;
+ loadModuleForSiblings(productID, branch, branchIndex)
+ loadPlanForSiblings(productID, branch, branchIndex)
+ $.ajaxSettings.async = true;
+
+ disableSelectedBranches()
+}
+
+/**
+ * Load branch for siblings.
+ *
+ * @paran int $procutID
+ * @param int $branch
+ * @param int $branchIndex
+ * @access public
+ * @return void
+ */
+function loadBranchForSiblings(productID, branch, branchIndex)
+{
+ var isSiblings = storyType == 'story' ? 'yes' : 'no';
+ $.get(createLink('branch', 'ajaxGetBranches', "productID=" + productID + "&oldBranch=" + branch + "¶m=active&projectID=" + executionID + "&withMainBranch=1&isSiblings=" + isSiblings + "&fieldID=" + branchIndex), function(data)
+ {
+ if(data)
+ {
+ /* reload branch */
+ $('#branches' + branchIndex).replaceWith(data);
+ $('#branches' + branchIndex + "_chosen").remove();
+ $('#branches' + branchIndex).next('.picker').remove();
+ $('#branches' + branchIndex).chosen();
+ }
+ })
+}
+
+/**
+ * Load module for siblings.
+ *
+ * @paran int $procutID
+ * @param int $branch
+ * @param int $branchIndex
+ * @access public
+ * @return void
+ */
+function loadModuleForSiblings(productID, branch, branchIndex)
+{
+ /* Load module */
+ var currentModule = 0;
+ var moduleLink = createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=story&branch=' + branch + '&rootModuleID=0&returnType=html&fieldID=' + branchIndex + '&needManage=false&extra=¤tModuleID=' + currentModule);
+ if(branchIndex > 0)
+ {
+ var $moduleIDBox = $('.addBranchesBox'+ branchIndex +' #moduleIdBox');
+ }
+ else
+ {
+ var $moduleIDBox = $('.switchBranch #moduleIdBox');
+ }
+ $moduleIDBox.load(moduleLink, function()
+ {
+ $moduleIDBox.find('#modules' + branchIndex).chosen();
+ if(branchIndex == 0)
+ {
+ $('.switchBranch #moduleIdBox > span:first-child').remove()
+ }
+ $moduleIDBox.prepend("" + storyModule + "" );
+
+ $moduleIDBox.fixInputGroup();
+ });
+}
+
+/**
+ * Load plan for siblings.
+ *
+ * @paran int $procutID
+ * @param int $branch
+ * @param int $branchIndex
+ * @access public
+ * @return void
+ */
+function loadPlanForSiblings(productID, branch, branchIndex)
+{
+ /* Load plan */
+ if(branch == '0') branch = '';
+ planLink = createLink('product', 'ajaxGetPlans', 'productID=' + productID + '&branch=' + branch + '&planID=0&fieldID=' + branchIndex + '&needCreate=false&expired=unexpired¶m=skipParent|forStory,' + config.currentMethod);
+ if(branchIndex > 0)
+ {
+ var $planIdBox = $('.addBranchesBox'+ branchIndex +' #planIdBox');
+ }
+ else
+ {
+ var $planIdBox = $('#planIdBox');
+ }
+ $planIdBox.load(planLink, function()
+ {
+ $planIdBox.find('#plans' + branchIndex).chosen();
+ $planIdBox.prepend("" + storyPlan + "");
+ $planIdBox.fixInputGroup();
+ });
+}
diff --git a/module/story/js/edit.js b/module/story/js/edit.js
index 8358c2009e..21116b0f48 100644
--- a/module/story/js/edit.js
+++ b/module/story/js/edit.js
@@ -1,5 +1,6 @@
$(function()
{
+ $('[data-toggle="tooltip"]').tooltip();
$('.main-side #branch').closest('td').find('#product_chosen .chosen-single').css('width', '153px');
if(storyStatus == 'reviewing')
diff --git a/module/story/js/view.js b/module/story/js/view.js
index 9a215855b9..94a0ce9abb 100644
--- a/module/story/js/view.js
+++ b/module/story/js/view.js
@@ -9,17 +9,19 @@ $(function()
/* Add for task #5385. */
if(config.onlybody == 'yes') $('.main-actions').css('width', '100%')
- $('#unlinkStory').click(function()
+ $('.legendStories').mouseover(function()
{
- if($('.removeButton').hasClass('hide'))
- {
- $('.removeButton').removeClass('hide');
- $(this).text(cancel);
- }
- else
- {
- $('.removeButton').addClass('hide');
- $(this).text(unlink);
- }
+ $(this).parent('ul').find('a.removeButton').addClass('hide');
+ $(this).find('.removeButton').removeClass('hide');
+ });
+
+ $('.legendStories').mouseout(function()
+ {
+ $(this).find('.removeButton').addClass('hide');
+ });
+
+ $('#legendStories').find('.removeButton').click(function()
+ {
+ $(this).closest('li').remove();
})
});
diff --git a/module/story/lang/de.php b/module/story/lang/de.php
index df828c127b..7111526b98 100644
--- a/module/story/lang/de.php
+++ b/module/story/lang/de.php
@@ -12,7 +12,7 @@
global $config;
$lang->story->create = "Story hinzufügen";
-$lang->story->requirement = zget($lang, 'URCommon', "Requirement");
+$lang->story->requirement = 'UR';
$lang->story->story = zget($lang, 'SRCommon', "Story");
$lang->story->createStory = 'Create ' . $lang->story->story;
$lang->story->createRequirement = 'Create ' . $lang->story->requirement;
@@ -76,6 +76,10 @@ $lang->story->undetermined = 'undetermined';
$lang->story->order = 'Order';
$lang->story->saveDraft = 'Save as draft';
$lang->story->doNotSubmit = 'Do Not Submit';
+$lang->story->currentBranch = 'Current Branch';
+$lang->story->siblings = 'Siblings story';
+$lang->story->relieved = 'Relieved';
+$lang->story->relievedSiblings = 'Relieved Siblings';
$lang->story->editAction = "Edit {$lang->SRCommon}";
$lang->story->changeAction = "Change Story";
@@ -96,6 +100,15 @@ $lang->story->storyRound = '%s time estimation';
$lang->story->float = "『%s』should be positive number, decimals included.";
$lang->story->saveDraftSuccess = 'Save as draft succeeded.';
+$lang->story->changeSyncTip = "The modification of this story will be synchronized to the following twin requirements";
+$lang->story->syncTip = "The twin story are synchronized except for product, branch, module, plan, and stage. After the twin relationship is dissolved, they are no longer synchronized.";
+$lang->story->relievedTip = "The twin relationship cannot be restored after dissolving, the content of the demand is no longer synchronized, whether to dissolving?";
+$lang->story->assignSyncTip = "Both twin stories modify the assignor synchronously";
+$lang->story->closeSyncTip = "Twin stories are closed synchronously";
+$lang->story->activateSyncTip = "Twin stories are activated synchronously";
+$lang->story->relievedSiblingsTip = "After product adjustment, the twin relationship of this story will be automatically removed, and the story will no longer be synchronized. Do you want to save?";
+$lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and this operation has been filtered.";
+
$lang->story->id = 'ID';
$lang->story->parent = 'Parent';
$lang->story->product = $lang->productCommon;
@@ -308,6 +321,7 @@ $lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist';
$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?";
$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?";
$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!";
+$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered.";
$lang->story->form = new stdclass();
$lang->story->form->area = 'Story Bereich';
@@ -341,6 +355,7 @@ $lang->story->action->subdividestory = array('main' => '$date, aufgeteilt
$lang->story->action->unlinkrelatedstory = array('main' => '$date, Verknüpfung aufgelöst durch $actor von Story $extra.');
$lang->story->action->unlinkchildstory = array('main' => '$date, Verknüpfung aufgelöst durch $actor Story $extra.');
$lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.');
+$lang->story->action->syncsiblings = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList');
/* Statistical statement. */
$lang->story->report = new stdclass();
@@ -429,6 +444,7 @@ $lang->story->chosen->reviewedBy = 'Prüfer wählen';
$lang->story->notice = new stdClass();
$lang->story->notice->closed = 'Die ausgewählten Storys wurden bereits geschlossen!';
$lang->story->notice->reviewerNotEmpty = 'This requirement needs to be reviewed, and the reviewedby is required.';
+$lang->story->notice->changePlan = 'The plan can be changed to only one item.';
$lang->story->convertToTask = new stdClass();
$lang->story->convertToTask->fieldList = array();
@@ -466,6 +482,17 @@ $lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed;
$lang->story->featureBar['browse']['draft'] = $lang->story->statusList['draft'];
$lang->story->featureBar['browse']['reviewing'] = $lang->story->statusList['reviewing'];
+$lang->story->operateList = array();
+$lang->story->operateList['assigned'] = 'assigned';
+$lang->story->operateList['closed'] = 'closed';
+$lang->story->operateList['activated'] = 'activated';
+$lang->story->operateList['changed'] = 'changed';
+$lang->story->operateList['reviewed'] = 'reviewed';
+$lang->story->operateList['edited'] = 'edited';
+$lang->story->operateList['submitreview'] = 'submit review';
+$lang->story->operateList['recalledchange'] = 'recalled change';
+$lang->story->operateList['recalled'] = 'recalled review';
+
$lang->requirement->common = $lang->URCommon;
$lang->requirement->create = 'Create Requirement';
$lang->requirement->batchCreate = "Batch Create";
@@ -490,3 +517,7 @@ $lang->requirement->batchAssignTo = "Batch Assign";
$lang->requirement->batchChangeModule = "Batch Change Modules";
$lang->requirement->submitReview = $lang->story->submitReview;
$lang->requirement->linkStory = 'Link Story';
+
+$lang->story->addBranch = 'Add %s';
+$lang->story->deleteBranch = 'Delete %s';
+$lang->story->notice->branch = 'Each branch will establish a requirement, and the requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later';
diff --git a/module/story/lang/en.php b/module/story/lang/en.php
index 6ad74b82df..664d1a114b 100644
--- a/module/story/lang/en.php
+++ b/module/story/lang/en.php
@@ -12,7 +12,7 @@
global $config;
$lang->story->create = "Create Story";
-$lang->story->requirement = zget($lang, 'URCommon', "Requirement");
+$lang->story->requirement = 'UR';
$lang->story->story = zget($lang, 'SRCommon', "Story");
$lang->story->createStory = 'Create ' . $lang->story->story;
$lang->story->createRequirement = 'Create ' . $lang->story->requirement;
@@ -76,6 +76,10 @@ $lang->story->undetermined = 'undetermined';
$lang->story->order = 'Order';
$lang->story->saveDraft = 'Save as draft';
$lang->story->doNotSubmit = 'Do Not Submit';
+$lang->story->currentBranch = 'Current Branch';
+$lang->story->siblings = 'Siblings story';
+$lang->story->relieved = 'Relieved';
+$lang->story->relievedSiblings = 'Relieved Siblings';
$lang->story->editAction = "Edit {$lang->SRCommon}";
$lang->story->changeAction = "Change {$lang->SRCommon}";
@@ -96,6 +100,15 @@ $lang->story->storyRound = '%s time estimation';
$lang->story->float = "『%s』should have positive number, or decimals.";
$lang->story->saveDraftSuccess = 'Save as draft succeeded.';
+$lang->story->changeSyncTip = "The modification of this story will be synchronized to the following twin requirements";
+$lang->story->syncTip = "The twin story are synchronized except for product, branch, module, plan, and stage. After the twin relationship is dissolved, they are no longer synchronized.";
+$lang->story->relievedTip = "The twin relationship cannot be restored after dissolving, the content of the demand is no longer synchronized, whether to dissolving?";
+$lang->story->assignSyncTip = "Both twin stories modify the assignor synchronously";
+$lang->story->closeSyncTip = "Twin stories are closed synchronously";
+$lang->story->activateSyncTip = "Twin stories are activated synchronously";
+$lang->story->relievedSiblingsTip = "After product adjustment, the twin relationship of this story will be automatically removed, and the story will no longer be synchronized. Do you want to save?";
+$lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and this operation has been filtered.";
+
$lang->story->id = 'ID';
$lang->story->parent = 'Parent';
$lang->story->product = $lang->productCommon;
@@ -308,6 +321,7 @@ $lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist';
$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?";
$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?";
$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!";
+$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered.";
$lang->story->form = new stdclass();
$lang->story->form->area = 'Scope';
@@ -341,6 +355,7 @@ $lang->story->action->subdividestory = array('main' => '$date, decomposed
$lang->story->action->unlinkrelatedstory = array('main' => '$date, unlinked by $actor from Story $extra.');
$lang->story->action->unlinkchildstory = array('main' => '$date, unlinked by $actor Decomposed Story $extra.');
$lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.');
+$lang->story->action->syncsiblings = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList');
/* Statistical statement. */
$lang->story->report = new stdclass();
@@ -429,6 +444,7 @@ $lang->story->chosen->reviewedBy = 'Choose ReviewedBy';
$lang->story->notice = new stdClass();
$lang->story->notice->closed = 'Story that you select is closed!';
$lang->story->notice->reviewerNotEmpty = 'This requirement needs to be reviewed, and the reviewedby is required.';
+$lang->story->notice->changePlan = 'The plan can be changed to only one item.';
$lang->story->convertToTask = new stdClass();
$lang->story->convertToTask->fieldList = array();
@@ -466,6 +482,17 @@ $lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed;
$lang->story->featureBar['browse']['draft'] = $lang->story->statusList['draft'];
$lang->story->featureBar['browse']['reviewing'] = $lang->story->statusList['reviewing'];
+$lang->story->operateList = array();
+$lang->story->operateList['assigned'] = 'assigned';
+$lang->story->operateList['closed'] = 'closed';
+$lang->story->operateList['activated'] = 'activated';
+$lang->story->operateList['changed'] = 'changed';
+$lang->story->operateList['reviewed'] = 'reviewed';
+$lang->story->operateList['edited'] = 'edited';
+$lang->story->operateList['submitreview'] = 'submit review';
+$lang->story->operateList['recalledchange'] = 'recalled change';
+$lang->story->operateList['recalled'] = 'recalled review';
+
$lang->requirement->common = $lang->URCommon;
$lang->requirement->create = 'Create Requirement';
$lang->requirement->batchCreate = "Batch Create";
@@ -490,3 +517,7 @@ $lang->requirement->batchAssignTo = "Batch Assign";
$lang->requirement->batchChangeModule = "Batch Change Modules";
$lang->requirement->submitReview = $lang->story->submitReview;
$lang->requirement->linkStory = 'Link Story';
+
+$lang->story->addBranch = 'Add %s';
+$lang->story->deleteBranch = 'Delete %s';
+$lang->story->notice->branch = 'Each branch will establish a requirement, and the requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later';
diff --git a/module/story/lang/fr.php b/module/story/lang/fr.php
index 380240861a..e11cd9a62c 100644
--- a/module/story/lang/fr.php
+++ b/module/story/lang/fr.php
@@ -12,7 +12,7 @@
global $config;
$lang->story->create = "Créer Story";
-$lang->story->requirement = zget($lang, 'URCommon', "Requirement");
+$lang->story->requirement = 'UR';
$lang->story->story = zget($lang, 'SRCommon', "Story");
$lang->story->createStory = 'Create ' . $lang->story->story;
$lang->story->createRequirement = 'Create ' . $lang->story->requirement;
@@ -76,6 +76,10 @@ $lang->story->undetermined = 'undetermined';
$lang->story->order = 'Order';
$lang->story->saveDraft = 'Save as draft';
$lang->story->doNotSubmit = 'Do Not Submit';
+$lang->story->currentBranch = 'Current Branch';
+$lang->story->siblings = 'Siblings story';
+$lang->story->relieved = 'Relieved';
+$lang->story->relievedSiblings = 'Relieved Siblings';
$lang->story->editAction = "Edit {$lang->SRCommon}";
$lang->story->changeAction = "Changer Story";
@@ -96,6 +100,15 @@ $lang->story->storyRound = '%s time estimation';
$lang->story->float = "『 %s 』doit avoir des nombres positifs ou décimaux.";
$lang->story->saveDraftSuccess = 'Save as draft succeeded.';
+$lang->story->changeSyncTip = "The modification of this story will be synchronized to the following twin requirements";
+$lang->story->syncTip = "The twin story are synchronized except for product, branch, module, plan, and stage. After the twin relationship is dissolved, they are no longer synchronized.";
+$lang->story->relievedTip = "The twin relationship cannot be restored after dissolving, the content of the demand is no longer synchronized, whether to dissolving?";
+$lang->story->assignSyncTip = "Both twin stories modify the assignor synchronously";
+$lang->story->closeSyncTip = "Twin stories are closed synchronously";
+$lang->story->activateSyncTip = "Twin stories are activated synchronously";
+$lang->story->relievedSiblingsTip = "After product adjustment, the twin relationship of this story will be automatically removed, and the story will no longer be synchronized. Do you want to save?";
+$lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and this operation has been filtered.";
+
$lang->story->id = 'ID';
$lang->story->parent = 'Parent';
$lang->story->product = $lang->productCommon;
@@ -308,6 +321,7 @@ $lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist';
$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?";
$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?";
$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!";
+$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered.";
$lang->story->form = new stdclass();
$lang->story->form->area = 'Périmètre';
@@ -341,6 +355,7 @@ $lang->story->action->subdividestory = array('main' => '$date, décompos
$lang->story->action->unlinkrelatedstory = array('main' => '$date, dissociée par $actor de la Story $extra.');
$lang->story->action->unlinkchildstory = array('main' => '$date, dissociée par $actor de la sous-Story $extra.');
$lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.');
+$lang->story->action->syncsiblings = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList');
/* Statistical statement. */
$lang->story->report = new stdclass();
@@ -429,6 +444,7 @@ $lang->story->chosen->reviewedBy = 'Choisir valideur';
$lang->story->notice = new stdClass();
$lang->story->notice->closed = 'La Story que vous avez sélectionnée est malheureusement fermée !';
$lang->story->notice->reviewerNotEmpty = 'This requirement needs to be reviewed, and the reviewedby is required.';
+$lang->story->notice->changePlan = 'The plan can be changed to only one item.';
$lang->story->convertToTask = new stdClass();
$lang->story->convertToTask->fieldList = array();
@@ -466,6 +482,17 @@ $lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed;
$lang->story->featureBar['browse']['draft'] = $lang->story->statusList['draft'];
$lang->story->featureBar['browse']['reviewing'] = $lang->story->statusList['reviewing'];
+$lang->story->operateList = array();
+$lang->story->operateList['assigned'] = 'assigned';
+$lang->story->operateList['closed'] = 'closed';
+$lang->story->operateList['activated'] = 'activated';
+$lang->story->operateList['changed'] = 'changed';
+$lang->story->operateList['reviewed'] = 'reviewed';
+$lang->story->operateList['edited'] = 'edited';
+$lang->story->operateList['submitreview'] = 'submit review';
+$lang->story->operateList['recalledchange'] = 'recalled change';
+$lang->story->operateList['recalled'] = 'recalled review';
+
$lang->requirement->common = $lang->URCommon;
$lang->requirement->create = 'Create Requirement';
$lang->requirement->batchCreate = "Batch Create";
@@ -490,3 +517,7 @@ $lang->requirement->batchAssignTo = "Batch Assign";
$lang->requirement->batchChangeModule = "Batch Change Modules";
$lang->requirement->submitReview = $lang->story->submitReview;
$lang->requirement->linkStory = 'Lier Story';
+
+$lang->story->addBranch = 'Add %s';
+$lang->story->deleteBranch = 'Delete %s';
+$lang->story->notice->branch = 'Each branch will establish a requirement, and the requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later';
diff --git a/module/story/lang/vi.php b/module/story/lang/vi.php
index 243011066f..475490d3c1 100644
--- a/module/story/lang/vi.php
+++ b/module/story/lang/vi.php
@@ -94,6 +94,14 @@ $lang->story->storyRound = '%s time estimation';
$lang->story->float = "『%s』should be positive number, decimals included.";
$lang->story->saveDraftSuccess = 'Save as draft succeeded.';
+$lang->story->changeSyncTip = "The modification of this story will be synchronized to the following twin requirements";
+$lang->story->syncTip = "The twin story are synchronized except for product, branch, module, plan, and stage. After the twin relationship is dissolved, they are no longer synchronized.";
+$lang->story->assignSyncTip = "Both twin stories modify the assignor synchronously";
+$lang->story->closeSyncTip = "Twin stories are closed synchronously";
+$lang->story->activateSyncTip = "Twin stories are activated synchronously";
+$lang->story->relievedSiblingsTip = "After product adjustment, the twin relationship of this story will be automatically removed, and the story will no longer be synchronized. Do you want to save?";
+$lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and this operation has been filtered.";
+
$lang->story->id = 'ID';
$lang->story->parent = 'Mẹ';
$lang->story->product = $lang->productCommon;
@@ -332,6 +340,7 @@ $lang->story->action->subdividestory = array('main' => '$date, được p
$lang->story->action->unlinkrelatedstory = array('main' => '$date, bị hủy bởi $actor từ Câu chuyện $extra.');
$lang->story->action->unlinkchildstory = array('main' => '$date, bị hủy bởi $actor Câu chuyện được phân rã $extra.');
$lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.');
+$lang->story->action->syncsiblings = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList');
/* Statistical statement. */
$lang->story->report = new stdclass();
@@ -456,3 +465,14 @@ $lang->story->featureBar['browse']['all'] = $lang->all;
$lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed;
$lang->story->featureBar['browse']['draft'] = $lang->story->statusList['draft'];
$lang->story->featureBar['browse']['reviewing'] = $lang->story->statusList['reviewing'];
+
+$lang->story->operateList = array();
+$lang->story->operateList['assigned'] = 'assigned';
+$lang->story->operateList['closed'] = 'closed';
+$lang->story->operateList['activated'] = 'activated';
+$lang->story->operateList['changed'] = 'changed';
+$lang->story->operateList['reviewed'] = 'reviewed';
+$lang->story->operateList['edited'] = 'edited';
+$lang->story->operateList['submitreview'] = 'submit review';
+$lang->story->operateList['recalledchange'] = 'recalled change';
+$lang->story->operateList['recalled'] = 'recalled review';
diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php
index b51eec05b2..048756a3f8 100644
--- a/module/story/lang/zh-cn.php
+++ b/module/story/lang/zh-cn.php
@@ -76,6 +76,10 @@ $lang->story->undetermined = '待定';
$lang->story->order = '排序';
$lang->story->saveDraft = '存为草稿';
$lang->story->doNotSubmit = '保存暂不提交';
+$lang->story->currentBranch = '当前%s';
+$lang->story->siblings = '孪生需求';
+$lang->story->relieved = '解除';
+$lang->story->relievedSiblings = '解除孪生需求';
$lang->story->editAction = "编辑{$lang->SRCommon}";
$lang->story->changeAction = "变更{$lang->SRCommon}";
@@ -96,6 +100,15 @@ $lang->story->storyRound = '第 %s 轮估算';
$lang->story->float = "『%s』应当是正数,可以是小数。";
$lang->story->saveDraftSuccess = '存为草稿成功';
+$lang->story->changeSyncTip = "该需求的修改会同步到如下的孪生需求";
+$lang->story->syncTip = "孪生需求间除产品、分支 、模块、计划、阶段外均同步,孪生关系解除后不再同步";
+$lang->story->relievedTip = "孪生关系解除后无法恢复,需求的内容不再同步,是否解除?";
+$lang->story->assignSyncTip = "孪生需求均同步修改指派人";
+$lang->story->closeSyncTip = "孪生需求均同步关闭";
+$lang->story->activateSyncTip = "孪生需求均同步激活";
+$lang->story->relievedSiblingsTip = '产品调整后,本需求自动解除孪生关系,需求不再同步,是否保存?';
+$lang->story->batchEditTip = "{$lang->SRCommon} %s为孪生需求,本次操作已被过滤。";
+
$lang->story->id = '编号';
$lang->story->parent = '父需求';
$lang->story->product = "所属{$lang->productCommon}";
@@ -308,6 +321,7 @@ $lang->story->errorDuplicateStory = $lang->SRCommon . '%s不存在';
$lang->story->confirmRecallChange = "撤销变更后,需求内容会回退至变更前的版本,您确定要撤销吗?";
$lang->story->confirmRecallReview = "您确定要撤回评审吗?";
$lang->story->noStoryToTask = "只有激活的{$lang->SRCommon}才能转为任务!";
+$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s 状态为已关闭,本次操作已被过滤。";
$lang->story->form = new stdclass();
$lang->story->form->area = "该{$lang->SRCommon}所属范围";
@@ -341,6 +355,7 @@ $lang->story->action->subdividestory = array('main' => "\$date, 由 story->action->unlinkrelatedstory = array('main' => "\$date, 由 \$actor 移除相关{$lang->SRCommon} \$extra。");
$lang->story->action->unlinkchildstory = array('main' => "\$date, 由 \$actor 移除细分{$lang->SRCommon} \$extra。");
$lang->story->action->recalledchange = array('main' => "\$date, 由 \$actor 撤销变更。");
+$lang->story->action->syncsiblings = array('main' => "\$date, 系统判断由于孪生需求 \$extra \$operate,本需求同步调整。", 'operate' => 'operateList');
/* 统计报表。*/
$lang->story->report = new stdclass();
@@ -429,6 +444,7 @@ $lang->story->chosen->reviewedBy = '选择评审人...';
$lang->story->notice = new stdClass();
$lang->story->notice->closed = "您选择的{$lang->SRCommon}已经被关闭了!";
$lang->story->notice->reviewerNotEmpty = '该需求需要评审,评审人员不能为空。';
+$lang->story->notice->changePlan = '所属计划只能改为一条,修改后才能保存成功。';
$lang->story->convertToTask = new stdClass();
$lang->story->convertToTask->fieldList = array();
@@ -466,6 +482,17 @@ $lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed;
$lang->story->featureBar['browse']['draft'] = $lang->story->statusList['draft'];
$lang->story->featureBar['browse']['reviewing'] = $lang->story->statusList['reviewing'];
+$lang->story->operateList = array();
+$lang->story->operateList['assigned'] = '指派';
+$lang->story->operateList['closed'] = '关闭';
+$lang->story->operateList['activated'] = '激活';
+$lang->story->operateList['changed'] = '变更';
+$lang->story->operateList['reviewed'] = '评审';
+$lang->story->operateList['edited'] = '编辑';
+$lang->story->operateList['submitreview'] = '提交评审';
+$lang->story->operateList['recalledchange'] = '撤销变更';
+$lang->story->operateList['recalled'] = '撤销评审';
+
$lang->requirement->common = $lang->URCommon;
$lang->requirement->create = "提{$lang->URCommon}";
$lang->requirement->batchCreate = "批量创建";
@@ -490,3 +517,7 @@ $lang->requirement->batchAssignTo = "批量指派";
$lang->requirement->batchChangeModule = "批量修改模块";
$lang->requirement->submitReview = $lang->story->submitReview;
$lang->requirement->linkStory = "关联{$lang->SRCommon}";
+
+$lang->story->addBranch = '添加%s';
+$lang->story->deleteBranch = '删除%s';
+$lang->story->notice->branch = '每个分支会建立一个需求,需求间互为孪生关系,孪生需求间除产品、分支、模块、计划、阶段字段外均同步,后期您可以手动解除孪生关系';
diff --git a/module/story/model.php b/module/story/model.php
index 25a7284f34..69e29a5af4 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -49,11 +49,18 @@ class storyModel extends model
$story->executions = $this->dao->select('t1.project, t2.name, t2.status, t2.type, t2.multiple')->from(TABLE_PROJECTSTORY)->alias('t1')
->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project = t2.id')
- ->where('t1.story')->eq($storyID)
- ->andWhere('t2.type')->in('sprint,stage,kanban')
+ ->where('t2.type')->in('sprint,stage,kanban')
+ ->beginIF($story->siblings)->andWhere('t1.story')->in(ltrim($story->siblings, ',') . $story->id)
+ ->beginIF(!$story->siblings)->andWhere('t1.story')->in($story->id)
->orderBy('t1.`order` DESC')
->fetchAll('project');
- $story->tasks = $this->dao->select('id, name, assignedTo, execution, project, status, consumed, `left`,type')->from(TABLE_TASK)->where('story')->eq($storyID)->andWhere('deleted')->eq(0)->orderBy('id DESC')->fetchGroup('execution');
+ $story->tasks = $this->dao->select('id, name, assignedTo, execution, project, status, consumed, `left`,type')->from(TABLE_TASK)
+ ->where('deleted')->eq(0)
+ ->beginIF($story->siblings)->andWhere('story')->in(ltrim($story->siblings, ',') . $story->id)
+ ->beginIF(!$story->siblings)->andWhere('story')->in($story->id)
+ ->orderBy('id DESC')
+ ->fetchGroup('execution');
+
$story->stages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->fetchPairs('branch', 'stage');
//$story->bugCount = $this->dao->select('COUNT(*)')->alias('count')->from(TABLE_BUG)->where('story')->eq($storyID)->fetch('count');
//$story->caseCount = $this->dao->select('COUNT(*)')->alias('count')->from(TABLE_CASE)->where('story')->eq($storyID)->fetch('count');
@@ -159,13 +166,20 @@ class storyModel extends model
}
/* Get affected bugs. */
- $story->bugs = $this->dao->findByStory($story->id)->from(TABLE_BUG)
+ $story->bugs = $this->dao->select('*')->from(TABLE_BUG)
+ ->where('status')->ne('closed')
+ ->beginIF($story->siblings)->andWhere('story')->in(ltrim($story->siblings, ',') . $story->id)
+ ->beginIF(!$story->siblings)->andWhere('story')->in($story->id)
->andWhere('status')->ne('closed')
->andWhere('deleted')->eq(0)
->orderBy('id desc')->fetchAll();
/* Get affected cases. */
- $story->cases = $this->dao->findByStory($story->id)->from(TABLE_CASE)->andWhere('deleted')->eq(0)->fetchAll();
+ $story->cases = $this->dao->select('*')->from(TABLE_CASE)
+ ->where('deleted')->eq(0)
+ ->beginIF($story->siblings)->andWhere('story')->in(ltrim($story->siblings, ',') . $story->id)
+ ->beginIF(!$story->siblings)->andWhere('story')->in($story->id)
+ ->fetchAll();
return $story;
}
@@ -228,173 +242,223 @@ class storyModel extends model
->join('assignedTo', '')
->join('mailto', ',')
->stripTags($this->config->story->editor->create['id'], $this->config->allowedTags)
- ->remove('files,labels,reviewer,needNotReview,newStory,uid,contactListMenu,URS,region,lane,ticket')
+ ->remove('files,labels,reviewer,needNotReview,newStory,uid,contactListMenu,URS,region,lane,ticket,branches,modules,plans')
->get();
/* Check repeat story. */
$result = $this->loadModel('common')->removeDuplicate('story', $story, "product={$story->product}");
if(isset($result['stop']) and $result['stop']) return array('status' => 'exists', 'id' => $result['duplicate']);
-
if($story->status != 'draft' and $this->checkForceReview()) $story->status = 'reviewing';
- if(strpos('draft,reviewing', $story->status) !== false) $story->stage = $this->post->plan > 0 ? 'planned' : 'wait';
$story = $this->loadModel('file')->processImgURL($story, $this->config->story->editor->create['id'], $this->post->uid);
- $requiredFields = "," . $this->config->story->create->requiredFields . ",";
-
- if($story->type == 'requirement') $requiredFields = str_replace(',plan,', ',', $requiredFields);
- if(strpos($requiredFields, ',estimate,') !== false)
+ $product = $this->loadModel('product')->getById($story->product);
+ if($product->type == 'normal' or $story->type == 'requirement')
{
- if(strlen(trim($story->estimate)) == 0) dao::$errors['estimate'] = sprintf($this->lang->error->notempty, $this->lang->story->estimate);
- $requiredFields = str_replace(',estimate,', ',', $requiredFields);
+ $this->post->branches = isset($story->branch) ? array($story->branch) : array(0 => 0);
+ $this->post->modules = isset($story->module) ? array($story->module) : array(0 => 0);
+ $this->post->plans = isset($story->plan) ? array($story->plan) : array(0 => 0);
}
- $requiredFields = trim($requiredFields, ',');
-
- $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify')
- ->autoCheck()
- ->checkIF($story->notifyEmail, 'notifyEmail', 'email')
- ->batchCheck($requiredFields, 'notempty')
- ->checkFlow()
- ->exec();
-
- if(!dao::isError())
+ /* check module */
+ $requiredFields = "," . $this->config->story->create->requiredFields . ",";
+ if(strpos($requiredFields, ',module,') !== false)
{
- $storyID = $this->dao->lastInsertID();
-
- /* Fix bug #21992, user story have no parent story. */
- if(isset($story->parent) and $story->parent)
+ foreach($this->post->modules as $module)
{
- $stories = array($storyID);
- $this->subdivide($story->parent, $stories);
- }
-
- if(!empty($story->plan))
- {
- $this->updateStoryOrderOfPlan($storyID, $story->plan); // Set story order in this plan.
- $this->loadModel('action')->create('productplan', $story->plan, 'linkstory', '', $storyID);
- }
-
- $this->file->updateObjectID($this->post->uid, $storyID, $story->type);
- $files = $this->file->saveUpload($story->type, $storyID, 1);
-
- $data = new stdclass();
- $data->story = $storyID;
- $data->version = 1;
- $data->title = $story->title;
- $data->spec = $story->spec;
- $data->verify = $story->verify;
- $data->files = join(',', array_keys($files));
- $this->dao->insert(TABLE_STORYSPEC)->data($data)->exec();
-
- /* Save the story reviewer to storyreview table. */
- if(isset($_POST['reviewer']))
- {
- foreach($this->post->reviewer as $reviewer)
+ if(empty($module))
{
- if(empty($reviewer)) continue;
-
- $reviewData = new stdclass();
- $reviewData->story = $storyID;
- $reviewData->version = 1;
- $reviewData->reviewer = $reviewer;
- $this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+ dao::$errors[] = sprintf($this->lang->error->notempty, $this->lang->story->module);
+ return false;
}
}
+ }
- /* Project or execution linked story. */
- if($executionID != 0)
+ $storyIds = array();
+ $storyFile = array();
+ $mainStoryID = 0;
+ foreach($this->post->branches as $key => $branch)
+ {
+ $story->branch = $branch;
+ $story->module = $this->post->modules[$key];
+ $story->plan = $this->post->plans[$key];
+
+ if(strpos('draft,reviewing', $story->status) !== false) $story->stage = $this->post->plan > 0 ? 'planned' : 'wait';
+ if($story->type == 'requirement') $requiredFields = str_replace(',plan,', ',', $requiredFields);
+ if(strpos($requiredFields, ',estimate,') !== false)
{
- $this->linkStory($executionID, $this->post->product, $storyID);
- if($executionID != $this->session->project) $this->linkStory($this->session->project, $this->post->product, $storyID);
-
- $this->loadModel('kanban');
-
- $laneID = isset($output['laneID']) ? $output['laneID'] : 0;
- if(isset($_POST['lane'])) $laneID = $_POST['lane'];
-
- $columnID = $this->kanban->getColumnIDByLaneID($laneID, 'backlog');
- if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0;
-
- if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, $laneID, $columnID, 'story', $storyID);
- if(empty($laneID) or empty($columnID)) $this->kanban->updateLane($executionID, 'story');
+ if(strlen(trim($story->estimate)) == 0) dao::$errors['estimate'] = sprintf($this->lang->error->notempty, $this->lang->story->estimate);
+ $requiredFields = str_replace(',estimate,', ',', $requiredFields);
}
- if(is_array($this->post->URS))
+ $requiredFields = trim($requiredFields, ',');
+
+ $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify')
+ ->autoCheck()
+ ->checkIF($story->notifyEmail, 'notifyEmail', 'email')
+ ->batchCheck($requiredFields, 'notempty')
+ ->checkFlow()
+ ->exec();
+
+ if(dao::isError()) return false;
+
+ if(!dao::isError())
{
- foreach($this->post->URS as $URID)
+ $storyID = $this->dao->lastInsertID();
+
+ /* Fix bug #21992, user story have no parent story. */
+ if(isset($story->parent) and $story->parent)
{
- $requirement = $this->getByID($URID);
- $data = new stdclass();
- $data->product = $story->product;
- $data->AType = 'requirement';
- $data->relation = 'subdivideinto';
- $data->BType = 'story';
- $data->AID = $URID;
- $data->BID = $storyID;
- $data->AVersion = $requirement->version;
- $data->BVersion = 1;
- $data->extra = 1;
-
- $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec();
-
- $data->AType = 'story';
- $data->relation = 'subdividedfrom';
- $data->BType = 'requirement';
- $data->AID = $storyID;
- $data->BID = $URID;
- $data->AVersion = 1;
- $data->BVersion = $requirement->version;
-
- $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec();
+ $stories = array($storyID);
+ $this->subdivide($story->parent, $stories);
}
- }
- if($bugID > 0)
- {
- if(($this->config->edition == 'biz' || $this->config->edition == 'max')) $oldBug = $this->dao->select('feedback, status')->from(TABLE_BUG)->where('id')->eq($bugID)->fetch();
-
- $bug = new stdclass();
- $bug->toStory = $storyID;
- $bug->status = 'closed';
- $bug->resolution = 'tostory';
- $bug->resolvedBy = $this->app->user->account;
- $bug->resolvedDate = $now;
- $bug->closedBy = $this->app->user->account;
- $bug->closedDate = $now;
- $bug->assignedTo = 'closed';
- $bug->assignedDate = $now;
- $this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec();
-
- $this->loadModel('action')->create('bug', $bugID, 'ToStory', '', $storyID);
- $this->action->create('bug', $bugID, 'Closed');
-
- if(($this->config->edition == 'biz' || $this->config->edition == 'max') && !dao::isError() && $oldBug->feedback) $this->loadModel('feedback')->updateStatus('bug', $oldBug->feedback, 'closed', $oldBug->status);
-
- /* add files to story from bug. */
- $files = $this->dao->select('*')->from(TABLE_FILE)
- ->where('objectType')->eq('bug')
- ->andWhere('objectID')->eq($bugID)
- ->fetchAll();
- if(!empty($files))
+ if(!empty($story->plan))
{
- foreach($files as $file)
+ $this->updateStoryOrderOfPlan($storyID, $story->plan); // Set story order in this plan.
+ $this->loadModel('action')->create('productplan', $story->plan, 'linkstory', '', $storyID);
+ }
+
+ $this->file->updateObjectID($this->post->uid, $storyID, $story->type);
+ $files = $this->file->saveUpload($story->type, $storyID, 1);
+ /* Multi branch sync files */
+ !empty($files) ? $storyFile = $files : $files = $storyFile;
+
+ $data = new stdclass();
+ $data->story = $storyID;
+ $data->version = 1;
+ $data->title = $story->title;
+ $data->spec = $story->spec;
+ $data->verify = $story->verify;
+ $data->files = join(',', array_keys($files));
+ $this->dao->insert(TABLE_STORYSPEC)->data($data)->exec();
+
+ /* Save the story reviewer to storyreview table. */
+ if(isset($_POST['reviewer']))
+ {
+ foreach($this->post->reviewer as $reviewer)
{
- $file->objectType = 'story';
- $file->objectID = $storyID;
- unset($file->id);
- $this->dao->insert(TABLE_FILE)->data($file)->exec();
+ if(empty($reviewer)) continue;
+
+ $reviewData = new stdclass();
+ $reviewData->story = $storyID;
+ $reviewData->version = 1;
+ $reviewData->reviewer = $reviewer;
+ $this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
}
}
+
+ /* Project or execution linked story. */
+ if($executionID != 0)
+ {
+ $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');
+
+ $laneID = isset($output['laneID']) ? $output['laneID'] : 0;
+ if(isset($_POST['lane'])) $laneID = $_POST['lane'];
+
+ $columnID = $this->kanban->getColumnIDByLaneID($laneID, 'backlog');
+ if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0;
+
+ if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, $laneID, $columnID, 'story', $storyID);
+ if(empty($laneID) or empty($columnID)) $this->kanban->updateLane($executionID, 'story');
+ }
+
+ if(is_array($this->post->URS))
+ {
+ foreach($this->post->URS as $URID)
+ {
+ $requirement = $this->getByID($URID);
+ $data = new stdclass();
+ $data->product = $story->product;
+ $data->AType = 'requirement';
+ $data->relation = 'subdivideinto';
+ $data->BType = 'story';
+ $data->AID = $URID;
+ $data->BID = $storyID;
+ $data->AVersion = $requirement->version;
+ $data->BVersion = 1;
+ $data->extra = 1;
+
+ $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec();
+
+ $data->AType = 'story';
+ $data->relation = 'subdividedfrom';
+ $data->BType = 'requirement';
+ $data->AID = $storyID;
+ $data->BID = $URID;
+ $data->AVersion = 1;
+ $data->BVersion = $requirement->version;
+
+ $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec();
+ }
+ }
+
+ if($bugID > 0)
+ {
+ if(($this->config->edition == 'biz' || $this->config->edition == 'max')) $oldBug = $this->dao->select('feedback, status')->from(TABLE_BUG)->where('id')->eq($bugID)->fetch();
+
+ $bug = new stdclass();
+ $bug->toStory = $storyID;
+ $bug->status = 'closed';
+ $bug->resolution = 'tostory';
+ $bug->resolvedBy = $this->app->user->account;
+ $bug->resolvedDate = $now;
+ $bug->closedBy = $this->app->user->account;
+ $bug->closedDate = $now;
+ $bug->assignedTo = 'closed';
+ $bug->assignedDate = $now;
+ $this->dao->update(TABLE_BUG)->data($bug)->where('id')->eq($bugID)->exec();
+
+ $this->loadModel('action')->create('bug', $bugID, 'ToStory', '', $storyID);
+ $this->action->create('bug', $bugID, 'Closed');
+
+ if(($this->config->edition == 'biz' || $this->config->edition == 'max') && !dao::isError() && $oldBug->feedback) $this->loadModel('feedback')->updateStatus('bug', $oldBug->feedback, 'closed', $oldBug->status);
+
+ /* add files to story from bug. */
+ $files = $this->dao->select('*')->from(TABLE_FILE)
+ ->where('objectType')->eq('bug')
+ ->andWhere('objectID')->eq($bugID)
+ ->fetchAll();
+ if(!empty($files))
+ {
+ foreach($files as $file)
+ {
+ $file->objectType = 'story';
+ $file->objectID = $storyID;
+ unset($file->id);
+ $this->dao->insert(TABLE_FILE)->data($file)->exec();
+ }
+ }
+ }
+ if(!defined('TUTORIAL')) $this->setStage($storyID);
+ if(!dao::isError()) $this->loadModel('score')->create('story', 'create',$storyID);
+
+ /* Callback the callable method to process the related data for object that is transfered to story. */
+ if($from && is_callable(array($this, $this->config->story->fromObjects[$from]['callback']))) call_user_func(array($this, $this->config->story->fromObjects[$from]['callback']), $storyID);
+
+ $storyIds[] = $storyID;
+ if(empty($mainStoryID)) $mainStoryID = $storyID;
}
- if(!defined('TUTORIAL')) $this->setStage($storyID);
- if(!dao::isError()) $this->loadModel('score')->create('story', 'create',$storyID);
-
- /* Callback the callable method to process the related data for object that is transfered to story. */
- if($from && is_callable(array($this, $this->config->story->fromObjects[$from]['callback']))) call_user_func(array($this, $this->config->story->fromObjects[$from]['callback']), $storyID);
-
- return array('status' => 'created', 'id' => $storyID);
}
- return false;
+
+ /* bind siblings story id */
+ if(count($storyIds) > 1)
+ {
+ foreach($storyIds as $siblingsStoryID)
+ {
+ $siblingsArr = array();
+ foreach($storyIds as $idItem)
+ {
+ if($idItem != $siblingsStoryID) $siblingsArr[] = $idItem;
+ $siblings = ',' . implode(',', $siblingsArr) . ',';
+ $this->dao->update(TABLE_STORY)->set('siblings')->eq($siblings)->where('id')->eq($siblingsStoryID)->exec();
+ }
+ }
+ }
+
+ return array('status' => 'created', 'id' => $mainStoryID, 'ids' => $storyIds);
}
/**
@@ -739,6 +803,16 @@ class storyModel extends model
$data->files = $story->files = $addedFiles . trim($storyFiles, ',');
$this->dao->insert(TABLE_STORYSPEC)->data($data)->exec();
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $data->story = $siblingID;
+ $this->dao->insert(TABLE_STORYSPEC)->data($data)->exec();
+ }
+ }
+
/* IF is story and has changed, update its relation version to new. */
if($oldStory->type == 'story')
{
@@ -767,6 +841,16 @@ class storyModel extends model
$reviewData->version = $story->version;
$reviewData->reviewer = $reviewer;
$this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $reviewData->story = $siblingID;
+ $this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+ }
+ }
}
if($reviewerHasChanged)
@@ -776,7 +860,9 @@ class storyModel extends model
}
}
- return common::createChanges($oldStory, $story);
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($oldStory->id, $oldStory->siblings, $changes, 'Changed');
+ return $changes;
}
}
@@ -803,6 +889,17 @@ class storyModel extends model
return false;
}
+ $storyPlan = !empty($_POST['plan']) ? array_filter($_POST['plan']) : array();
+ if(count($storyPlan) > 1)
+ {
+ $oldStoryPlan = !empty($oldStory->planTitle) ? array_keys($oldStory->planTitle) : array();
+ if(!empty(array_diff($storyPlan,$oldStoryPlan)) or !empty(array_diff($oldStoryPlan,$storyPlan)))
+ {
+ dao::$errors[] = $this->lang->story->notice->changePlan;
+ return false;
+ }
+ }
+
$story = fixer::input('post')
->cleanInt('product,module,pri,duplicateStory')
->cleanFloat('estimate')
@@ -836,6 +933,14 @@ class storyModel extends model
->remove('files,labels,comment,contactListMenu,stages,reviewer,needNotReview')
->get();
+ /* Relieve siblings when change product. */
+ if(!empty($oldStory->siblings) and $story->product != $oldStory->product)
+ {
+ $this->dbh->exec("UPDATE " . TABLE_STORY . " SET siblings = REPLACE(siblings, ',$storyID,', ',') WHERE `product` = $oldStory->product");
+ $this->dao->update(TABLE_STORY)->set('siblings')->eq('')->where('id')->eq($storyID)->orWhere('siblings')->eq(',')->exec();
+ $oldStory->siblings = '';
+ }
+
if($oldStory->type == 'story' and !isset($story->linkStories)) $story->linkStories = '';
if($oldStory->type == 'requirement' and !isset($story->linkRequirements)) $story->linkRequirements = '';
if($oldStory->status == 'changing' and $story->status == 'draft') $story->status = 'changing';
@@ -887,6 +992,19 @@ class storyModel extends model
->beginIF($oldStory->status == 'reviewing')->andWhere('reviewer')->notin(implode(',', $_POST['reviewer']))
->exec();
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)
+ ->where('story')->eq($siblingID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->beginIF($oldStory->status == 'reviewing')->andWhere('reviewer')->notin(implode(',', $_POST['reviewer']))
+ ->exec();
+ }
+ }
+
foreach($_POST['reviewer'] as $reviewer)
{
if($oldStory->status == 'reviewing' and in_array($reviewer, array_keys($oldReviewer))) continue;
@@ -896,6 +1014,16 @@ class storyModel extends model
$reviewData->version = $oldStory->version;
$reviewData->reviewer = $reviewer;
$this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $reviewData->story = $siblingID;
+ $this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+ }
+ }
}
if($oldStory->status == 'reviewing') $story = $this->updateStoryByReview($storyID, $oldStory, $story);
@@ -933,6 +1061,18 @@ class storyModel extends model
$data->verify = $story->verify;
$data->files = $story->files = $addedFiles . trim($storyFiles, ',');
$this->dao->update(TABLE_STORYSPEC)->data($data)->where('story')->eq((int)$storyID)->andWhere('version')->eq($oldStory->version)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->update(TABLE_STORYSPEC)->data($data)
+ ->where('story')->eq((int)$siblingID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->exec();
+ }
+ }
}
if($story->product != $oldStory->product)
@@ -1062,6 +1202,8 @@ class storyModel extends model
if(isset($story->finalResult)) $this->recordReviewAction($story);
}
+ if(!empty($oldStory->siblings)) $this->syncSiblings($oldStory->id, $oldStory->siblings, $changes, 'Edited');
+
return true;
}
}
@@ -1475,7 +1617,28 @@ class storyModel extends model
/* Fix bug #671. */
$this->lang->story->closedReason = $this->lang->story->rejectedReason;
- $this->dao->update(TABLE_STORYREVIEW)->set('result')->eq($this->post->result)->set('reviewDate')->eq($now)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->andWhere('reviewer')->eq($this->app->user->account)->exec();
+ $this->dao->update(TABLE_STORYREVIEW)
+ ->set('result')->eq($this->post->result)
+ ->set('reviewDate')->eq($now)
+ ->where('story')->eq($storyID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->andWhere('reviewer')->eq($this->app->user->account)
+ ->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->update(TABLE_STORYREVIEW)
+ ->set('result')->eq($this->post->result)
+ ->set('reviewDate')->eq($now)
+ ->where('story')->eq($siblingID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->andWhere('reviewer')->eq($this->app->user->account)
+ ->exec();
+ }
+ }
$story = $this->updateStoryByReview($storyID, $oldStory, $story);
@@ -1507,6 +1670,8 @@ class storyModel extends model
$this->action->logHistory($actionID, $changes);
}
+ if(!empty($oldStory->siblings)) $this->syncSiblings($oldStory->id, $oldStory->siblings, $changes, 'Reviewed');
+
return true;
}
@@ -1525,17 +1690,23 @@ class storyModel extends model
$actions = array();
$this->loadModel('action');
- $oldStories = $this->getByList($storyIdList);
- $hasResult = $this->dao->select('story,version,result')->from(TABLE_STORYREVIEW)->where('story')->in($storyIdList)->andWhere('reviewer')->eq($this->app->user->account)->andWhere('result')->ne('')->orderBy('version')->fetchAll('story');
- $reviewerList = $this->dao->select('story,reviewer,result,version')->from(TABLE_STORYREVIEW)->where('story')->in($storyIdList)->orderBy('version')->fetchGroup('story', 'reviewer');
+ $reviewedSiblings = array();
+ $oldStories = $this->getByList($storyIdList);
+ $hasResult = $this->dao->select('story,version,result')->from(TABLE_STORYREVIEW)->where('story')->in($storyIdList)->andWhere('reviewer')->eq($this->app->user->account)->andWhere('result')->ne('')->orderBy('version')->fetchAll('story');
+ $reviewerList = $this->dao->select('story,reviewer,result,version')->from(TABLE_STORYREVIEW)->where('story')->in($storyIdList)->orderBy('version')->fetchGroup('story', 'reviewer');
foreach($storyIdList as $storyID)
{
if(!$storyID) continue;
$isSuperReviewer = strpos(',' . trim(zget($this->config->story, 'superReviewers', ''), ',') . ',', ',' . $this->app->user->account . ',');
$oldStory = $oldStories[$storyID];
-
if($oldStory->status != 'reviewing') continue;
+
+ foreach($reviewerList[$storyID] as $reviewer => $reviewerInfo)
+ {
+ if($reviewerInfo->version != $oldStory->version) unset($reviewerList[$storyID][$reviewer]);
+ }
+
if(!in_array($this->app->user->account, array_keys($reviewerList[$storyID])) and $isSuperReviewer === false) continue;
if(isset($hasResult[$storyID]) and $hasResult[$storyID]->version == $oldStories[$storyID]->version) continue;
if($oldStory->version > 1 and $result == 'reject') continue;
@@ -1549,6 +1720,21 @@ class storyModel extends model
$this->dao->update(TABLE_STORYREVIEW)->set('result')->eq($result)->set('reviewDate')->eq($now)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->andWhere('reviewer')->eq($this->app->user->account)->exec();
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->update(TABLE_STORYREVIEW)
+ ->set('result')->eq($result)
+ ->set('reviewDate')->eq($now)
+ ->where('story')->eq($siblingID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->andWhere('reviewer')->eq($this->app->user->account)
+ ->exec();
+ }
+ }
+
/* Update the story status by review rules. */
$reviewedBy = explode(',', trim($story->reviewedBy, ','));
if($isSuperReviewer !== false)
@@ -1571,6 +1757,19 @@ class storyModel extends model
$story->id = $storyID;
$story->version = $oldStory->version;
$actions[$storyID] = $this->recordReviewAction($story, $result, $reason);
+
+ /* Sync siblings. */
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings))
+ {
+ $siblings = $oldStory->siblings;
+ foreach(explode(',', $siblings) as $siblingID)
+ {
+ if(in_array($siblingID, $storyIdList) or isset($reviewedSiblings[$siblingID])) $siblings = str_replace(",$siblingID,", ',', $siblings);
+ }
+ $this->syncSiblings($storyID, trim($siblings, ','), $changes, 'Reviewed');
+ foreach(explode(',', trim($siblings, ',')) as $reviewedID) $reviewedSiblings[$reviewedID] = $reviewedID;
+ }
}
return $actions;
@@ -1585,11 +1784,26 @@ class storyModel extends model
*/
public function recallReview($storyID)
{
- $story = $this->getById($storyID);
- $isChanged = $story->changedBy ? true : false;
- $status = $isChanged ? 'changing' : 'draft';
- $this->dao->update(TABLE_STORY)->set('status')->eq($status)->where('id')->eq($storyID)->exec();
- $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($story->version)->exec();
+ $oldStory = $this->getById($storyID);
+ $isChanged = $oldStory->changedBy ? true : false;
+
+ $story = clone $oldStory;
+ $story->status = $isChanged ? 'changing' : 'draft';
+ $this->dao->update(TABLE_STORY)->set('status')->eq($story->status)->where('id')->eq($storyID)->exec();
+
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($siblingID)->andWhere('version')->eq($oldStory->version)->exec();
+ }
+ }
+
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'recalled');
}
/**
@@ -1601,16 +1815,31 @@ class storyModel extends model
*/
public function recallChange($storyID)
{
- $story = $this->getById($storyID);
+ $oldStory = $this->getById($storyID);
/* Update story title and version and status. */
- $preVersion = $story->version - 1;
- $preTitle = $this->dao->select('title')->from(TABLE_STORYSPEC)->where('story')->eq($storyID)->andWHere('version')->eq($preVersion)->fetch('title');
- $this->dao->update(TABLE_STORY)->set('title')->eq($preTitle)->set('version')->eq($preVersion)->set('status')->eq('active')->where('id')->eq($storyID)->exec();
+ $story = clone $oldStory;
+ $story->version = $oldStory->version - 1;
+ $story->title = $this->dao->select('title')->from(TABLE_STORYSPEC)->where('story')->eq($storyID)->andWHere('version')->eq($story->version)->fetch('title');
+ $story->status = 'active';
+ $this->dao->update(TABLE_STORY)->set('title')->eq($story->title)->set('version')->eq($story->version)->set('status')->eq($story->status)->where('id')->eq($storyID)->exec();
/* Delete versions that is after this version. */
- $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($storyID)->andWHere('version')->eq($story->version)->exec();
- $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($story->version)->exec();
+ $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($storyID)->andWHere('version')->eq($oldStory->version)->exec();
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($siblingID)->andWHere('version')->eq($oldStory->version)->exec();
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($siblingID)->andWhere('version')->eq($oldStory->version)->exec();
+ }
+ }
+
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'recalledChange');
}
/**
@@ -1643,6 +1872,15 @@ class storyModel extends model
$this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->andWhere('version')->eq($oldStory->version)->exec();
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($siblingID)->andWhere('version')->eq($oldStory->version)->exec();
+ }
+ }
+
if(isset($_POST['reviewer']))
{
foreach($this->post->reviewer as $reviewer)
@@ -1654,12 +1892,25 @@ class storyModel extends model
$reviewData->version = $oldStory->version;
$reviewData->reviewer = $reviewer;
$this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $reviewData->story = $siblingID;
+ $this->dao->insert(TABLE_STORYREVIEW)->data($reviewData)->exec();
+ }
+ }
}
$story->status = 'reviewing';
}
$this->dao->update(TABLE_STORY)->data($story, 'reviewer')->where('id')->eq($storyID)->exec();
- if(!dao::isError()) return common::createChanges($oldStory, $story);
+
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'submitReview');
+ if(!dao::isError()) return $changes;
return false;
}
@@ -1797,7 +2048,10 @@ class storyModel extends model
if(($this->config->edition == 'biz' || $this->config->edition == 'max') && $oldStory->feedback) $this->loadModel('feedback')->updateStatus('story', $oldStory->feedback, $story->status, $oldStory->status);
}
- return common::createChanges($oldStory, $story);
+
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'Closed');
+ return $changes;
}
/**
@@ -1975,14 +2229,17 @@ class storyModel extends model
if($oldStory->stage == 'wait') $story->stage = 'planned';
if($this->session->currentProductType and $this->session->currentProductType != 'normal' and $oldStory->branch == 0)
{
- if(!isset($oldStoryStages[$storyID][$plan->branch]))
+ foreach(explode(',', $plan->branch) as $planBranch)
{
- $story->stage = 'planned';
- $newStoryStage = new stdclass();
- $newStoryStage->story = $storyID;
- $newStoryStage->branch = $plan->branch;
- $newStoryStage->stage = $story->stage;
- $this->dao->insert(TABLE_STORYSTAGE)->data($newStoryStage)->autoCheck()->exec();
+ if(!isset($oldStoryStages[$storyID][$planBranch]))
+ {
+ $story->stage = 'planned';
+ $newStoryStage = new stdclass();
+ $newStoryStage->story = $storyID;
+ $newStoryStage->branch = $planBranch;
+ $newStoryStage->stage = $story->stage;
+ $this->dao->insert(TABLE_STORYSTAGE)->data($newStoryStage)->autoCheck()->exec();
+ }
}
}
}
@@ -2312,7 +2569,10 @@ class storyModel extends model
$story = $this->loadModel('file')->processImgURL($story, $this->config->story->editor->assignto['id'], $this->post->uid);
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->checkFlow()->where('id')->eq((int)$storyID)->exec();
- if(!dao::isError()) return common::createChanges($oldStory, $story);
+
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'Assigned');
+ if(!dao::isError()) return $changes;
return false;
}
@@ -2329,9 +2589,15 @@ class storyModel extends model
$storyIdList = $this->post->storyIdList;
$assignedTo = $this->post->assignedTo;
$oldStories = $this->getByList($storyIdList);
+ $ignoreStories = '';
foreach($storyIdList as $storyID)
{
$oldStory = $oldStories[$storyID];
+ if($oldStory->status == 'closed')
+ {
+ $ignoreStories .= "#{$storyID},";
+ continue;
+ }
if($assignedTo == $oldStory->assignedTo) continue;
$story = new stdclass();
@@ -2344,6 +2610,13 @@ class storyModel extends model
$allChanges[$storyID] = common::createChanges($oldStory, $story);
}
+
+ if($ignoreStories)
+ {
+ $ignoreStories = trim($ignoreStories, ',');
+ echo js::alert(sprintf($this->lang->story->ignoreClosedStory, $ignoreStories));
+ }
+
return $allChanges;
}
@@ -2381,14 +2654,28 @@ class storyModel extends model
$story = $this->loadModel('file')->processImgURL($story, $this->config->story->editor->activate['id'], $this->post->uid);
$this->dao->update(TABLE_STORY)->data($story)->autoCheck()->checkFlow()->where('id')->eq($storyID)->exec();
- if($story->status == 'active') $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->exec();
+ if($story->status == 'active')
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($storyID)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($siblingID)->exec();
+ }
+ }
+ }
$this->setStage($storyID);
/* Update parent story status. */
if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent);
- return common::createChanges($oldStory, $story);
+ $changes = common::createChanges($oldStory, $story);
+ if(!empty($oldStory->siblings)) $this->syncSiblings($storyID, $oldStory->siblings, $changes, 'Activated');
+ return $changes;
}
/**
@@ -3481,6 +3768,7 @@ class storyModel extends model
->andWhere('status')->notin('closed,draft')
->andWhere('product')->eq($productID)
->andWhere('plan')->in('0,')
+ ->andWhere('siblings')->eq('')
->beginIF($append)->orWhere('id')->in($append)->fi()
->fetchPairs();
return array(0 => '') + $stories ;
@@ -5140,9 +5428,9 @@ class storyModel extends model
foreach($requirements as $requirement)
{
- $stories = $this->getRelation($requirement->id, 'requirement');
+ $stories = $this->getRelation($requirement->id, 'requirement', array('id', 'title', 'parent'));
$stories = empty($stories) ? array() : $stories;
- foreach($stories as $id => $title)
+ foreach($stories as $id => $story)
{
if($projectStories and !isset($projectStories[$id]))
{
@@ -5151,10 +5439,11 @@ class storyModel extends model
}
$stories[$id] = new stdclass();
- $stories[$id]->title = $title;
- $stories[$id]->cases = $this->loadModel('testcase')->getStoryCases($id);
- $stories[$id]->bugs = $this->loadModel('bug')->getStoryBugs($id);
- $stories[$id]->tasks = $this->loadModel('task')->getStoryTasks($id);
+ $stories[$id]->parent = $story->parent;
+ $stories[$id]->title = $story->title;
+ $stories[$id]->cases = $this->loadModel('testcase')->getStoryCases($id);
+ $stories[$id]->bugs = $this->loadModel('bug')->getStoryBugs($id);
+ $stories[$id]->tasks = $this->loadModel('task')->getStoryTasks($id);
if($this->config->edition == 'max')
{
$stories[$id]->designs = $this->dao->select('id, name')->from(TABLE_DESIGN)
@@ -5747,6 +6036,16 @@ class storyModel extends model
/* Delete versions that is after this version. */
$this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($story->id)->andWHere('version')->in($oldStory->version)->exec();
$this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($story->id)->andWhere('version')->in($oldStory->version)->exec();
+
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->eq($siblingID)->andWHere('version')->in($oldStory->version)->exec();
+ $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->eq($siblingID)->andWhere('version')->in($oldStory->version)->exec();
+ }
+ }
}
if($result == 'reject')
@@ -5852,6 +6151,19 @@ class storyModel extends model
->andWhere('result')->eq('')
->exec();
+ /* Sync siblings. */
+ if(!empty($oldStory->siblings))
+ {
+ foreach(explode(',', trim($oldStory->siblings, ',')) as $siblingID)
+ {
+ $this->dao->delete()->from(TABLE_STORYREVIEW)
+ ->where('story')->eq($siblingID)
+ ->andWhere('version')->eq($oldStory->version)
+ ->andWhere('result')->eq('')
+ ->exec();
+ }
+ }
+
return $story;
}
@@ -5979,7 +6291,16 @@ class storyModel extends model
/* Get related objects title or names. */
$relatedSpecs = $this->dao->select('*')->from(TABLE_STORYSPEC)->where('`story`')->in($storyIdList)->orderBy('version desc')->fetchGroup('story');
+
+ $fileIdList = array();
+ foreach($relatedSpecs as $relatedSpec)
+ {
+ if(!empty($relatedSpec[0]->files)) $fileIdList[] = $relatedSpec[0]->files;
+ }
+ $fileIdList = array_unique($fileIdList);
$relatedFiles = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('objectType')->eq('story')->andWhere('objectID')->in($storyIdList)->andWhere('extra')->ne('editor')->fetchGroup('objectID');
+ $filesInfo = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('id')->in($fileIdList)->andWhere('extra')->ne('editor')->fetchAll('id');
+
foreach($stories as $story)
{
$story->spec = '';
@@ -5990,6 +6311,11 @@ class storyModel extends model
$story->title = $storySpec->title;
$story->spec = $storySpec->spec;
$story->verify = $storySpec->verify;
+
+ if(!empty($storySpec->files) and empty($relatedFiles[$story->id]) and !empty($filesInfo[$storySpec->files]))
+ {
+ $relatedFiles[$story->id][0] = $filesInfo[$storySpec->files];
+ }
}
if($this->post->fileType == 'csv')
@@ -6078,22 +6404,33 @@ class storyModel extends model
* Get the story status after activation.
*
* @param int $storyID
+ * @param bool $hasSiblings
* @access public
* @return void
*/
- public function getActivateStatus($storyID)
+ public function getActivateStatus($storyID, $hasSiblings = true)
{
$status = 'active';
+ $action = $hasSiblings ? 'closed,reviewrejected,syncsiblings' : 'closed,reviewrejected';
$lastRecord = $this->dao->select('action,extra')->from(TABLE_ACTION)
->where('objectType')->eq('story')
->andWhere('objectID')->eq($storyID)
- ->andWhere('action')->in('closed,reviewrejected')
+ ->andWhere('action')->in($action)
->orderBy('id_desc')
->fetch();
$lastAction = $lastRecord->action;
- if($lastAction == 'reviewrejected') $status = 'draft';
- if($lastAction == 'closed') $status = strpos($lastRecord->extra, '|') !== false ? substr($lastRecord->extra, strpos($lastRecord->extra, '|') + 1) : 'active';
+ if(strpos('closed,reviewrejected', $lastAction) !== false)
+ {
+ $status = strpos($lastRecord->extra, '|') !== false ? substr($lastRecord->extra, strpos($lastRecord->extra, '|') + 1) : 'active';
+ }
+
+ /* When activating twin story, you need to check the status of the twin story selected when closing. */
+ if($lastAction == 'syncsiblings')
+ {
+ $syncStoryID = strpos($lastRecord->extra, '|') !== false ? substr($lastRecord->extra, strpos($lastRecord->extra, '|') + 1) : 0;
+ $status = $this->getActivateStatus($syncStoryID, false);
+ }
return $status;
}
@@ -6132,4 +6469,47 @@ class storyModel extends model
return $lastReviewer;
}
+
+ /**
+ * Sync siblings.
+ *
+ * @param int $storyID
+ * @param string $siblings
+ * @param array $changes
+ * @param string $operate
+ * @access public
+ * @return void
+ */
+ public function syncSiblings($storyID, $siblings, $changes, $operate)
+ {
+ if(empty($siblings) or empty($changes)) return;
+
+ /* Get the fields and values to be synchronized. */
+ $syncFieldList = array();
+ foreach($changes as $changeInfo)
+ {
+ $fieldName = $changeInfo['field'];
+ $fieldValue = $changeInfo['new'];
+
+ if(strpos('product,branch,module,plan,spec,verify,files,reviewers', $fieldName) !== false) continue;
+ $syncFieldList[$fieldName] = $fieldValue;
+ }
+
+ if(empty($syncFieldList)) return;
+
+ /* Synchronize and record dynamics. */
+ $this->loadModel('action');
+ $siblings = explode(',', trim($siblings, ','));
+ foreach($siblings as $siblingID)
+ {
+ $this->dao->update(TABLE_STORY)->data($syncFieldList)->where('id')->eq((int)$siblingID)->exec();
+ if(!dao::isError())
+ {
+ $this->setStage($siblingID);
+
+ $actionID = $this->action->create('story', $siblingID, 'syncsiblings', '', "$operate|$storyID");
+ $this->action->logHistory($actionID, $changes);
+ }
+ }
+ }
}
diff --git a/module/story/view/activate.html.php b/module/story/view/activate.html.php
index e1876c9dfa..4d6a4059d0 100644
--- a/module/story/view/activate.html.php
+++ b/module/story/view/activate.html.php
@@ -28,6 +28,12 @@
story->assignedTo;?> |
closedBy, 'class="form-control chosen"');?> | |
+ siblings)):?>
+
+ |
+ story->activateSyncTip;?> |
+
+
printExtendFields($story, 'table');?>
| story->comment;?> |
diff --git a/module/story/view/assignto.html.php b/module/story/view/assignto.html.php
index 8db1b45f73..fec93c1c35 100644
--- a/module/story/view/assignto.html.php
+++ b/module/story/view/assignto.html.php
@@ -28,7 +28,13 @@
| story->assign;?> |
assignedTo, "class='form-control chosen'");?> | |
-
+
+ siblings)):?>
+
+ |
+ story->assignSyncTip;?> |
+
+
| story->status;?> |
status);?> |
diff --git a/module/story/view/blocksibling.html.php b/module/story/view/blocksibling.html.php
new file mode 100644
index 0000000000..b5fdc07456
--- /dev/null
+++ b/module/story/view/blocksibling.html.php
@@ -0,0 +1,28 @@
+story->currentBranch = sprintf($this->lang->story->currentBranch, $this->lang->product->branchName[$product->type]);
+js::set('relieveURL', inlink('ajaxRelieveSibling'));
+js::set('storyID', $story->id);
+js::set('relieved', $lang->story->relieved);
+js::set('relievedTip', $lang->story->relievedTip);
+js::set('cancel', $lang->cancel);
+$canViewLinkStory = common::hasPriv('story', 'view');
+$canRelieved = common::hasPriv('story', 'relieved');
+array_unshift($siblings, $story);
+$class = isonlybody() ? 'showinonlybody' : 'iframe';
+
+foreach($siblings as $sibling)
+{
+ $id = $sibling->id;
+ $title = '#' . $id . ' '. $sibling->title;
+ $branch = isset($branches[$sibling->branch]) ? $branches[$sibling->branch] : '';
+ $stage = $lang->story->stageList[$sibling->stage];
+ $labelClass = $story->branch == $sibling->branch ? 'label-primary' : '';
+
+ echo "";
+ echo "{$branch} ";
+ echo "{$stage} ";
+ echo ($canViewLinkStory ? html::a($this->createLink('story', 'view', "id=$id", '', true), "$title", '', "class='$class viewlink' data-width='80%'") : "$title");
+ if($canRelieved) echo "";
+ echo "";
+}
+?>
diff --git a/module/story/view/change.html.php b/module/story/view/change.html.php
index c4fc435056..a07eb54b9c 100644
--- a/module/story/view/change.html.php
+++ b/module/story/view/change.html.php
@@ -73,6 +73,21 @@
fetch('file', 'buildform');?>
+
+
+ |
+ story->changeSyncTip;?>
+
+ |
+
+
+ |
+
+ |
+
+
| story->checkAffection;?> |
|
diff --git a/module/story/view/close.html.php b/module/story/view/close.html.php
index 8f09e2a651..aa1b6707f5 100644
--- a/module/story/view/close.html.php
+++ b/module/story/view/close.html.php
@@ -28,6 +28,12 @@
story->closedReason;?> |
| |
+ siblings)):?>
+
+ |
+ story->closeSyncTip;?> |
+
+
| story->duplicateStory;?> |
'') + $productStories, '', "class='form-control' placeholder='{$lang->bug->duplicateTip}'"); ?> | |
diff --git a/module/story/view/create.html.php b/module/story/view/create.html.php
index 3f92c9a6f5..dcd784e481 100644
--- a/module/story/view/create.html.php
+++ b/module/story/view/create.html.php
@@ -17,6 +17,9 @@
story->feedbackSource); ?>
story->create->requiredFields);?>
+
+
+
story->create->requiredFields) as $field)
{
@@ -51,35 +54,76 @@ foreach(explode(',', $config->story->create->requiredFields) as $field)
story->module : $lang->story->product;?> |
-
+
-
+
|
-
- ';
- }
- ?>
+ | switchBranch'>
+
+ |
+ switchBranch'> |
+ switchBranch'>
+ ';
+ }
+ ?>
|
-
- | story->assignedTo : $lang->story->planAB;?> |
-
+ | switchBranch' >
+ | product->branchName[$product->type];?> |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ story->notice->branch;?>
+ |
+
+
+ | story->planAB;?> |
+
story->create->requiredFields) as $field)
+
+
+
story->module);?>
+story->plan);?>
+
diff --git a/module/story/view/edit.html.php b/module/story/view/edit.html.php
index a201e015ef..cb81070841 100644
--- a/module/story/view/edit.html.php
+++ b/module/story/view/edit.html.php
@@ -24,6 +24,8 @@
story->feedbackSource); ?>
status);?>
+siblings)?>
+story->relievedSiblingsTip)?>
| | |