diff --git a/db/update18.0.sql b/db/update18.0.sql new file mode 100644 index 0000000000..ecdd8e79a2 --- /dev/null +++ b/db/update18.0.sql @@ -0,0 +1,3 @@ +ALTER TABLE `zt_story` ADD COLUMN `siblings` varchar(255) NOT NULL AFTER `linkRequirements`; + +ALTER TABLE `zt_productplan` MODIFY COLUMN `branch` varchar(255) NOT NULL DEFAULT '0'; diff --git a/db/zentao.sql b/db/zentao.sql index 6b1d67115b..f17e27812a 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -1129,7 +1129,7 @@ CREATE TABLE IF NOT EXISTS `zt_product` ( CREATE TABLE IF NOT EXISTS `zt_productplan` ( `id` mediumint(8) unsigned NOT NULL auto_increment, `product` mediumint(8) unsigned NOT NULL, - `branch` mediumint(8) unsigned NOT NULL, + `branch` varchar(255) NOT NULL DEFAULT '0', `parent` mediumint(9) NOT NULL DEFAULT '0', `title` varchar(90) NOT NULL, `status` enum('wait','doing','done','closed') NOT NULL default 'wait', @@ -1480,6 +1480,7 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( `childStories` varchar(255) NOT NULL, `linkStories` varchar(255) NOT NULL, `linkRequirements` varchar(255) NOT NULL, + `siblings` varchar(255) NOT NULL, `duplicateStory` mediumint(8) unsigned NOT NULL, `version` smallint(6) NOT NULL default '1', `feedbackBy` varchar(100) NOT NULL, diff --git a/module/action/lang/de.php b/module/action/lang/de.php index a2f9edd72d..c596635e12 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -225,6 +225,7 @@ $lang->action->desc->reopen = '$date, reopened by $actoraction->desc->merged = '$date, merged by $actor .' . "\n"; $lang->action->desc->submitreview = '$date, submitted for review by $actor.' . "\n"; $lang->action->desc->ganttmove = '$date, sort by $actor .' . "\n"; +$lang->action->desc->relieved = '$date, relieved by $actor .' . "\n"; $lang->action->desc->switchtolight = '$date, Switch from ALM mode to light mode by '. $lang->admin->system .'.' . "\n"; $lang->action->desc->unlinkproduct = '$date, the project is disassociated from the $extra, synchronization disassociates the ' . $lang->executionCommon . 's of the project from the $extra.' . "\n"; @@ -395,6 +396,8 @@ $lang->action->label->tolib = 'imported'; $lang->action->label->updatetolib = 'updated'; $lang->action->label->ganttmove = 'sorted'; $lang->action->label->submitreview = 'submitted'; +$lang->action->label->syncsiblings = 'synchronized changes'; +$lang->action->label->relieved = 'relieved'; $lang->action->label->switchtolight = 'switch from ALM mode to light mode'; $lang->action->label->linkedrepo = 'Linked Code Repo'; $lang->action->label->unlinkedrepo = 'Unlinked Code Repo'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index b64e8eb071..dae713028d 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -225,6 +225,7 @@ $lang->action->desc->reopen = '$date, reopened by $actoraction->desc->merged = '$date, merged by $actor .' . "\n"; $lang->action->desc->submitreview = '$date, submitted for review by $actor.' . "\n"; $lang->action->desc->ganttmove = '$date, sort by $actor .' . "\n"; +$lang->action->desc->relieved = '$date, relieved by $actor .' . "\n"; $lang->action->desc->switchtolight = '$date, Switch from ALM mode to light mode by '. $lang->admin->system .'.' . "\n"; $lang->action->desc->unlinkproduct = '$date, the project is disassociated from the $extra, synchronization disassociates the ' . $lang->executionCommon . 's of the project from the $extra.' . "\n"; @@ -395,6 +396,8 @@ $lang->action->label->tolib = 'imported'; $lang->action->label->updatetolib = 'updated'; $lang->action->label->ganttmove = 'sorted'; $lang->action->label->submitreview = 'submitted'; +$lang->action->label->syncsiblings = 'synchronized changes'; +$lang->action->label->relieved = 'relieved'; $lang->action->label->switchtolight = 'switch from ALM mode to light mode'; $lang->action->label->linkedrepo = 'Linked Code Repo'; $lang->action->label->unlinkedrepo = 'Unlinked Code Repo'; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 127d47ecc9..f57a68bc0c 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -225,6 +225,7 @@ $lang->action->desc->reopen = '$date, reopened by $actoraction->desc->merged = '$date, merged by $actor .' . "\n"; $lang->action->desc->submitreview = '$date, submitted for review by $actor.' . "\n"; $lang->action->desc->ganttmove = '$date, sort by $actor .' . "\n"; +$lang->action->desc->relieved = '$date, relieved by $actor .' . "\n"; $lang->action->desc->switchtolight = '$date, Switch from ALM mode to light mode by '. $lang->admin->system .'.' . "\n"; $lang->action->desc->unlinkproduct = '$date, the project is disassociated from the $extra, synchronization disassociates the ' . $lang->executionCommon . 's of the project from the $extra.' . "\n"; @@ -395,6 +396,8 @@ $lang->action->label->tolib = 'Importé'; $lang->action->label->updatetolib = 'MàJ'; $lang->action->label->ganttmove = 'sorted'; $lang->action->label->submitreview = 'submitted'; +$lang->action->label->syncsiblings = 'synchronized changes'; +$lang->action->label->relieved = 'relieved'; $lang->action->label->switchtolight = 'switch from ALM mode to light mode'; $lang->action->label->linkedrepo = 'Linked Code Repo'; $lang->action->label->unlinkedrepo = 'Unlinked Code Repo'; diff --git a/module/action/lang/vi.php b/module/action/lang/vi.php index f9e5281266..cc0766191f 100644 --- a/module/action/lang/vi.php +++ b/module/action/lang/vi.php @@ -306,6 +306,7 @@ $lang->action->label->syncproject = 'start'; $lang->action->label->syncexecution = 'start'; $lang->action->label->startProgram = '(The start of the project sets the status of the program as Ongoing)'; $lang->action->label->submitreview = 'submitted'; +$lang->action->label->syncsiblings = 'synchronized changes'; $lang->action->label->linkedrepo = 'Linked Code Repo'; $lang->action->label->unlinkedrepo = 'Unlinked Code Repo'; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index a4e7069f68..11dc8c0688 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -225,6 +225,7 @@ $lang->action->desc->reopen = '$date, 由 $actor $lang->action->desc->merged = '$date, 由 $actor 合并。' . "\n"; $lang->action->desc->submitreview = '$date, 由 $actor 提交评审。' . "\n"; $lang->action->desc->ganttmove = '$date, 由 $actor 排序。' . "\n"; +$lang->action->desc->relieved = '$date, 由 $actor 解除孪生需求关系。' . "\n"; $lang->action->desc->switchtolight = '$date, 由于 '. $lang->admin->system .' 从全生命周期管理模式切换为轻量管理模式,项目访问控制由项目集内公开调整为私有。' . "\n"; $lang->action->desc->unlinkproduct = '$date, 系统判断由于' . $lang->executionCommon . '所属项目与$extra取消关联,同步将' . $lang->executionCommon . '与$extra取消关联。' . "\n"; @@ -395,6 +396,8 @@ $lang->action->label->tolib = '导入了'; $lang->action->label->updatetolib = '更新了'; $lang->action->label->ganttmove = '排序了'; $lang->action->label->submitreview = '提交了评审'; +$lang->action->label->syncsiblings = '同步修改了'; +$lang->action->label->relieved = '解除了'; $lang->action->label->switchtolight = '从全生命周期管理模式切换为轻量管理模式'; $lang->action->label->linkedrepo = '关联代码库到'; $lang->action->label->unlinkedrepo = '取消了项目与代码库的关联'; diff --git a/module/action/model.php b/module/action/model.php index 95dbf0701f..17954cb970 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -50,7 +50,7 @@ class actionModel extends model $action->extra = $extra; if(!defined('IN_UPGRADE')) $action->vision = $this->config->vision; - if($objectType == 'story' and strpos(',reviewpassed,reviewrejected,reviewclarified,reviewreverted,', ",$actionType,") !== false) $action->actor = $this->lang->action->system; + if($objectType == 'story' and strpos(',reviewpassed,reviewrejected,reviewclarified,reviewreverted,syncsiblings,', ",$actionType,") !== false) $action->actor = $this->lang->action->system; /* Use purifier to process comment. Fix bug #2683. */ $action->comment = fixer::stripDataTags($comment); @@ -952,6 +952,17 @@ class actionModel extends model } } + if($action->objectType == 'story' and $action->action == 'syncsiblings') + { + if(!empty($extra) and strpos($extra, '|') !== false) + { + list($operate, $storyID) = explode('|', $extra); + $desc['operate'] = $this->lang->$objectType->{$desc['operate']}; + $link = common::hasPriv('story', 'view') ? html::a(helper::createLink('story', 'view', "storyID=$storyID"), "#$storyID ") : "#$storyID"; + $actionDesc = str_replace(array('$extra', '$operate'), array($link, $desc['operate'][$operate]), $desc['main']); + } + } + if($action->objectType == 'module' and strpos(',created,moved,', $action->action) !== false) { $moduleNames = $this->loadModel('tree')->getOptionMenu($action->objectID, 'story', 0, 'all', ''); diff --git a/module/branch/control.php b/module/branch/control.php index 832bd4b0d3..f362b79248 100644 --- a/module/branch/control.php +++ b/module/branch/control.php @@ -260,10 +260,12 @@ class branch extends control * @param string $param * @param int $projectID * @param bool $withMainBranch + * @param string $isSiblings + * @param string $fieldID * @access public * @return void */ - public function ajaxGetBranches($productID, $oldBranch = 0, $param = 'all', $projectID = 0, $withMainBranch = true) + public function ajaxGetBranches($productID, $oldBranch = 0, $param = 'all', $projectID = 0, $withMainBranch = true, $isSiblings = 'no', $fieldID = '0') { $product = $this->loadModel('product')->getById($productID); if(empty($product) or $product->type == 'normal') return; @@ -280,6 +282,7 @@ class branch extends control $branchTagOption[$oldBranch] = $oldBranch == BRANCH_MAIN ? $branch : ($branch->name . ($branch->status == 'closed' ? ' (' . $this->lang->branch->statusList['closed'] . ')' : '')); } + if($isSiblings == 'yes') return print(html::select("branches[$fieldID]", $branchTagOption, $oldBranch, "onchange='loadBranchRelation(this.value, $fieldID);' class='form-control chosen control-branch'")); return print(html::select('branch', $branchTagOption, $oldBranch, "class='form-control' onchange='loadBranch(this)' data-last='{$oldBranch}'")); } diff --git a/module/execution/control.php b/module/execution/control.php index 6ea8d61fc6..de72a5abee 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -898,7 +898,7 @@ class execution extends control $storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList, $executionID); $storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); - $plans = $this->execution->getPlans($products, 'skipParent|withMainPlan'); + $plans = $this->execution->getPlans($products, 'skipParent|withMainPlan', $executionID); $allPlans = array('' => ''); if(!empty($plans)) { @@ -1645,7 +1645,20 @@ class execution extends control } else { - return print(js::confirm($this->lang->execution->importPlanStory, inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=yes"), inlink('create', "projectID=$projectID&executionID=$executionID"))); + $executionProductList = $this->loadModel('product')->getProducts($executionID); + $multiBranchProduct = false; + foreach($executionProductList as $executionProduct) + { + if($executionProduct->type != 'normal') + { + $multiBranchProduct = true; + break; + } + } + + $importPlanStoryTips = $multiBranchProduct ? $this->lang->execution->importBranchPlanStory : $this->lang->execution->importPlanStory; + + return print(js::confirm($importPlanStoryTips, inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=yes"), inlink('create', "projectID=$projectID&executionID=$executionID"))); } } @@ -1689,10 +1702,11 @@ class execution extends control $linkedBranches = array(); foreach($products as $productIndex => $product) { + $productPlans[$productIndex] = array(); foreach($branches[$productIndex] as $branchID => $branch) { $linkedBranches[$productIndex][$branchID] = $branchID; - $productPlans[$productIndex][$branchID] = isset($plans[$productIndex][$branchID]) ? $plans[$productIndex][$branchID] : array(); + $productPlans[$productIndex] += isset($plans[$productIndex][$branchID]) ? $plans[$productIndex][$branchID] : array(); } } @@ -1724,10 +1738,11 @@ class execution extends control $linkedBranches = array(); foreach($products as $productIndex => $product) { + $productPlans[$productIndex] = array(); foreach($branches[$productIndex] as $branchID => $branch) { $linkedBranches[$productIndex][$branchID] = $branchID; - $productPlans[$productIndex][$branchID] = isset($plans[$productIndex][$branchID]) ? $plans[$productIndex][$branchID] : array(); + $productPlans[$productIndex] += isset($plans[$productIndex][$branchID]) ? $plans[$productIndex][$branchID] : array(); } } @@ -1776,27 +1791,15 @@ class execution extends control return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $this->createLink('doc', 'objectLibs', "type=execution"))); } - $planID = ''; - if(isset($_POST['plans'])) - { - foreach($_POST['plans'] as $plans) - { - foreach($plans as $planID) - { - if(!empty($planID)) break; - } - } - } - if(!empty($projectID) and $project->model == 'kanban') { $execution = $this->execution->getById($executionID); $this->loadModel('kanban')->createRDKanban($execution); } - if(!empty($planID)) + if(!empty($_POST['plans'])) { - return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=$planID&confirm=no"))); + return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => inlink('create', "projectID=$projectID&executionID=$executionID©ExecutionID=&planID=1&confirm=no"))); } else { @@ -1904,7 +1907,20 @@ class execution extends control } elseif(!empty($newPlans)) { - return print(js::confirm($this->lang->execution->importEditPlanStory, inlink('edit', "executionID=$executionID&action=edit&extra=&newPlans=$newPlans&confirm=yes"), inlink('view', "executionID=$executionID"))); + $executionProductList = $this->loadModel('product')->getProducts($executionID); + $multiBranchProduct = false; + foreach($executionProductList as $executionProduct) + { + if($executionProduct->type != 'normal') + { + $multiBranchProduct = true; + break; + } + } + + $importEditPlanStoryTips = $multiBranchProduct ? $this->lang->execution->importBranchEditPlanStory : $this->lang->execution->importEditPlanStory; + + return print(js::confirm($importEditPlanStoryTips, inlink('edit', "executionID=$executionID&action=edit&extra=&newPlans=$newPlans&confirm=yes"), inlink('view', "executionID=$executionID"))); } /* Set menu. */ @@ -2003,12 +2019,15 @@ class execution extends control foreach($linkedProducts as $productID => $linkedProduct) { if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->name; + $productPlans[$productID] = array(); + foreach($branches[$productID] as $branchID => $branch) { + $productPlans[$productID] += isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); + $linkedBranchList[$branchID] = $branchID; $linkedBranches[$productID][$branchID] = $branchID; - $productPlans[$productID][$branchID] = isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); - if($branchID != BRANCH_MAIN and isset($plans[$productID][BRANCH_MAIN])) $productPlans[$productID][$branchID] += $plans[$productID][BRANCH_MAIN]; + if($branchID != BRANCH_MAIN and isset($plans[$productID][BRANCH_MAIN])) $productPlans[$productID] += $plans[$productID][BRANCH_MAIN]; if(!empty($executionStories[$productID][$branchID])) { array_push($unmodifiableProducts, $productID); @@ -4121,9 +4140,11 @@ class execution extends control $count = 0; if(!empty($planStory)) { + $projectProducts = $this->loadModel('project')->getBranchesByProject($executionID); foreach($planStory as $id => $story) { - if($story->status == 'draft' or $story->status == 'reviewing') + $projectBranches = zget($projectProducts, $story->product, array()); + if($story->status == 'draft' or $story->status == 'reviewing' or (!empty($story->branch) and !empty($projectBranches) and !isset($projectBranches[$story->branch]))) { $count++; unset($planStory[$id]); @@ -4156,7 +4177,7 @@ class execution extends control } $haveDraft = sprintf($this->lang->execution->haveDraft, $count); - if(!$execution->multiple) $haveDraft = str_replace($this->lang->executionCommon, $this->lang->projectCommon, $haveDraft); + if(!$execution->multiple or $moduleName == 'projectstory') $haveDraft = str_replace($this->lang->executionCommon, $this->lang->projectCommon, $haveDraft); if($count != 0) echo js::alert($haveDraft) . js::locate($this->createLink($moduleName, $fromMethod, $param)); return print(js::locate(helper::createLink($moduleName, $fromMethod, $param))); } diff --git a/module/execution/css/create.css b/module/execution/css/create.css index 839355f188..b3ef4c9ba2 100644 --- a/module/execution/css/create.css +++ b/module/execution/css/create.css @@ -21,9 +21,9 @@ .chosen-container.chosen-highlight-selected .disabled-result.result-selected {background: none;} -#productsBox .row .col-sm-4 {padding-right: 13px;} -#plansBox .row {display: inline-table; width: 102%;} -#plansBox .row .col-sm-4 {display: inline-block; float: none; padding-right: 13px;} +.productsBox .row .col-sm-4 {padding-right: 13px;} +.plansBox .row {display: inline-table; width: 102%;} +.plansBox .row .col-sm-4 {display: inline-block; float: none; padding-right: 13px;} #dateRange {vertical-align: top; padding-top: 13px;} #dateRangeOption {vertical-align: top; padding-top: 13px;} @@ -32,3 +32,9 @@ #copyProjectModal .projectSelect {display: inline-block; margin-left: 10px; width: 20%;} #copyProjectModal .titleBox{position: relative; bottom: 3px; display: inline-block;} a.chosen-single > div > b {top: -4px !important;} + +.productsBox a {border-radius: 2px !important;} +.productsBox .required:after {right: -9.5px !important;} +.productsBox .input-group-addon > div {display: inline-block !important;} +.productsBox .required + .text-danger.help-text {position: relative; left: 10px;} +.productsBox > #productNameLabel {padding-top: 8px;} diff --git a/module/execution/js/common.js b/module/execution/js/common.js index 19d68b6372..b7a2eb2964 100644 --- a/module/execution/js/common.js +++ b/module/execution/js/common.js @@ -113,6 +113,15 @@ function computeEndDate(delta) computeWorkDays(); } +/* Auto compute the work days. */ +$(function() +{ + $(".date").bind('dateSelected', function() + { + computeWorkDays(this.id); + }) +}); + /** * Load branches. * @@ -122,118 +131,127 @@ function computeEndDate(delta) */ function loadBranches(product) { - if($('#productsBox .input-group:last select:first').val() != 0) + /* When selecting a product, delete a plan that is empty by default. */ + $("#planDefault").remove(); + + $(".productsBox select[name^='products']").each(function() { - if(model !== 'waterfall') + var $product = $(product); + var productID = $(this).val(); + if($product.val() != 0 && $product.val() == $(this).val() && $product.attr('id') != $(this).attr('id') && !multiBranchProducts[$product.val()]) { - var length = $('#productsBox .input-group').size(); - $('#productsBox .row').append('
' + $('#productsBox .col-sm-4:last').html().replace('required', '') + '
'); - if($('#productsBox .input-group:last select').size() >= 2) $('#productsBox .input-group:last select:last').remove(); - $('#productsBox .input-group:last .chosen-container').remove(); - $('#productsBox .input-group:last select:first').attr('name', 'products[' + length + ']').attr('id', 'products' + length); - $('#productsBox .input-group:last .chosen').chosen(); + bootbox.alert(errorSameProducts); + $product.val(0); + $product.trigger("chosen:updated"); + return false; } - adjustProductBoxMargin(); + }); + + var $tableRow = $(product).closest('.table-row'); + var index = $tableRow.find('select:first').attr('id').replace('products' , ''); + var oldBranch = $(product).attr('data-branch') !== undefined ? $(product).attr('data-branch') : 0; + if($(product).val() != 0) + { + $(product).closest('tr').find('.newProduct').addClass('hidden') + } + else + { + $(product).closest('tr').find('.newProduct').removeClass('hidden') } - var $inputgroup = $(product).closest('.input-group'); - if($inputgroup.find('select').size() >= 2) $inputgroup.removeClass('has-branch').find('select:last').remove(); - if($inputgroup.find('.chosen-container').size() >= 2) $inputgroup.find('.chosen-container:last').remove(); + if(!multiBranchProducts[$(product).val()]) + { + $tableRow.find('.table-col:last select').val('').trigger('chosen:updated'); + $tableRow.find('.table-col:last').addClass('hidden'); + } - var projectID = $('#project').val(); - if(typeof(projectID) == 'undefined') projectID = 0; - - var index = $inputgroup.find('select:first').attr('id').replace('products' , ''); - $.get(createLink('branch', 'ajaxGetBranches', "productID=" + $(product).val() + "&oldBranch=0¶m=active&projectID=" + projectID + "&withMainBranch=true"), function(data) + $.get(createLink('branch', 'ajaxGetBranches', "productID=" + $(product).val() + "&oldBranch=" + oldBranch + "¶m=active&projectID=" + projectID + "&withMainBranch=true"), function(data) { if(data) { - $inputgroup.addClass('has-branch').append(data); - $inputgroup.find('select:last').attr('name', 'branch[' + index + ']').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this.value)").chosen(); + $tableRow.find("select[name^='branch']").replaceWith(data); + $tableRow.find('.table-col:last .chosen-container').remove(); + $tableRow.find('.table-col:last').removeClass('hidden'); + $tableRow.find("select[name^='branch']").attr('multiple', '').attr('name', 'branch[' + index + '][]').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this)").chosen(); - $inputgroup.find('select:last').each(disableSelectedBranch); disableSelectedProduct(); } - var branchID = $('#branch' + index).val(); - loadPlans(product, branchID); + var branch = $('#branch' + index); + loadPlans(product, branch); }); - - if(!multiBranchProducts[$(product).val()]) disableSelectedProduct(); } /** - * Load plans by product id. + * Load plans. * - * @param int $product - * @param int $branchID + * @param obj $product + * @param obj $branchID * @access public * @return void */ -function loadPlans(product, branchID) +function loadPlans(product, branch) { - if($('#plansBox').size() == 0) return false; - var productID = $(product).val(); - var branchID = typeof(branchID) == 'undefined' ? 0 : branchID; + var branchID = $(branch).val() == null ? 0 : '0,' + $(branch).val(); + var planID = $(product).attr('data-plan') !== undefined ? $(product).attr('data-plan') : 0; var index = $(product).attr('id').replace('products', ''); - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=0&fieldID&needCreate=&expired=noclosed,unexpired¶m=skipParent,multiple'), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired,noclosed¶m=skipParent,multiple'), function(data) { if(data) { - if($("div#plan" + index).size() == 0) $("#plansBox .row").append('
'); - $("div#plan" + index).html(data).find('select').attr('name', 'plans[' + productID + '][' + branchID + '][]').attr('id', 'plans' + productID).chosen(); - - adjustPlanBoxMargin(); + $("div#plan" + index).find("select[name^='plans']").replaceWith(data); + $("div#plan" + index).find('.chosen-container').remove(); + $("div#plan" + index).find('select').attr('name', 'plans[' + productID + ']' + '[]').attr('id', 'plans' + productID).chosen(); } }); } /** - * Adjust product box margin. - * + * Add new line for link product. + * + * @param obj $obj * @access public * @return void */ -function adjustProductBoxMargin() +function addNewLine(obj) { - var productRows = Math.ceil($('#productsBox > .row > .col-sm-4').length / 3); - if(productRows > 1) + var newLine = $(obj).closest('tr').clone(); + var index = 0; + $(".productsBox select[name^='products']").each(function() { - for(i = 1; i <= productRows - 1; i++) - { - $('#productsBox .col-sm-4:lt(' + (i * 3) + ')').css('margin-bottom', '10px'); - } - } -} + var id = $(this).attr('id').replace('products' , ''); -/** - * Adjust plan box margin. - * - * @access public - * @return void - */ -function adjustPlanBoxMargin() -{ - var planRows = Math.ceil($('#plansBox > .row > .col-sm-4').length / 3); - if(planRows > 1) - { - for(j = 1; j <= planRows - 1; j++) - { - $('#plansBox .col-sm-4:lt(' + (j * 3) + ')').css('margin-bottom', '10px'); - } - } -} + id = parseInt(id); + id ++; -/* Auto compute the work days. */ -$(function() -{ - $(".date").bind('dateSelected', function() - { - computeWorkDays(this.id); + index = id > index ? id : index; }) -}); + + newLine.find('.newProduct').remove(); + newLine.find('.addProduct').remove(); + newLine.addClass('newLine'); + newLine.find('th').html(''); + newLine.find('.removeLine').css('visibility', 'visible'); + newLine.find('.chosen-container').remove(); + newLine.find('.productsBox .table-col:last').addClass('hidden'); + newLine.find("select[name^='products']").attr('name', 'products[' + index + ']').attr('id', 'products' + index).val('').chosen(); + newLine.find("select[name^='plans']").attr('name', 'plans[' + index + '][' + 0 + '][]').chosen(); + newLine.find("div[id^='plan']").attr('id', 'plan' + index); + + $(obj).closest('tr').after(newLine); + var product = newLine.find("select[name^='products']"); + var branch = newLine.find("select[name^='branch']"); + loadPlans(product, branch); + disableSelectedProduct(); +} + +function removeLine(obj) +{ + $(obj).closest('tr').remove(); + disableSelectedProduct(); +} $(function() { @@ -247,9 +265,6 @@ $(function() e.stopPropagation(); e.preventDefault(); }); - - adjustProductBoxMargin(); - adjustPlanBoxMargin(); }); /** diff --git a/module/execution/lang/de.php b/module/execution/lang/de.php index 48973a57cf..c5527bf317 100644 --- a/module/execution/lang/de.php +++ b/module/execution/lang/de.php @@ -209,82 +209,84 @@ $lang->execution->burnByList['estimate'] = "View by plan hours"; $lang->execution->burnByList['storyPoint'] = 'View by story point'; /* Method list. */ -$lang->execution->index = "Home"; -$lang->execution->task = 'Aufgaben'; -$lang->execution->groupTask = 'Nach Gruppen'; -$lang->execution->story = 'Storys'; -$lang->execution->qa = 'QA'; -$lang->execution->bug = 'Bugs'; -$lang->execution->testcase = 'Testcase List'; -$lang->execution->dynamic = 'Verlauf'; -$lang->execution->latestDynamic = 'Letzter Verlauf'; -$lang->execution->build = 'Builds'; -$lang->execution->testtask = 'Testaufgaben'; -$lang->execution->burn = 'Burndown'; -$lang->execution->computeBurn = 'Aktualisieren'; -$lang->execution->CFD = 'Cumulative Flow diagrams'; -$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; -$lang->execution->burnData = 'Burndown Daten'; -$lang->execution->fixFirst = 'Bearbeite Mannstunden des ersten Tags'; -$lang->execution->team = 'Teammitglieder'; -$lang->execution->doc = 'Dok'; -$lang->execution->doclib = 'Dok Bibliothek'; -$lang->execution->manageProducts = 'Verküpfe ' . $lang->productCommon; -$lang->execution->linkStory = 'Link Stories'; -$lang->execution->linkStoryByPlan = 'Verküpfe Story aus Plan'; -$lang->execution->linkPlan = 'Verküpfe Plan'; -$lang->execution->unlinkStoryTasks = 'Verknüpfung aufheben'; -$lang->execution->linkedProducts = 'Verküpfte Produkte'; -$lang->execution->unlinkedProducts = 'Produkt verknüpfung aufheben'; -$lang->execution->view = "Übersicht"; -$lang->execution->startAction = "Start Execution"; -$lang->execution->activateAction = "Activate Execution"; -$lang->execution->delayAction = "Delay Execution"; -$lang->execution->suspendAction = "Suspend Execution"; -$lang->execution->closeAction = "Close Execution"; -$lang->execution->testtaskAction = "Execution Request"; -$lang->execution->teamAction = "Execution Members"; -$lang->execution->kanbanAction = "Execution Kanban"; -$lang->execution->printKanbanAction = "Print Kanban"; -$lang->execution->treeAction = "Execution Tree View"; -$lang->execution->exportAction = "Export Execution"; -$lang->execution->computeBurnAction = "Compute Burn"; -$lang->execution->create = "Erstelle Projekt"; -$lang->execution->createExec = "Create Execution"; -$lang->execution->createAction = "Create {$lang->execution->common}"; -$lang->execution->copyExec = "Copy Execution"; -$lang->execution->copy = "Kopiere {$lang->executionCommon}"; -$lang->execution->delete = "Lösche"; -$lang->execution->deleteAB = "Lösche Execution"; -$lang->execution->browse = "Durchsuchen"; -$lang->execution->edit = "Bearbeiten"; -$lang->execution->editAction = "Edit Execution"; -$lang->execution->batchEdit = "Mehere bearbeiten"; -$lang->execution->batchEditAction = "Batch Edit"; -$lang->execution->manageMembers = 'Teams verwalten'; -$lang->execution->unlinkMember = 'Mitgliefer entfernen'; -$lang->execution->unlinkStory = 'Story entfernen'; -$lang->execution->unlinkStoryAB = 'Unlink'; -$lang->execution->batchUnlinkStory = 'Mehere Storys entfernen'; -$lang->execution->importTask = 'Importiere Aufgaben'; -$lang->execution->importPlanStories = 'Verknüpfe Story aus Plan'; -$lang->execution->importBug = 'Importiere Bugs'; -$lang->execution->tree = 'Baum'; -$lang->execution->treeTask = 'Aufgabe anzeigen'; -$lang->execution->treeStory = 'Story anzeigen'; -$lang->execution->treeViewTask = 'Tree View Task'; -$lang->execution->treeViewStory = 'Tree View Story'; -$lang->execution->storyKanban = 'Story Kanban'; -$lang->execution->storySort = 'Story sortieren'; -$lang->execution->importPlanStory = '' . $lang->executionCommon . ' wurde erstellt!\nMöchten Sie Storys aus dem Plan importieren?'; -$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; -$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; -$lang->execution->iteration = 'Iteration'; -$lang->execution->iterationInfo = '%s Iterationen'; -$lang->execution->viewAll = 'Alle anzeigen'; -$lang->execution->testreport = 'Test Report'; -$lang->execution->taskKanban = 'Task Kanban'; -$lang->execution->RDKanban = 'Research & Development Kanban'; +$lang->execution->index = "Home"; +$lang->execution->task = 'Aufgaben'; +$lang->execution->groupTask = 'Nach Gruppen'; +$lang->execution->story = 'Storys'; +$lang->execution->qa = 'QA'; +$lang->execution->bug = 'Bugs'; +$lang->execution->testcase = 'Testcase List'; +$lang->execution->dynamic = 'Verlauf'; +$lang->execution->latestDynamic = 'Letzter Verlauf'; +$lang->execution->build = 'Builds'; +$lang->execution->testtask = 'Testaufgaben'; +$lang->execution->burn = 'Burndown'; +$lang->execution->computeBurn = 'Aktualisieren'; +$lang->execution->CFD = 'Cumulative Flow diagrams'; +$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; +$lang->execution->burnData = 'Burndown Daten'; +$lang->execution->fixFirst = 'Bearbeite Mannstunden des ersten Tags'; +$lang->execution->team = 'Teammitglieder'; +$lang->execution->doc = 'Dok'; +$lang->execution->doclib = 'Dok Bibliothek'; +$lang->execution->manageProducts = 'Verküpfe ' . $lang->productCommon; +$lang->execution->linkStory = 'Link Stories'; +$lang->execution->linkStoryByPlan = 'Verküpfe Story aus Plan'; +$lang->execution->linkPlan = 'Verküpfe Plan'; +$lang->execution->unlinkStoryTasks = 'Verknüpfung aufheben'; +$lang->execution->linkedProducts = 'Verküpfte Produkte'; +$lang->execution->unlinkedProducts = 'Produkt verknüpfung aufheben'; +$lang->execution->view = "Übersicht"; +$lang->execution->startAction = "Start Execution"; +$lang->execution->activateAction = "Activate Execution"; +$lang->execution->delayAction = "Delay Execution"; +$lang->execution->suspendAction = "Suspend Execution"; +$lang->execution->closeAction = "Close Execution"; +$lang->execution->testtaskAction = "Execution Request"; +$lang->execution->teamAction = "Execution Members"; +$lang->execution->kanbanAction = "Execution Kanban"; +$lang->execution->printKanbanAction = "Print Kanban"; +$lang->execution->treeAction = "Execution Tree View"; +$lang->execution->exportAction = "Export Execution"; +$lang->execution->computeBurnAction = "Compute Burn"; +$lang->execution->create = "Erstelle Projekt"; +$lang->execution->createExec = "Create Execution"; +$lang->execution->createAction = "Create {$lang->execution->common}"; +$lang->execution->copyExec = "Copy Execution"; +$lang->execution->copy = "Kopiere {$lang->executionCommon}"; +$lang->execution->delete = "Lösche"; +$lang->execution->deleteAB = "Lösche Execution"; +$lang->execution->browse = "Durchsuchen"; +$lang->execution->edit = "Bearbeiten"; +$lang->execution->editAction = "Edit Execution"; +$lang->execution->batchEdit = "Mehere bearbeiten"; +$lang->execution->batchEditAction = "Batch Edit"; +$lang->execution->manageMembers = 'Teams verwalten'; +$lang->execution->unlinkMember = 'Mitgliefer entfernen'; +$lang->execution->unlinkStory = 'Story entfernen'; +$lang->execution->unlinkStoryAB = 'Unlink'; +$lang->execution->batchUnlinkStory = 'Mehere Storys entfernen'; +$lang->execution->importTask = 'Importiere Aufgaben'; +$lang->execution->importPlanStories = 'Verknüpfe Story aus Plan'; +$lang->execution->importBug = 'Importiere Bugs'; +$lang->execution->tree = 'Baum'; +$lang->execution->treeTask = 'Aufgabe anzeigen'; +$lang->execution->treeStory = 'Story anzeigen'; +$lang->execution->treeViewTask = 'Tree View Task'; +$lang->execution->treeViewStory = 'Tree View Story'; +$lang->execution->storyKanban = 'Story Kanban'; +$lang->execution->storySort = 'Story sortieren'; +$lang->execution->importPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importBranchPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->importBranchEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; +$lang->execution->iteration = 'Iteration'; +$lang->execution->iterationInfo = '%s Iterationen'; +$lang->execution->viewAll = 'Alle anzeigen'; +$lang->execution->testreport = 'Test Report'; +$lang->execution->taskKanban = 'Task Kanban'; +$lang->execution->RDKanban = 'Research & Development Kanban'; /* Group browsing. */ $lang->execution->allTasks = 'Alle'; @@ -349,7 +351,7 @@ $lang->execution->howToUpdateBurn = " Story verknüpfen。"; -$lang->execution->haveDraft = "There are %s draft stories can't be linked."; +$lang->execution->haveDraft = "There are %s draft stories or not associated with this {$lang->executionCommon} can't be linked."; $lang->execution->doneExecutions = 'Erledigt'; $lang->execution->selectDept = 'Abteilung wählen'; $lang->execution->selectDeptTitle = 'Abteilung wählen'; diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php index 45d52cf3bc..62c06847a5 100644 --- a/module/execution/lang/en.php +++ b/module/execution/lang/en.php @@ -209,82 +209,84 @@ $lang->execution->burnByList['estimate'] = "View by plan hours"; $lang->execution->burnByList['storyPoint'] = 'View by story point'; /* Method list. */ -$lang->execution->index = "{$lang->executionCommon} Home"; -$lang->execution->task = 'Task List'; -$lang->execution->groupTask = 'Group View'; -$lang->execution->story = 'Story List'; -$lang->execution->qa = 'QA'; -$lang->execution->bug = 'Bug List'; -$lang->execution->testcase = 'Testcase List'; -$lang->execution->dynamic = 'Dynamics'; -$lang->execution->latestDynamic = 'Dynamics'; -$lang->execution->build = 'Build List'; -$lang->execution->testtask = 'Request'; -$lang->execution->burn = 'Burndown'; -$lang->execution->computeBurn = 'Update'; -$lang->execution->CFD = 'Cumulative Flow diagrams'; -$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; -$lang->execution->burnData = 'Burndown Data'; -$lang->execution->fixFirst = 'Edit 1st-Day Estimates'; -$lang->execution->team = 'Members'; -$lang->execution->doc = 'Document'; -$lang->execution->doclib = 'Docoment Library'; -$lang->execution->manageProducts = 'Linked ' . $lang->productCommon . 's'; -$lang->execution->linkStory = 'Link Stories'; -$lang->execution->linkStoryByPlan = 'Link Stories By Plan'; -$lang->execution->linkPlan = 'Linked Plan'; -$lang->execution->unlinkStoryTasks = 'Unlink'; -$lang->execution->linkedProducts = "Linked {$lang->productCommon}s"; -$lang->execution->unlinkedProducts = "Unlinked {$lang->productCommon}s"; -$lang->execution->view = "Execution Detail"; -$lang->execution->startAction = "Start Execution"; -$lang->execution->activateAction = "Activate Execution"; -$lang->execution->delayAction = "Delay Execution"; -$lang->execution->suspendAction = "Suspend Execution"; -$lang->execution->closeAction = "Close Execution"; -$lang->execution->testtaskAction = "Execution Request"; -$lang->execution->teamAction = "Execution Members"; -$lang->execution->kanbanAction = "Execution Kanban"; -$lang->execution->printKanbanAction = "Print Kanban"; -$lang->execution->treeAction = "Execution Tree View"; -$lang->execution->exportAction = "Export Execution"; -$lang->execution->computeBurnAction = "Update Burndown"; -$lang->execution->create = "Create {$lang->executionCommon}"; -$lang->execution->createExec = "Create {$lang->execution->common}"; -$lang->execution->createAction = "Create {$lang->execution->common}"; -$lang->execution->copyExec = "Copy {$lang->execution->common}"; -$lang->execution->copy = "Copy {$lang->executionCommon}"; -$lang->execution->delete = "Delete {$lang->executionCommon}"; -$lang->execution->deleteAB = "Delete Execution"; -$lang->execution->browse = "{$lang->executionCommon} List"; -$lang->execution->edit = "Edit {$lang->executionCommon}"; -$lang->execution->editAction = "Edit Execution"; -$lang->execution->batchEdit = "Edit"; -$lang->execution->batchEditAction = "Batch Edit"; -$lang->execution->manageMembers = 'Manage Team'; -$lang->execution->unlinkMember = 'Remove Member'; -$lang->execution->unlinkStory = 'Unlink Story'; -$lang->execution->unlinkStoryAB = 'Unlink'; -$lang->execution->batchUnlinkStory = 'Batch Unlink Stories'; -$lang->execution->importTask = 'Transfer Task'; -$lang->execution->importPlanStories = 'Link Stories By Plan'; -$lang->execution->importBug = 'Import Bug'; -$lang->execution->tree = 'Tree'; -$lang->execution->treeTask = 'Show Task Only'; -$lang->execution->treeStory = 'Show Story Only'; -$lang->execution->treeViewTask = 'Tree View Task'; -$lang->execution->treeViewStory = 'Tree View Story'; -$lang->execution->storyKanban = 'Story Kanban'; -$lang->execution->storySort = 'Rank Story'; -$lang->execution->importPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; -$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; -$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; -$lang->execution->iteration = 'Iterations'; -$lang->execution->iterationInfo = '%s Iterations'; -$lang->execution->viewAll = 'View All'; -$lang->execution->testreport = 'Test Report'; -$lang->execution->taskKanban = 'Task Kanban'; -$lang->execution->RDKanban = 'Research & Development Kanban'; +$lang->execution->index = "{$lang->executionCommon} Home"; +$lang->execution->task = 'Task List'; +$lang->execution->groupTask = 'Group View'; +$lang->execution->story = 'Story List'; +$lang->execution->qa = 'QA'; +$lang->execution->bug = 'Bug List'; +$lang->execution->testcase = 'Testcase List'; +$lang->execution->dynamic = 'Dynamics'; +$lang->execution->latestDynamic = 'Dynamics'; +$lang->execution->build = 'Build List'; +$lang->execution->testtask = 'Request'; +$lang->execution->burn = 'Burndown'; +$lang->execution->computeBurn = 'Update'; +$lang->execution->CFD = 'Cumulative Flow diagrams'; +$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; +$lang->execution->burnData = 'Burndown Data'; +$lang->execution->fixFirst = 'Edit 1st-Day Estimates'; +$lang->execution->team = 'Members'; +$lang->execution->doc = 'Document'; +$lang->execution->doclib = 'Docoment Library'; +$lang->execution->manageProducts = 'Linked ' . $lang->productCommon . 's'; +$lang->execution->linkStory = 'Link Stories'; +$lang->execution->linkStoryByPlan = 'Link Stories By Plan'; +$lang->execution->linkPlan = 'Linked Plan'; +$lang->execution->unlinkStoryTasks = 'Unlink'; +$lang->execution->linkedProducts = "Linked {$lang->productCommon}s"; +$lang->execution->unlinkedProducts = "Unlinked {$lang->productCommon}s"; +$lang->execution->view = "Execution Detail"; +$lang->execution->startAction = "Start Execution"; +$lang->execution->activateAction = "Activate Execution"; +$lang->execution->delayAction = "Delay Execution"; +$lang->execution->suspendAction = "Suspend Execution"; +$lang->execution->closeAction = "Close Execution"; +$lang->execution->testtaskAction = "Execution Request"; +$lang->execution->teamAction = "Execution Members"; +$lang->execution->kanbanAction = "Execution Kanban"; +$lang->execution->printKanbanAction = "Print Kanban"; +$lang->execution->treeAction = "Execution Tree View"; +$lang->execution->exportAction = "Export Execution"; +$lang->execution->computeBurnAction = "Update Burndown"; +$lang->execution->create = "Create {$lang->executionCommon}"; +$lang->execution->createExec = "Create {$lang->execution->common}"; +$lang->execution->createAction = "Create {$lang->execution->common}"; +$lang->execution->copyExec = "Copy {$lang->execution->common}"; +$lang->execution->copy = "Copy {$lang->executionCommon}"; +$lang->execution->delete = "Delete {$lang->executionCommon}"; +$lang->execution->deleteAB = "Delete Execution"; +$lang->execution->browse = "{$lang->executionCommon} List"; +$lang->execution->edit = "Edit {$lang->executionCommon}"; +$lang->execution->editAction = "Edit Execution"; +$lang->execution->batchEdit = "Edit"; +$lang->execution->batchEditAction = "Batch Edit"; +$lang->execution->manageMembers = 'Manage Team'; +$lang->execution->unlinkMember = 'Remove Member'; +$lang->execution->unlinkStory = 'Unlink Story'; +$lang->execution->unlinkStoryAB = 'Unlink'; +$lang->execution->batchUnlinkStory = 'Batch Unlink Stories'; +$lang->execution->importTask = 'Transfer Task'; +$lang->execution->importPlanStories = 'Link Stories By Plan'; +$lang->execution->importBug = 'Import Bug'; +$lang->execution->tree = 'Tree'; +$lang->execution->treeTask = 'Show Task Only'; +$lang->execution->treeStory = 'Show Story Only'; +$lang->execution->treeViewTask = 'Tree View Task'; +$lang->execution->treeViewStory = 'Tree View Story'; +$lang->execution->storyKanban = 'Story Kanban'; +$lang->execution->storySort = 'Rank Story'; +$lang->execution->importPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importBranchPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->importBranchEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; +$lang->execution->iteration = 'Iterations'; +$lang->execution->iterationInfo = '%s Iterations'; +$lang->execution->viewAll = 'View All'; +$lang->execution->testreport = 'Test Report'; +$lang->execution->taskKanban = 'Task Kanban'; +$lang->execution->RDKanban = 'Research & Development Kanban'; /* Group browsing. */ $lang->execution->allTasks = 'All'; @@ -349,7 +351,7 @@ $lang->execution->howToUpdateBurn = " Link Stories now."; -$lang->execution->haveDraft = "%s stories in draft, so they can't be linked."; +$lang->execution->haveDraft = "There are %s draft stories or not associated with this {$lang->executionCommon} can't be linked."; $lang->execution->doneExecutions = 'Finished'; $lang->execution->selectDept = 'Select Department'; $lang->execution->selectDeptTitle = 'Select User'; diff --git a/module/execution/lang/fr.php b/module/execution/lang/fr.php index 5b39ac9731..434e14a929 100644 --- a/module/execution/lang/fr.php +++ b/module/execution/lang/fr.php @@ -209,82 +209,84 @@ $lang->execution->burnByList['estimate'] = "View by plan hours"; $lang->execution->burnByList['storyPoint'] = 'View by story point'; /* Method list. */ -$lang->execution->index = "Accueil {$lang->executionCommon}"; -$lang->execution->task = 'Liste Tâches'; -$lang->execution->groupTask = 'Vision Groupée'; -$lang->execution->story = 'Liste Stories'; -$lang->execution->qa = 'QA'; -$lang->execution->bug = 'Liste Bugs'; -$lang->execution->testcase = 'Testcase List'; -$lang->execution->dynamic = 'Historique'; -$lang->execution->latestDynamic = 'Historique'; -$lang->execution->build = 'Liste Builds'; -$lang->execution->testtask = 'Recette'; -$lang->execution->burn = ' Atterrissage'; -$lang->execution->computeBurn = 'Calculer'; -$lang->execution->CFD = 'Cumulative Flow diagrams'; -$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; -$lang->execution->burnData = "Données d'atterrissage"; -$lang->execution->fixFirst = 'Fixer 1er-Jour Estimation'; -$lang->execution->team = 'Membres'; -$lang->execution->doc = 'Documents'; -$lang->execution->doclib = 'Répertoire de Documents'; -$lang->execution->manageProducts = 'Liaisons du ' . $lang->executionCommon . ' avec les ' . $lang->productCommon . 's'; -$lang->execution->linkStory = 'Stories liées'; -$lang->execution->linkStoryByPlan = 'Stories liées par Plan'; -$lang->execution->linkPlan = 'Plans liés'; -$lang->execution->unlinkStoryTasks = 'Dissocier'; -$lang->execution->linkedProducts = "{$lang->productCommon}s liés à ce {$lang->executionCommon}"; -$lang->execution->unlinkedProducts = "{$lang->productCommon}s dissociés de ce {$lang->executionCommon}"; -$lang->execution->view = "Détail du Execution"; -$lang->execution->startAction = "Start Execution"; -$lang->execution->activateAction = "Activer le Execution"; -$lang->execution->delayAction = "Ajourner le Execution"; -$lang->execution->suspendAction = "Suspendre le Execution"; -$lang->execution->closeAction = "Fermer le Execution"; -$lang->execution->testtaskAction = "Recettes du Execution"; -$lang->execution->teamAction = "Membres du Execution"; -$lang->execution->kanbanAction = "Kaban Execution"; -$lang->execution->printKanbanAction = "Imprimer le Kanban"; -$lang->execution->treeAction = "Arborescence Execution"; -$lang->execution->exportAction = "Exporter Execution"; -$lang->execution->computeBurnAction = "Calculer Atterrissage"; -$lang->execution->create = "Créer {$lang->executionCommon}"; -$lang->execution->createExec = "Create Execution"; -$lang->execution->createAction = "Create {$lang->execution->common}"; -$lang->execution->copyExec = "Copy Execution"; -$lang->execution->copy = "Copier {$lang->executionCommon}"; -$lang->execution->delete = "Supprimer {$lang->executionCommon}"; -$lang->execution->deleteAB = "Delete Execution"; -$lang->execution->browse = "Liste du {$lang->executionCommon}"; -$lang->execution->edit = "Editer {$lang->executionCommon}"; -$lang->execution->editAction = "Edit Execution"; -$lang->execution->batchEdit = "Edition par lot"; -$lang->execution->batchEditAction = "Batch Edit"; -$lang->execution->manageMembers = 'Organiser Equipe'; -$lang->execution->unlinkMember = 'Retirer le membre'; -$lang->execution->unlinkStory = 'Dissocier Story'; -$lang->execution->unlinkStoryAB = 'Dissocier'; -$lang->execution->batchUnlinkStory = 'Dissocier Stories par lot'; -$lang->execution->importTask = 'Transfert Tâche'; -$lang->execution->importPlanStories = 'Lier Stories Par Plan'; -$lang->execution->importBug = 'Importer Bug'; -$lang->execution->tree = 'Arboressence'; -$lang->execution->treeTask = 'Seulement les Tâches'; -$lang->execution->treeStory = 'Seulement les Stories'; -$lang->execution->treeViewTask = 'Seulement les Tâches'; -$lang->execution->treeViewStory = 'Seulement les Stories'; -$lang->execution->storyKanban = 'Story Kanban'; -$lang->execution->storySort = 'Rang Story'; -$lang->execution->importPlanStory = $lang->executionCommon . ' est créé!\nVoulez-vous importer des stories qui ont été ajoutées au Plan ?'; -$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; -$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; -$lang->execution->iteration = 'Itérations'; -$lang->execution->iterationInfo = '%s Itérations'; -$lang->execution->viewAll = 'Voir Tout'; -$lang->execution->testreport = 'Test Report'; -$lang->execution->taskKanban = 'Task Kanban'; -$lang->execution->RDKanban = 'Research & Development Kanban'; +$lang->execution->index = "{$lang->executionCommon} Home"; +$lang->execution->task = 'Task List'; +$lang->execution->groupTask = 'Group View'; +$lang->execution->story = 'Story List'; +$lang->execution->qa = 'QA'; +$lang->execution->bug = 'Bug List'; +$lang->execution->testcase = 'Testcase List'; +$lang->execution->dynamic = 'Dynamics'; +$lang->execution->latestDynamic = 'Dynamics'; +$lang->execution->build = 'Build List'; +$lang->execution->testtask = 'Request'; +$lang->execution->burn = 'Burndown'; +$lang->execution->computeBurn = 'Update'; +$lang->execution->CFD = 'Cumulative Flow diagrams'; +$lang->execution->computeCFD = 'Compute Cumulative Flow diagrams'; +$lang->execution->burnData = 'Burndown Data'; +$lang->execution->fixFirst = 'Edit 1st-Day Estimates'; +$lang->execution->team = 'Members'; +$lang->execution->doc = 'Document'; +$lang->execution->doclib = 'Docoment Library'; +$lang->execution->manageProducts = 'Linked ' . $lang->productCommon . 's'; +$lang->execution->linkStory = 'Link Stories'; +$lang->execution->linkStoryByPlan = 'Link Stories By Plan'; +$lang->execution->linkPlan = 'Linked Plan'; +$lang->execution->unlinkStoryTasks = 'Unlink'; +$lang->execution->linkedProducts = "Linked {$lang->productCommon}s"; +$lang->execution->unlinkedProducts = "Unlinked {$lang->productCommon}s"; +$lang->execution->view = "Execution Detail"; +$lang->execution->startAction = "Start Execution"; +$lang->execution->activateAction = "Activate Execution"; +$lang->execution->delayAction = "Delay Execution"; +$lang->execution->suspendAction = "Suspend Execution"; +$lang->execution->closeAction = "Close Execution"; +$lang->execution->testtaskAction = "Execution Request"; +$lang->execution->teamAction = "Execution Members"; +$lang->execution->kanbanAction = "Execution Kanban"; +$lang->execution->printKanbanAction = "Print Kanban"; +$lang->execution->treeAction = "Execution Tree View"; +$lang->execution->exportAction = "Export Execution"; +$lang->execution->computeBurnAction = "Update Burndown"; +$lang->execution->create = "Create {$lang->executionCommon}"; +$lang->execution->createExec = "Create {$lang->execution->common}"; +$lang->execution->createAction = "Create {$lang->execution->common}"; +$lang->execution->copyExec = "Copy {$lang->execution->common}"; +$lang->execution->copy = "Copy {$lang->executionCommon}"; +$lang->execution->delete = "Delete {$lang->executionCommon}"; +$lang->execution->deleteAB = "Delete Execution"; +$lang->execution->browse = "{$lang->executionCommon} List"; +$lang->execution->edit = "Edit {$lang->executionCommon}"; +$lang->execution->editAction = "Edit Execution"; +$lang->execution->batchEdit = "Edit"; +$lang->execution->batchEditAction = "Batch Edit"; +$lang->execution->manageMembers = 'Manage Team'; +$lang->execution->unlinkMember = 'Remove Member'; +$lang->execution->unlinkStory = 'Unlink Story'; +$lang->execution->unlinkStoryAB = 'Unlink'; +$lang->execution->batchUnlinkStory = 'Batch Unlink Stories'; +$lang->execution->importTask = 'Transfer Task'; +$lang->execution->importPlanStories = 'Link Stories By Plan'; +$lang->execution->importBug = 'Import Bug'; +$lang->execution->tree = 'Tree'; +$lang->execution->treeTask = 'Show Task Only'; +$lang->execution->treeStory = 'Show Story Only'; +$lang->execution->treeViewTask = 'Tree View Task'; +$lang->execution->treeViewStory = 'Tree View Story'; +$lang->execution->storyKanban = 'Story Kanban'; +$lang->execution->storySort = 'Rank Story'; +$lang->execution->importPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? The stories in the draft will be automatically filtered out when imported.'; +$lang->execution->importBranchPlanStory = $lang->executionCommon . ' is created!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->importBranchEditPlanStory = $lang->executionCommon . ' is edited!\nDo you want to import stories that have been linked to the plan? Only the activation stories of the branch associated with this ' .$lang->executionCommon. ' will be associated with the import'; +$lang->execution->needLinkProducts = 'The execution has not been linked with any product, and the related functions cannot be used. Please link the product first and try again.'; +$lang->execution->iteration = 'Iterations'; +$lang->execution->iterationInfo = '%s Iterations'; +$lang->execution->viewAll = 'View All'; +$lang->execution->testreport = 'Test Report'; +$lang->execution->taskKanban = 'Task Kanban'; +$lang->execution->RDKanban = 'Research & Development Kanban'; /* Group browsing. */ $lang->execution->allTasks = 'Toutes'; @@ -349,7 +351,7 @@ $lang->execution->howToUpdateBurn = "executionCommon} qui sont associées à {$lang->productCommon} et vérifiez qu'elles ont bien été validées."; $lang->execution->projectNoStories = "No story can be linked. Please check whether there is any story in project and make sure it has been reviewed."; $lang->execution->productStories = "Les stories associées au {$lang->executionCommon} sont une portion des stories associées au {$lang->productCommon}. Les stories ne peuvent être associées à un {$lang->executionCommon} qu'après avoir été validées. Associer Stories maintenant."; -$lang->execution->haveDraft = "%s stories sont encore en conception, elles ne peuvent pas être associées au {$lang->executionCommon} actuellement."; +$lang->execution->haveDraft = "There are %s draft stories or not associated with this {$lang->executionCommon} can't be linked."; $lang->execution->doneExecutions = 'Terminé'; $lang->execution->selectDept = 'Sélection Compartiment'; $lang->execution->selectDeptTitle = 'Sélection Utilisateur'; diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php index 3ff779523e..d512818ee7 100644 --- a/module/execution/lang/zh-cn.php +++ b/module/execution/lang/zh-cn.php @@ -209,82 +209,84 @@ $lang->execution->burnByList['estimate'] = "按计划工时查看"; $lang->execution->burnByList['storyPoint'] = '按故事点查看'; /* 方法列表。*/ -$lang->execution->index = "{$lang->execution->common}主页"; -$lang->execution->task = '任务列表'; -$lang->execution->groupTask = '分组浏览任务'; -$lang->execution->story = "{$lang->SRCommon}列表"; -$lang->execution->qa = '测试仪表盘'; -$lang->execution->bug = 'Bug列表'; -$lang->execution->testcase = '用例列表'; -$lang->execution->dynamic = '动态'; -$lang->execution->latestDynamic = '最新动态'; -$lang->execution->build = '所有版本'; -$lang->execution->testtask = '测试单'; -$lang->execution->burn = '燃尽图'; -$lang->execution->computeBurn = '更新燃尽图'; -$lang->execution->CFD = '累积流图'; -$lang->execution->computeCFD = '更新累积流图'; -$lang->execution->burnData = '燃尽图数据'; -$lang->execution->fixFirst = '修改首天工时'; -$lang->execution->team = '团队成员'; -$lang->execution->doc = '文档列表'; -$lang->execution->doclib = '文档库列表'; -$lang->execution->manageProducts = '关联' . $lang->productCommon; -$lang->execution->linkStory = "关联{$lang->SRCommon}"; -$lang->execution->linkStoryByPlan = "按照计划关联"; -$lang->execution->linkPlan = "关联计划"; -$lang->execution->unlinkStoryTasks = "未关联{$lang->SRCommon}任务"; -$lang->execution->linkedProducts = '已关联'; -$lang->execution->unlinkedProducts = '未关联'; -$lang->execution->view = "{$lang->execution->common}概况"; -$lang->execution->startAction = "开始{$lang->execution->common}"; -$lang->execution->activateAction = "激活{$lang->execution->common}"; -$lang->execution->delayAction = "延期{$lang->execution->common}"; -$lang->execution->suspendAction = "挂起{$lang->execution->common}"; -$lang->execution->closeAction = "关闭{$lang->execution->common}"; -$lang->execution->testtaskAction = "{$lang->execution->common}测试单"; -$lang->execution->teamAction = "{$lang->execution->common}团队"; -$lang->execution->kanbanAction = "{$lang->execution->common}看板"; -$lang->execution->printKanbanAction = "打印看板"; -$lang->execution->treeAction = "{$lang->execution->common}树状图"; -$lang->execution->exportAction = "导出{$lang->execution->common}"; -$lang->execution->computeBurnAction = "计算燃尽图"; -$lang->execution->create = "添加{$lang->executionCommon}"; -$lang->execution->createExec = "添加{$lang->execution->common}"; -$lang->execution->createAction = "添加{$lang->execution->common}"; -$lang->execution->copyExec = "复制{$lang->execution->common}"; -$lang->execution->copy = "复制{$lang->executionCommon}"; -$lang->execution->delete = "删除{$lang->executionCommon}"; -$lang->execution->deleteAB = "删除{$lang->execution->common}"; -$lang->execution->browse = "浏览{$lang->execution->common}"; -$lang->execution->edit = "设置{$lang->executionCommon}"; -$lang->execution->editAction = "编辑{$lang->execution->common}"; -$lang->execution->batchEdit = "编辑"; -$lang->execution->batchEditAction = "批量编辑"; -$lang->execution->manageMembers = '团队管理'; -$lang->execution->unlinkMember = '移除成员'; -$lang->execution->unlinkStory = "移除{$lang->SRCommon}"; -$lang->execution->unlinkStoryAB = "移除{$lang->SRCommon}"; -$lang->execution->batchUnlinkStory = "批量移除{$lang->SRCommon}"; -$lang->execution->importTask = '转入任务'; -$lang->execution->importPlanStories = "按计划关联{$lang->SRCommon}"; -$lang->execution->importBug = '导入Bug'; -$lang->execution->tree = '树状图'; -$lang->execution->treeTask = '只看任务'; -$lang->execution->treeStory = "只看{$lang->SRCommon}"; -$lang->execution->treeViewTask = '树状图查看任务'; -$lang->execution->treeViewStory = "树状图查看{$lang->SRCommon}"; -$lang->execution->storyKanban = "{$lang->SRCommon}看板"; -$lang->execution->storySort = "{$lang->SRCommon}排序"; -$lang->execution->importPlanStory = '创建' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将自动过滤掉草稿状态的' . $lang->SRCommon . '。'; -$lang->execution->importEditPlanStory = '编辑' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将自动过滤掉草稿状态的' . $lang->SRCommon . '。'; -$lang->execution->needLinkProducts = '该执行还未关联任何产品,相关功能无法使用,请先关联产品后再试。'; -$lang->execution->iteration = '版本迭代'; -$lang->execution->iterationInfo = '迭代%s次'; -$lang->execution->viewAll = '查看所有'; -$lang->execution->testreport = '测试报告'; -$lang->execution->taskKanban = '任务看板'; -$lang->execution->RDKanban = '研发看板'; +$lang->execution->index = "{$lang->execution->common}主页"; +$lang->execution->task = '任务列表'; +$lang->execution->groupTask = '分组浏览任务'; +$lang->execution->story = "{$lang->SRCommon}列表"; +$lang->execution->qa = '测试仪表盘'; +$lang->execution->bug = 'Bug列表'; +$lang->execution->testcase = '用例列表'; +$lang->execution->dynamic = '动态'; +$lang->execution->latestDynamic = '最新动态'; +$lang->execution->build = '所有版本'; +$lang->execution->testtask = '测试单'; +$lang->execution->burn = '燃尽图'; +$lang->execution->computeBurn = '更新燃尽图'; +$lang->execution->CFD = '累积流图'; +$lang->execution->computeCFD = '更新累积流图'; +$lang->execution->burnData = '燃尽图数据'; +$lang->execution->fixFirst = '修改首天工时'; +$lang->execution->team = '团队成员'; +$lang->execution->doc = '文档列表'; +$lang->execution->doclib = '文档库列表'; +$lang->execution->manageProducts = '关联' . $lang->productCommon; +$lang->execution->linkStory = "关联{$lang->SRCommon}"; +$lang->execution->linkStoryByPlan = "按照计划关联"; +$lang->execution->linkPlan = "关联计划"; +$lang->execution->unlinkStoryTasks = "未关联{$lang->SRCommon}任务"; +$lang->execution->linkedProducts = '已关联'; +$lang->execution->unlinkedProducts = '未关联'; +$lang->execution->view = "{$lang->execution->common}概况"; +$lang->execution->startAction = "开始{$lang->execution->common}"; +$lang->execution->activateAction = "激活{$lang->execution->common}"; +$lang->execution->delayAction = "延期{$lang->execution->common}"; +$lang->execution->suspendAction = "挂起{$lang->execution->common}"; +$lang->execution->closeAction = "关闭{$lang->execution->common}"; +$lang->execution->testtaskAction = "{$lang->execution->common}测试单"; +$lang->execution->teamAction = "{$lang->execution->common}团队"; +$lang->execution->kanbanAction = "{$lang->execution->common}看板"; +$lang->execution->printKanbanAction = "打印看板"; +$lang->execution->treeAction = "{$lang->execution->common}树状图"; +$lang->execution->exportAction = "导出{$lang->execution->common}"; +$lang->execution->computeBurnAction = "计算燃尽图"; +$lang->execution->create = "添加{$lang->executionCommon}"; +$lang->execution->createExec = "添加{$lang->execution->common}"; +$lang->execution->createAction = "添加{$lang->execution->common}"; +$lang->execution->copyExec = "复制{$lang->execution->common}"; +$lang->execution->copy = "复制{$lang->executionCommon}"; +$lang->execution->delete = "删除{$lang->executionCommon}"; +$lang->execution->deleteAB = "删除{$lang->execution->common}"; +$lang->execution->browse = "浏览{$lang->execution->common}"; +$lang->execution->edit = "设置{$lang->executionCommon}"; +$lang->execution->editAction = "编辑{$lang->execution->common}"; +$lang->execution->batchEdit = "编辑"; +$lang->execution->batchEditAction = "批量编辑"; +$lang->execution->manageMembers = '团队管理'; +$lang->execution->unlinkMember = '移除成员'; +$lang->execution->unlinkStory = "移除{$lang->SRCommon}"; +$lang->execution->unlinkStoryAB = "移除{$lang->SRCommon}"; +$lang->execution->batchUnlinkStory = "批量移除{$lang->SRCommon}"; +$lang->execution->importTask = '转入任务'; +$lang->execution->importPlanStories = "按计划关联{$lang->SRCommon}"; +$lang->execution->importBug = '导入Bug'; +$lang->execution->tree = '树状图'; +$lang->execution->treeTask = '只看任务'; +$lang->execution->treeStory = "只看{$lang->SRCommon}"; +$lang->execution->treeViewTask = '树状图查看任务'; +$lang->execution->treeViewStory = "树状图查看{$lang->SRCommon}"; +$lang->execution->storyKanban = "{$lang->SRCommon}看板"; +$lang->execution->storySort = "{$lang->SRCommon}排序"; +$lang->execution->importPlanStory = '创建' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将自动过滤掉草稿状态的' . $lang->SRCommon . '。'; +$lang->execution->importEditPlanStory = '编辑' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将自动过滤掉草稿状态的' . $lang->SRCommon . '。'; +$lang->execution->importBranchPlanStory = '创建' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将只关联本' . $lang->executionCommon . '所关联分支的激活需求。'; +$lang->execution->importBranchEditPlanStory = '编辑' . $lang->executionCommon . '成功!\n是否导入计划关联的相关' . $lang->SRCommon . '?导入时将只关联本' . $lang->executionCommon . '所关联分支的激活需求。'; +$lang->execution->needLinkProducts = '该执行还未关联任何产品,相关功能无法使用,请先关联产品后再试。'; +$lang->execution->iteration = '版本迭代'; +$lang->execution->iterationInfo = '迭代%s次'; +$lang->execution->viewAll = '查看所有'; +$lang->execution->testreport = '测试报告'; +$lang->execution->taskKanban = '任务看板'; +$lang->execution->RDKanban = '研发看板'; /* 分组浏览。*/ $lang->execution->allTasks = '所有'; @@ -349,7 +351,7 @@ $lang->execution->howToUpdateBurn = "关联{$lang->SRCommon}。"; -$lang->execution->haveDraft = "有%s条草稿状态的{$lang->SRCommon}无法关联到该{$lang->executionCommon}"; +$lang->execution->haveDraft = "有%s条草稿状态或非本{$lang->executionCommon}关联分支的{$lang->SRCommon}无法关联到该{$lang->executionCommon}"; $lang->execution->doneExecutions = '已结束'; $lang->execution->selectDept = '选择部门'; $lang->execution->selectDeptTitle = '选择一个部门的成员'; diff --git a/module/execution/model.php b/module/execution/model.php index ed66a03b88..ac9033160a 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -332,6 +332,20 @@ class executionModel extends model $this->checkBeginAndEndDate($_POST['project'], $_POST['begin'], $_POST['end']); if(dao::isError()) return false; + if($_POST['products']) + { + $this->app->loadLang('project'); + $multipleProducts = $this->loadModel('product')->getMultiBranchPairs(); + foreach($_POST['products'] as $index => $productID) + { + if(isset($multipleProducts[$productID]) and empty($_POST['branch'][$index])) + { + dao::$errors[] = $this->lang->project->emptyBranch; + return false; + } + } + } + /* Determine whether to add a sprint or a stage according to the model of the execution. */ $project = $this->loadModel('project')->getByID($_POST['project']); $type = 'sprint'; @@ -514,6 +528,20 @@ class executionModel extends model return false; } + if($_POST['products']) + { + $this->app->loadLang('project'); + $multipleProducts = $this->loadModel('product')->getMultiBranchPairs(); + foreach($_POST['products'] as $index => $productID) + { + if(isset($multipleProducts[$productID]) and empty($_POST['branch'][$index])) + { + dao::$errors[] = $this->lang->project->emptyBranch; + return false; + } + } + } + /* Get the data from the post. */ $execution = fixer::input('post') ->add('id', $executionID) @@ -2370,23 +2398,28 @@ class executionModel extends model $oldPlan = 0; $branch = isset($branches[$i]) ? $branches[$i] : 0; - if(isset($existedProducts[$productID][$branch])) continue; + if(!is_array($branch)) $branch = array($branch); - if(isset($oldProducts[$productID][$branch])) + foreach($branch as $branchID) { - $oldProduct = $oldProducts[$productID][$branch]; - if($this->app->rawMethod != 'edit') $oldPlan = $oldProduct->plan; - } + if(isset($existedProducts[$productID][$branchID])) continue; - $data = new stdclass(); - $data->project = $executionID; - $data->product = $productID; - $data->branch = $branch; - $data->plan = isset($plans[$productID][$branch]) ? implode(',', $plans[$productID][$branch]) : $oldPlan; - $data->plan = trim($data->plan, ','); - $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; - $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); - $existedProducts[$productID][$branch] = true; + if(isset($oldProducts[$productID][$branchID])) + { + $oldProduct = $oldProducts[$productID][$branchID]; + if($this->app->rawMethod != 'edit') $oldPlan = $oldProduct->plan; + } + + $data = new stdclass(); + $data->project = $executionID; + $data->product = $productID; + $data->branch = $branchID; + $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : $oldPlan; + $data->plan = trim($data->plan, ','); + $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; + $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); + $existedProducts[$productID][$branchID] = true; + } } $oldProductKeys = array_keys($oldProducts); @@ -2882,19 +2915,21 @@ class executionModel extends model */ public function linkStories($executionID) { - $plans = $this->dao->select('plan')->from(TABLE_PROJECTPRODUCT) + $plans = $this->dao->select('product, plan')->from(TABLE_PROJECTPRODUCT) ->where('project')->eq($executionID) - ->fetchPairs('plan'); + ->fetchPairs('product', 'plan'); $planStories = array(); $planProducts = array(); $this->loadModel('story'); if(!empty($plans)) { - foreach($plans as $planIdList) + $executionProducts = $this->loadModel('project')->getBranchesByProject($executionID); + foreach($plans as $productID => $planIdList) { if(empty($planIdList)) continue; $planIdList = explode(',', $planIdList); + $executionBranches = zget($executionProducts, $productID, array()); foreach($planIdList as $planID) { $planStory = $this->story->getPlanStories($planID); @@ -2902,7 +2937,7 @@ class executionModel extends model { foreach($planStory as $id => $story) { - if($story->status == 'draft' or $story->status == 'reviewing') + if($story->status == 'draft' or $story->status == 'reviewing' or (!empty($story->branch) and !empty($executionBranches) and !isset($executionBranches[$story->branch]))) { unset($planStory[$id]); continue; @@ -4623,17 +4658,36 @@ class executionModel extends model $branchGroups = $this->getBranchByProduct(array_keys($products), $executionID, 'noclosed'); foreach($branchGroups as $branches) { - foreach($branches as $branchID => $branchName) $branchIdList[$branchID] = $branchID; + foreach($branches as $branchID => $branchName) $branchIdList[] = $branchID; } - $plans = $this->dao->select('id,title,product,parent,begin,end')->from(TABLE_PRODUCTPLAN) - ->where('product')->in(array_keys($products)) - ->andWhere('deleted')->eq(0) - ->andWhere('branch')->in($branchIdList)->fi() - ->orderBy('begin desc') + $branchQuery = '('; + if(!empty($branchIdList)) + { + $branchCount = count($branchIdList); + foreach($branchIdList as $index => $branchID) + { + $branchQuery .= "FIND_IN_SET('$branchID', branch)"; + if($index < $branchCount - 1) $branchQuery .= ' OR '; + } + } + else + { + $branchQuery .= "FIND_IN_SET('0', branch)"; + } + + $branchQuery .= ')'; + + $plans = $this->dao->select('t1.id,t1.title,t1.product,t1.parent,t1.begin,t1.end,t1.branch,t2.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product') + ->where('t1.product')->in(array_keys($products)) + ->andWhere('t1.deleted')->eq(0) + ->andWhere($branchQuery) + ->orderBy('t1.begin desc') ->fetchAll('id'); $plans = $this->productplan->reorder4Children($plans); + $plans = $this->productplan->relationBranch($plans); $productPlans = array(); foreach($plans as $plan) { @@ -4641,6 +4695,7 @@ class executionModel extends model if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $productPlans[$plan->product][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == '2030-01-01' and $plan->end == '2030-01-01') $productPlans[$plan->product][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; + if($plan->productType != 'normal') $productPlans[$plan->product][$plan->id] = $productPlans[$plan->product][$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); } return $productPlans; diff --git a/module/execution/view/create.html.php b/module/execution/view/create.html.php index 902cd65b1d..9fa8689fa2 100644 --- a/module/execution/view/create.html.php +++ b/module/execution/view/create.html.php @@ -130,62 +130,81 @@ printExtendFields('', 'table', 'columns=3');?> hasProduct)) $hidden = ''?> + + + - execution->manageProducts;?> - + project->manageProductPlan;?> +
- - - - type != 'normal' and isset($branchGroups[$product->id]));?> - id] as $branchID => $branch):?> -
-
- id, "class='form-control chosen' $class onchange='loadBranches(this)' data-last='" . $product->id . "'");?> - id);?> - - id], $branchID, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?> +
+
+
+ type != 'normal' and isset($branchGroups[$product->id]);?> +
'> + product->common;?> + id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='" . $product->type . "'");?> +
+
+
'> +
+ product->branchName['branch'];?> + branches);?> + id]) ? $branchGroups[$product->id] : array(), $branchIdList, "class='form-control chosen' multiple onchange=\"loadPlans('#products{$i}', this)\"");?> +
+
- - - - model) and $project->model == 'scrum') or empty($products)):?> -
-
- - +
+
> + product->plan;?> + id][]", isset($productPlans[$product->id]) ? $productPlans[$product->id] : array(), $product->plans, "class='form-control chosen' multiple");?> +
+ + > +
-
- - model) and !empty($project->hasProduct) and $project->model == 'scrum') $hidden = '';?> - - execution->linkPlan;?> - + + + + + project->manageProductPlan;?> +
- begin)):?> -
product}][{$plan->branch}][]", $productPlan, $plan->id, "class='form-control chosen' multiple");?>
- id)?> - - - - id] as $branchID => $branch):?> - id][$branchID]) ? $productPlans[$product->id][$branchID] : array();?> -
id}][$branchID][]", $plans, $branches[$product->id][$branchID]->plan, "class='form-control chosen' multiple");?>
- - - - -
- - +
+
+
+
+ product->common;?> + +
+
+ +
+
+
+
+ product->plan;?> + +
+ + +
+
+
+ execution->teamname;?> diff --git a/module/execution/view/edit.html.php b/module/execution/view/edit.html.php index 0f6507834d..f2eaf83b23 100644 --- a/module/execution/view/edit.html.php +++ b/module/execution/view/edit.html.php @@ -141,56 +141,81 @@ attribute, array('request', 'design', 'review'))): ?> hasProduct)) $hidden = ''?> + + + - execution->manageProducts;?> - - grade == 2 or $execution->type == 'stage') ? "disabled" : '';?> + project->manageProductPlan;?> +
- - - type != 'normal' and isset($branchGroups[$product->id]);?> - id] as $branchID => $branch):?> -
-
- id, "class='form-control chosen' $class onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='". $product->type ."' data-lastBranch='" . $branchID . "'");?> - - id], $branchID, "class='form-control chosen' $class onchange=\"loadPlans('#products{$i}', this.value)\" data-last='" . $branchID . "'");?> +
+
+
+ type != 'normal' and isset($branchGroups[$product->id]);?> +
'> + product->common;?> + id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='" . $product->type . "'");?> +
+
+
'> +
+ product->branchName['branch'];?> + branches);?> + id]) ? $branchGroups[$product->id] : array(), $branchIdList, "class='form-control chosen' multiple onchange=\"loadPlans('#products{$i}', this)\"");?> +
+
- - - - type != 'stage'):?> -
-
- - +
+
> + product->plan;?> + id][]", isset($productPlans[$product->id]) ? $productPlans[$product->id] : array(), $product->plans, "class='form-control chosen' multiple");?> +
+ + > +
-
- model) and $project->model == 'scrum') $hidden = '';?> - - execution->linkPlan;?> - + + + + + project->manageProductPlan;?> +
- - -
- - - id] as $branchID => $branch):?> - id][$branchID]) ? $productPlans[$product->id][$branchID] : array();?> -
id}][{$branchID}][]", $plans, $branches[$product->id][$branchID]->plan, "class='form-control chosen' multiple");?>
- - - - +
+
+
+
+ product->common;?> + +
+
+ +
+
+
+
+ product->plan;?> + +
+ + +
+
+
+ @@ -232,5 +257,6 @@ execution->confirmSync);?> +project);?> project->unLinkProductTip);?> diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index f2d23d70cb..0f1bca679b 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -546,6 +546,7 @@ $lang->resource->story->batchChangeModule = 'batchChangeModule'; $lang->resource->story->batchToTask = 'batchToTask'; $lang->resource->story->processStoryChange = 'processStoryChange'; $lang->resource->story->linkStories = 'linkStoriesAB'; +$lang->resource->story->relieved = 'relievedSiblings'; $lang->story->methodOrder[5] = 'create'; $lang->story->methodOrder[10] = 'batchCreate'; @@ -575,6 +576,7 @@ $lang->story->methodOrder[120] = 'batchChangeModule'; $lang->story->methodOrder[125] = 'batchToTask'; $lang->story->methodOrder[130] = 'processStoryChange'; $lang->story->methodOrder[135] = 'linkStories'; +$lang->story->methodOrder[140] = 'relieved'; /* Requirement. */ $lang->resource->requirement = new stdclass(); diff --git a/module/kanban/model.php b/module/kanban/model.php index 046a94de6e..9c1b8d325e 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -818,8 +818,11 @@ class kanbanModel extends model } elseif($branchID) { - $branchName = $this->branch->getById($branchID); - $branches = array($branchID => $branchName); + foreach(explode(',', $branchID) as $id) + { + $branchName = $this->branch->getById($id); + $branches[$id] = $branchName; + } } foreach($branches as $id => $name) diff --git a/module/product/control.php b/module/product/control.php index 5f9a78b411..a691565285 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -257,7 +257,7 @@ class product extends control $this->products = $this->product->getProducts($projectID, 'all', '', false); $projectProducts = $this->product->getProducts($projectID); - $productPlans = $this->execution->getPlans($projectProducts, 'skipParent'); + $productPlans = $this->execution->getPlans($projectProducts, 'skipParent', $projectID); if($browseType == 'bybranch') $param = $branchID; $stories = $this->story->getExecutionStories($projectID, $productID, $branchID, $sort, $browseType, $param, $storyType, '', $pager); @@ -923,6 +923,22 @@ class product extends control } } + /** + * Ajax get product by id. + * + * @param int $productID + * @access public + * @return void + */ + public function ajaxGetProductById($productID) + { + $product = $this->product->getById($productID); + + $product->branchSourceName = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); + $product->branchName = $this->lang->product->branchName[$product->type]; + echo json_encode($product); + } + /** * AJAX: get projects of a product in html select. * @@ -1037,12 +1053,19 @@ class product extends control public function ajaxGetPlans($productID, $branch = 0, $planID = 0, $fieldID = '', $needCreate = false, $expired = '', $param = '') { $param = strtolower($param); - $plans = $this->loadModel('productplan')->getPairs($productID, $branch, $expired, strpos($param, 'skipparent') !== false); - $field = $fieldID ? "plans[$fieldID]" : 'plan'; + if(strpos($param, 'forstory') === false) + { + $plans = $this->loadModel('productplan')->getPairs($productID, $branch, $expired, strpos($param, 'skipparent') !== false); + } + else + { + $plans = $this->loadModel('productplan')->getPairsForStory($productID, $branch == '0' ? 'all' : $branch, $param); + } + $field = $fieldID !== '' ? "plans[$fieldID]" : 'plan'; $multiple = strpos($param, 'multiple') === false ? '' : 'multiple'; $output = html::select($field, $plans, $planID, "class='form-control chosen' $multiple"); - if($branch == 0 and strpos($param, 'edit')) $output = html::select($field, $plans, $planID, "class='form-control chosen' multiple"); + if($branch == 0 and strpos($param, 'edit') and (strpos($param, 'forstory') === false)) $output = html::select($field, $plans, $planID, "class='form-control chosen' multiple"); if(count($plans) == 1 and $needCreate and $needCreate !== 'false') { diff --git a/module/productplan/control.php b/module/productplan/control.php index cce8f8b09d..91e31a77e2 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -84,7 +84,7 @@ class productplan extends control } $this->commonAction($productID, $branchID); - $lastPlan = $this->productplan->getLast($productID, $branchID, $parent); + $lastPlan = $this->productplan->getLast($productID, '', $parent); $product = $this->loadModel('product')->getById($productID); if($lastPlan) @@ -117,7 +117,7 @@ class productplan extends control $this->view->branches = $branchPairs; $this->view->defaultBranch = $defaultBranch; $this->view->parent = $parent; - $this->view->parentPlanPairs = $this->productplan->getTopPlanPairs($productID, $branchID, 'done,closed'); + $this->view->parentPlanPairs = $this->productplan->getTopPlanPairs($productID, '', 'done,closed'); $this->display(); } @@ -136,7 +136,7 @@ class productplan extends control $changes = $this->productplan->update($planID); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $change[$planID] = $changes; - $this->syncStory($change); + $this->unlinkOldBranch($change); if($changes) { $actionID = $this->loadModel('action')->create('productplan', $planID, 'edited'); @@ -178,7 +178,7 @@ class productplan extends control if(!empty($_POST['title'])) { $changes = $this->productplan->batchUpdate($productID); - $this->syncStory($changes); + $this->unlinkOldBranch($changes); $this->loadModel('action'); foreach($changes as $planID => $change) { @@ -194,15 +194,21 @@ class productplan extends control $this->commonAction($productID, $branch); - $plans = $this->productplan->getByIDList($this->post->planIDList); - $oldBranch = array(); + $plans = $this->productplan->getByIDList($this->post->planIDList); + $oldBranch = array(); + $parentIdList = array(); - foreach($plans as $plan) $oldBranch[$plan->id] = $plan->branch; + foreach($plans as $plan) + { + $oldBranch[$plan->id] = $plan->branch; + $parentIdList[$plan->parent] = $plan->parent; + } - $this->view->title = $this->lang->productplan->batchEdit; - $this->view->plans = $plans; - $this->view->oldBranch = $oldBranch; - $this->view->product = $this->loadModel('product')->getById($productID);; + $this->view->title = $this->lang->productplan->batchEdit; + $this->view->plans = $plans; + $this->view->oldBranch = $oldBranch; + $this->view->product = $this->loadModel('product')->getById($productID);; + $this->view->parentList = $this->productplan->getByIDList($parentIdList); $this->display(); } @@ -447,8 +453,6 @@ class productplan extends control if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent); if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id); - if($plan->branch > 0) $this->view->branchStatus = $this->loadModel('branch')->getById($plan->branch, $plan->product, 'status'); - $storyIdList = array(); $modulePairs = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); foreach($planStories as $story) @@ -675,7 +679,7 @@ class productplan extends control $this->config->product->search['style'] = 'simple'; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairsForStory($plan->product, $plan->branch, 'skipParent|withMainPlan'); - $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, $plan->branch); + $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); $storyStatusList = $this->lang->story->statusList; unset($storyStatusList['closed']); $this->config->product->search['params']['status'] = array('operator' => '=', 'control' => 'select', 'values' => $storyStatusList); @@ -687,8 +691,9 @@ class productplan extends control else { $this->config->product->search['fields']['branch'] = $this->lang->product->branch; - $branchName = $this->loadModel('branch')->getById($plan->branch); - $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $plan->branch => $branchName); + + $branchPairs = $this->dao->select('id, name')->from(TABLE_BRANCH)->where('id')->in($plan->branch)->fetchPairs(); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branchPairs; $this->config->product->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->product->search); @@ -809,9 +814,12 @@ class productplan extends control $this->config->bug->search['style'] = 'simple'; $this->config->bug->search['params']['plan']['values'] = $this->productplan->getPairsForStory($productID, $plan->branch, 'skipParent|withMainPlan'); $this->config->bug->search['params']['execution']['values'] = $this->loadModel('product')->getExecutionPairsByProduct($plan->product, $plan->branch); + $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs($productID, $branch = 'all', $params = ''); + $this->config->bug->search['params']['resolvedBuild']['values'] = $this->build->getBuildPairs($productID, $branch = 'all', $params = ''); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, 'all'); $this->config->bug->search['params']['openedBuild']['values'] = $this->loadModel('build')->getBuildPairs($productID, $branch = 'all', $params = 'releasetag'); $this->config->bug->search['params']['resolvedBuild']['values'] = $this->config->bug->search['params']['openedBuild']['values']; - $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, $plan->branch); + $this->config->bug->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'bug', 0, 'all'); $this->config->bug->search['params']['project']['values'] = $this->product->getProjectPairsByProduct($productID, $plan->branch); unset($this->config->bug->search['fields']['product']); @@ -823,8 +831,9 @@ class productplan extends control else { $this->config->bug->search['fields']['branch'] = $this->lang->product->branch; - $branchName = $this->loadModel('branch')->getById($plan->branch); - $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main, $plan->branch => $branchName); + + $branchPairs = $this->dao->select('id, name')->from(TABLE_BRANCH)->where('id')->in($plan->branch)->fetchPairs(); + $branches = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branchPairs; $this->config->bug->search['params']['branch']['values'] = $branches; } $this->loadModel('search')->setSearchParams($this->config->bug->search); @@ -891,13 +900,13 @@ class productplan extends control } /** - * Synchronize story when edit plan. + * Unlink story and bug when edit branch of plan. * @param int $planID * @param int $oldBranch * @access protected * @return void */ - protected function syncStory($changes) + protected function unlinkOldBranch($changes) { foreach($changes as $planID => $changes) { @@ -913,38 +922,66 @@ class productplan extends control } } $planStories = $this->loadModel('story')->getPlanStories($planID, 'all'); + $planBugs = $this->loadModel('bug')->getPlanBugs($planID, 'all'); if($oldBranch) { foreach($planStories as $storyID => $story) { - if($story->branch and $story->branch != $newBranch) $this->productplan->unlinkStory($storyID, $planID); + if($story->branch and strpos(",$newBranch,", ",$story->branch,") === false) $this->productplan->unlinkStory($storyID, $planID); + } + + foreach($planBugs as $bugID => $bug) + { + if($bug->branch and strpos(",$newBranch,", ",$bug->branch,") === false) $this->productplan->unlinkBug($bugID, $planID); } } } } /** - * AJAX: Get conflict story. + * AJAX: Get conflict story and bug. * * @param int $planID * @param int $branch * @access public * @return void */ - public function ajaxGetConflictStory($planID, $newBranch) + public function ajaxGetConflict($planID, $newBranch) { - $plan = $this->productplan->getByID($planID); - $oldBranch = $plan->branch; - $planStories = $this->loadModel('story')->getPlanStories($planID, 'all'); - $conflictStoryIdList = ''; + if($newBranch == '') return; + + $plan = $this->productplan->getByID($planID); + $oldBranch = $plan->branch; + $planStories = $this->loadModel('story')->getPlanStories($planID, 'all'); + $planBugs = $this->loadModel('bug')->getPlanBugs($planID, 'all'); + + $conflictStoryCounts = 0; + $conflictBugCounts = 0; if($oldBranch) { foreach($planStories as $storyID => $story) { - if($story->branch and $story->branch != $newBranch) $conflictStoryIdList .= '[' . $storyID . ']'; + if($story->branch and strpos(",$newBranch,", ",$story->branch,") === false) $conflictStoryCounts ++; + } + + foreach($planBugs as $bugID => $bug) + { + if($bug->branch and strpos(",$newBranch,", ",$bug->branch,") === false) $conflictBugCounts ++; } } - if($conflictStoryIdList != '') printf($this->lang->story->confirmChangePlan, $conflictStoryIdList); + + if($conflictStoryCounts and $conflictBugCounts) + { + printf($this->lang->productplan->confirmChangePlan, $conflictStoryCounts, $conflictBugCounts); + } + elseif($conflictStoryCounts) + { + printf($this->lang->productplan->confirmRemoveStory, $conflictStoryCounts); + } + elseif($conflictBugCounts) + { + printf($this->lang->productplan->confirmRemoveBug, $conflictBugCounts); + } } /** @@ -967,12 +1004,56 @@ class productplan extends control * * @param int $productID * @param int $branch + * @param int $planID * @access public * @return object */ - public function ajaxGetTopPlan($productID, $branch = 0) + public function ajaxGetTopPlan($productID, $branch = 0, $planID = 0) { $parentPlanPairs = $this->productplan->getTopPlanPairs($productID, $branch); - return print(html::select('parent', array('0' => '') + $parentPlanPairs, '', 'class="form-control"')); + if(isset($parentPlanPairs[$planID])) unset($parentPlanPairs[$planID]); + return print(html::select('parent', array(0 => '') + $parentPlanPairs, 0, 'class="form-control"')); + } + + /** + * AJAX: Get diff branches tips. + * + * @param int $productID + * @param int $parentID + * @param string $branches + * @access public + * @return void + */ + public function ajaxGetDiffBranchesTip($productID = 0, $parentID = 0, $branches = '') + { + if(empty($parentID) or empty($productID)) return; + + /* If it has children, return. */ + $parentBranch = $this->productplan->getByID($parentID); + if($parentBranch->parent == '-1') return; + + /* Find diff branches between parent plan and child plan. */ + $diffBranches = array(); + $diffBranchesTip = ''; + $product = $this->loadModel('product')->getByID($productID); + $branchPairs = $this->loadModel('branch')->getPairs($productID); + foreach(explode(',', $parentBranch->branch) as $parentBranchID) + { + if(empty($parentBranchID)) continue; + if(strpos(",$branches,", ",$parentBranchID,") === false) + { + $diffBranches[$parentBranchID] = $parentBranchID; + $diffBranchesTip .= "{$branchPairs[$parentBranchID]},"; + } + } + if(empty($diffBranchesTip)) return; + + /* Find stories and bugs in diff branches. */ + $unlinkStories = $this->dao->select('*')->from(TABLE_STORY)->where('branch')->in($diffBranches)->andWhere("CONCAT(',', plan, ',')")->like("%,{$parentID},%")->fetchAll('id'); + $unlinkBugs = $this->dao->select('*')->from(TABLE_BUG)->where('branch')->in($diffBranches)->andWhere('plan')->eq($parentID)->fetchAll('id'); + if(empty($unlinkStories) and empty($unlinkBugs)) return; + + $this->lang->productplan->diffBranchesTip = str_replace('@branch@', $this->lang->product->branchName[$product->type], $this->lang->productplan->diffBranchesTip); + printf($this->lang->productplan->diffBranchesTip, trim($diffBranchesTip, ',')); } } diff --git a/module/productplan/css/browse.css b/module/productplan/css/browse.css index 0c62007c4c..8a0fe5b64d 100644 --- a/module/productplan/css/browse.css +++ b/module/productplan/css/browse.css @@ -1,7 +1,7 @@ .table td.content div {height: 25px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; float: left; max-width: calc(100% - 20px);} .table td.content .more {position: absolute; right: 16px;} .table td.content .more .icon {color: #838a9d;} -td.c-branch {overflow: hidden; text-align: left !important; text-overflow: ellipsis; white-space: nowrap;} +td.c-branch {overflow: hidden; text-align: left !important; white-space: nowrap;} .table-children {border-left: 2px solid #cbd0db; border-right: 2px solid #cbd0db;} .table tbody > tr.table-children.table-child-top {border-top: 2px solid #cbd0db;} diff --git a/module/productplan/js/batchedit.js b/module/productplan/js/batchedit.js index 247b9b1772..9384b7631a 100644 --- a/module/productplan/js/batchedit.js +++ b/module/productplan/js/batchedit.js @@ -24,17 +24,17 @@ function changeDate(planID) * Get conflict stories. * * @param int $planID - * @param int $branch * @access public * @return void */ -function getConflictStories(planID, branch) +function getConflictStories(planID) { - $.get(createLink('productplan', 'ajaxGetConflictStory', 'planID=' + planID + '&newBranch=' + branch), function(conflictStories) + var newBranch = $('#branch' + planID).val() ? $('#branch' + planID).val().toString() : ''; + $.get(createLink('productplan', 'ajaxGetConflict', 'planID=' + planID + '&newBranch=' + newBranch), function(conflictStories) { if(conflictStories != '' && !confirm(conflictStories)) { - $('#branch' + planID).val(oldBranch[planID]); + $('#branch' + planID).val(oldBranch[planID].split(',')); $('#branch' + planID).trigger("chosen:updated"); } }); diff --git a/module/productplan/js/create.js b/module/productplan/js/create.js index bbf0718f74..4c4be3f040 100644 --- a/module/productplan/js/create.js +++ b/module/productplan/js/create.js @@ -65,9 +65,14 @@ $('#future').on('change', function() $('#branch').change(function() { - var branchID = $(this).val(); - var lastPlanLink = createLink('productplan', 'ajaxGetLast', "productID=" + productID + "&branch=" + branchID); - var topPlanLink = createLink('productplan', 'ajaxGetTopPlan', "productID=" + productID + "&branch=" + branchID); + if(parentPlanID) return; + + var branchIdList = $(this).val(); + if(!branchIdList) return; + + var branchIdList = branchIdList.toString(); + var lastPlanLink = createLink('productplan', 'ajaxGetLast', "productID=" + productID + "&branch=" + branchIdList); + var topPlanLink = createLink('productplan', 'ajaxGetTopPlan', "productID=" + productID + "&branch=" + branchIdList); $.post(lastPlanLink, function(data) { @@ -83,3 +88,18 @@ $('#branch').change(function() $('#parent').chosen(); }) }); + +$('#submit').click(function() +{ + var parentPlan = $('#parent').val(); + var branches = $('#branch').val(); + if(parentPlan != 0 && branches) + { + link = createLink('productplan', 'ajaxGetDiffBranchesTip', "produtID=" + productID + "&parentID=" + parentPlan + "&branches=" + branches.toString()); + $.post(link, function(diffBranchesTip) + { + if((diffBranchesTip != '' && confirm(diffBranchesTip)) || !diffBranchesTip) $('form#dataform').submit(); + }); + return false; + } +}); diff --git a/module/productplan/js/edit.js b/module/productplan/js/edit.js index 76f811cabf..402f42c589 100644 --- a/module/productplan/js/edit.js +++ b/module/productplan/js/edit.js @@ -17,34 +17,32 @@ function convertStringToDate(dateString) * Get conflict stories. * * @param int $planID - * @param int $branch * @access public * @return void */ -function getConflictStories(planID, branch) +function getConflictStories(planID) { - $.get(createLink('productplan', 'ajaxGetConflictStory', 'planID=' + planID + '&newBranch=' + branch), function(conflictStories) + var newBranch = $('#branch').val() ? $('#branch').val().toString() : ''; + $.get(createLink('productplan', 'ajaxGetConflict', 'planID=' + planID + '&newBranch=' + newBranch), function(conflictStories) { if(conflictStories != '') { var result = confirm(conflictStories) ? true : false; if(!result) { - $('#branch').val(oldBranch[planID]); + newBranch = oldBranch[planID]; + $('#branch').val(oldBranch[planID].split(',')); $('#branch').trigger("chosen:updated"); } } - if(conflictStories == '' || result) + var link = createLink('productplan', 'ajaxGetTopPlan', "productID=" + productID + "&branch=" + newBranch + "&planID=" + planID); + $.post(link, function(data) { - var link = createLink('productplan', 'ajaxGetTopPlan', "productID=" + productID + "&branch=" + branch); - $.post(link, function(data) - { - $('#parent').replaceWith(data); - $('#parent_chosen').remove(); - $('#parent').chosen(); - }) - } + $('#parent').replaceWith(data); + $('#parent_chosen').remove(); + $('#parent').chosen(); + }) }); } @@ -88,5 +86,19 @@ $('#future').on('change', function() } }); - $('#future').change(); + +$('#submit').click(function() +{ + var parentPlan = $('#parent').val(); + var branches = $('#branch').val(); + if(parentPlan != 0 && branches) + { + link = createLink('productplan', 'ajaxGetDiffBranchesTip', "produtID=" + productID + "&parentID=" + parentPlan + "&branches=" + branches.toString()); + $.post(link, function(diffBranchesTip) + { + if((diffBranchesTip != '' && confirm(diffBranchesTip)) || !diffBranchesTip) $('form#dataform').submit(); + }); + return false; + } +}); diff --git a/module/productplan/lang/de.php b/module/productplan/lang/de.php index 0776e1a3be..c402dd43ff 100644 --- a/module/productplan/lang/de.php +++ b/module/productplan/lang/de.php @@ -75,6 +75,9 @@ $lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $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 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 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->confirmRemoveBug = "After the branch 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'; $lang->productplan->product = $lang->productCommon; @@ -136,6 +139,7 @@ $lang->productplan->beginGreaterChildTip = "The start date of the parent plan[%s $lang->productplan->endLetterChildTip = "The completion date of the parent plan[%s]: %s, cannot be less than the completion date of the child plan: %s."; $lang->productplan->beginLetterParentTip = "The start date of the child plan[%s]: %s, cannot be less than the start date of the parent plan: %s."; $lang->productplan->endGreaterParentTip = "The completion date of the child plan[%s]: %s, cannot be greater than the completion date of the parent plan: %s."; +$lang->productplan->diffBranchesTip = "The @branch@ %s of parent plan is not linked with the child plan. @branch@'s stories and bugs whill be removed from the plan. Do you want to save?"; $lang->productplan->featureBar['browse']['all'] = 'Alle'; $lang->productplan->featureBar['browse']['undone'] = 'Undone'; diff --git a/module/productplan/lang/en.php b/module/productplan/lang/en.php index 41b5fe348c..3306bc5bdb 100644 --- a/module/productplan/lang/en.php +++ b/module/productplan/lang/en.php @@ -75,6 +75,9 @@ $lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $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 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 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->confirmRemoveBug = "After the branch 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'; $lang->productplan->product = $lang->productCommon; @@ -136,6 +139,7 @@ $lang->productplan->beginGreaterChildTip = "The start date of the parent plan[%s $lang->productplan->endLetterChildTip = "The completion date of the parent plan[%s]: %s, cannot be less than the completion date of the child plan: %s."; $lang->productplan->beginLetterParentTip = "The start date of the child plan[%s]: %s, cannot be less than the start date of the parent plan: %s."; $lang->productplan->endGreaterParentTip = "The completion date of the child plan[%s]: %s, cannot be greater than the completion date of the parent plan: %s."; +$lang->productplan->diffBranchesTip = "The @branch@ %s of parent plan is not linked with the child plan. @branch@'s stories and bugs whill be removed from the plan. Do you want to save?"; $lang->productplan->featureBar['browse']['all'] = 'All'; $lang->productplan->featureBar['browse']['undone'] = 'Undone'; diff --git a/module/productplan/lang/fr.php b/module/productplan/lang/fr.php index dc5390ac80..46bfcaf196 100644 --- a/module/productplan/lang/fr.php +++ b/module/productplan/lang/fr.php @@ -75,6 +75,9 @@ $lang->productplan->projectNotEmpty = 'Project cannot be empty.'; $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 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 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->confirmRemoveBug = "After the branch 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'; $lang->productplan->product = $lang->productCommon; @@ -136,6 +139,7 @@ $lang->productplan->beginGreaterChildTip = "The start date of the parent plan[%s $lang->productplan->endLetterChildTip = "The completion date of the parent plan[%s]: %s, cannot be less than the completion date of the child plan: %s."; $lang->productplan->beginLetterParentTip = "The start date of the child plan[%s]: %s, cannot be less than the start date of the parent plan: %s."; $lang->productplan->endGreaterParentTip = "The completion date of the child plan[%s]: %s, cannot be greater than the completion date of the parent plan: %s."; +$lang->productplan->diffBranchesTip = "The @branch@ %s of parent plan is not linked with the child plan. @branch@'s stories and bugs whill be removed from the plan. Do you want to save?"; $lang->productplan->featureBar['browse']['all'] = 'Tous'; $lang->productplan->featureBar['browse']['undone'] = 'Undone'; diff --git a/module/productplan/lang/zh-cn.php b/module/productplan/lang/zh-cn.php index 119714e9de..b1765303ea 100644 --- a/module/productplan/lang/zh-cn.php +++ b/module/productplan/lang/zh-cn.php @@ -75,6 +75,9 @@ $lang->productplan->projectNotEmpty = '所属项目不能为空。'; $lang->productplan->nextStep = "下一步"; $lang->productplan->summary = "本页共 %s 个计划,父计划 %s,子计划 %s,独立计划 %s。"; $lang->productplan->checkedSummary = "共选中 %total% 个计划,父计划 %parent%,子计划 %child%,独立计划 %independent%。"; +$lang->productplan->confirmChangePlan = "分支解除关联后,分支下的%s个{$lang->SRCommon}和%s个Bug将同步从计划中移除,是否解除?"; +$lang->productplan->confirmRemoveStory = "分支解除关联后,分支下的%s个{$lang->SRCommon}将同步从计划中移除,是否解除?"; +$lang->productplan->confirmRemoveBug = "分支解除关联后,分支下的%s个Bug将同步从计划中移除,是否解除?"; $lang->productplan->id = '编号'; $lang->productplan->product = $lang->productCommon; @@ -136,6 +139,7 @@ $lang->productplan->beginGreaterChildTip = "父计划[%s]的开始日期:%s, $lang->productplan->endLetterChildTip = "父计划[%s]的完成日期:%s,不能小于子计划的完成日期: %s"; $lang->productplan->beginLetterParentTip = "子计划[%s]的开始日期:%s,不能小于父计划的开始日期: %s"; $lang->productplan->endGreaterParentTip = "子计划[%s]的完成日期:%s,不能大于父计划的完成日期: %s"; +$lang->productplan->diffBranchesTip = "父计划的@branch@ %s 未被子计划关联,对应@branch@的需求和bug将自动从计划中移除,是否保存?"; $lang->productplan->featureBar['browse']['all'] = '全部'; $lang->productplan->featureBar['browse']['undone'] = '未完成'; diff --git a/module/productplan/model.php b/module/productplan/model.php index 0ff986ebe6..d378d62382 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -52,15 +52,28 @@ class productplanModel extends model * @access public * @return object */ - public function getLast($productID, $branch = 0, $parent = 0) + public function getLast($productID, $branch = '', $parent = 0) { + $branchQuery = ''; + if($branch !== '') + { + $branchQuery .= '('; + $branchCount = count(explode(',', $branch)); + foreach(explode(',', $branch) as $index => $branchID) + { + $branchQuery .= "CONCAT(',', branch, ',') LIKE '%,$branchID,%'"; + if($index < $branchCount - 1) $branchQuery .= ' AND '; + } + $branchQuery .= ')'; + } + return $this->dao->select('*')->from(TABLE_PRODUCTPLAN) ->where('deleted')->eq(0) ->beginIF($parent <= 0)->andWhere('parent')->le((int)$parent)->fi() ->beginIF($parent > 0)->andWhere('parent')->eq((int)$parent)->fi() ->andWhere('product')->eq((int)$productID) ->andWhere('end')->ne($this->config->productplan->future) - ->andWhere('branch')->eq($branch) + ->beginIF($branch !== '' and !empty($branchQuery))->andWhere($branchQuery)->fi() ->orderBy('end desc') ->limit(1) ->fetch(); @@ -216,9 +229,22 @@ class productplanModel extends model */ public function getTopPlanPairs($productID, $branch = '', $exclude = '') { + $branchQuery = ''; + if($branch !== '') + { + $branchQuery .= '('; + $branchCount = count(explode(',', $branch)); + foreach(explode(',', $branch) as $index => $branchID) + { + $branchQuery .= "CONCAT(',', branch, ',') LIKE '%,$branchID,%'"; + if($index < $branchCount - 1) $branchQuery .= ' AND '; + } + $branchQuery .= ')'; + } + $planPairs = $this->dao->select("id,title")->from(TABLE_PRODUCTPLAN) ->where('product')->eq($productID) - ->beginIF($branch !== '')->andWhere('branch')->eq($branch)->fi() + ->beginIF($branch !== '' and !empty($branchQuery))->andWhere($branchQuery)->fi() ->andWhere('parent')->le(0) ->andWhere('deleted')->eq(0) ->beginIF($exclude)->andWhere('status')->notin($exclude) @@ -240,28 +266,43 @@ class productplanModel extends model */ public function getPairs($product = 0, $branch = '', $param = '', $skipParent = false) { - $date = date('Y-m-d'); - $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t2.name as branchName,t3.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') - ->leftJoin(TABLE_BRANCH)->alias('t2')->on('t2.id=t1.branch') + $this->app->loadLang('branch'); + + $date = date('Y-m-d'); + + $branchQuery = ''; + if($branch !== '' and $branch != 'all') + { + $branchQuery .= '('; + $branchCount = count(explode(',', $branch)); + foreach(explode(',', $branch) as $index => $branchID) + { + $branchQuery .= "FIND_IN_SET('$branchID', t1.branch)"; + if($index < $branchCount - 1) $branchQuery .= ' OR '; + } + $branchQuery .= ')'; + } + + $plans = $this->dao->select('t1.id,t1.title,t1.parent,t1.begin,t1.end,t3.type as productType,t1.branch')->from(TABLE_PRODUCTPLAN)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t3.id=t1.product') ->where('t1.product')->in($product) ->andWhere('t1.deleted')->eq(0) - ->beginIF($branch !== '' and $branch != 'all')->andWhere('t1.branch')->in("0,$branch")->fi() + ->beginIF(!empty($branchQuery))->andWhere($branchQuery)->fi() ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge($date)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('t1.status')->ne('closed')->fi() ->orderBy('t1.begin desc') ->fetchAll('id'); $plans = $this->reorder4Children($plans); + $plans = $this->relationBranch($plans); $planPairs = array(); - $this->app->loadLang('branch'); foreach($plans as $plan) { if($skipParent and $plan->parent == '-1') continue; if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; $planPairs[$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planPairs[$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; - if($plan->productType != 'normal') $planPairs[$plan->id] = ($plan->branchName ? $plan->branchName : $this->lang->branch->main) . ' / ' . $planPairs[$plan->id]; + if($plan->productType != 'normal') $planPairs[$plan->id] = $planPairs[$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); } return array('' => '') + $planPairs; } @@ -280,12 +321,26 @@ class productplanModel extends model $date = date('Y-m-d'); $param = strtolower($param); $branch = strpos($param, 'withmainplan') !== false ? "0,$branch" : $branch; + + $branchQuery = ''; + if($branch !== '' and $branch != 'all') + { + $branchQuery .= '('; + $branchCount = count(explode(',', $branch)); + foreach(explode(',', $branch) as $index => $branchID) + { + $branchQuery .= "FIND_IN_SET('$branchID', branch)"; + if($index < $branchCount - 1) $branchQuery .= ' OR '; + } + $branchQuery .= ')'; + } + $plans = $this->dao->select('id,title,parent,begin,end')->from(TABLE_PRODUCTPLAN) ->where('product')->in($product) ->andWhere('deleted')->eq(0) ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() ->beginIF(strpos($param, 'noclosed') !== false)->andWhere('status')->ne('closed')->fi() - ->beginIF($branch !== 'all' and $branch !== '')->andWhere("branch")->in($branch)->fi() + ->beginIF($branch !== 'all' and $branch !== '')->andWhere($branchQuery)->fi() ->orderBy('begin desc') ->fetchAll('id'); @@ -310,7 +365,7 @@ class productplanModel extends model /** * Get plans for products * - * @param int $products + * @param array $products * @access public * @return void */ @@ -348,32 +403,39 @@ class productplanModel extends model { $date = date('Y-m-d'); $param = strtolower($param); - $plans = $this->dao->select('*')->from(TABLE_PRODUCTPLAN) - ->where('deleted')->eq(0) - ->beginIF($products)->andWhere('product')->in($products)->fi() - ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('end')->ge($date)->fi() - ->orderBy($orderBy) + $plans = $this->dao->select('t1.*,t2.type as productType')->from(TABLE_PRODUCTPLAN)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t2.id=t1.product') + ->where('t1.deleted')->eq(0) + ->beginIF($products)->andWhere('t1.product')->in($products)->fi() + ->beginIF(strpos($param, 'unexpired') !== false)->andWhere('t1.end')->ge($date)->fi() + ->orderBy('t1.' . $orderBy) ->fetchAll('id'); if(!empty($plans) and $field == 'name') $plans = $this->reorder4Children($plans); + $plans = $this->relationBranch($plans); + $parentTitle = array(); $planGroup = array(); foreach($plans as $plan) { - if(!isset($planGroup[$plan->product][$plan->branch])) $planGroup[$plan->product][$plan->branch] = array('' => ''); - - if($plan->parent == '-1' and strpos($param, 'skipparent') !== false) continue; - - if($field == 'name') + foreach(explode(',', $plan->branch) as $branch) { - if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; - $planGroup[$plan->product][$plan->branch][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; - if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planGroup[$plan->product][$plan->branch][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; - } - else - { - $planGroup[$plan->product][$plan->branch][$plan->id] = $plan; + if(!isset($planGroup[$plan->product][$branch])) $planGroup[$plan->product][$branch] = array('' => ''); + + if($plan->parent == '-1' and strpos($param, 'skipparent') !== false) continue 2; + + if($field == 'name') + { + if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; + $planGroup[$plan->product][$branch][$plan->id] = $plan->title . " [{$plan->begin} ~ {$plan->end}]"; + if($plan->begin == $this->config->productplan->future and $plan->end == $this->config->productplan->future) $planGroup[$plan->product][$branch][$plan->id] = $plan->title . ' ' . $this->lang->productplan->future; + if($plan->productType != 'normal') $planGroup[$plan->product][$branch][$plan->id] = $planGroup[$plan->product][$branch][$plan->id] . ' / ' . ($plan->branchName ? $plan->branchName : $this->lang->branch->main); + } + else + { + $planGroup[$plan->product][$branch][$plan->id] = $plan; + } } } return $planGroup; @@ -429,10 +491,13 @@ class productplanModel extends model $planPairs = array(); foreach($plans as $planID => $plan) { - if($skipParent and $plan->parent == '-1') continue; + foreach(explode(',', $plan->branch) as $branch) + { + if($skipParent and $plan->parent == '-1') continue 2; - if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; - $planPairs[$plan->branch][$planID] = $plan->title . ' [' . $plan->begin . '~' . $plan->end . ']'; + if($plan->parent > 0 and isset($plans[$plan->parent])) $plan->title = $plans[$plan->parent]->title . ' /' . $plan->title; + $planPairs[$branch][$planID] = $plan->title . ' [' . $plan->begin . '~' . $plan->end . ']'; + } } return $planPairs; } @@ -450,9 +515,18 @@ class productplanModel extends model ->setIF($this->post->future || empty($_POST['end']), 'end', $this->config->productplan->future) ->setDefault('createdBy', $this->app->user->account) ->setDefault('createdDate', helper::now()) + ->setDefault('branch', 0) + ->join('branch', ',') ->remove('delta,uid,future') ->get(); + $product = $this->loadModel('product')->getByID($plan->product); + if($product->type != 'normal' and !isset($_POST['branch'])) + { + $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); + dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); + } + if($plan->parent > 0) { $parentPlan = $this->getByID($plan->parent); @@ -477,8 +551,7 @@ class productplanModel extends model if(dao::isError()) return false; $plan = $this->loadModel('file')->processImgURL($plan, $this->config->productplan->editor->create['id'], $this->post->uid); - $this->dao->insert(TABLE_PRODUCTPLAN) - ->data($plan) + $this->dao->insert(TABLE_PRODUCTPLAN)->data($plan) ->autoCheck() ->batchCheck($this->config->productplan->create->requiredFields, 'notempty') ->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin) @@ -489,24 +562,10 @@ class productplanModel extends model $planID = $this->dao->lastInsertID(); $this->file->updateObjectID($this->post->uid, $planID, 'plan'); $this->loadModel('score')->create('productplan', 'create', $planID); - if(!empty($plan->parent)) + if(!empty($plan->parent) and $parentPlan->parent == '0') { - if($parentPlan->parent == '0') - { - $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); - - /* Transfer stories and bugs linked with the parent plan to the child plan. */ - $this->dao->update(TABLE_PLANSTORY)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); - $this->dao->update(TABLE_BUG)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); - $stories = $this->dao->select('*')->from(TABLE_STORY)->where("CONCAT(',', plan, ',')")->like("%,{$plan->parent},%")->fetchAll('id'); - foreach($stories as $storyID => $story) - { - $storyPlan = str_replace(",{$plan->parent},", ",$planID,", ",$story->plan,"); - $storyPlan = trim($storyPlan, ','); - - $this->dao->update(TABLE_STORY)->set('plan')->eq($storyPlan)->where('id')->eq($storyID)->exec(); - } - } + $plan->id = $planID; + $this->transferStoriesAndBugs($plan); } return $planID; } @@ -525,10 +584,19 @@ class productplanModel extends model $plan = fixer::input('post')->stripTags($this->config->productplan->editor->edit['id'], $this->config->allowedTags) ->setIF($this->post->future or empty($_POST['begin']), 'begin', $this->config->productplan->future) ->setIF($this->post->future or empty($_POST['end']), 'end', $this->config->productplan->future) + ->setDefault('branch', 0) + ->join('branch', ',') ->add('id', $planID) ->remove('delta,uid,future') ->get(); + $product = $this->loadModel('product')->getByID($oldPlan->product); + if($product->type != 'normal' and $oldPlan->parent != -1 and !isset($_POST['branch'])) + { + $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); + dao::$errors['branch'] = sprintf($this->lang->error->notempty, $this->lang->product->branch); + } + $parentPlan = $this->getByID($plan->parent); $futureTime = $this->config->productplan->future; @@ -545,9 +613,9 @@ class productplanModel extends model } elseif($oldPlan->parent == -1 and ($plan->begin != $futureTime or $plan->end != $futureTime)) { - $childPlans = $this->getChildren($planID); - $minBegin = $plan->begin; - $maxEnd = $plan->end; + $childPlans = $this->getChildren($planID); + $minBegin = $plan->begin; + $maxEnd = $plan->end; foreach($childPlans as $childID => $childPlan) { $childPlan = isset($plans[$childID]) ? $plans[$childID] : $childPlan; @@ -560,8 +628,7 @@ class productplanModel extends model if(dao::isError()) return false; $plan = $this->loadModel('file')->processImgURL($plan, $this->config->productplan->editor->edit['id'], $this->post->uid); - $this->dao->update(TABLE_PRODUCTPLAN) - ->data($plan) + $this->dao->update(TABLE_PRODUCTPLAN)->data($plan) ->autoCheck() ->batchCheck($this->config->productplan->edit->requiredFields, 'notempty') ->checkIF(!$this->post->future && !empty($_POST['begin']) && !empty($_POST['end']), 'end', 'ge', $plan->begin) @@ -576,25 +643,7 @@ class productplanModel extends model if(!dao::isError()) { $this->file->updateObjectID($this->post->uid, $planID, 'plan'); - if(!empty($plan->parent)) - { - if($parentPlan->parent == '0') - { - $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); - - /* Transfer stories and bugs linked with the parent plan to the child plan. */ - $this->dao->update(TABLE_PLANSTORY)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); - $this->dao->update(TABLE_BUG)->set('plan')->eq($planID)->where('plan')->eq($plan->parent)->exec(); - $stories = $this->dao->select('*')->from(TABLE_STORY)->where("CONCAT(',', plan, ',')")->like("%,{$plan->parent},%")->fetchAll('id'); - foreach($stories as $storyID => $story) - { - $storyPlan = str_replace(",{$plan->parent},", ",$planID,", ",$story->plan,"); - $storyPlan = trim($storyPlan, ','); - - $this->dao->update(TABLE_STORY)->set('plan')->eq($storyPlan)->where('id')->eq($storyID)->exec(); - } - } - } + if(!empty($plan->parent) and $parentPlan->parent == '0') $this->transferStoriesAndBugs($plan); return common::createChanges($oldPlan, $plan); } } @@ -707,13 +756,20 @@ class productplanModel extends model $plans = array(); $extendFields = $this->getFlowExtendFields(); + $product = $this->loadModel('product')->getByID($productID); foreach($data->id as $planID) { + if($product->type != 'normal' and $oldPlans[$planID]->parent != '-1' and !isset($data->branch[$planID])) + { + $this->lang->product->branch = sprintf($this->lang->product->branch, $this->lang->product->branchName[$product->type]); + return helper::end(js::error(sprintf($this->lang->error->notempty, $this->lang->product->branch))); + } + $isFuture = isset($data->future[$planID]) ? true : false; if(isset($data->status[$planID]) and $data->status[$planID] != 'wait') $isFuture = false; $plan = new stdclass(); - $plan->branch = isset($data->branch[$planID]) ? $data->branch[$planID] : $oldPlans[$planID]->branch; + $plan->branch = isset($data->branch[$planID]) ? join(',', $data->branch[$planID]) : $oldPlans[$planID]->branch; $plan->title = $data->title[$planID]; $plan->begin = isset($data->begin[$planID]) ? $data->begin[$planID] : ''; $plan->end = isset($data->end[$planID]) ? $data->end[$planID] : ''; @@ -721,8 +777,8 @@ class productplanModel extends model $plan->parent = $oldPlans[$planID]->parent; $plan->id = $planID; - if(empty($plan->title)) return print(js::alert(sprintf($this->lang->productplan->errorNoTitle, $planID))); - if($plan->begin > $plan->end and !empty($plan->end)) return print(js::alert(sprintf($this->lang->productplan->beginGeEnd, $planID))); + if(empty($plan->title)) return helper::end(js::alert(sprintf($this->lang->productplan->errorNoTitle, $planID))); + if($plan->begin > $plan->end and !empty($plan->end)) return helper::end(js::alert(sprintf($this->lang->productplan->beginGeEnd, $planID))); if($plan->begin == '') $plan->begin = $this->config->productplan->future; if($plan->end == '') $plan->end = $this->config->productplan->future; @@ -746,14 +802,14 @@ class productplanModel extends model /* Determine whether the begin and end dates of the parent plan and the child plan are correct. */ if($parentID > 0) { - $parent = isset($plans[$parentID]) ? $plans[$parentID] : $this->getByID($parentID); + $parent = isset($plans[$parentID]) ? $plans[$parentID] : $this->getByID($parentID); if($parent->begin != $this->config->productplan->future and $plan->begin != $this->config->productplan->future and $plan->begin < $parent->begin) { - return print(js::alert(sprintf($this->lang->productplan->beginLetterParentTip, $planID, $plan->begin, $parent->begin))); + return helper::end(js::alert(sprintf($this->lang->productplan->beginLetterParentTip, $planID, $plan->begin, $parent->begin))); } elseif($parent->end != $this->config->productplan->future and $plan->end != $this->config->productplan->future and $plan->end > $parent->end) { - return print(js::alert(sprintf($this->lang->productplan->endGreaterParentTip, $planID, $plan->end, $parent->end))); + return helper::end(js::alert(sprintf($this->lang->productplan->endGreaterParentTip, $planID, $plan->end, $parent->end))); } } elseif($parentID == -1 and $plan->begin != $this->config->productplan->future) @@ -767,8 +823,8 @@ class productplanModel extends model if($childPlan->begin < $minBegin and $minBegin != $this->config->productplan->future) $minBegin = $childPlan->begin; if($childPlan->end > $maxEnd and $maxEnd != $this->config->productplan->future) $maxEnd = $childPlan->end; } - if($minBegin < $plan->begin and $minBegin != $this->config->productplan->future) return print(js::alert(sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin))); - if($maxEnd > $plan->end and $maxEnd != $this->config->productplan->future) return print(js::alert(sprintf($this->lang->productplan->endLetterChildTip, $planID, $plan->end, $maxEnd))); + if($minBegin < $plan->begin and $minBegin != $this->config->productplan->future) return helper::end(js::alert(sprintf($this->lang->productplan->beginGreaterChildTip, $planID, $plan->begin, $minBegin))); + if($maxEnd > $plan->end and $maxEnd != $this->config->productplan->future) return helper::end(js::alert(sprintf($this->lang->productplan->endLetterChildTip, $planID, $plan->end, $maxEnd))); } $change = common::createChanges($oldPlans[$planID], $plan); @@ -776,7 +832,7 @@ class productplanModel extends model { if($parentID > 0 and !isset($parents[$parentID])) $parents[$parentID] = $parentID; $this->dao->update(TABLE_PRODUCTPLAN)->data($plan)->autoCheck()->checkFlow()->where('id')->eq($planID)->exec(); - if(dao::isError()) return print(js::error(dao::getError())); + if(dao::isError()) return helper::end(js::error(dao::getError())); $changes[$planID] = $change; } } @@ -918,27 +974,9 @@ 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. */ - if($this->session->currentProductType == 'normal' or $story->branch != 0 or empty($story->plan)) - { - $this->dao->update(TABLE_STORY)->set("plan")->eq($planID)->where('id')->eq((int)$storyID)->exec(); + $this->dao->update(TABLE_STORY)->set("plan")->eq($planID)->where('id')->eq((int)$storyID)->exec(); - $this->story->updateStoryOrderOfPlan($storyID, $planID, $story->plan); - } - else - { - $branchPlanPairs = $this->dao->select('branch, id as plan')->from(TABLE_PRODUCTPLAN) - ->where('product')->eq($story->product) - ->andWhere('id')->in($story->plan) - ->fetchPairs(); - if(isset($branchPlanPairs[$plan->branch])) $branchPlanPairs[$plan->branch] = $planID; - - $plansOfStory = implode(',', $branchPlanPairs); - $plansOfStory = trim($plansOfStory, ','); - - $this->dao->update(TABLE_STORY)->set("plan")->eq($plansOfStory)->where('id')->eq((int)$storyID)->exec(); - - $this->story->updateStoryOrderOfPlan($storyID, $planID); - } + $this->story->updateStoryOrderOfPlan($storyID, $planID, $story->plan); $this->action->create('story', $storyID, 'linked2plan', '', $planID); $this->story->setStage($storyID); @@ -1026,9 +1064,12 @@ class productplanModel extends model $planStory = $this->loadModel('story')->getPlanStories($planID); if(!empty($planStory)) { + $projectProducts = $this->loadModel('project')->getBranchesByProject($projectID); + foreach($planStory as $id => $story) { - if($story->status == 'draft' or $story->status == 'reviewing') + $projectBranches = zget($projectProducts, $story->product, array()); + if($story->status == 'draft' or $story->status == 'reviewing' or (!empty($story->branch) and !empty($projectBranches) and !isset($projectBranches[$story->branch]))) { unset($planStory[$id]); continue; @@ -1080,6 +1121,46 @@ class productplanModel extends model return $plans; } + /** + * Get relation branch for plans. + * + * @param array $plans + * @access public + * @return array + */ + public function relationBranch($plans) + { + if(empty($plans)) return $plans; + + $this->app->loadLang('branch'); + $branchMap = $this->dao->select('id, name')->from(TABLE_BRANCH) + ->where('status')->eq('active') + ->andWhere('deleted')->eq('0') + ->fetchPairs('id', 'name'); + $branchMap[BRANCH_MAIN] = $this->lang->branch->main; + + foreach($plans as &$plan) + { + if($plan->branch) + { + $branchName = ''; + foreach(explode(',', $plan->branch) as $planBranch) + { + $branchName .= isset($branchMap[$planBranch]) ? $branchMap[$planBranch] : ''; + $branchName .= ','; + } + + $plan->branchName = trim($branchName, ','); + } + else + { + $plan->branchName = $this->lang->branch->main; + } + } + + return $plans; + } + /** * Judge an action is clickable or not. * @@ -1246,4 +1327,47 @@ class productplanModel extends model if($product->type == 'normal') unset($this->config->productplan->search['fields']['branch']); $this->loadModel('search')->setSearchParams($this->config->productplan->search); } + + /** + * Transfer stories and bugs to new plan. + * + * @param object $plan + * @access public + * @return void + */ + public function transferStoriesAndBugs($plan) + { + $this->dao->update(TABLE_PRODUCTPLAN)->set('parent')->eq('-1')->where('id')->eq($plan->parent)->andWhere('parent')->eq('0')->exec(); + + /* Transfer stories linked with the parent plan to the child plan. */ + $stories = $this->dao->select('*')->from(TABLE_STORY)->where("CONCAT(',', plan, ',')")->like("%,{$plan->parent},%")->fetchAll('id'); + $unlinkStories = array(); + foreach($stories as $storyID => $story) + { + if(!empty($story->branch) and strpos(",$plan->branch,", ",$story->branch,") === false) + { + $unlinkStories[$storyID] = $storyID; + $storyPlan = str_replace(",{$plan->parent},", ',', ",$story->plan,"); + } + else + { + $storyPlan = str_replace(",{$plan->parent},", ",$plan->id,", ",$story->plan,"); + } + $storyPlan = trim($storyPlan, ','); + + $this->dao->update(TABLE_STORY)->set('plan')->eq($storyPlan)->where('id')->eq($storyID)->exec(); + } + if(!empty($unlinkStories)) $this->dao->delete()->from(TABLE_PLANSTORY)->where('plan')->eq($plan->parent)->andWhere('story')->in($unlinkStories)->exec(); + $this->dao->update(TABLE_PLANSTORY)->set('plan')->eq($plan->id)->where('plan')->eq($plan->parent)->exec(); + + /* Transfer bugs linked with the parent plan to the child plan. */ + $bugs = $this->dao->select('*')->from(TABLE_BUG)->where('plan')->eq($plan->parent)->fetchAll('id'); + $unlinkBugs = array(); + foreach($bugs as $bugID => $bug) + { + if(!empty($bug->branch) and strpos(",$plan->branch,", ",$bug->branch,") === false) $unlinkBugs[$bugID] = $bugID; + } + if(!empty($unlinkBugs)) $this->dao->update(TABLE_BUG)->set('plan')->eq(0)->where('plan')->eq($plan->parent)->andWhere('id')->in($unlinkBugs)->exec(); + $this->dao->update(TABLE_BUG)->set('plan')->eq($plan->id)->where('plan')->eq($plan->parent)->exec(); + } } diff --git a/module/productplan/view/batchedit.html.php b/module/productplan/view/batchedit.html.php index 545a57289a..c9823725b0 100644 --- a/module/productplan/view/batchedit.html.php +++ b/module/productplan/view/batchedit.html.php @@ -23,7 +23,7 @@ productplan->id?> type != 'normal'):?> - productplan->branch;?> + product->branch;?> productplan->title?> productplan->status?> @@ -43,8 +43,18 @@ id . html::hidden("id[$plan->id]", $plan->id);?> type != 'normal'):?> - parent == -1 ? "disabled='disabled'" : '';?> - id]", $plan->parent == '-1' ? '' : $branchTagOption, $plan->branch, "onchange='getConflictStories($plan->id, this.value); 'class='form-control chosen' $disabled");?> + parent == -1 ? "disabled='disabled'" : ''; + $parentBranches = array(); + if($plan->parent > 0 and isset($parentList[$plan->parent])) + { + foreach($branchTagOption as $branchID => $branchName) + { + if(strpos(",{$parentList[$plan->parent]->branch},", ",$branchID,") !== false) $parentBranches[$branchID] = $branchName; + } + } + echo html::select("branch[$plan->id][]", ($plan->parent > 0 and !empty($parentBranches)) ? $parentBranches : $branchTagOption, $plan->branch, "onchange='getConflictStories($plan->id); 'class='form-control chosen' multiple $disabled"); + ?> id]", $plan->title, "class='form-control'")?> diff --git a/module/productplan/view/browsebylist.html.php b/module/productplan/view/browsebylist.html.php index a81c574f0e..7eabe9d192 100644 --- a/module/productplan/view/browsebylist.html.php +++ b/module/productplan/view/browsebylist.html.php @@ -161,7 +161,9 @@ status)?> session->currentProductType != 'normal'):?> - parent != '-1') echo $branchOption[$plan->branch];?> + + branch) as $branchID) $planBranches .= $branchOption[$branchID] . ',';?> + '> begin == $config->productplan->future ? $lang->productplan->future : $plan->begin;?> end == $config->productplan->future ? $lang->productplan->future : $plan->end;?> diff --git a/module/productplan/view/create.html.php b/module/productplan/view/create.html.php index 823eb1a0e4..01a0a00b82 100644 --- a/module/productplan/view/create.html.php +++ b/module/productplan/view/create.html.php @@ -15,6 +15,7 @@ execution->weekend);?> productplan->last);?> +
@@ -35,13 +36,22 @@ productplan->product;?> name;?> + type != 'normal'):?> product->branch;?> - + $branchName) + { + if(strpos(",$parentPlan->branch,", ",$branchID,") === false) unset($branches[$branchID]); + } + } + ?> + - productplan->title;?> @@ -50,7 +60,7 @@ productplan->parent;?> - '') + $parentPlanPairs, '', "class='form-control chosen'");?> + '') + $parentPlanPairs, 0, "class='form-control chosen'");?> @@ -79,7 +89,6 @@ id);?> - branch);?> diff --git a/module/productplan/view/edit.html.php b/module/productplan/view/edit.html.php index 2deed208bc..469eb90018 100644 --- a/module/productplan/view/edit.html.php +++ b/module/productplan/view/edit.html.php @@ -33,7 +33,7 @@ type != 'normal' and $plan->parent != '-1'):?> product->branch;?> - branch, "onchange='getConflictStories($plan->id, this.value); 'class='form-control'");?> + branch, "onchange='getConflictStories($plan->id);' class='form-control chosen' multiple");?> @@ -46,7 +46,7 @@ productplan->parent;?> - '') + $parentPlanPairs, $plan->parent, "class='form-control chosen'");?> + '') + $parentPlanPairs, $plan->parent, "class='form-control chosen'");?> diff --git a/module/productplan/view/view.html.php b/module/productplan/view/view.html.php index 4a7c514fda..47cf7f6a8a 100644 --- a/module/productplan/view/view.html.php +++ b/module/productplan/view/view.html.php @@ -31,7 +31,9 @@
id;?> title;?> - type !== 'normal') echo "" . $branchOption[$branch] . '';?> + + type !== 'normal') echo "" . $branchOption[$branchID] . '';?> + begin == $config->productplan->future || $plan->end == $config->productplan->future) ? $lang->productplan->future : $plan->begin . '~' . $plan->end;?> @@ -559,7 +561,11 @@ type != 'normal'):?> product->branch;?> - branch];?> + + diff --git a/module/project/control.php b/module/project/control.php index 29463c7bb3..653339f752 100755 --- a/module/project/control.php +++ b/module/project/control.php @@ -492,12 +492,9 @@ class project extends control $planIdList = array(); foreach($_POST['plans'] as $plans) { - foreach($plans as $planList) + foreach($plans as $planID) { - foreach($planList as $planID) - { - $planIdList[$planID] = $planID; - } + $planIdList[$planID] = $planID; } } @@ -587,10 +584,8 @@ class project extends control if(!$copyProject->hasProduct) $shadow = 1; foreach($products as $product) { - foreach($product->branches as $branch) - { - $productPlans[$product->id][$branch] = $this->loadModel('productplan')->getPairs($product->id, $branch, 'noclosed', true); - } + $branches = implode(',', $product->branches); + $productPlans[$product->id] = $this->loadModel('productplan')->getPairs($product->id, $branches, 'noclosed', true); } } @@ -722,9 +717,8 @@ class project extends control $withProgram = $this->config->systemMode == 'ALM' ? true : false; - $linkedBranches = array(); $linkedBranchList = array(); - $productPlans = array(0 => ''); + $productPlans = array(); $branches = $this->project->getBranchesByProject($projectID); $linkedProductIdList = empty($branches) ? '' : array_keys($branches); $allProducts = $this->program->getProductPairs($project->parent, 'all', 'noclosed', '', 0, $withProgram); @@ -743,11 +737,10 @@ class project extends control if(!isset($allProducts[$productID])) $allProducts[$productID] = $linkedProduct->name; foreach($branches[$productID] as $branchID => $branch) { - $linkedBranchList[$branchID] = $branchID; - $linkedBranches[$productID][$branchID] = $branchID; + $linkedBranchList[$branchID] = $branchID; - if($branch != BRANCH_MAIN) $productPlans[$productID][$branchID] = isset($plans[$productID][BRANCH_MAIN]) ? $plans[$productID][BRANCH_MAIN] : array(); - $productPlans[$productID][$branchID] += isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); + if($branch != BRANCH_MAIN) $productPlans[$productID] = isset($plans[$productID][BRANCH_MAIN]) ? $plans[$productID][BRANCH_MAIN] : array(); + $productPlans[$productID] += isset($plans[$productID][$branchID]) ? $plans[$productID][$branchID] : array(); if(!empty($projectStories[$productID][$branchID]) or !empty($projectBranches[$productID][$branchID])) { @@ -773,7 +766,6 @@ class project extends control $this->view->multiBranchProducts = $this->loadModel('product')->getMultiBranchPairs(); $this->view->productPlans = array_filter($productPlans); $this->view->linkedProducts = $linkedProducts; - $this->view->linkedBranches = $linkedBranches; $this->view->branches = $branches; $this->view->executions = $this->execution->getPairs($projectID); $this->view->unmodifiableProducts = $unmodifiableProducts; @@ -1286,7 +1278,6 @@ class project extends control $this->view->branchOption = $branchOption; $this->view->branchTagOption = $branchTagOption; $this->view->executions = $executions; - $this->view->plans = $this->loadModel('productplan')->getPairs($productID ? $productID : array_keys($products)); $this->view->stories = $storyList; $this->view->tasks = $taskList; $this->view->projectPairs = $this->project->getPairsByProgram(); diff --git a/module/project/css/create.css b/module/project/css/create.css index cddcaaa13a..9d2c6d922c 100755 --- a/module/project/css/create.css +++ b/module/project/css/create.css @@ -15,16 +15,14 @@ #budget {border-right: 0px;} #budgetUnit {border-left: 0px;} #projectName {display: inline-block; width: 32%;} -#productsBox a {border-radius: 2px !important;} -#productsBox .col-sm-4 {padding-right: 5px;} -#productsBox .required:after {right: -9.5px !important;} -#productsBox .addProduct {padding-left: 0px !important; padding-right: 10px;} -#productsBox .addProduct.required:after {right: -1px !important;} -#productsBox .input-group-addon > div {display: inline-block !important;} -#productsBox .required + .text-danger.help-text {position: relative; left: 10px;} -#productsBox > #productNameLabel {padding-top: 8px;} -#plansBox .col-sm-4 {float: none; display: inline-block; padding-right: 5px;} +.productsBox a {border-radius: 2px !important;} +.productsBox .required:after {right: -9.5px !important;} +.productsBox .addProduct {padding-left: 0px !important;} +.productsBox .addProduct.required:after {right: -1px !important;} +.productsBox .input-group-addon > div {display: inline-block !important;} +.productsBox .required + .text-danger.help-text {position: relative; left: 10px;} +.productsBox > #productNameLabel {padding-top: 8px;} .futureBox {vertical-align: top !important; padding-top: 13px !important;} #projectType {padding-top: 7px !important;} .division .icon-help {margin-left: 15px} -.has-info {border-color: #0c64eb!important;} \ No newline at end of file +.has-info {border-color: #0c64eb!important;} diff --git a/module/project/css/edit.css b/module/project/css/edit.css index e586511016..12113d5d32 100644 --- a/module/project/css/edit.css +++ b/module/project/css/edit.css @@ -5,5 +5,4 @@ #dateRange, .futureBox {vertical-align: top !important; padding-top: 13px !important;} #endList {vertical-align: top; padding-top: 13px;} #linkPlan {padding-top: 25px !important;} -#productsBox .row .col-sm-4.required::after {right: -5px;} #projectType {padding-top: 7px !important;} diff --git a/module/project/js/common.js b/module/project/js/common.js index 9234174496..7af601555d 100644 --- a/module/project/js/common.js +++ b/module/project/js/common.js @@ -138,134 +138,6 @@ function computeEndDate(delta) $('#days').trigger('mousedown'); } -/** - * Load branches. - * - * @param object product - * @access public - * @return void - */ -function loadBranches(product) -{ - /* When selecting a product, delete a plan that is empty by default. */ - $("#planDefault").remove(); - - var chosenProducts = []; - $("#productsBox select[name^='products']").each(function() - { - var $product = $(product); - var productID = $(this).val(); - if(productID > 0 && chosenProducts.indexOf(productID) == -1) chosenProducts.push(productID); - if($product.val() != 0 && $product.val() == $(this).val() && $product.attr('id') != $(this).attr('id') && !multiBranchProducts[$product.val()]) - { - bootbox.alert(errorSameProducts); - $product.val(0); - $product.trigger("chosen:updated"); - return false; - } - }); - - chosenProducts.length > 1 ? $('.division').removeClass('hide') : $('.division').addClass('hide'); - - if($('#productsBox .input-group:last select:first').val() != 0) - { - var length = $('#productsBox .row .input-group').size(); - $('#productsBox .row').append('
' + $('#productsBox .col-sm-4:last').html() + '
'); - if($('#productsBox .input-group:last select').size() >= 2) $('#productsBox .input-group:last select:last').remove(); - $('#productsBox .input-group:last .chosen-container').remove(); - $('#productsBox .input-group:last select:first').attr('name', 'products[' + length + ']').attr('id', 'products' + length); - $('#productsBox .input-group:last .chosen').chosen(); - - adjustProductBoxMargin(); - } - - var $inputgroup = $(product).closest('.input-group'); - if($inputgroup.find('select').size() >= 2) $inputgroup.removeClass('has-branch').find('select:last').remove(); - if($inputgroup.find('.chosen-container').size() >= 2) $inputgroup.find('.chosen-container:last').remove(); - - var index = $inputgroup.find('select:first').attr('id').replace('products' , ''); - var oldBranch = $(product).attr('data-branch') !== undefined ? $(product).attr('data-branch') : 0; - $.get(createLink('branch', 'ajaxGetBranches', "productID=" + $(product).val() + "&oldBranch=" + oldBranch + "¶m=active"), function(data) - { - if(data) - { - $inputgroup.addClass('has-branch').append(data); - $inputgroup.find('select:last').attr('name', 'branch[' + index + ']').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this.value)").chosen(); - - $inputgroup.find('select:last').each(disableSelectedBranch); - disableSelectedProduct(); - } - - var branchID = $('#branch' + index).val(); - loadPlans(product, branchID); - }); - - if(!multiBranchProducts[$(product).val()]) disableSelectedProduct(); - - $("input[name='products[" + index + "]']").remove(); - $("input[name='branch[" + index + "]']").remove(); -} - -function loadPlans(product, branchID) -{ - if($('#plansBox').size() == 0) return false; - - var productID = $(product).val(); - var branchID = typeof(branchID) == 'undefined' ? 0 : branchID; - var planID = $(product).attr('data-plan') !== undefined ? $(product).attr('data-plan') : 0; - var index = $(product).attr('id').replace('products', ''); - - $("input[name='products[" + index + "]']").remove(); - $("input[name='branch[" + index + "]']").remove(); - - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired,noclosed¶m=skipParent,multiple'), function(data) - { - if(data) - { - if($("div#plan" + index).size() == 0) $("#plansBox .row").append('
'); - $("div#plan" + index).html(data).find('select').attr('name', 'plans[' + productID + ']' + '[' + branchID + '][]').attr('id', 'plans' + productID).chosen(); - - adjustPlanBoxMargin(); - } - }); -} - -/** - * Adjust the layout of product selection. - * - * @access public - * @return void - */ -function adjustProductBoxMargin() -{ - var productRows = Math.ceil($('#productsBox > .row > .col-sm-4').length / 3); - if(productRows > 1) - { - for(i = 1; i <= productRows - 1; i++) - { - $('#productsBox .col-sm-4:lt(' + (i * 3) + ')').css('margin-bottom', '10px'); - } - } -} - -/** - * Adjust the layout of the plan selection. - * - * @access public - * @return void - */ -function adjustPlanBoxMargin() -{ - var planRows = Math.ceil($('#plansBox > .row > .col-sm-4').length / 3); - if(planRows > 1) - { - for(j = 1; j <= planRows - 1; j++) - { - $('#plansBox .col-sm-4:lt(' + (j * 3) + ')').css('margin-bottom', '10px'); - } - } -} - /** * Initialization operation. * diff --git a/module/project/js/create.js b/module/project/js/create.js index 64daca13ba..1d1cdb8c1e 100644 --- a/module/project/js/create.js +++ b/module/project/js/create.js @@ -11,7 +11,7 @@ $(function() if(hasProduct == 1) { var chosenProducts = 0; - $("#productsBox select[name^='products']").each(function() + $(".productsBox select[name^='products']").each(function() { if($(this).val() > 0) chosenProducts ++; }); @@ -29,16 +29,16 @@ $(function() setAclList($("#parent").val()); - if(typeof(currentPlanID) == 'undefined') + if(typeof(currentPlanID) == 'undefined' && copyProjectID == 0) { - $('#productsBox select[id^="products"]').each(function() + $('.productsBox select[id^="products"]').each(function() { - var branchID = 0; - if($(this).closest('.input-group').find('select[id^="branch"]').size() > 0) + var branch = 0; + if($(this).closest('.table-row').find('select[name^="branch"]').size() > 0) { - var branchID = $(this).closest('.input-group').find('select[id^="branch"]').val(); + var branch = $(this).closest('.table-row').find('select[name^="branch"]'); } - loadPlans($(this), branchID); + loadPlans($(this), branch); }); } @@ -63,7 +63,7 @@ $(function() $('#daysLabelInfo').remove(); /* Determine whether the products of the same branch are linked. */ - $("#productsBox select[name^='products']").each(function() + $(".productsBox select[name^='products']").each(function() { var productID = $(this).val(); if(typeof(products[productID]) == 'undefined') products[productID] = new Array(); @@ -98,7 +98,6 @@ $(function() } /* Init for copy execution. */ - $("select[id^=branch]").each(disableSelectedBranch); disableSelectedProduct(); /* Check the all products and branches control when uncheck the product. */ @@ -106,14 +105,10 @@ $(function() { if($(this).val() == 0) { - $("select[id^='branch']").each(disableSelectedBranch); - disableSelectedProduct(); } }); - $(document).on('change', "select[id^='branch']", disableSelectedBranch); - if(copyProjectID > 0 && copyType != 'previous') { $('#name').addClass('has-info') @@ -167,7 +162,7 @@ function setParentProgram(parentProgram) var productSelectHtml = data.allProducts; var planSelectHtml = data.plans; - $('#productsBox .row .col-sm-4 .input-group').each(function(index) + $('.productsBox .row .col-sm-4 .input-group').each(function(index) { var selectedProduct = $(this).find('[name^=products]').val(); var selectedBranch = $(this).find('[name^=branch]').val(); @@ -177,7 +172,7 @@ function setParentProgram(parentProgram) $(this).find('[name^=products]').attr('name', 'products[' + index + ']').attr('id', 'products' + index).attr('data-branch', selectedBranch).attr('data-plan', selectedPlan); $(this).find('[name^=products]').val(selectedProduct).chosen().change(); }); - $('#productsBox .row .col-sm-4:last').find('.input-group').append($('#productsBox .addProduct .input-group:first .input-group-addon').prop('outerHTML')); + $('.productsBox .row .col-sm-4:last').find('.input-group').append($('.productsBox .addProduct .input-group:first .input-group-addon').prop('outerHTML')); } if(parentProgram != 0) @@ -198,13 +193,13 @@ function setParentProgram(parentProgram) if(parentProgram != '0') { - $('#productsBox .addProduct .input-group:first').addClass('required'); - $('#productsBox .row .input-group:first').addClass('required'); + $('.productsBox .addProduct .input-group:first').addClass('required'); + $('.productsBox .row .input-group:first').addClass('required'); } else { - $('#productsBox .addProduct .input-group').removeClass('required'); - $('#productsBox .row .input-group').removeClass('required'); + $('.productsBox .addProduct .input-group').removeClass('required'); + $('.productsBox .row .input-group').removeClass('required'); } } @@ -232,35 +227,37 @@ function addNewProduct(obj) if($(obj).attr('checked')) { /* Hide product and plan dropdown controls. */ - $('#productsBox .row .col-sm-4').addClass('hidden'); - $('#productsBox .row .col-sm-4 .input-group').find('select').attr('disabled', true).trigger("chosen:updated"); - $('#plansBox').closest('tr').addClass('hidden'); - $('#plansBox .col-sm-4').find('select').attr('disabled', true).trigger("chosen:updated"); + $('.newLine').addClass('hidden'); + $('.productsBox .row').addClass('hidden'); + $('.productsBox .row .input-group').find('select').attr('disabled', true).trigger("chosen:updated"); $('.division').addClass('hide'); /* Displays the input box for creating a product. */ + $(obj).closest('td').attr('colspan', 1); $("[name^='newProduct']").prop('checked', true); $('#productName').removeAttr('disabled', true); - $('#productsBox .addProduct').removeClass('hidden'); + $('.productsBox .addProduct').removeClass('hidden'); $('#productTitle').html(productName); } else { /* Show product and product dropdown controls. */ - $('#productsBox .row .col-sm-4').removeClass('hidden'); - $('#productsBox .row .col-sm-4 .input-group').find('select').removeAttr('disabled').trigger("chosen:updated"); - $('#plansBox').closest('tr').removeClass('hidden'); - $('#plansBox .col-sm-4').find('select').removeAttr('disabled', true).trigger("chosen:updated"); + $('.newLine').removeClass('hidden'); + $('.productsBox .row').removeClass('hidden'); + $('.productsBox .row .input-group').find('select').removeAttr('disabled').trigger("chosen:updated"); if($('#plansBox .col-sm-4').find('select').length > 1) $('.division').removeClass('hide'); /* Hide the input box for creating a product. */ + $(obj).closest('td').attr('colspan', 3); $("[name^='newProduct']").prop('checked', false); $('#productName').attr('disabled', true); - $('#productsBox .addProduct').addClass('hidden'); + $('.productsBox .addProduct').addClass('hidden'); - $('#productTitle').html(manageProducts); + $('#productTitle').html(manageProductPlan); } - $('#productsBox div + .text-danger.help-text').remove(); + + $('.productsBox div + .text-danger.help-text').remove(); + disableSelectedProduct(); } /** @@ -295,7 +292,7 @@ function loadBranches(product) $("#planDefault").remove(); var chosenProducts = []; - $("#productsBox select[name^='products']").each(function() + $(".productsBox select[name^='products']").each(function() { var $product = $(product); var productID = $(this).val(); @@ -311,111 +308,110 @@ function loadBranches(product) (chosenProducts.length > 1 && model == 'waterfall') ? $('.division').removeClass('hide') : $('.division').addClass('hide'); - if($('#productsBox .row .input-group:last select:first').val() != 0) + var $tableRow = $(product).closest('.table-row'); + var index = $tableRow.find('select:first').attr('id').replace('products' , ''); + var oldBranch = $(product).attr('data-branch') !== undefined ? $(product).attr('data-branch') : 0; + if($(product).val() != 0) { - var length = $('#productsBox .row .input-group').size(); - var $html = $('#productsBox .row .col-sm-4:last').html().replace('required', ''); - $('#productsBox .row .col-sm-4:last').find('.input-group-addon').remove(); - $('#productsBox .row').append('
' + $html + '
'); - if($('#productsBox .row .input-group:last select').size() >= 2) $('#productsBox .row .input-group:last select:last').remove(); - $('#productsBox .row .input-group:last .chosen-container').remove(); - $('#productsBox .row .input-group:last select:first').attr('name', 'products[' + length + ']').attr('id', 'products' + length); - $('#productsBox .row .input-group:last .chosen').chosen(); - - $('[data-toggle="popover"]').popover(); - - adjustProductBoxMargin(); + $(product).closest('tr').find('.newProduct').addClass('hidden') + } + else + { + $(product).closest('tr').find('.newProduct').removeClass('hidden') } - var $inputgroup = $(product).closest('.input-group'); - if($inputgroup.find('select').size() >= 2) $inputgroup.removeClass('has-branch').find('select:last').remove(); - if($inputgroup.find('.chosen-container').size() >= 2) $inputgroup.find('.chosen-container:last').remove(); + if(!multiBranchProducts[$(product).val()]) + { + $tableRow.find('.table-col:last select').val('').trigger('chosen:updated'); + $tableRow.find('.table-col:last').addClass('hidden'); + } - var index = $inputgroup.find('select:first').attr('id').replace('products' , ''); - var oldBranch = $(product).attr('data-branch') !== undefined ? $(product).attr('data-branch') : 0; $.get(createLink('branch', 'ajaxGetBranches', "productID=" + $(product).val() + "&oldBranch=" + oldBranch + "¶m=active"), function(data) { if(data) { - $inputgroup.addClass('has-branch').append(data); - $inputgroup.find('select:last').attr('name', 'branch[' + index + ']').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this.value)").chosen(); + $tableRow.find("select[name^='branch']").replaceWith(data); + $tableRow.find('.table-col:last .chosen-container').remove(); + $tableRow.find('.table-col:last').removeClass('hidden'); + $tableRow.find("select[name^='branch']").attr('multiple', '').attr('name', 'branch[' + index + '][]').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this)").chosen(); - $inputgroup.find('select:last').each(disableSelectedBranch); disableSelectedProduct(); } - var branchID = $('#branch' + index).val(); - loadPlans(product, branchID); + var branch = $('#branch' + index); + loadPlans(product, branch); }); - - if(!multiBranchProducts[$(product).val()]) disableSelectedProduct(); } /** * Load plans. * * @param obj $product - * @param int $branchID + * @param obj $branchID * @access public * @return void */ -function loadPlans(product, branchID) +function loadPlans(product, branch) { - if($('#plansBox').size() == 0) return false; - var productID = $(product).val(); - var branchID = typeof(branchID) == 'undefined' ? 0 : branchID; + var branchID = $(branch).val() == null ? 0 : '0,' + $(branch).val(); var planID = $(product).attr('data-plan') !== undefined ? $(product).attr('data-plan') : 0; var index = $(product).attr('id').replace('products', ''); - $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=0,' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired,noclosed¶m=skipParent,multiple'), function(data) + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired,noclosed¶m=skipParent,multiple'), function(data) { if(data) { - if($("div#plan" + index).size() == 0) $("#plansBox .row").append('
'); - $("div#plan" + index).html(data).find('select').attr('name', 'plans[' + productID + '][' + branchID + '][]').attr('id', 'plans' + productID).chosen(); - - adjustPlanBoxMargin(); + $("div#plan" + index).find("select[name^='plans']").replaceWith(data); + $("div#plan" + index).find('.chosen-container').remove(); + $("div#plan" + index).find('select').attr('name', 'plans[' + productID + ']' + '[]').attr('id', 'plans' + productID).chosen(); } }); } /** - * Adjust product box margin. - * + * Add new line for link product. + * + * @param obj $obj * @access public * @return void */ -function adjustProductBoxMargin() +function addNewLine(obj) { - var productRows = Math.ceil($('#productsBox > .row > .col-sm-4').length / 3); - if(productRows > 1) + var newLine = $(obj).closest('tr').clone(); + var index = 0; + $(".productsBox select[name^='products']").each(function() { - for(i = 1; i <= productRows - 1; i++) - { - $('#productsBox .row .col-sm-4:lt(' + (i * 3) + ')').css('margin-bottom', '10px'); - $('#productsBox .row .col-sm-4').eq(i * 3).css('padding-right', '6px'); - } - } + var id = $(this).attr('id').replace('products' , ''); + + id = parseInt(id); + id ++; + + index = id > index ? id : index; + }) + + newLine.find('.newProduct').remove(); + newLine.find('.addProduct').remove(); + newLine.addClass('newLine'); + newLine.find('th').html(''); + newLine.find('.removeLine').css('visibility', 'visible'); + newLine.find('.chosen-container').remove(); + newLine.find('.productsBox .table-col:last').addClass('hidden'); + newLine.find("select[name^='products']").attr('name', 'products[' + index + ']').attr('id', 'products' + index).val('').chosen(); + newLine.find("select[name^='plans']").attr('name', 'plans[' + index + '][' + 0 + '][]').chosen(); + newLine.find("div[id^='plan']").attr('id', 'plan' + index); + + $(obj).closest('tr').after(newLine); + var product = newLine.find("select[name^='products']"); + var branch = newLine.find("select[name^='branch']"); + loadPlans(product, branch); + disableSelectedProduct(); } -/** - * Adjust plan box margin. - * - * @access public - * @return void - */ -function adjustPlanBoxMargin() +function removeLine(obj) { - var planRows = Math.ceil($('#plansBox > .row > .col-sm-4').length / 3); - if(planRows > 1) - { - for(j = 1; j <= planRows - 1; j++) - { - $('#plansBox .col-sm-4:lt(' + (j * 3) + ')').css('margin-bottom', '10px'); - $('#plansBox .col-sm-4').eq(j * 3).css('padding-right', '6px'); - } - } + $(obj).closest('tr').remove(); + disableSelectedProduct(); } /** diff --git a/module/project/js/edit.js b/module/project/js/edit.js index 73d391e9c5..e676a6a499 100644 --- a/module/project/js/edit.js +++ b/module/project/js/edit.js @@ -32,9 +32,6 @@ $(function() }); }); - adjustProductBoxMargin(); - adjustPlanBoxMargin(); - $(document).on('change', '[name*=products]', function() { var current = $(this).val(); @@ -173,3 +170,127 @@ function setAclList(programID) $('.aclBox').html($('#projectAcl').html()); } } + +/** + * Load branches. + * + * @param int $product + * @access public + * @return void + */ +function loadBranches(product) +{ + /* When selecting a product, delete a plan that is empty by default. */ + $("#planDefault").remove(); + + var chosenProducts = []; + $(".productsBox select[name^='products']").each(function() + { + var $product = $(product); + var productID = $(this).val(); + if(productID > 0 && chosenProducts.indexOf(productID) == -1) chosenProducts.push(productID); + if($product.val() != 0 && $product.val() == $(this).val() && $product.attr('id') != $(this).attr('id') && !multiBranchProducts[$product.val()]) + { + bootbox.alert(errorSameProducts); + $product.val(0); + $product.trigger("chosen:updated"); + return false; + } + }); + + (chosenProducts.length > 1 && model == 'waterfall') ? $('.division').removeClass('hide') : $('.division').addClass('hide'); + + var $tableRow = $(product).closest('.table-row'); + var index = $tableRow.find('select:first').attr('id').replace('products' , ''); + var oldBranch = $(product).attr('data-branch') !== undefined ? $(product).attr('data-branch') : 0; + + if(!multiBranchProducts[$(product).val()]) + { + $tableRow.find('.table-col:last select').val('').trigger('chosen:updated'); + $tableRow.find('.table-col:last').addClass('hidden'); + } + + $.get(createLink('branch', 'ajaxGetBranches', "productID=" + $(product).val() + "&oldBranch=" + oldBranch + "¶m=active"), function(data) + { + if(data) + { + $tableRow.find("select[name^='branch']").replaceWith(data); + $tableRow.find('.table-col:last .chosen-container').remove(); + $tableRow.find('.table-col:last').removeClass('hidden'); + $tableRow.find("select[name^='branch']").attr('multiple', '').attr('name', 'branch[' + index + '][]').attr('id', 'branch' + index).attr('onchange', "loadPlans('#products" + index + "', this)").chosen(); + + disableSelectedProduct(); + } + + var branch = $('#branch' + index); + loadPlans(product, branch); + }); +} + +/** + * Load plans. + * + * @param obj $product + * @param obj $branchID + * @access public + * @return void + */ +function loadPlans(product, branch) +{ + var productID = $(product).val(); + var branchID = $(branch).val() == null ? 0 : '0,' + $(branch).val(); + var planID = $(product).attr('data-plan') !== undefined ? $(product).attr('data-plan') : 0; + var index = $(product).attr('id').replace('products', ''); + + $.get(createLink('product', 'ajaxGetPlans', "productID=" + productID + '&branch=' + branchID + '&planID=' + planID + '&fieldID&needCreate=&expired=unexpired,noclosed¶m=skipParent,multiple'), function(data) + { + if(data) + { + $("div#plan" + index).find("select[name^='plans']").replaceWith(data); + $("div#plan" + index).find('.chosen-container').remove(); + $("div#plan" + index).find('select').attr('name', 'plans[' + productID + ']' + '[]').attr('id', 'plans' + productID).chosen(); + } + }); +} + +/** + * Add new line for link product. + * + * @param obj $obj + * @access public + * @return void + */ +function addNewLine(obj) +{ + var newLine = $(obj).closest('tr').clone(); + var index = 0; + $(".productsBox select[name^='products']").each(function() + { + var id = $(this).attr('id').replace('products' , ''); + + id = parseInt(id); + id ++; + + index = id > index ? id : index; + }) + + newLine.addClass('newLine'); + newLine.find('th').html(''); + newLine.find('.removeLine').css('visibility', 'visible'); + newLine.find('.chosen-container').remove(); + newLine.find('.productsBox .table-col:last').addClass('hidden'); + newLine.find("select[name^='products']").attr('name', 'products[' + index + ']').attr('id', 'products' + index).val('').chosen(); + newLine.find("select[name^='plans']").attr('name', 'plans[' + index + '][' + 0 + '][]').chosen(); + newLine.find("div[id^='plan']").attr('id', 'plan' + index); + + $(obj).closest('tr').after(newLine); + var product = newLine.find("select[name^='products']"); + var branch = newLine.find("select[name^='branch']"); + loadPlans(product, branch); + disableSelectedProduct(); +} + +function removeLine(obj) +{ + $(obj).closest('tr').remove(); +} diff --git a/module/project/lang/de.php b/module/project/lang/de.php index f6d8f3a357..8eeeb2bc01 100644 --- a/module/project/lang/de.php +++ b/module/project/lang/de.php @@ -42,6 +42,7 @@ $lang->project->addWhitelist = 'Project Add Whitelist'; $lang->project->unbindWhitelist = 'Project Remove Whitelist'; $lang->project->manageProducts = 'Manage Products'; $lang->project->manageOtherProducts = 'Manage Other Products'; +$lang->project->manageProductPlan = 'Manage Products And Plans'; $lang->project->copyTitle = 'Please select an project to copy'; $lang->project->errorSameProducts = 'Project cannot be associated with multiple identical products.'; $lang->project->errorSameBranches = 'Project cannot be associated with multiple identical branches.'; @@ -338,6 +339,7 @@ $lang->project->scrum = 'Scrum'; $lang->project->waterfall = 'CMMI'; $lang->project->cannotCreateChild = 'It is not empty, so you cannot add a child. You can add a parent for it, and then create a child.'; $lang->project->emptyPM = 'No manager'; +$lang->project->emptyBranch = 'Branch can not be empty!'; $lang->project->cannotChangeToCat = "It is not empty, so you cannot change it to a parent."; $lang->project->cannotCancelCat = "It has child projects, so you cannot unmark the parent."; $lang->project->parentBeginEnd = "Parent begin&end date: %s ~ %s"; diff --git a/module/project/lang/en.php b/module/project/lang/en.php index 9ee36cc2a1..b94fa2148b 100644 --- a/module/project/lang/en.php +++ b/module/project/lang/en.php @@ -42,6 +42,7 @@ $lang->project->addWhitelist = 'Project Add Whitelist'; $lang->project->unbindWhitelist = 'Project Remove Whitelist'; $lang->project->manageProducts = 'Manage Products'; $lang->project->manageOtherProducts = 'Manage Other Products'; +$lang->project->manageProductPlan = 'Manage Products And Plans'; $lang->project->copyTitle = 'Please select an project to copy'; $lang->project->errorSameProducts = 'Project cannot be associated with multiple identical products.'; $lang->project->errorSameBranches = 'Project cannot be associated with multiple identical branches.'; @@ -338,6 +339,7 @@ $lang->project->scrum = 'Scrum'; $lang->project->waterfall = 'Waterfall'; $lang->project->cannotCreateChild = 'It is not empty, so you cannot add a child. You can add a parent for it, and then create a child.'; $lang->project->emptyPM = 'No manager'; +$lang->project->emptyBranch = 'Branch can not be empty!'; $lang->project->cannotChangeToCat = "It is not empty, so you cannot change it to a parent."; $lang->project->cannotCancelCat = "It has child projects, so you cannot unmark the parent."; $lang->project->parentBeginEnd = "Parent begin&end date: %s ~ %s"; diff --git a/module/project/lang/fr.php b/module/project/lang/fr.php index f56ac61792..ee429c5607 100644 --- a/module/project/lang/fr.php +++ b/module/project/lang/fr.php @@ -42,6 +42,7 @@ $lang->project->addWhitelist = 'Project Add Whitelist'; $lang->project->unbindWhitelist = 'Project Remove Whitelist'; $lang->project->manageProducts = 'Manage Products'; $lang->project->manageOtherProducts = 'Manage Other Products'; +$lang->project->manageProductPlan = 'Manage Products And Plans'; $lang->project->copyTitle = 'Please select an project to copy'; $lang->project->errorSameProducts = 'Project cannot be associated with multiple identical products.'; $lang->project->errorSameBranches = 'Project cannot be associated with multiple identical branches.'; @@ -338,6 +339,7 @@ $lang->project->scrum = 'Scrum'; $lang->project->waterfall = 'CMMI'; $lang->project->cannotCreateChild = 'It is not empty, so you cannot add a child. You can add a parent for it, and then create a child.'; $lang->project->emptyPM = 'No manager'; +$lang->project->emptyBranch = 'Branch can not be empty!'; $lang->project->cannotChangeToCat = "It is not empty, so you cannot change it to a parent."; $lang->project->cannotCancelCat = "It has child projects, so you cannot unmark the parent."; $lang->project->parentBeginEnd = "Parent begin&end date: %s ~ %s"; diff --git a/module/project/lang/vi.php b/module/project/lang/vi.php index e6b35cab56..46a718201c 100644 --- a/module/project/lang/vi.php +++ b/module/project/lang/vi.php @@ -42,6 +42,7 @@ $lang->project->addWhitelist = 'Project Add Whitelist'; $lang->project->unbindWhitelist = 'Project Remove Whitelist'; $lang->project->manageProducts = 'Manage Products'; $lang->project->manageOtherProducts = 'Manage Other Products'; +$lang->project->manageProductPlan = 'Manage Products And Plans'; $lang->project->copyTitle = 'Please select an project to copy'; $lang->project->errorSameProducts = 'Project cannot be associated with multiple identical products.'; $lang->project->errorSameBranches = 'Project cannot be associated with multiple identical branches.'; @@ -335,6 +336,7 @@ $lang->project->scrum = 'Scrum'; $lang->project->waterfall = 'Waterfall'; $lang->project->cannotCreateChild = 'It is not empty, so you cannot add a child. You can add a parent for it, and then create a child.'; $lang->project->emptyPM = 'No manager'; +$lang->project->emptyBranch = 'Branch can not be empty!'; $lang->project->cannotChangeToCat = "It is not empty, so you cannot change it to a parent."; $lang->project->cannotCancelCat = "It has child projects, so you cannot unmark the parent."; $lang->project->parentBeginEnd = "Parent begin&end date: %s ~ %s"; diff --git a/module/project/lang/zh-cn.php b/module/project/lang/zh-cn.php index e8f21c9b32..acd85f63a7 100644 --- a/module/project/lang/zh-cn.php +++ b/module/project/lang/zh-cn.php @@ -42,6 +42,7 @@ $lang->project->addWhitelist = '项目添加白名单'; $lang->project->unbindWhitelist = '项目删除白名单'; $lang->project->manageProducts = '关联产品'; $lang->project->manageOtherProducts = '关联其他产品'; +$lang->project->manageProductPlan = '关联产品和计划'; $lang->project->copyTitle = '请选择要复制的项目'; $lang->project->errorSameProducts = '项目不能关联多个相同的产品。'; $lang->project->errorSameBranches = '项目不能关联多个相同的分支。'; @@ -233,7 +234,7 @@ $lang->project->hundredMillion = '亿'; $lang->project->unitList['CNY'] = '人民币'; $lang->project->unitList['USD'] = '美元'; $lang->project->unitList['HKD'] = '港元'; -$lang->project->unitList['NTD'] = '台元'; +$lang->project->unitList['NTD'] = '台币'; $lang->project->unitList['EUR'] = '欧元'; $lang->project->unitList['DEM'] = '马克'; $lang->project->unitList['CHF'] = '瑞士法郎'; @@ -338,6 +339,7 @@ $lang->project->scrum = 'Scrum'; $lang->project->waterfall = '瀑布'; $lang->project->cannotCreateChild = '该项目已经有实际的内容,无法直接添加子项目。您可以为当前项目创建一个父项目,然后在新的父项目下面添加子项目。'; $lang->project->emptyPM = '暂无'; +$lang->project->emptyBranch = '分支不能为空!'; $lang->project->cannotChangeToCat = "该项目已经有实际的内容,无法修改为父项目"; $lang->project->cannotCancelCat = "该项目下已经有子项目,无法取消父项目标记"; $lang->project->parentBeginEnd = "父项目起止时间:%s ~ %s"; @@ -379,4 +381,4 @@ $lang->project->copyProject = new stdClass(); $lang->project->copyProject->nameTips = '『项目名称』不可重复需要修改。'; $lang->project->copyProject->codeTips = '『项目代号』不可重复需要修改。'; $lang->project->copyProject->endTips = '『计划完成』不能为空。'; -$lang->project->copyProject->daysTips = '『可用工作日』应当是数字。'; \ No newline at end of file +$lang->project->copyProject->daysTips = '『可用工作日』应当是数字。'; diff --git a/module/project/lang/zh-tw.php b/module/project/lang/zh-tw.php index 454946432b..ceafb35e2c 100644 --- a/module/project/lang/zh-tw.php +++ b/module/project/lang/zh-tw.php @@ -190,7 +190,7 @@ $lang->project->tenThousand = '萬'; $lang->project->unitList['CNY'] = '人民幣'; $lang->project->unitList['USD'] = '美元'; $lang->project->unitList['HKD'] = '港元'; -$lang->project->unitList['NTD'] = '台元'; +$lang->project->unitList['NTD'] = '台幣'; $lang->project->unitList['EUR'] = '歐元'; $lang->project->unitList['DEM'] = '馬克'; $lang->project->unitList['CHF'] = '瑞士法郎'; diff --git a/module/project/model.php b/module/project/model.php index 4718eb0b9b..b2ab359b66 100644 --- a/module/project/model.php +++ b/module/project/model.php @@ -1243,6 +1243,20 @@ class projectModel extends model } } + if($_POST['products']) + { + $topProgramID = $this->loadModel('program')->getTopByID($project->parent); + $multipleProducts = $this->loadModel('product')->getMultiBranchPairs($topProgramID); + foreach($_POST['products'] as $index => $productID) + { + if(isset($multipleProducts[$productID]) and empty($_POST['branch'][$index])) + { + dao::$errors[] = $this->lang->project->emptyBranch; + return false; + } + } + } + $program = new stdClass(); if($project->parent) { @@ -1485,6 +1499,7 @@ class projectModel extends model ->setDefault('team', $this->post->name) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', helper::now()) + ->setDefault('parent', 0) ->setIF($this->post->delta == 999, 'end', LONG_TIME) ->setIF($this->post->delta == 999, 'days', 0) ->setIF($this->post->begin == '0000-00-00', 'begin', '') @@ -1508,12 +1523,27 @@ class projectModel extends model if(dao::isError()) return false; } + if($_POST['products']) + { + $topProgramID = $this->loadModel('program')->getTopByID($project->parent); + $multipleProducts = $this->loadModel('product')->getMultiBranchPairs($topProgramID); + foreach($_POST['products'] as $index => $productID) + { + if(isset($multipleProducts[$productID]) and empty($_POST['branch'][$index])) + { + dao::$errors[] = $this->lang->project->emptyBranch; + return false; + } + } + } + /* Judge products not empty. */ $linkedProductsCount = 0; foreach($_POST['products'] as $product) { if(!empty($product)) $linkedProductsCount++; } + if(empty($linkedProductsCount)) { dao::$errors[] = $this->lang->project->errorNoProducts; @@ -2403,24 +2433,29 @@ class projectModel extends model $oldPlan = 0; $branch = isset($branches[$i]) ? $branches[$i] : 0; - if(isset($existedProducts[$productID][$branch])) continue; + if(!is_array($branch)) $branch = array($branch => $branch); - if(isset($oldProjectProducts[$productID][$branch])) + foreach($branch as $branchID) { - $oldProjectProduct = $oldProjectProducts[$productID][$branch]; - if($this->app->rawMethod != 'edit') $oldPlan = $oldProjectProduct->plan; + if(isset($existedProducts[$productID][$branchID])) continue; + + if(isset($oldProjectProducts[$productID][$branchID])) + { + $oldProjectProduct = $oldProjectProducts[$productID][$branchID]; + if($this->app->rawMethod != 'edit') $oldPlan = $oldProjectProduct->plan; + } + + $data = new stdclass(); + $data->project = $projectID; + $data->product = $productID; + $data->branch = $branchID; + $data->plan = isset($plans[$productID]) ? implode(',', $plans[$productID]) : $oldPlan; + $data->plan = trim($data->plan, ','); + $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; + + $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); + $existedProducts[$productID][$branchID] = true; } - - $data = new stdclass(); - $data->project = $projectID; - $data->product = $productID; - $data->branch = $branch; - $data->plan = isset($plans[$productID][$branch]) ? implode(',', $plans[$productID][$branch]) : $oldPlan; - $data->plan = trim($data->plan, ','); - $data->plan = empty($data->plan) ? 0 : ",$data->plan,"; - - $this->dao->insert(TABLE_PROJECTPRODUCT)->data($data)->exec(); - $existedProducts[$productID][$branch] = true; } /* Delete the execution linked products that is not linked with the execution. */ diff --git a/module/project/view/create.html.php b/module/project/view/create.html.php index c10995d240..22221761dd 100755 --- a/module/project/view/create.html.php +++ b/module/project/view/create.html.php @@ -26,7 +26,7 @@ product->name);?> -project->manageProducts);?> +project->manageProductPlan);?> project->budgetOverrun);?> project->currencySymbol)?> project->parentBudget);?> @@ -143,64 +143,91 @@
+ + + - project->manageProducts;?> - + project->manageProductPlan;?> +
- - - branches as $branch):?> -
- type != 'normal' and isset($branchGroups[$product->id]);?> -
- id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "'");?> - - id], $branch, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\"");?> +
+
+
+ type != 'normal' and isset($branchGroups[$product->id]);?> +
'> + product->common;?> + id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='" . $product->type . "'");?> +
+
+
'> +
+ product->branchName['branch'];?> + branches);?> + id]) ? $branchGroups[$product->id] : array(), $branchIdList, "class='form-control chosen' multiple onchange=\"loadPlans('#products{$i}', this)\"");?> +
+
- - - -
' style="padding-right: 6px;"> -
- - - - project->addProduct, '', "onchange=addNewProduct(this);");?> -
-
- +
+
> + product->plan;?> + id][]", isset($productPlans[$product->id]) ? $productPlans[$product->id] : array(), $product->plans, "class='form-control chosen' multiple");?> +
+ + > +
- + + + - execution->linkPlan;?> - + project->manageProductPlan;?> +
- - - - id, array(0 => ''));?> - branches as $branch):?> - ''));?> -
id . '][' . $branch . '][]', $plans, '', "class='form-control chosen' multiple");?>
- - - - -
- +
+
+
+
+ product->common;?> + + + + project->addProduct, '', "onchange=addNewProduct(this);");?> +
+
+ +
+
+ +
+
+
+
+ product->plan;?> + +
+ + +
+
+
+
+ + project->division;?> diff --git a/module/project/view/edit.html.php b/module/project/view/edit.html.php index b9430faf21..32d0f8f238 100755 --- a/module/project/view/edit.html.php +++ b/module/project/view/edit.html.php @@ -146,49 +146,45 @@ hasProduct and $this->config->vision != 'lite'):?> + + - project->manageProducts;?> - + project->manageProductPlan;?> +
- - - type != 'normal' and isset($branchGroups[$product->id]);?> - id] as $branchID => $branch):?> -
-
- id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='" . $product->type . "' data-lastBranch='" . $branchID . "'");?> - - id], $branchID, "class='form-control chosen' onchange=\"loadPlans('#products{$i}', this.value)\" data-last='" . $branchID . "'");?> +
+
+
+ type != 'normal' and isset($branchGroups[$product->id]);?> +
'> + product->common;?> + id, "class='form-control chosen' onchange='loadBranches(this)' data-last='" . $product->id . "' data-type='" . $product->type . "'");?> +
+
+
'> +
+ product->branchName['branch'];?> + branches);?> + id]) ? $branchGroups[$product->id] : array(), $branchIdList, "class='form-control chosen' multiple onchange=\"loadPlans('#products{$i}', this)\"");?> +
+
- - - -
' style="padding-right: 6px;"> -
- - +
+
> + product->plan;?> + id][]", isset($productPlans[$product->id]) ? $productPlans[$product->id] : array(), $product->plans, "class='form-control chosen' multiple");?> +
+ + > +
- - execution->linkPlan;?> - -
- - - id] as $branchID => $branch):?> - id][$branchID]) ? $productPlans[$product->id][$branchID] : array();?> -
id}][{$branchID}][]", $plans, $branches[$product->id][$branchID]->plan, "class='form-control chosen' multiple");?>
- - - -
-
- - + + model == 'waterfall'):?> division and count($linkedProducts) < 2) ? 'hide' : '';?> diff --git a/module/story/control.php b/module/story/control.php index 1f4128f858..f2e1fa60fa 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(); @@ -437,8 +440,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->planID = $planID; + $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; $this->view->color = $color; @@ -931,6 +933,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; @@ -1023,6 +1026,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 +1264,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 +1435,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 +1656,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 +1893,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 +1916,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 +1938,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 +2286,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'); @@ -2970,4 +3015,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..b40eee1b83 100644 --- a/module/story/js/view.js +++ b/module/story/js/view.js @@ -9,17 +9,14 @@ $(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'); + }); }); 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 93a96f3a85..05472837b3 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; } /** @@ -2312,7 +2566,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 +2586,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 +2607,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 +2651,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 +3765,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 ; @@ -5748,6 +6033,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') @@ -5853,6 +6148,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; } @@ -5980,7 +6288,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 = ''; @@ -5991,6 +6308,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') @@ -6079,22 +6401,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; } @@ -6133,4 +6466,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..7b7152b54e --- /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 = $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;?>
    - + - +
    - -
    - -
    story->module;?>
    - - "; - echo html::a($this->createLink('tree', 'browse', "rootID=$productID&view=story¤tModuleID=0&branch=$branch", '', true), $lang->tree->manage, '', "class='text-primary' data-toggle='modal' data-type='iframe' data-width='90%'"); - echo '  '; - echo html::a("javascript:void(0)", $lang->refreshIcon, '', "class='refresh' title='refresh' onclick='loadProductModules($productID)'"); - echo '
    '; - } - ?> + switchBranch'> +
    +
    story->assignedTo;?>
    + +
    + + switchBranch'> + switchBranch'> +
    +
    story->module;?>
    + "; + echo html::a($this->createLink('tree', 'browse', "rootID=$productID&view=story¤tModuleID=0&branch=$branch", '', true), $lang->tree->manage, '', "class='text-primary' data-toggle='modal' data-type='iframe' data-width='90%'"); + echo '  '; + echo html::a("javascript:void(0)", $lang->refreshIcon, '', "class='refresh' title='refresh' onclick='loadProductModules($productID)'"); + echo '
    '; + } + ?>
    - - story->assignedTo : $lang->story->planAB;?> - + switchBranch' > + product->branchName[$product->type];?> + +
    +
    +
    + product->branch, $lang->product->branchName[$product->type])?> + +
    +
    +
    +
    + story->module?> + +
    +
    +
    +
    + story->plan;?> + +
    +
    +
    + + +
    + +
    + + + + + + 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..2cd17c4cf0 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)?>
    @@ -98,6 +100,22 @@
    printExtendFields($story, 'div', 'position=left');?> + +
    +
    + story->changeSyncTip;?> + +
    +
    +
    +
      + + +
    +
    +
    +
    +
    story->comment;?>
    @@ -174,8 +192,9 @@ story->plan;?>
    - session->currentProductType != 'normal' and empty($story->branch)) ? true : false;?> - plan, "class='form-control chosen'" . ($multiple ? ' multiple' : '')); + planTitle) ? count($story->planTitle) : 0?> + session->currentProductType != 'normal' and empty($story->branch) and $planCount > 1) ? 'multiple' : '';?> + plan, "class='form-control chosen' " . $multiple); if(count($plans) == 1) { echo ""; diff --git a/module/story/view/header.html.php b/module/story/view/header.html.php index 5d84b1a6b8..0610c437f3 100644 --- a/module/story/view/header.html.php +++ b/module/story/view/header.html.php @@ -11,6 +11,17 @@ */ function loadProduct(productID) { + if(page == 'edit' && siblings && productID != oldProductID) + { + confirmRelievedSiblings = confirm(relievedSiblingsTip); + if(!confirmRelievedSiblings) + { + $('#product').val(oldProductID); + $('#product').trigger("chosen:updated"); + return false; + } + } + if(typeof parentStory != 'undefined' && parentStory) { confirmLoadProduct = confirm(moveChildrenTips); @@ -81,20 +92,81 @@ function loadProductBranches(productID) if(page == 'create') param = 'active'; $('#branch').remove(); $('#branch_chosen').remove(); - $.get(createLink('branch', 'ajaxGetBranches', "productID=" + productID + "&oldBranch=0¶m=" + param + "&projectID=" + executionID), function(data) - { - var $product = $('#product'); - var $inputGroup = $product.closest('.input-group'); - $inputGroup.find('.input-group-addon').toggleClass('hidden', !data); - if(data) - { - $inputGroup.append(data); - $('#branch').css('width', config.currentMethod == 'create' ? '120px' : '65px').chosen(); - } - $inputGroup.fixInputGroup(); - loadProductModules(productID, $('#branch').val()); - loadProductPlans(productID, $('#branch').val()); + var isSiblings = storyType == 'story' && page == 'create' ? 'yes' : 'no'; + $.get(createLink('branch', 'ajaxGetBranches', "productID=" + productID + "&oldBranch=0¶m=" + param + "&projectID=" + executionID + "&withMainBranch=1&isSiblings=" + isSiblings), function(data) + { + if(storyType == 'story' && page == 'create') + { + var newProductType = data ? 'normal' : 'branch'; + if(originProductType != newProductType) + { + $('.switchBranch').toggleClass('hidden'); + $('.switchBranch').toggleClass('disable'); + } + originProductType = newProductType; + + $('tr[class^="addBranchesBox"]').remove(); + + if(data) + { + $.ajaxSettings.async = false; + $.get(createLink('product', 'ajaxGetProductById', "productID=" + productID), function(data) + { + $.cookie('branchSourceName', data.branchSourceName) + $.cookie('branchName', data.branchName) + }, 'json') + $.ajaxSettings.async = true; + + gap = $('#product').closest('td').next().width(); + $('#planIdBox').css('flex', '0 0 ' + gap + 'px') + + $('.switchBranch #branchBox .input-group .input-group-addon').html($.cookie('branchSourceName')) + $('.switchBranch #branchBox').closest('td').prev().html($.cookie('branchName')) + + /* reload branch */ + $('#branches0').replaceWith(data); + $('#branches0' + "_chosen").remove(); + $('#branches0').next('.picker').remove(); + $('#branches0').chosen(); + + loadModuleForSiblings(productID, 0, 0) + loadPlanForSiblings(productID, 0, 0) + + /* Init multi branch icon-plus. */ + 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') + } + else + { + $('.table-col .icon-plus').parent().css('pointer-events', 'auto') + $('.table-col .icon-plus').parent().removeClass('disabled') + } + } + else + { + loadProductModules(productID, 0); + loadProductPlans(productID, 0); + } + } + else + { + var $product = $('#product'); + var $inputGroup = $product.closest('.input-group'); + $inputGroup.find('.input-group-addon').toggleClass('hidden', !data); + if(data) + { + $inputGroup.append(data); + $('#branch').css('width', config.currentMethod == 'create' ? '120px' : '65px').chosen(); + } + $inputGroup.fixInputGroup(); + + loadProductModules(productID, $('#branch').val()); + loadProductPlans(productID, $('#branch').val()); + } + }) } @@ -139,9 +211,12 @@ function loadProductPlans(productID, branch) { if(typeof(branch) == 'undefined') branch = 0; if(!branch) branch = 0; - var expired = config.currentMethod == 'create' ? 'unexpired' : ''; - planLink = createLink('product', 'ajaxGetPlans', 'productID=' + productID + '&branch=' + branch + '&planID=' + $('#plan').val() + '&fieldID=&needCreate=true&expired='+ expired +'¶m=skipParent,' + config.currentMethod); - var $planIdBox = $('#planIdBox'); + + var param = rawMethod == 'edit' ? 'skipParent|forStory' : 'skipParent'; + var expired = config.currentMethod == 'create' ? 'unexpired' : ''; + var planLink = createLink('product', 'ajaxGetPlans', 'productID=' + productID + '&branch=' + branch + '&planID=' + $('#plan').val() + '&fieldID=&needCreate=true&expired='+ expired +'¶m=skipParent|forStory|' + config.currentMethod); + var $planIdBox = rawMethod == 'create' ? $('.switchBranch #planIdBox') : $('#planIdBox'); + $planIdBox.load(planLink, function() { $planIdBox.find('#plan').chosen(); diff --git a/module/story/view/view.html.php b/module/story/view/view.html.php index 5ca218ab56..456a984ce2 100644 --- a/module/story/view/view.html.php +++ b/module/story/view/view.html.php @@ -419,17 +419,27 @@
    + +
    +
      + +
    +
    + config->URAndSR and !$hiddenURS):?> -
    +
    ' id='legendStories'>
      id] = $item->title; foreach($relation as $id => $title) { - echo "
    • " . ($canViewLinkStory ? html::a($this->createLink('story', 'view', "id=$id&version=0¶m=0&storyType=$relationType", '', true), "#$id $title", '', "class='iframe' data-width='80%'") : "#$id $title"); - echo html::a($this->createLink('story', 'linkStory', "storyID=$story->id&type=remove&linkedID=$id&browseType=&queryID=0&storyType=$story->type"), '', 'hiddenwin', "class='hide removeButton'"); + echo "
    • " . ($canViewLinkStory ? html::a($this->createLink('story', 'view', "id=$id&version=0¶m=0&storyType=$relationType", '', true), "#$id $title", '', "class='iframe' data-width='80%'") : "#$id $title"); + echo html::a($this->createLink('story', 'linkStory', "storyID=$story->id&type=remove&linkedID=$id&browseType=&queryID=0&storyType=$story->type"), '', 'hiddenwin', "class='hide removeButton'"); } ?> type == 'story') ? $lang->story->requirement : $lang->story->story;?>
    • type, 'linkStory')) echo html::a($this->createLink('story', 'linkStory', "storyID=$story->id&type=linkStories&linkedID=0&browseType=&queryID=0&storyType=$story->type", '', true), $lang->story->link . $linkLang, '', "class='btn btn-info iframe' data-width='95%' id='linkButton'");?> - story->unlink, '', "class='btn btn-info' id='unlinkStory'");?>
    type == 'story'):?> -
    +
      tasks as $executionTasks) @@ -662,7 +671,6 @@ js::set('unlink', $lang->story->unlink); js::set('cancel', $lang->cancel); js::set('rawModule', $this->app->rawModule); ?> - + diff --git a/module/testcase/view/view.html.php b/module/testcase/view/view.html.php index 979324e339..716a59ed69 100644 --- a/module/testcase/view/view.html.php +++ b/module/testcase/view/view.html.php @@ -176,7 +176,7 @@ foreach($modulePath as $key => $module) { - if($tab == 'qa' || $tab == 'ops') + if($tab == 'qa' || $tab == 'ops' || $tab == 'product') { if($isLibCase) { diff --git a/module/tree/control.php b/module/tree/control.php index 1041211554..90280d30ea 100644 --- a/module/tree/control.php +++ b/module/tree/control.php @@ -552,13 +552,13 @@ class tree extends control $changeFunc = ''; if($viewType == 'bug' or $viewType == 'case') $changeFunc = "onchange='loadModuleRelated()'"; if($viewType == 'task') $changeFunc = "onchange='setStories(this.value, $rootID)'"; - $field = $fieldID ? "modules[$fieldID]" : 'module'; + $field = $fieldID !== '' ? "modules[$fieldID]" : 'module'; $currentModule = $this->tree->getById($currentModuleID); $currentModuleID = (isset($currentModule->branch) and $currentModule->branch == 0) ? $currentModuleID : 0; $output = html::select("$field", $optionMenu, $currentModuleID, "class='form-control' $changeFunc"); - if(count($optionMenu) == 1 and $needManage) + if(count($optionMenu) == 1 and $needManage !== 'false') { $output .= ""; $output .= html::a($this->createLink('tree', 'browse', "rootID=$rootID&view=$viewType¤tModuleID=0&branch=$branch", '', true), $this->lang->tree->manage, '', "class='text-primary' data-toggle='modal' data-type='iframe' data-width='95%'"); diff --git a/www/js/my.full.js b/www/js/my.full.js index 30bf823463..24a60642db 100644 --- a/www/js/my.full.js +++ b/www/js/my.full.js @@ -1044,12 +1044,7 @@ function disableSelectedProduct() $("select[id^='products']").each(function() { var selectedProduct = $(this).val(); - if(selectedProduct != 0 && $.inArray(selectedProduct, selectedVal) < 0 && !multiBranchProducts[selectedProduct]) selectedVal.push(selectedProduct); - if(multiBranchProducts[selectedProduct]) - { - var isDisabled = checkMultiProducts(this); - if(isDisabled) selectedVal.push(selectedProduct); - } + if(selectedProduct != 0 && $.inArray(selectedProduct, selectedVal) < 0) selectedVal.push(selectedProduct); }) $("select[id^='products']").each(function()