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) {