* Finish task #96577.

This commit is contained in:
wangyidong
2023-07-05 11:20:38 +08:00
parent b2c76988f5
commit 2d0d55d1cd
8 changed files with 254 additions and 146 deletions
+7 -1
View File
@@ -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');
+24 -88
View File
@@ -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}'");
}
}
+2
View File
@@ -0,0 +1,2 @@
.duplicateStoryBox{width:50%;}
.childStoryBox{width:50%;}
+47
View File
@@ -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("<input type='hidden' name='title[" + story.id + "]' value='" + story.title + "' />");
$status.attr('disabled', 'disabled');
if(story.twins)
{
$title.wrap("<div class='input-group'></div>");
$title.after("<span class='input-group-addon'>" + langTwins + "<span class='text-secondary'>" + twinsCount[story.id] + "</span></span>");
}
var $closedReason = $row.find('.form-batch-input[data-name="closedReason"]');
$closedReason.attr('onchange', 'setDuplicateAndChild(this)').wrap("<div class='input-group'></div>");
var appendStoryHtml = "<span class='duplicateStoryBox" + (story.closedReason != 'duplicate' ? " hidden" : '') + "'>";
appendStoryHtml += "<select class='form-control form-batch-input' name='duplicateStory[" + story.id + "]' id='duplicateStory_" + index + "' data-name='duplicateStory' data-id='" + story.id + "' onmouseenter='getDuplicateStories(this)'>";
appendStoryHtml += "<option value=''></option>";
appendStoryHtml += '</select></span>';
$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');
});
}
};
+29 -54
View File
@@ -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;
}
/**
+52
View File
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* The UI view file of story module of ZenTaoPMS.
* @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
* @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
* @author Wang Yidong <yidong@easycorp.ltd>
* @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();
+1 -1
View File
@@ -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;
+92 -2
View File
@@ -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;