From 2d0d55d1cd1962ca143327fd508f2436f9775c63 Mon Sep 17 00:00:00 2001 From: wangyidong Date: Wed, 5 Jul 2023 11:20:06 +0800 Subject: [PATCH] * Finish task #96577. --- module/story/config/form.php | 8 +- module/story/control.php | 112 ++++++---------------------- module/story/css/batchclose.ui.css | 2 + module/story/js/batchclose.ui.js | 47 ++++++++++++ module/story/model.php | 83 +++++++-------------- module/story/ui/batchclose.html.php | 52 +++++++++++++ module/story/ui/batchedit.html.php | 2 +- module/story/zen.php | 94 ++++++++++++++++++++++- 8 files changed, 254 insertions(+), 146 deletions(-) create mode 100644 module/story/css/batchclose.ui.css create mode 100644 module/story/js/batchclose.ui.js create mode 100644 module/story/ui/batchclose.html.php diff --git a/module/story/config/form.php b/module/story/config/form.php index 795432231b..5cfb6745f3 100644 --- a/module/story/config/form.php +++ b/module/story/config/form.php @@ -65,11 +65,17 @@ $config->story->form->batchEdit['assignedTo'] = array('type' => 'string', 'wid $config->story->form->batchEdit['source'] = array('type' => 'string', 'width' => '160px', 'control' => 'select', 'required' => false, 'default' => '', 'options' => array_filter($lang->story->sourceList)); $config->story->form->batchEdit['sourceNote'] = array('type' => 'string', 'width' => '200px', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->batchEdit['status'] = array('type' => 'string', 'width' => '80px', 'control' => 'static', 'required' => false, 'default' => 0, 'options' => array_filter($lang->story->statusList)); -$config->story->form->batchEdit['stage'] = array('type' => 'string', 'width' => '100px', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array_filter($lang->story->stageList)); +$config->story->form->batchEdit['stage'] = array('type' => 'string', 'width' => '120px', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array_filter($lang->story->stageList)); $config->story->form->batchEdit['closedBy'] = array('type' => 'string', 'width' => '136px', 'control' => 'select', 'required' => false, 'default' => '', 'options' => 'users'); $config->story->form->batchEdit['closedReason'] = array('type' => 'string', 'width' => '80px', 'control' => 'select', 'required' => false, 'default' => '', 'options' => array_filter($lang->story->reasonList)); $config->story->form->batchEdit['keywords'] = array('type' => 'string', 'width' => '200px', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); +$config->story->form->batchClose = array(); +$config->story->form->batchClose['title'] = array('type' => 'string', 'width' => '300px', 'control' => 'text', 'required' => false, 'filter' => 'trim', 'base' => true); +$config->story->form->batchClose['status'] = array('type' => 'string', 'width' => '90px', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array_filter($lang->story->statusList)); +$config->story->form->batchClose['closedReason'] = array('type' => 'string', 'width' => '300px', 'control' => 'select', 'required' => true, 'default' => ''); +$config->story->form->batchClose['comment'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); + $config->story->form->change = array(); $config->story->form->change['reviewer'] = array('type' => 'array', 'control' => 'multi-select', 'required' => true, 'default' => '', 'filter' => 'join', 'options' => ''); $config->story->form->change['title'] = array('type' => 'string', 'control' => 'text', 'required' => true, 'filter' => 'trim'); diff --git a/module/story/control.php b/module/story/control.php index b05b063f0c..5b671a4e98 100755 --- a/module/story/control.php +++ b/module/story/control.php @@ -1166,17 +1166,31 @@ class story extends control */ public function batchClose($productID = 0, $executionID = 0, $storyType = 'story', $from = '') { - $this->app->loadLang('bug'); - if(!$this->post->storyIdList) return print(js::locate($this->session->storyList, 'parent')); + if(!$this->post->storyIdList) return $this->send(array('result' => 'success', 'load' => $this->session->storyList)); $storyIdList = $this->post->storyIdList; $storyIdList = array_unique($storyIdList); + $this->app->loadLang('bug'); $this->story->replaceURLang($storyType); + if($this->post->comment) + { + $stories = $this->storyZen->buildStoriesForBatchClose(); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $this->story->batchClose($stories); + if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); + + $this->loadModel('score')->create('ajax', 'batchOther'); + return $this->send(array('result' => 'success', 'load' => $this->session->storyList)); + } + + $this->storyZen->setMenuForBatchClose($productID, $executionID, $from); + /* Get edited stories. */ - $stories = $this->story->getByList($storyIdList); - $ignoreTwins = array(); - $twinsCount = array(); + $stories = $this->story->getByList($storyIdList); + $ignoreTwins = array(); + $twinsCount = array(); foreach($stories as $story) { if(!empty($ignoreTwins) and isset($ignoreTwins[$story->id])) @@ -1185,16 +1199,9 @@ class story extends control continue; } - if($story->parent == -1) - { - $skipStory[] = $story->id; - unset($stories[$story->id]); - } - if($story->status == 'closed') - { - $closedStory[] = $story->id; - unset($stories[$story->id]); - } + if($story->parent == -1) $skipStory[] = $story->id; + if($story->status == 'closed') $closedStory[] = $story->id; + if($story->parent == -1 || $story->status == 'closed') unset($stories[$story->id]); if(!empty($story->twins)) { @@ -1207,86 +1214,15 @@ class story extends control } } - if($this->post->comments) - { - $allChanges = $this->story->batchClose(); - - if($allChanges) - { - foreach($allChanges as $storyID => $changes) - { - $preStatus = $stories[$storyID]->status; - $isChanged = $stories[$storyID]->changedBy ? true : false; - if($preStatus == 'reviewing') $preStatus = $isChanged ? 'changing' : 'draft'; - - $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]->twins)) $this->story->syncTwins($storyID, $stories[$storyID]->twins, $changes, 'Closed'); - } - - $this->dao->update(TABLE_STORY)->set('assignedTo')->eq('closed')->where('id')->in(array_keys($allChanges))->exec(); - } - - if(!dao::isError()) $this->loadModel('score')->create('ajax', 'batchOther'); - return print(js::locate($this->session->storyList, 'parent')); - } - $errorTips = ''; if(isset($closedStory)) $errorTips .= sprintf($this->lang->story->closedStory, implode(',', $closedStory)); if(isset($skipStory)) $errorTips .= sprintf($this->lang->story->skipStory, implode(',', $skipStory)); - if(isset($skipStory) || isset($closedStory)) echo js::alert($errorTips); - /* The stories of a product. */ - if($this->app->tab == 'product') - { - $this->product->setMenu($productID); - $product = $this->product->getByID($productID); - $this->view->title = $product->name . $this->lang->colon . $this->lang->story->batchClose; - } - /* The stories of a execution. */ - elseif($executionID) - { - $this->lang->story->menu = $this->lang->execution->menu; - $this->lang->story->menuOrder = $this->lang->execution->menuOrder; - $this->execution->setMenu($executionID); - $execution = $this->execution->getByID($executionID); - $this->view->title = $execution->name . $this->lang->colon . $this->lang->story->batchClose; - } - else - { - if($this->app->tab == 'project') - { - $this->project->setMenu($this->session->project); - $this->view->title = $this->lang->story->batchEdit; - } - else - { - $this->lang->story->menu = $this->lang->my->menu; - $this->lang->story->menuOrder = $this->lang->my->menuOrder; - - if($from == 'work') $this->lang->my->menu->work['subModule'] = 'story'; - if($from == 'contribute') $this->lang->my->menu->contribute['subModule'] = 'story'; - - $this->view->title = $this->lang->story->batchEdit; - } - } - - /* Judge whether the editedStories is too large and set session. */ - $countInputVars = count($stories) * $this->config->story->batchClose->columns; - $showSuhosinInfo = common::judgeSuhosinSetting($countInputVars); - if($showSuhosinInfo) $this->view->suhosinInfo = extension_loaded('suhosin') ? sprintf($this->lang->suhosinInfo, $countInputVars) : sprintf($this->lang->maxVarsInfo, $countInputVars); - - unset($this->lang->story->reasonList['subdivided']); - - $this->view->moduleOptionMenu = $this->tree->getOptionMenu($productID, 'story'); - $this->view->plans = $this->loadModel('productplan')->getPairs($productID); $this->view->productID = $productID; $this->view->stories = $stories; - $this->view->storyIdList = $storyIdList; $this->view->storyType = $storyType; - $this->view->reasonList = $this->lang->story->reasonList; $this->view->twinsCount = $twinsCount; + $this->view->errorTips = $errorTips; $this->display(); } @@ -2374,6 +2310,6 @@ class story extends control $story = $this->story->getByID($storyID); $stories = $this->story->getProductStoryPairs($story->product, $story->branch, 0, 'all', 'id_desc', 0, '', $story->type); - return print html::select("duplicateStoryIDList[$storyID]", $stories, '', "class='form-control' placeholder='{$this->lang->bug->placeholder->duplicate}'"); + return print html::select("duplicateStory[$storyID]", $stories, '', "class='form-control' placeholder='{$this->lang->bug->placeholder->duplicate}'"); } } diff --git a/module/story/css/batchclose.ui.css b/module/story/css/batchclose.ui.css new file mode 100644 index 0000000000..09cb2ca10f --- /dev/null +++ b/module/story/css/batchclose.ui.css @@ -0,0 +1,2 @@ +.duplicateStoryBox{width:50%;} +.childStoryBox{width:50%;} diff --git a/module/story/js/batchclose.ui.js b/module/story/js/batchclose.ui.js new file mode 100644 index 0000000000..9ab31ffa9f --- /dev/null +++ b/module/story/js/batchclose.ui.js @@ -0,0 +1,47 @@ +window.renderRowData = function($row, index, story) +{ + var $title = $row.find('.form-batch-input[data-name="title"]'); + var $status = $row.find('.form-batch-input[data-name="status"]'); + + $title.attr('disabled', 'disabled').attr('title', story.title).after(""); + $status.attr('disabled', 'disabled'); + + if(story.twins) + { + $title.wrap("
"); + $title.after("" + langTwins + "" + twinsCount[story.id] + ""); + } + + var $closedReason = $row.find('.form-batch-input[data-name="closedReason"]'); + $closedReason.attr('onchange', 'setDuplicateAndChild(this)').wrap("
"); + + var appendStoryHtml = ""; + appendStoryHtml += "'; + $closedReason.after(appendStoryHtml); + + if(story.status == 'draft') $closedReason.find('option[value="cancel"]').remove(); +}; + +window.setDuplicateAndChild = function(obj) +{ + var $this = $(obj); + $this.closest('.input-group').find('.duplicateStoryBox').toggleClass('hidden', $this.val() != 'duplicate'); +}; + +window.getDuplicateStories = function(obj) +{ + var $this = $(obj); + var options = $this.find('option').length; + if(options <= 1) + { + var storyID = $this.data('id'); + var link = $.createLink('story', 'ajaxGetStoryPairs', 'storyID=' + storyID); + + $this.closest('.duplicateStoryBox').load(link, function() + { + $(this).find('select').addClass('form-batch-input').attr('data-name', 'duplicateStory'); + }); + } +}; diff --git a/module/story/model.php b/module/story/model.php index 27adcee01d..2156cb8635 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -1723,76 +1723,51 @@ class storyModel extends model /** * Batch close story. * + * @param array $stories * @access public * @return void */ - public function batchClose() + public function batchClose(array $stories) { - /* Init vars. */ - $stories = array(); - $allChanges = array(); - $now = helper::now(); - $data = fixer::input('post')->get(); - $storyIdList = $data->storyIdList ? $data->storyIdList : array(); - - $oldStories = $this->getByList($storyIdList); - foreach($storyIdList as $storyID) - { - $oldStory = $oldStories[$storyID]; - if($oldStory->parent == -1) continue; - if($oldStory->status == 'closed') continue; - - $story = new stdclass(); - $story->lastEditedBy = $this->app->user->account; - $story->lastEditedDate = $now; - $story->closedBy = $this->app->user->account; - $story->closedDate = $now; - $story->assignedDate = $now; - $story->status = 'closed'; - $story->stage = 'closed'; - - $story->closedReason = $data->closedReasons[$storyID]; - $story->duplicateStory = $data->duplicateStoryIDList[$storyID] ? $data->duplicateStoryIDList[$storyID] : $oldStory->duplicateStory; - $story->childStories = $data->childStoriesIDList[$storyID] ? $data->childStoriesIDList[$storyID] : $oldStory->childStories; - - if($story->closedReason != 'done') $story->plan = ''; - - $stories[$storyID] = $story; - unset($story); - } - + $this->loadModel('action'); + $oldStories = $this->getByList(array_keys($stories)); foreach($stories as $storyID => $story) { - if(!$story->closedReason) continue; + if(empty($story->closedReason)) continue; $oldStory = $oldStories[$storyID]; - $this->dao->update(TABLE_STORY)->data($story) - ->autoCheck() + $this->dao->update(TABLE_STORY)->data($story, 'comment')->autoCheck() ->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty') ->where('id')->eq($storyID)->exec(); - if(!dao::isError()) - { - /* Update parent story status. */ - if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent); - $this->setStage($storyID); - $allChanges[$storyID] = common::createChanges($oldStory, $story); + if(dao::isError()) return dao::$errors[] = 'story#' . $storyID . dao::getError(true); - if(($this->config->edition == 'biz' || $this->config->edition == 'max') && $oldStory->feedback && !isset($feedbacks[$oldStory->feedback])) - { - $feedbacks[$oldStory->feedback] = $oldStory->feedback; - $this->loadModel('feedback')->updateStatus('story', $oldStory->feedback, $story->status, $oldStory->status); - } - } - else + /* Update parent story status. */ + if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent); + $this->setStage($storyID); + + $changes = common::createChanges($oldStory, $story); + if($changes) { - helper::end(js::error('story#' . $storyID . dao::getError(true))); + $preStatus = $oldStory->status; + $isChanged = $oldStory->changedBy ? true : false; + if($preStatus == 'reviewing') $preStatus = $isChanged ? 'changing' : 'draft'; + + $actionID = $this->action->create('story', $storyID, 'Closed', $story->comment, ucfirst($story->closedReason) . ($story->duplicateStory ? ':' . (int)$story->duplicateStory : '') . "|{$preStatus}"); + $this->action->logHistory($actionID, $changes); + + if(!empty($oldStory->twins)) $this->syncTwins($storyID, $oldStory->twins, $changes, 'Closed'); } - if(!dao::isError()) $this->loadModel('score')->create('story', 'close', $storyID); + + if($this->config->edition != 'open' && $oldStory->feedback && !isset($feedbacks[$oldStory->feedback])) + { + $feedbacks[$oldStory->feedback] = $oldStory->feedback; + $this->loadModel('feedback')->updateStatus('story', $oldStory->feedback, $story->status, $oldStory->status); + } + + $this->loadModel('score')->create('story', 'close', $storyID); } - - return $allChanges; } /** diff --git a/module/story/ui/batchclose.html.php b/module/story/ui/batchclose.html.php new file mode 100644 index 0000000000..bb96273d19 --- /dev/null +++ b/module/story/ui/batchclose.html.php @@ -0,0 +1,52 @@ + + * @package story + * @link https://www.zentao.net + */ +namespace zin; + +jsVar('twinsCount', $twinsCount); +jsVar('langTwins', $lang->story->twins . ': '); +if(!empty($errorTips)) js("zui.Modal.alert({message: '{$errorTips}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'});\n"); + +unset($lang->story->reasonList['subdivided']); +$fields = $config->story->form->batchClose; +$fields['closedReason']['options'] = array_filter($lang->story->reasonList); + +$items = array(); +$items['storyIdList'] = array('name' => 'storyIdList', 'label' => '', 'control' => 'hidden', 'hidden' => true); +$items['id'] = array('name' => 'id', 'label' => $lang->idAB, 'control' => 'index', 'width' => '60px'); +foreach($fields as $fieldName => $field) +{ + $items[$fieldName] = array('name' => $fieldName, 'label' => zget($lang->story, $fieldName), 'control' => $field['control'], 'width' => $field['width'], 'required' => $field['required'], 'items' => zget($field, 'options', array())); +} +$items['comment']['label'] = $lang->comment; + +/* Build form field value for batch edit. */ +$fieldNameList = array_keys($items); +$data = array(); +foreach($stories as $storyID => $story) +{ + $data[$storyID] = $story; + foreach($fieldNameList as $fieldName) + { + if($fieldName == 'storyIdList') $data[$storyID]->storyIdList = $story->id; + if(!isset($story->$fieldName)) $story->$fieldName = ''; + } +} + +formBatchPanel +( + set::title($lang->story->batchClose), + set::mode('edit'), + set::items($items), + set::data(array_values($data)), + set::onRenderRow(jsRaw('renderRowData')), +); + +render(); diff --git a/module/story/ui/batchedit.html.php b/module/story/ui/batchedit.html.php index 75908d093a..45e4ad579c 100644 --- a/module/story/ui/batchedit.html.php +++ b/module/story/ui/batchedit.html.php @@ -17,7 +17,7 @@ jsVar('meeting', isset($meetings) ? $meetings : array()); jsVar('researchReports', isset($researchReports) ? $researchReports : array()); jsVar('productStoryList', $productStoryList); -if(!empty($twinsTip)) js("zui.Modal.alert('{$twinsTip}');\n"); +if(!empty($twinsTip)) js("zui.Modal.alert({message: '{$twinsTip}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'});\n"); $fields = $config->story->form->batchEdit; diff --git a/module/story/zen.php b/module/story/zen.php index 2db608b850..b8b573711e 100644 --- a/module/story/zen.php +++ b/module/story/zen.php @@ -140,7 +140,7 @@ class storyZen extends story * @access protected * @return void */ - protected function setMenuForBatchEdit(int $productID, string $branch = '', int $executionID = 0, string $from): void + protected function setMenuForBatchEdit(int $productID, string $branch = '', int $executionID = 0, string $from = ''): void { $this->view->hiddenPlan = false; if($this->app->tab == 'product') @@ -185,6 +185,54 @@ class storyZen extends story } } + /** + * 设置批量关闭需求页面的导航。 + * Set menu for batch close. + * + * @param int $productID + * @param int $executionID + * @param string $from + * @access protected + * @return void + */ + protected function setMenuForBatchClose(int $productID, int $executionID = 0, string $from = ''): void + { + /* The stories of a product. */ + if($this->app->tab == 'product') + { + $this->product->setMenu($productID); + $product = $this->product->getByID($productID); + $this->view->title = $product->name . $this->lang->colon . $this->lang->story->batchClose; + } + /* The stories of a execution. */ + elseif($executionID) + { + $this->lang->story->menu = $this->lang->execution->menu; + $this->lang->story->menuOrder = $this->lang->execution->menuOrder; + $this->execution->setMenu($executionID); + $execution = $this->execution->getByID($executionID); + $this->view->title = $execution->name . $this->lang->colon . $this->lang->story->batchClose; + } + else + { + if($this->app->tab == 'project') + { + $this->project->setMenu($this->session->project); + $this->view->title = $this->lang->story->batchClose; + } + else + { + $this->lang->story->menu = $this->lang->my->menu; + $this->lang->story->menuOrder = $this->lang->my->menuOrder; + + if($from == 'work') $this->lang->my->menu->work['subModule'] = 'story'; + if($from == 'contribute') $this->lang->my->menu->contribute['subModule'] = 'story'; + + $this->view->title = $this->lang->story->batchClose; + } + } + } + /** * 如果是看板执行,设置界面中要用到关于看板的视图变量。 * Set view vars for kanban. @@ -874,7 +922,49 @@ class storyZen extends story if($story->closedBy && empty($story->closedReason)) dao::$errors['closedReason'] = sprintf($this->lang->error->notempty, $this->lang->story->closedReason); if($story->closedReason == 'done' && empty($story->stage)) dao::$errors['stage'] = sprintf($this->lang->error->notempty, $this->lang->story->stage); - if($story->closedReason == 'duplicate' && empty($story->duplicateStory)) dao::$errors['stage'] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory); + if($story->closedReason == 'duplicate' && empty($story->duplicateStory)) dao::$errors['duplicateStory'] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory); + } + + return $stories; + } + + /** + * 构建批量关闭需求数据。 + * Build stories for batch close. + * + * @access protected + * @return array + */ + protected function buildStoriesForBatchClose(): array + { + $fields = $this->config->story->form->batchClose; + $account = $this->app->user->account; + $now = helper::now(); + + $fields['duplicateStory'] = array('type' => 'int', 'required' => false, 'default' => 0); + + $data = form::batchData($fields)->get(); + $oldStories = $this->story->getByList(array_keys($data)); + $stories = array(); + foreach($data as $storyID => $story) + { + $oldStory = $oldStories[$storyID]; + if($oldStory->parent == -1) continue; + if($oldStory->status == 'closed') continue; + + $story->lastEditedBy = $account; + $story->lastEditedDate = $now; + $story->closedBy = $account; + $story->closedDate = $now; + $story->assignedTo = 'closed'; + $story->assignedDate = $now; + $story->status = 'closed'; + $story->stage = 'closed'; + + if($story->closedReason != 'done') $story->plan = ''; + if($story->closedReason == 'duplicate' && empty($story->duplicateStory)) dao::$errors['duplicateStory'] = sprintf($this->lang->error->notempty, $this->lang->story->duplicateStory); + + $stories[$storyID] = $story; } return $stories;