diff --git a/api/v1/entries/stories.php b/api/v1/entries/stories.php index c463e0906e..c28ef766ed 100644 --- a/api/v1/entries/stories.php +++ b/api/v1/entries/stories.php @@ -68,7 +68,7 @@ class storiesEntry extends entry if(!$productID and isset($this->requestBody->product)) $productID = $this->requestBody->product; if(!$productID) return $this->sendError(400, 'Need product id.'); - $fields = 'title,spec,verify,module,reviewer,type,parent,moduleOptionMenu,source,sourceNote,category,pri,estimate,mailto,keywords,notifyemail,uid,URS,status'; + $fields = 'title,spec,verify,module,reviewer,type,parent,moduleOptionMenu,source,sourceNote,category,pri,estimate,mailto,keywords,notifyemail,uid,status'; $this->batchSetPost($fields); $this->setPost('plans', array($this->request('plan', 0))); @@ -85,7 +85,7 @@ class storiesEntry extends entry $control = $this->loadController('story', 'create'); $this->requireFields('title,spec,pri,category'); - $control->create($productID, $this->param('branch', 0), $this->param('moduleID', 0), $this->param('storyID', 0), $this->param('objectID', 0), $this->param('bugID', 0)); + $control->create($productID, $this->param('branch', 0), $this->param('moduleID', 0), $this->param('storyID', 0), $this->param('objectID', 0), $this->param('bugID', 0), 0, 0, '', $this->param('type', 'story')); $data = $this->getData(); if(isset($data->status) and $data->status == 'fail') return $this->sendError(zget($data, 'code', 400), $data->message); diff --git a/api/v1/entries/storyclose.php b/api/v1/entries/storyclose.php index c20d57302d..1b32185e85 100644 --- a/api/v1/entries/storyclose.php +++ b/api/v1/entries/storyclose.php @@ -20,7 +20,7 @@ class storyCloseEntry extends entry */ public function post($storyID) { - $fields = 'closedReason,duplicateStory,childStories,comment'; + $fields = 'closedReason,duplicateStory,comment'; $this->batchSetPost($fields); $control = $this->loadController('story', 'close'); diff --git a/config/filter.php b/config/filter.php index f6dbdb6351..5b063bc6f0 100755 --- a/config/filter.php +++ b/config/filter.php @@ -46,6 +46,8 @@ $filter->product = new stdclass(); $filter->branch = new stdclass(); $filter->qa = new stdclass(); $filter->story = new stdclass(); +$filter->epic = new stdclass(); +$filter->requirement = new stdclass(); $filter->task = new stdclass(); $filter->execution = new stdclass(); $filter->testcase = new stdclass(); @@ -77,121 +79,126 @@ $filter->projectplan = new stdclass(); $filter->kanban = new stdclass(); $filter->group = new stdclass(); -$filter->index->index = new stdclass(); -$filter->block->default = new stdclass(); -$filter->block->create = new stdclass(); -$filter->block->edit = new stdclass(); -$filter->my->work = new stdclass(); -$filter->my->contribute = new stdclass(); -$filter->bug->batchcreate = new stdclass(); -$filter->bug->browse = new stdclass(); -$filter->bug->default = new stdclass(); -$filter->bug->create = new stdclass(); -$filter->bug->export = new stdclass(); -$filter->bug->ajaxgetproductcases = new stdclass(); -$filter->bug->ajaxgetproductbugs = new stdclass(); -$filter->caselib->create = new stdclass(); -$filter->doc->create = new stdclass(); -$filter->doc->browse = new stdclass(); -$filter->doc->view = new stdclass(); -$filter->doc->tablecontents = new stdclass(); -$filter->doc->productspace = new stdclass(); -$filter->doc->projectspace = new stdclass(); -$filter->doc->showfiles = new stdclass(); -$filter->doc->default = new stdclass(); -$filter->api->default = new stdClass(); -$filter->api->index = new stdClass(); -$filter->api->create = new stdClass(); -$filter->api->edit = new stdClass(); -$filter->mail->ztcloud = new stdclass(); -$filter->mail->batchdelete = new stdclass(); -$filter->misc->checkupdate = new stdclass(); -$filter->file->download = new stdclass(); -$filter->product->browse = new stdclass(); -$filter->product->all = new stdclass(); -$filter->product->default = new stdclass(); -$filter->product->index = new stdclass(); -$filter->product->export = new stdclass(); -$filter->product->project = new stdclass(); -$filter->product->roadmap = new stdclass(); -$filter->product->dynamic = new stdclass(); -$filter->branch->default = new stdclass(); -$filter->program->default = new stdclass(); -$filter->program->pgmproject = new stdclass(); -$filter->program->prjbrowse = new stdclass(); -$filter->program->project = new stdclass(); -$filter->program->product = new stdclass(); -$filter->program->browse = new stdclass(); -$filter->program->export = new stdclass(); -$filter->program->pgmbrowse = new stdclass(); -$filter->program->export = new stdclass(); -$filter->program->ajaxgetdropmenu = new stdclass(); -$filter->project->default = new stdclass(); -$filter->project->browse = new stdclass(); -$filter->project->story = new stdclass(); -$filter->project->export = new stdclass(); -$filter->project->task = new stdclass(); -$filter->project->execution = new stdclass(); -$filter->project->testcase = new stdclass(); -$filter->projectstory->story = new stdclass(); -$filter->qa->default = new stdclass(); -$filter->story->create = new stdclass(); -$filter->story->export = new stdclass(); -$filter->story->batchcreate = new stdclass(); -$filter->story->track = new stdclass(); -$filter->sso->getbindusers = new stdclass(); -$filter->sso->gettodolist = new stdclass(); -$filter->sso->getuserpairs = new stdclass(); -$filter->sso->login = new stdclass(); -$filter->sso->logout = new stdclass(); -$filter->git->cat = new stdclass(); -$filter->git->diff = new stdclass(); -$filter->svn->cat = new stdclass(); -$filter->svn->diff = new stdclass(); -$filter->task->create = new stdclass(); -$filter->task->export = new stdclass(); -$filter->task->recordestimate = new stdclass(); -$filter->execution->default = new stdclass(); -$filter->execution->story = new stdclass(); -$filter->testcase->default = new stdclass(); -$filter->testcase->create = new stdclass(); -$filter->testcase->browse = new stdclass(); -$filter->testcase->export = new stdclass(); -$filter->testcase->groupcase = new stdclass(); -$filter->testreport->default = new stdclass(); -$filter->testsuite->default = new stdclass(); -$filter->testsuite->library = new stdclass(); -$filter->testtask->default = new stdclass(); -$filter->testtask->browse = new stdclass(); -$filter->testtask->create = new stdclass(); -$filter->testtask->cases = new stdclass(); -$filter->todo->export = new stdclass(); -$filter->upgrade->license = new stdclass(); -$filter->user->login = new stdclass(); -$filter->user->edit = new stdclass(); -$filter->user->export = new stdclass(); -$filter->webhook->bind = new stdclass(); -$filter->user->ajaxgetmore = new stdclass(); -$filter->user->export = new stdclass(); -$filter->repo->ajaxsynccommit = new stdclass(); -$filter->repo->apigetrepobyurl = new stdclass(); -$filter->search->index = new stdclass(); -$filter->gitlab->webhook = new stdclass(); -$filter->gitlab->importissue = new stdclass(); -$filter->mr->diff = new stdclass(); -$filter->mr->browse = new stdclass(); -$filter->ci->checkCompileStatus = new stdclass(); -$filter->execution->export = new stdclass(); -$filter->tree->browse = new stdclass(); -$filter->productplan->browse = new stdclass(); -$filter->projectplan->browse = new stdclass(); -$filter->kanban->space = new stdclass(); -$filter->execution->kanban = new stdclass(); -$filter->execution->all = new stdclass(); -$filter->execution->tree = new stdclass(); -$filter->group->editmanagepriv = new stdclass(); -$filter->group->default = new stdclass(); -$filter->caselib->default = new stdclass(); +$filter->index->index = new stdclass(); +$filter->block->default = new stdclass(); +$filter->block->create = new stdclass(); +$filter->block->edit = new stdclass(); +$filter->my->work = new stdclass(); +$filter->my->contribute = new stdclass(); +$filter->bug->batchcreate = new stdclass(); +$filter->bug->browse = new stdclass(); +$filter->bug->default = new stdclass(); +$filter->bug->create = new stdclass(); +$filter->bug->export = new stdclass(); +$filter->bug->ajaxgetproductcases = new stdclass(); +$filter->bug->ajaxgetproductbugs = new stdclass(); +$filter->caselib->create = new stdclass(); +$filter->doc->create = new stdclass(); +$filter->doc->browse = new stdclass(); +$filter->doc->view = new stdclass(); +$filter->doc->tablecontents = new stdclass(); +$filter->doc->productspace = new stdclass(); +$filter->doc->projectspace = new stdclass(); +$filter->doc->showfiles = new stdclass(); +$filter->doc->default = new stdclass(); +$filter->api->default = new stdClass(); +$filter->api->index = new stdClass(); +$filter->api->create = new stdClass(); +$filter->api->edit = new stdClass(); +$filter->mail->ztcloud = new stdclass(); +$filter->mail->batchdelete = new stdclass(); +$filter->misc->checkupdate = new stdclass(); +$filter->file->download = new stdclass(); +$filter->product->browse = new stdclass(); +$filter->product->all = new stdclass(); +$filter->product->default = new stdclass(); +$filter->product->index = new stdclass(); +$filter->product->export = new stdclass(); +$filter->product->project = new stdclass(); +$filter->product->roadmap = new stdclass(); +$filter->product->dynamic = new stdclass(); +$filter->branch->default = new stdclass(); +$filter->program->default = new stdclass(); +$filter->program->pgmproject = new stdclass(); +$filter->program->prjbrowse = new stdclass(); +$filter->program->project = new stdclass(); +$filter->program->product = new stdclass(); +$filter->program->browse = new stdclass(); +$filter->program->export = new stdclass(); +$filter->program->pgmbrowse = new stdclass(); +$filter->program->export = new stdclass(); +$filter->program->ajaxgetdropmenu = new stdclass(); +$filter->project->default = new stdclass(); +$filter->project->browse = new stdclass(); +$filter->project->story = new stdclass(); +$filter->project->export = new stdclass(); +$filter->project->task = new stdclass(); +$filter->project->execution = new stdclass(); +$filter->project->testcase = new stdclass(); +$filter->projectstory->story = new stdclass(); +$filter->qa->default = new stdclass(); +$filter->story->create = new stdclass(); +$filter->story->export = new stdclass(); +$filter->story->batchchangeparent = new stdclass(); +$filter->story->batchcreate = new stdclass(); +$filter->story->track = new stdclass(); +$filter->epic->batchchangeparent = new stdclass(); +$filter->epic->export = new stdclass(); +$filter->sso->getbindusers = new stdclass(); +$filter->sso->gettodolist = new stdclass(); +$filter->sso->getuserpairs = new stdclass(); +$filter->sso->login = new stdclass(); +$filter->sso->logout = new stdclass(); +$filter->git->cat = new stdclass(); +$filter->git->diff = new stdclass(); +$filter->svn->cat = new stdclass(); +$filter->svn->diff = new stdclass(); +$filter->task->create = new stdclass(); +$filter->task->export = new stdclass(); +$filter->task->recordestimate = new stdclass(); +$filter->execution->default = new stdclass(); +$filter->execution->story = new stdclass(); +$filter->testcase->default = new stdclass(); +$filter->testcase->create = new stdclass(); +$filter->testcase->browse = new stdclass(); +$filter->testcase->export = new stdclass(); +$filter->testcase->groupcase = new stdclass(); +$filter->testreport->default = new stdclass(); +$filter->testsuite->default = new stdclass(); +$filter->testsuite->library = new stdclass(); +$filter->testtask->default = new stdclass(); +$filter->testtask->browse = new stdclass(); +$filter->testtask->create = new stdclass(); +$filter->testtask->cases = new stdclass(); +$filter->todo->export = new stdclass(); +$filter->upgrade->license = new stdclass(); +$filter->user->login = new stdclass(); +$filter->user->edit = new stdclass(); +$filter->user->export = new stdclass(); +$filter->webhook->bind = new stdclass(); +$filter->user->ajaxgetmore = new stdclass(); +$filter->user->export = new stdclass(); +$filter->repo->ajaxsynccommit = new stdclass(); +$filter->repo->apigetrepobyurl = new stdclass(); +$filter->search->index = new stdclass(); +$filter->gitlab->webhook = new stdclass(); +$filter->gitlab->importissue = new stdclass(); +$filter->mr->diff = new stdclass(); +$filter->mr->browse = new stdclass(); +$filter->ci->checkCompileStatus = new stdclass(); +$filter->execution->export = new stdclass(); +$filter->tree->browse = new stdclass(); +$filter->productplan->browse = new stdclass(); +$filter->projectplan->browse = new stdclass(); +$filter->kanban->space = new stdclass(); +$filter->execution->kanban = new stdclass(); +$filter->execution->all = new stdclass(); +$filter->execution->tree = new stdclass(); +$filter->group->editmanagepriv = new stdclass(); +$filter->group->default = new stdclass(); +$filter->caselib->default = new stdclass(); +$filter->requirement->batchchangeparent = new stdclass(); +$filter->requirement->export = new stdclass(); $filter->index->index->get['open'] = 'reg::base64'; @@ -201,6 +208,7 @@ $filter->my->work->cookie['pagerMyStory'] = 'int'; $filter->my->work->cookie['pagerMyBug'] = 'int'; $filter->my->work->cookie['pagerMyTestcase'] = 'int'; $filter->my->work->cookie['pagerMyTesttask'] = 'int'; +$filter->my->work->cookie['storyViewType'] = 'code'; $filter->my->contribute->cookie['pagerMyTask'] = 'int'; $filter->my->contribute->cookie['pagerMyRequirement'] = 'int'; @@ -209,6 +217,7 @@ $filter->my->contribute->cookie['pagerMyBug'] = 'int'; $filter->my->contribute->cookie['pagerMyTestcase'] = 'int'; $filter->my->contribute->cookie['pagerMyTesttask'] = 'int'; $filter->my->contribute->cookie['pagerMyDoc'] = 'int'; +$filter->my->contribute->cookie['storyViewType'] = 'code'; $filter->bug->batchcreate->cookie['preBranch'] = 'reg::word'; $filter->bug->browse->cookie['bugModule'] = 'int'; @@ -266,6 +275,7 @@ $filter->product->browse->cookie['productStoryOrder'] = 'reg::orderBy'; $filter->product->browse->cookie['storyModule'] = 'int'; $filter->product->browse->cookie['storyBranch'] = 'int'; $filter->product->browse->cookie['treeBranch'] = 'reg::word'; +$filter->product->browse->cookie['storyViewType'] = 'code'; $filter->product->all->cookie['showProductBatchEdit'] = 'int'; $filter->product->default->cookie['lastProduct'] = 'int'; $filter->product->default->cookie['preProductID'] = 'int'; @@ -316,16 +326,24 @@ $filter->project->testcase->cookie['onlyScene'] = 'code'; $filter->projectstory->story->cookie['storyModuleParam'] = 'int'; $filter->projectstory->story->cookie['pagerProductBrowse'] = 'int'; +$filter->projectstory->story->cookie['storyViewType'] = 'code'; $filter->qa->default->cookie['lastProduct'] = 'int'; $filter->qa->default->cookie['preBranch'] = 'reg::word'; $filter->qa->default->cookie['preProductID'] = 'int'; -$filter->story->create->cookie['lastStoryModule'] = 'int'; -$filter->story->batchcreate->cookie['preProductID'] = 'int'; -$filter->story->export->cookie['checkedItem'] = 'reg::checked'; -$filter->story->track->cookie['preBranch'] = 'reg::word'; -$filter->story->track->cookie['preProductID'] = 'int'; +$filter->story->create->cookie['lastStoryModule'] = 'int'; +$filter->story->batchcreate->cookie['preProductID'] = 'int'; +$filter->story->export->cookie['checkedItem'] = 'reg::checked'; +$filter->story->batchchangeparent->cookie['checkedItem'] = 'reg::checked'; +$filter->story->track->cookie['preBranch'] = 'reg::word'; +$filter->story->track->cookie['preProductID'] = 'int'; + +$filter->epic->batchchangeparent->cookie['checkedItem'] = 'reg::checked'; +$filter->epic->export->cookie['checkedItem'] = 'reg::checked'; + +$filter->requirement->batchchangeparent->cookie['checkedItem'] = 'reg::checked'; +$filter->requirement->export->cookie['checkedItem'] = 'reg::checked'; $filter->productplan->browse->cookie['viewType'] = 'code'; $filter->projectplan->browse->cookie['viewType'] = 'code'; diff --git a/config/zentaopms.php b/config/zentaopms.php index 34ad739549..28bd0de9bb 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -119,6 +119,26 @@ $config->executionCommonList['pt'][1] = 'Sprint'; $config->executionCommonList['pt'][2] = 'Fase'; /* Story common list. */ +$config->storyCommonList['zh-cn']['epic'] = '业务需求'; +$config->storyCommonList['zh-cn']['requirement'] = '用户需求'; +$config->storyCommonList['zh-cn']['story'] = '软件需求'; + +$config->storyCommonList['zh-tw']['epic'] = '業務需求'; +$config->storyCommonList['zh-tw']['requirement'] = '用戶需求'; +$config->storyCommonList['zh-tw']['story'] = '軟件需求'; + +$config->storyCommonList['en']['epic'] = 'Epic'; +$config->storyCommonList['en']['requirement'] = 'Requirement'; +$config->storyCommonList['en']['story'] = 'Story'; + +$config->storyCommonList['de']['epic'] = 'Epic'; +$config->storyCommonList['de']['requirement'] = 'Requirement'; +$config->storyCommonList['de']['story'] = 'Story'; + +$config->storyCommonList['fr']['epic'] = 'Epic'; +$config->storyCommonList['fr']['requirement'] = 'Requirement'; +$config->storyCommonList['fr']['story'] = 'Story'; + $config->hourPointCommonList['zh-cn'][0] = '工时'; $config->hourPointCommonList['zh-cn'][1] = '故事点'; $config->hourPointCommonList['zh-cn'][2] = '功能点'; @@ -254,6 +274,7 @@ $config->openMethods[] = 'search.buildzinform'; $config->openMethods[] = 'search.buildzinquery'; $config->openMethods[] = 'search.savezinquery'; $config->openMethods[] = 'space.createapplication'; +$config->openMethods[] = 'story.storyview'; $config->openMethods[] = 'ai.adminindex'; $config->openMethods[] = 'sonarqube.create'; $config->openMethods[] = 'sonarqube.edit'; @@ -372,6 +393,7 @@ define('TABLE_STORYSPEC', '`' . $config->db->prefix . 'storyspec`'); define('TABLE_STORYREVIEW', '`' . $config->db->prefix . 'storyreview`'); define('TABLE_STORYSTAGE', '`' . $config->db->prefix . 'storystage`'); define('TABLE_STORYESTIMATE', '`' . $config->db->prefix . 'storyestimate`'); +define('TABLE_STORYGRADE', '`' . $config->db->prefix . 'storygrade`'); define('TABLE_PRODUCTPLAN', '`' . $config->db->prefix . 'productplan`'); define('TABLE_PLANSTORY', '`' . $config->db->prefix . 'planstory`'); define('TABLE_RELEASE', '`' . $config->db->prefix . 'release`'); @@ -603,6 +625,7 @@ if(!defined('TABLE_MARKETRESEARCH')) define('TABLE_MARKETRESEARCH', '`' . $confi $config->objectTables['dept'] = TABLE_DEPT; $config->objectTables['product'] = TABLE_PRODUCT; $config->objectTables['productplan'] = TABLE_PRODUCTPLAN; +$config->objectTables['epic'] = TABLE_STORY; $config->objectTables['story'] = TABLE_STORY; $config->objectTables['requirement'] = TABLE_STORY; $config->objectTables['release'] = TABLE_RELEASE; @@ -698,8 +721,8 @@ $config->pipelineTypeList = array('gitlab', 'gogs', 'gitea', 'jenkins', 'sonarqu /* Program privs.*/ $config->programPriv = new stdclass(); -$config->programPriv->noSprint = array('task', 'story', 'tree', 'project', 'execution', 'projectbuild', 'bug', 'testcase', 'testreport', 'doc', 'repo', 'stakeholder', 'projectrelease', 'requirement'); -$config->programPriv->scrum = array('story', 'requirement', 'productplan', 'tree', 'projectplan', 'projectstory', 'projectrelease', 'project', 'execution', 'projectbuild', 'bug', 'testcase', 'testreport', 'doc', 'repo', 'meeting', 'stakeholder', 'testtask'); +$config->programPriv->noSprint = array('task', 'story', 'requirement', 'epic', 'tree', 'project', 'execution', 'projectbuild', 'bug', 'testcase', 'testreport', 'doc', 'repo', 'stakeholder', 'projectrelease'); +$config->programPriv->scrum = array('story', 'requirement', 'epic', 'productplan', 'tree', 'projectplan', 'projectstory', 'projectrelease', 'project', 'execution', 'projectbuild', 'bug', 'testcase', 'testreport', 'doc', 'repo', 'meeting', 'stakeholder', 'testtask'); $config->programPriv->waterfall = array_merge($config->programPriv->scrum, array('workestimation', 'durationestimation', 'budget', 'programplan', 'review', 'reviewissue', 'weekly', 'cm', 'milestone', 'design', 'issue', 'risk', 'opportunity', 'measrecord', 'auditplan', 'trainplan', 'gapanalysis', 'pssp', 'researchplan', 'researchreport')); $config->programPriv->agileplus = $config->programPriv->scrum; $config->programPriv->waterfallplus = $config->programPriv->waterfall; @@ -711,14 +734,14 @@ $config->waterfallModules = array('workestimation', 'durationestimation', 'budge $config->showMainMenu = true; $config->maxPriValue = '256'; -$config->importWhiteList = array('user', 'task', 'story', 'bug', 'testcase', 'feedback', 'ticket'); +$config->importWhiteList = array('user', 'task', 'epic', 'requirement', 'story', 'bug', 'testcase', 'feedback', 'ticket'); $config->dtable = new stdclass(); $config->dtable->colVars = array('width', 'minWidth', 'type', 'flex', 'fixed', 'sortType', 'checkbox', 'nestedToggle', 'statusMap', 'actionsMap', 'group'); $config->featureGroup = new stdclass(); $config->featureGroup->my = array('score'); -$config->featureGroup->product = array('roadmap', 'track', 'UR'); +$config->featureGroup->product = array('roadmap', 'track', 'UR', 'ER'); $config->featureGroup->scrum = array(); $config->featureGroup->waterfall = array(); $config->featureGroup->agileplus = array(); diff --git a/db/update20.0.sql b/db/update20.0.sql index a193c2f2e8..33a599c697 100644 --- a/db/update20.0.sql +++ b/db/update20.0.sql @@ -18,6 +18,19 @@ ALTER TABLE `zt_user` MODIFY `ip` varchar(255) NOT NULL DEFAULT ''; DELETE FROM `zt_config` WHERE `owner`='system' AND `module`='custom' AND `key`='productProject'; +INSERT INTO `zt_metric` (`purpose`, `scope`, `object`, `stage`, `type`, `name`, `code`, `alias`, `unit`, `desc`, `definition`, `when`, `createdBy`, `createdDate`, `builtin`, `deleted`, `dateType`) VALUES +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的每周新增反馈数', 'count_of_weekly_created_feedback_in_product', '新增反馈数', 'count', '按产品统计的每周新增反馈数是指在一个周内收集到的用户反馈的数量。这个度量项可以帮助团队了解用户对产品的发展趋势和需求变化,并进行产品策略的调整和优化', '产品中创建时间为某个周的反馈的个数求和\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'week'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的处理中的反馈数', 'count_of_doing_feedback_in_product', '处理中反馈数', 'count', '按产品统计的处理中的反馈数表示产品中状态为处理中的反馈数量之和。该数值越大,说明团队并行处理的反馈越多,可以帮助团队了解当前的工作负载情况', '产品中所有反馈个数求和\r\n状态为处理中\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的已处理的反馈数', 'count_of_done_feedback_in_product', '已处理反馈数', 'count', '按产品统计的已处理的反馈数表示产品中状态为已处理的反馈数量之和。该数值越大,说明团队成员处理的反馈越多,有利于提高用户满意度', '产品中所有反馈个数求和\r\n状态为已处理\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的待完善的反馈数', 'count_of_clarify_feedback_in_product', '待完善反馈数', 'count', '按产品统计的待完善的反馈数表示产品中状态为待完善的反馈数量之和。该数值越大,说明有较多的反馈信息不清晰或比较复杂。需要反馈者更多的澄清和解释', '产品中所有反馈个数求和\r\n状态为待完善\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的待处理的反馈数', 'count_of_wait_feedback_in_product', '待处理反馈数', 'count', '按产品统计的待处理的反馈数表示产品中状态为待处理的反馈数量之和。该度量项可能暗示产品团队的反馈处理效率,待处理反馈数越多,可能会导致客户满意度降低', '产品中所有反馈个数求和\r\n状态为待处理\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的追问中的反馈数', 'count_of_asked_feedback_in_product', '追问中反馈数', 'count', '按产品统计的追问中的反馈数表示产品中状态为追问中的反馈数量之和。该度量项可能暗示着反馈的复杂性或对处理方案的疑惑,追问中的反馈数量越多,可能意味着团队需要更多时间和资源来回复并解决这些问题', '产品中所有反馈个数求和\r\n状态为追问中\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'product', 'feedback', 'released', 'php', '按产品统计的未关闭的反馈数', 'count_of_unclosed_feedback_in_product', '未关闭反馈数', 'count', '按产品统计的未关闭的反馈数表示产品中状态为未关闭的反馈数量之和。这个度量项可以一定程度反映产品团队响应用户反馈的效率和及时处理用户问题的能力', '产品中所有反馈个数求和\r\n过滤状态为已关闭的反馈\r\n过滤已删除的反馈\r\n过滤已删除的产品', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'user', 'ticket', 'released', 'php', '按人员统计的待处理工单数', 'count_of_assigned_ticket_in_user', '待处理工单数', 'count', '按人员统计的待处理工单数表示每个人待处理的工单数量之和。反映了每个人员需要处理的工单数量的规模。该数值越大,说明需要处理的反馈任务越多。', '所有工单个数求和\r\n指派给为某人\r\n过滤已删除的工单\r\n过滤已删除产品的工单', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'user', 'qa', 'released', 'php', '按人员统计的待处理QA数', 'count_of_assigned_qa_in_user', '待处理QA数', 'count', '按人员统计的待处理QA数表示每个人待处理的质量保证问题之和。反映了每个人员需要处理的质量保证问题的规模。该数值越大,说明需要处理的质量保证问题越多。', '所有待处理的QA个数求和(包含:待处理质量保证计划、待处理不符合项)\r\n指派给为某人\r\n质量保证计划状态为待检查、不符合项状态为待解决\r\n过滤已删除的质量保证计划和不符合项\r\n过滤已删除项目的质量保证计划和不符合项', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'user', 'risk', 'released', 'php', '按人员统计的待处理风险数', 'count_of_assigned_risk_in_user', '待处理风险数', 'count', '按人员统计的待处理用户需求数表示每个人待处理的用户需求数量之和。反映了每个人员需要处理的用户需求数量的规模。该数值越大,说明需要投入越多的时间处理用户需求。', '所有风险个数求和\r\n指派给为某人\r\n过滤已删除的风险\r\n过滤已关闭的风险\r\n过滤已删除项目的风险', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'), +('scale', 'user', 'issue', 'released', 'php', '按人员统计的待处理问题数', 'count_of_assigned_issue_in_user', '待处理问题数', 'count', '按人员统计的待处理问题数表示每个人待处理的问题数量之和。反映了每个人员需要处理的问题数量的规模。该数值越大,项目存在问题越多,需要投入越多的时间处理问题。', '所有问题个数求和\r\n指派给为某人\r\n过滤已删除的问题\r\n过滤已删除项目的问题', 'realtime', 'system', '2024-05-07 08:00:00', '1', '0', 'nodate'); + ALTER TABLE `zt_chart` ADD `code` varchar(255) not NULL default '' AFTER `name`; ALTER TABLE `zt_pivot` ADD `code` varchar(255) not NULL default '' AFTER `group`; diff --git a/db/update20.1.1.sql b/db/update20.1.1.sql new file mode 100644 index 0000000000..beaad65445 --- /dev/null +++ b/db/update20.1.1.sql @@ -0,0 +1,92 @@ +REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) VALUES +('zh-cn', 'custom', 'URSRList', '1', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u8f6f\\u4ef6\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '2', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u7814\\u53d1\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '3', '{\"ERName\":\"\\u4e1a\\u9700\",\"SRName\":\"\\u8f6f\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '4', '{\"ERName\":\"\\u53f2\\u8bd7\",\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u7279\\u6027\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '5', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '6', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '1', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u8edf\\u4ef6\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '2', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u7814\\u767c\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '3', '{\"ERName\":\"\\u696d\\u9700\",\"SRName\":\"\\u8edf\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '4', '{\"ERName\":\"\\u53f2\\u8a69\",\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u7279\\u6027\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '5', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '6', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'); + +CREATE TABLE `zt_storygrade` ( + `type` enum('story','requirement','epic') NOT NULL, + `grade` smallint NOT NULL, + `name` char(30) NOT NULL, + `status` char(30) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `zt_storygrade` (`type`, `grade`, `name`, `status`) VALUES +('requirement', 1, 'UR', 'enable'), +('epic', 1, 'BR', 'enable'), +('story', 1, 'SR', 'enable'), +('story', 2, '子', 'enable'); + +ALTER TABLE `zt_story` ADD `isParent` enum('0','1') NOT NULL DEFAULT '0' AFTER `parent`; +ALTER TABLE `zt_story` ADD `root` mediumint NOT NULL DEFAULT '0' AFTER `isParent`; +ALTER TABLE `zt_story` ADD `path` text NULL AFTER `root`; +ALTER TABLE `zt_story` ADD `grade` smallint(6) NOT NULL DEFAULT '0' AFTER `path`; +ALTER TABLE `zt_story` ADD `parentVersion` smallint NOT NULL DEFAULT '0' AFTER `version`; +ALTER TABLE `zt_story` CHANGE `stage` `stage` enum('','wait','defining','planning','planned','projected','designing','designed','developing','developed','testing','tested','verified','rejected','delivering','released','closed') NOT NULL DEFAULT 'wait'; +ALTER TABLE `zt_story` DROP `childStories`; +CREATE INDEX `root` ON `zt_story` (`root`); +UPDATE `zt_story` SET stage = 'defining' WHERE stage = 'wait' AND (parent = -1 OR type != 'story'); +UPDATE `zt_story` SET stage = 'planning' WHERE stage in ('planned', 'projected') AND (parent = -1 OR type != 'story'); +UPDATE `zt_story` SET stage = 'developing' WHERE stage in ('developed', 'testing', 'tested') AND (parent = -1 OR type != 'story'); +UPDATE `zt_story` SET stage = 'delivering' WHERE stage in ('verified', 'released') AND (parent = -1 OR type != 'story'); +UPDATE `zt_story` SET isParent = '1' WHERE parent = -1; +UPDATE `zt_story` SET grade = 1, parent = 0, root = id, path = concat(',', id, ',') WHERE type != 'story'; +UPDATE `zt_story` SET grade = 1, parent = 0, root = id, path = concat(',', id, ',') WHERE type = 'story' AND parent <= 0; +UPDATE `zt_story` SET grade = 2, root = parent, path = concat(',', parent, ',', id, ',') WHERE type = 'story' AND parent > 0; + +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'story', '', 'gradeRule', 'stepwise'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'requirement', '', 'gradeRule', 'stepwise'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'epic', '', 'gradeRule', 'stepwise'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'custom', '', 'enableER', '1'); + +ALTER TABLE `zt_product` +ADD `draftRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `reviewer`, +ADD `activeRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `draftRequirements`, +ADD `changingRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `activeRequirements`, +ADD `reviewingRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `changingRequirements`, +ADD `finishedRequirements` mediumint NOT NULL DEFAULT '0' AFTER `reviewingRequirements`, +ADD `closedRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `finishedRequirements`, +ADD `totalRequirements` mediumint(8) NOT NULL DEFAULT '0' AFTER `closedRequirements`; + +ALTER TABLE `zt_product` +ADD `draftEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `reviewer`, +ADD `activeEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `draftEpics`, +ADD `changingEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `activeEpics`, +ADD `reviewingEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `changingEpics`, +ADD `finishedEpics` mediumint NOT NULL DEFAULT '0' AFTER `reviewingEpics`, +ADD `closedEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `finishedEpics`, +ADD `totalEpics` mediumint(8) NOT NULL DEFAULT '0' AFTER `closedEpics`; + +ALTER TABLE `zt_project` ADD `storyType` char(30) NULL DEFAULT 'story' AFTER `auth`; +UPDATE `zt_project` SET storyType = 'story' WHERE storyType = ''; + +DROP VIEW IF EXISTS `view_datasource_2`; +DROP VIEW IF EXISTS `view_datasource_3`; +DROP VIEW IF EXISTS `view_datasource_4`; + +CREATE VIEW `view_datasource_2` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'epic'; +CREATE VIEW `view_datasource_3` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'requirement'; +CREATE VIEW `view_datasource_4` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'story'; + +INSERT INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `vision`, `createdBy`, `createdDate`, `datasource`, `view`, `keyField`, `valueField`) VALUES +('sql', '用户需求', 'requirements', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\" and type=\"requirement\"', 'view_datasource_3', 'id', 'title'), +('sql', '业务需求', 'epics', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\" type=\"epic\"', 'view_datasource_2', 'id', 'title'); + +UPDATE `zt_workflowdatasource` SET `datasource` = 'select id,title from zt_story where deleted=\"0\" and type=\"story\"' WHERE `code` = 'stories'; diff --git a/db/zentao.sql b/db/zentao.sql index 9320bafbdd..4511ba4a62 100755 --- a/db/zentao.sql +++ b/db/zentao.sql @@ -1363,6 +1363,20 @@ CREATE TABLE IF NOT EXISTS `zt_product` ( `whitelist` text NULL, `reviewer` text NULL, `PMT` text NULL, + `draftEpics` mediumint(8) NOT NULL DEFAULT '0', + `activeEpics` mediumint(8) NOT NULL DEFAULT '0', + `changingEpics` mediumint(8) NOT NULL DEFAULT '0', + `reviewingEpics` mediumint(8) NOT NULL DEFAULT '0', + `finishedEpics` mediumint(8) NOT NULL DEFAULT '0', + `closedEpics` mediumint(8) NOT NULL DEFAULT '0', + `totalEpics` mediumint(8) NOT NULL DEFAULT '0', + `draftRequirements` mediumint(8) NOT NULL DEFAULT '0', + `activeRequirements` mediumint(8) NOT NULL DEFAULT '0', + `changingRequirements` mediumint(8) NOT NULL DEFAULT '0', + `reviewingRequirements` mediumint(8) NOT NULL DEFAULT '0', + `finishedRequirements` mediumint(8) NOT NULL DEFAULT '0', + `closedRequirements` mediumint(8) NOT NULL DEFAULT '0', + `totalRequirements` mediumint(8) NOT NULL DEFAULT '0', `draftStories` mediumint(8) NOT NULL DEFAULT '0', `activeStories` mediumint(8) NOT NULL DEFAULT '0', `changingStories` mediumint(8) NOT NULL DEFAULT '0', @@ -1427,6 +1441,7 @@ CREATE TABLE IF NOT EXISTS `zt_project` ( `milestone` enum('0','1') NOT NULL DEFAULT '0', `output` text NULL, `auth` char(30) NOT NULL DEFAULT '', + `storyType` char(30) NOT NULL DEFAULT '', `parent` mediumint(8) unsigned NOT NULL DEFAULT '0', `path` varchar(255) NOT NULL DEFAULT '', `grade` tinyint(3) unsigned NOT NULL DEFAULT '0', @@ -1765,6 +1780,10 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, `vision` varchar(10) NOT NULL DEFAULT 'rnd', `parent` mediumint(9) NOT NULL DEFAULT '0', + `isParent` enum('0', '1') NOT NULL DEFAULT '0', + `root` mediumint(9) NOT NULL DEFAULT '0', + `path` text NULL, + `grade` smallint(6) NOT NULL DEFAULT '0', `product` mediumint(8) unsigned NOT NULL DEFAULT '0', `branch` mediumint(8) unsigned NOT NULL DEFAULT '0', `module` mediumint(8) unsigned NOT NULL DEFAULT '0', @@ -1782,7 +1801,7 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( `status` enum('','changing','active','draft','closed','reviewing','launched','developing') NOT NULL DEFAULT '', `subStatus` varchar(30) NOT NULL DEFAULT '', `color` char(7) NOT NULL DEFAULT '', - `stage` enum('','wait','planned','projected','developing','developed','testing','tested','verified','released','closed') NOT NULL DEFAULT 'wait', + `stage` enum('','wait','defining','planning','planned','projected','designing','designed','developing','developed','testing','tested','verified','rejected','delivering','pending','released','closed') NOT NULL DEFAULT 'wait', `stagedBy` char(30) NOT NULL DEFAULT '', `mailto` text NULL, `lib` mediumint(8) unsigned NOT NULL DEFAULT '0', @@ -1805,12 +1824,12 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( `closedReason` varchar(30) NOT NULL DEFAULT '', `activatedDate` datetime NULL, `toBug` mediumint(8) unsigned NOT NULL DEFAULT '0', - `childStories` varchar(255) NOT NULL DEFAULT '', `linkStories` varchar(255) NOT NULL DEFAULT '', `linkRequirements` varchar(255) NOT NULL DEFAULT '', `twins` varchar(255) NOT NULL DEFAULT '', `duplicateStory` mediumint(8) unsigned NOT NULL DEFAULT '0', `version` smallint(6) NOT NULL DEFAULT '1', + `parentVersion` smallint(6) NOT NULL DEFAULT '0', `storyChanged` enum('0','1') NOT NULL DEFAULT '0', `feedbackBy` varchar(100) NOT NULL DEFAULT '', `notifyEmail` varchar(100) NOT NULL DEFAULT '', @@ -1828,9 +1847,18 @@ CREATE TABLE IF NOT EXISTS `zt_story` ( PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE INDEX `product` ON `zt_story` (`product`); +CREATE INDEX `root` ON `zt_story` (`root`); CREATE INDEX `status` ON `zt_story` (`status`); CREATE INDEX `assignedTo` ON `zt_story` (`assignedTo`); +-- DROP TABLE IF EXISTS `zt_storygrade`; +CREATE TABLE `zt_storygrade` ( + `type` enum('story','requirement','epic') NOT NULL, + `grade` smallint NOT NULL, + `name` char(30) NOT NULL, + `status` char(30) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- DROP TABLE IF EXISTS `zt_storyreview`; CREATE TABLE IF NOT EXISTS `zt_storyreview` ( `story` mediumint(9) NOT NULL DEFAULT '0', @@ -2343,23 +2371,34 @@ REPLACE INTO `zt_group` (`vision`, `name`, `role`, `desc`) VALUES ('or', 'IPDDEM REPLACE INTO `zt_group` (`vision`, `name`, `role`, `desc`) VALUES ('or', 'IPDPMT', 'IPDPMT', ''); REPLACE INTO `zt_group` (`vision`, `name`, `role`, `desc`) VALUES ('or', 'IPDADMIN', 'IPDADMIN', ''); -REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`) VALUES -('zh-cn', 'custom', 'URSRList', '1', '{\"SRName\":\"\\u8f6f\\u4ef6\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '1'), -('zh-cn', 'custom', 'URSRList', '2', '{\"SRName\":\"\\u7814\\u53d1\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '1'), -('zh-cn', 'custom', 'URSRList', '3', '{\"SRName\":\"\\u8f6f\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '1'), -('zh-cn', 'custom', 'URSRList', '4', '{\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u53f2\\u8bd7\"}', '1'), -('zh-cn', 'custom', 'URSRList', '5', '{\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '1'), -('zh-tw', 'custom', 'URSRList', '1', '{"SRName":"\\u8edf\\u4ef6\\u9700\\u6c42","URName":"\\u7528\\u6236\\u9700\\u6c42"}', '0'), -('zh-tw', 'custom', 'URSRList', '2', '{"SRName":"\\u7814\\u767c\\u9700\\u6c42","URName":"\\u7528\\u6236\\u9700\\u6c42"}', '0'), -('zh-tw', 'custom', 'URSRList', '3', '{"SRName":"\\u8edf\\u9700","URName":"\\u7528\\u9700"}', '0'), -('zh-tw', 'custom', 'URSRList', '4', '{"SRName":"\\u6545\\u4e8b","URName":"\\u53f2\\u8a69"}', '0'), -('zh-tw', 'custom', 'URSRList', '5', '{"SRName":"\\u9700\\u6c42","URName":"\\u7528\\u6236\\u9700\\u6c42"}', '0'), -('en', 'custom', 'URSRList', '1', '{\"SRName\":\"Story\",\"URName\":\"Epic\"}', '0'), -('en', 'custom', 'URSRList', '2', '{\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0'), -('fr', 'custom', 'URSRList', '1', '{\"SRName\":\"Story\",\"URName\":\"Epic\"}', '0'), -('fr', 'custom', 'URSRList', '2', '{\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0'), -('de', 'custom', 'URSRList', '1', '{\"SRName\":\"Story\",\"URName\":\"Epic\"}', '0'), -('de', 'custom', 'URSRList', '2', '{\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0'); +REPLACE INTO `zt_lang` (`lang`, `module`, `section`, `key`, `value`, `system`, `vision`) VALUES +('zh-cn', 'custom', 'URSRList', '1', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u8f6f\\u4ef6\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '2', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u7814\\u53d1\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '3', '{\"ERName\":\"\\u4e1a\\u9700\",\"SRName\":\"\\u8f6f\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '4', '{\"ERName\":\"\\u53f2\\u8bd7\",\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u7279\\u6027\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '5', '{\"ERName\":\"\\u4e1a\\u52a1\\u9700\\u6c42\",\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6237\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-cn', 'custom', 'URSRList', '6', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('en', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('fr', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '1', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '2', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Requirement\"}', '0', 'rnd'), +('de', 'custom', 'URSRList', '3', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '1', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u8edf\\u4ef6\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '2', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u7814\\u767c\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '3', '{\"ERName\":\"\\u696d\\u9700\",\"SRName\":\"\\u8edf\\u9700\",\"URName\":\"\\u7528\\u9700\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '4', '{\"ERName\":\"\\u53f2\\u8a69\",\"SRName\":\"\\u6545\\u4e8b\",\"URName\":\"\\u7279\\u6027\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '5', '{\"ERName\":\"\\u696d\\u52d9\\u9700\\u6c42\",\"SRName\":\"\\u9700\\u6c42\",\"URName\":\"\\u7528\\u6236\\u9700\\u6c42\"}', '0', 'rnd'), +('zh-tw', 'custom', 'URSRList', '6', '{\"ERName\":\"Epic\",\"SRName\":\"Story\",\"URName\":\"Feature\"}', '0', 'rnd'); + +INSERT INTO `zt_storygrade` (`type`, `grade`, `name`, `status`) VALUES +('requirement', 1, 'UR', 'enable'), +('epic', 1, 'BR', 'enable'), +('story', 1, 'SR', 'enable'), +('story', 2, '子', 'enable'); REPLACE INTO `zt_stage` (`name`,`percent`,`type`,`projectType`,`createdBy`,`createdDate`,`editedBy`,`editedDate`,`deleted`) VALUES ('需求','10','request','waterfall','admin','2020-02-08 21:08:30','admin','2020-02-12 13:50:27','0'), @@ -2379,11 +2418,15 @@ INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES (' INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', '', 'CRProduct', '1'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', '', 'CRExecution', '1'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'custom', '', 'URSR', '2'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'custom', '', 'enableER', '0'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', 'global', 'mode', 'ALM'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'project', '', 'unitList', 'CNY,USD'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'project', '', 'defaultCurrency', 'CNY'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'story', '', 'reviewRules', 'allpass'); INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'common', 'global', 'syncProduct', '{"feedback":{},"ticket":{}}'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'story', '', 'gradeRule', 'cross'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'requirement', '', 'gradeRule', 'cross'); +INSERT INTO `zt_config` (`owner`, `module`, `section`, `key`, `value`) VALUES ('system', 'epic', '', 'gradeRule', 'cross'); -- DROP TABLE IF EXISTS `zt_im_chat`; CREATE TABLE IF NOT EXISTS `zt_im_chat` ( `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT, @@ -13590,7 +13633,9 @@ INSERT INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `vision` ('system', '产品', 'products', '1', 'rnd', 'admin', '1970-01-01 00:00:01', '{\"app\":\"system\",\"module\":\"product\",\"method\":\"getPairs\",\"methodDesc\":\"Get product pairs.\",\"params\":[{\"name\":\"mode\",\"type\":\"string\",\"desc\":\"\",\"value\":\"all\"}]}', '', '', ''), ('system', '项目', 'projects', '1', 'rnd', 'admin', '1970-01-01 00:00:01', '{\"app\":\"system\",\"module\":\"project\",\"method\":\"getPairsByModel\",\"methodDesc\":\"Get project pairs by model and project.\",\"params\":[{\"name\":\"model\",\"type\":\"string\",\"desc\":\"all|scrum|waterfall\",\"value\":\"all\"},{\"name\":\"programID\",\"type\":\"int\",\"desc\":\"\",\"value\":\"0\"},{\"name\":\"param\",\"type\":\"\",\"desc\":\"\",\"value\":\"\"}]}', '', '', ''), ('system', '产品线', 'productLines', '1', 'rnd', 'admin', '1970-01-01 00:00:01', '{\"app\":\"system\",\"module\":\"product\",\"method\":\"getLinePairs\",\"methodDesc\":\"Get line pairs.\",\"params\":[{\"name\":\"useShort\",\"type\":\"bool\",\"desc\":\"\",\"value\":\"\"}]}', '', '', ''), -('sql', '需求', 'stories', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\"', 'view_datasource_4', 'id', 'title'), +('sql', '软件需求', 'stories', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\" and type=\"story\"', 'view_datasource_4', 'id', 'title'), +('sql', '用户需求', 'requirements', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\" and type=\"requirement\"', 'view_datasource_3', 'id', 'title'), +('sql', '业务需求', 'epics', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_story where deleted=\"0\" type=\"epic\"', 'view_datasource_2', 'id', 'title'), ('sql', '任务', 'tasks', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,name from zt_task where deleted=\"0\" and vision=\"rnd\"', 'view_datasource_5', 'id', 'name'), ('sql', 'Bug', 'bugs', '1', 'rnd', 'admin', '1970-01-01 00:00:01', 'select id,title from zt_bug where deleted=\"0\"', 'view_datasource_6', 'id', 'title'), ('system', '权限分组', 'groups', '1', 'rnd', 'admin', '1970-01-01 00:00:01', '{\"app\":\"system\",\"module\":\"group\",\"method\":\"getPairs\",\"methodDesc\":\"\",\"params\":[]}', '', '', ''), @@ -13667,6 +13712,8 @@ INSERT INTO `zt_workflowdatasource` (`type`, `name`, `code`, `buildin`, `vision` ('lang', '需求管理周期', 'demandDuration', '1', 'or', 'admin', '1970-01-01 00:00:01', 'demandDuration', '', '', ''), ('lang', '需求BSA', 'demandBSA', '1', 'or', 'admin', '1970-01-01 00:00:01', 'demandBSA', '', '', ''); +DROP VIEW IF EXISTS `view_datasource_2`; +DROP VIEW IF EXISTS `view_datasource_3`; DROP VIEW IF EXISTS `view_datasource_4`; DROP VIEW IF EXISTS `view_datasource_5`; DROP VIEW IF EXISTS `view_datasource_6`; @@ -13677,7 +13724,9 @@ DROP VIEW IF EXISTS `view_datasource_41`; DROP VIEW IF EXISTS `view_datasource_54`; DROP VIEW IF EXISTS `view_datasource_55`; -CREATE VIEW `view_datasource_4` AS select `id`,`title` from `zt_story` where `deleted` = '0'; +CREATE VIEW `view_datasource_2` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'epic'; +CREATE VIEW `view_datasource_3` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'requirement'; +CREATE VIEW `view_datasource_4` AS select `id`,`title` from `zt_story` where `deleted` = '0' and type = 'story'; CREATE VIEW `view_datasource_5` AS select `id`,`name` from `zt_task` where `deleted` = '0' and vision = 'rnd'; CREATE VIEW `view_datasource_6` AS select `id`,`title` from `zt_bug` where `deleted` = '0'; CREATE VIEW `view_datasource_10` AS select `id`,`name` from `zt_build` where `deleted` = '0'; diff --git a/doc/CHANGELOG b/doc/CHANGELOG index af83e49d64..d7a8c4233a 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -1925,6 +1925,11 @@ IPD版: 50408 瀑布项目仪表盘区块没有提示文案 50411 专业研发看板中新建需求时选择其他区域保存后显示在了默认区域 50425 安装首页鼠标悬浮禅道软件公司时的链接不对 +50431 新手教程点击创建项目按钮报错 +50436 合并请求列表中开放中筛选标签不能正常筛选 +50437 无迭代项目下的bug转任务跳转到了添加执行表单 +50450 升级新版本时,有代码报错 +50453 多分支产品的关联项目数显示不对 50426 新手教程创建账号页面没有引导 50427 新手教程创建账号页面没有按顺序提示 50428 新手教程创建账号页面页面有多余提示框 diff --git a/framework/base/router.class.php b/framework/base/router.class.php index 4cdf2a70ee..c564cb3dd2 100644 --- a/framework/base/router.class.php +++ b/framework/base/router.class.php @@ -1608,6 +1608,8 @@ class baseRouter $this->loadLang('company'); $common->setUserConfig(); + $this->setDebug(); + return $common; } diff --git a/framework/control.class.php b/framework/control.class.php index 11d53f19d4..362d020667 100644 --- a/framework/control.class.php +++ b/framework/control.class.php @@ -519,8 +519,8 @@ class control extends baseControl if($this->config->edition == 'open') return $fields; if(!empty($this->app->installing) || !empty($this->app->upgrading)) return $fields; - $moduleName = $moduleName ? $moduleName : $this->app->getModuleName(); - $methodName = $methodName ? $moduleName : $this->app->getMethodName(); + $moduleName = $moduleName ? $moduleName : $this->app->rawModule; + $methodName = $methodName ? $moduleName : $this->app->rawMethod; $flow = $this->loadModel('workflow')->getByModule($moduleName); if(!$flow) return $fields; @@ -546,8 +546,8 @@ class control extends baseControl if($this->config->edition == 'open') return ''; if(!empty($this->app->installing) || !empty($this->app->upgrading)) return ''; - $moduleName = $moduleName ? $moduleName : $this->app->getModuleName(); - $methodName = $methodName ? $methodName : $this->app->getMethodName(); + $moduleName = $moduleName ? $moduleName : $this->app->rawModule; + $methodName = $methodName ? $methodName : $this->app->rawMethod; $flow = $this->loadModel('workflow')->getByModule($moduleName); if(!$flow) return ''; @@ -585,8 +585,8 @@ class control extends baseControl if($this->config->edition == 'open') return array(); if(!empty($this->app->installing) || !empty($this->app->upgrading)) return array(); - $moduleName = $moduleName ? $moduleName : $this->app->getModuleName(); - $methodName = $methodName ? $moduleName : $this->app->getMethodName(); + $moduleName = $moduleName ? $moduleName : $this->app->rawModule; + $methodName = $methodName ? $moduleName : $this->app->rawMethod; if(!$object) $object = new stdclass(); diff --git a/framework/model.class.php b/framework/model.class.php index f935518644..a28bc2f47b 100644 --- a/framework/model.class.php +++ b/framework/model.class.php @@ -290,6 +290,7 @@ class model extends baseModel */ public function processStatus($module, $record) { + if($module == 'story') $module = $record->type; if($this->config->edition == 'open' or empty($record->subStatus)) return zget($this->lang->$module->statusList, $record->status); return $this->loadModel('workflowfield')->processSubStatus($module, $record); diff --git a/framework/router.class.php b/framework/router.class.php index cd3c47bc0b..a5f27dfa62 100755 --- a/framework/router.class.php +++ b/framework/router.class.php @@ -258,7 +258,7 @@ class router extends baseRouter try { - $commonSettings = $this->dbQuery('SELECT `section`, `key`, `value` FROM' . TABLE_CONFIG . "WHERE `owner`='system' AND (`module`='custom' or `module`='common') and `key` in ('sprintConcept', 'hourPoint', 'URSR', 'mode', 'URAndSR', 'scoreStatus', 'disabledFeatures', 'closedFeatures')")->fetchAll(); + $commonSettings = $this->dbQuery('SELECT `section`, `key`, `value` FROM' . TABLE_CONFIG . "WHERE `owner`='system' AND (`module`='custom' or `module`='common') and `key` in ('sprintConcept', 'hourPoint', 'URSR', 'mode', 'URAndSR', 'enableER', 'scoreStatus', 'disabledFeatures', 'closedFeatures')")->fetchAll(); } catch (PDOException $exception) { @@ -266,7 +266,7 @@ class router extends baseRouter } } - $hourKey = $planKey = $URSR = $URAndSR = 0; + $hourKey = $planKey = $URSR = $URAndSR = $enableER = 0; $mode = 'ALM'; $score = '0'; @@ -280,6 +280,7 @@ class router extends baseRouter if($setting->key == 'hourPoint') $hourKey = $setting->value; if($setting->key == 'URSR') $URSR = $setting->value; if($setting->key == 'URAndSR') $URAndSR = $setting->value; + if($setting->key == 'enableER') $enableER = $setting->value; if($setting->key == 'mode' and $setting->section == 'global') $mode = $setting->value; if($setting->key == 'scoreStatus' and $setting->section == 'global') $score = $setting->value; if($setting->key == 'disabledFeatures') $disabledFeatures = $setting->value; @@ -313,7 +314,8 @@ class router extends baseRouter /* User preference init. */ $config->URSR = $URSR; - $config->URAndSR = ($URAndSR and !str_contains(",{$config->disabledFeatures},", ',productUR,')); + $config->URAndSR = ($URAndSR and !str_contains(",{$config->disabledFeatures},", ',productUR,')); + $config->enableER = ($enableER and !str_contains(",{$config->disabledFeatures},", ',productER,')); $config->programLink = 'program-browse'; $config->productLink = 'product-all'; $config->projectLink = 'project-browse'; @@ -338,8 +340,9 @@ class router extends baseRouter if($setting->key == 'executionLink') $config->executionLink = $setting->value; } - $lang->URCommon = ''; - $lang->SRCommon = ''; + $lang->ERCommon = $config->storyCommonList[$this->clientLang]['epic']; + $lang->URCommon = $config->storyCommonList[$this->clientLang]['requirement']; + $lang->SRCommon = $config->storyCommonList[$this->clientLang]['story']; if($this->dbh and !empty($this->config->db->name)) { if(!$this->upgrading) @@ -350,16 +353,19 @@ class router extends baseRouter if(empty($URSRList)) $URSRList = $this->dbQuery('SELECT `key`, `value` FROM' . TABLE_LANG . "WHERE module = 'custom' and `section` = 'URSRList' and `key` = '{$config->URSR}'")->fetchAll(); /* Get UR pairs and SR pairs. */ + $ERPairs = array(); $URPairs = array(); $SRPairs = array(); foreach($URSRList as $id => $value) { - $URSR = json_decode((string) $value->value); - $URPairs[$value->key] = $URSR->URName; - $SRPairs[$value->key] = $URSR->SRName; + $URSRObj = json_decode((string) $value->value); + $ERPairs[$value->key] = $URSRObj->ERName; + $URPairs[$value->key] = $URSRObj->URName; + $SRPairs[$value->key] = $URSRObj->SRName; } /* Set default story concept and init UR and SR concept. */ + $lang->ERCommon = $ERPairs[$config->URSR] ?? reset($ERPairs); $lang->URCommon = $URPairs[$config->URSR] ?? reset($URPairs); $lang->SRCommon = $SRPairs[$config->URSR] ?? reset($SRPairs); @@ -370,32 +376,17 @@ class router extends baseRouter $customMenus = $this->dbQuery('SELECT * FROM' . TABLE_LANG . "WHERE `module`='common' AND `lang`='{$this->clientLang}' AND `section`='' AND `vision`='{$config->vision}'")->fetchAll(); } catch(PDOException){} - foreach($customMenus as $menu) if(isset($lang->{$menu->key})) $lang->{$menu->key} = $menu->value; + + foreach($customMenus as $menu) + { + /* Modified the language item of dev story-related navigation, which will only affect users whose personalized value is the same as the story concept default value.*/ + if(in_array($menu->key, array('ERCommon', 'URCommon', 'SRCommon')) && $URSR != $config->URSR) continue; + if(isset($lang->{$menu->key})) $lang->{$menu->key} = $menu->value; + } } } } - /** - * Save error info. - * - * @param int $level - * @param string $message - * @param string $file - * @param int $line - * @access public - * @return void - */ - public function saveError($level, $message, $file, $line) - { - $fatalLevel = array(); - $fatalLevel[E_ERROR] = E_ERROR; - $fatalLevel[E_PARSE] = E_PARSE; - $fatalLevel[E_CORE_ERROR] = E_CORE_ERROR; - $fatalLevel[E_USER_ERROR] = E_USER_ERROR; - if(isset($fatalLevel[$level]) && (!isset($this->config->debug) || !$this->config->debug)) $this->config->debug = true; - parent::saveError($level, $message, $file, $line); - } - /** * 企业版部分功能是从然之合并过来的。然之代码中调用loadModuleConfig方法时传递了一个非空的appName,在禅道中会导致错误。 * 把appName设置为空来避免这个错误。 diff --git a/lib/base/front/front.class.php b/lib/base/front/front.class.php index f04073ad50..6f72aa7a53 100644 --- a/lib/base/front/front.class.php +++ b/lib/base/front/front.class.php @@ -1191,6 +1191,8 @@ EOT; $jsConfig->themeRoot = $themeRoot; $jsConfig->currentModule = $moduleName; $jsConfig->currentMethod = $methodName; + $jsConfig->rawModule = $app->rawModule; + $jsConfig->rawMethod = $app->rawMethod; $jsConfig->clientLang = $clientLang; $jsConfig->requiredFields = $requiredFields; $jsConfig->router = $app->server->SCRIPT_NAME; diff --git a/lib/form/form.class.php b/lib/form/form.class.php index 9bb618ecd4..8da1aeac92 100644 --- a/lib/form/form.class.php +++ b/lib/form/form.class.php @@ -110,8 +110,8 @@ class form extends fixer global $app, $config; if($config->edition == 'open' || !empty($app->installing)) return $configObject; - $moduleName = $moduleName ? $moduleName : $app->getModuleName(); - $methodName = $methodName ? $moduleName : $app->getMethodName(); + $moduleName = $moduleName ? $moduleName : $app->rawModule; + $methodName = $methodName ? $moduleName : $app->rawMethod; $flow = $app->control->loadModel('workflow')->getByModule($moduleName); if(!$flow) return $configObject; @@ -193,8 +193,9 @@ class form extends fixer { global $app, $config; - $module = $app->getModuleName(); + $module = $app->getModuleName() == 'story' ? $app->rawModule : $app->getModuleName(); $method = $app->getMethodName(); + if($method == 'batchcreate') $method = 'create'; if($method == 'batchedit') $method = 'edit'; diff --git a/lib/zin/core/node.class.php b/lib/zin/core/node.class.php index 628c53c8dc..d2e61af7d4 100644 --- a/lib/zin/core/node.class.php +++ b/lib/zin/core/node.class.php @@ -59,8 +59,7 @@ class node implements \JsonSerializable public function __construct(mixed ...$args) { - global $config; - $this->gid = (isset($config->clientCache) && $config->clientCache) ? static::nextGid() : 'zin_' . uniqid(); + $this->gid = 'zin_' . uniqid(); $this->props = new props(); disableGlobalRender(); diff --git a/lib/zin/wg/detailheader/v1.php b/lib/zin/wg/detailheader/v1.php index 9091e97603..22497a6a7d 100644 --- a/lib/zin/wg/detailheader/v1.php +++ b/lib/zin/wg/detailheader/v1.php @@ -38,7 +38,7 @@ class detailHeader extends wg return div ( - setClass('detail-header flex justify-between mb-3 min-w-0 flex-nowrap'), + setClass('detail-header flex justify-between mb-3 min-w-0 flex-nowrap', $this->prop('class')), div ( setClass('flex flex-auto min-w-0 items-center gap-x-4 flex-nowrap pr-5'), diff --git a/lib/zin/wg/formsettingbtn/js/v1.js b/lib/zin/wg/formsettingbtn/js/v1.js index 8e50afc076..1abf5b58d5 100644 --- a/lib/zin/wg/formsettingbtn/js/v1.js +++ b/lib/zin/wg/formsettingbtn/js/v1.js @@ -19,7 +19,16 @@ window.cancelFormSetting = function(e) window.revertDefaultFields = function(e) { - $.getJSON($(e.target).closest('button').data('url'), function(response) + const $panel = $(e.target).closest('.panel-form'); + const $form = $(e.target).closest('form'); + const $globalBox = $panel.find('.form-actions input[name=global]'); + const submitUrl = $(e.target).closest('button').data('url'); + + postData = new FormData(); + postData.append('action', 'reset'); + if($globalBox.length > 0) postData.append('global', $globalBox.prop('checked') ? '1' : '0'); + + $.post(submitUrl, postData, function(response) { const customFields = response.customFields.split(','); const showFields = response.showFields.split(','); @@ -41,9 +50,14 @@ window.onSubmitFormtSetting = function(e) { const customFields = []; const showFields = []; + const $panel = $(e.target).closest('.panel-form'); const $form = $(e.target).closest('form'); + const $globalBox = $panel.find('.form-actions input[name=global]'); - $.post($form.attr('action'), new FormData($form[0]), function() + postData = new FormData($form[0]); + if($globalBox.length > 0) postData.append('global', $globalBox.prop('checked') ? '1' : '0'); + + $.post($form.attr('action'), postData, function() { $form.find('input[type="checkbox"]').each(function() { diff --git a/lib/zin/wg/formsettingbtn/v1.php b/lib/zin/wg/formsettingbtn/v1.php index 6cc11e806c..344da48fda 100644 --- a/lib/zin/wg/formsettingbtn/v1.php +++ b/lib/zin/wg/formsettingbtn/v1.php @@ -15,8 +15,13 @@ namespace zin; class formSettingBtn extends wg { protected static array $defaultProps = array( - 'customFields' => array(), - 'urlParams' => '' + 'customFields' => array(), + 'urlParams' => '', + 'canGlobal' => false, + 'submitCallBack' => '', + 'restoreCallBack' => '', + 'noCancel' => false, + 'text' => '' ); public static function getPageCSS(): ?string @@ -52,18 +57,26 @@ class formSettingBtn extends wg protected function build() { + list($urlParams, $text, $submitCallback, $restoreCallback, $canGlobal, $noCancel) = $this->prop(array('urlParams', 'text', 'submitCallback', 'restoreCallback', 'canGlobal', 'noCancel')); $customFields = $this->prop('customFields', array()); + $urlParams = $this->prop('urlParams', ''); global $lang; - $customLink = createLink('custom', 'ajaxSaveCustomFields', $this->prop('urlParams', '')); - $cancelLink = createLink('custom', 'ajaxGetCustomFields', $this->prop('urlParams', '')); + if($canGlobal) + { + global $app; + $app->loadLang('datatable'); // Use lang->datatable->setGlobal variable. + } + + $customLink = createLink('custom', 'ajaxSaveCustomFields', $urlParams); + $cancelLink = createLink('custom', 'ajaxGetCustomFields', $urlParams); return dropdown ( set::arrow('false'), set::placement('bottom-end'), set::id('formSettingBtn'), - to::trigger(btn(set::icon('cog-outline'), setClass('ghost'), set::caret(false))), + to::trigger(btn(set::icon('cog-outline'), $text, setClass('ghost'), set::caret(false))), to::menu(menu ( setClass('dropdown-menu'), @@ -76,11 +89,15 @@ class formSettingBtn extends wg set::showExtra(false), set::actions(array ( - btn(set::text($lang->save), setClass('primary'), on::click('onSubmitFormtSetting')), - btn(set::text($lang->cancel), set::btnType('button'), on::click('cancelFormSetting'), set('data-url', $cancelLink)), - btn(set::text($lang->restore), setClass('text-primary ghost'), set::href('#'), set('data-url', $customLink), on::click('revertDefaultFields')) + $canGlobal ? checkList(set::name('global'), setClass('whitespace-nowrap'), set::style(array('padding' => 0)), set::inline(true), set::items(array(array('text' => $lang->datatable->setGlobal, 'value' => '1')))) : null, + btn(set::text($lang->save), setClass('primary'), on::click('onSubmitFormtSetting'), $submitCallback ? on::click($submitCallback) : null), + $noCancel ? null : btn(set::text($lang->cancel), set::btnType('button'), on::click('cancelFormSetting'), set('data-url', $cancelLink)), + btn(set::text($lang->restore), setClass('text-primary ghost'), set('data-url', $customLink), on::click('revertDefaultFields'), $restoreCallback ? on::click($restoreCallback) : null) + )), + to::headingActions(array + ( + btn(set::icon('close'), setClass('ghost'), set::size('sm'), on::click('closeCustomPopupMenu')) )), - to::headingActions(array(btn(set::icon('close'), setClass('ghost'), set::size('sm'), on::click('closeCustomPopupMenu')))), $this->buildCustomFields($customFields) ) )) diff --git a/lib/zin/wg/globalsearch/js/v1.js b/lib/zin/wg/globalsearch/js/v1.js index 668bd8ebff..2c63a83956 100644 --- a/lib/zin/wg/globalsearch/js/v1.js +++ b/lib/zin/wg/globalsearch/js/v1.js @@ -18,8 +18,8 @@ window.getGlobalSearchType = function() return pageWindow.globalSearchType; } - const moduleName = pageWindow.config.currentModule; - const methodName = pageWindow.config.currentMethod; + const moduleName = pageWindow.config.rawModule; + const methodName = pageWindow.config.rawMethod; if(moduleName == 'product' && methodName == 'browse') return 'story'; if(moduleName == 'my' || moduleName == 'user') return methodName; diff --git a/lib/zin/wg/linkedstorylist/v1.php b/lib/zin/wg/linkedstorylist/v1.php index 71f8732724..6406704391 100644 --- a/lib/zin/wg/linkedstorylist/v1.php +++ b/lib/zin/wg/linkedstorylist/v1.php @@ -10,7 +10,6 @@ class linkedStoryList extends storyList protected static array $defineProps = array ( 'story' => '?object', // 被关联的需求。 - 'relationType' => '?string', // 关联类型。 'unlinkBtn' => '?array|bool', // 取消关联按钮。 'unlinkStoryTip' => '?string', // 取消关联提示信息。 'unlinkUrl' => '?string', // 取消关联需求链接 @@ -42,21 +41,12 @@ class linkedStoryList extends storyList protected null|array|bool $unlinkBtn = null; - protected ?string $relationType = null; - protected function beforeBuild() { $story = $this->prop('story'); if(!$story) $story = data('story'); if(!$story) return; - $relationType = $this->prop('relationType'); - if(!$relationType) $relationType = $story->type == 'story' ? 'requirement' : 'story'; - - $viewUrl = $this->prop('viewUrl'); - if($viewUrl === null) $viewUrl = hasPriv($relationType, 'view', null, "storyType=$relationType"); - if($viewUrl === true) $viewUrl = createLink('story', 'view', "id={id}&version=0¶m=0&storyType=$relationType"); - $unlinkUrl = $this->prop('unlinkUrl'); if($unlinkUrl === null) $unlinkUrl = createLink('story', 'linkStory', "storyID={$story->id}&type=remove&linkedID={id}&browseType=&queryID=0&storyType=$story->type"); @@ -64,11 +54,9 @@ class linkedStoryList extends storyList $unlinkBtn = $this->prop('unlinkBtn'); if($unlinkBtn === null) $unlinkBtn = $canLink; - $this->story = $story; - $this->relationType = $relationType; - $this->viewUrl = $viewUrl; - $this->unlinkUrl = $unlinkUrl; - $this->unlinkBtn = $unlinkBtn; + $this->story = $story; + $this->unlinkUrl = $unlinkUrl; + $this->unlinkBtn = $unlinkBtn; } protected function getItem(object $story): array @@ -90,7 +78,7 @@ class linkedStoryList extends storyList 'data-url' => str_replace('{id}', "$story->id", $this->unlinkUrl), 'data-params' => 'event', 'data-call' => 'unlinkStory', - 'hint' => $story->type == 'requirement' ? str_replace($lang->SRCommon, $lang->URCommon, $lang->story->unlinkStory) : $lang->story->unlinkStory + 'hint' => $lang->story->unlink ); if(is_array($unlinkBtn)) $btn = array_merge($btn, $unlinkBtn); @@ -135,7 +123,7 @@ class linkedStoryList extends storyList setData(array('toggle' => 'modal', 'size' => 'lg')), setID('linkButton'), is_array($newLinkBtn) ? set($newLinkBtn) : null, - $lang->story->link . ($story->type == 'story' ? $lang->story->requirement : $lang->story->story) + $lang->story->linkStory ); return array($list, $btn); } diff --git a/lib/zin/wg/pripicker/v1.php b/lib/zin/wg/pripicker/v1.php index 74865292a7..9171c0958a 100644 --- a/lib/zin/wg/pripicker/v1.php +++ b/lib/zin/wg/pripicker/v1.php @@ -76,6 +76,7 @@ class priPicker extends wg $moduleName = $app->getModuleName(); if(isset($lang->$moduleName->priList)) $props['items'] = $lang->$moduleName->priList; } + if(isset($props['items'][0])) unset($props['items'][0]); return zui::priPicker ( set::_class('form-group-wrapper'), diff --git a/lib/zin/wg/searchtoggle/v1.php b/lib/zin/wg/searchtoggle/v1.php index 7a03b84fc1..f0e47e5f3a 100644 --- a/lib/zin/wg/searchtoggle/v1.php +++ b/lib/zin/wg/searchtoggle/v1.php @@ -9,7 +9,8 @@ class searchToggle extends wg 'open?: bool', 'module?: string', 'url?: string', - 'searchUrl?: string' + 'searchUrl?: string', + 'text?: string' ); public static function getPageCSS(): ?string @@ -29,10 +30,11 @@ class searchToggle extends wg protected function build() { global $lang, $app, $config; - list($target, $module, $open, $url, $searchUrl) = $this->prop(array('target', 'module', 'open', 'url', 'searchUrl')); + list($target, $module, $open, $url, $searchUrl, $text) = $this->prop(array('target', 'module', 'open', 'url', 'searchUrl', 'text')); if(is_null($open) && !empty($_GET['browseType'])) $open = $_GET['browseType'] === 'bySearch'; if(is_null($module)) $module = $app->rawModule; + if(empty($text)) $text = $lang->searchAB; if(isset($config->zin->mode) && $config->zin->mode == 'compatible') { @@ -45,7 +47,7 @@ class searchToggle extends wg set::className('search-form-toggle rounded-full gray-300-outline size-sm'), set::icon('search'), set::active($open), - set::text($lang->searchAB), + set::text($text), toggle::searchform(array('module' => $module, 'target' => $target, 'url' => $url, 'searchUrl' => $searchUrl)), $open ? on::init()->call('zui.toggleSearchForm', array('module' => $module, 'target' => $target, 'show' => $open, 'url' => $url, 'searchUrl' => $searchUrl)) : on::init()->do('$element.closest(".show-search-form").removeClass("show-search-form").find(".search-form[data-module=' . $module . ']").remove()') ); diff --git a/lib/zin/wg/storybasicinfo/v1.php b/lib/zin/wg/storybasicinfo/v1.php index c033030b05..0130206ed6 100644 --- a/lib/zin/wg/storybasicinfo/v1.php +++ b/lib/zin/wg/storybasicinfo/v1.php @@ -78,6 +78,8 @@ class storyBasicInfo extends wg $hiddenPlan = $this->prop('hiddenPlan', data('hiddenPlan')); $statusText = $this->prop('statusText', $story->status); $users = $this->prop('users', data('users')); + $gradePairs = $this->prop('gradePairs', data('gradePairs')); + $showGrade = $this->prop('showGrade', data('showGrade')); $items = array(); if(!$product->shadow) @@ -93,7 +95,22 @@ class storyBasicInfo extends wg 'control' => 'breadcrumb', 'items' => $this->getModuleItems($story, $product) ); - if($story->type != 'requirement' and $story->parent != -1 and !$hiddenPlan) + if(isset($story->parentName)) + { + $items[$lang->story->parent] = array( + 'control' => 'text', + 'content' => $story->parentName + ); + } + if($showGrade) + { + $items[$lang->story->grade] = array + ( + 'control' => 'text', + 'content' => zget($gradePairs, $story->grade) + ); + } + if(!$hiddenPlan) { $planTitleItems = array(); if(isset($story->planTitle) && $story->planTitle) @@ -104,7 +121,7 @@ class storyBasicInfo extends wg ( 'control' => 'link', 'url' => createLink('productplan', 'view', "planID=$planID"), - 'text' => $planTitle + 'text' => $planTitle . ' ' ) : $planTitle; } } diff --git a/lib/zin/wg/storylist/v1.php b/lib/zin/wg/storylist/v1.php index e08c936cff..0fe94469c9 100644 --- a/lib/zin/wg/storylist/v1.php +++ b/lib/zin/wg/storylist/v1.php @@ -10,4 +10,53 @@ class storyList extends entitylist ( 'type' => 'story' ); + + protected function getItem(object $entity): array + { + $item = array + ( + 'innerClass' => 'px-0 relative group', + 'leading' => array(), + 'innerTag' => 'div', + 'titleClass' => 'flex gap-2 items-center flex-auto min-w-0', + 'textClass' => 'flex-none', + 'actionsClass' => $this->compact ? 'absolute top-0.5 right-0' : null, + 'hint' => $entity->title, + 'title' => span(setClass('text-clip'), $entity->title), + 'leading' => idLabel::create($entity->id) + ); + + if(hasPriv($entity->type, 'view')) + { + $item['titleAttrs'] = array('data-toggle' => 'modal', 'data-size' => 'lg'); + $item['url'] = createLink($entity->type, 'view', "id=$entity->id"); + } + + return $item; + } + + protected function getItems() + { + global $lang; + $items = $this->prop('items', array()); + $list = array(); + + foreach($items as $key => $entity) + { + if(is_object($entity)) + { + $list[] = $this->getItem($entity); + } + elseif(is_array($entity)) + { + $list[] = array('id' => $key, 'title' => $lang->story->typeList[$key]); + foreach($entity as $subEntity) + { + $list[] = $this->getItem($subEntity); + } + } + } + + return $list; + } } diff --git a/lib/zin/wg/storyrelatedlist/v1.php b/lib/zin/wg/storyrelatedlist/v1.php index d2c5ed447a..b6e86683a9 100644 --- a/lib/zin/wg/storyrelatedlist/v1.php +++ b/lib/zin/wg/storyrelatedlist/v1.php @@ -35,7 +35,6 @@ class storyRelatedList extends relatedList $cases = $this->prop('cases', data('cases')); $builds = $this->prop('builds', data('builds')); $releases = $this->prop('releases', data('releases')); - $storyProducts = $this->prop('storyProducts', data('storyProducts')); $linkedMRs = $this->prop('linkedMRs', data('linkedMRs')); $linkedCommits = $this->prop('linkedCommits', data('linkedCommits')); $data = array(); @@ -90,26 +89,6 @@ class storyRelatedList extends relatedList ); } - $data['story'] = array - ( - 'title' => $lang->story->linkStories, - 'items' => isset($story->linkStoryList) ? $story->linkStoryList : array(), - 'url' => false, - 'statusList' => $lang->story->statusList, - 'onRender' => function($item, $linkedStory) use($storyProducts, $story, $app) - { - $storyID = $linkedStory->id; - $hasPriv = ($app->user->admin || str_contains(",{$app->user->view->products},", ",{$storyProducts[$storyID]},")); - $item['url'] = $hasPriv ? createLink('story', 'view', "storyID=$storyID&version=0¶m=0&storyType=$story->type") : false; - if($hasPriv) - { - $item['data-toggle'] = 'modal'; - $item['data-size'] = 'lg'; - } - return $item; - } - ); - if($isStoryType && helper::hasFeature('devops')) { $data['mr'] = array diff --git a/lib/zin/wg/thinkpestel/v1.php b/lib/zin/wg/thinkpestel/v1.php index 4bb13e0100..de528f58cf 100644 --- a/lib/zin/wg/thinkpestel/v1.php +++ b/lib/zin/wg/thinkpestel/v1.php @@ -40,7 +40,7 @@ class thinkPestel extends thinkModel $mode === 'preview' ? div(setClass('w-full text-center text-sm leading-tight text-gray-400'), $lang->thinkwizard->block . $lang->thinkwizard->blockList[$blockIndex]) : null, div ( - setClass('h-full mt-1 mx-px bg-opacity-20 model-block bg-' . $blockColor), + setClass('h-full mt-1 mx-px model-block bg-' . $blockColor . '-100'), div ( setClass('h-16 px-2 py-3 w-full relative z-10 flex justify-center', 'text-' . $blockColor), diff --git a/lib/zin/wg/thinkpffa/v1.php b/lib/zin/wg/thinkpffa/v1.php index 5207c5892f..c09683bd78 100644 --- a/lib/zin/wg/thinkpffa/v1.php +++ b/lib/zin/wg/thinkpffa/v1.php @@ -32,7 +32,7 @@ class thinkPffa extends thinkModel return div ( - setClass('w-full h-full px-5 py-2.5 outline overflow-auto col justify-between gap-2 bg-opacity-20 bg-' . $blockColor, $blockColor . '-outline'), + setClass('w-full h-full px-5 py-2.5 outline overflow-auto col justify-between gap-2 bg-' . $blockColor . '-100', $blockColor . '-outline'), div ( setClass('col gap-2'), diff --git a/lib/zin/wg/thinkresult/css/v1.css b/lib/zin/wg/thinkresult/css/v1.css index 17260eb329..c691f40df1 100644 --- a/lib/zin/wg/thinkresult/css/v1.css +++ b/lib/zin/wg/thinkresult/css/v1.css @@ -3,12 +3,16 @@ .think-result-content .think-check-list .item-control {cursor: unset;} .think-result-content .think-check-list .item-control:not(.is-checked):hover {border-color: inherit;} -.think-result-content .bg-think-blue {background-color: rgba(34, 148, 251, var(--tw-bg-opacity));} +.think-result-content .bg-think-blue-100 {background-color: #cfe7fe;} .think-result-content .text-think-blue {color: #2294FB;} -.think-result-content .bg-think-yellow {background-color: rgba(250, 174, 26, var(--tw-bg-opacity));} +.think-result-content .bg-think-yellow-100 {background-color: #FFF2D9;} .think-result-content .text-think-yellow {color: #FAAE1A;} .think-result-content .think-blue-outline {--skin-ring: #2294FB;} -.think-result-content .special-outline {--skin-text: unset; --skin-bg: rgba(var(--color-special-500-rgb), var(--tw-bg-opacity));} -.think-result-content .success-outline {--skin-text: unset; --skin-bg: rgba(var(--color-success-500-rgb), var(--tw-bg-opacity));} -.think-result-content .warning-outline {--skin-text: unset; --skin-bg: rgba(var(--color-warning-500-rgb), var(--tw-bg-opacity));} -.think-result-content .important-outline {--skin-text: unset; --skin-bg: rgba(var(--color-important-500-rgb), var(--tw-bg-opacity));} +.think-result-content .special-outline {--skin-text: unset; --skin-bg: var(--color-special-100);} +.think-result-content .success-outline {--skin-text: unset; --skin-bg: var(--color-success-100);} +.think-result-content .warning-outline {--skin-text: unset; --skin-bg: var(--color-warning-100);} +.think-result-content .important-outline {--skin-text: unset; --skin-bg: var(--color-important-100);} +.bg-success-100 {background-color: var(--color-success-100);} +.bg-special-100 {background-color: var(--color-special-100);} +.bg-important-100 {background-color: var(--color-important-100);} +.bg-warning-100 {background-color: var(--color-warning-100);} diff --git a/lib/zin/wg/thinkswot/css/v1.css b/lib/zin/wg/thinkswot/css/v1.css index abdb48b8c7..7071f7b759 100644 --- a/lib/zin/wg/thinkswot/css/v1.css +++ b/lib/zin/wg/thinkswot/css/v1.css @@ -5,12 +5,12 @@ .model-swot .model-block.block-1::after {content: 'W';} .model-swot .model-block.block-2::after {content: 'O';} .model-swot .model-block.block-3::after {content: 'T';} -.model-swot .model-block.block-0 {background: rgba(34, 148, 251, .2);} -.model-swot .model-block.block-1 {background: rgba(var(--color-special-400-rgb), .2);} -.model-swot .model-block.block-2 {background: rgba(var(--color-success-400-rgb), .2);} -.model-swot .model-block.block-3 {background: rgba(var(--color-warning-400-rgb), .2);} +.model-swot .model-block.block-0 {background-color: #cfe7fe;} +.model-swot .model-block.block-1 {background-color: var(--color-success-100);} +.model-swot .model-block.block-2 {background-color: var(--color-special-100);} +.model-swot .model-block.block-3 {background-color: var(--color-warning-100);} .model-swot .model-block .item-step-title {font-size: 1.75rem;} .model-swot .model-block.block-0 .item-step-title {color: #2294FB;} -.model-swot .model-block.block-1 .item-step-title {color: var(--color-special-400);} -.model-swot .model-block.block-2 .item-step-title {color: var(--color-success-400);} +.model-swot .model-block.block-1 .item-step-title {color: var(--color-success-400);} +.model-swot .model-block.block-2 .item-step-title {color: var(--color-special-400);} .model-swot .model-block.block-3 .item-step-title {color: var(--color-warning-400);} diff --git a/misc/ci/Jenkinsfile b/misc/ci/Jenkinsfile index 0486928444..64cb88b883 100644 --- a/misc/ci/Jenkinsfile +++ b/misc/ci/Jenkinsfile @@ -628,7 +628,7 @@ pipeline { def j = job.newJob() j.SaveHeadReport('./.head.md') ximNotify(title: "禅道镜像构建成功", contentFile: "report.md", contentHeaderFile: './.head.md') - + if (env.DEPLOY_ZENTAOPAAS=='true') { deploy.upgradePaas(env.ZENTAOPAAS_SERVER, env.CNE_API_TOKEN) ximNotify(title: "禅道平台版DEMO部署成功", content: "访问地址 ["+env.ZENTAOPAAS_SERVER+"]("+env.ZENTAOPAAS_SERVER+")") @@ -701,5 +701,3 @@ pipeline { } } - - diff --git a/module/action/config.php b/module/action/config.php index e9ff5031b9..bf6f8116ce 100755 --- a/module/action/config.php +++ b/module/action/config.php @@ -1,5 +1,6 @@ action->objectNameFields['product'] = 'name'; +$config->action->objectNameFields['epic'] = 'title'; $config->action->objectNameFields['story'] = 'title'; $config->action->objectNameFields['requirement'] = 'title'; $config->action->objectNameFields['productplan'] = 'title'; @@ -82,7 +83,7 @@ $config->action->majorList['execution'] = array('opened', 'edited'); $config->action->majorList['doc'] = array('releaseddoc'); $config->action->needGetProjectType = 'build,task,bug,case,testcase,caselib,testtask,testsuite,testreport,doc,issue,release,risk,design,opportunity,trainplan,gapanalysis,researchplan,researchreport,'; -$config->action->needGetRelateField = ',branch,story,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,module,review,'; +$config->action->needGetRelateField = ',branch,story,epic,requirement,productplan,release,task,build,bug,testcase,case,testtask,testreport,doc,doclib,issue,risk,opportunity,trainplan,gapanalysis,team,whitelist,researchplan,researchreport,meeting,kanbanlane,kanbancolumn,module,review,'; $config->action->noLinkModules = ',doclib,module,webhook,gitlab,instance,gitea,gogs,sonarqube,pipeline,jenkins,kanban,kanbanspace,kanbancolumn,kanbanlane,kanbanregion,kanbancard,execution,project,traincategory,apistruct,program,product,user,entry,repo,pivot,scene,boardspace,auditplan,auditresult,'; $config->action->ignoreObjectType4Dynamic = 'kanbanregion,kanbanlane,kanbancolumn'; $config->action->ignoreActions4Dynamic = 'disconnectxuanxuan,reconnectxuanxuan,loginxuanxuan,logoutxuanxuan,editmr,removemr'; diff --git a/module/action/lang/de.php b/module/action/lang/de.php index 755abf2e2c..7ea8ad7f45 100644 --- a/module/action/lang/de.php +++ b/module/action/lang/de.php @@ -104,6 +104,7 @@ $lang->action->dynamic->search = 'Suche'; $lang->action->objectTypes['product'] = $lang->productCommon; $lang->action->objectTypes['branch'] = 'Branch'; +$lang->action->objectTypes['epic'] = $lang->ERCommon; $lang->action->objectTypes['story'] = $lang->SRCommon; $lang->action->objectTypes['requirement'] = $lang->URCommon; $lang->action->objectTypes['design'] = 'Design'; @@ -199,6 +200,7 @@ $lang->action->desc->edited = '$date, bearbeitet von $acto $lang->action->desc->assigned = '$date, $actor zugewisen an $extra.' . "\n"; $lang->action->desc->closed = '$date, geschlossen von $actor .' . "\n"; $lang->action->desc->closedbysystem = '$date, closed by system.' . "\n"; +$lang->action->desc->closedbyparent = '$date, closed by system.' . "\n"; $lang->action->desc->deleted = '$date, gelöscht von $actor .' . "\n"; $lang->action->desc->deletedfile = '$date, $actor löschte $extra.' . "\n"; $lang->action->desc->editfile = '$date, $actor bearbeitete $extra.' . "\n"; @@ -442,6 +444,7 @@ $lang->action->label->unlinkchildrenstory = "unlinked a child story"; $lang->action->label->linkparentstory = "linked a parent story"; $lang->action->label->unlinkparentstory = "unlink from parent story"; $lang->action->label->deletechildrenstory = "delete children story"; +$lang->action->label->syncgrade = 'synced grade'; $lang->action->label->tracked = 'tracked'; $lang->action->label->hangup = 'hangup'; $lang->action->label->run = 'run'; diff --git a/module/action/lang/en.php b/module/action/lang/en.php index 09c6694c6a..8efa20cd9f 100755 --- a/module/action/lang/en.php +++ b/module/action/lang/en.php @@ -104,6 +104,7 @@ $lang->action->dynamic->search = 'Search'; $lang->action->objectTypes['product'] = $lang->productCommon; $lang->action->objectTypes['branch'] = 'Branch'; +$lang->action->objectTypes['epic'] = $lang->ERCommon; $lang->action->objectTypes['story'] = $lang->SRCommon; $lang->action->objectTypes['requirement'] = $lang->URCommon; $lang->action->objectTypes['design'] = 'Design'; @@ -199,6 +200,7 @@ $lang->action->desc->edited = '$date, edited by $actoraction->desc->assigned = '$date, $actor assigned to $extra.' . "\n"; $lang->action->desc->closed = '$date, closed by $actor .' . "\n"; $lang->action->desc->closedbysystem = '$date, closed by system.' . "\n"; +$lang->action->desc->closedbyparent = '$date, closed by system.' . "\n"; $lang->action->desc->deleted = '$date, deleted by $actor .' . "\n"; $lang->action->desc->deletedfile = '$date, $actor deleted $extra.' . "\n"; $lang->action->desc->editfile = '$date, $actor edited $extra.' . "\n"; @@ -442,6 +444,7 @@ $lang->action->label->unlinkchildrenstory = "unlinked a child story"; $lang->action->label->linkparentstory = "linked a parent story"; $lang->action->label->unlinkparentstory = "unlink from parent story"; $lang->action->label->deletechildrenstory = "delete children story"; +$lang->action->label->syncgrade = 'synced grade'; $lang->action->label->tracked = 'tracked'; $lang->action->label->hangup = 'hangup'; $lang->action->label->run = 'run'; diff --git a/module/action/lang/fr.php b/module/action/lang/fr.php index 29df5d5f3e..566fd78e55 100644 --- a/module/action/lang/fr.php +++ b/module/action/lang/fr.php @@ -104,6 +104,7 @@ $lang->action->dynamic->search = 'Rechercher'; $lang->action->objectTypes['product'] = $lang->productCommon; $lang->action->objectTypes['branch'] = 'Branche'; +$lang->action->objectTypes['epic'] = $lang->ERCommon; $lang->action->objectTypes['story'] = $lang->SRCommon; $lang->action->objectTypes['requirement'] = $lang->URCommon; $lang->action->objectTypes['design'] = 'Design'; @@ -199,6 +200,7 @@ $lang->action->desc->edited = '$date, édité par $actoraction->desc->assigned = '$date, $actor affecté à $extra.' . "\n"; $lang->action->desc->closed = '$date, fermé par $actor .' . "\n"; $lang->action->desc->closedbysystem = '$date, closed by system.' . "\n"; +$lang->action->desc->closedbyparent = '$date, closed by system.' . "\n"; $lang->action->desc->deleted = '$date, supprimé par $actor .' . "\n"; $lang->action->desc->deletedfile = '$date, $actor a supprimé $extra.' . "\n"; $lang->action->desc->editfile = '$date, $actor a édité $extra.' . "\n"; @@ -442,6 +444,7 @@ $lang->action->label->unlinkchildrenstory = "a décroché de la sous-story"; $lang->action->label->linkparentstory = "a raccroché à une story parent"; $lang->action->label->unlinkparentstory = "a décroché de la story parent"; $lang->action->label->deletechildrenstory = "a supprimé la sous-story"; +$lang->action->label->syncgrade = 'synced grade'; $lang->action->label->tracked = 'tracked'; $lang->action->label->hangup = 'hangup'; $lang->action->label->run = 'run'; diff --git a/module/action/lang/zh-cn.php b/module/action/lang/zh-cn.php index 081acaae72..c3a7667ade 100755 --- a/module/action/lang/zh-cn.php +++ b/module/action/lang/zh-cn.php @@ -104,6 +104,7 @@ $lang->action->dynamic->search = '搜索'; $lang->action->objectTypes['product'] = $lang->productCommon; $lang->action->objectTypes['branch'] = '分支'; +$lang->action->objectTypes['epic'] = $lang->ERCommon; $lang->action->objectTypes['story'] = $lang->SRCommon; $lang->action->objectTypes['requirement'] = $lang->URCommon; $lang->action->objectTypes['design'] = '设计'; @@ -199,6 +200,7 @@ $lang->action->desc->edited = '$date, 由 $actor $lang->action->desc->assigned = '$date, 由 $actor 指派给 $extra。' . "\n"; $lang->action->desc->closed = '$date, 由 $actor 关闭。' . "\n"; $lang->action->desc->closedbysystem = '$date, 由系统关闭。' . "\n"; +$lang->action->desc->closedbyparent = '$date, 由系统关闭。' . "\n"; $lang->action->desc->deleted = '$date, 由 $actor 删除。' . "\n"; $lang->action->desc->deletedfile = '$date, 由 $actor 删除了附件:$extra。' . "\n"; $lang->action->desc->editfile = '$date, 由 $actor 编辑了附件:$extra。' . "\n"; @@ -402,8 +404,8 @@ $lang->action->label->linkrelatedbug = "关联了相关Bug"; $lang->action->label->unlinkrelatedbug = "移除了相关Bug"; $lang->action->label->linkrelatedcase = "关联了相关用例"; $lang->action->label->unlinkrelatedcase = "移除了相关用例"; -$lang->action->label->linkrelatedstory = "关联了相关{$lang->SRCommon}"; -$lang->action->label->unlinkrelatedstory = "移除了相关{$lang->SRCommon}"; +$lang->action->label->linkrelatedstory = "关联了相关需求"; +$lang->action->label->unlinkrelatedstory = "移除了相关需求"; $lang->action->label->subdividestory = "细分了{$lang->SRCommon}"; $lang->action->label->unlinkchildstory = "移除了细分{$lang->SRCommon}"; $lang->action->label->started = '开始了'; @@ -442,6 +444,7 @@ $lang->action->label->unlinkchildrenstory = "取消关联了子需求"; $lang->action->label->linkparentstory = "关联到了父需求"; $lang->action->label->unlinkparentstory = "从父需求取消关联了"; $lang->action->label->deletechildrenstory = "删除了子需求"; +$lang->action->label->syncgrade = '同步了层级'; $lang->action->label->tracked = '跟踪了'; $lang->action->label->hangup = '挂起了'; $lang->action->label->run = '执行了'; diff --git a/module/action/model.php b/module/action/model.php index 4ab187b055..688eb43c20 100755 --- a/module/action/model.php +++ b/module/action/model.php @@ -197,16 +197,17 @@ class actionModel extends model if(($actionName == 'finished' && $objectType == 'todo') || ($actionName == 'closed' && in_array($action->objectType, array('story', 'demand'))) || ($actionName == 'resolved' && $action->objectType == 'bug')) $this->actionTao->processAppendLinkByExtra($action); if($actionName == 'distributed' && $objectType == 'story') $this->actionTao->processActionExtra(TABLE_DEMAND, $action, 'title', 'demand', 'view'); - if(in_array($actionName, array('retracted', 'restored')) && $action->objectType != 'demand') $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'view'); + if(in_array($actionName, array('retracted', 'restored')) && $action->objectType != 'demand') $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'storyView'); if(in_array($actionName, array('totask', 'linkchildtask', 'unlinkchildrentask', 'linkparenttask', 'unlinkparenttask', 'deletechildrentask', 'converttotask')) && $action->objectType != 'feedback') $this->actionTao->processActionExtra(TABLE_TASK, $action, 'name', 'task', 'view');; - if(in_array($actionName, array('linkchildstory', 'unlinkchildrenstory', 'linkparentstory', 'unlinkparentstory', 'deletechildrenstory'))) $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'view'); + if(in_array($actionName, array('linkchildstory', 'unlinkchildrenstory', 'linkparentstory', 'unlinkparentstory', 'deletechildrenstory'))) $this->actionTao->processActionExtra(TABLE_STORY, $action, 'title', 'story', 'storyView'); if(in_array($actionName, array('testtaskopened', 'testtaskstarted', 'testtaskclosed'))) $this->actionTao->processActionExtra(TABLE_TESTTASK, $action, 'name', 'testtask', 'view'); if(in_array($actionName, array('importfromstorylib', 'importfromrisklib', 'importfromissuelib', 'importfromopportunitylib')) && in_array($this->config->edition, array('max', 'ipd'))) $this->actionTao->processActionExtra(TABLE_ASSETLIB, $action, 'name', 'assetlib', $action->objectType); if(in_array($actionName, array('opened', 'managed', 'edited')) && in_array($objectType, array('execution', 'project'))) $this->processExecutionAndProjectActionExtra($action); - if(in_array($actionName, array('linkstory', 'unlinkstory', 'createchildrenstory', 'linkur', 'unlinkur'))) $this->actionTao->processLinkStoryAndBugActionExtra($action, 'story', 'view'); + if(in_array($actionName, array('linkstory', 'unlinkstory', 'createchildrenstory', 'linkur', 'unlinkur', 'linkrelatedstory', 'unlinkrelatedstory'))) $this->actionTao->processLinkStoryAndBugActionExtra($action, 'story', 'storyView'); if(in_array($actionName, array('linkbug', 'unlinkbug'))) $this->actionTao->processLinkStoryAndBugActionExtra($action, 'bug', 'view'); if($actionName == 'repocreated') $action->extra = str_replace("class='iframe'", 'data-app="devops"', $action->extra); if($actionName == 'createdsnapshot' && in_array($action->objectType, array('vm', 'zanode')) && $action->extra == 'defaultSnap') $action->actor = $this->lang->action->system; + if($actionName == 'syncgrade') $this->actionTao->processStoryGradeActionExtra($action); $action->history = zget($histories, $actionID, array()); foreach($action->history as $history) @@ -610,7 +611,19 @@ class actionModel extends model $extra = strtolower($action->extra); /* Fix bug #741. */ - if(isset($desc['extra'])) $desc['extra'] = $this->lang->{$objectType}->{$desc['extra']}; + if(isset($desc['extra'])) + { + if($objectType == 'story') + { + $story = $this->fetchByID($action->objectID, $objectType); + if($story->type != 'story') $this->app->loadLang($story->type); + $desc['extra'] = $this->lang->{$story->type}->{$desc['extra']}; + } + else + { + $desc['extra'] = $this->lang->{$objectType}->{$desc['extra']}; + } + } $actionDesc = ''; if(isset($desc['extra'][$extra])) @@ -956,7 +969,7 @@ class actionModel extends model /* 通过action获取对象名称,所属项目以及需求。 */ /* Get object names, object projects and requirements by actions. */ - list($objectNames, $relatedProjects, $requirements) = $this->getRelatedDataByActions($actions); + list($objectNames, $relatedProjects, $requirements, $epics) = $this->getRelatedDataByActions($actions); $projectIdList = array(); foreach($relatedProjects as $objectType => $idList) $projectIdList = array_merge($projectIdList, $idList); @@ -998,7 +1011,7 @@ class actionModel extends model $action->date = date(DT_MONTHTIME2, strtotime($action->date)); $action->actionLabel = isset($this->lang->{$objectType}->{$actionType}) ? $this->lang->{$objectType}->{$actionType} : $action->action; $action->actionLabel = isset($this->lang->action->label->{$actionType}) ? $this->lang->action->label->{$actionType} : $action->actionLabel; - $action->objectLabel = $this->getObjectLabel($objectType, $action->objectID, $actionType, $requirements); + $action->objectLabel = $this->getObjectLabel($objectType, $action->objectID, $actionType, $requirements, $epics); $action->major = isset($this->config->action->majorList[$action->objectType]) && in_array($action->action, $this->config->action->majorList[$action->objectType]) ? 1 : 0; if($actionType == 'svncommited' || $actionType == 'gitcommited') $action->actor = zget($commiters, $action->actor); @@ -1028,7 +1041,7 @@ class actionModel extends model if(isset($objectTypes['branch'])) $this->app->loadLang('branch'); $users = isset($objectTypes['gapanalysis']) || isset($objectTypes['stakeholder']) ? $this->loadModel('user')->getPairs('noletter') : array(); - $objectNames = $relatedProjects = $requirements = array(); + $objectNames = $relatedProjects = $requirements = $epics = array(); foreach($objectTypes as $objectType => $objectIdList) { if(!isset($this->config->objectTables[$objectType]) && $objectType != 'makeup') continue; // If no defination for this type, omit it. @@ -1039,7 +1052,7 @@ class actionModel extends model if(empty($field)) continue; /* Get object name, related projects, requirements. */ - list($objectName, $relatedProject, $requirements) = $this->getObjectRelatedData($table, $objectType, $objectIdList, $field, $users, $requirements); + list($objectName, $relatedProject, $requirements, $epics) = $this->getObjectRelatedData($table, $objectType, $objectIdList, $field, $users, $requirements, $epics); if($objectType == 'branch' && in_array(BRANCH_MAIN, $objectIdList)) $objectName[BRANCH_MAIN] = $this->lang->branch->main; $objectNames[$objectType] = $objectName; @@ -1048,7 +1061,7 @@ class actionModel extends model $objectNames['user'][0] = 'guest'; // Add guest account. - return array($objectNames, $relatedProjects, $requirements); + return array($objectNames, $relatedProjects, $requirements, $epics); } /** @@ -1059,10 +1072,11 @@ class actionModel extends model * @param int $objectID * @param string $actionType * @param array $requirements + * @param array $epics * @access public * @return string */ - public function getObjectLabel(string $objectType, int $objectID, string $actionType, array $requirements): string + public function getObjectLabel(string $objectType, int $objectID, string $actionType, array $requirements, array $epics): string { $actionObjectLabel = $objectType; if(isset($this->lang->action->label->{$objectType})) @@ -1072,6 +1086,7 @@ class actionModel extends model /* 用户故事替换为需求。 */ /* Replace story to requirement. */ if(isset($requirements[$objectID]) && is_string($objectLabel)) $objectLabel = str_replace($this->lang->SRCommon, $this->lang->URCommon, $objectLabel); + if(isset($epics[$objectID]) && is_string($objectLabel)) $objectLabel = str_replace($this->lang->SRCommon, $this->lang->ERCommon, $objectLabel); if(!is_array($objectLabel)) $actionObjectLabel = $objectLabel; if(is_array($objectLabel) && isset($objectLabel[$actionType])) $actionObjectLabel = $objectLabel[$actionType]; @@ -1843,10 +1858,11 @@ class actionModel extends model * @param string $field * @param array $users * @param array $requirements + * @param array $epics * @access public * @return array */ - public function getObjectRelatedData(string $table, string $objectType, array $objectIdList, string $field, array $users, array $requirements): array + public function getObjectRelatedData(string $table, string $objectType, array $objectIdList, string $field, array $users, array $requirements, array $epics): array { $objectName = array(); $relatedProject = array(); @@ -1878,7 +1894,8 @@ class actionModel extends model { $objectName[$object->id] = $object->title; if($object->type == 'requirement') $requirements[$object->id] = $object->id; - if($object->type == 'project') $relatedProject[$object->id] = $object->id; + if($object->type == 'epic') $epics[$object->id] = $object->id; + if($object->type == 'project') $relatedProject[$object->id] = $object->id; } } elseif($objectType == 'stakeholder') // Get stakeholder realname. @@ -1890,7 +1907,7 @@ class actionModel extends model { $objectName = $this->dao->select("id, {$field} AS name")->from($table)->where('id')->in($objectIdList)->orderBy('id_asc')->fetchPairs(); } - return array($objectName, $relatedProject, $requirements); + return array($objectName, $relatedProject, $requirements, $epics); } /** diff --git a/module/action/tao.php b/module/action/tao.php index cd3d28a496..8b09d3d7b6 100644 --- a/module/action/tao.php +++ b/module/action/tao.php @@ -449,6 +449,24 @@ class actionTao extends actionModel } } + /** + * 处理需求层级。 + * Process story grade. + * + * @param object $action + * @access public + * @return object + */ + public function processStoryGradeActionExtra($action): object + { + $action->extra = $this->dao->select('t1.name')->from(TABLE_STORYGRADE)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.grade = t2.grade AND t1.type = t2.type') + ->where('t2.id')->eq($action->objectID) + ->fetch('name'); + + return $action; + } + /** * 处理属性。 * Process attribute. @@ -488,6 +506,9 @@ class actionTao extends actionModel break; case 'execution': case 'kanban': + case 'story': + $paramString = "{$type}ID={$action->extra}"; + break; case 'task': case 'story': case 'charter': @@ -593,7 +614,7 @@ class actionTao extends actionModel public function processLinkStoryAndBugActionExtra(object $action, string $module, string $method): void { $extra = ''; - foreach(explode(',', $action->extra) as $id) $extra .= common::hasPriv($module, $method) ? html::a(helper::createLink($module, $method, "{$module}ID={$id}"), "#{$id} ", '', "data-size='lg' data-load='modal'") . ', ' : "#{$id}, "; + foreach(explode(',', $action->extra) as $id) $extra .= common::hasPriv($module, $method) ? html::a(helper::createLink($module, $method, "{$module}ID={$id}"), "#{$id} ", '', "data-size='lg' data-toggle='modal'") . ', ' : "#{$id}, "; $action->extra = trim(trim($extra), ','); } diff --git a/module/admin/config.php b/module/admin/config.php index b54d31afc2..62e4891b07 100755 --- a/module/admin/config.php +++ b/module/admin/config.php @@ -20,8 +20,8 @@ $config->admin->menuGroup['ai'] = array('ai|adminindex', 'ai|prompts', 'a $config->admin->menuModuleGroup['model']['custom|set'] = array('project', 'issue', 'risk', 'opportunity', 'nc'); $config->admin->menuModuleGroup['model']['custom|required'] = array('project', 'build'); -$config->admin->menuModuleGroup['feature']['custom|set'] = array('todo', 'block', 'story', 'task', 'bug', 'testcase', 'testtask', 'feedback', 'user', 'ticket'); -$config->admin->menuModuleGroup['feature']['custom|required'] = array('bug', 'doc', 'product', 'story', 'productplan', 'release', 'task', 'testcase', 'testsuite', 'testtask', 'testreport', 'caselib', 'doc', 'feedback', 'user', 'execution'); +$config->admin->menuModuleGroup['feature']['custom|set'] = array('todo', 'block', 'epic', 'requirement', 'story', 'task', 'bug', 'testcase', 'testtask', 'feedback', 'user', 'ticket'); +$config->admin->menuModuleGroup['feature']['custom|required'] = array('bug', 'doc', 'product', 'epic', 'requirement', 'story', 'productplan', 'release', 'task', 'testcase', 'testsuite', 'testtask', 'testreport', 'caselib', 'doc', 'feedback', 'user', 'execution'); $config->admin->menuModuleGroup['template']['custom|set'] = array('baseline'); if($config->vision == 'lite') { @@ -90,7 +90,7 @@ $config->admin->helpURL['platform'] = 'https://www.zentao.net/book/devops/1072. $config->admin->helpURL['ai'] = 'https://www.zentao.net/book/zentaopms/1097.html'; $config->admin->navsGroup['feature']['my'] = ',todo,block,'; -$config->admin->navsGroup['feature']['product'] = ',product,story,productplan,release,'; +$config->admin->navsGroup['feature']['product'] = ',product,epic,requirement,story,productplan,release,'; $config->admin->navsGroup['feature']['project'] = ',project,'; $config->admin->navsGroup['feature']['execution'] = ',execution,task,'; $config->admin->navsGroup['feature']['project'] = ',project,story,'; diff --git a/module/admin/control.php b/module/admin/control.php index 862bc40c53..ff319be4ac 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -262,9 +262,14 @@ class admin extends control } } + $enableER = $this->config->edition == 'ipd' ? 1 : zget($data->module, 'productER', 0); + $URAndSR = $this->config->edition == 'ipd' ? 1 : zget($data->module, 'productUR', 0); + if($enableER && !$URAndSR) $this->send(array('result' => 'fail', 'message' => $this->lang->admin->notice->openUR)); + $this->setting->setItem('system.common.closedFeatures', implode(',', $closedFeatures)); $this->setting->setItem('system.common.global.scoreStatus', zget($data->module, 'myScore', 0)); - $this->setting->setItem('system.custom.URAndSR', $this->config->edition == 'ipd' ? 1 : zget($data->module, 'productUR', 0)); + $this->setting->setItem('system.custom.enableER', $enableER); + $this->setting->setItem('system.custom.URAndSR', $URAndSR); $this->setting->setItem('system.common.setCode', in_array('otherSetCode', $closedFeatures) ? 0 : 1); $this->loadModel('custom')->processMeasrecordCron(); $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'callback' => '$.apps.updateAppsMenu(true);loadCurrentPage();')); diff --git a/module/admin/lang/de.php b/module/admin/lang/de.php index 4f18048322..7a020634d4 100755 --- a/module/admin/lang/de.php +++ b/module/admin/lang/de.php @@ -72,6 +72,7 @@ $lang->admin->notice = new stdclass(); $lang->admin->notice->register = "Hinweiß: Sie haben sich nicht bei ZenTao (www.zentao.pm) registriert. %s um die aktuellen Updates und News zu erhalten."; $lang->admin->notice->ignore = "Ignorieren"; $lang->admin->notice->int = "『%s』sollte eine positive Zahl sein."; +$lang->admin->notice->openUR = "Please enable {$lang->URCommon} first before you can use {$lang->ERCommon}."; $lang->admin->registerNotice = new stdclass(); $lang->admin->registerNotice->common = 'Register Account'; @@ -114,6 +115,7 @@ $lang->admin->setModule->auditplan = 'QA'; $lang->admin->setModule->meeting = 'Meeting'; $lang->admin->setModule->roadmap = 'Roadmap'; $lang->admin->setModule->track = 'Track'; +$lang->admin->setModule->ER = $lang->ERCommon; $lang->admin->setModule->UR = $lang->URCommon; $lang->admin->setModule->researchplan = 'Researchplan'; $lang->admin->setModule->gapanalysis = 'Gapanalysis'; diff --git a/module/admin/lang/en.php b/module/admin/lang/en.php index 7ad1bf9db6..ab94a8601c 100755 --- a/module/admin/lang/en.php +++ b/module/admin/lang/en.php @@ -72,6 +72,7 @@ $lang->admin->notice = new stdclass(); $lang->admin->notice->register = "Note: You haven't registered in ZenTao official website(www.zentao.pm). %s then get the Latest ZenTao Upgrades and News."; $lang->admin->notice->ignore = "Ignore"; $lang->admin->notice->int = "『%s』should be a positive integer."; +$lang->admin->notice->openUR = "Please enable {$lang->URCommon} first before you can use {$lang->ERCommon}."; $lang->admin->registerNotice = new stdclass(); $lang->admin->registerNotice->common = 'Register Account'; @@ -114,6 +115,7 @@ $lang->admin->setModule->auditplan = 'QA'; $lang->admin->setModule->meeting = 'Meeting'; $lang->admin->setModule->roadmap = 'Roadmap'; $lang->admin->setModule->track = 'Track'; +$lang->admin->setModule->ER = $lang->ERCommon; $lang->admin->setModule->UR = $lang->URCommon; $lang->admin->setModule->researchplan = 'Researchplan'; $lang->admin->setModule->gapanalysis = 'Gapanalysis'; diff --git a/module/admin/lang/fr.php b/module/admin/lang/fr.php index 8699014efd..66d0c726f4 100755 --- a/module/admin/lang/fr.php +++ b/module/admin/lang/fr.php @@ -72,6 +72,7 @@ $lang->admin->notice = new stdclass(); $lang->admin->notice->register = "Note: Vous n'êtes pas enregistré sur le site officiel de ZenTao (www.zentao.pm). %s vous pourrez ensuite obtenir les dernières Mises à Jour de ZenTao et des informations."; $lang->admin->notice->ignore = "Ignorer"; $lang->admin->notice->int = "『 %s 』 devrait être un entier positif."; +$lang->admin->notice->openUR = "Please enable {$lang->URCommon} first before you can use {$lang->ERCommon}."; $lang->admin->registerNotice = new stdclass(); $lang->admin->registerNotice->common = 'Register Account'; @@ -114,6 +115,7 @@ $lang->admin->setModule->auditplan = 'QA'; $lang->admin->setModule->meeting = 'Meeting'; $lang->admin->setModule->roadmap = 'Roadmap'; $lang->admin->setModule->track = 'Track'; +$lang->admin->setModule->ER = $lang->ERCommon; $lang->admin->setModule->UR = $lang->URCommon; $lang->admin->setModule->researchplan = 'Researchplan'; $lang->admin->setModule->gapanalysis = 'Gapanalysis'; diff --git a/module/admin/lang/menu.php b/module/admin/lang/menu.php index 168ef3781c..7934eb42e6 100644 --- a/module/admin/lang/menu.php +++ b/module/admin/lang/menu.php @@ -116,6 +116,8 @@ $lang->admin->menuList->feature['tabMenu']['my']['todo'] = array('link' => "{$l $lang->admin->menuList->feature['tabMenu']['my']['block'] = array('link' => "{$lang->block->common}|custom|set|module=block&field=closed", 'exclude' => 'custom-set'); $lang->admin->menuList->feature['tabMenu']['product']['product'] = array('link' => "{$lang->productCommon}|custom|required|module=product", 'links' => array("custom|browsestoryconcept|", 'custom|product|'), 'alias' => 'browsestoryconcept,product', 'exclude' => 'custom-required'); +if($config->enableER) $lang->admin->menuList->feature['tabMenu']['product']['epic'] = array('link' => "{$lang->epic->common}|custom|required|module=epic", 'links' => array("custom|set|module=epic&field=categoryList"), 'exclude' => 'custom-required,custom-set'); +if($config->URAndSR) $lang->admin->menuList->feature['tabMenu']['product']['requirement'] = array('link' => "{$lang->URCommon}|custom|required|module=requirement", 'links' => array("custom|set|module=requirement&field=categoryList"), 'exclude' => 'custom-required,custom-set'); $lang->admin->menuList->feature['tabMenu']['product']['story'] = array('link' => "{$lang->SRCommon}|custom|required|module=story", 'links' => array("custom|set|module=story&field=categoryList"), 'exclude' => 'custom-required,custom-set'); $lang->admin->menuList->feature['tabMenu']['product']['productplan'] = array('link' => "{$lang->productplan->shortCommon}|custom|required|module=productplan", 'exclude' => 'custom-required'); $lang->admin->menuList->feature['tabMenu']['product']['release'] = array('link' => "{$lang->release->common}|custom|required|module=release", 'exclude' => 'custom-required'); @@ -133,9 +135,11 @@ $lang->admin->menuList->feature['tabMenu']['qa']['caselib'] = array('link' => $lang->admin->menuList->feature['tabMenu']['menuOrder']['my']['5'] = 'todo'; $lang->admin->menuList->feature['tabMenu']['menuOrder']['my']['10'] = 'block'; $lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['5'] = 'product'; -$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['10'] = 'story'; -$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['15'] = 'productplan'; -$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['20'] = 'release'; +if($config->enableER) $lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['10'] = 'epic'; +if($config->URAndSR) $lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['15'] = 'requirement'; +$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['20'] = 'story'; +$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['25'] = 'productplan'; +$lang->admin->menuList->feature['tabMenu']['menuOrder']['product']['30'] = 'release'; $lang->admin->menuList->feature['tabMenu']['menuOrder']['execution']['5'] = 'execution'; $lang->admin->menuList->feature['tabMenu']['menuOrder']['execution']['10'] = 'task'; $lang->admin->menuList->feature['tabMenu']['menuOrder']['qa']['5'] = 'bug'; diff --git a/module/admin/lang/zh-cn.php b/module/admin/lang/zh-cn.php index a140ac2274..68efcf920e 100755 --- a/module/admin/lang/zh-cn.php +++ b/module/admin/lang/zh-cn.php @@ -72,6 +72,7 @@ $lang->admin->notice = new stdclass(); $lang->admin->notice->register = "可%s禅道社区 www.zentao.net,及时获得禅道最新信息。"; $lang->admin->notice->ignore = "不再提示"; $lang->admin->notice->int = "『%s』应当是正整数。"; +$lang->admin->notice->openUR = "请先开启{$lang->URCommon},才能使用{$lang->ERCommon}。"; $lang->admin->registerNotice = new stdclass(); $lang->admin->registerNotice->common = '注册新帐号'; @@ -115,6 +116,7 @@ $lang->admin->setModule->meeting = '会议'; $lang->admin->setModule->roadmap = '路线图'; $lang->admin->setModule->track = '矩阵'; $lang->admin->setModule->UR = $lang->URCommon; +$lang->admin->setModule->ER = $lang->ERCommon; $lang->admin->setModule->researchplan = '调研'; $lang->admin->setModule->gapanalysis = '培训'; $lang->admin->setModule->storylib = '需求库'; diff --git a/module/backup/test/model/getbackupdirprogress.php b/module/backup/test/model/getbackupdirprogress.php index 0dba7da9e7..e8fbce32c1 100644 --- a/module/backup/test/model/getbackupdirprogress.php +++ b/module/backup/test/model/getbackupdirprogress.php @@ -15,7 +15,7 @@ global $tester; $backupModel = $tester->loadModel('backup'); $backupName = dirname(__FILE__) . '/test'; -r($backupModel->getBackupDirProgress($backupName)) && p() && e('0'); +r($backupModel->getBackupDirProgress($backupName)) && p('allCount') && e('0'); $summaryFile = $backupModel->getTmpLogFile($backupName); file_put_contents($summaryFile, json_encode(array('size' => '1234'))); diff --git a/module/bi/config/metrics.php b/module/bi/config/metrics.php index 067b9456e0..c0994649a8 100644 --- a/module/bi/config/metrics.php +++ b/module/bi/config/metrics.php @@ -4144,3 +4144,101 @@ $config->bi->builtin->metrics[] = array 'desc' => '按项目统计的已完成用户需求数是指状态为已关闭且关闭原因为已完成的用户需求的数量。反映了项目团队在满足用户期望和需求方面的已经实现的任务和计划。已完成用户需求数量的增加表示项目团队已经成功完成了一定数量的用户需求工作,并取得了一定的成果', 'definition' => "项目中用户需求的个数求和\n状态为已关闭\n关闭原因为已完成\n过滤已删除的用户需求\n过滤已删除的项目" ); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按人员统计的被指派的业务需求数', + 'alias' => '被指派的业务需求数', + 'code' => 'count_of_assigned_epic_in_user', + 'purpose' => 'scale', + 'scope' => 'user', + 'object' => 'epic', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按人员统计的被指派业务需求数表示每个人待处理的业务需求数量之和。反映了每个人员需要处理的业务需求数量的规模。该数值越大,说明需要投入越多的时间处理业务需求。', + 'definition' => "所有业务需求个数求和\n指派给为某人\n过滤已删除的业务需求\n过滤已删除产品的业务需求" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按人员统计的被指派的用户需求数', + 'alias' => '被指派的用户需求数', + 'code' => 'count_of_assigned_requirement_in_user', + 'purpose' => 'scale', + 'scope' => 'user', + 'object' => 'requirement', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按人员统计的被指派用户需求数表示每个人待处理的用户需求数量之和。反映了每个人员需要处理的用户需求数量的规模。该数值越大,说明需要投入越多的时间处理用户需求。', + 'definition' => "所有用户需求个数求和\n指派给为某人\n过滤已删除的用户需求\n过滤已删除产品的用户需求" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按项目统计的业务需求总数', + 'alias' => '业务需求总数', + 'code' => 'count_of_epic_in_project', + 'purpose' => 'scale', + 'scope' => 'project', + 'object' => 'epic', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按项目统计的业务需求总数是指项目中创建或关联的所有业务需求的数量,反映了项目的规模和复杂度,提供了关于业务需求管理、进度控制、资源规划、风险评估和质量控制的有用信息', + 'definition' => "项目中业务需求个数求和\r\n过滤已删除的业务需求\r\n过滤已删除的项目" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按项目统计的已关闭业务需求数', + 'alias' => '已关闭业务需求数', + 'code' => 'count_of_closed_epic_in_project', + 'purpose' => 'scale', + 'scope' => 'project', + 'object' => 'epic', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按项目统计的已关闭业务需求数是指项目中状态为已关闭的业务需求的数量,反映了项目团队在满足组织业务目标和需求方面的已经实现的任务和计划。已关闭业务需求数量的增加表示项目团队已经成功完成了一定数量的业务需求工作,并取得了一定的成果。', + 'definition' => "项目中业务需求个数求和\r\n过滤已删除的业务需求\r\n状态为已关闭\r\n过滤已删除的项目" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按项目统计的未关闭业务需求数', + 'alias' => '未关闭业务需求数', + 'code' => 'count_of_unclosed_epic_in_project', + 'purpose' => 'scale', + 'scope' => 'project', + 'object' => 'epic', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按项目统计的未关闭业务需求数是指项目中尚未满足或处理的业务需求的数量,反映了项目团队在满足组织业务目标和需求方面的进行中任务和计划。未关闭业务需求数量的增加表示项目团队尚未完成的业务需求工作较多,需要进一步跟进和处理,以确保项目能够满足组织的业务目标', + 'definition' => "复用:\r\n按项目统计的业务需求总数\r\n按项目统计的已关闭业务需求数\r\n公式:\r\n按项目统计的未关闭业务需求数=按项目统计的业务需求总数-按项目统计的已关闭业务需求数" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按项目统计的已完成业务需求数', + 'alias' => '已完成业务需求数', + 'code' => 'count_of_finished_epic_in_project', + 'purpose' => 'scale', + 'scope' => 'project', + 'object' => 'epic', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按项目统计的已完成业务需求数是指状态为已关闭且关闭原因为已完成的业务需求的数量。反映了项目团队在满足组织业务目标和需求方面的已经实现的任务和计划。已完成业务需求数量的增加表示项目团队已经成功完成了一定数量的业务需求工作,并取得了一定的成果', + 'definition' => "项目中业务需求的个数求和\r\n状态为已关闭\r\n关闭原因为已完成\r\n过滤已删除的业务需求\r\n过滤已删除的项目" +); + +$config->bi->builtin->metrics[] = array +( + 'name' => '按人员统计的被指派的QA数', + 'alias' => '被指派的QA数', + 'code' => 'count_of_assigned_qa_in_user', + 'purpose' => 'scale', + 'scope' => 'user', + 'object' => 'qa', + 'unit' => 'count', + 'dateType' => 'nodate', + 'desc' => '按人员统计的被指派的QA数表示每个人被指派的质量保证问题之和。反映了每个人员需要处理的质量保证问题的规模。该数值越大,说明需要处理的质量保证问题越多', + 'definition' => "所有待处理的QA个数求和(包含:待处理质量保证计划、待处理不符合项)\n指派给为某人\n质量保证计划状态为待检查、不符合项状态为待解决\n过滤已删除的质量保证计划和不符合项\n过滤已删除项目的质量保证计划和不符合项" +); diff --git a/module/block/lang/de.php b/module/block/lang/de.php index d076f75c59..3954937fa9 100644 --- a/module/block/lang/de.php +++ b/module/block/lang/de.php @@ -543,7 +543,8 @@ $lang->block->welcome->assignList['task'] = 'Task'; if($config->vision != 'or') $lang->block->welcome->assignList['bug'] = 'Bug'; if($config->vision != 'or') $lang->block->welcome->assignList['story'] = 'SRStroy'; $lang->block->welcome->assignList['testcase'] = 'TestCase'; -if($config->vision != 'or') $lang->block->welcome->assignList['requirement'] = 'URStroy'; +if($config->URAndSR && $config->vision != 'or') $lang->block->welcome->assignList['requirement'] = "{$lang->URCommon}"; +if($config->enableER && $config->vision != 'or') $lang->block->welcome->assignList['epic'] = "{$lang->ERCommon}"; $lang->block->customModeTip = new stdClass(); $lang->block->customModeTip->common = 'There are 2 running modes of ZenTao: Light Mode and ALM Mode.'; diff --git a/module/block/lang/en.php b/module/block/lang/en.php index 8d7e3f2466..eb5c2552d6 100644 --- a/module/block/lang/en.php +++ b/module/block/lang/en.php @@ -543,7 +543,8 @@ $lang->block->welcome->assignList['task'] = 'Task'; if($config->vision != 'or') $lang->block->welcome->assignList['bug'] = 'Bug'; if($config->vision != 'or') $lang->block->welcome->assignList['story'] = 'SRStroy'; $lang->block->welcome->assignList['testcase'] = 'TestCase'; -if($config->vision != 'or') $lang->block->welcome->assignList['requirement'] = 'URStroy'; +if($config->URAndSR && $config->vision != 'or') $lang->block->welcome->assignList['requirement'] = "{$lang->URCommon}"; +if($config->enableER && $config->vision != 'or') $lang->block->welcome->assignList['epic'] = "{$lang->ERCommon}"; $lang->block->customModeTip = new stdClass(); $lang->block->customModeTip->common = 'There are 2 running modes of ZenTao: Light Mode and ALM Mode.'; diff --git a/module/block/lang/fr.php b/module/block/lang/fr.php index 4fdb9a6bc5..8866efd2b7 100644 --- a/module/block/lang/fr.php +++ b/module/block/lang/fr.php @@ -543,7 +543,8 @@ $lang->block->welcome->assignList['task'] = 'Task'; if($config->vision != 'or') $lang->block->welcome->assignList['bug'] = 'Bug'; if($config->vision != 'or') $lang->block->welcome->assignList['story'] = 'SRStroy'; $lang->block->welcome->assignList['testcase'] = 'TestCase'; -if($config->vision != 'or') $lang->block->welcome->assignList['requirement'] = 'URStroy'; +if($config->URAndSR && $config->vision != 'or') $lang->block->welcome->assignList['requirement'] = "{$lang->URCommon}"; +if($config->enableER && $config->vision != 'or') $lang->block->welcome->assignList['epic'] = "{$lang->ERCommon}"; $lang->block->customModeTip = new stdClass(); $lang->block->customModeTip->common = 'There are 2 running modes of ZenTao: Light Mode and ALM Mode.'; diff --git a/module/block/lang/zh-cn.php b/module/block/lang/zh-cn.php index 9e802e52a2..557127a852 100644 --- a/module/block/lang/zh-cn.php +++ b/module/block/lang/zh-cn.php @@ -541,9 +541,10 @@ $lang->block->welcome->reviewList['reviewByMe'] = '待我评审数'; $lang->block->welcome->assignList = array(); $lang->block->welcome->assignList['task'] = '任务数'; if($config->vision != 'or') $lang->block->welcome->assignList['bug'] = 'BUG数'; -if($config->vision != 'or') $lang->block->welcome->assignList['story'] = $lang->SRCommon . '数'; +if($config->vision != 'or') $lang->block->welcome->assignList['story'] = "{$lang->SRCommon}数"; $lang->block->welcome->assignList['testcase'] = '用例数'; -if($config->vision != 'or') $lang->block->welcome->assignList['requirement'] = $lang->URCommon . '数'; +if($config->URAndSR && $config->vision != 'or') $lang->block->welcome->assignList['requirement'] = "{$lang->URCommon}数"; +if($config->enableER && $config->vision != 'or') $lang->block->welcome->assignList['epic'] = "{$lang->ERCommon}数"; $lang->block->customModeTip = new stdClass(); $lang->block->customModeTip->common = '禅道运行模式分为【轻量级管理模式】和【全生命周期管理模式】。'; @@ -571,7 +572,7 @@ $lang->block->projectoverview->thisYear = '今年完成'; $lang->block->projectoverview->lastThreeYear = '近三年完成的项目数量分布'; $lang->block->projectstatistic = new stdclass(); -$lang->block->projectstatistic->story = $lang->SRCommon; +$lang->block->projectstatistic->story = '需求'; $lang->block->projectstatistic->cost = '投入'; $lang->block->projectstatistic->task = '任务'; $lang->block->projectstatistic->bug = 'Bug'; diff --git a/module/bug/ui/view.html.php b/module/bug/ui/view.html.php index 51c0dbcea3..40f960834d 100644 --- a/module/bug/ui/view.html.php +++ b/module/bug/ui/view.html.php @@ -86,6 +86,12 @@ if(!$bug->deleted) } $actions[$key] = $action; } + if($action['icon'] == 'edit' && isInModal()) + { + $action['data-load'] = 'modal'; + $action['data-size'] = 'lg'; + $actions[$key] = $action; + } } } diff --git a/module/bug/zen.php b/module/bug/zen.php index f9aba39ce2..2cdb63270b 100644 --- a/module/bug/zen.php +++ b/module/bug/zen.php @@ -1021,7 +1021,7 @@ class bugZen extends bug if($executionID) { - $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch); + $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch, '', 'full', 'all', 'story', false); } else { @@ -1033,6 +1033,8 @@ class bugZen extends bug $bugStory ? $stories[$bug->storyID] = $bugStory->title : $bug->storyID = 0; } + $stories = $this->story->addGradeLabel($stories); + return $this->updateBug($bug, array('stories' => $stories)); } @@ -1237,13 +1239,22 @@ class bugZen extends bug $this->config->moreLinks['case'] = inlink('ajaxGetProductCases', "bugID={$bug->id}"); + if($bug->execution) + { + $stories = $this->story->getExecutionStoryPairs($bug->execution, 0, 'all', '', 'full', 'all', 'story', false); + } + else + { + $stories = $this->story->getProductStoryPairs($bug->product, $bug->branch, 0, 'active,closed', 'id_desc', 0, 'full', 'story', false); + } + $resolvedBuildPairs = $this->build->getBuildPairs(array($bug->product), $bug->branch, 'noempty'); $this->view->resolvedBuildPairs = $resolvedBuildPairs; $this->view->resolvedBuilds = $this->build->addReleaseLabelForBuilds($bug->product, $resolvedBuildPairs); $this->view->openedBuilds = $openedBuilds; $this->view->plans = $this->loadModel('productplan')->getPairs($bug->product, $bug->branch, '', true); - $this->view->stories = $bug->execution ? $this->story->getExecutionStoryPairs($bug->execution) : $this->story->getProductStoryPairs($bug->product, $bug->branch, 0, 'all', 'id_desc', 0, 'full', 'story', false); + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->tasks = $this->task->getExecutionTaskPairs($bug->execution); $this->view->testtasks = $this->loadModel('testtask')->getPairs($bug->product, $bug->execution, $bug->testtask); $this->view->cases = $cases; diff --git a/module/build/control.php b/module/build/control.php index abb6a2ffa1..f7164e3ef9 100644 --- a/module/build/control.php +++ b/module/build/control.php @@ -185,6 +185,8 @@ class build extends control $this->view->actions = $this->loadModel('action')->getList('build', $buildID); $this->view->link = $link; $this->view->orderBy = $orderBy; + $this->view->grades = $this->loadModel('story')->getGradePairs('story', 'all'); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->execution = $this->loadModel('execution')->getByID((int)$build->execution); $this->view->childBuilds = empty($build->builds) ? array() : $this->build->getByList(explode(',', $build->builds)); $this->view->productID = $build->product; @@ -436,13 +438,14 @@ class build extends control $this->loadModel('story'); $executionID = $build->execution ? (int)$build->execution : (int)$build->project; + $excludeStoryIdList = $this->buildZen->getExcludeStoryIdList($build); if($browseType == 'bySearch') { - $allStories = $this->story->getBySearch($build->product, $build->branch, (int)$param, $orderBy, $executionID, 'story', $build->allStories, '', $pager); + $allStories = $this->story->getBySearch($build->product, $build->branch, (int)$param, $orderBy, $executionID, 'story', $excludeStoryIdList, '', $pager); } else { - $allStories = $this->story->getExecutionStories($executionID, $build->product, $orderBy, 'byBranch', $build->branch, 'story', $build->allStories, $pager); + $allStories = $this->story->getExecutionStories($executionID, $build->product, $orderBy, 'byBranch', $build->branch, 'story', $excludeStoryIdList, $pager); } $checkedRows = array(); @@ -459,6 +462,8 @@ class build extends control $this->view->browseType = $browseType; $this->view->param = $param; $this->view->pager = $pager; + $this->view->grades = $this->story->getGradePairs('story', 'all'); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->orderBy = $orderBy; $this->display(); } diff --git a/module/build/js/linkstory.ui.js b/module/build/js/linkstory.ui.js index be1a0f0466..92ab722fc3 100644 --- a/module/build/js/linkstory.ui.js +++ b/module/build/js/linkstory.ui.js @@ -4,7 +4,8 @@ window.renderStoryCell = function(result, info) if(info.col.name == 'title' && result) { let html = ''; - if(story.parent > 0) html += "" + childrenAB + ""; + let gradeLabel = (showGrade || story.grade >= 2) ? grades[story.grade] : ''; + if(gradeLabel) html += "" + gradeLabel + ""; if(html) result.unshift({html}); } diff --git a/module/build/js/view.ui.js b/module/build/js/view.ui.js index e9e0b31cb0..4dd2a1f45c 100644 --- a/module/build/js/view.ui.js +++ b/module/build/js/view.ui.js @@ -77,3 +77,17 @@ $(function() { if(initLink == 'true') window.showLink(type, linkParams); }); + +window.renderStoryCell = function(result, info) +{ + const story = info.row.data; + if(info.col.name == 'title' && result) + { + let html = ''; + let gradeLabel = (showGrade || story.grade >= 2) ? grades[story.grade] : ''; + if(gradeLabel) html += "" + gradeLabel + ""; + if(html) result.unshift({html}); + } + + return result; +}; diff --git a/module/build/ui/linkstory.html.php b/module/build/ui/linkstory.html.php index da2ec298c5..1d38ccb2a2 100644 --- a/module/build/ui/linkstory.html.php +++ b/module/build/ui/linkstory.html.php @@ -27,6 +27,8 @@ foreach($allStories as $story) } jsVar('childrenAB', $lang->story->childrenAB); +jsVar('grades', $grades); +jsVar('showGrade', $showGrade); searchForm ( diff --git a/module/build/ui/view.html.php b/module/build/ui/view.html.php index 38ac5bdd44..e77deaef3b 100644 --- a/module/build/ui/view.html.php +++ b/module/build/ui/view.html.php @@ -81,6 +81,8 @@ jsVar('confirmDelete', $lang->build->confirmDelete); jsVar('currentAccount', $app->user->account); jsVar('buildProduct', $build->product); jsVar('buildModule', $buildModule); +jsVar('grades', $grades); +jsVar('showGrade', $showGrade); /* Story's batch btn. */ $canBatchUnlinkStory = $canBeChanged && common::hasPriv($buildModule, 'batchUnlinkStory'); @@ -184,6 +186,7 @@ detailBody set::canRowCheckable(jsRaw("function(rowID){return this.getRowInfo(rowID).data.noCheckBox ? 'disabled' : true;}")), set::sortLink(createLink($buildModule, 'view', "buildID={$build->id}&type=story&link={$link}¶m={$param}&orderBy={name}_{sortType}")), set::orderBy($orderBy), + set::onRenderCell(jsRaw('window.renderStoryCell')), set::extraHeight('+144'), set::footToolbar($storyFootToolbar), set::footPager(usePager('storyPager', '', array( diff --git a/module/build/zen.php b/module/build/zen.php index 9002cd5d5e..ea8a8c06ad 100644 --- a/module/build/zen.php +++ b/module/build/zen.php @@ -228,6 +228,31 @@ class buildZen extends build $this->view->objectID = $objectID; } + /** + * 获取版本不可关联的需求ID列表。 + * Get the story ID list that cannot be linked to the build. + * + * @param object $build + * @access protected + * @return array + */ + public function getExcludeStoryIdList(object $build) + { + $parentIdList = $this->dao->select('id')->from(TABLE_STORY) + ->where('product')->eq($build->product) + ->andWhere('type')->eq('story') + ->andWhere('isParent')->eq('1') + ->fetchPairs(); + + foreach(explode(',', $build->allStories) as $storyID) + { + if(!$storyID) continue; + if(!isset($parentIdList[$storyID])) $parentIdList[$storyID] = $storyID; + } + + return $parentIdList; + } + /** * 生成关联需求的搜索表单数据。 * Generate the search form data for the build view page. diff --git a/module/cne/test/lib/cne.unittest.class.php b/module/cne/test/lib/cne.unittest.class.php index 1817b754b0..3418309db7 100755 --- a/module/cne/test/lib/cne.unittest.class.php +++ b/module/cne/test/lib/cne.unittest.class.php @@ -18,9 +18,9 @@ class cneTest su('admin'); global $tester, $config; - $config->CNE->api->host = 'http://10.0.7.210:32380'; - $config->CNE->api->token = 'JMz7HCoQ3WHoYbpNyYNpvMfHqde9ugtV'; - $config->CNE->app->domain = 'dops.corp.cc'; + $config->CNE->api->host = 'http://20.205.128.72:32380'; + $config->CNE->api->token = 'esVqIv7c1LxMCwlCdCBv73LZTYANoApK'; + $config->CNE->app->domain = 'pgeo.corp.cc'; $this->objectModel = $tester->loadModel('cne'); } diff --git a/module/cne/test/model/certinfo.php b/module/cne/test/model/certinfo.php index d7e31ea8a8..59dc6f8950 100755 --- a/module/cne/test/model/certinfo.php +++ b/module/cne/test/model/certinfo.php @@ -25,4 +25,4 @@ $certName = 'fail-name'; r($cneModel->certInfoTest($certName)) && p() && e('0'); // 证书名称错误 $certName = 'tls-haogs-cn'; -r($cneModel->certInfoTest($certName)) && p('sans:0') && e('dops.corp.cc'); // 证书名称正确 \ No newline at end of file +r($cneModel->certInfoTest($certName)) && p('sans:0') && e('pgeo.corp.cc'); // 证书名称正确 diff --git a/module/cne/test/model/cnemetrics.php b/module/cne/test/model/cnemetrics.php index dbaa815ee1..f12775b279 100755 --- a/module/cne/test/model/cnemetrics.php +++ b/module/cne/test/model/cnemetrics.php @@ -14,10 +14,10 @@ cid=1 include dirname(__FILE__, 5) . '/test/lib/init.php'; global $tester, $config; -$config->CNE->api->host = 'http://10.0.7.210:32380'; -$config->CNE->api->token = 'JMz7HCoQ3WHoYbpNyYNpvMfHqde9ugtV'; -$config->CNE->app->domain = 'dops.corp.cc'; +$config->CNE->api->host = 'http://20.205.128.72:32380'; +$config->CNE->api->token = 'esVqIv7c1LxMCwlCdCBv73LZTYANoApK'; +$config->CNE->app->domain = 'pgeo.corp.cc'; $cneModel = $tester->loadModel('cne'); -r($cneModel->cneMetrics()) && p('status') && e('normal'); // 获取CNE平台的集群度量 \ No newline at end of file +r($cneModel->cneMetrics()) && p('status') && e('normal'); // 获取CNE平台的集群度量 diff --git a/module/common/lang/common.php b/module/common/lang/common.php index af91bf1a4d..c13b4367bf 100644 --- a/module/common/lang/common.php +++ b/module/common/lang/common.php @@ -14,6 +14,7 @@ $lang->scrum = new stdclass(); $lang->waterfall = new stdclass(); $lang->execution = new stdclass(); $lang->kanban = new stdclass(); +$lang->epic = new stdclass(); $lang->story = new stdclass(); $lang->requirement = new stdclass(); $lang->release = new stdclass(); diff --git a/module/common/lang/de.php b/module/common/lang/de.php index f0f8cbf21b..3e1abfc940 100644 --- a/module/common/lang/de.php +++ b/module/common/lang/de.php @@ -224,7 +224,8 @@ $lang->metric->common = 'Metric'; $lang->report->common = 'Report'; $lang->system->common = 'System'; $lang->admin->common = 'Admin'; -$lang->story->common = 'Story'; +$lang->epic->common = $lang->ERCommon; +$lang->story->common = $lang->SRCommon; $lang->task->common = 'Task'; $lang->bug->common = 'Bug'; $lang->testcase->common = 'Testcase'; @@ -437,7 +438,8 @@ $lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; -$lang->storyConcept = 'Story Concpet'; +$lang->storyConcept = 'Story Concpet'; +$lang->defaultERName = 'Epic'; $lang->searchTips = ''; $lang->searchAB = 'Search'; @@ -445,7 +447,9 @@ $lang->searchAB = 'Search'; /* Object list in search form. */ $lang->searchObjects['all'] = 'All'; $lang->searchObjects['bug'] = 'Bug'; -$lang->searchObjects['story'] = 'Story'; +$lang->searchObjects['story'] = $lang->SRCommon; +if($config->enableER) $lang->searchObjects['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->searchObjects['requirement'] = $lang->URCommon; $lang->searchObjects['task'] = 'Aufgaben'; $lang->searchObjects['testcase'] = 'Fall'; $lang->searchObjects['product'] = $lang->productCommon; diff --git a/module/common/lang/en.php b/module/common/lang/en.php index 176cce7eb7..21bdcb07e2 100644 --- a/module/common/lang/en.php +++ b/module/common/lang/en.php @@ -224,7 +224,8 @@ $lang->metric->common = 'Metric'; $lang->report->common = 'Report'; $lang->system->common = 'System'; $lang->admin->common = 'Admin'; -$lang->story->common = 'Story'; +$lang->epic->common = $lang->ERCommon; +$lang->story->common = $lang->SRCommon; $lang->task->common = 'Task'; $lang->bug->common = 'Bug'; $lang->testcase->common = 'Testcase'; @@ -437,7 +438,8 @@ $lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; -$lang->storyConcept = 'Story Concpet'; +$lang->storyConcept = 'Story Concpet'; +$lang->defaultERName = 'Epic'; $lang->searchTips = ''; $lang->searchAB = 'Search'; @@ -445,7 +447,9 @@ $lang->searchAB = 'Search'; /* Object list in search form. */ $lang->searchObjects['all'] = 'All'; $lang->searchObjects['bug'] = 'Bug'; -$lang->searchObjects['story'] = 'Story'; +$lang->searchObjects['story'] = $lang->SRCommon; +if($config->enableER) $lang->searchObjects['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->searchObjects['requirement'] = $lang->URCommon; $lang->searchObjects['task'] = 'Task'; $lang->searchObjects['testcase'] = 'Case'; $lang->searchObjects['product'] = $lang->productCommon; diff --git a/module/common/lang/fr.php b/module/common/lang/fr.php index 6b66073cfc..87eb334194 100644 --- a/module/common/lang/fr.php +++ b/module/common/lang/fr.php @@ -224,7 +224,8 @@ $lang->metric->common = 'Metric'; $lang->report->common = 'Report'; $lang->system->common = 'System'; $lang->admin->common = 'Admin'; -$lang->story->common = 'Story'; +$lang->epic->common = $lang->ERCommon; +$lang->story->common = $lang->SRCommon; $lang->task->common = 'Task'; $lang->bug->common = 'Bug'; $lang->testcase->common = 'Testcase'; @@ -437,7 +438,8 @@ $lang->admin->cache = 'Cache'; $lang->convert->importJira = 'Import Jira'; -$lang->storyConcept = 'Story Concpet'; +$lang->storyConcept = 'Story Concpet'; +$lang->defaultERName = 'Epic'; $lang->searchTips = ''; $lang->searchAB = 'Search'; @@ -445,7 +447,9 @@ $lang->searchAB = 'Search'; /* Object list in search form. */ $lang->searchObjects['all'] = 'All'; $lang->searchObjects['bug'] = 'Bug'; -$lang->searchObjects['story'] = 'Story'; +$lang->searchObjects['story'] = $lang->SRCommon; +if($config->enableER) $lang->searchObjects['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->searchObjects['requirement'] = $lang->URCommon; $lang->searchObjects['task'] = 'Tâche'; $lang->searchObjects['testcase'] = 'CasTest'; $lang->searchObjects['product'] = $lang->productCommon; diff --git a/module/common/lang/menu.php b/module/common/lang/menu.php index 831122f332..cf5501a8a5 100644 --- a/module/common/lang/menu.php +++ b/module/common/lang/menu.php @@ -112,6 +112,7 @@ $lang->my->menu->work['subMenu'] = new stdclass(); $lang->my->menu->work['subMenu']->task = array('link' => "{$lang->task->common}|my|work|mode=task", 'subModule' => 'task', 'alias' => 'task'); $lang->my->menu->work['subMenu']->story = array('link' => "$lang->SRCommon|my|work|mode=story", 'alias' => 'story'); $lang->my->menu->work['subMenu']->requirement = array('link' => "$lang->URCommon|my|work|mode=requirement", 'alias' => 'requirement'); +$lang->my->menu->work['subMenu']->epic = array('link' => "$lang->ERCommon|my|work|mode=epic", 'alias' => 'epic'); $lang->my->menu->work['subMenu']->bug = array('link' => "{$lang->bug->common}|my|work|mode=bug", 'subModule' => 'bug', 'alias' => 'bug'); $lang->my->menu->work['subMenu']->testcase = array('link' => "{$lang->testcase->common}|my|work|mode=testcase", 'subModule' => 'testtask,testcase', 'alias' => 'testcase'); $lang->my->menu->work['subMenu']->testtask = array('link' => "{$lang->testtask->common}|my|work|mode=testtask", 'alias' => 'testtask'); @@ -119,17 +120,20 @@ $lang->my->menu->work['subMenu']->testtask = array('link' => "{$lang->testtas $lang->my->menu->work['menuOrder'][5] = 'task'; $lang->my->menu->work['menuOrder'][10] = 'story'; $lang->my->menu->work['menuOrder'][15] = 'requirement'; +$lang->my->menu->work['menuOrder'][17] = 'epic'; $lang->my->menu->work['menuOrder'][20] = 'bug'; $lang->my->menu->work['menuOrder'][25] = 'testcase'; $lang->my->menu->work['menuOrder'][30] = 'testtask'; $lang->my->menu->work['menuOrder'][35] = 'audit'; -if(!$config->URAndSR) unset($lang->my->menu->work['subMenu']->requirement, $lang->my->menu->work['menuOrder'][10]); +if(!$config->enableER) unset($lang->my->menu->work['subMenu']->epic, $lang->my->menu->work['menuOrder'][12]); +if(!$config->URAndSR) unset($lang->my->menu->work['subMenu']->requirement, $lang->my->menu->work['menuOrder'][15]); $lang->my->menu->contribute['subMenu'] = new stdclass(); $lang->my->menu->contribute['subMenu']->task = array('link' => "{$lang->task->common}|my|contribute|mode=task", 'alias' => 'task'); $lang->my->menu->contribute['subMenu']->story = array('link' => "$lang->SRCommon|my|contribute|mode=story", 'alias' => 'story'); $lang->my->menu->contribute['subMenu']->requirement = array('link' => "$lang->URCommon|my|contribute|mode=requirement", 'alias' => 'requirement'); +$lang->my->menu->contribute['subMenu']->epic = array('link' => "$lang->ERCommon|my|contribute|mode=epic", 'alias' => 'epic'); $lang->my->menu->contribute['subMenu']->bug = array('link' => "{$lang->bug->common}|my|contribute|mode=bug", 'alias' => 'bug'); $lang->my->menu->contribute['subMenu']->testcase = array('link' => "{$lang->testcase->shortCommon}|my|contribute|mode=testcase", 'alias' => 'testcase'); $lang->my->menu->contribute['subMenu']->testtask = array('link' => "{$lang->testtask->common}|my|contribute|mode=testtask", 'alias' => 'testtask'); @@ -139,13 +143,15 @@ $lang->my->menu->contribute['subMenu']->doc = array('link' => "{$lang->d $lang->my->menu->contribute['menuOrder'][5] = 'task'; $lang->my->menu->contribute['menuOrder'][10] = 'story'; $lang->my->menu->contribute['menuOrder'][15] = 'requirement'; +$lang->my->menu->contribute['menuOrder'][17] = 'epic'; $lang->my->menu->contribute['menuOrder'][20] = 'bug'; $lang->my->menu->contribute['menuOrder'][25] = 'testcase'; $lang->my->menu->contribute['menuOrder'][30] = 'testtask'; $lang->my->menu->contribute['menuOrder'][35] = 'audit'; $lang->my->menu->contribute['menuOrder'][40] = 'doc'; -if(!$config->URAndSR) unset($lang->my->menu->contribute['subMenu']->requirement, $lang->my->menu->contribute['menuOrder'][10]); +if(!$config->enableER) unset($lang->my->menu->contribute['subMenu']->epic, $lang->my->menu->contribute['menuOrder'][12]); +if(!$config->URAndSR) unset($lang->my->menu->contribute['subMenu']->requirement, $lang->my->menu->contribute['menuOrder'][15]); $lang->my->dividerMenu = ',work,dynamic,'; @@ -180,12 +186,13 @@ $lang->product->homeMenu->kanban = array('link' => "{$lang->product->kanban}|pro $lang->product->menu = new stdclass(); $lang->product->menu->dashboard = array('link' => "{$lang->dashboard}|product|dashboard|productID=%s"); -$lang->product->menu->story = array('link' => "$lang->SRCommon|product|browse|productID=%s", 'alias' => 'batchedit', 'subModule' => 'story', 'exclude' => (isset($_GET['storyType']) ? ($_GET['storyType'] == 'requirement' ? 'browse,story-report,story-create,story-batchcreate' : '') : '') . ',batchtotask'); +$lang->product->menu->epic = array('link' => "{$lang->ERCommon}|product|browse|productID=%s&branch=&browseType=unclosed¶m=0&storyType=epic", 'subModule' => 'story'); +$lang->product->menu->requirement = array('link' => "{$lang->URCommon}|product|browse|productID=%s&branch=&browseType=unclosed¶m=0&storyType=requirement", 'alias' => 'batchedit', 'subModule' => 'story'); +$lang->product->menu->story = array('link' => "{$lang->SRCommon}|product|browse|productID=%s", 'alias' => 'batchedit', 'subModule' => 'story', 'exclude' => (isset($_GET['storyType']) ? ($_GET['storyType'] == 'requirement' ? 'browse,story-report,story-create,story-batchcreate' : '') : '') . ',batchtotask'); $lang->product->menu->plan = array('link' => "{$lang->productplan->shortCommon}|productplan|browse|productID=%s", 'subModule' => 'productplan,bug'); $lang->product->menu->project = array('link' => "{$lang->projectCommon}|product|project|status=all&productID=%s"); $lang->product->menu->release = array('link' => "{$lang->release->common}|release|browse|productID=%s", 'subModule' => 'release'); $lang->product->menu->roadmap = array('link' => "{$lang->roadmap}|product|roadmap|productID=%s"); -$lang->product->menu->requirement = array('link' => "{$lang->URCommon}|product|browse|productID=%s&branch=&browseType=unclosed¶m=0&storyType=requirement", 'alias' => 'batchedit', 'subModule' => 'story'); $lang->product->menu->track = array('link' => "{$lang->track}|product|track|productID=%s"); $lang->product->menu->doc = array('link' => "{$lang->doc->common}|doc|productSpace|objectID=%s", 'subModule' => 'doc,api'); $lang->product->menu->dynamic = array('link' => "{$lang->dynamic}|product|dynamic|productID=%s"); @@ -193,18 +200,19 @@ $lang->product->menu->settings = array('link' => "{$lang->settings}|product|v /* Product menu order. */ $lang->product->menuOrder[5] = 'dashboard'; -$lang->product->menuOrder[10] = 'story'; -$lang->product->menuOrder[15] = 'plan'; -$lang->product->menuOrder[20] = 'project'; -$lang->product->menuOrder[25] = 'release'; -$lang->product->menuOrder[30] = 'roadmap'; -$lang->product->menuOrder[35] = 'requirement'; -$lang->product->menuOrder[40] = 'track'; -$lang->product->menuOrder[45] = 'doc'; -$lang->product->menuOrder[50] = 'dynamic'; -$lang->product->menuOrder[55] = 'settings'; -$lang->product->menuOrder[60] = 'create'; -$lang->product->menuOrder[65] = 'all'; +$lang->product->menuOrder[10] = 'epic'; +$lang->product->menuOrder[15] = 'requirement'; +$lang->product->menuOrder[20] = 'story'; +$lang->product->menuOrder[25] = 'plan'; +$lang->product->menuOrder[30] = 'project'; +$lang->product->menuOrder[35] = 'release'; +$lang->product->menuOrder[40] = 'roadmap'; +$lang->product->menuOrder[45] = 'track'; +$lang->product->menuOrder[50] = 'doc'; +$lang->product->menuOrder[55] = 'dynamic'; +$lang->product->menuOrder[60] = 'settings'; +$lang->product->menuOrder[65] = 'create'; +$lang->product->menuOrder[70] = 'all'; $lang->product->menu->settings['subMenu'] = new stdclass(); $lang->product->menu->settings['subMenu']->view = array('link' => "{$lang->overview}|product|view|productID=%s", 'alias' => 'edit'); @@ -212,7 +220,7 @@ $lang->product->menu->settings['subMenu']->module = array('link' => "{$lang-> $lang->product->menu->settings['subMenu']->branch = array('link' => "@branch@|branch|manage|product=%s", 'subModule' => 'branch'); $lang->product->menu->settings['subMenu']->whitelist = array('link' => "{$lang->whitelist}|product|whitelist|product=%s", 'subModule' => 'personnel'); -$lang->product->dividerMenu = $config->URAndSR ? ',story,requirement,settings,' : ',story,track,settings,'; +$lang->product->dividerMenu = ',epic,track,settings,'; /* Project menu. */ $lang->project->homeMenu = new stdclass(); @@ -223,8 +231,7 @@ $lang->project->homeMenu->kanban = array('link' => "{$lang->project->kanban}|pro $lang->scrum->menu = new stdclass(); $lang->scrum->menu->index = array('link' => "{$lang->dashboard}|project|index|project=%s"); $lang->scrum->menu->execution = array('link' => "$lang->executionCommon|project|execution|status=undone&projectID=%s", 'exclude' => 'execution-testreport', 'subModule' => 'task'); -$lang->scrum->menu->storyGroup = array('link' => "{$lang->common->story}|projectstory|story|projectID=%s&product=%s",'class' => 'dropdown dropdown-hover', 'exclude' => 'tree-browse'); -$lang->scrum->menu->story = array('link' => "$lang->SRCommon|projectstory|story|projectID=%s", 'subModule' => 'projectstory,tree', 'alias' => 'story,track', 'exclude' => 'tree-browse'); +$lang->scrum->menu->story = array('link' => "{$lang->common->story}|projectstory|story|projectID=%s", 'subModule' => 'projectstory,tree,epic,requirement', 'alias' => 'story,track', 'exclude' => 'tree-browse'); $lang->scrum->menu->projectplan = array('link' => "{$lang->productplan->shortCommon}|projectplan|browse|productID=%s", 'subModule' => 'productplan'); $lang->scrum->menu->doc = array('link' => "{$lang->doc->common}|doc|projectSpace|objectID=%s", 'subModule' => 'doc,api'); $lang->scrum->menu->qa = array('link' => "{$lang->qa->common}|project|bug|projectID=%s", 'subModule' => 'testcase,testtask,bug,testreport', 'alias' => 'bug,testtask,testcase,testreport', 'exclude' => 'execution-create,execution-batchedit'); @@ -234,17 +241,12 @@ $lang->scrum->menu->release = array('link' => "{$lang->release->common}|proj $lang->scrum->menu->dynamic = array('link' => "$lang->dynamic|project|dynamic|project=%s"); $lang->scrum->menu->settings = array('link' => "$lang->settings|project|view|project=%s", 'subModule' => 'tree,stakeholder', 'alias' => 'edit,manageproducts,group,managemembers,manageview,managepriv,whitelist,addwhitelist,team', 'exclude' => 'tree-browsetask'); -$lang->scrum->menu->storyGroup['dropMenu'] = new stdclass(); -$lang->scrum->menu->storyGroup['dropMenu']->story = array('link' => "{$lang->SRCommon}|projectstory|story|projectID=%s&productID=%s", 'subModule' => 'tree,story'); -$lang->scrum->menu->storyGroup['dropMenu']->requirement = array('link' => "{$lang->URCommon}|projectstory|story|projectID=%s&productID=%s&branch=0&browseType=¶m=0&storyType=requirement", 'subModule' => 'tree,story'); - $lang->scrum->dividerMenu = ',execution,programplan,doc,settings,'; /* Scrum menu order. */ $lang->scrum->menuOrder[5] = 'index'; $lang->scrum->menuOrder[10] = 'execution'; $lang->scrum->menuOrder[15] = 'story'; -$lang->scrum->menuOrder[16] = 'storyGroup'; $lang->scrum->menuOrder[18] = 'projectplan'; $lang->scrum->menuOrder[20] = 'qa'; $lang->scrum->menuOrder[25] = 'devops'; @@ -281,8 +283,7 @@ $lang->scrum->menu->devops['menuOrder'][15] = 'mr'; $lang->waterfall->menu = new stdclass(); $lang->waterfall->menu->index = array('link' => "$lang->dashboard|project|index|project=%s"); $lang->waterfall->menu->execution = array('link' => "{$lang->stage->common}|project|execution|status=undone&projectID=%s", 'subModule' => 'programplan,task'); -$lang->waterfall->menu->storyGroup = array('link' => "{$lang->common->story}|projectstory|story|projectID=%s",'class' => 'dropdown dropdown-hover', 'exclude' => 'tree-browse,projectstory-track'); -$lang->waterfall->menu->story = array('link' => "$lang->SRCommon|projectstory|story|projectID=%s", 'subModule' => 'projectstory,tree', 'alias' => 'story', 'exclude' => 'projectstory-track,tree-browse'); +$lang->waterfall->menu->story = array('link' => "{$lang->common->story}|projectstory|story|projectID=%s", 'subModule' => 'projectstory,tree', 'alias' => 'story', 'exclude' => 'projectstory-track,tree-browse'); $lang->waterfall->menu->design = array('link' => "{$lang->design->common}|design|browse|project=%s"); $lang->waterfall->menu->qa = array('link' => "{$lang->qa->common}|project|bug|projectID=%s", 'subModule' => 'testcase,testtask,bug,testreport', 'alias' => 'bug,testtask,testcase,testreport'); $lang->waterfall->menu->doc = array('link' => "{$lang->doc->common}|doc|projectSpace|objectID=%s"); @@ -294,14 +295,11 @@ $lang->waterfall->menu->dynamic = array('link' => "$lang->dynamic|project|dyn $lang->waterfall->menu->settings = $lang->scrum->menu->settings; $lang->waterfall->dividerMenu = ',programplan,build,dynamic,'; -$lang->waterfall->menu->storyGroup['dropMenu'] = $lang->scrum->menu->storyGroup['dropMenu']; - /* Waterfall menu order. */ $lang->waterfall->menuOrder[5] = 'index'; $lang->waterfall->menuOrder[15] = 'programplan'; $lang->waterfall->menuOrder[20] = 'execution'; $lang->waterfall->menuOrder[25] = 'story'; -$lang->waterfall->menuOrder[26] = 'storyGroup'; $lang->waterfall->menuOrder[30] = 'design'; $lang->waterfall->menuOrder[35] = 'devops'; $lang->waterfall->menuOrder[55] = 'qa'; @@ -367,8 +365,7 @@ $lang->execution->menu->view = array('link' => "$lang->view|execution|grouptas if($config->edition != 'open') $lang->execution->menu->view = array('link' => "$lang->view|execution|gantt|executionID=%s", 'alias' => 'grouptask,tree,taskeffort,gantt,calendar,relation,maintainrelation'); -$lang->execution->menu->storyGroup = array('link' => "{$lang->common->story}|execution|story|executionID=%s",'class' => 'dropdown dropdown-hover', 'subModule' => 'story', 'alias' => 'batchcreate,storykanban,batchtotask'); -$lang->execution->menu->story = array('link' => "$lang->SRCommon|execution|story|executionID=%s", 'subModule' => 'story', 'alias' => 'storyview,storykanban,linkstory,batchtotask'); +$lang->execution->menu->story = array('link' => "{$lang->common->story}|execution|story|executionID=%s", 'subModule' => 'story,requirement,epic', 'alias' => 'storyview,storykanban,linkstory,batchtotask'); $lang->execution->menu->qa = array('link' => "{$lang->qa->common}|execution|bug|executionID=%s", 'subModule' => 'bug,testcase,testtask,testreport', 'alias' => 'qa,bug,testcase,testtask,testreport'); if(helper::hasFeature('devops')) $lang->execution->menu->devops = array('link' => "{$lang->devops->common}|repo|browse|repoID=0&branchID=&objectID=%s", 'subModule' => 'repo,mr', 'alias' => 'create'); $lang->execution->menu->doc = array('link' => "{$lang->doc->common}|execution|doc|objectID=%s", 'subModule' => 'doc'); @@ -377,10 +374,6 @@ $lang->execution->menu->action = array('link' => "$lang->dynamic|execution|d $lang->execution->menu->settings = array('link' => "$lang->settings|execution|view|executionID=%s", 'subModule' => 'personnel', 'alias' => 'edit,manageproducts,team,whitelist,addwhitelist,managemembers', 'class' => 'dropdown dropdown-hover'); $lang->execution->menu->more = array('link' => "$lang->more|execution|more|%s"); -$lang->execution->menu->storyGroup['dropMenu'] = new stdclass(); -$lang->execution->menu->storyGroup['dropMenu']->story = array('link' => "{$lang->SRCommon}|execution|story|executionID=%s", 'subModule' => 'story'); -$lang->execution->menu->storyGroup['dropMenu']->requirement = array('link' => "{$lang->URCommon}|execution|story|executionID=%s&storyType=requirement", 'subModule' => 'story'); - /* Execution menu order. */ $lang->execution->menuOrder[5] = 'task'; $lang->execution->menuOrder[10] = 'kanban'; @@ -436,7 +429,6 @@ $lang->project->noMultiple->scrum->menu->kanban = $lang->execution->menu->ka $lang->project->noMultiple->scrum->menu->burn = $lang->execution->menu->burn; $lang->project->noMultiple->scrum->menu->view = $lang->execution->menu->view; $lang->project->noMultiple->scrum->menu->story = $lang->execution->menu->story; -$lang->project->noMultiple->scrum->menu->storyGroup = $lang->execution->menu->storyGroup; $lang->project->noMultiple->scrum->menu->qa = $lang->scrum->menu->qa; $lang->project->noMultiple->scrum->menu->devops = $lang->scrum->menu->devops; $lang->project->noMultiple->scrum->menu->doc = $lang->scrum->menu->doc; @@ -606,6 +598,7 @@ $lang->navGroup->product = 'product'; $lang->navGroup->productplan = 'product'; $lang->navGroup->release = 'product'; $lang->navGroup->branch = 'product'; +$lang->navGroup->epic = 'product'; $lang->navGroup->story = 'product'; $lang->navGroup->requirement = 'product'; $lang->navGroup->productsettings = 'product'; @@ -760,7 +753,8 @@ $lang->navGroup->index = 'index'; $lang->navGroup->misc = 'misc'; $lang->navGroup->upgrade = 'upgrade'; -if(!$config->URAndSR) unset($lang->product->menu->requirement, $lang->product->menuOrder[35]); +if(!$config->enableER) unset($lang->product->menu->epic, $lang->product->menuOrder[10]); +if(!$config->URAndSR) unset($lang->product->menu->requirement, $lang->product->menuOrder[15]); if(!helper::hasFeature('product_roadmap')) unset($lang->product->menu->roadmap, $lang->product->menuOrder[30]); if(!helper::hasFeature('product_track')) { diff --git a/module/common/lang/zh-cn.php b/module/common/lang/zh-cn.php index 8b3d198938..459cf48a38 100644 --- a/module/common/lang/zh-cn.php +++ b/module/common/lang/zh-cn.php @@ -224,6 +224,7 @@ $lang->metric->common = '度量项'; $lang->report->common = '统计'; $lang->system->common = '组织'; $lang->admin->common = '后台'; +$lang->epic->common = $lang->ERCommon; $lang->story->common = $lang->SRCommon; $lang->task->common = '任务'; $lang->bug->common = 'Bug'; @@ -437,7 +438,8 @@ $lang->admin->cache = '缓存'; $lang->convert->importJira = '导入Jira数据'; -$lang->storyConcept = '需求概念'; +$lang->storyConcept = '需求概念'; +$lang->defaultERName = '业务需求'; $lang->searchTips = ''; $lang->searchAB = '搜索'; @@ -445,7 +447,9 @@ $lang->searchAB = '搜索'; /* 查询中可以选择的对象列表。*/ $lang->searchObjects['all'] = '全部'; $lang->searchObjects['bug'] = 'Bug'; -$lang->searchObjects['story'] = '需求'; +$lang->searchObjects['story'] = $lang->SRCommon; +if($config->enableER) $lang->searchObjects['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->searchObjects['requirement'] = $lang->URCommon; $lang->searchObjects['task'] = '任务'; $lang->searchObjects['testcase'] = '用例'; $lang->searchObjects['product'] = $lang->productCommon; diff --git a/module/common/model.php b/module/common/model.php index 36e5b6059f..004dd70c50 100644 --- a/module/common/model.php +++ b/module/common/model.php @@ -2446,7 +2446,7 @@ eof; { if(!empty($actionData['url']) && is_array($actionData['url'])) { - $moduleName = $actionData['url']['module']; + $moduleName = ($actionData['url']['module'] == 'story' && $moduleName == 'story') ? $data->type : $actionData['url']['module']; $methodName = $actionData['url']['method']; $params = $actionData['url']['params']; if(!common::hasPriv($moduleName, $methodName)) return false; @@ -2454,7 +2454,7 @@ eof; } else if(!empty($actionData['data-url']) && is_array($actionData['data-url'])) { - $moduleName = $actionData['data-url']['module']; + $moduleName = ($actionData['data-url']['module'] == 'story' && $moduleName == 'story') ? $data->type : $actionData['data-url']['module']; $methodName = $actionData['data-url']['method']; $params = $actionData['data-url']['params']; if(!common::hasPriv($moduleName, $methodName)) return false; diff --git a/module/cron/test/model/runnable.php b/module/cron/test/model/runnable.php index f8f1281b90..6d62a1ef8a 100755 --- a/module/cron/test/model/runnable.php +++ b/module/cron/test/model/runnable.php @@ -38,7 +38,7 @@ $tester->config->cron->run->status = 'running'; $res5 = $cron->runnableTest(); r($res1) && p() && e('0'); //cron配置为空情况 -r($res2) && p() && e('1'); //cron配置存在,执行时间大于最大可执行时间情况 -r($res3) && p() && e('1'); //cron状态为空情况 -r($res4) && p() && e('1'); //cron状态是stop情况 +r($res2) && p() && e('0'); //cron配置存在,执行时间大于最大可执行时间情况 +r($res3) && p() && e('0'); //cron状态为空情况 +r($res4) && p() && e('0'); //cron状态是stop情况 r($res5) && p() && e('0'); //cron状态存在不是stop情况 diff --git a/module/custom/config.php b/module/custom/config.php index 250272da3b..4fed63169c 100644 --- a/module/custom/config.php +++ b/module/custom/config.php @@ -1,21 +1,25 @@ custom = new stdClass(); -$config->custom->canAdd['story'] = 'reasonList,sourceList,priList,categoryList'; -$config->custom->canAdd['task'] = 'priList,typeList,reasonList'; -$config->custom->canAdd['bug'] = 'priList,severityList,osList,browserList,typeList,resolutionList'; -$config->custom->canAdd['testcase'] = 'priList,typeList,stageList,resultList,statusList'; -$config->custom->canAdd['testtask'] = 'priList,typeList'; -$config->custom->canAdd['todo'] = 'priList,typeList'; -$config->custom->canAdd['user'] = 'roleList'; -$config->custom->canAdd['block'] = ''; -$config->custom->canAdd['project'] = 'unitList'; +$config->custom->canAdd['epic'] = 'reasonList,sourceList,priList,categoryList'; +$config->custom->canAdd['story'] = 'reasonList,sourceList,priList,categoryList'; +$config->custom->canAdd['requirement'] = 'reasonList,sourceList,priList,categoryList'; +$config->custom->canAdd['task'] = 'priList,typeList,reasonList'; +$config->custom->canAdd['bug'] = 'priList,severityList,osList,browserList,typeList,resolutionList'; +$config->custom->canAdd['testcase'] = 'priList,typeList,stageList,resultList,statusList'; +$config->custom->canAdd['testtask'] = 'priList,typeList'; +$config->custom->canAdd['todo'] = 'priList,typeList'; +$config->custom->canAdd['user'] = 'roleList'; +$config->custom->canAdd['block'] = ''; +$config->custom->canAdd['project'] = 'unitList'; $config->custom->noModuleMenu = array(); -$config->custom->requiredModules[10] = 'product'; -$config->custom->requiredModules[15] = 'story'; -$config->custom->requiredModules[20] = 'productplan'; -$config->custom->requiredModules[25] = 'release'; +$config->custom->requiredModules[5] = 'product'; +$config->custom->requiredModules[10] = 'epic'; +$config->custom->requiredModules[15] = 'requirement'; +$config->custom->requiredModules[20] = 'story'; +$config->custom->requiredModules[25] = 'productplan'; +$config->custom->requiredModules[30] = 'release'; $config->custom->requiredModules[30] = 'project'; $config->custom->requiredModules[35] = 'execution'; @@ -23,7 +27,7 @@ $config->custom->requiredModules[40] = 'task'; $config->custom->requiredModules[45] = 'build'; $config->custom->allFeatures = array('program', 'productRR', 'productUR', 'productLine', 'projectScrum', 'projectWaterfall', 'projectKanban', 'projectAgileplus', 'projectWaterfallplus', 'execution', 'qa', 'devops', 'ai', 'report', 'kanban', 'doc', 'system', 'admin', 'vision'); -$config->custom->dataFeatures = array('productUR', 'waterfall', 'waterfallplus'); +$config->custom->dataFeatures = array('productER', 'productUR', 'waterfall', 'waterfallplus'); $config->custom->scrumFeatures = array(); $config->custom->requiredModules[50] = 'bug'; @@ -32,9 +36,7 @@ $config->custom->requiredModules[60] = 'testsuite'; $config->custom->requiredModules[65] = 'testreport'; $config->custom->requiredModules[70] = 'caselib'; $config->custom->requiredModules[75] = 'testtask'; - $config->custom->requiredModules[80] = 'doc'; - $config->custom->requiredModules[85] = 'user'; $config->custom->fieldList['program']['create'] = 'budget,PM,desc'; @@ -43,10 +45,18 @@ $config->custom->fieldList['project']['create'] = 'budget,PM,desc'; $config->custom->fieldList['project']['edit'] = 'budget,PM,desc'; $config->custom->fieldList['product']['create'] = 'PO,QD,RD,type,desc'; $config->custom->fieldList['product']['edit'] = 'PO,QD,RD,type,desc,status'; +$config->custom->fieldList['epic']['create'] = 'module,plan,source,pri,estimate,keywords,spec,verify'; +$config->custom->fieldList['epic']['change'] = 'comment,spec,verify'; +$config->custom->fieldList['epic']['close'] = 'comment'; +$config->custom->fieldList['epic']['review'] = 'reviewedDate,comment'; $config->custom->fieldList['story']['create'] = 'module,plan,source,pri,estimate,keywords,spec,verify'; $config->custom->fieldList['story']['change'] = 'comment,spec,verify'; $config->custom->fieldList['story']['close'] = 'comment'; $config->custom->fieldList['story']['review'] = 'reviewedDate,comment'; +$config->custom->fieldList['requirement']['create'] = 'module,plan,source,pri,estimate,keywords,spec,verify'; +$config->custom->fieldList['requirement']['change'] = 'comment,spec,verify'; +$config->custom->fieldList['requirement']['close'] = 'comment'; +$config->custom->fieldList['requirement']['review'] = 'reviewedDate,comment'; $config->custom->fieldList['productplan'] = 'begin,end,desc'; $config->custom->fieldList['release']['create'] = 'desc'; $config->custom->fieldList['release']['edit'] = 'desc'; diff --git a/module/custom/config/dtable.php b/module/custom/config/dtable.php index 8f63510f17..c53736ed7e 100644 --- a/module/custom/config/dtable.php +++ b/module/custom/config/dtable.php @@ -3,15 +3,22 @@ $config->custom->browseStoryConcept->dtable = new stdclass(); $config->custom->browseStoryConcept->dtable->fieldList['default']['type'] = 'id'; $config->custom->browseStoryConcept->dtable->fieldList['default']['sortType'] = false; +if($config->enableER) +{ + $config->custom->browseStoryConcept->dtable->fieldList['ERName']['title'] = $lang->custom->ERConcept; + $config->custom->browseStoryConcept->dtable->fieldList['ERName']['type'] = 'category'; + $config->custom->browseStoryConcept->dtable->fieldList['ERName']['sortType'] = false; +} + if($config->URAndSR) { $config->custom->browseStoryConcept->dtable->fieldList['URName']['title'] = $lang->custom->URConcept; - $config->custom->browseStoryConcept->dtable->fieldList['URName']['type'] = 'title'; + $config->custom->browseStoryConcept->dtable->fieldList['URName']['type'] = 'category'; $config->custom->browseStoryConcept->dtable->fieldList['URName']['sortType'] = false; } $config->custom->browseStoryConcept->dtable->fieldList['SRName']['title'] = $lang->custom->SRConcept; -$config->custom->browseStoryConcept->dtable->fieldList['SRName']['type'] = 'title'; +$config->custom->browseStoryConcept->dtable->fieldList['SRName']['type'] = 'category'; $config->custom->browseStoryConcept->dtable->fieldList['SRName']['sortType'] = false; $config->custom->browseStoryConcept->dtable->fieldList['actions']['type'] = 'actions'; diff --git a/module/custom/control.php b/module/custom/control.php index dfc47c7e9b..868ce55d17 100644 --- a/module/custom/control.php +++ b/module/custom/control.php @@ -60,7 +60,6 @@ class custom extends control $this->view->title = $this->lang->custom->common . $this->lang->hyphen . $this->lang->$module->common; $this->view->field = $field; - $this->view->lang2Set = str_replace('_', '-', $lang); $this->view->module = $module; $this->view->currentLang = $currentLang; $this->view->canAdd = strpos($this->config->custom->canAdd[$module], $field) !== false; @@ -254,6 +253,66 @@ class custom extends control return $this->send(array('result' => 'success', 'load' => inlink('browseStoryConcept'))); } + /** + * 停用需求等级。 + * Close story grade. + * + * @param string $type + * @param int $gradeID + * @access public + * @return void + */ + public function closeGrade(string $type = 'story', int $gradeID = 0) + { + if($gradeID) $this->dao->update(TABLE_STORYGRADE)->set('status')->eq('disable')->where('grade')->eq($gradeID)->andWhere('type')->eq($type)->exec(); + return $this->sendSuccess(array('load' => true)); + } + + /** + * 启用需求等级。 + * Activate story grade. + * + * @param string $type + * @param int $gradeID + * @access public + * @return void + */ + public function activateGrade(string $type = 'story', int $gradeID = 0) + { + if($gradeID) $this->dao->update(TABLE_STORYGRADE)->set('status')->eq('enable')->where('grade')->eq($gradeID)->andWhere('type')->eq($type)->exec(); + return $this->sendSuccess(array('load' => true)); + } + + /** + * 删除需求等级。 + * Delete story grade. + * + * @param string $type + * @param int $gradeID + * @param string $confirm + * @access public + * @return void + */ + public function deleteGrade(string $type = 'story', int $gradeID = 0, string $confirm = 'no') + { + $story = $this->dao->select('id')->from(TABLE_STORY)->where('grade')->eq($gradeID)->andWhere('type')->eq($type)->limit(1)->fetch('id'); + if($story) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('{$this->lang->custom->notice->gradeNotEmpty}');")); + + if($confirm == 'no') + { + $formUrl = $this->createLink('custom', 'deleteGrade', "type=$type&gradeID=$gradeID&confirm=yes"); + return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->custom->notice->deleteGrade}'}).then((res) => {if(res) $.ajaxSubmit({url: '{$formUrl}'});});")); + } + + if($gradeID) + { + $grades = $this->dao->select('*')->from(TABLE_STORYGRADE)->where('grade')->gt($gradeID)->andWhere('type')->eq($type)->orderBy('grade_asc')->fetchAll(); + $this->dao->delete()->from(TABLE_STORYGRADE)->where('grade')->eq($gradeID)->andWhere('type')->eq($type)->exec(); + foreach($grades as $grade) $this->dao->update(TABLE_STORYGRADE)->set('grade')->eq($grade->grade - 1)->where('grade')->eq($grade->grade)->andWhere('type')->eq($type)->exec(); + } + return $this->sendSuccess(array('load' => true)); + } + /** * 执行关闭设置。 * Set whether the closed execution is read-only. @@ -396,19 +455,25 @@ class custom extends control if(!isset($this->config->custom->customFields[$module][$section])) return; if(!in_array($key, $this->config->custom->customFields[$module][$section])) return; + $this->loadModel('setting'); + $global = $this->post->global; + $action = $this->post->action; $account = $this->app->user->account; - if($this->server->request_method == 'POST') + if($global) $account = 'system'; + if($this->server->request_method == 'POST' && $action != 'reset') { $fields = $this->post->fields; if(is_array($fields)) $fields = implode(',', $fields); if($module == 'execution' && $section == 'custom' && $key == 'createFields' && strpos(",{$fields},", ',team,') !== false) $fields .= ',teams'; - $this->loadModel('setting')->setItem("{$account}.{$module}.{$section}.{$key}", $fields); + + $this->setting->setItem("{$account}.{$module}.{$section}.{$key}", $fields); + if(in_array($module, array('story', 'task', 'testcase')) && $section == 'custom' && $key == 'batchCreateFields') return; if($module == 'bug' && $section == 'custom' && $key == 'batchCreateFields') return; } else { - $this->loadModel('setting')->deleteItems("owner={$account}&module={$module}§ion={$section}&key={$key}"); + $this->setting->deleteItems("owner={$account}&module={$module}§ion={$section}&key={$key}"); } $this->loadModel('common')->loadConfigFromDB(); @@ -416,8 +481,8 @@ class custom extends control $this->app->loadConfig($module); if($module == 'programplan' && $section == 'custom') $key = 'createFields'; - $customFields = $this->config->$module->list->{'custom' . ucfirst($key)}; - $showFields = $this->config->$module->custom->$key; + $customFields = zget(zget($this->config->$module, 'list', array()), $section . ucfirst($key), ''); + $showFields = zget(zget($this->config->$module, $section, array()), $key, ''); if($module == 'marketresearch') return print(js::reload('parent')); return $this->send(array('result' => 'success', 'key' => $key, 'callback' => 'loadCurrentPage', 'customFields' => $customFields, 'showFields' => $showFields)); } diff --git a/module/custom/css/required.ui.css b/module/custom/css/required.ui.css new file mode 100644 index 0000000000..0b35b3a9fe --- /dev/null +++ b/module/custom/css/required.ui.css @@ -0,0 +1,3 @@ +#requiredForm .form-row .form-group {padding-left: 8rem;} +#requiredForm .form-row .form-label {width: 8rem;} +:root {--form-horz-label-width: 7rem} diff --git a/module/custom/css/set.ui.css b/module/custom/css/set.ui.css index bc6e8da68e..609bbc250c 100644 --- a/module/custom/css/set.ui.css +++ b/module/custom/css/set.ui.css @@ -4,3 +4,16 @@ .lang-row .form-group {padding-left: 0; justify-content: center;} .lang-row .form-group > .form-label {width: var(--form-horz-label-width);} .unit-box .checkbox-primary {flex: 0 1 15%;} +#gradeList {width: 50%} +.gradeRuleNotice {margin-left: 4.25rem} +td.required:after { + --tw-translate-y: 0.125rem; + --tw-scale-x: 1.25; + --tw-scale-y: 1.25; + --tw-text-opacity: 1; + color: rgba(var(--color-danger-500-rgb), var(--tw-text-opacity)); + content: "*"; + display: inline-block; + margin-left: 0.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} diff --git a/module/custom/js/set.ui.js b/module/custom/js/set.ui.js index 3055f5f889..5ee6f5e494 100644 --- a/module/custom/js/set.ui.js +++ b/module/custom/js/set.ui.js @@ -89,3 +89,26 @@ window.savaTestcaseReview = function() $.ajaxSubmit({url, data:form}); } + +window.deleteGrade = function() +{ + $(this).closest('tr').remove(); + $('#gradeList tbody').find('tr').last().find('.btn-add-grade').removeClass('hidden'); +} + +window.addGrade = function() +{ + let newRow = $(this).closest('tr').clone(); + let maxIndex = $(this).closest('tbody').find('tr').length; + newIndex = parseInt(maxIndex); + + newRow.find('input').val(''); + newRow.find('.btn-delete-grade').attr('href', 'javascript:void').removeClass('ajax-submit hidden').on('click', deleteGrade); + newRow.find('.btn-close').remove(); + newRow.find("input[type=hidden]").val(newIndex); + newRow.find('td.index').text(newIndex); + newRow.find('td.grade-status').text(enableLang); + $(this).closest('tbody').append(newRow); + + $(this).addClass('hidden'); +}; diff --git a/module/custom/lang/de.php b/module/custom/lang/de.php index 0131cc2d07..cf311ce6f8 100644 --- a/module/custom/lang/de.php +++ b/module/custom/lang/de.php @@ -40,6 +40,7 @@ $lang->custom->setStoryConcept = "Set Story Concept"; $lang->custom->setDefaultConcept = "Set Default Concept"; $lang->custom->browseStoryConcept = "List of story concepts"; $lang->custom->deleteStoryConcept = "Delete story Concept"; +$lang->custom->ERConcept = "ER Concept"; $lang->custom->URConcept = "UR Concept"; $lang->custom->SRConcept = "SR Concept"; $lang->custom->reviewRule = 'Review Rules'; @@ -63,7 +64,14 @@ $lang->custom->setPercent = "Enable or Disable {$lang->stage->percent} $lang->custom->beginAndEndDate = 'Begin & End'; $lang->custom->beginAndEndDateRange = 'The Range Of Begin & End'; $lang->custom->limitTaskDateAction = 'Set start and end date required'; +$lang->custom->closeGrade = 'Close Story Grade'; +$lang->custom->activateGrade = 'Activate Story Grade'; +$lang->custom->deleteGrade = 'Delete Story Grade'; $lang->custom->closeSetting = 'Close Setting'; +$lang->custom->gradeRule = 'Allow cross-level segmentation'; + +$lang->custom->gradeRuleList['cross'] = 'Yes'; +$lang->custom->gradeRuleList['stepwise'] = 'No'; $lang->custom->unitList['efficiency'] = 'Working Hours/'; $lang->custom->unitList['manhour'] = 'Man-hour/'; @@ -95,6 +103,9 @@ $lang->custom->closedExecution = 'Closed ' . $lang->executionCommon; $lang->custom->closedKanban = 'Closed ' . $lang->custom->kanban; $lang->custom->closedProduct = 'Closed ' . $lang->productCommon; +$lang->custom->gradeStatusList['enable'] = 'Normal'; +$lang->custom->gradeStatusList['disable'] = 'Disabled'; + $lang->custom->block = new stdclass(); $lang->custom->block->fields['closed'] = 'Closed Block'; @@ -125,6 +136,9 @@ $lang->custom->story->fields['reviewRules'] = 'Review Rules'; $lang->custom->story->fields['reviewResultList'] = 'Review Result'; $lang->custom->story->fields['review'] = 'Review Required'; +$lang->custom->epic = clone $lang->custom->story; +$lang->custom->requirement = clone $lang->custom->story; + $lang->custom->task = new stdClass(); $lang->custom->task->fields['required'] = $lang->custom->required; $lang->custom->task->fields['priList'] = 'Priority'; @@ -214,6 +228,11 @@ $lang->custom->notice->selectAllTip = 'After selecting all people, the re $lang->custom->notice->repeatKey = 'Repeat Key %s'; $lang->custom->notice->readOnlyOfCode = "A code is a management term that exists for secrecy or as an antonym. When code management is enabled, the code information of {$lang->productCommon}, {$lang->projectCommon}, and execution in the system will be displayed in the creation, editing, detail, and list pages."; $lang->custom->notice->readOnlyOfPercent = "The \"Workload Ratio\" is used to divide the workload of a {$lang->projectCommon} into different stages. The sum of the percentages of the same level stages cannot exceed 100%. After enabling the \"Workload Ratio\", users have to fill in the ratio fields when setting up the stages in the Waterfall {$lang->projectCommon} and Waterfall Plus {$lang->projectCommon} management models."; +$lang->custom->notice->closeGrade = "After closing, the grade cannot be used to create new stories."; +$lang->custom->notice->activateGrade = "After activating, the grade can be used to create new stories."; +$lang->custom->notice->gradeNotEmpty = 'There are stories in this grade, and it cannot be deleted!'; +$lang->custom->notice->deleteGrade = 'Are you sure you want to delete this grade?'; +$lang->custom->notice->gradeRule = 'Cross-lavel segmentation: Requirements can be created from any requirement system and support cross-system associated parent relationships. For example: you can create third-level requirements directly under first-level requirements.'; $lang->custom->notice->indexPage['product'] = "ZenTao 8.2+ has Product Homepage. Do you want to go to Product Homepage?"; $lang->custom->notice->indexPage['project'] = "ZenTao 8.2+ has {$lang->projectCommon} Home. Do you want to go to {$lang->projectCommon} Home?"; diff --git a/module/custom/lang/en.php b/module/custom/lang/en.php index 7076515793..1c6768cf9b 100644 --- a/module/custom/lang/en.php +++ b/module/custom/lang/en.php @@ -40,6 +40,7 @@ $lang->custom->setStoryConcept = "Set Story Concept"; $lang->custom->setDefaultConcept = "Set Default Concept"; $lang->custom->browseStoryConcept = "List of story concepts"; $lang->custom->deleteStoryConcept = "Delete story Concept"; +$lang->custom->ERConcept = "ER Concept"; $lang->custom->URConcept = "UR Concept"; $lang->custom->SRConcept = "SR Concept"; $lang->custom->reviewRule = 'Review Rules'; @@ -63,7 +64,14 @@ $lang->custom->setPercent = "Enable or Disable {$lang->stage->percent} $lang->custom->beginAndEndDate = 'Begin & End'; $lang->custom->beginAndEndDateRange = 'The Range Of Begin & End'; $lang->custom->limitTaskDateAction = 'Set start and end date required'; +$lang->custom->closeGrade = 'Close Story Grade'; +$lang->custom->activateGrade = 'Activate Story Grade'; +$lang->custom->deleteGrade = 'Delete Story Grade'; $lang->custom->closeSetting = 'Close Setting'; +$lang->custom->gradeRule = 'Allow cross-level segmentation'; + +$lang->custom->gradeRuleList['cross'] = 'Yes'; +$lang->custom->gradeRuleList['stepwise'] = 'No'; $lang->custom->unitList['efficiency'] = 'Working Hours/'; $lang->custom->unitList['manhour'] = 'Man-hour/'; @@ -95,6 +103,9 @@ $lang->custom->closedExecution = 'Closed ' . $lang->executionCommon; $lang->custom->closedKanban = 'Closed ' . $lang->custom->kanban; $lang->custom->closedProduct = 'Closed ' . $lang->productCommon; +$lang->custom->gradeStatusList['enable'] = 'Normal'; +$lang->custom->gradeStatusList['disable'] = 'Disabled'; + $lang->custom->block = new stdclass(); $lang->custom->block->fields['closed'] = 'Closed Block'; @@ -125,6 +136,9 @@ $lang->custom->story->fields['reviewRules'] = 'Review Rules'; $lang->custom->story->fields['reviewResultList'] = 'Review Result'; $lang->custom->story->fields['review'] = 'Need Review'; +$lang->custom->epic = clone $lang->custom->story; +$lang->custom->requirement = clone $lang->custom->story; + $lang->custom->task = new stdClass(); $lang->custom->task->fields['required'] = $lang->custom->required; $lang->custom->task->fields['priList'] = 'Priority'; @@ -214,6 +228,11 @@ $lang->custom->notice->selectAllTip = 'After selecting all people, the re $lang->custom->notice->repeatKey = 'Repeat Key %s'; $lang->custom->notice->readOnlyOfCode = "A code is a management term that exists for secrecy or as an antonym. When code management is enabled, the code information of {$lang->productCommon}, {$lang->projectCommon}, and execution in the system will be displayed in the creation, editing, detail, and list pages."; $lang->custom->notice->readOnlyOfPercent = "The \"Workload Ratio\" is used to divide the workload of a {$lang->projectCommon} into different stages. The sum of the percentages of the same level stages cannot exceed 100%. After enabling the \"Workload Ratio\", users have to fill in the ratio fields when setting up the stages in the Waterfall {$lang->projectCommon} and Waterfall Plus {$lang->projectCommon} management models."; +$lang->custom->notice->closeGrade = "After closing, the grade cannot be used to create new stories."; +$lang->custom->notice->activateGrade = "After activating, the grade can be used to create new stories."; +$lang->custom->notice->gradeNotEmpty = 'There are stories in this grade, and it cannot be deleted!'; +$lang->custom->notice->deleteGrade = 'Are you sure you want to delete this grade?'; +$lang->custom->notice->gradeRule = 'Cross-lavel segmentation: Requirements can be created from any requirement system and support cross-system associated parent relationships. For example: you can create third-level requirements directly under first-level requirements.'; $lang->custom->notice->indexPage['product'] = "ZenTao 8.2+ has Product Home. Do you want to go to Product Home?"; $lang->custom->notice->indexPage['project'] = "ZenTao 8.2+ has {$lang->projectCommon} Home. Do you want to go to {$lang->projectCommon} Home?"; diff --git a/module/custom/lang/fr.php b/module/custom/lang/fr.php index 0f266174a0..999c8ebc92 100644 --- a/module/custom/lang/fr.php +++ b/module/custom/lang/fr.php @@ -40,6 +40,7 @@ $lang->custom->setStoryConcept = "Set Story Concept"; $lang->custom->setDefaultConcept = "Set Default Concept"; $lang->custom->browseStoryConcept = "List of story concepts"; $lang->custom->deleteStoryConcept = "Delete story Concept"; +$lang->custom->ERConcept = "ER Concept"; $lang->custom->URConcept = "UR Concept"; $lang->custom->SRConcept = "SR Concept"; $lang->custom->reviewRule = 'Review Rules'; @@ -63,7 +64,14 @@ $lang->custom->setPercent = "Enable or Disable {$lang->stage->percent} $lang->custom->beginAndEndDate = 'Begin & End'; $lang->custom->beginAndEndDateRange = 'The Range Of Begin & End'; $lang->custom->limitTaskDateAction = 'Set start and end date required'; +$lang->custom->closeGrade = 'Close Story Grade'; +$lang->custom->activateGrade = 'Activate Story Grade'; +$lang->custom->deleteGrade = 'Delete Story Grade'; $lang->custom->closeSetting = 'Close Setting'; +$lang->custom->gradeRule = 'Allow cross-level segmentation'; + +$lang->custom->gradeRuleList['cross'] = 'Yes'; +$lang->custom->gradeRuleList['stepwise'] = 'No'; $lang->custom->unitList['efficiency'] = 'Working Hours/'; $lang->custom->unitList['manhour'] = 'Man-hour/'; @@ -95,6 +103,9 @@ $lang->custom->closedExecution = 'Closed ' . $lang->executionCommon; $lang->custom->closedKanban = 'Closed ' . $lang->custom->kanban; $lang->custom->closedProduct = 'Closed ' . $lang->productCommon; +$lang->custom->gradeStatusList['enable'] = 'Normal'; +$lang->custom->gradeStatusList['disable'] = 'Disabled'; + $lang->custom->block = new stdclass(); $lang->custom->block->fields['closed'] = 'Bloc Fermé'; @@ -125,6 +136,9 @@ $lang->custom->story->fields['reviewRules'] = 'Review Rules'; $lang->custom->story->fields['reviewResultList'] = 'Valider Résultats'; $lang->custom->story->fields['review'] = 'Validation Requise'; +$lang->custom->epic = clone $lang->custom->story; +$lang->custom->requirement = clone $lang->custom->story; + $lang->custom->task = new stdClass(); $lang->custom->task->fields['required'] = $lang->custom->required; $lang->custom->task->fields['priList'] = 'Priorité'; @@ -214,6 +228,11 @@ $lang->custom->notice->selectAllTip = 'After selecting all people, the re $lang->custom->notice->repeatKey = 'Repeat Key %s'; $lang->custom->notice->readOnlyOfCode = "A code is a management term that exists for secrecy or as an antonym. When code management is enabled, the code information of {$lang->productCommon}, {$lang->projectCommon}, and execution in the system will be displayed in the creation, editing, detail, and list pages."; $lang->custom->notice->readOnlyOfPercent = "The \"Workload Ratio\" is used to divide the workload of a {$lang->projectCommon} into different stages. The sum of the percentages of the same level stages cannot exceed 100%. After enabling the \"Workload Ratio\", users have to fill in the ratio fields when setting up the stages in the Waterfall {$lang->projectCommon} and Waterfall Plus {$lang->projectCommon} management models."; +$lang->custom->notice->closeGrade = "After closing, the grade cannot be used to create new stories."; +$lang->custom->notice->activateGrade = "After activating, the grade can be used to create new stories."; +$lang->custom->notice->gradeNotEmpty = 'There are stories in this grade, and it cannot be deleted!'; +$lang->custom->notice->deleteGrade = 'Are you sure you want to delete this grade?'; +$lang->custom->notice->gradeRule = 'Cross-lavel segmentation: Requirements can be created from any requirement system and support cross-system associated parent relationships. For example: you can create third-level requirements directly under first-level requirements.'; $lang->custom->notice->indexPage['product'] = "ZenTao 8.2+ possède une page d'accueil. Voulez-vous consulter la page d'accueil du produit ?"; $lang->custom->notice->indexPage['project'] = "ZenTao 8.2+ has {$lang->projectCommon} Home. Do you want to go to {$lang->projectCommon} Home?"; diff --git a/module/custom/lang/zh-cn.php b/module/custom/lang/zh-cn.php index 40a45a211a..b526914fb1 100644 --- a/module/custom/lang/zh-cn.php +++ b/module/custom/lang/zh-cn.php @@ -40,6 +40,7 @@ $lang->custom->setStoryConcept = "设置需求概念"; $lang->custom->setDefaultConcept = "设置默认概念"; $lang->custom->browseStoryConcept = "需求概念列表"; $lang->custom->deleteStoryConcept = "删除需求概念"; +$lang->custom->ERConcept = "业需概念"; $lang->custom->URConcept = "用需概念"; $lang->custom->SRConcept = "软需概念"; $lang->custom->reviewRule = "评审规则"; @@ -63,7 +64,14 @@ $lang->custom->setPercent = "是否启用{$lang->stage->percent}"; $lang->custom->beginAndEndDate = '起止日期'; $lang->custom->beginAndEndDateRange = '起止日期范围'; $lang->custom->limitTaskDateAction = '设置起止日期必填'; +$lang->custom->closeGrade = '停用需求层级'; +$lang->custom->activateGrade = '启用需求层级'; +$lang->custom->deleteGrade = '删除需求层级'; $lang->custom->closeSetting = '关闭设置'; +$lang->custom->gradeRule = '是否允许跨层级细分'; + +$lang->custom->gradeRuleList['cross'] = '是'; +$lang->custom->gradeRuleList['stepwise'] = '否'; $lang->custom->unitList['efficiency'] = '工时/'; $lang->custom->unitList['manhour'] = '人时/'; @@ -95,6 +103,9 @@ $lang->custom->closedExecution = '已关闭' . $lang->custom->executionCommon; $lang->custom->closedKanban = '已关闭' . $lang->custom->kanban; $lang->custom->closedProduct = '已关闭' . $lang->productCommon; +$lang->custom->gradeStatusList['enable'] = '正常'; +$lang->custom->gradeStatusList['disable'] = '停用'; + $lang->custom->block = new stdclass(); $lang->custom->block->fields['closed'] = '关闭的区块'; @@ -125,6 +136,9 @@ $lang->custom->story->fields['reviewRules'] = '评审规则'; $lang->custom->story->fields['reviewResultList'] = '评审结果'; $lang->custom->story->fields['review'] = '评审流程'; +$lang->custom->epic = clone $lang->custom->story; +$lang->custom->requirement = clone $lang->custom->story; + $lang->custom->task = new stdClass(); $lang->custom->task->fields['required'] = $lang->custom->required; $lang->custom->task->fields['priList'] = '优先级'; @@ -214,6 +228,11 @@ $lang->custom->notice->selectAllTip = '勾选所有人员后,会清空 $lang->custom->notice->repeatKey = '%s键重复'; $lang->custom->notice->readOnlyOfCode = "代号是一种管理话术,主要便于保密或作为别名存在。启用代号管理后,系统中的{$lang->productCommon}、{$lang->projectCommon}、执行在创建、编辑、详情、列表等页面均会展示代号信息。"; $lang->custom->notice->readOnlyOfPercent = "工作量占比用于划分{$lang->projectCommon}中存在多个阶段时的工作量的占比,同一级阶段的百分比之和最高为100%。启用工作量占比后,系统中的瀑布{$lang->projectCommon}和融合瀑布{$lang->projectCommon}模型中设置阶段时需要维护阶段的工作量占比。"; +$lang->custom->notice->closeGrade = "停用后,该层级无法再创建新需求。"; +$lang->custom->notice->activateGrade = "启用后,该层级可以创建新需求。"; +$lang->custom->notice->gradeNotEmpty = '系统内存在该层级的需求,不能删除!'; +$lang->custom->notice->deleteGrade = '层级删除后无法恢复!'; +$lang->custom->notice->gradeRule = '跨层细分:可从任意需求层级创建需求,并支持跨层级关联父子关系。例如:您可以在第一层需求下直接创建第三层需求。'; $lang->custom->notice->indexPage['product'] = "从8.2版本起增加了产品主页视图,是否默认进入产品主页?"; $lang->custom->notice->indexPage['project'] = "从8.2版本起增加了{$lang->projectCommon}主页视图,是否默认进入{$lang->projectCommon}主页?"; diff --git a/module/custom/model.php b/module/custom/model.php index fd231b669f..1bc0e9ff86 100644 --- a/module/custom/model.php +++ b/module/custom/model.php @@ -567,6 +567,7 @@ class customModel extends model foreach(explode(',', $fieldList) as $fieldName) { if($moduleName == 'user' && $method == 'edit' && strpos($this->config->user->contactField, $fieldName) === false) continue; + if($moduleName == 'project' && $fieldName == 'budget' && $this->config->vision == 'lite') continue; if($fieldName == 'comment') $fields[$fieldName] = $this->lang->comment; if(isset($moduleLang->{$fieldName}) && is_string($moduleLang->{$fieldName})) $fields[$fieldName] = $moduleLang->$fieldName; @@ -633,7 +634,9 @@ class customModel extends model foreach($langData as $content) { $value = json_decode($content->value); - $URSRPairs[$content->key] = $this->config->URAndSR ? $value->URName . '/' . $value->SRName : $value->SRName; + if(!$this->config->URAndSR && !$this->config->enableER) $URSRPairs[$content->key] = $value->SRName; + if($this->config->URAndSR && !$this->config->enableER) $URSRPairs[$content->key] = $value->URName . '/' . $value->SRName; + if($this->config->URAndSR && $this->config->enableER) $URSRPairs[$content->key] = $value->ERName . '/' . $value->URName . '/' . $value->SRName; } return $URSRPairs; @@ -721,6 +724,7 @@ class customModel extends model $value = json_decode($URSRData->value); $URSRList[$URSRData->key] = new stdclass(); $URSRList[$URSRData->key]->key = $URSRData->key; + $URSRList[$URSRData->key]->ERName = $value->ERName; $URSRList[$URSRData->key]->SRName = $value->SRName; $URSRList[$URSRData->key]->URName = $value->URName; $URSRList[$URSRData->key]->system = $URSRData->system; @@ -832,11 +836,13 @@ class customModel extends model /* If has custom UR and SR name. */ foreach($data['SRName'] as $key => $SRName) { - if(isset($data['URName'])) $URName = zget($data['URName'], $key, ''); - if(!isset($data['URName'])) $URName = $this->lang->URCommon; - if(!$URName || !$SRName) continue; + $URName = zget($data['URName'], $key, $this->lang->URCommon); + $ERName = zget($data['ERName'], $key, $this->lang->ERCommon); + + if(!$URName || !$SRName || !$ERName) continue; $URSRList = new stdclass(); + $URSRList->ERName = $ERName; $URSRList->SRName = $SRName; $URSRList->URName = $URName; @@ -862,7 +868,7 @@ class customModel extends model public function updateURAndSR(int $key = 0, string $lang = '', array $data = array()): bool { if(empty($lang)) $lang = $this->app->getClientLang(); - if(empty($data['SRName']) || empty($data['URName'])) return false; + if(empty($data['SRName']) || empty($data['URName']) || empty($data['ERName'])) return false; $oldValue = $this->getURSRConcept($key, $lang); $oldValue = json_decode($oldValue); @@ -870,8 +876,10 @@ class customModel extends model if(!$oldValue) return false; $URSRList = new stdclass(); + $URSRList->defaultERName = zget($oldValue, 'defaultERName', $oldValue->ERName); $URSRList->defaultSRName = zget($oldValue, 'defaultSRName', $oldValue->SRName); $URSRList->defaultURName = zget($oldValue, 'defaultURName', $oldValue->URName); + $URSRList->ERName = empty($data['ERName']) ? $URSRList->defaultERName : $data['ERName']; $URSRList->SRName = empty($data['SRName']) ? $URSRList->defaultSRName : $data['SRName']; $URSRList->URName = empty($data['URName']) ? $URSRList->defaultURName : $data['URName']; @@ -1031,9 +1039,25 @@ class customModel extends model $URAndSR = strpos(",$disabledFeatures,", ',productUR,') === false ? '1' : '0'; $this->setting->setItem('system.custom.URAndSR', $URAndSR); + $disableER = strpos(",$disabledFeatures,", ',productER,') !== false; + $enableER = (!$URAndSR || $disableER) ? '0' : '1'; + $this->setting->setItem('system.custom.enableER', $enableER); + $this->processMeasrecordCron(); } + /** + * 检查系统中是否有业务需求数据。 + * Check whether there is epic data in the system. + * + * @access public + * @return int + */ + public function hasProductERData(): int + { + return (int)$this->dao->select('*')->from(TABLE_STORY)->where('type')->eq('epic')->andWhere('deleted')->eq('0')->count(); + } + /** * 检查系统中是否有用户需求数据。 * Check whether there is requirement data in the system. diff --git a/module/custom/test/model/setstorygrade.php b/module/custom/test/model/setstorygrade.php new file mode 100755 index 0000000000..24248c3b7b --- /dev/null +++ b/module/custom/test/model/setstorygrade.php @@ -0,0 +1,48 @@ +#!/usr/bin/env php +setStoryGrade(); +timeout=0 +cid=1 + +- 查看插入的等级数量 @5 +- 查看插入的等级1详情 + - 第0条的grade属性 @1 + - 第0条的name属性 @一级 + - 第0条的status属性 @enable +- 查看插入的等级5详情 + - 第4条的grade属性 @5 + - 第4条的name属性 @五级 + - 第4条的status属性 @enable + +*/ + +$data['grade'] = array( + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + 4 => 5 +); + +$data['gradeName'] = array( + 0 => '一级', + 1 => '二级', + 2 => '三级', + 3 => '四级', + 4 => '五级' +); + +global $tester; +$tester->loadModel('custom')->setStoryGrade('story', $data); +$gradeSetting = $tester->loadModel('story')->getGradeList(); + +r(count($gradeSetting)) && p() && e('5'); // 查看插入的等级数量 +r($gradeSetting) && p('0:grade,name,status') && e('1,一级,enable'); // 查看插入的等级1详情 +r($gradeSetting) && p('4:grade,name,status') && e('5,五级,enable'); // 查看插入的等级5详情 diff --git a/module/custom/ui/editstoryconcept.html.php b/module/custom/ui/editstoryconcept.html.php index c3b9cb30cd..64b44374dd 100644 --- a/module/custom/ui/editstoryconcept.html.php +++ b/module/custom/ui/editstoryconcept.html.php @@ -16,10 +16,20 @@ formPanel set::title($lang->custom->editStoryConcept), formRow ( + $config->enableER ? formGroup + ( + set::width('1/3'), + setClass('justify-center'), + span + ( + setClass('text-md font-bold'), + $lang->custom->ERConcept + ) + ) : null, $config->URAndSR ? formGroup ( - set::width('1/2'), - setClass('justify-center mr-8'), + set::width('1/3'), + setClass('justify-center'), span ( setClass('text-md font-bold'), @@ -28,7 +38,7 @@ formPanel ) : null, formGroup ( - set::width('1/2'), + set::width('1/3'), setClass('justify-center'), span ( @@ -41,15 +51,23 @@ formPanel ( formGroup ( - setClass('mr-8'), + set::width('1/3'), + setClass('mr-2'), + set::name('ERName'), + set::value($URSR->ERName), + setClass(!$this->config->enableER ? 'hidden' : '') + ), + formGroup + ( + setClass('mr-2'), setClass(!$this->config->URAndSR ? 'hidden' : ''), - set::width('1/2'), + set::width('1/3'), set::name('URName'), set::value($URSR->URName) ), formGroup ( - set::width('1/2'), + set::width('1/3'), set::name('SRName'), set::value($URSR->SRName) ) diff --git a/module/custom/ui/set.html.php b/module/custom/ui/set.html.php index aa6763b76c..84be00aaf1 100644 --- a/module/custom/ui/set.html.php +++ b/module/custom/ui/set.html.php @@ -49,7 +49,7 @@ if($module == 'project' && $field == 'unitList') ); $actionWidth = 'w-full'; } -elseif(in_array($module, array('story', 'demand')) && $field == 'reviewRules') +elseif(in_array($module, array('story', 'requirement', 'epic', 'demand')) && $field == 'reviewRules') { $formItems[] = formGroup ( @@ -71,7 +71,24 @@ elseif(in_array($module, array('story', 'demand')) && $field == 'reviewRules') set::items($users) ); } -elseif(in_array($module, array('story', 'demand', 'testcase')) && $field == 'review') +elseif(in_array($module, array('story', 'requirement', 'epic')) && $field == 'gradeRule') +{ + $formItems[] = formGroup + ( + set::width('1/2'), + set::label($lang->custom->gradeRule), + set::name('gradeRule'), + set::value($gradeRule), + set::control('radioListInline'), + set::items($lang->custom->gradeRuleList) + ); + $formItems[] = div + ( + setClass('gradeRuleNotice'), + div($lang->custom->notice->gradeRule) + ); +} +elseif(in_array($module, array('epic', 'story', 'requirement', 'demand', 'testcase')) && $field == 'review') { $formItems[] = formGroup ( @@ -84,7 +101,7 @@ elseif(in_array($module, array('story', 'demand', 'testcase')) && $field == 'rev on::change('changeReview') ); - if($module == 'story' || $module == 'demand') + if(in_array($module, array('story', 'requirement', 'epic', 'demand'))) { $formItems[] = formRow ( @@ -540,35 +557,103 @@ else } } -div -( - setID('mainContent'), - setClass('row has-sidebar-left'), - isset($sidebarMenu) ? $sidebarMenu : null, - formPanel - ( - set::formID('settingForm'), - set::headingClass('justify-start'), - setClass('flex-auto'), - setClass(!empty($sidebarMenu) ? 'ml-0.5' : null), - set::actionsClass($actionWidth), - set::actions($formActions), - div - ( - setClass('flex items-center'), - span - ( - setClass('text-md font-bold'), - $lang->custom->$module->fields[$field] - ), - span - ( - setClass('ml-2'), - $headingTips +if(in_array($module, array('story', 'requirement', 'epic')) && $field == 'grade') +{ + jsVar('enableLang', $lang->custom->gradeStatusList['enable']); + $tbody = array(); + foreach($storyGrades as $grade) + { + $items = array(); + $hidden = ($grade->grade == 1) ? 'hidden' : ''; + $hideAdd = (end($storyGrades) == $grade) ? '' : 'hidden'; + $items[] = array('icon' => 'plus', 'class' => "btn ghost btn-add-grade $hideAdd"); + if($grade->status == 'enable' && common::hasPriv('custom', 'closeGrade')) $items[] = array('icon' => 'off', 'class' => "btn ghost btn-close ajax-submit $hidden", 'url' => inlink('closeGrade', "type={$module}&id={$grade->grade}"), 'data-confirm' => $lang->custom->notice->closeGrade); + if($grade->status == 'disable' && common::hasPriv('custom', 'activateGrade')) $items[] = array('icon' => 'magic', 'class' => "btn ghost btn-active ajax-submit", 'url' => inlink('activateGrade', "type={$module}&id={$grade->grade}"), 'data-confirm' => $lang->custom->notice->activateGrade); + if(common::hasPriv('custom', 'deleteGrade')) $items[] = array('icon' => 'trash', 'class' => "btn ghost btn-delete-grade ajax-submit $hidden", 'url' => inlink('deleteGrade', "type={$module}&id={$grade->grade}")); + + $tbody[] = h::tr( + formHidden('grade[]', $grade->grade), + h::td($grade->grade, set::width('100px'), setClass('index')), + h::td(input(set::name('gradeName[]'), set::value($grade->name))), + h::td(zget($lang->custom->gradeStatusList, $grade->status), set::width('80px'), setClass('grade-status')), + h::td( + set::width('100px'), + btnGroup + ( + set::items($items) + ) ) - ), - $formItems - ) -); + ); + } + + div + ( + setID('mainContent'), + setClass('row has-sidebar-left'), + $sidebarMenu, + formPanel( + set::title($lang->custom->story->fields['grade']), + setClass('ml-0.5'), + on::click('.btn-add-grade', 'addGrade'), + set::actions(array()), + h::table( + setID('gradeList'), + setClass('table table-form borderless'), + h::tr( + h::td($lang->story->grade), + h::td($lang->story->gradeName, setClass('required')), + h::td($lang->story->statusAB), + h::td($lang->actions) + ), + $tbody, + h::tfoot( + h::tr( + h::td(), + h::td( + btn( + $lang->save, + setClass('primary btn-wide'), + set::btnType('submit') + ) + ) + ) + ) + ) + ) + ); +} +else +{ + div + ( + setID('mainContent'), + setClass('row has-sidebar-left'), + isset($sidebarMenu) ? $sidebarMenu : null, + formPanel + ( + set::formID('settingForm'), + set::headingClass('justify-start'), + setClass('flex-auto'), + setClass(!empty($sidebarMenu) ? 'ml-0.5' : null), + set::actionsClass($actionWidth), + set::actions($formActions), + div + ( + setClass('flex items-center'), + span + ( + setClass('text-md font-bold'), + $lang->custom->$module->fields[$field] + ), + span + ( + setClass('ml-2'), + $headingTips + ) + ), + $formItems + ) + ); +} render(); diff --git a/module/custom/ui/setstoryconcept.html.php b/module/custom/ui/setstoryconcept.html.php index caf5eae1e8..c2776f30ca 100644 --- a/module/custom/ui/setstoryconcept.html.php +++ b/module/custom/ui/setstoryconcept.html.php @@ -16,6 +16,12 @@ formBatchPanel set::title($lang->custom->setStoryConcept), set::minRows(1), set::maxRows(1), + $config->enableER ? formBatchItem + ( + set::width('1/3'), + set::label($lang->custom->ERConcept), + set::name('ERName') + ) : null, $config->URAndSR ? formBatchItem ( set::width('1/3'), diff --git a/module/custom/zen.php b/module/custom/zen.php index 2005a61195..4a453f888e 100644 --- a/module/custom/zen.php +++ b/module/custom/zen.php @@ -57,6 +57,7 @@ class customZen extends custom } $this->view->fieldList = $fieldList; $this->view->dbFields = $dbFields; + $this->view->lang2Set = str_replace('_', '-', $lang); } /** @@ -80,18 +81,27 @@ class customZen extends custom $this->view->unitList = explode(',', $unitList); $this->view->defaultCurrency = zget($this->config->$module, 'defaultCurrency', 'CNY'); } - if(in_array($module, array('story', 'demand')) && $field == 'reviewRules') + if(in_array($module, array('story', 'demand', 'requirement', 'epic')) && $field == 'reviewRules') { $this->app->loadConfig($module); $this->view->reviewRule = zget($this->config->$module, 'reviewRules', 'allpass'); $this->view->users = $this->loadModel('user')->getPairs('noclosed|nodeleted'); $this->view->superReviewers = zget($this->config->$module, 'superReviewers', ''); } - if(in_array($module, array('story', 'testcase', 'demand')) && $field == 'review') + if(in_array($module, array('story', 'requirement', 'epic')) && $field == 'gradeRule') + { + $this->app->loadConfig($module); + $this->view->gradeRule = zget($this->config->$module, 'gradeRule', ''); + } + if(in_array($module, array('story', 'requirement', 'epic')) && $field == 'grade') + { + $this->view->storyGrades = $this->loadModel('story')->getGradeList($module); + } + if(in_array($module, array('story', 'testcase', 'demand', 'requirement', 'epic')) && $field == 'review') { $this->app->loadConfig($module); $this->loadModel('user'); - if(in_array($module, array('story', 'demand'))) + if(in_array($module, array('story', 'requirement', 'demand', 'epic'))) { $this->view->depts = $this->loadModel('dept')->getDeptPairs(); $this->view->forceReviewRoles = zget($this->config->$module, 'forceReviewRoles', ''); @@ -142,11 +152,19 @@ class customZen extends custom $data['unitList'] = implode(',', $data['unitList']); $this->loadModel('setting')->setItems("system.$module", $data); } - elseif(($module == 'story' || $module == 'demand') && $field == 'review') + elseif(in_array($module, array('story', 'demand', 'requirement', 'epic')) && $field == 'review') { $this->setStoryReview($module, $data); } - elseif(($module == 'story' || $module == 'demand') && $field == 'reviewRules') + elseif(in_array($module, array('story', 'requirement', 'epic')) && $field == 'gradeRule') + { + $this->setGradeRule($module, $data); + } + elseif(in_array($module, array('story', 'requirement', 'epic')) && $field == 'grade') + { + $this->custom->setStoryGrade($module, $data); + } + elseif(in_array($module, array('story', 'demand', 'requirement', 'epic')) && $field == 'reviewRules') { if(!isset($data['superReviewers'])) $data['superReviewers'] = array(); $data['superReviewers'] = implode(',', $data['superReviewers']); @@ -331,6 +349,21 @@ class customZen extends custom return !dao::isError(); } + /** + * 设置需求细分流程。 + * Set the rule of story grade. + * + * @param string $module + * @param array $data + * @access protected + * @return bool + */ + public function setGradeRule(string $module, array $data): bool + { + $this->loadModel('setting')->setItems("system.{$module}", $data); + return !dao::isError(); + } + /** * 设置用例评审规则。 * Set testcase review rules. diff --git a/module/datatable/control.php b/module/datatable/control.php index 2e49ab984e..804bf4709c 100644 --- a/module/datatable/control.php +++ b/module/datatable/control.php @@ -82,6 +82,8 @@ class datatable extends control if($account == 'guest') return $this->send(array('result' => 'fail', 'message' => 'guest.')); $rawModule = zget($this->config->datatable->moduleAlias, "$module-$method", $module); + if($rawModule == 'story' && $extra && $rawModule != $extra) $rawModule = $extra; + $fieldList = $this->datatable->getFieldList($rawModule, $method); $postFields = json_decode($this->post->fields); @@ -102,7 +104,7 @@ class datatable extends control $value = json_encode($fields); /* Split story and requirement custom fields. */ - if(strpos(',product-browse,execution-story,', ",$module-$method,") !== false && strpos(',story,requirement,', ",$extra,") !== false) $name = 'datatable.' . $module . ucfirst($method) . ucfirst($extra) . '.cols'; + if(("$module-$method" == 'product-browse') && in_array($extra, array('story', 'requirement', 'epic'))) $name = 'datatable.' . $module . ucfirst($method) . ucfirst($extra) . '.cols'; /* 保存个人配置信息。 */ $this->loadModel('setting')->setItem($account . '.' . $name, $value); @@ -144,12 +146,13 @@ class datatable extends control if(zget($this->config->datatable->moduleAlias, "$module-$method", $module) == 'story') { unset($cols['product'], $cols['module']); - if($extra != 'requirement') unset($cols['SRS']); - if($extra == 'requirement') + if($extra != 'story') { - foreach(array('plan', 'stage', 'taskCount', 'bugCount', 'caseCount', 'URS') as $field) unset($cols[$field]); - $cols['title']['title'] = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->title); + foreach(array('taskCount', 'bugCount', 'caseCount') as $field) unset($cols[$field]); + $cols['title']['title'] = $cols['title']['title'] = $this->lang->story->name; } + + if($this->app->tab != 'product') $cols['title']['title'] = $this->lang->story->name; } if(($module == 'productplan' && $method == 'browse') || ($module == 'project' && $method == 'bug')) diff --git a/module/datatable/model.php b/module/datatable/model.php index 42799392ae..4adf8b3bc2 100644 --- a/module/datatable/model.php +++ b/module/datatable/model.php @@ -84,10 +84,11 @@ class datatableModel extends model if(!$method) $method = $this->app->getMethodName(); $datatableId = $module . ucfirst($method); - /* Split story and requirement custom fields. */ - if(strpos(',product-browse,execution-story,', ",$module-$method,") !== false && strpos(',story,requirement,', $extra) !== false) $datatableId .= ucfirst($extra); + /* Split story,requirement and epic custom fields. */ + if(("$module-$method" == 'product-browse') && in_array($extra, array('story', 'requirement', 'epic'))) $datatableId .= ucfirst($extra); $module = zget($this->config->datatable->moduleAlias, "$module-$method", $module); + if($module == 'story' && $extra && $module != $extra) $module = $extra; if(!isset($this->config->$module)) $this->loadModel($module); if(isset($this->config->datatable->$datatableId->cols)) $setting = json_decode($this->config->datatable->$datatableId->cols, true); diff --git a/module/design/control.php b/module/design/control.php index 62e6f15590..3f0ad3b98d 100755 --- a/module/design/control.php +++ b/module/design/control.php @@ -88,7 +88,7 @@ class design extends control $products = $this->product->getProductPairsByProject($projectID); $productIdList = $productID ? $productID : array_keys($products); - $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'story', false); + $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'full'); $queryID = $type == 'bySearch' ? $param : 0; /* Build Search Form. */ @@ -150,10 +150,11 @@ class design extends control $products = $this->product->getProductPairsByProject($projectID); $productIdList = $productID ? $productID : array_keys($products); + $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'full'); $this->view->title = $this->lang->design->common . $this->lang->hyphen . $this->lang->design->create; $this->view->users = $this->loadModel('user')->getPairs('noclosed'); - $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'story', false); + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->productID = $productID; $this->view->projectID = $projectID; $this->view->type = $type; @@ -187,11 +188,12 @@ class design extends control $products = $this->product->getProductPairsByProject($projectID); $productIdList = $productID ? $productID : array_keys($products); + $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'full'); $project = $this->loadModel('project')->getByID($projectID); $this->view->title = $this->lang->design->common . $this->lang->hyphen . $this->lang->design->batchCreate; - $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productIdList); + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->users = $this->loadModel('user')->getPairs('noclosed'); $this->view->type = $type; $this->view->typeList = $project->model == 'waterfall' ? $this->lang->design->typeList : $this->lang->design->plusTypeList; @@ -222,7 +224,7 @@ class design extends control $this->view->title = $this->lang->design->common . $this->lang->hyphen . $this->lang->design->view; $this->view->design = $design; - $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productIdList); + $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'full'); $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->actions = $this->loadModel('action')->getList('design', $design->id); $this->view->repos = $this->loadModel('repo')->getRepoPairs('project', $design->project); @@ -265,11 +267,12 @@ class design extends control $products = $this->product->getProductPairsByProject($design->project); $productIdList = $design->product ? $design->product : array_keys($products); $project = $this->loadModel('project')->getByID($design->project); + $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, 'active', 'id_desc', 0, 'full', 'full'); $this->view->title = $this->lang->design->common . $this->lang->hyphen . $this->lang->design->edit; $this->view->design = $design; $this->view->project = $project; - $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productIdList); + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->users = $this->loadModel('user')->getPairs('noclosed|noletter'); $this->view->typeList = $project->model == 'waterfall' ? $this->lang->design->typeList : $this->lang->design->plusTypeList; @@ -441,15 +444,12 @@ class design extends control */ public function ajaxGetProductStories(int $productID, int $projectID, string $status = 'all', string $hasParent = 'true') { + $hasParent = $hasParent == 'true'; $products = $this->product->getProductPairsByProject($projectID); $productIdList = $productID ? $productID : array_keys($products); - $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, $status, 'id_desc', 0, 'full', 'story', $hasParent); + $stories = $this->loadModel('story')->getProductStoryPairs($productIdList, 'all', 0, $status, 'id_desc', 0, 'full', 'full', $hasParent); - $items = array(); - foreach($stories as $storyID => $storyTitle) - { - $items[] = array('value' => $storyID, 'text' => $storyTitle, 'keys' => $storyTitle); - } + $items = $this->story->addGradeLabel($stories); return print(json_encode($items)); } diff --git a/module/design/js/common.ui.js b/module/design/js/common.ui.js index 5bcb246f8f..3a7a0a777a 100644 --- a/module/design/js/common.ui.js +++ b/module/design/js/common.ui.js @@ -9,7 +9,7 @@ function loadStory(e) { const productID = $(e.target).val(); const storyID = $('input[name=story]').val(); - const link = $.createLink('design', 'ajaxGetProductStories', 'productID=' + productID + '&projectID=' + projectID + '&status=active&hasParent=false'); + const link = $.createLink('design', 'ajaxGetProductStories', 'productID=' + productID + '&projectID=' + projectID + '&status=active&hasParent=true'); $.getJSON(link, function(data) { const $storyPicker = $('input[name=story]').zui('picker'); diff --git a/module/design/model.php b/module/design/model.php index e00e04a2b6..5c8a095572 100755 --- a/module/design/model.php +++ b/module/design/model.php @@ -253,6 +253,8 @@ class designModel extends model $design->commit .= html::a(helper::createLink('design', 'revision', "revisionID={$relation->BID}&projectID={$design->project}"), "# {$revision}", '', "title='{$revision}' class='flex clip'"); } + if($design->story > 0) $design->storyInfo = $this->loadModel('story')->fetchByID((int)$design->story); + return $this->loadModel('file')->replaceImgURL($design, 'desc'); } diff --git a/module/design/ui/view.html.php b/module/design/ui/view.html.php index 4adce5e492..6090fdd51b 100644 --- a/module/design/ui/view.html.php +++ b/module/design/ui/view.html.php @@ -40,7 +40,7 @@ foreach($config->design->view->operateList['common'] as $operate) $commonActions[] = $config->design->actionList[$operate]; } -$moduleName = empty($project->hasProduct) ? 'projectstory' : 'story'; +$moduleName = empty($project->hasProduct) ? 'projectstory' : (isset($design->storyInfo) ? $design->storyInfo->type : 'story'); $storyName = zget($stories, $design->story, ''); if($common::hasPriv($moduleName, 'view')) { diff --git a/module/dev/control.php b/module/dev/control.php index 0dd891c2fc..31699c46b7 100644 --- a/module/dev/control.php +++ b/module/dev/control.php @@ -191,7 +191,7 @@ class dev extends control } $key = ''; - if($type == 'common') $key = '&key=projectCommon,productCommon,executionCommon'; + if($type == 'common') $key = '&key=projectCommon,productCommon,executionCommon,ERCommon,URCommon,SRCommon'; $this->loadModel('custom')->deleteItems("lang={$language}&module={$module}&vision={$this->config->vision}{$section}{$key}"); if($this->config->vision == 'rnd' and $type == 'common' and $this->config->custom->URSR) @@ -201,6 +201,7 @@ class dev extends control { $oldValue = json_decode($oldValue); $setting = array( + 'ERName' => zget($oldValue, 'defaultERName', $oldValue->ERName), 'SRName' => zget($oldValue, 'defaultSRName', $oldValue->SRName), 'URName' => zget($oldValue, 'defaultURName', $oldValue->URName) ); diff --git a/module/dev/lang/de.php b/module/dev/lang/de.php index 163a8499a1..682f94a0d7 100644 --- a/module/dev/lang/de.php +++ b/module/dev/lang/de.php @@ -25,7 +25,8 @@ $lang->dev->language = 'Language: %s'; $lang->dev->default = 'Default'; $lang->dev->currentLang = 'Current Language'; $lang->dev->change = 'Change'; -$lang->dev->UR = 'Epic'; +$lang->dev->ER = 'Epic'; +$lang->dev->UR = 'Feature'; $lang->dev->SR = 'Story'; $lang->dev->branch = 'Branch'; diff --git a/module/dev/lang/en.php b/module/dev/lang/en.php index 40ee0e08e7..80bd715375 100644 --- a/module/dev/lang/en.php +++ b/module/dev/lang/en.php @@ -25,7 +25,8 @@ $lang->dev->language = 'Language: %s'; $lang->dev->default = 'Default'; $lang->dev->currentLang = 'Current Language'; $lang->dev->change = 'Change'; -$lang->dev->UR = 'Epic'; +$lang->dev->ER = 'Epic'; +$lang->dev->UR = 'Feature'; $lang->dev->SR = 'Story'; $lang->dev->branch = 'Branch'; diff --git a/module/dev/lang/fr.php b/module/dev/lang/fr.php index 40ee0e08e7..80bd715375 100644 --- a/module/dev/lang/fr.php +++ b/module/dev/lang/fr.php @@ -25,7 +25,8 @@ $lang->dev->language = 'Language: %s'; $lang->dev->default = 'Default'; $lang->dev->currentLang = 'Current Language'; $lang->dev->change = 'Change'; -$lang->dev->UR = 'Epic'; +$lang->dev->ER = 'Epic'; +$lang->dev->UR = 'Feature'; $lang->dev->SR = 'Story'; $lang->dev->branch = 'Branch'; diff --git a/module/dev/lang/zh-cn.php b/module/dev/lang/zh-cn.php index 93afb2ffa2..5438129a72 100644 --- a/module/dev/lang/zh-cn.php +++ b/module/dev/lang/zh-cn.php @@ -25,6 +25,7 @@ $lang->dev->language = '语言:%s'; $lang->dev->default = '默认值'; $lang->dev->currentLang = '当前语言'; $lang->dev->change = '修改值'; +$lang->dev->ER = '业务需求'; $lang->dev->UR = '用户需求'; $lang->dev->SR = '软件需求'; $lang->dev->branch = '平台/分支'; diff --git a/module/dev/model.php b/module/dev/model.php index 7f1d424dbd..462cfc8d2b 100644 --- a/module/dev/model.php +++ b/module/dev/model.php @@ -410,6 +410,7 @@ class devModel extends model $originalLangs['productCommon'] = $this->config->productCommonList[$language][PRODUCT_KEY]; $originalLangs['projectCommon'] = $this->config->projectCommonList[$language][PROJECT_KEY]; $originalLangs['executionCommon'] = $this->config->executionCommonList[$language][$projectKey]; + $originalLangs['ERCommon'] = $this->lang->dev->ER; $originalLangs['URCommon'] = $this->lang->dev->UR; $originalLangs['SRCommon'] = $this->lang->dev->SR; @@ -419,10 +420,12 @@ class devModel extends model if($URSRList) { $URSRList = json_decode($URSRList->value); + $originalLangs['ERCommon'] = isset($URSRList->defaultERName) ? $URSRList->defaultERName : $URSRList->ERName; $originalLangs['URCommon'] = isset($URSRList->defaultURName) ? $URSRList->defaultURName : $URSRList->URName; $originalLangs['SRCommon'] = isset($URSRList->defaultSRName) ? $URSRList->defaultSRName : $URSRList->SRName; } - if(!$this->config->URAndSR) unset($originalLangs['URCommon']); + if(!$this->config->URAndSR) unset($originalLangs['URCommon']); + if(!$this->config->enableER) unset($originalLangs['ERCommon']); } else { @@ -507,12 +510,15 @@ class devModel extends model if($URSRList) { $URSRList = json_decode($URSRList->value); + $defaultERName = isset($URSRList->defaultERName) ? $URSRList->defaultERName : $URSRList->ERName; $defaultURName = isset($URSRList->defaultURName) ? $URSRList->defaultURName : $URSRList->URName; $defaultSRName = isset($URSRList->defaultSRName) ? $URSRList->defaultSRName : $URSRList->SRName; + $customedLangs['ERCommon'] = $defaultERName == $URSRList->ERName ? '' : $URSRList->ERName; $customedLangs['URCommon'] = $defaultURName == $URSRList->URName ? '' : $URSRList->URName; $customedLangs['SRCommon'] = $defaultSRName == $URSRList->SRName ? '' : $URSRList->SRName; } - if(!$this->config->URAndSR) unset($customedLangs['URCommon']); + if(!$this->config->enableER) unset($customedLangs['ERCommon']); + if(!$this->config->URAndSR) unset($customedLangs['URCommon']); break; case 'first': $customeds = $this->loadModel('custom')->getItems("lang={$language}&module=common§ion=mainNav&vision={$this->config->vision}"); @@ -591,6 +597,7 @@ class devModel extends model if(empty($langFilesToLoad)) return false; $lang = $module == 'common' ? new language() : $this->defaultLang; + $lang->ERCommon = '$ERCOMMON'; $lang->URCommon = '$URCOMMON'; $lang->SRCommon = '$SRCOMMON'; $lang->productCommon = '$PRODUCTCOMMON'; @@ -1014,7 +1021,6 @@ class devModel extends model $this->loadModel('custom')->deleteItems("lang={$language}&module={$moduleName}&vision={$this->config->vision}{$section}{$key}"); $data = fixer::input('post')->get(); - if($type == 'common') unset($data->common_SRCommon, $data->common_URCommon); foreach($data as $langKey => $customedLang) { if(strpos($langKey, "{$moduleName}_") !== 0) continue; @@ -1044,7 +1050,8 @@ class devModel extends model $setting = array( 'SRName' => $this->post->common_SRCommon !== false ? $this->post->common_SRCommon : zget($oldValue, 'defaultSRName', $oldValue->SRName), - 'URName' => $this->post->common_URCommon !== false ? $this->post->common_URCommon : zget($oldValue, 'defaultURName', $oldValue->URName) + 'URName' => $this->post->common_URCommon !== false ? $this->post->common_URCommon : zget($oldValue, 'defaultURName', $oldValue->URName), + 'ERName' => $this->post->common_ERCommon !== false ? $this->post->common_ERCommon : zget($oldValue, 'defaultERName', $oldValue->ERName) ); $this->custom->updateURAndSR($this->config->custom->URSR, $language, $setting); } diff --git a/module/editor/test/model/save.php b/module/editor/test/model/save.php index 5ab161531e..b62a51991f 100644 --- a/module/editor/test/model/save.php +++ b/module/editor/test/model/save.php @@ -27,6 +27,6 @@ putenv('IS_CONTAINER=true'); $filePath = '/tmp/test.php'; $editor->saveTest($filePath); $errors = dao::getError(); -r((int)str_contains($errors[0], '只能修改禅道文件')) && p() && e('1'); //不修改禅道文件。 +r((int)str_contains($errors[0], '只能修改禅道文件')) && p() && e('0'); //不修改禅道文件。 putenv("IS_CONTAINER={$oldENV}"); diff --git a/module/epic/config.php b/module/epic/config.php new file mode 100644 index 0000000000..5cb42bb64d --- /dev/null +++ b/module/epic/config.php @@ -0,0 +1,4 @@ +loadConfig('story'); +$config->epic = clone $config->story; diff --git a/module/epic/control.php b/module/epic/control.php new file mode 100644 index 0000000000..9ff02fbeca --- /dev/null +++ b/module/epic/control.php @@ -0,0 +1,435 @@ +fetch('story', 'create', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=$storyID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=epic"); + } + + /** + * Create a batch stories. + * + * @param int $productID + * @param string $branch + * @param int $moduleID + * @param int $storyID + * @param int $executionID projectID|executionID + * @param int $plan + * @param string $storyType + * @param string $extra for example feedbackID=0 + * @access public + * @return void + */ + public function batchCreate(int $productID = 0, string $branch = '', int $moduleID = 0, int $storyID = 0, int $executionID = 0, int $plan = 0, string $storyType = 'epic', string $extra = '') + { + echo $this->fetch('story', 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=$storyID&executionID=$executionID&plan=$plan&storyType=epic&extra=$extra"); + } + + /** + * View a epic. + * + * @param int $storyID + * @param int $version + * @param int $param executionID|projectID + * @param string $storyType + * @access public + * @return void + */ + public function view(int $storyID, int $version = 0, int $param = 0) + { + echo $this->fetch('story', 'view', "storyID=$storyID&version=$version¶m=$param&storyType=epic"); + } + + /** + * Edit a epic. + * + * @param int $storyID + * @param string $kanbanGroup + * @access public + * @return void + */ + public function edit(int $storyID, string $kanbanGroup = 'default') + { + echo $this->fetch('story', 'edit', "storyID=$storyID&kanbanGroup=$kanbanGroup&storyType=epic"); + } + + /** + * Batch edit epic. + * + * @param int $productID + * @param int $executionID + * @param int $branch + * @param string $storyType + * @param string $from + * @access public + * @return void + */ + public function batchEdit(int $productID = 0, int $executionID = 0, string $branch = '', string $storyType = 'epic', string $from = '') + { + echo $this->fetch('story', 'batchEdit', "productID=$productID&executionID=$executionID&branch=$branch&storyType=epic&from=$from"); + } + + /** + * 关联Epic。 + * Link related epics. + * + * @param int $storyID + * @param string $browseType + * @param string $excludeStories + * @param int $param + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function linkStories(int $storyID, string $browseType = '', string $excludeStories = '', int $param = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) + { + echo $this->fetch('story', 'linkStories', "storyID=$storyID&browseType=$browseType&excludeStories=$excludeStories¶m=$param&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); + } + + /** + * 关联用户需求。 + * Link related requirements. + * + * @param int $storyID + * @param string $browseType + * @param string $excludeStories + * @param int $param + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function linkRequirements(int $storyID, string $browseType = '', string $excludeStories = '', int $param = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) + { + echo $this->fetch('story', 'linkRequirements', "storyID=$storyID&browseType=$browseType&excludeStories=$excludeStories¶m=$param&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); + } + + /** + * 导出需求数据。 + * Get the data of the requiremens to export. + * + * @param int $productID + * @param string $orderBy + * @param int $executionID + * @param string $browseType + * @access public + * @return void + */ + public function export(int $productID, string $orderBy, int $executionID = 0, string $browseType = '') + { + echo $this->fetch('story', 'export', "productID=$productID&orderBy=$orderBy&executionID=$executionID&browseType=$browseType&storyType=epic"); + } + + /** + * 导入需求数据。 + * Import the data of the requiremens. + * + * @param int $productID + * @param int $branch + * @param string $storyType + * @param int $projectID + * @access public + * @return void + */ + public function import($productID, $branch = 0, $storyType = 'epic', $projectID = 0) + { + echo $this->fetch('story', 'import', "productID=$productID&branch=$branch&storyType=epic&projectID=$projectID"); + } + + /** + * 显示导入需求数据的页面。 + * Show the page of importing the data of the requiremens. + * + * @param int $productID + * @param int $branch + * @param string $type + * @param int $projectID + * @param int $pagerID + * @param int $maxImport + * @param string $insert + * @access public + * @return void + */ + public function showImport($productID, $branch = 0, $type = 'epic', $projectID = 0, $pagerID = 1, $maxImport = 0, $insert = '') + { + echo $this->fetch('story', 'showImport', "productID=$productID&branch=$branch&type=epic&projectID=$projectID&pagerID=$pagerID&maxImport=$maxImport&insert=$insert"); + } + + /** + * 导出需求模板。 + * Export the template of the requiremens. + * + * @param int $productID + * @param int $branch + * @param string $storyType + * @access public + * @return void + */ + public function exportTemplate($productID, $branch = 0, $storyType = 'epic') + { + echo $this->fetch('story', 'exportTemplate', "productID=$productID&branch=$branch&storyType=epic"); + } + + /** + * Delete a epic. + * + * @param int $storyID + * @param string $confirm yes|no + * @param string $from taskkanban + * @access public + * @return void + */ + public function delete(int $storyID, string $confirm = 'no', string $from = '') + { + echo $this->fetch('story', 'delete', "storyID=$storyID&confirm=$confirm&from=$from&storyType=epic"); + } + + /** + * Change a epic. + * + * @param int $storyID + * @param string $from + * @access public + * @return void + */ + public function change(int $storyID, string $from = '') + { + echo $this->fetch('story', 'change', "storyID=$storyID&from=$from&storyType=epic"); + } + + /** + * Review a epic. + * + * @param int $storyID + * @param string $from product|project + * @access public + * @return void + */ + public function review(int $storyID, string $from = 'product') + { + echo $this->fetch('story', 'review', "storyID=$storyID&from=$from&storyType=epic"); + } + + /** + * Submit review. + * + * @param int $storyID + * @access public + * @return void + */ + public function submitReview(int $storyID) + { + echo $this->fetch('story', 'submitReview', "storyID=$storyID&storyType=epic"); + } + + /** + * Batch review epics. + * + * @param string $result + * @param string $reason + * @access public + * @return void + */ + public function batchReview(string $result, string $reason = '') + { + echo $this->fetch('story', 'batchReview', "result=$result&reason=$reason&storyType=epic"); + } + + /** + * Recall the epic review or epic change. + * + * @param int $storyID + * @param string $from list + * @param string $confirm no|yes + * @access public + * @return void + */ + public function recall(int $storyID, string $from = 'list', string $confirm = 'no') + { + echo $this->fetch('story', 'recall', "storyID=$storyID&from=$from&confirm=$confirm&storyType=epic"); + } + + /** + * 需求的指派给页面。 + * Assign the epic to a user. + * + * @param int $storyID + * @access public + * @return void + */ + public function assignTo(int $storyID) + { + echo $this->fetch('story', 'assignTo', "storyID=$storyID"); + } + + /** + * 关闭需求。 + * Close the epic. + * + * @param int $storyID + * @param string $from taskkanban + * @access public + * @return void + */ + public function close(int $storyID, string $from = '') + { + echo $this->fetch('story', 'close', "storyID=$storyID&from=$from&storyType=epic"); + } + + /** + * 批量关闭需求。 + * Batch close the requiremens. + * + * @param int $productID + * @param int $executionID + * @param string $storyType + * @param string $from contribute|work + * @access public + * @return void + */ + public function batchClose(int $productID = 0, int $executionID = 0, string $storyType = 'epic', string $from = '') + { + echo $this->fetch('story', 'batchClose', "productID=$productID&executionID=$executionID&storyType=epic&from=$from"); + } + + /** + * 激活需求。 + * Activate a epic. + * + * @param int $storyID + * @access public + * @return void + */ + public function activate(int $storyID) + { + echo $this->fetch('story', 'activate', "storyID=$storyID&storyType=epic"); + } + + /** + * 查看需求的报告。 + * The report page. + * + * @param int $productID + * @param int $branchID + * @param string $storyType + * @param string $browseType + * @param int $moduleID + * @param string $chartType + * @param int $projectID + * @access public + * @return void + */ + public function report(int $productID, int $branchID, string $storyType = 'epic', string $browseType = 'unclosed', int $moduleID = 0, string $chartType = 'pie', int $projectID = 0) + { + echo $this->fetch('story', 'report', "productID=$productID&branchID=$branchID&storyType=epic&browseType=$browseType&moduleID=$moduleID&chartType=$chartType&projectID=$projectID"); + } + + /** + * Batch change branch. + * + * @param int $branchID + * @param string $confirm yes|no + * @param string $storyIdList + * @access public + * @return void + */ + public function batchChangeBranch(int $branchID, string $confirm = '', string $storyIdList = '') + { + echo $this->fetch('story', 'batchChangeBranch', "branchID=$branchID&confirm=$confirm&storyIdList=$storyIdList&storyType=epic"); + } + + /** + * Batch assign to. + * + * @param string $storyType story|epic + * @access public + * @return void + */ + public function batchAssignTo(string $storyType = 'epic', string $assignedTo = '') + { + echo $this->fetch('story', 'batchAssignTo', "storyType=epic&assignedTo=$assignedTo"); + } + + /** + * Batch change the module of story. + * + * @param int $moduleID + * @access public + * @return void + */ + public function batchChangeModule(int $moduleID) + { + echo $this->fetch('story', 'batchChangeModule', "moduleID=$moduleID&storyType=epic"); + } + + /** + * Batch change parent. + * + * @param int $productID + * @param string $storyType + * @access public + * @return void + */ + public function batchChangeParent(int $productID = 0, string $storyType = 'epic') + { + echo $this->fetch('story', 'batchChangeParent', "productID=$productID&storyType=epic"); + } + + /** + * Batch change grade. + * + * @param int $grade + * @access public + * @return void + */ + public function batchChangeGrade(int $grade) + { + echo $this->fetch('story', 'batchChangeGrade', "grade=$grade&storyType=requirement"); + } + + /** + * Batch change plan. + * + * @param int $planID + * @param int $oldPlanID + * @access public + * @return void + */ + public function batchChangePlan(int $planID, int $oldPlanID = 0) + { + echo $this->fetch('story', 'batchChangePlan', "planID=$planID&oldPlanID=$oldPlanID&storyType=epic"); + } + + /** + * Ajax get the stories of a user. + * + * @param int $userID + * @param string $id + * @param int $appendID + * @param string $storyType + * @access public + * @return void + */ + public function ajaxGetUserStories(int $userID = 0, string $id = '', int $appendID = 0, $storyType = 'epic') + { + echo $this->fetch('story', 'ajaxGetUserStories', "userID=$userID&id=$id&appendID=$appendID&storyType=epic"); + } +} diff --git a/module/epic/lang/de.php b/module/epic/lang/de.php new file mode 100644 index 0000000000..51a42db40d --- /dev/null +++ b/module/epic/lang/de.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->epic = clone $lang->story; + +foreach($lang->epic as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->epic->$key = str_replace($lang->SRCommon, $lang->ERCommon, $value); +} + +$lang->epic->common = $lang->ERCommon; + +$lang->epic->stageList = array(); +$lang->epic->stageList[''] = ''; +$lang->epic->stageList['defining'] = 'Defining'; +$lang->epic->stageList['planning'] = 'Planning'; +$lang->epic->stageList['developing'] = 'Developing'; +$lang->epic->stageList['delivering'] = 'Delivering'; +$lang->epic->stageList['closed'] = 'Closed'; diff --git a/module/epic/lang/en.php b/module/epic/lang/en.php new file mode 100644 index 0000000000..51a42db40d --- /dev/null +++ b/module/epic/lang/en.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->epic = clone $lang->story; + +foreach($lang->epic as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->epic->$key = str_replace($lang->SRCommon, $lang->ERCommon, $value); +} + +$lang->epic->common = $lang->ERCommon; + +$lang->epic->stageList = array(); +$lang->epic->stageList[''] = ''; +$lang->epic->stageList['defining'] = 'Defining'; +$lang->epic->stageList['planning'] = 'Planning'; +$lang->epic->stageList['developing'] = 'Developing'; +$lang->epic->stageList['delivering'] = 'Delivering'; +$lang->epic->stageList['closed'] = 'Closed'; diff --git a/module/epic/lang/fr.php b/module/epic/lang/fr.php new file mode 100644 index 0000000000..51a42db40d --- /dev/null +++ b/module/epic/lang/fr.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->epic = clone $lang->story; + +foreach($lang->epic as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->epic->$key = str_replace($lang->SRCommon, $lang->ERCommon, $value); +} + +$lang->epic->common = $lang->ERCommon; + +$lang->epic->stageList = array(); +$lang->epic->stageList[''] = ''; +$lang->epic->stageList['defining'] = 'Defining'; +$lang->epic->stageList['planning'] = 'Planning'; +$lang->epic->stageList['developing'] = 'Developing'; +$lang->epic->stageList['delivering'] = 'Delivering'; +$lang->epic->stageList['closed'] = 'Closed'; diff --git a/module/epic/lang/zh-cn.php b/module/epic/lang/zh-cn.php new file mode 100644 index 0000000000..a6013fed7f --- /dev/null +++ b/module/epic/lang/zh-cn.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->epic = clone $lang->story; + +foreach($lang->epic as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->epic->$key = str_replace($lang->SRCommon, $lang->ERCommon, $value); +} + +$lang->epic->common = $lang->ERCommon; + +$lang->epic->stageList = array(); +$lang->epic->stageList[''] = ''; +$lang->epic->stageList['defining'] = '定义中'; +$lang->epic->stageList['planning'] = '规划中'; +$lang->epic->stageList['developing'] = '研发中'; +$lang->epic->stageList['delivering'] = '交付中'; +$lang->epic->stageList['closed'] = '已关闭'; diff --git a/module/epic/model.php b/module/epic/model.php new file mode 100644 index 0000000000..84823d051a --- /dev/null +++ b/module/epic/model.php @@ -0,0 +1,10 @@ +control->loadModel('story'); + return call_user_func_array(array('storyModel', 'isClickable'), array($data, $action)); + } +} diff --git a/module/execution/config/dtable.php b/module/execution/config/dtable.php index 59155a791b..5cb164ce91 100644 --- a/module/execution/config/dtable.php +++ b/module/execution/config/dtable.php @@ -248,15 +248,16 @@ $config->execution->linkStory->dtable->fieldList['id']['sortType'] = true; $config->execution->linkStory->dtable->fieldList['id']['fixed'] = 'left'; $config->execution->linkStory->dtable->fieldList['id']['group'] = 1; -$config->execution->linkStory->dtable->fieldList['title']['title'] = $lang->story->title; -$config->execution->linkStory->dtable->fieldList['title']['type'] = 'title'; -$config->execution->linkStory->dtable->fieldList['title']['link'] = helper::createLink('story', 'view', 'storyID={id}'); -$config->execution->linkStory->dtable->fieldList['title']['sortType'] = true; -$config->execution->linkStory->dtable->fieldList['title']['fixed'] = 'left'; -$config->execution->linkStory->dtable->fieldList['title']['group'] = 2; -$config->execution->linkStory->dtable->fieldList['title']['data-app'] = $app->tab; -$config->execution->linkStory->dtable->fieldList['title']['data-toggle'] = 'modal'; -$config->execution->linkStory->dtable->fieldList['title']['data-size'] = 'lg'; +$config->execution->linkStory->dtable->fieldList['title']['title'] = $lang->story->name; +$config->execution->linkStory->dtable->fieldList['title']['type'] = 'title'; +$config->execution->linkStory->dtable->fieldList['title']['link'] = helper::createLink('story', 'storyview', 'storyID={id}'); +$config->execution->linkStory->dtable->fieldList['title']['sortType'] = true; +$config->execution->linkStory->dtable->fieldList['title']['fixed'] = 'left'; +$config->execution->linkStory->dtable->fieldList['title']['group'] = 2; +$config->execution->linkStory->dtable->fieldList['title']['nestedToggle'] = true; +$config->execution->linkStory->dtable->fieldList['title']['data-app'] = $app->tab; +$config->execution->linkStory->dtable->fieldList['title']['data-toggle'] = 'modal'; +$config->execution->linkStory->dtable->fieldList['title']['data-size'] = 'lg'; $config->execution->linkStory->dtable->fieldList['pri']['title'] = $lang->story->pri; $config->execution->linkStory->dtable->fieldList['pri']['type'] = 'pri'; diff --git a/module/execution/control.php b/module/execution/control.php index e940da987e..b7d1feeb25 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -47,7 +47,7 @@ class execution extends control $this->executions = $this->execution->getPairs(0, 'all', "nocode,noprefix,{$mode}"); $skipCreateStep = array('computeburn', 'ajaxgetdropmenu', 'executionkanban', 'ajaxgetteammembers', 'all', 'ajaxgetcopyprojectexecutions'); - if(in_array($this->methodName, $skipCreateStep) && $this->app->tab == 'execution') return false; + if(in_array($this->methodName, $skipCreateStep)) return false; if($this->executions || $this->methodName == 'index' || $this->methodName == 'create' || $this->app->getViewType() == 'mhtml') return false; if($this->app->tab == 'project' && in_array($this->app->rawMethod, array('linkstory', 'importplanstories', 'unlinkstory', 'export'))) return false; @@ -429,6 +429,8 @@ class execution extends control { /* Load these models. */ $this->loadModel('story'); + $this->loadModel('requirement'); + $this->loadModel('epic'); $this->loadModel('product'); /* Change for requirement story title. */ @@ -455,6 +457,7 @@ class execution extends control $execution = $this->commonAction($executionID); $executionID = $execution->id; + $project = $this->loadModel('project')->fetchById($execution->project); /* Build the search form. */ $products = $this->product->getProducts($executionID); @@ -466,7 +469,7 @@ class execution extends control if($this->app->getViewType() == 'xhtml') $recPerPage = 10; $pager = new pager($recTotal, $recPerPage, $pageID); - $stories = $this->story->getExecutionStories($executionID, 0, $sort, $type, (string)$param, $storyType, '', $pager); + $stories = $this->story->getExecutionStories($executionID, 0, $sort, $type, (string)$param, $project->storyType, '', $pager); $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false); if(!empty($stories)) $stories = $this->story->mergeReviewer($stories); @@ -475,6 +478,7 @@ class execution extends control $this->executionZen->assignCountForStory($executionID, $stories, $storyType); $this->executionZen->assignRelationForStory($execution, $products, $productID, $type, $storyType, $param, $orderBy, $pager); + $this->view->project = $project; $this->display(); } @@ -1012,7 +1016,7 @@ class execution extends control if(!empty($_POST)) { /* Filter empty plans. */ - if(isset($_POST['attribute']) and in_array($_POST['attribute'], array('request', 'design', 'review'))) unset($_POST['plans']); + if(isset($_POST['attribute']) and $_POST['attribute'] == 'review') unset($_POST['plans']); if(!empty($_POST['plans'])) { foreach($_POST['plans'] as $key => $planItem) $_POST['plans'][$key] = array_filter($_POST['plans'][$key]); @@ -1656,6 +1660,8 @@ class execution extends control public function kanban(int $executionID, string $browseType = 'all', string $orderBy = 'id_asc', string $groupBy = 'default') { $this->app->loadLang('bug'); + $this->app->loadLang('epic'); + $this->app->loadLang('requirement'); $this->app->loadLang('kanban'); if($this->config->vision != 'lite') $this->lang->execution->menu = new stdclass(); @@ -1718,6 +1724,8 @@ class execution extends control { if(!$this->loadModel('common')->checkPrivByObject('execution', $executionID)) return $this->execution->accessDenied(); /* Load model and language. */ + $this->app->loadLang('epic'); + $this->app->loadLang('requirement'); $this->app->loadLang('task'); $this->app->loadLang('bug'); $this->loadModel('kanban'); @@ -1744,9 +1752,10 @@ class execution extends control $this->session->set('execLaneType', $browseType); /* Get kanban data. */ - $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy; + $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy; $this->kanban->createLaneIfNotExist($executionID); $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); /* Show lanes of the attribute: no story and bug in request, no bug in design. */ if(!isset($this->lang->execution->menu->story)) unset($kanbanGroup['story']); @@ -1761,6 +1770,7 @@ class execution extends control $this->view->browseType = $browseType; $this->view->features = $features; $this->view->kanbanGroup = $kanbanGroup; + $this->view->links = $links; $this->view->groupBy = $groupBy; $this->display(); @@ -1891,15 +1901,17 @@ class execution extends control $execution = $this->execution->getById($executionID); $project = $this->project->getById($execution->project); - if($execution->lifetime == 'ops') unset($this->lang->execution->treeLevel['story']); + if($execution->lifetime == 'ops') unset($this->lang->execution->treeLevel['story']); if($this->app->tab == 'project') $this->view->projectID = $execution->project; + $gradeGroup = $this->loadModel('story')->getGradeGroup(); + $this->view->title = $this->lang->execution->tree; $this->view->execution = $execution; $this->view->executionID = $executionID; $this->view->level = $type; - $this->view->tree = $this->execution->buildTree($tree, (bool)$project->hasProduct); + $this->view->tree = $this->execution->buildTree($tree, (bool)$project->hasProduct, $gradeGroup); $this->view->features = $this->execution->getExecutionFeatures($execution); $this->display(); } @@ -2224,7 +2236,7 @@ class execution extends control $object = $this->project->getByID($objectID, 'project,sprint,stage,kanban'); $browseLink = $this->createLink('execution', 'story', "executionID=$objectID&storyType=$storyType"); - if($this->app->tab == 'project' and $object->multiple) $browseLink = $this->createLink('projectstory', 'story', "objectID=$objectID&productID=0&branch=0&browseType=¶m=0&storyType=$storyType"); + if($this->app->tab == 'project' and $object->multiple) $browseLink = $this->createLink('projectstory', 'story', "objectID=$objectID"); if($object->type == 'kanban' && !$object->hasProduct) $this->lang->productCommon = $this->lang->project->common; @@ -2273,29 +2285,21 @@ class execution extends control } } - if($storyType == 'requirement') - { - $this->app->loadLang('projectstory'); - $this->lang->story->title = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->title); - $this->lang->projectstory->whyNoStories = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->projectstory->whyNoStories); - $this->lang->execution->linkStory = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->linkStory); - if(isset($this->config->product->search['fields']['stage'])) unset($this->config->product->search['fields']['stage']); - } - /* Build the search form. */ $actionURL = $this->createLink($this->app->rawModule, 'linkStory', "objectID=$objectID&browseType=bySearch&queryID=myQueryID&orderBy=$orderBy&recPerPage=$recPerPage&pageID=$pageID&extra=$extra&storyType=$storyType"); $branchGroups = $this->loadModel('branch')->getByProducts(array_keys($products)); $queryID = ($browseType == 'bySearch') ? (int)$param : 0; $this->execution->buildStorySearchForm($products, $branchGroups, $modules, $queryID, $actionURL, 'linkStory', $object); - if($browseType == 'bySearch') $allStories = $this->story->getBySearch('all', '', $queryID, $orderBy, $objectID, $storyType); - if($browseType != 'bySearch') $allStories = $this->story->getProductStories(implode(',', array_keys($products)), $branchIDList, '0', 'active', $storyType, $orderBy, false, '', null); - $linkedStories = $this->story->getExecutionStoryPairs($objectID, 0, 'all', 0, 'full', 'all', $storyType); + $project = (strpos('sprint,stage,kanban', $object->type) !== false) ? $this->loadModel('project')->getByID($object->project) : $object; + $storyType = (($object->type == 'stage' && in_array($object->attribute, array('mix', 'request', 'design'))) || $object->type == 'project' || !$object->multiple) ? $project->storyType : 'story'; + if($browseType == 'bySearch') $allStories = $this->story->getBySearch('all', '', $queryID, $orderBy, $objectID, $storyType); + if($browseType != 'bySearch') $allStories = $this->story->getProductStories(implode(',', array_keys($products)), $branchIDList, '0', 'active,launched', $storyType, $orderBy, true, '', null); + $linkedStories = $this->story->getExecutionStoryPairs($objectID, 0, 'all', 0, 'full', 'all', $storyType); foreach($allStories as $id => $story) { if(isset($linkedStories[$story->id])) unset($allStories[$id]); - if($story->parent < 0) unset($allStories[$id]); if(!isset($modules[$story->module])) { @@ -2307,14 +2311,15 @@ class execution extends control $story->estimate = $story->estimate . $this->config->hourUnit; } + $gradeGroup = array(); + $gradeList = $this->story->getGradeList(''); + foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; + /* Set the pager. */ $this->app->loadClass('pager', true); $pager = new pager(count($allStories), $recPerPage, $pageID); $allStories = array_chunk($allStories, $pager->recPerPage); - $project = $object; - if(strpos('sprint,stage,kanban', $object->type) !== false) $project = $this->loadModel('project')->getByID($object->project); - $productPairs = array(); foreach($products as $id => $product) $productPairs[$id] = $product->name; @@ -2324,6 +2329,8 @@ class execution extends control $this->view->extra = $extra; $this->view->object = $object; $this->view->orderBy = $orderBy; + $this->view->gradeGroup = $gradeGroup; + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->productPairs = $productPairs; $this->view->allStories = empty($allStories) ? $allStories : $allStories[$pageID - 1]; $this->view->pager = $pager; @@ -2334,7 +2341,6 @@ class execution extends control $this->view->branchGroups = $branchGroups; $this->view->browseLink = $browseLink; - /* NOT used in zin. */ $this->view->project = $project; $this->view->executionID = $object->id; $this->view->projectID = $object->id; @@ -3041,6 +3047,9 @@ class execution extends control */ public function treeStory(int $storyID, int $version = 0) { + $this->loadModel('requirement'); + $this->loadModel('epic'); + $story = $this->loadModel('story')->getById($storyID, $version, true); $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fields('name, id, `type`, shadow')->fetch(); @@ -3146,7 +3155,14 @@ class execution extends control $enterTime = date('Y-m-d H:i:s', $enterTime); if(in_array(true, array(empty($lastEditedTime), strtotime($lastEditedTime) < 0, $lastEditedTime > $enterTime, $groupBy != 'default', !empty($searchValue)))) { - $kanbanGroup = $from == 'taskkanban' ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $searchValue, $orderBy) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy, $searchValue); + if($from == 'taskkanban') + { + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $searchValue, $orderBy); + } + else + { + $kanbanGroup = $this->kanban->getRDKanban($executionID, $browseType, $orderBy, 0, $groupBy, $searchValue); + } return print(json_encode($kanbanGroup)); } } diff --git a/module/execution/css/kanban.ui.css b/module/execution/css/kanban.ui.css index cb21e7f33c..61c44d431d 100644 --- a/module/execution/css/kanban.ui.css +++ b/module/execution/css/kanban.ui.css @@ -2,3 +2,4 @@ .kanban-header-title .text-danger {color:rgb(255, 93, 93); opacity: 1;} .as-subtitle {opacity: 0.5; text-wrap: nowrap; font-weight: 700;} .is-collapsed .icon-angle-top:before { content: "\e313"; } +.card.selected .child-item .icon {color: var(--color-primary-500)} diff --git a/module/execution/css/taskkanban.ui.css b/module/execution/css/taskkanban.ui.css index f6e45d97e0..f80c2f69cf 100644 --- a/module/execution/css/taskkanban.ui.css +++ b/module/execution/css/taskkanban.ui.css @@ -3,3 +3,4 @@ .toolbar .kanbanSetting.btn-link .caret{display: none;} .dropdown-menu .menu-divider a{display:none;} .as-subtitle {opacity: 0.5; text-wrap: nowrap; font-weight: 700;} +.card.selected .child-item .icon {color: var(--color-primary-500)} diff --git a/module/execution/css/view.ui.css b/module/execution/css/view.ui.css index db00d686cd..573ce2fce6 100644 --- a/module/execution/css/view.ui.css +++ b/module/execution/css/view.ui.css @@ -8,5 +8,4 @@ .actions-menu {bottom: 12px;} #mainContent {margin-bottom: 60px;} - #mainContent .desc-box {max-height: 6rem;} diff --git a/module/execution/js/create.ui.js b/module/execution/js/create.ui.js index 05564525de..c43429e11a 100644 --- a/module/execution/js/create.ui.js +++ b/module/execution/js/create.ui.js @@ -65,7 +65,7 @@ function loadMembers() function loadProjectExecutions(projectID) { projectID = parseInt(projectID) ? projectID : $('#copyExecutionModal input[name=project]').val(); - projectID = parseInt(projectID); + projectID = projectID == undefined ? 0 : parseInt(projectID); loadTarget($.createLink('execution', 'ajaxGetCopyProjectExecutions', 'projectID=' + projectID + '©ExecutionID=' + copyExecutionID), '#copyExecutions'); } diff --git a/module/execution/js/kanban.ui.js b/module/execution/js/kanban.ui.js index 98dd4f1712..7a69cbac4a 100644 --- a/module/execution/js/kanban.ui.js +++ b/module/execution/js/kanban.ui.js @@ -8,6 +8,7 @@ window.getLane = function(lane) window.getCol = function(col) { /* 计算WIP。*/ + if(ERURColumn.includes(col.type)) return; const limit = col.limit == -1 ? "" : col.limit; const cards = col.cards; @@ -91,7 +92,7 @@ window.buildColActions = function(col) let actions = []; if(col.actionList && col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'edit'}); - if(col.actionList && col.actionList.includes('setWIP')) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'alert'}); + if(col.actionList && col.actionList.includes('setWIP') && !ERURColumn.includes(col.type)) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'alert'}); return actions; } @@ -139,7 +140,7 @@ window.getItem = function(info) const avatar = renderAvatar(info.item); info.item.titleAttrs = {}; - if(info.item.cardType == 'story') + if(['story', 'parentStory'].includes(info.item.cardType)) { info.item.icon = 'product'; if(priv.canViewStory) @@ -208,9 +209,14 @@ window.getItem = function(info) window.renderAvatar = function(item) { let assignLink = ''; - if(item.cardType == 'story' && priv.canAssignStory) assignLink = $.createLink('story', 'assignTo', "id=" + item.id); - if(item.cardType == 'bug' && priv.canAssignBug) assignLink = $.createLink('bug', 'assignTo', "id=" + item.id); - if(item.cardType == 'task' && priv.canAssignTask) assignLink = $.createLink('task', 'assignTo', "executionID=" + executionID + "&id=" + item.id); + if(item.cardType == 'parentStory') + { + return '
'; + } + + if(item.cardType == 'story' && priv.canAssignStory) assignLink = $.createLink('story', 'assignTo', "id=" + item.id); + if(item.cardType == 'bug' && priv.canAssignBug) assignLink = $.createLink('bug', 'assignTo', "id=" + item.id); + if(item.cardType == 'task' && priv.canAssignTask) assignLink = $.createLink('task', 'assignTo', "executionID=" + executionID + "&id=" + item.id); if(item.assignedTo.length == 0) { @@ -227,9 +233,10 @@ window.renderAvatar = function(item) window.getItemActions = function(item) { let actions = []; - if(item.cardType == 'story') actions = buildStoryActions(item); - else if(item.cardType == 'bug') actions = buildBugActions(item); - else if(item.cardType == 'task') actions = buildTaskActions(item); + if(item.cardType == 'story') actions = buildStoryActions(item); + else if(item.cardType == 'parentStory') actions = buildParentStoryActions(item); + else if(item.cardType == 'bug') actions = buildBugActions(item); + else if(item.cardType == 'task') actions = buildTaskActions(item); if(actions.length) { @@ -242,6 +249,19 @@ window.getItemActions = function(item) } } +window.buildParentStoryActions = function(item) +{ + let actions = []; + + if(priv.canEditStory) actions.push({text: storyLang.edit, icon: 'edit', url: $.createLink('story', 'edit', 'storyID=' + item.id + '&kanbanGroup=' + groupBy), 'data-toggle': 'modal', 'data-size': 'lg'}); + if(priv.canChangeStory && item.status == 'active') actions.push({text: storyLang.change, icon: 'change', url: $.createLink('story', 'change', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'}); + if(priv.canActivateStory && item.status == 'closed') actions.push({text: executionLang.activate, icon: 'magic', url: $.createLink('story', 'activate', 'storyID=' + item.id), 'data-toggle': 'modal', 'data-size': 'lg'}); + if(priv.canUnlinkStory) actions.push({text: executionLang.unlinkStory, icon: 'unlink', url: $.createLink('execution', 'unlinkStory', 'executionID=' + executionID + '&storyID=' + item.id + '&confirm=no&from=' + '&laneID=' + item.lane + '&columnID=' + item.column), 'innerClass' : 'ajax-submit'}); + if(priv.canDeleteStory) actions.push({text: storyLang.delete, icon: 'trash', url: $.createLink('story', 'delete', 'storyID=' + item.id), 'innerClass': 'ajax-submit'}); + + return actions; +} + window.buildStoryActions = function(item) { let actions = []; @@ -345,10 +365,11 @@ if(!window.kanbanDropRules) { backlog: ['ready', 'backlog'], ready: ['backlog', 'ready'], - tested: ['verified'], - verified: ['tested', 'released'], + tested: ['verified', 'rejected'], + verified: ['tested', 'pending', 'released'], + pending: ['verified', 'released'], released: ['verified', 'closed'], - closed: ['released'], + closed: ['released'] }, bug: { diff --git a/module/execution/js/linkstory.ui.js b/module/execution/js/linkstory.ui.js index 9cd5297107..015ccb29bc 100644 --- a/module/execution/js/linkstory.ui.js +++ b/module/execution/js/linkstory.ui.js @@ -35,11 +35,12 @@ window.onRenderLinkStoryCell = function(result, {row, col}) { if(col.name == 'title') { - if(row.data.parent > 0) - { - html = ""+ childrenAB +" "; - result.unshift({html}); - } + const story = row.data; + const gradeLabel = (showGrade || story.grade >= 2) ? gradeGroup[story.type][story.grade] : ''; + + let html = ''; + if(gradeLabel) html += "" + gradeLabel + " "; + if(html) result.unshift({html}); } if(col.name !== 'branch') diff --git a/module/execution/js/story.ui.js b/module/execution/js/story.ui.js index 9ee28e9c69..f1121c286e 100644 --- a/module/execution/js/story.ui.js +++ b/module/execution/js/story.ui.js @@ -122,19 +122,25 @@ window.setStatistics = function(element, checkedIdList) let SRTotal = 0; let total = 0; + let rateCount = 0; checkedIdList.forEach((rowID) => { - const story = element.getRowInfo(rowID); + const story = element.getRowInfo(rowID); if(storyType == 'requirement' && story.data.type == 'story') SRTotal += 1; if(storyType == story.data.type) total += 1; if(story) { checkedEstimate += parseFloat(story.data.estimate); if(cases[rowID]) checkedCase += 1; + if(story.data.isParent == '0') rateCount += 1; } }) - const rate = Math.round(checkedCase / checkedTotal * 10000) / 100 + '' + '%'; - return {html: checkedSummary.replace('%total%', total).replace('%estimate%', checkedEstimate).replace('%rate%', rate).replace('%SRTotal%', SRTotal)}; + const rate = rateCount ? Math.round(checkedCase / rateCount * 10000) / 100 + '' + '%' : 0 + '%'; + return { + html: checkedSummary.replace('%total%', checkedTotal) + .replace('%estimate%', checkedEstimate) + .replace('%rate%', rate) + }; } window.renderStoryCell = function(result, info) @@ -143,10 +149,9 @@ window.renderStoryCell = function(result, info) if(info.col.name == 'title' && result) { let html = ''; - - if(story.parentName != undefined && story.parent > 0 && ($.cookie.get('tab') == 'execution' || ($.cookie.get('tab') == 'project' && hasProduct))) html += story.parentName + ' / '; + let gradeLabel = gradeGroup[story.type][story.grade]; if(typeof modulePairs[story.moduleID] != 'undefined') html += "" + modulePairs[story.moduleID] + " "; - if(story.parent > 0) html += "" + (storyType == 'requirement' ? 'SR' : childrenAB) + ""; + if(gradeLabel) html += "" + gradeLabel + ""; if(html) result.unshift({html}); } if(info.col.name == 'status' && result) @@ -155,3 +160,12 @@ window.renderStoryCell = function(result, info) } return result; }; + +window.setShowGrades = function() +{ + const showGrades = $('[name^=showGrades]').zui('picker').$.state.value; + if(oldShowGrades == showGrades) return; + + const link = $.createLink('product', 'ajaxSetShowGrades', 'module=execution&showGrades=' + showGrades); + $.get(link, function() { loadCurrentPage(); }); +} diff --git a/module/execution/js/taskkanban.ui.js b/module/execution/js/taskkanban.ui.js index fb107d1f01..3e3338afd6 100644 --- a/module/execution/js/taskkanban.ui.js +++ b/module/execution/js/taskkanban.ui.js @@ -6,12 +6,13 @@ const kanbanDropRules = { story: { - 'backlog': ['ready'], - 'ready': ['backlog'], - 'tested': ['verified'], - 'verified': ['tested', 'released'], - 'released': ['verified', 'closed'], - 'closed': ['released'], + backlog: ['ready', 'backlog'], + ready: ['backlog', 'ready'], + tested: ['verified', 'rejected'], + verified: ['tested', 'pending', 'released'], + pending: ['verified', 'released'], + released: ['verified', 'closed'], + closed: ['released'] }, bug: { @@ -109,6 +110,7 @@ window.getLaneActions = function(lane) window.getCol = function(col) { /* 计算WIP。*/ + if(ERURColumn.includes(col.type)) return; const limit = col.limit == -1 ? "" : col.limit; const cards = col.cards; @@ -187,8 +189,8 @@ window.buildColActions = function(col) { let actions = []; - if(priv.canEditName && col.actionList && col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'edit'}); - if(priv.canSetWIP && col.actionList && col.actionList.includes('setWIP')) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'alert'}); + if(priv.canEditName && col.actionList && col.actionList.includes('setColumn')) actions.push({text: kanbanLang.setColumn, url: $.createLink('kanban', 'setColumn', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'edit'}); + if(priv.canSetWIP && col.actionList && col.actionList.includes('setWIP') && !ERURColumn.includes(col.type)) actions.push({text: kanbanLang.setWIP, url: $.createLink('kanban', 'setWIP', `columnID=${col.id}&executionID=${executionID}&from=RDKanban`), 'data-toggle': 'modal', 'icon': 'alert'}); return actions; } @@ -231,7 +233,14 @@ window.getItem = function(info) if(info.laneInfo.type == 'task' && priv.canAssignTask) assignLink = $.createLink('task', 'assignto', 'executionID=' + executionID + '&id=' + info.item.id + '&kanbanGroup=default&from=taskkanban'); if(info.laneInfo.type == 'bug' && priv.canAssignBug) assignLink = $.createLink('bug', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban'); if(info.laneInfo.type == 'risk' && priv.canAssignRisk) assignLink = $.createLink('risk', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban'); - if(assignLink) avatar = "" + avatar + ""; + if(assignLink) + { + avatar = "" + avatar + ""; + } + else if(['parentStory', 'requirement', 'epic'].includes(info.laneInfo.type)) + { + avatar = '
'; + } if(info.laneInfo.type == 'risk') { @@ -263,9 +272,10 @@ window.getItem = function(info) info.item.title = {html: info.item.title}; } - if(info.laneInfo.type == 'story') + if(['story', 'epic', 'requirement', 'parentStory'].includes(info.laneInfo.type)) { info.item.titleUrl = $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`); + $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`); } else if(info.laneInfo.type == 'task' || info.laneInfo.type == 'bug') { @@ -275,6 +285,7 @@ window.getItem = function(info) { info.item.titleUrl = $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}&from=execution`); } + info.item.titleAttrs = {'data-toggle': 'modal', 'data-size': 'lg', 'title': info.item.title, 'class': 'card-title clip'}; info.item.content = {html: content}; diff --git a/module/execution/lang/de.php b/module/execution/lang/de.php index bcc0f33742..b463dca937 100644 --- a/module/execution/lang/de.php +++ b/module/execution/lang/de.php @@ -281,7 +281,7 @@ $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->treeStory = 'Only Stories'; $lang->execution->treeViewTask = 'Tree View Task'; $lang->execution->treeViewStory = 'Tree View Story'; $lang->execution->storyKanban = 'Story Kanban'; @@ -411,7 +411,6 @@ $lang->execution->accessDenied = "Zugriff zu {$lang->executionCo $lang->execution->tips = 'Hinweis'; $lang->execution->afterInfo = "%s wurde erstellt. Als nächstes können Sie "; $lang->execution->setTeam = 'Team setzen'; -$lang->execution->linkStory = 'Link Stories'; $lang->execution->createTask = 'Aufgaben erstellen'; $lang->execution->goback = "Zurückkehren"; $lang->execution->gobackExecution = "Go Back {$lang->execution->common} List"; @@ -579,7 +578,7 @@ $lang->execution->treeLevel = array(); $lang->execution->treeLevel['all'] = 'Alle aufklappen'; $lang->execution->treeLevel['root'] = 'Alle zuklappen'; $lang->execution->treeLevel['task'] = 'Aufgabe anzeigen'; -$lang->execution->treeLevel['story'] = 'Story anzeigen'; +$lang->execution->treeLevel['story'] = 'Only Stories'; $lang->execution->action = new stdclass(); $lang->execution->action->opened = '$date, created by $actor. $extra' . "\n"; diff --git a/module/execution/lang/en.php b/module/execution/lang/en.php index 0d3d2b3f65..0bc6dd130e 100644 --- a/module/execution/lang/en.php +++ b/module/execution/lang/en.php @@ -411,7 +411,6 @@ $lang->execution->accessDenied = "Your access to {$lang->executi $lang->execution->tips = 'Note'; $lang->execution->afterInfo = "%s is created. Next you can "; $lang->execution->setTeam = 'Set Team'; -$lang->execution->linkStory = 'Link Story'; $lang->execution->createTask = 'Create Task'; $lang->execution->goback = "Go Back Task List"; $lang->execution->gobackExecution = "Go Back {$lang->execution->common} List"; diff --git a/module/execution/lang/fr.php b/module/execution/lang/fr.php index eeb167f974..4c6c553cb2 100644 --- a/module/execution/lang/fr.php +++ b/module/execution/lang/fr.php @@ -411,7 +411,6 @@ $lang->execution->accessDenied = "Votre accès au {$lang->execut $lang->execution->tips = 'Note'; $lang->execution->afterInfo = "Le %s a été créé avec succès ! Ensuite vous pouvez "; $lang->execution->setTeam = "Composer l'Equipe"; -$lang->execution->linkStory = 'Stories liées'; $lang->execution->createTask = 'Créer des Tâches'; $lang->execution->goback = "Revenir en arrière"; $lang->execution->gobackExecution = "Go Back {$lang->execution->common} List"; diff --git a/module/execution/lang/zh-cn.php b/module/execution/lang/zh-cn.php index ef279fdc8f..06433e9165 100644 --- a/module/execution/lang/zh-cn.php +++ b/module/execution/lang/zh-cn.php @@ -239,7 +239,7 @@ $lang->execution->team = '团队成员'; $lang->execution->doc = '文档列表'; $lang->execution->doclib = '文档库'; $lang->execution->manageProducts = '关联' . $lang->productCommon; -$lang->execution->linkStory = "关联{$lang->SRCommon}"; +$lang->execution->linkStory = "关联需求"; $lang->execution->linkStoryByPlan = "按照计划关联"; $lang->execution->linkPlan = "关联计划"; $lang->execution->unlinkStoryTasks = "未关联{$lang->SRCommon}任务"; @@ -281,7 +281,7 @@ $lang->execution->importPlanStories = "按计划关联{$lang->SRCommon}" $lang->execution->importBug = '导入Bug'; $lang->execution->tree = '树状图'; $lang->execution->treeTask = '只看任务'; -$lang->execution->treeStory = "只看{$lang->SRCommon}"; +$lang->execution->treeStory = "仅需求"; $lang->execution->treeViewTask = '树状图查看任务'; $lang->execution->treeViewStory = "树状图查看{$lang->SRCommon}"; $lang->execution->storyKanban = "{$lang->SRCommon}看板"; @@ -359,11 +359,11 @@ $lang->execution->groupSummaryAB = "
总任务 %s : < $lang->execution->wbs = "分解任务"; $lang->execution->batchWBS = "批量分解"; $lang->execution->howToUpdateBurn = "帮助 "; -$lang->execution->whyNoStories = "看起来没有{$lang->SRCommon}可以关联。请检查下{$lang->executionCommon}关联的{$lang->productCommon}中有没有{$lang->SRCommon},而且要确保它们已经审核通过。"; -$lang->execution->projectNoStories = "看起来没有{$lang->SRCommon}可以关联。请检查下{$lang->projectCommon}中有没有{$lang->SRCommon},而且要确保它们已经审核通过。"; -$lang->execution->productStories = "{$lang->executionCommon}关联的{$lang->SRCommon}是{$lang->productCommon}{$lang->SRCommon}的子集,并且只有评审通过的{$lang->SRCommon}才能关联。请关联{$lang->SRCommon}。"; -$lang->execution->haveBranchDraft = "导入完成!有%s条非激活状态或不是{$lang->executionCommon}关联分支的{$lang->SRCommon}无法导入"; -$lang->execution->haveDraft = "导入完成!有%s条非激活状态的{$lang->SRCommon}无法导入"; +$lang->execution->whyNoStories = "看起来没有需求可以关联。请检查下{$lang->executionCommon}关联的{$lang->productCommon}中有没有需求,而且要确保它们已经审核通过。"; +$lang->execution->projectNoStories = "看起来没有需求可以关联。请检查下{$lang->projectCommon}中有没有需求,而且要确保它们已经审核通过。"; +$lang->execution->productStories = "{$lang->executionCommon}关联的{$lang->SRCommon}是{$lang->productCommon}需求的子集,并且只有评审通过的{$lang->SRCommon}才能关联。请关联{$lang->SRCommon}。"; +$lang->execution->haveBranchDraft = "导入完成!有%s条非激活状态或不是{$lang->executionCommon}关联分支的需求无法导入"; +$lang->execution->haveDraft = "导入完成!有%s条非激活状态的需求无法导入"; $lang->execution->doneExecutions = '已结束'; $lang->execution->selectDept = '选择部门'; $lang->execution->selectDeptTitle = '选择一个部门的成员'; @@ -411,7 +411,6 @@ $lang->execution->accessDenied = "您无权访问该{$lang->exec $lang->execution->tips = '提示'; $lang->execution->afterInfo = "%s添加成功,您现在可以进行以下操作:"; $lang->execution->setTeam = '设置团队'; -$lang->execution->linkStory = "关联{$lang->SRCommon}"; $lang->execution->createTask = '创建任务'; $lang->execution->goback = "返回任务列表"; $lang->execution->gobackExecution = "返回{$lang->execution->common}列表"; @@ -579,7 +578,7 @@ $lang->execution->treeLevel = array(); $lang->execution->treeLevel['all'] = '全部展开'; $lang->execution->treeLevel['root'] = '全部折叠'; $lang->execution->treeLevel['task'] = '全部显示'; -$lang->execution->treeLevel['story'] = "只看{$lang->SRCommon}"; +$lang->execution->treeLevel['story'] = "仅需求"; $lang->execution->action = new stdclass(); $lang->execution->action->opened = '$date, 由 $actor 创建。$extra' . "\n"; diff --git a/module/execution/model.php b/module/execution/model.php index bc9eaa8fc8..d5086c41b6 100755 --- a/module/execution/model.php +++ b/module/execution/model.php @@ -77,7 +77,6 @@ class executionModel extends model if(in_array($execution->attribute, array('request', 'review'))) { - $features['story'] = false; $features['plan'] = false; } } @@ -157,7 +156,6 @@ class executionModel extends model /* Set stroy navigation for no-product project. */ $this->loadModel('project')->setNoMultipleMenu($executionID); - if(isset($this->lang->execution->menu->storyGroup)) unset($this->lang->execution->menu->storyGroup); if(isset($this->lang->execution->menu->story['dropMenu']) && $this->app->getMethodName() == 'storykanban') { $this->lang->execution->menu->story['link'] = str_replace(array($this->lang->common->story, 'story'), array($this->lang->SRCommon, 'storykanban'), $this->lang->execution->menu->story['link']); @@ -2309,7 +2307,7 @@ class executionModel extends model $this->config->product->search['actionURL'] = $actionURL; $this->config->product->search['queryID'] = $queryID; - $this->config->product->search['fields']['title'] = str_replace($this->lang->SRCommon, $this->lang->SRCommon, $this->lang->story->title); + $this->config->product->search['fields']['title'] = $this->lang->story->name; $this->config->product->search['params']['product']['values'] = $productPairs + array('all' => $this->lang->product->allProductsOfProject); $this->config->product->search['params']['plan']['values'] = $planPairs; $this->config->product->search['params']['module']['values'] = $modules; @@ -2339,6 +2337,20 @@ class executionModel extends model } } + $gradePairs = array(); + $gradeList = $this->loadModel('story')->getGradeList(''); + $storyTypes = isset($project->storyType) ? $project->storyType : 'story'; + if(!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design')))) $storyTypes = 'story'; + foreach($gradeList as $grade) + { + if(strpos($storyTypes, $grade->type) === false) continue; + $key = (string)$grade->type . (string)$grade->grade; + $gradePairs[$key] = $grade->name; + } + asort($gradePairs); + + $this->config->product->search['params']['grade']['values'] = $gradePairs; + $this->loadModel('search')->setSearchParams($this->config->product->search); } @@ -2820,11 +2832,13 @@ class executionModel extends model $versions = $this->loadModel('story')->getVersions($stories); $linkedStories = $this->dao->select('story,`order`')->from(TABLE_PROJECTSTORY)->where('project')->eq($executionID)->orderBy('order_desc')->fetchPairs('story', 'order'); $lastOrder = (int)reset($linkedStories); - $storyList = $this->dao->select('id, status, branch, product')->from(TABLE_STORY)->where('id')->in(array_values($stories))->fetchAll('id'); + $storyList = $this->dao->select('id, status, branch, product, type')->from(TABLE_STORY)->where('id')->in(array_values($stories))->fetchAll('id'); $execution = $this->fetchByID($executionID); $notAllowedStatus = $this->app->rawMethod == 'batchcreate' ? 'closed' : 'draft,reviewing,closed'; $laneID = isset($output['laneID']) ? $output['laneID'] : 0; + $project = $execution->type == 'project' ? $execution : $this->loadModel('project')->fetchById($execution->project); + foreach($stories as $storyID) { if(isset($linkedStories[$storyID])) continue; @@ -2833,11 +2847,14 @@ class executionModel extends model $storyID = (int)$storyID; $story = zget($storyList, $storyID, ''); if(empty($story)) continue; + if(strpos($project->storyType, "$story->type") === false) continue; + + if($execution->multiple && $story->type != 'story' && (!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design'))) && $execution->type != 'project')) continue; if(!empty($lanes[$storyID])) $laneID = $lanes[$storyID]; $columnID = $this->kanban->getColumnIDByLaneID((int)$laneID, 'backlog'); if(empty($columnID)) $columnID = isset($output['columnID']) ? $output['columnID'] : 0; - if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, (int)$laneID, (int)$columnID, 'story', (string)$storyID); + if(!empty($laneID) and !empty($columnID)) $this->kanban->addKanbanCell($executionID, (int)$laneID, (int)$columnID, $storyType, (string)$storyID); $data = new stdclass(); $data->project = $executionID; @@ -2857,7 +2874,7 @@ class executionModel extends model if($storyType == 'requirement' and $this->config->edition == 'ipd') $this->dao->update(TABLE_STORY)->set('status')->eq('developing')->where('id')->eq($storyID)->exec(); } - if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID, 'story'); + if(!isset($output['laneID']) or !isset($output['columnID'])) $this->kanban->updateLane($executionID); return true; } @@ -2907,8 +2924,9 @@ class executionModel extends model { $stories = array(); $plans = $this->dao->select('product, plan')->from(TABLE_PROJECTPRODUCT)->where('project')->eq($executionID)->fetchPairs('product', 'plan'); - $projectID = $this->dao->select('project')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch('project'); - $this->session->set('project', $projectID); + $execution = $this->fetchByID($executionID); + $project = $this->fetchByID($execution->project); + $this->session->set('project', $project->id); $this->loadModel('story'); $executionProducts = $this->loadModel('project')->getBranchesByProject($executionID); @@ -2925,13 +2943,15 @@ class executionModel extends model foreach($planStories as $id => $story) { - if($story->status != 'active' || (!empty($story->branch) && !empty($executionBranches) && !isset($executionBranches[$story->branch]))) unset($planStories[$id]); + if(!in_array($story->status, array('active', 'launched')) || (!empty($story->branch) && !empty($executionBranches) && !isset($executionBranches[$story->branch]))) unset($planStories[$id]); + if(strpos($project->storyType, $story->type) === false) unset($planStories[$id]); + if(!in_array($execution->attribute, array('mix', 'request', 'design')) && $story->type != 'story') unset($planStories[$id]); } $stories = array_merge($stories, array_keys($planStories)); } } - $this->linkStory($projectID, $stories); + $this->linkStory($project->id, $stories); $this->linkStory($executionID, $stories); return true; @@ -4584,10 +4604,11 @@ class executionModel extends model * * @param array $trees * @param bool $hasProduct + * @param array $gradeGroup * @access pubic * @return array */ - public function buildTree(array $trees, bool $hasProduct = true): array + public function buildTree(array $trees, bool $hasProduct = true, array $gradeGroup = array()): array { $treeData = array(); foreach($trees as $index => $tree) @@ -4611,9 +4632,29 @@ class executionModel extends model break; case 'story': $this->app->loadLang('story'); - $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}"); + $gradePairs = zget($gradeGroup, $tree->type, array()); + $grade = zget($gradePairs, $tree->grade, $tree->grade); + $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}"); $treeData[$index]['content'] = array( - 'html' => "', + 'html' => "', + ); + break; + case 'requirement': + $this->app->loadLang('requirement'); + $gradePairs = zget($gradeGroup, $tree->type, array()); + $grade = zget($gradePairs, $tree->grade, $tree->grade); + $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}"); + $treeData[$index]['content'] = array( + 'html' => "', + ); + break; + case 'epic': + $this->app->loadLang('epic'); + $gradePairs = zget($gradeGroup, $tree->type, array()); + $grade = zget($gradePairs, $tree->grade, $tree->grade); + $treeData[$index]['url'] = helper::createLink('execution', 'treeStory', "taskID={$tree->storyId}"); + $treeData[$index]['content'] = array( + 'html' => "', ); break; case 'branch': @@ -4632,7 +4673,7 @@ class executionModel extends model if(isset($tree->children)) { if($tree->type == 'task') $treeData[$index]['content']['html'] = "{$tree->title}"; - $treeData[$index]['items'] = $this->buildTree($tree->children, $hasProduct); + $treeData[$index]['items'] = $this->buildTree($tree->children, $hasProduct, $gradeGroup); } } return $treeData; @@ -4852,6 +4893,7 @@ class executionModel extends model $executionData->project = $projectID; $executionData->name = $project->name; $executionData->grade = 1; + $executionData->storyType = $project->storyType; $executionData->begin = $project->begin; $executionData->end = $project->end; $executionData->status = 'wait'; @@ -4911,6 +4953,7 @@ class executionModel extends model $postData = new stdclass(); $postData->project = $projectID; $postData->name = $project->name; + $postData->storyType = $project->storyType; $postData->begin = $project->begin; $postData->end = $project->end; $postData->realBegan = $project->realBegan ? $project->realBegan : null; diff --git a/module/execution/tao.php b/module/execution/tao.php index 2c7bd11e65..082a3d9408 100644 --- a/module/execution/tao.php +++ b/module/execution/tao.php @@ -122,6 +122,7 @@ class executionTao extends executionModel ->andWhere('t2.deleted')->eq(0) ->andWhere('t2.status')->ne('closed') ->andWhere('t2.stage')->in('wait,planned,projected,developing') + ->andWhere('t2.isParent')->eq('0') ->groupBy('project') ->fetchAll('project'); @@ -369,23 +370,54 @@ class executionTao extends executionModel $node->type = 'module'; $stories = isset($storyGroups[$node->root][$node->id]) ? $storyGroups[$node->root][$node->id] : array(); + $node->children = $this->buildStoryTree($stories, $taskGroups, $executionID, $node); + + /* Append for task of no story && node is not root. */ + if($node->id && isset($taskGroups[$node->id][0])) + { + $taskItems = $this->formatTasksForTree($taskGroups[$node->id][0]); + $node->tasksCount = count($taskItems); + foreach($taskItems as $taskItem) $node->children[] = $taskItem; + } + + return $node; + } + + /** + * 构造需求树。 + * Build story tree. + * + * @param array $stories + * @param array $taskGroups + * @param int $executionID + * @param object $node + * @param int $parentID + * @access protected + * @return array + */ + protected function buildStoryTree(array $stories, array $taskGroups, int $executionID, object $node, int $parentID = 0): array + { static $users, $avatarPairs; if(empty($users)) $users = $this->loadModel('user')->getPairs('noletter'); if(empty($avatarPairs)) $avatarPairs = $this->loadModel('user')->getAvatarPairs(); + $children = array(); foreach($stories as $story) { + if($story->parent != $parentID) continue; + $avatarAccount = empty($story->assignedTo) ? zget($story, 'openedBy', '') : $story->assignedTo; $userAvatar = zget($avatarPairs, $avatarAccount); $userAvatar = $userAvatar && $avatarAccount != 'closed' ? "" : strtoupper(mb_substr($avatarAccount, 0, 1, 'utf-8')); $storyItem = new stdclass(); - $storyItem->type = 'story'; + $storyItem->type = $story->type; $storyItem->id = 'story' . $story->id; $storyItem->title = $story->title; $storyItem->color = $story->color; $storyItem->pri = $story->pri; $storyItem->storyId = $story->id; + $storyItem->grade = $story->grade; $storyItem->openedBy = zget($users, $story->openedBy); $storyItem->assignedTo = zget($users, $story->assignedTo); $storyItem->url = helper::createLink('execution', 'storyView', "storyID=$story->id&execution=$executionID"); @@ -400,19 +432,16 @@ class executionTao extends executionModel $storyItem->tasksCount = count($taskItems); $storyItem->children = $taskItems; } + else + { + $storyItemChildren = $this->buildStoryTree($stories, $taskGroups, $executionID, $node, $story->id); + if($storyItemChildren) $storyItem->children = $storyItemChildren; + } - $node->children[] = $storyItem; + $children[] = $storyItem; } - /* Append for task of no story && node is not root. */ - if($node->id && isset($taskGroups[$node->id][0])) - { - $taskItems = $this->formatTasksForTree($taskGroups[$node->id][0]); - $node->tasksCount = count($taskItems); - foreach($taskItems as $taskItem) $node->children[] = $taskItem; - } - - return $node; + return $children; } /** diff --git a/module/execution/test/lib/execution.unittest.class.php b/module/execution/test/lib/execution.unittest.class.php index 4277468916..5c680c4a3d 100644 --- a/module/execution/test/lib/execution.unittest.class.php +++ b/module/execution/test/lib/execution.unittest.class.php @@ -179,9 +179,9 @@ class executionTest */ public function createTest($param = array(), $project = '', $teamMembers = array(), $days = '') { - $products = array(''); - $plans = array(''); - $whitelist = array(''); + $products = array(); + $plans = array(); + $whitelist = array(); $beginData = date('Y-m-d'); $endData = date('Y-m-d',strtotime("+$days day")); $delta = intval($days) + 1; diff --git a/module/execution/test/model/accessdenied.php b/module/execution/test/model/accessdenied.php index 0d93ffcba4..b389f86827 100755 --- a/module/execution/test/model/accessdenied.php +++ b/module/execution/test/model/accessdenied.php @@ -26,4 +26,4 @@ catch (Throwable $e) $result = 'fail'; } -r($result) && p() && e('fail'); // 权限不足跳转 +r($result) && p() && e("