diff --git a/module/product/model.php b/module/product/model.php
index 113768fa44..6a2f26a678 100755
--- a/module/product/model.php
+++ b/module/product/model.php
@@ -1195,10 +1195,17 @@ class productModel extends model
$rateCount = 0;
$allCount = 0;
$SRTotal = 0;
+ $parents = array();
+ foreach($stories as $story)
+ {
+ if($story->parent > 0) $parents[$story->parent] = $story->parent;
+ }
+
foreach($stories as $story)
{
if($storyType == 'requirement' && $story->type == 'story') $SRTotal += 1;
if(!empty($story->type) && $story->type != $storyType) continue;
+ if(isset($parents[$story->id])) continue;
$totalEstimate += $story->estimate;
$allCount ++;
diff --git a/module/product/ui/browse.html.php b/module/product/ui/browse.html.php
index 6092400bb5..a75b89e13c 100644
--- a/module/product/ui/browse.html.php
+++ b/module/product/ui/browse.html.php
@@ -215,7 +215,7 @@ $fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project,
$canBatchChangeStage = $canBeChanged && hasPriv('story', 'batchChangeStage') && $storyType == 'story';
$canBatchChangeBranch = $canBeChanged && hasPriv($storyType, 'batchChangeBranch') && $product && $product->type != 'normal' && $productID;
$canBatchChangeModule = $canBeChanged && hasPriv($storyType, 'batchChangeModule') && $productID && $product && $product->type == 'normal';
- $canBatchChangePlan = $canBeChanged && hasPriv('story', 'batchChangePlan') && $storyType == 'story' && (!$isProjectStory || $projectHasProduct || ($isProjectStory && isset($project->model) && $project->model == 'scrum')) && $productID && $product && $product->type == 'normal';
+ $canBatchChangePlan = $canBeChanged && hasPriv('story', 'batchChangePlan') && (!$isProjectStory || $projectHasProduct || ($isProjectStory && isset($project->model) && $project->model == 'scrum')) && $productID && $product && $product->type == 'normal';
$canBatchAssignTo = $canBeChanged && hasPriv($storyType, 'batchAssignTo');
$canBatchUnlink = $canBeChanged && $projectHasProduct && hasPriv('projectstory', 'batchUnlinkStory');
$canBatchImportToLib = $canBeChanged && $isProjectStory && in_array($this->config->edition, array('max', 'ipd')) && hasPriv('story', 'batchImportToLib') && helper::hasFeature('storylib');
diff --git a/module/productplan/control.php b/module/productplan/control.php
index 1323b35f43..2339c9b2a3 100644
--- a/module/productplan/control.php
+++ b/module/productplan/control.php
@@ -349,6 +349,8 @@ class productplan extends control
*/
public function view(int $planID = 0, string $type = 'story', string $orderBy = 'order_desc', string $link = 'false', string $param = '', int $recTotal = 0, int $recPerPage = 100, int $pageID = 1)
{
+ $this->app->loadLang('requirement');
+ $this->app->loadLang('epic');
$plan = $this->productplan->getByID($planID, true);
if(!$plan)
{
@@ -567,11 +569,11 @@ class productplan extends control
$planStories = $this->loadModel('story')->getPlanStories($planID);
if($browseType == 'bySearch')
{
- $allStories = $this->story->getBySearch($plan->product, "0,{$plan->branch}", (int)$param, 'id', 0, 'story', array_keys($planStories), '', $pager);
+ $allStories = $this->story->getBySearch($plan->product, "0,{$plan->branch}", (int)$param, 'id', 0, 'all', array_keys($planStories), '', $pager);
}
else
{
- $allStories = $this->story->getProductStories($this->view->product->id, $plan->branch ? "0,{$plan->branch}" : 0, '0', 'draft,reviewing,active,changing', 'story', 'id_desc', $hasParent = false, array_keys($planStories), $pager);
+ $allStories = $this->story->getProductStories($this->view->product->id, $plan->branch ? "0,{$plan->branch}" : 0, '0', 'draft,reviewing,active,changing', 'all', 'id_desc', $hasParent = false, array_keys($planStories), $pager);
}
$modules = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all');
diff --git a/module/productplan/js/view.ui.js b/module/productplan/js/view.ui.js
index f6e8053b53..b53558f452 100644
--- a/module/productplan/js/view.ui.js
+++ b/module/productplan/js/view.ui.js
@@ -12,7 +12,8 @@ window.renderStoryCell = function(result, info)
if(info.col.name == 'title' && result)
{
let html = '';
- if(story.parent > 0) html += "" + childrenAB + "";
+ let gradeLabel = gradeGroup[story.type][story.grade];
+ if(gradeLabel) html += "" + gradeLabel + " ";
if(html) result.unshift({html});
}
return result;
diff --git a/module/productplan/lang/de.php b/module/productplan/lang/de.php
index fd8f0c76f7..8061ffd77f 100644
--- a/module/productplan/lang/de.php
+++ b/module/productplan/lang/de.php
@@ -78,8 +78,8 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt
$lang->productplan->nextStep = "Next step";
$lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s.";
$lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%.";
-$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
-$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->id = 'ID';
@@ -104,6 +104,7 @@ $lang->productplan->order = "Rank";
$lang->productplan->deleted = "Deleted";
$lang->productplan->mailto = "Mailto";
$lang->productplan->planStatus = "Status";
+$lang->productplan->storyTitle = "Title";
$lang->productplan->statusList['wait'] = 'Wait';
$lang->productplan->statusList['doing'] = 'Doing';
diff --git a/module/productplan/lang/en.php b/module/productplan/lang/en.php
index 86357b1cce..0589716c8a 100644
--- a/module/productplan/lang/en.php
+++ b/module/productplan/lang/en.php
@@ -78,8 +78,8 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt
$lang->productplan->nextStep = "Next step";
$lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s.";
$lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%.";
-$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
-$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->id = 'ID';
@@ -104,6 +104,7 @@ $lang->productplan->order = "Order";
$lang->productplan->deleted = "Deleted";
$lang->productplan->mailto = "Mailto";
$lang->productplan->planStatus = "Status";
+$lang->productplan->storyTitle = "Title";
$lang->productplan->statusList['wait'] = 'Wait';
$lang->productplan->statusList['doing'] = 'Doing';
diff --git a/module/productplan/lang/fr.php b/module/productplan/lang/fr.php
index c59cf9d607..067e2af324 100644
--- a/module/productplan/lang/fr.php
+++ b/module/productplan/lang/fr.php
@@ -78,8 +78,8 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt
$lang->productplan->nextStep = "Next step";
$lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s.";
$lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%.";
-$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
-$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
+$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?";
$lang->productplan->id = 'ID';
@@ -104,6 +104,7 @@ $lang->productplan->order = "Order";
$lang->productplan->deleted = "Deleted";
$lang->productplan->mailto = "Mailto";
$lang->productplan->planStatus = "Status";
+$lang->productplan->storyTitle = "Title";
$lang->productplan->statusList['wait'] = 'Wait';
$lang->productplan->statusList['doing'] = 'Doing';
diff --git a/module/productplan/lang/zh-cn.php b/module/productplan/lang/zh-cn.php
index 5e795d2ce1..c1ef8c9cd1 100644
--- a/module/productplan/lang/zh-cn.php
+++ b/module/productplan/lang/zh-cn.php
@@ -40,12 +40,12 @@ $lang->productplan->batchUnlink = "批量移除";
$lang->productplan->batchClose = "批量关闭";
$lang->productplan->batchChangeStatus = "批量修改状态";
$lang->productplan->unlinkAB = "移除";
-$lang->productplan->linkStory = "关联{$lang->SRCommon}";
-$lang->productplan->unlinkStory = "移除{$lang->SRCommon}";
+$lang->productplan->linkStory = "关联需求";
+$lang->productplan->unlinkStory = "移除需求";
$lang->productplan->unlinkStoryAB = "移除";
-$lang->productplan->batchUnlinkStory = "批量移除{$lang->SRCommon}";
-$lang->productplan->linkedStories = $lang->SRCommon;
-$lang->productplan->unlinkedStories = "未关联{$lang->SRCommon}";
+$lang->productplan->batchUnlinkStory = "批量移除需求";
+$lang->productplan->linkedStories = '需求';
+$lang->productplan->unlinkedStories = "未关联需求";
$lang->productplan->updateOrder = '排序';
$lang->productplan->createChildren = "创建子计划";
$lang->productplan->createExecution = "创建{$lang->execution->common}";
@@ -65,7 +65,7 @@ $lang->productplan->expired = "已过期";
$lang->productplan->closedReason = "关闭原因";
$lang->productplan->confirmDelete = "您确认删除该计划吗?";
-$lang->productplan->confirmUnlinkStory = "您确认移除该{$lang->SRCommon}吗?";
+$lang->productplan->confirmUnlinkStory = "您确认移除该需求吗?";
$lang->productplan->confirmUnlinkBug = "您确认移除该Bug吗?";
$lang->productplan->confirmStart = "您确认开始该计划吗?";
$lang->productplan->confirmFinish = "您确认完成该计划吗?";
@@ -78,8 +78,8 @@ $lang->productplan->projectNotEmpty = "所属{$lang->projectCommon}不能为
$lang->productplan->nextStep = "下一步";
$lang->productplan->summary = "本页共 %s 个计划,父计划 %s,子计划 %s,独立计划 %s。";
$lang->productplan->checkedSummary = "共选中 %total% 个计划,父计划 %parent%,子计划 %child%,独立计划 %independent%。";
-$lang->productplan->confirmChangePlan = "分支『%s』解除关联后,分支下的%s个{$lang->SRCommon}和%s个Bug将同步从计划中移除,是否解除?";
-$lang->productplan->confirmRemoveStory = "分支『%s』解除关联后,分支下的%s个{$lang->SRCommon}将同步从计划中移除,是否解除?";
+$lang->productplan->confirmChangePlan = "分支『%s』解除关联后,分支下的%s个需求和%s个Bug将同步从计划中移除,是否解除?";
+$lang->productplan->confirmRemoveStory = "分支『%s』解除关联后,分支下的%s个需求将同步从计划中移除,是否解除?";
$lang->productplan->confirmRemoveBug = "分支『%s』解除关联后,分支下的%s个Bug将同步从计划中移除,是否解除?";
$lang->productplan->id = '编号';
@@ -92,7 +92,7 @@ $lang->productplan->end = '结束日期';
$lang->productplan->status = '状态';
$lang->productplan->last = "上次计划";
$lang->productplan->future = '待定';
-$lang->productplan->stories = "{$lang->SRCommon}数";
+$lang->productplan->stories = "需求数";
$lang->productplan->bugs = 'Bug数';
$lang->productplan->hour = $lang->hourCommon;
$lang->productplan->execution = $lang->execution->common;
@@ -104,6 +104,7 @@ $lang->productplan->order = "排序";
$lang->productplan->deleted = "已删除";
$lang->productplan->mailto = "抄送给";
$lang->productplan->planStatus = "状态";
+$lang->productplan->storyTitle = "需求名称";
$lang->productplan->statusList['wait'] = '未开始';
$lang->productplan->statusList['doing'] = '进行中';
diff --git a/module/productplan/model.php b/module/productplan/model.php
index 0eaa87335f..78ae46b510 100644
--- a/module/productplan/model.php
+++ b/module/productplan/model.php
@@ -855,10 +855,12 @@ class productplanModel extends model
if(strpos(",$story->plan,", ",{$planID},") !== false) continue;
/* Update the plan linked with the story and the order of the story in the plan. */
- $storyID = (int)$storyID;
- $this->dao->update(TABLE_STORY)->set('plan')->eq($planID)->where('id')->eq($storyID)->exec();
+ $storyID = (int)$storyID;
+ $newPlanID = $story->type == 'story' ? $planID : trim($story->plan, ',') . ',' . $planID;
+ $this->dao->update(TABLE_STORY)->set('plan')->eq($newPlanID)->where('id')->eq($storyID)->exec();
- $this->story->updateStoryOrderOfPlan($storyID, (string)$planID, $story->plan);
+ $oldPlanID = $story->type == 'story' ? $story->plan : '';
+ $this->story->updateStoryOrderOfPlan($storyID, (string)$planID, $oldPlanID);
$this->action->create('story', $storyID, 'linked2plan', '', $planID);
$this->story->setStage($storyID);
diff --git a/module/productplan/ui/linkstory.html.php b/module/productplan/ui/linkstory.html.php
index dc431e1175..ea90836073 100644
--- a/module/productplan/ui/linkstory.html.php
+++ b/module/productplan/ui/linkstory.html.php
@@ -14,7 +14,7 @@ $cols = array();
foreach($config->productplan->defaultFields['linkStory'] as $field) $cols[$field] = zget($config->story->dtable->fieldList, $field, array());
$cols = array_map(function($col){$col['show'] = true; return $col;}, $cols);
$cols['title']['link'] = $this->createLink('story', 'view', "storyID={id}");
-$cols['title']['nestedToggle'] = false;
+$cols['title']['title'] = $lang->productplan->storyTitle;
$cols['plan']['name'] = 'planTitle';
$cols['assignedTo']['type'] = 'user';
$cols['module']['type'] = 'text';
@@ -22,6 +22,7 @@ $cols['module']['map'] = $modules;
foreach($allStories as $story) $story->estimate = $story->estimate . $config->hourUnit;
+$config->product->search['fields']['title'] = $lang->productplan->storyTitle;
searchForm
(
set::module('story'),
diff --git a/module/productplan/ui/view.html.php b/module/productplan/ui/view.html.php
index 1800abf0dc..dff7d4a557 100644
--- a/module/productplan/ui/view.html.php
+++ b/module/productplan/ui/view.html.php
@@ -32,6 +32,7 @@ jsVar('planID', $plan->id);
jsVar('confirmLang', $confirmLang);
jsVar('unlinkURL', $unlinkURL);
jsVar('childrenAB', $lang->story->childrenAB);
+jsVar('gradeGroup', $gradeGroup);
$bugCols = array();
$storyCols = array();
@@ -39,7 +40,7 @@ foreach($config->productplan->defaultFields['story'] as $field) $storyCols[$fiel
foreach($config->productplan->defaultFields['bug'] as $field) $bugCols[$field] = zget($config->bug->dtable->fieldList, $field, array());
$storyCols['title']['link'] = $this->createLink('story', 'view', "storyID={id}");
-$storyCols['title']['nestedToggle'] = false;
+$storyCols['title']['title'] = $lang->productplan->storyTitle;
$storyCols['assignedTo']['type'] = 'user';
$storyCols['actions']['width'] = 50;
$bugCols['assignedTo']['type'] = 'user';
@@ -136,8 +137,12 @@ $planStories = initTableData($planStories, $storyCols, $this->productplan);
$planBugs = initTableData($planBugs, $bugCols, $this->productplan);
foreach($planStories as $story) $story->estimate = $story->estimate . $config->hourUnit;
-$createStoryLink = common::hasPriv('story', 'create') ? $this->createLink('story', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null;
-$batchCreateStoryLink = common::hasPriv('story', 'batchCreate') ? $this->createLink('story', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null;
+$createStoryLink = common::hasPriv('story', 'create') ? $this->createLink('story', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null;
+$batchCreateStoryLink = common::hasPriv('story', 'batchCreate') ? $this->createLink('story', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null;
+$createRequirementLink = common::hasPriv('requirement', 'create') ? $this->createLink('requirement', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null;
+$batchCreateRequirementLink = common::hasPriv('requirement', 'batchCreate') ? $this->createLink('requirement', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null;
+$createEpicLink = common::hasPriv('epic', 'create') ? $this->createLink('epic', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null;
+$batchCreateEpicLink = common::hasPriv('epic', 'batchCreate') ? $this->createLink('epic', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null;
$branchNames = '';
if($product->type != 'normal')
@@ -192,7 +197,13 @@ detailBody
dropdown
(
btn(set::text($lang->story->create), set::target('_parent'), setClass('secondary' . (empty($createStoryLink) ? ' disabled' : '')), set::icon('plus'), set::caret(true), set::url($createStoryLink)),
- set::items(array(array('text' => $lang->story->batchCreate, 'url' => $batchCreateStoryLink, 'class' => empty($batchCreateStoryLink) ? 'disabled' : ''))),
+ set::items(array(
+ array('text' => $lang->story->batchCreate . $lang->SRCommon, 'url' => $batchCreateStoryLink, 'class' => empty($batchCreateStoryLink) ? 'disabled' : ''),
+ array('text' => $lang->requirement->create, 'url' => $createRequirementLink, 'class' => empty($createRequirementLink) ? 'disabled' : ''),
+ array('text' => $lang->requirement->batchCreate . $lang->URCommon, 'url' => $batchCreateRequirementLink, 'class' => empty($batchCreateRequirementLink) ? 'disabled' : ''),
+ array('text' => $lang->epic->create, 'url' => $createEpicLink, 'class' => empty($createEpicLink) ? 'disabled' : ''),
+ array('text' => $lang->epic->batchCreate . $lang->ERCommon, 'url' => $batchCreateEpicLink, 'class' => empty($batchCreateEpicLink) ? 'disabled' : ''),
+ )),
set::trigger('hover'),
set::placement('bottom-end')
),
diff --git a/module/productplan/zen.php b/module/productplan/zen.php
index b69df5a125..c5129e5738 100644
--- a/module/productplan/zen.php
+++ b/module/productplan/zen.php
@@ -233,7 +233,12 @@ class productplanZen extends productplan
if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent);
if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id);
+ $gradeList = $this->loadModel('story')->getGradeList('');
+ $gradeGroup = array();
+ foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name;
+
$this->view->plan = $plan;
+ $this->view->gradeGroup = $gradeGroup;
$this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id);
$this->view->users = $this->loadModel('user')->getPairs('noletter');
$this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true);
@@ -265,6 +270,7 @@ class productplanZen extends productplan
$this->config->product->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$plan->id&type=story&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID'));
$this->config->product->search['queryID'] = $queryID;
$this->config->product->search['style'] = 'simple';
+ $this->config->product->search['fields']['title'] = $this->lang->productplan->storyTitle;
$this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject);
$this->config->product->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true);
$this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all');
diff --git a/module/story/config/form.php b/module/story/config/form.php
index b8342d824f..2cf4996714 100644
--- a/module/story/config/form.php
+++ b/module/story/config/form.php
@@ -8,7 +8,7 @@ $config->story->form->create = array();
$config->story->form->create['product'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array());
$config->story->form->create['branch'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array());
$config->story->form->create['module'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array());
-$config->story->form->create['plan'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array());
+$config->story->form->create['plan'] = array('type' => 'array', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join');
$config->story->form->create['assignedTo'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => 'users');
$config->story->form->create['source'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->sourceList);
$config->story->form->create['sourceNote'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim');
@@ -48,7 +48,7 @@ $config->story->form->edit['title'] = array('type' => 'string', 'contr
$config->story->form->edit['color'] = array('type' => 'string', 'control' => 'color', 'required' => false, 'default' => '');
$config->story->form->edit['spec'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '');
$config->story->form->edit['verify'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '');
-$config->story->form->edit['plan'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array());
+$config->story->form->edit['plan'] = array('type' => 'array', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join');
$config->story->form->edit['source'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->sourceList);
$config->story->form->edit['sourceNote'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim');
$config->story->form->edit['stage'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->stageList);
@@ -70,7 +70,7 @@ $config->story->form->edit['childStories'] = array('type' => 'array', 'contr
$config->story->form->batchCreate = common::formConfig('story', 'batchCreate');
$config->story->form->batchCreate['branch'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array());
$config->story->form->batchCreate['module'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array());
-$config->story->form->batchCreate['plan'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array());
+$config->story->form->batchCreate['plan'] = array('ditto' => true, 'type' => 'array', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array(), 'filter' => 'join');
$config->story->form->batchCreate['parent'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array());
$config->story->form->batchCreate['grade'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => 0, 'options' => array());
$config->story->form->batchCreate['region'] = array('ditto' => false, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => 0, 'options' => array());
@@ -93,7 +93,7 @@ $config->story->form->batchCreate['mailto'] = array('ditto' => false, 'type'
$config->story->form->batchEdit = array();
$config->story->form->batchEdit['branch'] = array('type' => 'int', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array());
$config->story->form->batchEdit['module'] = array('type' => 'int', 'width' => '200px', 'control' => array('control' => 'picker', 'required' => true), 'required' => false, 'default' => 0, 'options' => array());
-$config->story->form->batchEdit['plan'] = array('type' => 'int', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array());
+$config->story->form->batchEdit['plan'] = array('type' => 'array', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join');
$config->story->form->batchEdit['parent'] = array('type' => 'int', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array());
$config->story->form->batchEdit['grade'] = array('type' => 'int', 'width' => '136px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array());
$config->story->form->batchEdit['title'] = array('type' => 'string', 'width' => '240px', 'control' => array('control' => 'colorInput', 'inputClass' => 'filter-none'), 'required' => true, 'filter' => 'trim', 'base' => true);
diff --git a/module/story/js/batchedit.ui.js b/module/story/js/batchedit.ui.js
index d380fcf60a..5790442e9f 100644
--- a/module/story/js/batchedit.ui.js
+++ b/module/story/js/batchedit.ui.js
@@ -140,8 +140,9 @@ window.renderRowData = function($row, index, story)
items.push({text: plans[plan], value: plan});
}
options.items = items;
- options.disabled = story.parent < 0 || story.type == 'requirement';
+ options.disabled = story.parent < 0;
options.defaultValue = story.plan;
+ options.multiple = story.type != 'story' ? true : false;
$picker.render(options);
});
diff --git a/module/story/model.php b/module/story/model.php
index 1bbf860cfb..c2438e8a83 100644
--- a/module/story/model.php
+++ b/module/story/model.php
@@ -475,7 +475,11 @@ class storyModel extends model
if(!empty($story->plan))
{
$this->updateStoryOrderOfPlan($storyID, (string)$story->plan); // Set story order in this plan.
- $this->action->create('productplan', (int)$story->plan, 'linkstory', '', $storyID);
+ foreach(explode(',', $story->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $this->action->create('productplan', (int)$planID, 'linkstory', '', $storyID);
+ }
}
$this->setStage($storyID);
@@ -605,7 +609,11 @@ class storyModel extends model
if($story->plan)
{
$this->updateStoryOrderOfPlan($storyID, (string)$story->plan);
- $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},$storyID";
+ foreach(explode(',', (string)$story->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID";
+ }
}
if(!empty($story->URS)) $this->storyTao->doCreateURRelations($storyID, $story->URS);
@@ -1004,8 +1012,22 @@ class storyModel extends model
$oldStory = $oldStories[$storyID];
if($story->plan != $oldStory->plan)
{
- if(!empty($oldStory->plan)) $unlinkPlans[$oldStory->plan] = empty($unlinkPlans[$oldStory->plan]) ? $storyID : "{$unlinkPlans[$oldStory->plan]},{$storyID}";
- if(!empty($story->plan)) $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},{$storyID}";
+ if(!empty($oldStory->plan))
+ {
+ foreach(explode(',', (string)$oldStory->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $unlinkPlans[$planID] = empty($unlinkPlans[$planID]) ? $storyID : "{$unlinkPlans[$planID]},$storyID";
+ }
+ }
+ if(!empty($story->plan))
+ {
+ foreach(explode(',', (string)$story->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID";
+ }
+ }
}
if($story->grade > $oldStory->grade)
@@ -1035,7 +1057,6 @@ class storyModel extends model
if(dao::isError()) return false;
- /* Update story sort of plan when story plan has changed. */
if($story->grade != $oldStory->grade)
{
$gradeDiff = (int)$story->grade - (int)$oldStory->grade;
@@ -1047,6 +1068,7 @@ class storyModel extends model
->exec();
$this->dao->update(TABLE_STORY)->set('grade')->eq($story->grade)->where('id')->eq($storyID)->exec();
}
+ /* Update story sort of plan when story plan has changed. */
if($oldStory->plan != $story->plan) $this->updateStoryOrderOfPlan($storyID, (string)$story->plan, $oldStory->plan);
$parentChanged = $story->parent != $oldStory->parent;
if($parentChanged) $this->doChangeParent($storyID, $story, $oldStory->parent);
@@ -1072,8 +1094,8 @@ class storyModel extends model
}
$this->loadModel('score')->create('ajax', 'batchEdit');
- foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', $planID, 'unlinkstory', '', $stories);
- foreach($link2Plans as $planID => $stories) $this->action->create('productplan', $planID, 'linkstory', '', $stories);
+ foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'unlinkstory', '', $stories);
+ foreach($link2Plans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'linkstory', '', $stories);
return true;
}
@@ -1567,7 +1589,7 @@ class storyModel extends model
if(empty($oldStory)) continue;
if($oldStory->branch != BRANCH_MAIN and $plan->branch != BRANCH_MAIN and !in_array($oldStory->branch, explode(',', $plan->branch))) continue;
- /* Ignore parent story, closed story and story linked to this plan already. */
+ /* Ignore closed story and story linked to this plan already. */
if($oldStory->parent < 0) continue;
if($oldStory->status == 'closed') continue;
if(strpos(",{$oldStory->plan},", ",$planID,") !== false) continue;
@@ -1581,7 +1603,8 @@ class storyModel extends model
if($oldPlanID) $story->plan = trim(str_replace(",$oldPlanID,", ',', ",$oldStory->plan,"), ',');
/* Update the order of the story in the plan. */
- $this->updateStoryOrderOfPlan((int)$storyID, (string)$planID, $oldStory->plan);
+ $oldStoryPlan = $oldStory->type == 'story' ? $oldStory->plan : '';
+ $this->updateStoryOrderOfPlan((int)$storyID, (string)$planID, $oldStoryPlan);
/* Replace plan field if product is normal or not linked to plan or story linked to a branch. */
$productType = $products[$oldStory->product]->type;
@@ -1617,16 +1640,30 @@ class storyModel extends model
if(!dao::isError())
{
$allChanges[$storyID] = common::createChanges($oldStory, $story);
- if($story->plan != $oldStory->plan and !empty($oldStory->plan) and strpos((string)$oldStory->plan, ',') === false) $unlinkPlans[$oldStory->plan] = empty($unlinkPlans[$oldStory->plan]) ? $storyID : "{$unlinkPlans[$oldStory->plan]},$storyID";
- if($story->plan != $oldStory->plan and !empty($story->plan) and strpos((string)$story->plan, ',') === false) $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},$storyID";
+ if($story->plan != $oldStory->plan and !empty($oldStory->plan) and $oldStory->type == 'story')
+ {
+ foreach(explode(',', (string)$oldStory->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $unlinkPlans[$planID] = empty($unlinkPlans[$planID]) ? $storyID : "{$unlinkPlans[$planID]},$storyID";
+ }
+ }
+ if($story->plan != $oldStory->plan and !empty($story->plan))
+ {
+ foreach(explode(',', (string)$story->plan) as $planID)
+ {
+ if(!$planID) continue;
+ $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID";
+ }
+ }
}
}
if(!dao::isError())
{
$this->loadModel('action');
- foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', $planID, 'unlinkstory', '', $stories);
- foreach($link2Plans as $planID => $stories) $this->action->create('productplan', $planID, 'linkstory', '', $stories);
+ foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'unlinkstory', '', $stories);
+ foreach($link2Plans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'linkstory', '', $stories);
}
return $allChanges;
@@ -2011,7 +2048,7 @@ class storyModel extends model
->beginIF(!empty($excludeStories))->andWhere('id')->notIN($excludeStories)->fi()
->beginIF($status and $status != 'all')->andWhere('status')->in($status)->fi()
->andWhere("FIND_IN_SET('{$this->config->vision}', vision)")
- ->andWhere('type')->eq($type)
+ ->beginIF($type != 'all')->andWhere('type')->eq($type)->fi()
->orderBy($orderBy)
->page($pager)
->fetchAll('id');
@@ -2448,7 +2485,7 @@ class storyModel extends model
->beginIF($productID != 'all' && $productID != '' && $productID != 0)->andWhere('t1.`product`')->eq((int)$productID)->fi()
->andWhere('t1.deleted')->eq(0)
->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)")
- ->andWhere('t1.type')->eq($type)
+ ->beginIF($type != 'all')->andWhere('t1.type')->eq($type)->fi()
->orderBy($orderBy)
->page($pager, 't1.id')
->fetchAll('id');
diff --git a/module/story/ui/batchcreate.html.php b/module/story/ui/batchcreate.html.php
index e3c1235c38..aa23722f84 100644
--- a/module/story/ui/batchcreate.html.php
+++ b/module/story/ui/batchcreate.html.php
@@ -59,6 +59,7 @@ $fnGenerateFields = function() use ($app, $lang, $type, $fields, $stories, $cust
if(str_contains(",{$config->{$app->rawModule}->create->requiredFields},", ",{$colName},")) $cols[$index]['required'] = true;
if(isset($customFields[$colName]) && strpos(",$showFields,", ",$colName,") === false) $cols[$index]['hidden'] = true;
if($colName == 'sourceNote' && strpos(",$showFields,", ",source,") === false) $cols[$index]['hidden'] = true;
+ if($colName == 'plan' && $type != 'story') $cols[$index]['multiple'] = true;
if($colName == 'grade' && $gradeRule == 'stepwise') $cols[$index]['disabled'] = true;
if($colName == 'grade' && $hiddenGrade) $cols[$index]['hidden'] = true;
if($colName == 'spec') unset($cols[$index]['control']);
diff --git a/module/story/ui/batchedit.html.php b/module/story/ui/batchedit.html.php
index 4c8d548837..dc870c9848 100644
--- a/module/story/ui/batchedit.html.php
+++ b/module/story/ui/batchedit.html.php
@@ -49,7 +49,6 @@ $items['stage']['defaultDitto'] = 'off';
if($storyType == 'requirement')
{
- unset($items['plan']);
unset($items['stage']);
}
diff --git a/module/story/ui/create.field.php b/module/story/ui/create.field.php
index f614b5d501..7d1720ab52 100644
--- a/module/story/ui/create.field.php
+++ b/module/story/ui/create.field.php
@@ -130,7 +130,7 @@ if(!isset($createFields['branch']) && isset($createFields['plan']))
->required($createFields['plan']['required'])
->control('inputGroup')
->items(false)
- ->itemBegin('plan')->control('picker')->id('planIdBox')->items($createFields['plan']['options'])->value($createFields['plan']['default'])->itemEnd()
+ ->itemBegin('plan')->control('picker')->id('planIdBox')->items($createFields['plan']['options'])->value($createFields['plan']['default'])->multiple($type != 'story')->itemEnd()
->item(empty($createFields['plan']['options']) ? field()->control('btn')->icon('plus')->url(createLink('productplan', 'create', 'productID=' . data('productID') . '&branch=' . data('branch')))->set(array('data-toggle' => 'modal', 'data-size' => 'lg'))->set('title', $lang->productplan->create) : null)
->item(empty($createFields['plan']['options']) ? field()->control('btn')->icon('refresh')->id("loadProductPlans")->set('title', $lang->refresh) : null);
}
diff --git a/module/story/ui/edit.html.php b/module/story/ui/edit.html.php
index 23a8218cdc..136c2021a7 100644
--- a/module/story/ui/edit.html.php
+++ b/module/story/ui/edit.html.php
@@ -16,7 +16,7 @@ $forceReview = $this->story->checkForceReview();
$assignedToList = $story->status == 'closed' ? array('closed' => 'Closed') : $users;
$planCount = !empty($story->planTitle) ? count($story->planTitle) : 0;
-$multiplePlan = ($product->type != 'normal' && empty($story->branch) && $planCount > 1);
+$multiplePlan = ($product->type != 'normal' && empty($story->branch) && $planCount > 1) || ($story->type != 'story');
$minStage = $story->stage;
$stageList = implode(',', array_keys($this->lang->story->stageList));
@@ -250,7 +250,7 @@ detailBody
set::name($lang->story->grade),
picker(setID('grade'), set::name('grade'), set::required(true), set::items($fields['grade']['options']), set::value($story->grade), set::disabled($gradeRule == 'stepwise'))
) : picker(setID('grade'), set::name('grade'), set::required(true), set::items($fields['grade']['options']), set::value($story->grade), set::hidden(true)),
- $story->type == 'story' ? item
+ item
(
set::trClass(zget($fields['plan'], 'className', '')),
set::name($lang->story->plan),
@@ -264,7 +264,7 @@ detailBody
empty($fields['plan']['options']) ? btn(set::url($this->createLink('productplan', 'create', "productID={$story->product}&branch={$story->branch}")), setData(array('toggle' => 'modal')), icon('plus')) : null,
empty($fields['plan']['options']) ? btn(set('onclick', "loadProductPlans({$story->product})"), setClass('refresh'), icon('refresh')) : null
)
- ) : null,
+ ),
item
(
set::name($lang->story->source),
diff --git a/module/story/ui/view.html.php b/module/story/ui/view.html.php
index c38d9b29cf..df268c96d4 100644
--- a/module/story/ui/view.html.php
+++ b/module/story/ui/view.html.php
@@ -289,7 +289,7 @@ detailBody
set::name($lang->story->grade),
$story->grade
) : null,
- ($story->type != 'requirement' and $story->parent != -1 and !$hiddenPlan) ? item
+ ($story->parent != -1 and !$hiddenPlan) ? item
(
set::trClass('plan-line'),
set::name($lang->story->plan),
diff --git a/module/story/zen.php b/module/story/zen.php
index 05e0c8f5fe..8ddc4e7ddf 100644
--- a/module/story/zen.php
+++ b/module/story/zen.php
@@ -717,7 +717,7 @@ class storyZen extends story
if($this->story->checkForceReview()) $fields['reviewer']['required'] = true;
if(empty($branches)) unset($fields['branch']);
- if($this->view->hiddenPlan || $storyType == 'requirement') unset($fields['plan']);
+ if($this->view->hiddenPlan) unset($fields['plan']);
if($storyType == 'requirement')
{
unset($fields['parent']);
@@ -1205,12 +1205,12 @@ class storyZen extends story
if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther;
if(strpos('draft,changing', $oldStory->status) !== false and $this->story->checkForceReview() and empty($_POST['reviewer'])) dao::$errors[] = $this->lang->story->notice->reviewerNotEmpty;
if(!empty($_POST['plan'])) $storyPlan = is_array($_POST['plan']) ? array_filter($_POST['plan']) : array($_POST['plan']);
- if(count($storyPlan) > 1)
+ if(count($storyPlan) > 1 && $oldStory->type == 'story')
{
$oldStoryPlan = !empty($oldStory->plan) ? array_filter(explode(',', $oldStory->plan)) : array();
$oldPlanDiff = array_diff($storyPlan, $oldStoryPlan);
$storyPlanDiff = array_diff($oldStoryPlan, $storyPlan);
- if(!empty($oldPlanDiff) or !empty($storyPlanDiff)) dao::$errors[] = $this->lang->story->notice->changePlan;
+ if(!empty($oldPlanDiff) or !empty($storyPlanDiff)) dao::$errors['plan'] = $this->lang->story->notice->changePlan;
}
if(strpos(',draft,changing,', $oldStory->status) !== false)
{