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..70a302a296 --- /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','pending','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..ef2df5ad2f 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -1925,6 +1925,11 @@ IPD版: 50408 瀑布项目仪表盘区块没有提示文案 50411 专业研发看板中新建需求时选择其他区域保存后显示在了默认区域 50425 安装首页鼠标悬浮禅道软件公司时的链接不对 +50431 新手教程点击创建项目按钮报错 +50436 合并请求列表中开放中筛选标签不能正常筛选 +50437 无迭代项目下的bug转任务跳转到了添加执行表单 +50450 升级新版本时,有代码报错 +50453 多分支产品的关联项目数显示不对 50426 新手教程创建账号页面没有引导 50427 新手教程创建账号页面没有按顺序提示 50428 新手教程创建账号页面页面有多余提示框 @@ -1970,6 +1975,7 @@ IPD版: 50547 待处理用例编辑页保存后跳转不对 50551 搜索没有权限的执行的任务ID有报错 50552 提测版本关联了主干+分支时,测试下点测试单有报错 +>>>>>>> master 2024-04-30 20.0 完成的需求 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 7dc14f697d..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,7 +376,13 @@ 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; + } } } } 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/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/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/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/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/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 6186857589..1bc0e9ff86 100644 --- a/module/custom/model.php +++ b/module/custom/model.php @@ -634,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; @@ -722,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; @@ -833,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; @@ -863,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); @@ -871,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']; @@ -1032,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/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 3f21f4be78..6d1752c85b 100644 --- a/module/execution/control.php +++ b/module/execution/control.php @@ -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,12 +1752,12 @@ 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; - $kanbanGroup = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); + $orderBy = $groupBy == 'story' && $browseType == 'task' && !isset($this->lang->kanban->orderList[$orderBy]) ? 'id_asc' : $orderBy; + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, '', $orderBy); if(empty($kanbanGroup)) { $this->kanban->createExecutionLane($executionID, $browseType); - $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. */ @@ -1765,6 +1773,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(); @@ -1895,15 +1904,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(); } @@ -2228,7 +2239,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; @@ -2277,29 +2288,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])) { @@ -2311,14 +2314,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; @@ -2328,6 +2332,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; @@ -2338,7 +2344,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; @@ -3045,6 +3050,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(); @@ -3150,7 +3158,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/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 59822b9edf..74d7f1ff79 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: { @@ -101,6 +102,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; @@ -174,8 +176,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; } @@ -217,7 +219,14 @@ window.getItem = function(info) if(info.laneInfo.type == 'story' && priv.canAssignStory) assignLink = $.createLink('story', 'assignto', 'id=' + info.item.id + '&kanbanGroup=default&from=taskkanban'); 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(assignLink) avatar = "" + avatar + ""; + if(assignLink) + { + avatar = "" + avatar + ""; + } + else if(['parentStory', 'requirement', 'epic'].includes(info.laneInfo.type)) + { + avatar = '
'; + } const content = `
@@ -233,7 +242,7 @@ window.getItem = function(info) info.item.title = info.item.title.replaceAll(searchValue, "" + searchValue + ""); info.item.title = {html: info.item.title}; } - info.item.titleUrl = info.laneInfo.type == 'story' ? $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`) : $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`); + info.item.titleUrl = ['story', 'epic', 'requirement', 'parentStory'].includes(info.laneInfo.type) ? $.createLink('execution', 'storyView', `id=${info.item.id}&executionID=${executionID}`) : $.createLink(info.laneInfo.type, 'view', `id=${info.item.id}`); 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/ui/cfd.html.php b/module/execution/ui/cfd.html.php index b989e5c4c1..705b085f36 100644 --- a/module/execution/ui/cfd.html.php +++ b/module/execution/ui/cfd.html.php @@ -89,7 +89,7 @@ featureBar ); $index = 0; -$chartColors = array('#33B4DB', '#7ECF69', '#FFC73A', '#FF5A61', '#50C8D0', '#AF5AFF', '#4EA3FF', '#FF8C5A', '#6C73FF'); +$chartColors = array('#33B4DB', '#7ECF69', '#FFC73A', '#FF5A61', '#50C8D0', '#AF5AFF', '#4EA3FF', '#FF8C5A', '#6C73FF', '#B9E78C', '#E58CE7', '#EDE590', '#CFB5FA'); $chartSeries = array(); if(!empty($chartData['line'])) { diff --git a/module/execution/ui/kanban.html.php b/module/execution/ui/kanban.html.php index f6b5c7a4ad..7c03112fab 100644 --- a/module/execution/ui/kanban.html.php +++ b/module/execution/ui/kanban.html.php @@ -32,7 +32,7 @@ foreach($kanbanList as $current => $region) $group['maxLaneHeight'] = $execution->displayCards * 70; } - if(common::canModify('execution', $execution)) + if($canModifyExecution) { $group['canDrop'] = jsRaw('window.canDrop'); $group['onDrop'] = jsRaw('window.onDrop'); @@ -42,6 +42,7 @@ foreach($kanbanList as $current => $region) } $laneCount += isset($region['laneCount']) ? $region['laneCount'] : 0; + $links = isset($region['links']) ? $region['links'] : array(); } $operationMenu = array(); @@ -62,22 +63,23 @@ $canBatchCreateBug = $features['qa'] && $canModifyExecution && common::hasPriv $canImportBug = $features['qa'] && $canModifyExecution && common::hasPriv('execution', 'importBug') && $execution->multiple && $productID && $this->config->vision != 'lite'; $hasBugButton = $canCreateBug || $canBatchCreateBug; -$canCreateStory = $features['story'] && $canModifyExecution && common::hasPriv('story', 'create') && $productID && $this->config->vision != 'lite'; -$canBatchCreateStory = $features['story'] && $canModifyExecution && common::hasPriv('story', 'batchCreate') && $productID && $this->config->vision != 'lite'; -$canLinkStory = $features['story'] && $canModifyExecution && common::hasPriv('execution', 'linkStory') && !empty($execution->hasProduct) && $productID && $this->config->vision != 'lite'; -$canLinkStoryByPlan = $features['story'] && $canModifyExecution && common::hasPriv('execution', 'importplanstories') && !empty($project->hasProduct) && $productID && $this->config->vision != 'lite'; -$hasStoryButton = $canCreateStory || $canBatchCreateStory || $canLinkStory || $canLinkStoryByPlan; +$canCreateStory = $features['story'] && $canModifyExecution && common::hasPriv('story', 'create') && common::canModify('execution', $execution) && $productID && $this->config->vision != 'lite'; +$canBatchCreateStory = $features['story'] && $canModifyExecution && common::hasPriv('story', 'batchCreate') && common::canModify('execution', $execution) && $productID && $this->config->vision != 'lite'; + +$canLinkStory = $features['story'] && $canModifyExecution && common::hasPriv('execution', 'linkStory') && !empty($execution->hasProduct) && common::canModify('execution', $execution) && $productID && $this->config->vision != 'lite'; +$canLinkStoryByPlan = $features['story'] && $canModifyExecution && common::hasPriv('execution', 'importplanstories') && !empty($project->hasProduct) && common::canModify('execution', $execution) && $productID && $this->config->vision != 'lite'; +$hasStoryButton = $features['story'] && ($canCreateStory || $canBatchCreateStory || $canLinkStory || $canLinkStoryByPlan); $hasTaskButton = $canCreateTask || $canBatchCreateTask || $canImportBug; $createMenu = array(); $modal = $productID ? 'modal' : false; -if($canCreateStory) $createMenu[] = array('text' => $lang->story->create, 'url' => $productID ? helper::createLink('story', 'create', "productID=$productID&branch=0&moduleID=0&story=0&execution=$execution->id") : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); -if($canBatchCreateStory) $createMenu[] = array('text' => $lang->story->batchCreate, 'url' => $productID ? (count($productNames) > 1 ? '#batchCreateStory' : helper::createLink('story', 'batchCreate', "productID=$productID&branch=$branchID&moduleID=0&story=0&execution=$execution->id")) : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); -if($canLinkStory) $createMenu[] = array('text' => $lang->execution->linkStory, 'url' => $productID ? helper::createLink('execution', 'linkStory', "execution=$execution->id") : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); -if($canLinkStoryByPlan) $createMenu[] = array('text' => $lang->execution->linkStoryByPlan, 'url' => $productID ? "#linkStoryByPlan" : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'sm', 'data-on' => 'click', 'data-call' => 'checkProducts'); +if($canCreateStory) $createMenu[] = array('text' => $lang->story->create, 'url' => $productID ? helper::createLink('story', 'create', "productID=$productID&branch=0&moduleID=0&story=0&execution=$execution->id") : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); +if($canBatchCreateStory) $createMenu[] = array('text' => $lang->story->batchCreate, 'url' => $productID ? (count($productNames) > 1 ? '#batchCreateStory' : helper::createLink('story', 'batchCreate', "productID=$productID&branch=$branchID&moduleID=0&story=0&execution=$execution->id")) : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); +if($canLinkStory) $createMenu[] = array('text' => $lang->execution->linkStory, 'url' => $productID ? helper::createLink('execution', 'linkStory', "execution=$execution->id") : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'lg', 'data-on' => 'click', 'data-call' => 'checkProducts'); +if($canLinkStoryByPlan) $createMenu[] = array('text' => $lang->execution->linkStoryByPlan, 'url' => $productID ? "#linkStoryByPlan" : 'javascript:;', 'data-toggle' => $modal, 'data-size' => 'sm', 'data-on' => 'click', 'data-call' => 'checkProducts'); if($hasStoryButton && $hasTaskButton) $createMenu[] = array('type' => 'divider'); -if($canCreateBug) $createMenu[] = array('text' => $lang->bug->create, 'url' => helper::createLink('bug', 'create', "productID=$productID&branch=0&extra=executionID=$execution->id"), 'data-toggle' => 'modal', 'data-size' => 'lg'); +if($canCreateBug) $createMenu[] = array('text' => $lang->bug->create, 'url' => helper::createLink('bug', 'create', "productID=$productID&branch=0&extra=executionID=$execution->id"), 'data-toggle' => 'modal', 'data-size' => 'lg'); if($canBatchCreateBug) { if(count($productNames) > 1) @@ -101,6 +103,7 @@ jsVar('storyLang', $lang->story); jsVar('executionLang', $lang->execution); jsVar('laneLang', $lang->kanbanlane); jsVar('cardLang', $lang->kanbancard); +jsVar('ERURColumn', array_keys($lang->kanban->ERURColumn)); jsVar('bugLang', $lang->bug); jsVar('taskLang', $lang->task); jsVar('executionID', $execution->id); @@ -114,49 +117,50 @@ jsVar('minColWidth', $execution->fluidBoard == '0' ? $execution->colWidth : $exe jsVar('maxColWidth', $execution->fluidBoard == '0' ? $execution->colWidth : $execution->maxColWidth); jsVar('priv', array( - 'canCreateTask' => $canCreateTask, - 'canBatchCreateTask' => $canBatchCreateTask, - 'canImportBug' => $canImportBug, - 'canCreateBug' => $canCreateBug, - 'canBatchCreateBug' => $canBatchCreateBug, - 'canCreateStory' => $canCreateStory, - 'canBatchCreateStory' => $canBatchCreateStory, - 'canLinkStory' => $canLinkStory, - 'canLinkStoryByPlan' => $canLinkStoryByPlan, - 'canViewBug' => common::hasPriv('bug', 'view'), - 'canAssignBug' => Common::hasPriv('bug', 'assignto') && $canModifyExecution, - 'canConfirmBug' => Common::hasPriv('bug', 'confirm') && $canModifyExecution, - 'canResolveBug' => Common::hasPriv('bug', 'resolve') && $canModifyExecution, - 'canCopyBug' => Common::hasPriv('bug', 'create') && $canModifyExecution, - 'canEditBug' => Common::hasPriv('bug', 'edit') && $canModifyExecution, - 'canDeleteBug' => Common::hasPriv('bug', 'delete') && $canModifyExecution, - 'canActivateBug' => Common::hasPriv('bug', 'activate') && $canModifyExecution, - 'canViewTask' => common::hasPriv('task', 'view'), - 'canAssignTask' => common::hasPriv('task', 'assignto') && $canModifyExecution, - 'canFinishTask' => common::hasPriv('task', 'finish') && $canModifyExecution, - 'canPauseTask' => common::hasPriv('task', 'pause') && $canModifyExecution, - 'canCancelTask' => common::hasPriv('task', 'cancel') && $canModifyExecution, - 'canCloseTask' => common::hasPriv('task', 'close') && $canModifyExecution, - 'canActivateTask' => common::hasPriv('task', 'activate') && $canModifyExecution, - 'canActivateStory' => common::hasPriv('story', 'activate') && $canModifyExecution, - 'canStartTask' => common::hasPriv('task', 'start') && $canModifyExecution, - 'canRestartTask' => common::hasPriv('task', 'restart') && $canModifyExecution, - 'canEditTask' => common::hasPriv('task', 'edit') && $canModifyExecution, - 'canDeleteTask' => common::hasPriv('task', 'delete') && $canModifyExecution, - 'canRecordWorkhourTask' => common::hasPriv('task', 'recordWorkhour') && $canModifyExecution, - 'canToStoryBug' => common::hasPriv('story', 'create') && $canModifyExecution, - 'canAssignStory' => common::hasPriv('story', 'assignto') && $canModifyExecution, - 'canEditStory' => common::hasPriv('story', 'edit') && $canModifyExecution, - 'canDeleteStory' => common::hasPriv('story', 'delete') && $canModifyExecution, - 'canChangeStory' => common::hasPriv('story', 'change') && $canModifyExecution, - 'canCloseStory' => common::hasPriv('story', 'close') && $canModifyExecution, - 'canUnlinkStory' => (common::hasPriv('execution', 'unlinkStory') && !empty($execution->hasProduct)) && $canModifyExecution, - 'canViewStory' => common::hasPriv('execution', 'storyView') + 'canCreateTask' => $canCreateTask, + 'canBatchCreateTask' => $canBatchCreateTask, + 'canImportBug' => $canImportBug, + 'canCreateBug' => $canCreateBug, + 'canBatchCreateBug' => $canBatchCreateBug, + 'canCreateStory' => $canCreateStory, + 'canBatchCreateStory' => $canBatchCreateStory, + 'canLinkStory' => $canLinkStory, + 'canLinkStoryByPlan' => $canLinkStoryByPlan, + 'canViewBug' => common::hasPriv('bug', 'view'), + 'canAssignBug' => common::hasPriv('bug', 'assignto') && $canModifyExecution, + 'canConfirmBug' => common::hasPriv('bug', 'confirm') && $canModifyExecution, + 'canResolveBug' => common::hasPriv('bug', 'resolve') && $canModifyExecution, + 'canCopyBug' => common::hasPriv('bug', 'create') && $canModifyExecution, + 'canEditBug' => common::hasPriv('bug', 'edit') && $canModifyExecution, + 'canDeleteBug' => common::hasPriv('bug', 'delete') && $canModifyExecution, + 'canActivateBug' => common::hasPriv('bug', 'activate') && $canModifyExecution, + 'canViewTask' => common::hasPriv('task', 'view'), + 'canAssignTask' => common::hasPriv('task', 'assignto') && $canModifyExecution, + 'canFinishTask' => common::hasPriv('task', 'finish') && $canModifyExecution, + 'canPauseTask' => common::hasPriv('task', 'pause') && $canModifyExecution, + 'canCancelTask' => common::hasPriv('task', 'cancel') && $canModifyExecution, + 'canCloseTask' => common::hasPriv('task', 'close'), + 'canActivateTask' => common::hasPriv('task', 'activate') && $canModifyExecution, + 'canActivateStory' => common::hasPriv('story', 'activate') && $canModifyExecution, + 'canStartTask' => common::hasPriv('task', 'start') && $canModifyExecution, + 'canRestartTask' => common::hasPriv('task', 'restart') && $canModifyExecution, + 'canEditTask' => common::hasPriv('task', 'edit') && $canModifyExecution, + 'canDeleteTask' => common::hasPriv('task', 'delete') && $canModifyExecution, + 'canRecordWorkhourTask' => common::hasPriv('task', 'recordWorkhour') && $canModifyExecution, + 'canToStoryBug' => common::hasPriv('story', 'create') && $canModifyExecution, + 'canAssignStory' => common::hasPriv('story', 'assignto') && $canModifyExecution, + 'canEditStory' => common::hasPriv('story', 'edit') && $canModifyExecution, + 'canDeleteStory' => common::hasPriv('story', 'delete') && $canModifyExecution, + 'canChangeStory' => common::hasPriv('story', 'change') && $canModifyExecution, + 'canCloseStory' => common::hasPriv('story', 'close'), + 'canUnlinkStory' => (common::hasPriv('execution', 'unlinkStory') && !empty($execution->hasProduct)) && $canModifyExecution, + 'canViewStory' => common::hasPriv('execution', 'storyView') ) ); if(!$features['story']) unset($lang->kanban->type['story']); if(!$features['qa']) unset($lang->kanban->type['bug']); +unset($lang->kanban->type['epic'], $lang->kanban->type['requirement']); $executionItems = array(); foreach($executionList as $childExecution) $executionItems[] = array('text' => $childExecution->name, 'url' => createLink('execution', 'kanban', "kanbanID={$childExecution->id}")); @@ -291,7 +295,10 @@ div ( set::key('kanban'), set::items($kanbanList), - set::height('calc(100vh - 120px)') + set::height('calc(100vh - 120px)'), + set::links($links), + set::selectable(true), + set::showLinkOnSelected(true) ) ); diff --git a/module/execution/ui/linkstory.html.php b/module/execution/ui/linkstory.html.php index a2526732bf..8be372119f 100644 --- a/module/execution/ui/linkstory.html.php +++ b/module/execution/ui/linkstory.html.php @@ -12,12 +12,10 @@ declare(strict_types=1); namespace zin; -data('activeMenuID', $storyType); - featureBar(li ( setClass('nav-item'), - a(setClass('active'), $lang->execution->linkStory), + a(setClass('active'), $lang->execution->linkStory) )); searchForm @@ -32,7 +30,6 @@ $cols = $config->execution->linkStory->dtable->fieldList; $cols['module']['map'] = $modules; $cols['product']['map'] = $productPairs; -if($storyType == 'requirement') $cols['title']['title'] = str_replace($lang->SRCommon, $lang->URCommon, $lang->story->title); if($productType != 'normal') { $cols['branch']['title'] = $lang->product->branchName[$productType]; @@ -43,7 +40,8 @@ else } jsVar('branchGroups', $branchGroups); - +jsVar('gradeGroup', $gradeGroup); +jsVar('showGrade', $showGrade); $footToolbar['items'][] = array( 'text' => $lang->save, @@ -63,7 +61,6 @@ if(!isInModal()) $objectType = $object->type == 'project' ? 'projectstory' : 'execution'; -jsVar('childrenAB', $lang->story->childrenAB); dtable ( set::groupDivider(true), diff --git a/module/execution/ui/story.html.php b/module/execution/ui/story.html.php index 405d06bdfd..e46b87d767 100644 --- a/module/execution/ui/story.html.php +++ b/module/execution/ui/story.html.php @@ -11,11 +11,13 @@ declare(strict_types=1); namespace zin; -data('activeMenuID', $storyType); -jsVar('executionID', $execution->id); -jsVar('childrenAB', $lang->story->childrenAB); -jsVar('modulePairs', $modulePairs); -jsVar('hasProduct', $execution->hasProduct); +data('activeMenuID', $storyType); +jsVar('executionID', $execution->id); +jsVar('childrenAB', $lang->story->childrenAB); +jsVar('modulePairs', $modulePairs); +jsVar('oldShowGrades', $showGrades); +jsVar('gradeGroup', $gradeGroup); +jsVar('hasProduct', $execution->hasProduct); jsVar('linkedTaskStories', $linkedTaskStories); jsVar('URChanged', $lang->story->URChanged); jsVar('confirmStoryToTask', $lang->execution->confirmStoryToTask); @@ -26,6 +28,23 @@ jsVar('hourPointNotError', sprintf($lang->story->float, $lang->story->convertRe /* Show feature bar. */ featureBar ( + $config->edition == 'ipd' ? to::leading + ( + picker + ( + set::tree(), + set::name('showGrades'), + set::items($gradeMenu), + set::search(false), + set::multiple(true), + set::width('150px'), + setStyle('justify-content', 'center'), + set::display($lang->story->viewAllGrades), + set::menu(array('checkbox' => true)), + set::value($showGrades), + set::onPopHidden(jsRaw('setShowGrades')) + ) + ) : null, set::current($this->session->storyBrowseType), set::link(createLink($app->rawModule, $app->rawMethod, "&executionID=$execution->id&storyType=$storyType&orderBy=$orderBy&type={key}")), li(searchToggle(set::module('executionStory'), set::open($type == 'bysearch'))) @@ -69,12 +88,22 @@ if(!$product) $product = new stdclass(); $product->id = 0; } -$canModifyProduct = common::canModify('product', $product); -$canModifyExecution = common::canModify('execution', $execution); -$canCreate = $canModifyProduct && $canModifyExecution && hasPriv('story', 'create'); -$canBatchCreate = $canModifyProduct && $canModifyExecution && hasPriv('story', 'batchCreate'); -$createLink = createLink('story', 'create', "product={$product->id}&branch=0&moduleID={$moduleID}&storyID=0&objectID={$execution->id}&bugID=0&planID=0&todoID=0&extra=&storyType={$storyType}") . "#app={$app->tab}"; -$batchCreateLink = createLink('story', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}&plan=0&storyType={$storyType}") . "#app={$app->tab}"; + +$canModifyProduct = common::canModify('product', $product); +$canModifyExecution = common::canModify('execution', $execution); +$canOpreate['create'] = $canModifyProduct && $canModifyExecution && hasPriv('story', 'create'); +$canOpreate['batchCreate'] = $canModifyProduct && $canModifyExecution && hasPriv('story', 'batchCreate'); +$canOpreate['createEpic'] = $canModifyProduct && $canModifyExecution && hasPriv('epic', 'create') && strpos($project->storyType, 'epic') !== false; +$canOpreate['batchCreateEpic'] = $canModifyProduct && $canModifyExecution && hasPriv('epic', 'batchCreate') && strpos($project->storyType, 'epic') !== false; +$canOpreate['createRequirement'] = $canModifyProduct && $canModifyExecution && hasPriv('requirement', 'create') && strpos($project->storyType, 'requirement') !== false; +$canOpreate['batchCreateRequirement'] = $canModifyProduct && $canModifyExecution && hasPriv('requirement', 'batchCreate') && strpos($project->storyType, 'requirement') !== false; + +$createLink = createLink('story', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}&bugID=0&planID=0&todoID=0&extra=&storyType={$storyType}") . "#app={$app->tab}"; +$batchCreateLink = createLink('story', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}&plan=0&storyType={$storyType}") . "#app={$app->tab}"; +$createEpicLink = createLink('epic', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}") . "#app={$app->tab}"; +$batchCreateEpicLink = createLink('epic', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}") . "#app={$app->tab}"; +$createRequirementLink = createLink('requirement', 'create', "product={$product->id}&branch=0&moduleID=0&storyID=0&objectID={$execution->id}") . "#app={$app->tab}"; +$batchCreateRequirementLink = createLink('requirement', 'batchCreate', "productID={$product->id}&branch=0&moduleID=0&storyID=0&executionID={$execution->id}") . "#app={$app->tab}"; /* Tutorial create link. */ if(commonModel::isTutorialMode()) @@ -84,8 +113,26 @@ if(commonModel::isTutorialMode()) $canBatchCreate = false; } -$createItem = array('text' => $lang->story->create, 'url' => $createLink); -$batchCreateItem = array('text' => $lang->story->batchCreate, 'url' => $batchCreateLink); +$createItems = array(); +$batchItems = array(); +if($canOpreate['batchCreate']) $batchItems[] = array('text' => $lang->SRCommon, 'url' => $batchCreateLink); +if(in_array($execution->attribute, array('mix', 'request', 'design')) || !$execution->multiple) +{ + if($canOpreate['createRequirement']) $createItems[] = array('text' => $lang->requirement->create, 'url' => $createRequirementLink); + if($canOpreate['createEpic']) $createItems[] = array('text' => $lang->epic->create, 'url' => $createEpicLink); + if($canOpreate['batchCreateRequirement']) $batchItems[] = array('text' => $lang->URCommon, 'url' => $batchCreateRequirementLink); + if($canOpreate['batchCreateEpic']) $batchItems[] = array('text' => $lang->ERCommon, 'url' => $batchCreateEpicLink); +} + +if(count($batchItems) > 1) +{ + $createItems[] = array('text' => $lang->story->batchCreate, 'items' => $batchItems); +} +else +{ + $batchItems[0]['text'] = $lang->story->batchCreate; + $createItems = array_merge($createItems, $batchItems); +} $canLinkStory = ($execution->hasProduct || $app->tab == 'execution') && $canModifyProduct && $canModifyExecution && hasPriv('execution', 'linkStory'); $canlinkPlanStory = ($execution->hasProduct || $app->tab == 'execution') && $canModifyProduct && $canModifyExecution && hasPriv('execution', 'importPlanStories') && $storyType == 'story'; @@ -101,6 +148,32 @@ if(commonModel::isTutorialMode()) $linkItem = array('text' => $lang->story->linkStory, 'url' => $linkStoryUrl, 'data-app' => $app->tab); $linkPlanItem = array('text' => $lang->execution->linkStoryByPlan, 'url' => '#linkStoryByPlan', 'data-toggle' => 'modal', 'data-size' => 'sm'); +$createBtnGroup = null; +if($canOpreate['create']) +{ + $createBtnGroup = btngroup + ( + btn + ( + setClass('btn secondary'), + set::icon('plus'), + set::url($createLink), + $lang->story->create + ), + dropdown + ( + btn(setClass('btn secondary dropdown-toggle'), + setStyle(array('padding' => '6px', 'border-radius' => '0 2px 2px 0'))), + set::items($createItems), + set::placement('bottom-end') + ) + ); +} +elseif(count($createItems) == 1) +{ + $createBtnGroup = item(set($createItems[0] + array('class' => 'btn secondary', 'icon' => 'plus'))); +} + $product ? toolbar ( common::hasPriv('execution', 'storykanban') && $storyType == 'story' ? btnGroup @@ -137,25 +210,7 @@ $product ? toolbar 'data-toggle' => 'modal' ))) : null, - $canCreate && $canBatchCreate ? btngroup - ( - btn - ( - setClass('btn secondary'), - set::icon('plus'), - set::url($createLink), - $lang->story->create - ), - dropdown - ( - btn(setClass('btn secondary dropdown-toggle'), - setStyle(array('padding' => '6px', 'border-radius' => '0 2px 2px 0'))), - set::items(array_filter(array($createItem, $batchCreateItem))), - set::placement('bottom-end') - ) - ) : null, - $canCreate && !$canBatchCreate ? item(set($createItem + array('class' => 'btn primary', 'icon' => 'plus'))) : null, - $canBatchCreate && !$canCreate ? item(set($batchCreateItem + array('class' => 'btn primary', 'icon' => 'plus'))) : null, + $createBtnGroup, $canLinkStory && $canlinkPlanStory ? btngroup ( @@ -392,14 +447,18 @@ foreach($setting as $col) if(!$execution->hasProduct && $col['name'] == 'branch') continue; if(!$execution->hasProduct && !$execution->multiple && $col['name'] == 'plan') continue; - if($col['name'] == 'title') $col['link'] = createLink('execution', 'storyView', array('storyID' => '{id}', 'execution' => $execution->id)); + if($col['name'] == 'title') + { + $col['link'] = createLink('execution', 'storyView', array('storyID' => '{id}', 'execution' => $execution->id)); + $col['title'] = $this->lang->story->name; + } $cols[] = $col; } /* DataTable data. */ $data = array(); -$actionMenus = array('submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'createTask', 'batchCreateTask', 'divider', 'storyEstimate', 'testcase', 'unlink', 'processStoryChange'); +$actionMenus = array('submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'createTask', 'batchCreateTask', 'divider', 'storyEstimate', 'testcase', 'batchCreate', 'unlink', 'processStoryChange'); if(empty($execution->hasProduct) && empty($execution->multiple)) { $actionMenus = array('submitreview', 'recall', 'recalledchange', 'review', 'dropdown', 'createTask', 'batchCreateTask', 'edit', 'divider', 'storyEstimate', 'testcase', 'batchCreate', 'close', 'processStoryChange'); @@ -417,21 +476,14 @@ foreach($stories as $story) { $story->moduleID = $story->module; $story->from = 'execution'; - $data[] = $this->story->formatStoryForList($story, $options, $storyType); + $data[] = $this->story->formatStoryForList($story, $options, $storyType, $maxGradeGroup); if(!isset($story->children)) continue; - - /* Children. */ - foreach($story->children as $key => $child) - { - $child->moduleID = $child->module; - $data[] = $this->story->formatStoryForList($child, $options, $storyType); - } } jsVar('cases', $storyCases); jsVar('summary', $summary); +jsVar('checkedSummary', $lang->product->checkedAllSummary); jsVar('storyType', $storyType); -jsVar('checkedSummary', $storyType == 'story' ? $lang->product->checkedSRSummary : $lang->product->checkedURSummary); dtable ( setClass('shadow rounded'), @@ -453,7 +505,7 @@ dtable set::checkInfo(jsRaw('function(checkedIDList){return window.setStatistics(this, checkedIDList);}')), set::emptyTip($lang->story->noStory), set::createTip($lang->story->create), - set::createLink($canCreate ? $createLink : '') + set::createLink($canOpreate['create'] ? $createLink : '') ); render(); diff --git a/module/execution/ui/taskkanban.html.php b/module/execution/ui/taskkanban.html.php index 7d707dc267..205a074729 100644 --- a/module/execution/ui/taskkanban.html.php +++ b/module/execution/ui/taskkanban.html.php @@ -63,6 +63,7 @@ jsVar('lanePairs', $lanePairs); jsVar('colPairs', $colPairs); jsVar('vision', $config->vision); jsVar('colorList', $config->kanban->cardColorList); +jsVar('ERURColumn', array_keys($lang->kanban->ERURColumn)); jsVar('executionID', $executionID); jsVar('productID', $productID); jsVar('kanbanGroup', $kanbanGroup); @@ -125,13 +126,22 @@ jsVar('canImportBug', $features['qa']); jsVar('canChangeObject', common::canModify('execution', $execution)); $canCreateObject = ($canCreateTask or $canBatchCreateTask or $canImportBug or $canCreateBug or $canBatchCreateBug or $canCreateStory or $canBatchCreateStory or $canLinkStory or $canLinkStoryByPlan); +if($execution->type != 'stage' && !in_array($execution->attribute, array('mix', 'request', 'design'))) +{ + unset($lang->kanban->type['epic'], $lang->kanban->type['requirement']); +} +else +{ + if(strpos($project->storyType, 'epic') === false) unset($lang->kanban->type['epic']); + if(strpos($project->storyType, 'requirement') === false) unset($lang->kanban->type['requirement']); +} row ( setClass('items-center justify-between mb-3'), cell ( setClass('flex'), - $features['qa'] ? inputControl + inputControl ( setClass('c-type'), picker @@ -143,7 +153,7 @@ row set::required(true), set::onchange('changeBrowseType()') ) - ) : null, + ), $browseType != 'all' ? inputControl ( setClass('c-group ml-5'), @@ -233,7 +243,10 @@ div ( set::key('kanban'), set::items($kanbanGroup), - set::height('calc(100vh - 120px)') + set::height('calc(100vh - 120px)'), + set::links($links), + set::selectable(true), + set::showLinkOnSelected(true) ) ); diff --git a/module/execution/ui/view.html.php b/module/execution/ui/view.html.php index af6d89b6a9..5f84e956d9 100644 --- a/module/execution/ui/view.html.php +++ b/module/execution/ui/view.html.php @@ -195,7 +195,7 @@ div setClass('text-lg font-bold'), $statData->storyCount ), - $lang->story->common + $lang->common->story ) : null, div ( diff --git a/module/execution/zen.php b/module/execution/zen.php index 0a981e45c2..544f2bd9b3 100644 --- a/module/execution/zen.php +++ b/module/execution/zen.php @@ -211,7 +211,7 @@ class executionZen extends execution $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts($storyIdList, $executionID); $this->view->storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList, $executionID); $this->view->storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); - $this->view->summary = $this->loadModel('product')->summary($stories, $storyType); + $this->view->summary = $this->loadModel('product')->summary($stories, 'all'); } /** @@ -257,11 +257,18 @@ class executionZen extends execution } } + $project = $this->loadModel('project')->fetchByID($execution->project); + if(!($execution->type == 'stage' && in_array($execution->attribute, array('mix', 'request', 'design'))) && $project->multiple) $project->storyType = 'story'; + $productPairs = $this->loadModel('product')->getProductPairsByProject($execution->id); // Get execution's product. if(empty($productID)) $productID = (int)key($productPairs); $this->assignModuleForStory($type, $param, $storyType, $execution, $productID); + $gradeList = $this->loadModel('story')->getGradeList(''); + $gradeGroup = array(); + foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; + /* Assign. */ $this->view->title = $execution->name . $this->lang->hyphen . $this->lang->execution->story; $this->view->storyType = $storyType; @@ -274,6 +281,10 @@ class executionZen extends execution $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->multiBranch = $multiBranch; $this->view->execution = $execution; + $this->view->gradeMenu = $this->loadModel('story')->getGradeMenu($storyType, $project); + $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); + $this->view->gradeGroup = $gradeGroup; + $this->view->showGrades = isset($this->config->execution->showGrades) ? $this->config->execution->showGrades : $this->story->getDefaultShowGrades($this->view->gradeMenu); $this->view->canBeChanged = common::canModify('execution', $execution); // Determines whether an object is editable. $this->view->branchPairs = $this->loadModel('branch')->getPairs($productID, 'withClosed'); $this->view->linkedTaskStories = $this->loadModel('story')->getIdListWithTask($execution->id); @@ -362,6 +373,7 @@ class executionZen extends execution $this->view->allPlans = $allPlans; $this->view->hiddenPlan = $project->model !== 'scrum'; $this->view->execution = $execution; + $this->view->project = $project; $this->view->canBeChanged = common::canModify('execution', $execution); } diff --git a/module/group/lang/de.php b/module/group/lang/de.php index e712bf2f80..927c9be515 100644 --- a/module/group/lang/de.php +++ b/module/group/lang/de.php @@ -235,6 +235,11 @@ $lang->group->package->importStory = 'Import Story'; $lang->group->package->deleteStory = 'Delete Story'; $lang->group->package->reviewStory = 'Review Story'; $lang->group->package->deleteProduct = 'Delete Product'; +$lang->group->package->browseEpic = 'Browse Epic'; +$lang->group->package->manageEpic = 'Manage Epic'; +$lang->group->package->importEpic = 'Import Epic'; +$lang->group->package->deleteEpic = 'Delete Epic'; +$lang->group->package->reviewEpic = 'Review Epic'; $lang->group->package->browseRequirement = 'Browse Requirement'; $lang->group->package->manageRequirement = 'Manage Requirement'; $lang->group->package->importRequirement = 'Import Requirement'; @@ -250,6 +255,7 @@ $lang->group->package->deleteRelease = 'Delete Release'; $lang->group->package->releaseNotify = 'Release Notify'; $lang->group->package->projectPlan = 'Project Plan'; $lang->group->package->manageProjectStory = 'Manage Project Story'; +$lang->group->package->importProjectStory = 'Import Project Story'; $lang->group->package->browseProject = 'Browse Project'; $lang->group->package->manageProject = 'Manage Project'; $lang->group->package->importProject = 'Import Project'; diff --git a/module/group/lang/en.php b/module/group/lang/en.php index 3a1a220c5c..e2bd0f5985 100644 --- a/module/group/lang/en.php +++ b/module/group/lang/en.php @@ -235,6 +235,11 @@ $lang->group->package->importStory = 'Import Story'; $lang->group->package->deleteStory = 'Delete Story'; $lang->group->package->reviewStory = 'Review Story'; $lang->group->package->deleteProduct = 'Delete Product'; +$lang->group->package->browseEpic = 'Browse Epic'; +$lang->group->package->manageEpic = 'Manage Epic'; +$lang->group->package->importEpic = 'Import Epic'; +$lang->group->package->deleteEpic = 'Delete Epic'; +$lang->group->package->reviewEpic = 'Review Epic'; $lang->group->package->browseRequirement = 'Browse Requirement'; $lang->group->package->manageRequirement = 'Manage Requirement'; $lang->group->package->importRequirement = 'Import Requirement'; @@ -250,6 +255,7 @@ $lang->group->package->deleteRelease = 'Delete Release'; $lang->group->package->releaseNotify = 'Release Notify'; $lang->group->package->projectPlan = 'Project Plan'; $lang->group->package->manageProjectStory = 'Manage Project Story'; +$lang->group->package->importProjectStory = 'Import Project Story'; $lang->group->package->browseProject = 'Browse Project'; $lang->group->package->manageProject = 'Manage Project'; $lang->group->package->importProject = 'Import Project'; diff --git a/module/group/lang/fr.php b/module/group/lang/fr.php index a9fffb6045..24181d925c 100644 --- a/module/group/lang/fr.php +++ b/module/group/lang/fr.php @@ -235,6 +235,11 @@ $lang->group->package->importStory = 'Import Story'; $lang->group->package->deleteStory = 'Delete Story'; $lang->group->package->reviewStory = 'Review Story'; $lang->group->package->deleteProduct = 'Delete Product'; +$lang->group->package->browseEpic = 'Browse Epic'; +$lang->group->package->manageEpic = 'Manage Epic'; +$lang->group->package->importEpic = 'Import Epic'; +$lang->group->package->deleteEpic = 'Delete Epic'; +$lang->group->package->reviewEpic = 'Review Epic'; $lang->group->package->browseRequirement = 'Browse Requirement'; $lang->group->package->manageRequirement = 'Manage Requirement'; $lang->group->package->importRequirement = 'Import Requirement'; @@ -250,6 +255,7 @@ $lang->group->package->deleteRelease = 'Delete Release'; $lang->group->package->releaseNotify = 'Release Notify'; $lang->group->package->projectPlan = 'Project Plan'; $lang->group->package->manageProjectStory = 'Manage Project Story'; +$lang->group->package->importProjectStory = 'Import Project Story'; $lang->group->package->browseProject = 'Browse Project'; $lang->group->package->manageProject = 'Manage Project'; $lang->group->package->importProject = 'Import Project'; diff --git a/module/group/lang/resource.php b/module/group/lang/resource.php index f3526b6936..bd86754efd 100644 --- a/module/group/lang/resource.php +++ b/module/group/lang/resource.php @@ -19,7 +19,8 @@ $lang->moduleOrder[15] = 'program'; $lang->moduleOrder[20] = 'personnel'; $lang->moduleOrder[25] = 'product'; $lang->moduleOrder[30] = 'story'; -$lang->moduleOrder[31] = 'requirement'; +$lang->moduleOrder[32] = 'requirement'; +$lang->moduleOrder[33] = 'epic'; $lang->moduleOrder[35] = 'productplan'; $lang->moduleOrder[40] = 'release'; @@ -270,6 +271,13 @@ $lang->resource->projectstory->linkStory = 'linkStory'; $lang->resource->projectstory->unlinkStory = 'unlinkStory'; $lang->resource->projectstory->batchUnlinkStory = 'batchUnlinkStory'; $lang->resource->projectstory->importplanstories = 'importplanstories'; +$lang->resource->projectstory->report = 'report'; +$lang->resource->projectstory->export = 'export'; +$lang->resource->projectstory->batchReview = 'batchReview'; +$lang->resource->projectstory->batchClose = 'batchClose'; +$lang->resource->projectstory->batchChangePlan = 'batchChangePlan'; +$lang->resource->projectstory->batchAssignTo = 'batchAssignTo'; +$lang->resource->projectstory->batchEdit = 'batchEdit'; $lang->projectstory->methodOrder[5] = 'story'; $lang->projectstory->methodOrder[10] = 'track'; @@ -277,6 +285,13 @@ $lang->projectstory->methodOrder[15] = 'view'; $lang->projectstory->methodOrder[20] = 'linkStory'; $lang->projectstory->methodOrder[25] = 'unlinkStory'; $lang->projectstory->methodOrder[23] = 'importplanstories'; +$lang->projectstory->methodOrder[30] = 'report'; +$lang->projectstory->methodOrder[35] = 'export'; +$lang->projectstory->methodOrder[40] = 'batchReview'; +$lang->projectstory->methodOrder[45] = 'batchClose'; +$lang->projectstory->methodOrder[50] = 'batchChangePlan'; +$lang->projectstory->methodOrder[55] = 'batchAssignTo'; +$lang->projectstory->methodOrder[60] = 'batchEdit'; /* Release. */ $lang->resource->projectrelease = new stdclass(); @@ -431,6 +446,7 @@ $lang->resource->product = new stdclass(); $lang->resource->product->index = 'indexAction'; $lang->resource->product->browse = 'browse'; $lang->resource->product->requirement = 'requirement'; +$lang->resource->product->epic = 'epic'; $lang->resource->product->create = 'create'; $lang->resource->product->view = 'view'; $lang->resource->product->edit = 'edit'; @@ -455,6 +471,7 @@ $lang->resource->product->unbindWhitelist = 'unbindWhitelist'; $lang->product->methodOrder[0] = 'index'; $lang->product->methodOrder[5] = 'browse'; $lang->product->methodOrder[6] = 'requirement'; +$lang->product->methodOrder[7] = 'epic'; $lang->product->methodOrder[10] = 'create'; $lang->product->methodOrder[15] = 'view'; $lang->product->methodOrder[20] = 'edit'; @@ -526,9 +543,9 @@ $lang->resource->story->batchChangeBranch = 'batchChangeBranch'; $lang->resource->story->batchChangeStage = 'batchChangeStage'; $lang->resource->story->batchAssignTo = 'batchAssignTo'; $lang->resource->story->batchChangeModule = 'batchChangeModule'; +$lang->resource->story->batchChangeParent = 'batchChangeParent'; $lang->resource->story->batchToTask = 'batchToTask'; $lang->resource->story->processStoryChange = 'processStoryChange'; -$lang->resource->story->linkStories = 'linkStoriesAB'; $lang->resource->story->relieved = 'relievedTwins'; $lang->resource->story->createBranch = 'codeBranch'; $lang->resource->story->unlinkBranch = 'unlinkBranch'; @@ -536,6 +553,7 @@ $lang->resource->story->unlinkBranch = 'unlinkBranch'; $lang->story->methodOrder[5] = 'create'; $lang->story->methodOrder[10] = 'batchCreate'; $lang->story->methodOrder[15] = 'edit'; +$lang->story->methodOrder[17] = 'batchEdit'; $lang->story->methodOrder[20] = 'export'; $lang->story->methodOrder[25] = 'delete'; $lang->story->methodOrder[30] = 'view'; @@ -558,9 +576,9 @@ $lang->story->methodOrder[105] = 'report'; $lang->story->methodOrder[110] = 'linkStory'; $lang->story->methodOrder[115] = 'batchChangeBranch'; $lang->story->methodOrder[120] = 'batchChangeModule'; -$lang->story->methodOrder[125] = 'batchToTask'; -$lang->story->methodOrder[130] = 'processStoryChange'; -$lang->story->methodOrder[135] = 'linkStories'; +$lang->story->methodOrder[125] = 'batchChangeParent'; +$lang->story->methodOrder[130] = 'batchToTask'; +$lang->story->methodOrder[135] = 'processStoryChange'; $lang->story->methodOrder[140] = 'relieved'; $lang->story->methodOrder[145] = 'createBranch'; $lang->story->methodOrder[150] = 'unlinkBranch'; @@ -588,7 +606,9 @@ $lang->resource->requirement->report = 'reportAction'; $lang->resource->requirement->batchChangeBranch = 'batchChangeBranch'; $lang->resource->requirement->batchAssignTo = 'batchAssignTo'; $lang->resource->requirement->batchChangeModule = 'batchChangeModule'; -$lang->resource->requirement->linkRequirements = 'linkRequirementsAB'; +$lang->resource->requirement->batchChangeParent = 'batchChangeParent'; +$lang->resource->requirement->batchChangeGrade = 'batchChangeGrade'; +$lang->resource->requirement->batchChangePlan = 'batchChangePlan'; $lang->requirement->methodOrder[5] = 'create'; $lang->requirement->methodOrder[10] = 'batchCreate'; @@ -610,7 +630,61 @@ $lang->requirement->methodOrder[80] = 'report'; $lang->requirement->methodOrder[85] = 'linkStory'; $lang->requirement->methodOrder[90] = 'batchChangeBranch'; $lang->requirement->methodOrder[95] = 'batchChangeModule'; -$lang->requirement->methodOrder[100] = 'linkRequirements'; +$lang->requirement->methodOrder[100] = 'batchChangeParent'; +$lang->requirement->methodOrder[105] = 'batchChangeGrade'; +$lang->requirement->methodOrder[110] = 'batchChangePlan'; + +/* Epic. */ +$lang->resource->epic = new stdclass(); +$lang->resource->epic->create = 'create'; +$lang->resource->epic->batchCreate = 'batchCreate'; +$lang->resource->epic->edit = 'editAction'; +$lang->resource->epic->batchEdit = 'batchEdit'; +$lang->resource->epic->linkStory = 'linkStory'; +$lang->resource->epic->export = 'exportAction'; +$lang->resource->epic->delete = 'deleteAction'; +$lang->resource->epic->view = 'view'; +$lang->resource->epic->change = 'changeAction'; +$lang->resource->epic->review = 'reviewAction'; +$lang->resource->epic->submitReview = 'submitReview'; +$lang->resource->epic->batchReview = 'batchReview'; +$lang->resource->epic->recall = 'recall'; +$lang->resource->epic->assignTo = 'assignAction'; +$lang->resource->epic->close = 'closeAction'; +$lang->resource->epic->batchClose = 'batchClose'; +$lang->resource->epic->activate = 'activateAction'; +$lang->resource->epic->report = 'reportAction'; +$lang->resource->epic->batchChangeBranch = 'batchChangeBranch'; +$lang->resource->epic->batchAssignTo = 'batchAssignTo'; +$lang->resource->epic->batchChangeModule = 'batchChangeModule'; +$lang->resource->epic->batchChangeParent = 'batchChangeParent'; +$lang->resource->epic->batchChangeGrade = 'batchChangeGrade'; +$lang->resource->epic->batchChangePlan = 'batchChangePlan'; + +$lang->epic->methodOrder[5] = 'create'; +$lang->epic->methodOrder[10] = 'batchCreate'; +$lang->epic->methodOrder[15] = 'edit'; +$lang->epic->methodOrder[16] = 'batchEdit'; +$lang->epic->methodOrder[17] = 'linkStory'; +$lang->epic->methodOrder[20] = 'export'; +$lang->epic->methodOrder[25] = 'delete'; +$lang->epic->methodOrder[30] = 'view'; +$lang->epic->methodOrder[35] = 'change'; +$lang->epic->methodOrder[40] = 'review'; +$lang->epic->methodOrder[44] = 'submitReview'; +$lang->epic->methodOrder[45] = 'batchReview'; +$lang->epic->methodOrder[50] = 'recall'; +$lang->epic->methodOrder[55] = 'close'; +$lang->epic->methodOrder[60] = 'batchClose'; +$lang->epic->methodOrder[65] = 'assignTo'; +$lang->epic->methodOrder[70] = 'batchAssignTo'; +$lang->epic->methodOrder[75] = 'activate'; +$lang->epic->methodOrder[80] = 'report'; +$lang->epic->methodOrder[90] = 'batchChangeBranch'; +$lang->epic->methodOrder[95] = 'batchChangeModule'; +$lang->epic->methodOrder[100] = 'batchChangeParent'; +$lang->epic->methodOrder[105] = 'batchChangeGrade'; +$lang->epic->methodOrder[110] = 'batchChangePlan'; /* Product plan. */ $lang->resource->productplan = new stdclass(); @@ -1506,29 +1580,35 @@ $lang->resource->custom->editStoryConcept = 'editStoryConcept'; $lang->resource->custom->browseStoryConcept = 'browseStoryConcept'; $lang->resource->custom->setDefaultConcept = 'setDefaultConcept'; $lang->resource->custom->deleteStoryConcept = 'deleteStoryConcept'; +$lang->resource->custom->closeGrade = 'closeGrade'; +$lang->resource->custom->activateGrade = 'activateGrade'; +$lang->resource->custom->deleteGrade = 'deleteGrade'; $lang->resource->custom->kanban = 'kanban'; $lang->resource->custom->code = 'code'; $lang->resource->custom->hours = 'hours'; $lang->resource->custom->percent = 'percent'; $lang->resource->custom->limitTaskDate = 'limitTaskDateAction'; -$lang->custom->methodOrder[10] = 'set'; -$lang->custom->methodOrder[15] = 'product'; -$lang->custom->methodOrder[20] = 'execution'; -$lang->custom->methodOrder[25] = 'required'; -$lang->custom->methodOrder[30] = 'restore'; -$lang->custom->methodOrder[35] = 'flow'; -$lang->custom->methodOrder[45] = 'timezone'; -$lang->custom->methodOrder[50] = 'setStoryConcept'; -$lang->custom->methodOrder[55] = 'editStoryConcept'; -$lang->custom->methodOrder[60] = 'browseStoryConcept'; -$lang->custom->methodOrder[65] = 'setDefaultConcept'; -$lang->custom->methodOrder[70] = 'deleteStoryConcept'; -$lang->custom->methodOrder[75] = 'kanban'; -$lang->custom->methodOrder[80] = 'code'; -$lang->custom->methodOrder[85] = 'hours'; -$lang->custom->methodOrder[90] = 'percent'; -$lang->custom->methodOrder[95] = 'limitTaskDate'; +$lang->custom->methodOrder[10] = 'set'; +$lang->custom->methodOrder[15] = 'product'; +$lang->custom->methodOrder[20] = 'execution'; +$lang->custom->methodOrder[25] = 'required'; +$lang->custom->methodOrder[30] = 'restore'; +$lang->custom->methodOrder[35] = 'flow'; +$lang->custom->methodOrder[45] = 'timezone'; +$lang->custom->methodOrder[50] = 'setStoryConcept'; +$lang->custom->methodOrder[55] = 'editStoryConcept'; +$lang->custom->methodOrder[60] = 'browseStoryConcept'; +$lang->custom->methodOrder[65] = 'setDefaultConcept'; +$lang->custom->methodOrder[70] = 'deleteStoryConcept'; +$lang->custom->methodOrder[75] = 'closeGrade'; +$lang->custom->methodOrder[80] = 'activateGrade'; +$lang->custom->methodOrder[85] = 'deleteGrade'; +$lang->custom->methodOrder[90] = 'kanban'; +$lang->custom->methodOrder[95] = 'code'; +$lang->custom->methodOrder[100] = 'hours'; +$lang->custom->methodOrder[105] = 'percent'; +$lang->custom->methodOrder[110] = 'limitTaskDate'; $lang->resource->datatable = new stdclass(); $lang->resource->datatable->setGlobal = 'setGlobal'; @@ -2096,6 +2176,11 @@ if(!$inUpgrade) unset($lang->resource->story->linkStory); unset($lang->resource->requirement); } + if(!$config->enableER) + { + unset($lang->resource->product->epic); + unset($lang->resource->epic); + } if($config->systemMode == 'light') { unset($lang->resource->program); diff --git a/module/group/lang/zh-cn.php b/module/group/lang/zh-cn.php index bfbe0af26f..27f0db70cc 100644 --- a/module/group/lang/zh-cn.php +++ b/module/group/lang/zh-cn.php @@ -235,6 +235,11 @@ $lang->group->package->importStory = '导入导出研发需求'; $lang->group->package->deleteStory = '删除研发需求'; $lang->group->package->reviewStory = '评审研发需求'; $lang->group->package->deleteProduct = '删除产品'; +$lang->group->package->browseEpic = '浏览业务需求'; +$lang->group->package->manageEpic = '创建维护业务需求'; +$lang->group->package->importEpic = '导入导出业务需求'; +$lang->group->package->deleteEpic = '删除业务需求'; +$lang->group->package->reviewEpic = '评审业务需求'; $lang->group->package->browseRequirement = '浏览用户需求'; $lang->group->package->manageRequirement = '创建维护用户需求'; $lang->group->package->importRequirement = '导入导出用户需求'; @@ -249,7 +254,8 @@ $lang->group->package->importRelease = '导入导出发布'; $lang->group->package->deleteRelease = '删除发布'; $lang->group->package->releaseNotify = '发布通知'; $lang->group->package->projectPlan = '项目计划'; -$lang->group->package->manageProjectStory = '维护项目研发需求'; +$lang->group->package->manageProjectStory = '维护项目需求'; +$lang->group->package->importProjectStory = '导入导出项目需求'; $lang->group->package->browseProject = '浏览项目'; $lang->group->package->manageProject = '创建维护项目'; $lang->group->package->importProject = '导入导出项目'; @@ -504,7 +510,7 @@ $lang->group->package->executionRelation = '维护任务关系'; $lang->group->package->browseBuild = '浏览版本'; $lang->group->package->browseExecutionStory = '浏览执行研发需求'; $lang->group->package->manageCard = '维护卡片'; -$lang->group->package->browseProjectStory = '浏览项目研发需求'; +$lang->group->package->browseProjectStory = '浏览项目需求'; $lang->group->package->chckAuditPlan = '检查质量保证计划'; $lang->group->package->reviewAssess = '评审基线评审'; $lang->group->package->reviewAudit = '审计基线评审'; diff --git a/module/group/packagemanager.php b/module/group/packagemanager.php index ba074bd435..7bb079b682 100644 --- a/module/group/packagemanager.php +++ b/module/group/packagemanager.php @@ -45,6 +45,10 @@ $config->group->subset->requirement = new stdclass(); $config->group->subset->requirement->order = 120; $config->group->subset->requirement->nav = 'product'; +$config->group->subset->epic = new stdclass(); +$config->group->subset->epic->order = 125; +$config->group->subset->epic->nav = 'product'; + $config->group->subset->productplan = new stdclass(); $config->group->subset->productplan->order = 130; $config->group->subset->productplan->nav = 'product'; @@ -645,15 +649,16 @@ $config->group->package->browseProduct = new stdclass(); $config->group->package->browseProduct->order = 5; $config->group->package->browseProduct->subset = 'product'; $config->group->package->browseProduct->privs = array(); -$config->group->package->browseProduct->privs['product-index'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array(), 'recommend' => array('product-all', 'product-dashboard', 'product-kanban', 'product-view')); -$config->group->package->browseProduct->privs['product-view'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 4, 'depend' => array('product-all', 'product-browse', 'product-dashboard'), 'recommend' => array('branch-manage', 'product-all', 'product-create', 'product-dashboard', 'product-dynamic', 'product-edit', 'product-kanban', 'product-whitelist')); -$config->group->package->browseProduct->privs['product-roadmap'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array('product-all', 'product-browse', 'product-dashboard'), 'recommend' => array('release-view')); -$config->group->package->browseProduct->privs['product-track'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 6, 'depend' => array('product-browse'), 'recommend' => array('product-browse', 'product-requirement', 'requirement-view', 'story-view')); -$config->group->package->browseProduct->privs['product-dynamic'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 7, 'depend' => array('product-browse'), 'recommend' => array('product-all', 'product-kanban', 'product-view')); -$config->group->package->browseProduct->privs['product-project'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 8, 'depend' => array('product-browse'), 'recommend' => array('project-create')); -$config->group->package->browseProduct->privs['product-dashboard'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 3, 'depend' => array(), 'recommend' => array('product-dynamic', 'product-roadmap')); -$config->group->package->browseProduct->privs['product-all'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 1, 'depend' => array(), 'recommend' => array('product-create', 'product-dashboard', 'product-dynamic', 'product-edit', 'product-index', 'product-kanban', 'product-updateOrder', 'product-view')); -$config->group->package->browseProduct->privs['product-kanban'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 2, 'depend' => array(), 'recommend' => array('product-all', 'product-dashboard', 'product-dynamic', 'product-index', 'product-view')); +$config->group->package->browseProduct->privs['product-index'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 0, 'depend' => array(), 'recommend' => array('product-all', 'product-dashboard', 'product-kanban', 'product-view')); +$config->group->package->browseProduct->privs['product-view'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 4, 'depend' => array('product-all', 'product-browse', 'product-dashboard'), 'recommend' => array('branch-manage', 'product-all', 'product-create', 'product-dashboard', 'product-dynamic', 'product-edit', 'product-kanban', 'product-whitelist')); +$config->group->package->browseProduct->privs['product-roadmap'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 5, 'depend' => array('product-all', 'product-browse', 'product-dashboard'), 'recommend' => array('release-view')); +$config->group->package->browseProduct->privs['product-track'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 6, 'depend' => array('product-browse'), 'recommend' => array('product-browse', 'product-requirement', 'requirement-view', 'story-view', 'bug-view', 'testcase-view', 'epic-view', 'design-view', 'project-view', 'execution-task', 'task-view', 'repo-revision')); +$config->group->package->browseProduct->privs['product-exportTrack'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd', 'order' => 15, 'depend' => array('product-track'), 'recommend' => array('product-browse')); +$config->group->package->browseProduct->privs['product-dynamic'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 7, 'depend' => array('product-browse'), 'recommend' => array('product-all', 'product-kanban', 'product-view')); +$config->group->package->browseProduct->privs['product-project'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 8, 'depend' => array('product-browse'), 'recommend' => array('project-create')); +$config->group->package->browseProduct->privs['product-dashboard'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 3, 'depend' => array(), 'recommend' => array('product-dynamic', 'product-roadmap')); +$config->group->package->browseProduct->privs['product-all'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 1, 'depend' => array(), 'recommend' => array('product-create', 'product-dashboard', 'product-dynamic', 'product-edit', 'product-index', 'product-kanban', 'product-updateOrder', 'product-view')); +$config->group->package->browseProduct->privs['product-kanban'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 2, 'depend' => array(), 'recommend' => array('product-all', 'product-dashboard', 'product-dynamic', 'product-index', 'product-view')); $config->group->package->manageProductLine = new stdclass(); $config->group->package->manageProductLine->order = 10; @@ -714,26 +719,27 @@ $config->group->package->manageStory = new stdclass(); $config->group->package->manageStory->order = 10; $config->group->package->manageStory->subset = 'story'; $config->group->package->manageStory->privs = array(); -$config->group->package->manageStory->privs['story-create'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 0, 'depend' => array(), 'recommend' => array('story-activate', 'story-assignTo', 'story-batchCreate', 'story-change', 'story-close', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relation', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-create'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 0, 'depend' => array(), 'recommend' => array('story-activate', 'story-assignTo', 'story-batchCreate', 'story-change', 'story-close', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relation', 'story-relieved', 'story-submitReview')); $config->group->package->manageStory->privs['story-batchCreate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 1, 'depend' => array('product-browse'), 'recommend' => array('story-create')); -$config->group->package->manageStory->privs['story-edit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 2, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-batchEdit', 'story-change', 'story-close', 'story-create', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-change'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 4, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-submitReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 5, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved')); -$config->group->package->manageStory->privs['story-recall'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 6, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-close'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 7, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-edit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 2, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-batchEdit', 'story-change', 'story-close', 'story-create', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-batchEdit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 3, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); +$config->group->package->manageStory->privs['story-change'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 4, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-submitReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 5, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved')); +$config->group->package->manageStory->privs['story-recall'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 6, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-close'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 7, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); $config->group->package->manageStory->privs['story-batchClose'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 8, 'depend' => array('product-browse'), 'recommend' => array('story-close')); $config->group->package->manageStory->privs['story-batchChangePlan'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 9, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); $config->group->package->manageStory->privs['story-batchChangeStage'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 10, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); -$config->group->package->manageStory->privs['story-assignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 11, 'depend' => array('product-browse'), 'recommend' => array('story-activate', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-assignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 11, 'depend' => array('product-browse'), 'recommend' => array('story-activate', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); $config->group->package->manageStory->privs['story-batchAssignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 12, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); -$config->group->package->manageStory->privs['story-activate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 13, 'depend' => array('story-view'), 'recommend' => array('story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-linkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 14, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-activate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 13, 'depend' => array('story-view'), 'recommend' => array('story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-linkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 14, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); $config->group->package->manageStory->privs['story-batchChangeBranch'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 15, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); $config->group->package->manageStory->privs['story-batchChangeModule'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 16, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); -$config->group->package->manageStory->privs['story-processStoryChange'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 17, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-recall', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-linkStories'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 18, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-relieved', 'story-submitReview')); -$config->group->package->manageStory->privs['story-relieved'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 20, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStories', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-submitReview')); -$config->group->package->manageStory->privs['story-batchEdit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 3, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); +$config->group->package->manageStory->privs['story-batchChangeParent'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 17, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); +$config->group->package->manageStory->privs['story-batchChangeGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 18, 'depend' => array('product-browse'), 'recommend' => array('story-edit')); +$config->group->package->manageStory->privs['story-processStoryChange'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 19, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-recall', 'story-relieved', 'story-submitReview')); +$config->group->package->manageStory->privs['story-relieved'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 21, 'depend' => array('story-view'), 'recommend' => array('story-activate', 'story-assignTo', 'story-change', 'story-close', 'story-create', 'story-edit', 'story-linkStory', 'story-processStoryChange', 'story-recall', 'story-submitReview')); $config->group->package->manageStory->privs['story-relation'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 19, 'depend' => array('story-view'), 'recommend' => array()); $config->group->package->manageStory->privs['story-createBranch'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 3, 'depend' => array('story-view'), 'recommend' => array('story-unlinkBranch')); $config->group->package->manageStory->privs['story-unlinkBranch'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 3, 'depend' => array('story-view', 'story-createBranch'), 'recommend' => array()); @@ -792,6 +798,9 @@ $config->group->package->manageRequirement->privs['requirement-activate'] $config->group->package->manageRequirement->privs['requirement-linkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 90, 'depend' => array('requirement-view'), 'recommend' => array('requirement-activate', 'requirement-assignTo', 'requirement-batchChangeModule', 'requirement-change', 'requirement-close', 'requirement-create', 'requirement-edit', 'requirement-linkRequirements', 'requirement-recall', 'requirement-submitReview')); $config->group->package->manageRequirement->privs['requirement-batchChangeBranch'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 95, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); $config->group->package->manageRequirement->privs['requirement-batchChangeModule'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 100, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); +$config->group->package->manageRequirement->privs['requirement-batchChangeParent'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 101, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); +$config->group->package->manageRequirement->privs['requirement-batchChangeGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 102, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); +$config->group->package->manageRequirement->privs['requirement-batchChangePlan'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 103, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); $config->group->package->manageRequirement->privs['requirement-linkRequirements'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 105, 'depend' => array('requirement-view'), 'recommend' => array('requirement-activate', 'requirement-assignTo', 'requirement-change', 'requirement-close', 'requirement-create', 'requirement-edit', 'requirement-linkStory', 'requirement-recall', 'requirement-submitReview')); $config->group->package->manageRequirement->privs['requirement-batchEdit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 110, 'depend' => array('product-requirement'), 'recommend' => array('requirement-edit')); $config->group->package->manageRequirement->privs['requirement-batchChangeRoadmap'] = array('edition' => 'ipd', 'vision' => 'or', 'order' => 125, 'depend' => array('product-requirement'), 'recommend' => array('requirement-batchCreate', 'requirement-create', 'requirement-edit')); @@ -812,6 +821,54 @@ $config->group->package->reviewRequirement->privs = array(); $config->group->package->reviewRequirement->privs['requirement-review'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 40, 'depend' => array('requirement-view'), 'recommend' => array('requirement-batchReview')); $config->group->package->reviewRequirement->privs['requirement-batchReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 50, 'depend' => array('product-requirement'), 'recommend' => array('requirement-review')); +$config->group->package->browseEpic = new stdclass(); +$config->group->package->browseEpic->order = 5; +$config->group->package->browseEpic->subset = 'epic'; +$config->group->package->browseEpic->privs = array(); +$config->group->package->browseEpic->privs['product-epic'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 15, 'depend' => array('product-browse'), 'recommend' => array('epic-report', 'epic-view')); +$config->group->package->browseEpic->privs['epic-view'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 30, 'depend' => array(), 'recommend' => array()); +$config->group->package->browseEpic->privs['epic-report'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 85, 'depend' => array('product-epic'), 'recommend' => array()); + +$config->group->package->manageEpic = new stdclass(); +$config->group->package->manageEpic->order = 10; +$config->group->package->manageEpic->subset = 'epic'; +$config->group->package->manageEpic->privs = array(); +$config->group->package->manageEpic->privs['epic-create'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 5, 'depend' => array(), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-batchCreate', 'epic-change', 'epic-close', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-batchCreate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 10, 'depend' => array('product-epic'), 'recommend' => array('epic-create')); +$config->group->package->manageEpic->privs['epic-edit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 15, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-batchEdit', 'epic-change', 'epic-close', 'epic-create', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-delete'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 25, 'depend' => array('epic-view'), 'recommend' => array('epic-create', 'epic-edit')); +$config->group->package->manageEpic->privs['epic-change'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 35, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-submitReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 45, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-change', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall')); +$config->group->package->manageEpic->privs['epic-recall'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 55, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-change', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-close'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 60, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-batchClose', 'epic-change', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-batchClose'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 65, 'depend' => array('product-epic'), 'recommend' => array('epic-close')); +$config->group->package->manageEpic->privs['epic-assignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 70, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-batchAssignTo', 'epic-change', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-batchAssignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 75, 'depend' => array('product-epic'), 'recommend' => array('epic-assignTo')); +$config->group->package->manageEpic->privs['epic-activate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 80, 'depend' => array('epic-view'), 'recommend' => array('epic-assignTo', 'epic-change', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-linkStory', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-linkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 90, 'depend' => array('epic-view'), 'recommend' => array('epic-activate', 'epic-assignTo', 'epic-batchChangeModule', 'epic-change', 'epic-close', 'epic-create', 'epic-edit', 'epic-linkepics', 'epic-recall', 'epic-submitReview')); +$config->group->package->manageEpic->privs['epic-batchChangeBranch'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 95, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchChangeModule'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 100, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchChangeParent'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 101, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchChangeGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 102, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchChangePlan'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 103, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchEdit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 110, 'depend' => array('product-epic'), 'recommend' => array('epic-edit')); +$config->group->package->manageEpic->privs['epic-batchChangeRoadmap'] = array('edition' => 'ipd', 'vision' => 'or', 'order' => 125, 'depend' => array('product-epic'), 'recommend' => array('epic-batchCreate', 'epic-create', 'epic-edit')); + +$config->group->package->importEpic = new stdclass(); +$config->group->package->importEpic->order = 20; +$config->group->package->importEpic->subset = 'epic'; +$config->group->package->importEpic->privs = array(); +$config->group->package->importEpic->privs['epic-export'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 20, 'depend' => array('product-epic'), 'recommend' => array()); +$config->group->package->importEpic->privs['epic-import'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 115, 'depend' => array('product-epic', 'epic-exportTemplate'), 'recommend' => array()); +$config->group->package->importEpic->privs['epic-exportTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 120, 'depend' => array('product-epic'), 'recommend' => array()); + +$config->group->package->reviewEpic = new stdclass(); +$config->group->package->reviewEpic->order = 15; +$config->group->package->reviewEpic->subset = 'epic'; +$config->group->package->reviewEpic->privs = array(); +$config->group->package->reviewEpic->privs['epic-review'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 40, 'depend' => array('epic-view'), 'recommend' => array('epic-batchReview')); +$config->group->package->reviewEpic->privs['epic-batchReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,or', 'order' => 50, 'depend' => array('product-epic'), 'recommend' => array('epic-review')); + $config->group->package->browseProductPlan = new stdclass(); $config->group->package->browseProductPlan->order = 5; $config->group->package->browseProductPlan->subset = 'productplan'; @@ -902,6 +959,21 @@ $config->group->package->manageProjectStory->privs['projectstory-importplanstori $config->group->package->manageProjectStory->privs['projectstory-unlinkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 30, 'depend' => array('projectstory-story'), 'recommend' => array('projectstory-batchUnlinkStory', 'projectstory-importFromLib', 'projectstory-importplanstories', 'projectstory-linkStory', 'story-batchToTask')); $config->group->package->manageProjectStory->privs['projectstory-batchUnlinkStory'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 35, 'depend' => array('projectstory-story'), 'recommend' => array('projectstory-unlinkStory')); $config->group->package->manageProjectStory->privs['projectstory-importFromLib'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 40, 'depend' => array('projectstory-story'), 'recommend' => array('assetlib-story', 'assetlib-storylib', 'projectstory-batchUnlinkStory', 'projectstory-importplanstories', 'projectstory-linkStory', 'projectstory-unlinkStory', 'story-batchToTask')); +$config->group->package->manageProjectStory->privs['projectstory-batchReview'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 50, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->manageProjectStory->privs['projectstory-batchClose'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 60, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->manageProjectStory->privs['projectstory-batchChangePlan'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 70, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->manageProjectStory->privs['projectstory-batchAssignTo'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 80, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->manageProjectStory->privs['projectstory-batchEdit'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 90, 'depend' => array('projectstory-story'), 'recommend' => array()); + +$config->group->package->importProjectStory = new stdclass(); +$config->group->package->importProjectStory->order = 15; +$config->group->package->importProjectStory->subset = 'projectstory'; +$config->group->package->importProjectStory->privs = array(); +$config->group->package->importProjectStory->privs['projectstory-export'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 5, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->importProjectStory->privs['projectstory-import'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 10, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->importProjectStory->privs['projectstory-exportTemplate'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 20, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->importProjectStory->privs['projectstory-importToLib'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 30, 'depend' => array('story-view'), 'recommend' => array('story-batchImportToLib')); +$config->group->package->importProjectStory->privs['projectstory-batchImportToLib'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 40, 'depend' => array('product-browse'), 'recommend' => array('story-importToLib')); $config->group->package->browseProject = new stdclass(); $config->group->package->browseProject->order = 5; @@ -1557,6 +1629,7 @@ $config->group->package->handleFeedback->privs['feedback-toBug'] = array(' $config->group->package->handleFeedback->privs['feedback-toStory'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd', 'order' => 3, 'depend' => array('feedback-admin', 'story-create'), 'recommend' => array('feedback-reply', 'feedback-review', 'feedback-toBug', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo', 'feedback-toUserStory')); $config->group->package->handleFeedback->privs['feedback-toTicket'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd', 'order' => 8, 'depend' => array('feedback-admin', 'ticket-create'), 'recommend' => array('feedback-reply', 'feedback-review', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTodo', 'feedback-toUserStory')); $config->group->package->handleFeedback->privs['feedback-toUserStory'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 4, 'depend' => array('feedback-admin', 'requirement-create'), 'recommend' => array('feedback-reply', 'feedback-review', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo')); +$config->group->package->handleFeedback->privs['feedback-toEpic'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 4, 'depend' => array('feedback-admin', 'epic-create'), 'recommend' => array('feedback-reply', 'feedback-review', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo')); $config->group->package->handleFeedback->privs['feedback-toDemand'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 4, 'depend' => array('feedback-admin', 'demand-create'), 'recommend' => array('feedback-reply', 'feedback-review', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo')); $config->group->package->handleFeedback->privs['feedback-review'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 0, 'depend' => array('feedback-admin'), 'recommend' => array('feedback-batchReview', 'feedback-reply', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo', 'feedback-toUserStory')); $config->group->package->handleFeedback->privs['feedback-reply'] = array('edition' => 'biz,max,ipd', 'vision' => 'rnd,or', 'order' => 2, 'depend' => array('feedback-admin'), 'recommend' => array('feedback-review', 'feedback-toBug', 'feedback-toStory', 'feedback-toTask', 'feedback-toTicket', 'feedback-toTodo', 'feedback-toUserStory')); @@ -2875,13 +2948,6 @@ $config->group->package->docTemplate->privs['baseline-editBook'] = array(' $config->group->package->docTemplate->privs['baseline-manageBook'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 7, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate', 'baseline-delete', 'baseline-editBook', 'baseline-editTemplate')); $config->group->package->docTemplate->privs['baseline-delete'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 6, 'depend' => array('baseline-template'), 'recommend' => array('baseline-createTemplate')); -$config->group->package->importStoryLib = new stdclass(); -$config->group->package->importStoryLib->order = 30; -$config->group->package->importStoryLib->subset = 'projectstory'; -$config->group->package->importStoryLib->privs = array(); -$config->group->package->importStoryLib->privs['story-importToLib'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 165, 'depend' => array('story-view'), 'recommend' => array('story-batchImportToLib')); -$config->group->package->importStoryLib->privs['story-batchImportToLib'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 170, 'depend' => array('product-browse'), 'recommend' => array('story-importToLib')); - $config->group->package->projectStakeholder = new stdclass(); $config->group->package->projectStakeholder->order = 5; $config->group->package->projectStakeholder->subset = 'projectsettings'; @@ -3045,6 +3111,9 @@ $config->group->package->custom->privs['custom-deleteStoryConcept'] = array('edi $config->group->package->custom->privs['custom-kanban'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 11, 'depend' => array('admin-index', 'admin-register'), 'recommend' => array()); $config->group->package->custom->privs['custom-setStoryConcept'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 6, 'depend' => array('custom-browseStoryConcept'), 'recommend' => array()); $config->group->package->custom->privs['custom-limitTaskDate'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array('custom-execution'), 'recommend' => array()); +$config->group->package->custom->privs['custom-closeGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 12, 'depend' => array('custom-set'), 'recommend' => array()); +$config->group->package->custom->privs['custom-activateGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 13, 'depend' => array('custom-set'), 'recommend' => array()); +$config->group->package->custom->privs['custom-deleteGrade'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite,or', 'order' => 14, 'depend' => array('custom-set'), 'recommend' => array()); $config->group->package->approvalflow = new stdclass(); $config->group->package->approvalflow->order = 10; @@ -3195,7 +3264,8 @@ $config->group->package->track = new stdclass(); $config->group->package->track->order = 5; $config->group->package->track->subset = 'projecttrack'; $config->group->package->track->privs = array(); -$config->group->package->track->privs['projectstory-track'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array('project-index'), 'recommend' => array('design-revision', 'projectstory-story', 'projectstory-view')); +$config->group->package->track->privs['projectstory-track'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 10, 'depend' => array('project-index'), 'recommend' => array('design-revision', 'projectstory-story', 'projectstory-view', 'bug-view', 'testcase-view', 'epic-view', 'requirement-view', 'design-view', 'project-view', 'execution-task', 'task-view', 'repo-revision')); +$config->group->package->track->privs['projectstory-exportTrack'] = array('edition' => 'max,ipd', 'vision' => 'rnd', 'order' => 20, 'depend' => array('projectstory-track'), 'recommend' => array('projectstory-story')); $config->group->package->workflowRelation = new stdclass(); $config->group->package->workflowRelation->order = 65; @@ -3330,8 +3400,9 @@ $config->group->package->browseProjectStory = new stdclass(); $config->group->package->browseProjectStory->order = 5; $config->group->package->browseProjectStory->subset = 'projectstory'; $config->group->package->browseProjectStory->privs = array(); -$config->group->package->browseProjectStory->privs['projectstory-story'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 5, 'depend' => array('project-index'), 'recommend' => array('projectstory-view')); -$config->group->package->browseProjectStory->privs['projectstory-view'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 15, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->browseProjectStory->privs['projectstory-story'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 5, 'depend' => array('project-index'), 'recommend' => array('projectstory-view')); +$config->group->package->browseProjectStory->privs['projectstory-view'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 15, 'depend' => array('projectstory-story'), 'recommend' => array()); +$config->group->package->browseProjectStory->privs['projectstory-report'] = array('edition' => 'open,biz,max,ipd', 'vision' => 'rnd,lite', 'order' => 25, 'depend' => array('projectstory-story'), 'recommend' => array()); $config->group->package->chckAuditPlan = new stdclass(); $config->group->package->chckAuditPlan->order = 15; diff --git a/module/kanban/config.php b/module/kanban/config.php index 1bed44b813..1e33b7bb6d 100644 --- a/module/kanban/config.php +++ b/module/kanban/config.php @@ -1,5 +1,5 @@ kanban = new stdclass(); $config->kanban->require = new stdclass(); @@ -59,29 +59,50 @@ $config->kanban->releaseField = array('name', 'status', 'date', 'deleted'); $config->kanban->buildField = array('name', 'date', 'builder', 'deleted'); $config->kanban->default = new stdclass(); +$config->kanban->laneColorList = array('#3C4353', '#838A9D', '#476BDA', '#A65BD4', '#449C9D', '#D75557', '#CE814C', '#E7D057', '#73BE60', '#F6998A', '#8DA8ED', '#93DAF6', '#B9E78C', '#E58CE7', '#EDE590', '#CFB5FA'); + +$config->kanban->default->epic = new stdclass(); +$config->kanban->default->epic->name = $lang->ERCommon; +$config->kanban->default->epic->color = '#3C4353'; +$config->kanban->default->epic->order = '5'; + +$config->kanban->default->requirement = new stdclass(); +$config->kanban->default->requirement->name = $lang->URCommon; +$config->kanban->default->requirement->color = '#838A9D'; +$config->kanban->default->requirement->order = '10'; + +$config->kanban->default->parentStory = new stdclass(); +$config->kanban->default->parentStory->name = $lang->kanban->laneTypeList['parentStory']; +$config->kanban->default->parentStory->color = '#838A9D'; +$config->kanban->default->parentStory->order = '15'; + $config->kanban->default->story = new stdclass(); $config->kanban->default->story->name = $lang->SRCommon; $config->kanban->default->story->color = '#7ec5ff'; -$config->kanban->default->story->order = '5'; +$config->kanban->default->story->order = '20'; $config->kanban->default->bug = new stdclass(); $config->kanban->default->bug->name = $lang->bug->common; $config->kanban->default->bug->color = '#ba55d3'; -$config->kanban->default->bug->order = '10'; +$config->kanban->default->bug->order = '25'; $config->kanban->default->task = new stdclass(); $config->kanban->default->task->name = $lang->task->common; $config->kanban->default->task->color = '#4169e1'; -$config->kanban->default->task->order = '15'; +$config->kanban->default->task->order = '30'; $config->kanban->storyColumnStageList = array(); $config->kanban->storyColumnStageList['backlog'] = 'projected'; $config->kanban->storyColumnStageList['ready'] = 'projected'; +$config->kanban->storyColumnStageList['designing'] = 'designing'; +$config->kanban->storyColumnStageList['designed'] = 'designed'; $config->kanban->storyColumnStageList['developing'] = 'developing'; $config->kanban->storyColumnStageList['developed'] = 'developed'; $config->kanban->storyColumnStageList['testing'] = 'testing'; $config->kanban->storyColumnStageList['tested'] = 'tested'; $config->kanban->storyColumnStageList['verified'] = 'verified'; +$config->kanban->storyColumnStageList['rejected'] = 'rejected'; +$config->kanban->storyColumnStageList['pending'] = 'pending'; $config->kanban->storyColumnStageList['released'] = 'released'; $config->kanban->storyColumnStageList['closed'] = 'closed'; @@ -117,4 +138,6 @@ $config->kanban->laneColorList = array('#3C4353', '#838A9D', '#476BDA', '#A65B $config->kanban->columnColorList = array('#333', '#2b519c', '#e48610', '#d2313d', '#2a9f23', '#777', '#d2691e', '#2e8b8b', '#2f8b58', '#4168e0', '#4b0082', '#f58072', '#ba55d3', '#6a8e22'); $config->kanban->cardColorList = array('#fff', '#937c5a', '#fc5959', '#ff9f46'); -$config->kanban->batchCreate = 10; +$config->kanban->RDLaneOrder = array('parentStory' => 5, 'story' => 10, 'bug' => 15, 'task' => 20); + +$config->kanban->batchCreate = 10; diff --git a/module/kanban/control.php b/module/kanban/control.php index 79755c1ab1..ccadf8543f 100644 --- a/module/kanban/control.php +++ b/module/kanban/control.php @@ -1611,7 +1611,14 @@ class kanban extends control $taskSearchValue = $this->session->taskSearchValue ? $this->session->taskSearchValue : ''; $rdSearchValue = $this->session->rdSearchValue ? $this->session->rdSearchValue : ''; - $kanbanGroup = $regionID == 0 ? $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $taskSearchValue) : $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $regionID, $groupBy, $rdSearchValue); + if($regionID == 0) + { + list($kanbanGroup, $links) = $this->kanban->getExecutionKanban($executionID, $browseType, $groupBy, $taskSearchValue); + } + else + { + $kanbanGroup = $this->kanban->getRDKanban($executionID, $browseType, $orderBy, $regionID, $groupBy, $rdSearchValue); + } echo json_encode($kanbanGroup); } diff --git a/module/kanban/lang/de.php b/module/kanban/lang/de.php index fbb07e7638..71e95038ec 100644 --- a/module/kanban/lang/de.php +++ b/module/kanban/lang/de.php @@ -147,10 +147,13 @@ $lang->kanban->alignmentList['center'] = 'Center'; $lang->kanban->alignmentList['left'] = 'Left'; $lang->kanban->type = array(); -$lang->kanban->type['all'] = "All KanBan"; -$lang->kanban->type['story'] = "Story KanBan"; -$lang->kanban->type['task'] = "Task KanBan"; -$lang->kanban->type['bug'] = "Bug KanBan"; +$lang->kanban->type['all'] = "All KanBan"; +$lang->kanban->type['epic'] = "{$lang->ERCommon} KanBan"; +$lang->kanban->type['requirement'] = "{$lang->URCommon} KanBan"; +$lang->kanban->type['parentStory'] = "Parent Story KanBan"; +$lang->kanban->type['story'] = "Story KanBan"; +$lang->kanban->type['task'] = "Task KanBan"; +$lang->kanban->type['bug'] = "Bug KanBan"; $lang->kanban->group = new stdclass(); @@ -163,6 +166,10 @@ $lang->kanban->group->story['module'] = "Story Module"; $lang->kanban->group->story['source'] = "Story Source"; $lang->kanban->group->story['assignedTo'] = "Assigned To"; +$lang->kanban->group->epic = $lang->kanban->group->story; +$lang->kanban->group->requirement = $lang->kanban->group->story; +$lang->kanban->group->parentStory = $lang->kanban->group->story; + $lang->kanban->group->task = array(); $lang->kanban->group->task['default'] = "Default Group"; $lang->kanban->group->task['pri'] = "Task Priority"; @@ -248,13 +255,24 @@ $lang->kanban->defaultColumn['done'] = 'done'; $lang->kanban->defaultColumn['closed'] = 'close'; $lang->kanban->laneTypeList = array(); -$lang->kanban->laneTypeList['story'] = $lang->SRCommon; -$lang->kanban->laneTypeList['bug'] = 'Bug'; -$lang->kanban->laneTypeList['task'] = 'Task'; +$lang->kanban->laneTypeList['parentStory'] = "Parent " . $lang->SRCommon; +$lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$lang->kanban->laneTypeList['bug'] = 'Bug'; +$lang->kanban->laneTypeList['task'] = 'Task'; + +$lang->kanban->ERURColumn = array(); +$lang->kanban->ERURColumn['defining'] = 'Defining'; +$lang->kanban->ERURColumn['planning'] = 'Planning'; +$lang->kanban->ERURColumn['developing'] = 'Developing'; +$lang->kanban->ERURColumn['delivering'] = 'Delivering'; +$lang->kanban->ERURColumn['closed'] = 'Closed'; $lang->kanban->storyColumn = array(); $lang->kanban->storyColumn['backlog'] = 'Backlog'; $lang->kanban->storyColumn['ready'] = 'Ready'; +$lang->kanban->storyColumn['design'] = 'Design'; +$lang->kanban->storyColumn['designing'] = 'Designing'; +$lang->kanban->storyColumn['designed'] = 'Designed'; $lang->kanban->storyColumn['develop'] = 'Development'; $lang->kanban->storyColumn['developing'] = 'Doing'; $lang->kanban->storyColumn['developed'] = 'Done'; @@ -262,6 +280,8 @@ $lang->kanban->storyColumn['test'] = 'Testing'; $lang->kanban->storyColumn['testing'] = 'Doing'; $lang->kanban->storyColumn['tested'] = 'Done'; $lang->kanban->storyColumn['verified'] = 'Verified'; +$lang->kanban->storyColumn['rejected'] = 'Rejected'; +$lang->kanban->storyColumn['pending'] = 'Pending'; $lang->kanban->storyColumn['released'] = 'Released'; $lang->kanban->storyColumn['closed'] = 'Closed'; diff --git a/module/kanban/lang/en.php b/module/kanban/lang/en.php index 924c75a7dc..e8ba61f3a7 100644 --- a/module/kanban/lang/en.php +++ b/module/kanban/lang/en.php @@ -147,10 +147,13 @@ $lang->kanban->alignmentList['center'] = 'Center'; $lang->kanban->alignmentList['left'] = 'Left'; $lang->kanban->type = array(); -$lang->kanban->type['all'] = "All KanBan"; -$lang->kanban->type['story'] = "Story KanBan"; -$lang->kanban->type['task'] = "Task KanBan"; -$lang->kanban->type['bug'] = "Bug KanBan"; +$lang->kanban->type['all'] = "All KanBan"; +$lang->kanban->type['epic'] = "{$lang->ERCommon} KanBan"; +$lang->kanban->type['requirement'] = "{$lang->URCommon} KanBan"; +$lang->kanban->type['parentStory'] = "Parent Story KanBan"; +$lang->kanban->type['story'] = "Story KanBan"; +$lang->kanban->type['task'] = "Task KanBan"; +$lang->kanban->type['bug'] = "Bug KanBan"; $lang->kanban->group = new stdclass(); @@ -163,6 +166,10 @@ $lang->kanban->group->story['module'] = "Story Module"; $lang->kanban->group->story['source'] = "Story Source"; $lang->kanban->group->story['assignedTo'] = "Assigned To"; +$lang->kanban->group->epic = $lang->kanban->group->story; +$lang->kanban->group->requirement = $lang->kanban->group->story; +$lang->kanban->group->parentStory = $lang->kanban->group->story; + $lang->kanban->group->task = array(); $lang->kanban->group->task['default'] = "Default Group"; $lang->kanban->group->task['pri'] = "Task Priority"; @@ -248,13 +255,24 @@ $lang->kanban->defaultColumn['done'] = 'done'; $lang->kanban->defaultColumn['closed'] = 'close'; $lang->kanban->laneTypeList = array(); -$lang->kanban->laneTypeList['story'] = $lang->SRCommon; -$lang->kanban->laneTypeList['bug'] = 'Bug'; -$lang->kanban->laneTypeList['task'] = 'Task'; +$lang->kanban->laneTypeList['parentStory'] = "Parent " . $lang->SRCommon; +$lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$lang->kanban->laneTypeList['bug'] = 'Bug'; +$lang->kanban->laneTypeList['task'] = 'Task'; + +$lang->kanban->ERURColumn = array(); +$lang->kanban->ERURColumn['defining'] = 'Defining'; +$lang->kanban->ERURColumn['planning'] = 'Planning'; +$lang->kanban->ERURColumn['developing'] = 'Developing'; +$lang->kanban->ERURColumn['delivering'] = 'Delivering'; +$lang->kanban->ERURColumn['closed'] = 'Closed'; $lang->kanban->storyColumn = array(); $lang->kanban->storyColumn['backlog'] = 'Backlog'; $lang->kanban->storyColumn['ready'] = 'Ready'; +$lang->kanban->storyColumn['design'] = 'Design'; +$lang->kanban->storyColumn['designing'] = 'Designing'; +$lang->kanban->storyColumn['designed'] = 'Designed'; $lang->kanban->storyColumn['develop'] = 'Development'; $lang->kanban->storyColumn['developing'] = 'Doing'; $lang->kanban->storyColumn['developed'] = 'Done'; @@ -262,6 +280,8 @@ $lang->kanban->storyColumn['test'] = 'Testing'; $lang->kanban->storyColumn['testing'] = 'Doing'; $lang->kanban->storyColumn['tested'] = 'Done'; $lang->kanban->storyColumn['verified'] = 'Verified'; +$lang->kanban->storyColumn['rejected'] = 'Rejected'; +$lang->kanban->storyColumn['pending'] = 'Pending'; $lang->kanban->storyColumn['released'] = 'Released'; $lang->kanban->storyColumn['closed'] = 'Closed'; diff --git a/module/kanban/lang/fr.php b/module/kanban/lang/fr.php index 0a121915bf..dc55ca522a 100644 --- a/module/kanban/lang/fr.php +++ b/module/kanban/lang/fr.php @@ -147,10 +147,13 @@ $lang->kanban->alignmentList['center'] = 'Center'; $lang->kanban->alignmentList['left'] = 'Left'; $lang->kanban->type = array(); -$lang->kanban->type['all'] = "All KanBan"; -$lang->kanban->type['story'] = "Story KanBan"; -$lang->kanban->type['task'] = "Task KanBan"; -$lang->kanban->type['bug'] = "Bug KanBan"; +$lang->kanban->type['all'] = "All KanBan"; +$lang->kanban->type['epic'] = "{$lang->ERCommon} KanBan"; +$lang->kanban->type['requirement'] = "{$lang->URCommon} KanBan"; +$lang->kanban->type['parentStory'] = "Parent Story KanBan"; +$lang->kanban->type['story'] = "Story KanBan"; +$lang->kanban->type['task'] = "Task KanBan"; +$lang->kanban->type['bug'] = "Bug KanBan"; $lang->kanban->group = new stdclass(); @@ -163,6 +166,10 @@ $lang->kanban->group->story['module'] = "Story Module"; $lang->kanban->group->story['source'] = "Story Source"; $lang->kanban->group->story['assignedTo'] = "Assigned To"; +$lang->kanban->group->epic = $lang->kanban->group->story; +$lang->kanban->group->requirement = $lang->kanban->group->story; +$lang->kanban->group->parentStory = $lang->kanban->group->story; + $lang->kanban->group->task = array(); $lang->kanban->group->task['default'] = "Default Group"; $lang->kanban->group->task['pri'] = "Task Priority"; @@ -248,13 +255,24 @@ $lang->kanban->defaultColumn['done'] = 'done'; $lang->kanban->defaultColumn['closed'] = 'close'; $lang->kanban->laneTypeList = array(); -$lang->kanban->laneTypeList['story'] = $lang->SRCommon; -$lang->kanban->laneTypeList['bug'] = 'Bug'; -$lang->kanban->laneTypeList['task'] = 'Task'; +$lang->kanban->laneTypeList['parentStory'] = "Parent " . $lang->SRCommon; +$lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$lang->kanban->laneTypeList['bug'] = 'Bug'; +$lang->kanban->laneTypeList['task'] = 'Task'; + +$lang->kanban->ERURColumn = array(); +$lang->kanban->ERURColumn['defining'] = 'Defining'; +$lang->kanban->ERURColumn['planning'] = 'Planning'; +$lang->kanban->ERURColumn['developing'] = 'Developing'; +$lang->kanban->ERURColumn['delivering'] = 'Delivering'; +$lang->kanban->ERURColumn['closed'] = 'Closed'; $lang->kanban->storyColumn = array(); $lang->kanban->storyColumn['backlog'] = 'Backlog'; $lang->kanban->storyColumn['ready'] = 'Ready'; +$lang->kanban->storyColumn['design'] = 'Design'; +$lang->kanban->storyColumn['designing'] = 'Designing'; +$lang->kanban->storyColumn['designed'] = 'Designed'; $lang->kanban->storyColumn['develop'] = 'Development'; $lang->kanban->storyColumn['developing'] = 'Doing'; $lang->kanban->storyColumn['developed'] = 'Done'; @@ -262,6 +280,8 @@ $lang->kanban->storyColumn['test'] = 'Testing'; $lang->kanban->storyColumn['testing'] = 'Doing'; $lang->kanban->storyColumn['tested'] = 'Done'; $lang->kanban->storyColumn['verified'] = 'Verified'; +$lang->kanban->storyColumn['rejected'] = 'Rejected'; +$lang->kanban->storyColumn['pending'] = 'Pending'; $lang->kanban->storyColumn['released'] = 'Released'; $lang->kanban->storyColumn['closed'] = 'Closed'; diff --git a/module/kanban/lang/zh-cn.php b/module/kanban/lang/zh-cn.php index 4c24565eb8..135434f95a 100644 --- a/module/kanban/lang/zh-cn.php +++ b/module/kanban/lang/zh-cn.php @@ -147,10 +147,13 @@ $lang->kanban->alignmentList['center'] = '居中'; $lang->kanban->alignmentList['left'] = '居左'; $lang->kanban->type = array(); -$lang->kanban->type['all'] = "综合看板"; -$lang->kanban->type['story'] = "{$lang->SRCommon}看板"; -$lang->kanban->type['task'] = "任务看板"; -$lang->kanban->type['bug'] = "Bug看板"; +$lang->kanban->type['all'] = "综合看板"; +$lang->kanban->type['epic'] = "{$lang->ERCommon}看板"; +$lang->kanban->type['requirement'] = "{$lang->URCommon}看板"; +$lang->kanban->type['parentStory'] = "父{$lang->SRCommon}看板"; +$lang->kanban->type['story'] = "{$lang->SRCommon}看板"; +$lang->kanban->type['task'] = "任务看板"; +$lang->kanban->type['bug'] = "Bug看板"; $lang->kanban->group = new stdclass(); @@ -163,6 +166,10 @@ $lang->kanban->group->story['module'] = "需求模块"; $lang->kanban->group->story['source'] = "需求来源"; $lang->kanban->group->story['assignedTo'] = "指派人员"; +$lang->kanban->group->epic = $lang->kanban->group->story; +$lang->kanban->group->requirement = $lang->kanban->group->story; +$lang->kanban->group->parentStory = $lang->kanban->group->story; + $lang->kanban->group->task = array(); $lang->kanban->group->task['default'] = "默认分组"; $lang->kanban->group->task['pri'] = "任务优先级"; @@ -248,13 +255,24 @@ $lang->kanban->defaultColumn['done'] = '已完成'; $lang->kanban->defaultColumn['closed'] = '已关闭'; $lang->kanban->laneTypeList = array(); -$lang->kanban->laneTypeList['story'] = $lang->SRCommon; -$lang->kanban->laneTypeList['bug'] = 'Bug'; -$lang->kanban->laneTypeList['task'] = '任务'; +$lang->kanban->laneTypeList['parentStory'] = "父" . $lang->SRCommon; +$lang->kanban->laneTypeList['story'] = $lang->SRCommon; +$lang->kanban->laneTypeList['bug'] = 'Bug'; +$lang->kanban->laneTypeList['task'] = '任务'; + +$lang->kanban->ERURColumn = array(); +$lang->kanban->ERURColumn['defining'] = '定义中'; +$lang->kanban->ERURColumn['planning'] = '规划中'; +$lang->kanban->ERURColumn['developing'] = '研发中'; +$lang->kanban->ERURColumn['delivering'] = '交付中'; +$lang->kanban->ERURColumn['closed'] = '已关闭'; $lang->kanban->storyColumn = array(); $lang->kanban->storyColumn['backlog'] = 'Backlog'; $lang->kanban->storyColumn['ready'] = '准备好'; +$lang->kanban->storyColumn['design'] = '设计'; +$lang->kanban->storyColumn['designing'] = '设计中'; +$lang->kanban->storyColumn['designed'] = '设计完毕'; $lang->kanban->storyColumn['develop'] = '开发'; $lang->kanban->storyColumn['developing'] = '进行中'; $lang->kanban->storyColumn['developed'] = '完成'; @@ -262,6 +280,8 @@ $lang->kanban->storyColumn['test'] = '测试'; $lang->kanban->storyColumn['testing'] = '进行中'; $lang->kanban->storyColumn['tested'] = '完成'; $lang->kanban->storyColumn['verified'] = '已验收'; +$lang->kanban->storyColumn['rejected'] = '验收失败'; +$lang->kanban->storyColumn['pending'] = '待发布'; $lang->kanban->storyColumn['released'] = '已发布'; $lang->kanban->storyColumn['closed'] = '已关闭'; diff --git a/module/kanban/model.php b/module/kanban/model.php index bae15c39a6..1bce19766a 100755 --- a/module/kanban/model.php +++ b/module/kanban/model.php @@ -1062,7 +1062,7 @@ class kanbanModel extends model ->where('deleted')->eq('0') ->andWhere('region')->in($regions) ->beginIf($browseType != 'all')->andWhere('type')->eq($browseType)->fi() - ->orderBy('order') + ->orderBy('order_asc') ->fetchAll(); $actions = array('sortLane', 'deleteLane', 'editLaneName', 'editLaneColor'); @@ -1382,9 +1382,10 @@ class kanbanModel extends model ->fetchgroup('lane', 'column'); /* Get group objects. */ - if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); - if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); - if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); + if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); $cardGroup = array(); @@ -1403,6 +1404,7 @@ class kanbanModel extends model $object = zget($objects, $cardID, array()); if(empty($object)) continue; + if($cell->type == 'parentStory' && $object->isParent != '1') continue; $cardData = $this->kanbanTao->initCardItem($object, $cell, $order, $avatarPairs, $users); $cardData['acl'] = 'open'; @@ -1431,7 +1433,7 @@ class kanbanModel extends model */ public function getExecutionKanban(int $executionID, string $browseType = 'all', string $groupBy = 'default', string $searchValue = '', string $orderBy = 'id_asc'): array { - if($groupBy != 'default') return $this->getKanban4Group($executionID, $browseType, $groupBy, $searchValue, $orderBy); + if($groupBy != 'default') return array($this->getKanban4Group($executionID, $browseType, $groupBy, $searchValue, $orderBy), array()); $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) ->where('execution')->eq($executionID) @@ -1452,31 +1454,80 @@ class kanbanModel extends model ->fetchGroup('lane', 'id'); /* Get group objects. */ - if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); - if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); - if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); + if($browseType == 'all' || $browseType == 'epic') $objectGroup['epic'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic'); + if($browseType == 'all' || $browseType == 'requirement') $objectGroup['requirement'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement'); + if($browseType == 'all' || $browseType == 'parentStory') $objectGroup['parentStory'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'story') $objectGroup['story'] = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + if($browseType == 'all' || $browseType == 'bug') $objectGroup['bug'] = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'all' || $browseType == 'task') $objectGroup['task'] = $this->loadModel('execution')->getKanbanTasks($executionID, "id"); /* Get objects cards menus. */ $menus = array(); - $menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array(); - $menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array(); - $menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array(); + $menus['epic'] = $browseType == 'all' || $browseType == 'epic' ? $this->getKanbanCardMenu($executionID, $objectGroup['epic'], 'epic') : array(); + $menus['requirement'] = $browseType == 'all' || $browseType == 'requirement' ? $this->getKanbanCardMenu($executionID, $objectGroup['requirement'], 'requirement') : array(); + $menus['parentStory'] = $browseType == 'all' || $browseType == 'parentStory' ? $this->getKanbanCardMenu($executionID, $objectGroup['parentStory'], 'parentStory') : array(); + $menus['story'] = $browseType == 'all' || $browseType == 'story' ? $this->getKanbanCardMenu($executionID, $objectGroup['story'], 'story') : array(); + $menus['bug'] = $browseType == 'all' || $browseType == 'bug' ? $this->getKanbanCardMenu($executionID, $objectGroup['bug'], 'bug') : array(); + $menus['task'] = $browseType == 'all' || $browseType == 'task' ? $this->getKanbanCardMenu($executionID, $objectGroup['task'], 'task') : array(); - /* Build kanban group data. */ - $kanbanGroup = array(); + /* 获取看板连线的fromKanbanID. */ + $fromKanbanID = ''; foreach($lanes as $lane) { - list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus); - - $kanbanGroup[$lane->type]['id'] = $lane->id; - $kanbanGroup[$lane->type]['key'] = 'group' . $lane->id; - $kanbanGroup[$lane->type]['data'] = array(); - $kanbanGroup[$lane->type]['data']['lanes'] = array($laneData); - $kanbanGroup[$lane->type]['data']['cols'] = $columnData; - $kanbanGroup[$lane->type]['data']['items'] = $cardsData; + if($lane->type == 'parentStory') $fromKanbanID = 'group' . $lane->id; } - return array_values($kanbanGroup); + /* Build kanban group data. */ + $kanbanGroup = array(); + $links = array(); + $execution = $this->loadModel('execution')->fetchById($executionID); + $project = $this->loadModel('project')->fetchById($execution->project); + foreach($lanes as $lane) + { + $laneType = $lane->type; + if(in_array($laneType, array('epic', 'requirement')) && strpos($project->storyType, $laneType) === false) continue; + list($laneData, $columnData, $cardsData) = $this->buildExecutionGroup($lane, $columns, $objectGroup, $searchValue, $menus); + + $kanbanID = 'group' . $lane->id; + + $kanbanGroup[$laneType]['id'] = $lane->id; + $kanbanGroup[$laneType]['key'] = $kanbanID; + $kanbanGroup[$laneType]['data'] = array(); + $kanbanGroup[$laneType]['data']['lanes'] = array($laneData); + $kanbanGroup[$laneType]['data']['cols'] = $columnData; + $kanbanGroup[$laneType]['data']['items'] = $cardsData; + + foreach($cardsData as $columnCard) + { + foreach($columnCard as $cards) + { + foreach($cards as $card) + { + if($card['parent'] == 0) continue; + /* 获取看板卡片的连线关系,业需、用需和父需求共用同一组看板列,所以fromKanban和toKanban是一样的。 */ + $link = array('from' => $card['parent'], 'to' => $card['id'], 'fromKanban' => $fromKanbanID, 'toKanban' => $kanbanID); + if(in_array($laneType, array('epic', 'requirement'))) $link['toKanban'] = $fromKanbanID; + + $links[] = $link; + } + } + } + } + + /* 展示所有泳道的时候,业务、用需与父需求泳道共用看板列。 */ + if($browseType == 'all') + { + foreach(array('requirement', 'epic') as $laneType) + { + if(!isset($kanbanGroup[$laneType])) continue; + + array_unshift($kanbanGroup['parentStory']['data']['lanes'], $kanbanGroup[$laneType]['data']['lanes'][0]); + $kanbanGroup['parentStory']['data']['items'] += $kanbanGroup[$laneType]['data']['items']; + unset($kanbanGroup[$laneType]); + } + } + + return array(array_values($kanbanGroup), $links); } /** @@ -1590,6 +1641,7 @@ class kanbanModel extends model $cardData['column'] = $column->id; $cardData['pri'] = zget($object, 'pri', 0); $cardData['group'] = $laneType; + $cardData['parent'] = zget($object, 'originParent', 0); $cardData['status'] = zget($object, 'status', ''); $cardData['estimate'] = zget($object, 'estimate', 0); $cardData['assignedTo'] = $object->assignedTo; @@ -1630,9 +1682,12 @@ class kanbanModel extends model { /* Get card data. */ $cardList = array(); - if($browseType == 'story') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); - if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID); - if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID); + if(in_array($browseType, array('story', 'parentStory'))) $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory'); + + if($browseType == 'epic') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'epic'); + if($browseType == 'requirement') $cardList = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', 'requirement'); + if($browseType == 'bug') $cardList = $this->loadModel('bug')->getExecutionBugs($executionID); + if($browseType == 'task') $cardList = $this->loadModel('execution')->getKanbanTasks($executionID); if($browseType == 'task' && $groupBy == 'assignedTo') $cardList = $this->appendTeamMember($cardList); @@ -2305,8 +2360,14 @@ class kanbanModel extends model */ public function createExecutionLane(int $executionID, string $type = 'all') { + $ERURLanes = array(); + $parentStoryLaneID = 0; + $execution = $this->loadModel('execution')->fetchById($executionID); foreach($this->config->kanban->default as $type => $lane) { + /* 只有综合、需求、设计阶段,才可关联业需、用需。 */ + if($execution->type != 'stage' && !in_array($execution->attribute, array('mix', 'request', 'design')) && in_array($type, array('epic', 'requirement'))) continue; + $lane->type = $type; $lane->execution = $executionID; $lane->region = 0; @@ -2316,7 +2377,29 @@ class kanbanModel extends model $this->dao->insert(TABLE_KANBANLANE)->data($lane)->exec(); $laneID = $this->dao->lastInsertId(); - $this->createExecutionColumns($laneID, $type, $executionID); + if($type == 'parentStory') $parentStoryLaneID = $laneID; + + if(in_array($type, array('epic', 'requirement'))) + { + $ERURLanes[$type] = $laneID; + } + else + { + $this->createExecutionColumns($laneID, $type, $executionID); + } + } + + if($ERURLanes) + { + $columnIDList = $this->dao->select('`column`')->from(TABLE_KANBANCELL)->where('lane')->eq($parentStoryLaneID)->fetchPairs(); + foreach($ERURLanes as $type => $laneID) + { + foreach($columnIDList as $columnID) + { + $this->addKanbanCell($executionID, $laneID, $columnID, $type); + if(dao::isError()) return false; + } + } } } @@ -2332,9 +2415,10 @@ class kanbanModel extends model */ public function createExecutionColumns(int|array $laneID, string $type, int $executionID) { - $devColumnID = $testColumnID = $resolvingColumnID = 0; + $designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0; $columns = array(); + if(in_array($type, array('epic', 'requirement', 'parentStory'))) $columns = $this->lang->kanban->ERURColumn; if($type == 'story') $columns = $this->lang->kanban->storyColumn; if($type == 'bug') $columns = $this->lang->kanban->bugColumn; if($type == 'task') $columns = $this->lang->kanban->taskColumn; @@ -2348,6 +2432,7 @@ class kanbanModel extends model $data->type = $colType; $data->region = 0; + if(str_contains(',designing,designed,', ",{$colType},")) $data->parent = $designColumnID; if(str_contains(',developing,developed,', ",{$colType},")) $data->parent = $devColumnID; if(str_contains(',testing,tested,', ",{$colType},")) $data->parent = $testColumnID; if(str_contains(',fixing,fixed,', ",{$colType},")) $data->parent = $resolvingColumnID; @@ -2356,6 +2441,7 @@ class kanbanModel extends model $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); $colID = $this->dao->lastInsertId(); + if($colType == 'design') $designColumnID = $colID; if($colType == 'develop') $devColumnID = $colID; if($colType == 'test') $testColumnID = $colID; if($colType == 'resolving') $resolvingColumnID = $colID; @@ -2524,7 +2610,9 @@ class kanbanModel extends model */ public function createRDLane(int $executionID, int $regionID) { - $laneIndex = 0; + $execution = $this->loadModel('execution')->fetchByID($executionID); + $project = $this->loadModel('project')->fetchByID($execution->project); + $index = 0; foreach($this->lang->kanban->laneTypeList as $type => $name) { $groupID = $this->createGroup($executionID, $regionID); @@ -2536,13 +2624,16 @@ class kanbanModel extends model $lane->region = $regionID; $lane->group = $groupID; $lane->name = $name; - $lane->color = $this->config->kanban->laneColorList[$laneIndex]; - $lane->order = ++ $laneIndex * 5; + $lane->color = $this->config->kanban->laneColorList[$index]; + $lane->order = $this->config->kanban->RDLaneOrder[$type]; $this->dao->insert(TABLE_KANBANLANE)->data($lane)->autoCheck()->exec(); if(dao::isError()) return false; + $laneID = $this->dao->lastInsertId(); - $this->createRDColumn($regionID, $groupID, $this->dao->lastInsertId(), $type, $executionID); + $this->createRDColumn($regionID, $groupID, $laneID, $type, $executionID); + + $index ++; } } @@ -2561,10 +2652,11 @@ class kanbanModel extends model */ public function createRDColumn(int $regionID, int $groupID, int $laneID, string $laneType, int $executionID) { - $devColumnID = $testColumnID = $resolvingColumnID = 0; - if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn; - if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn; - if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn; + $designColumnID = $devColumnID = $testColumnID = $resolvingColumnID = 0; + if($laneType == 'parentStory') $columnList = $this->lang->kanban->ERURColumn; + if($laneType == 'story') $columnList = $this->lang->kanban->storyColumn; + if($laneType == 'bug') $columnList = $this->lang->kanban->bugColumn; + if($laneType == 'task') $columnList = $this->lang->kanban->taskColumn; foreach($columnList as $type => $name) { @@ -2575,16 +2667,18 @@ class kanbanModel extends model $data->group = $groupID; $data->region = $regionID; + if(strpos(',designing,designed,', $type) !== false) $data->parent = $designColumnID; if(strpos(',developing,developed,', $type) !== false) $data->parent = $devColumnID; - if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID; - if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID; - if(strpos(',develop,test,resolving,', $type) !== false) $data->parent = -1; + if(strpos(',testing,tested,', $type) !== false) $data->parent = $testColumnID; + if(strpos(',fixing,fixed,', $type) !== false) $data->parent = $resolvingColumnID; + if(strpos(',design,develop,test,resolving,', $type) !== false) $data->parent = -1; $this->dao->insert(TABLE_KANBANCOLUMN)->data($data)->exec(); if(dao::isError()) return false; - if($type == 'develop') $devColumnID = $this->dao->lastInsertId(); - if($type == 'test') $testColumnID = $this->dao->lastInsertId(); + if($type == 'design') $designColumnID = $this->dao->lastInsertId(); + if($type == 'develop') $devColumnID = $this->dao->lastInsertId(); + if($type == 'test') $testColumnID = $this->dao->lastInsertId(); if($type == 'resolving') $resolvingColumnID = $this->dao->lastInsertId(); $this->addKanbanCell($executionID, $laneID, $this->dao->lastInsertId(), $laneType); @@ -2636,7 +2730,7 @@ class kanbanModel extends model * @access public * @return void */ - public function updateLane(int $executionID, string $laneType, int $cardID = 0) + public function updateLane(int $executionID, string $laneType = '', int $cardID = 0) { $execution = $this->loadModel('execution')->getByID($executionID); if($execution->type == 'kanban') @@ -2648,18 +2742,18 @@ class kanbanModel extends model ->andWhere('t2.deleted')->eq(0) ->andWhere('t1.kanban')->eq($executionID) ->andWhere('t2.execution')->eq($executionID) - ->andWhere('t2.type')->eq($laneType) + ->beginIF($laneType)->andWhere('t2.type')->eq($laneType)->fi() ->beginIF(!empty($cardID))->andWhere('t3.cards')->like("%,$cardID,%")->fi() ->orderBy('t1.`order` asc, t2.`order` asc') ->fetchAll('id'); - if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 1); + if(count($lanes) > 1) $lanes = array_slice($lanes, 0, 2); } else { $lanes = $this->dao->select('*')->from(TABLE_KANBANLANE) ->where('execution')->eq($executionID) - ->andWhere('type')->eq($laneType) + ->beginIF($laneType)->andWhere('type')->eq($laneType)->fi() ->fetchAll('id'); } @@ -2702,6 +2796,7 @@ class kanbanModel extends model if(empty($cardPairs)) return; $sourceCards = $cardPairs; + if(in_array($laneType, array('epic', 'requirement', 'parentStory'))) $cardPairs = $this->kanbanTao->refreshERURCards($cardPairs, $executionID, $otherCardList, $laneType); if($laneType == 'story') $cardPairs = $this->kanbanTao->refreshStoryCards($cardPairs, $executionID, $otherCardList); if($laneType == 'bug') $cardPairs = $this->kanbanTao->refreshBugCards($cardPairs, $executionID, $otherCardList); if($laneType == 'task') $cardPairs = $this->kanbanTao->refreshTaskCards($cardPairs, $executionID, $otherCardList); @@ -3486,6 +3581,15 @@ class kanbanModel extends model $menus = array(); switch ($objecType) { + case 'epic': + $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects); + break; + case 'requirement': + $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects); + break; + case 'parentStory': + $menus = $this->kanbanTao->getERURCardMenu($executionID, $objects); + break; case 'story': if($execution) $menus = $this->kanbanTao->getStoryCardMenu($execution, $objects); break; diff --git a/module/kanban/tao.php b/module/kanban/tao.php index a2bb4801bf..7766dc1039 100644 --- a/module/kanban/tao.php +++ b/module/kanban/tao.php @@ -426,18 +426,45 @@ class kanbanTao extends kanbanModel */ protected function buildRDRegionData(array $regionData, array $groups, array $laneGroup, array $columnGroup, array $cardGroup, string $searchValue = '') { - $laneCount = 0; - $groupData = array(); + $laneCount = 0; + $groupData = array(); + $fromKanbanID = ''; foreach($groups as $group) { $lanes = zget($laneGroup, $group->id, array()); if(!$lanes) continue; + $kanbanID = "group{$group->id}"; + $cols = zget($columnGroup, $group->id, array()); $items = zget($cardGroup, $group->id, array()); if($searchValue != '' and empty($items)) continue; + $lane = current($lanes); + if($lane['type'] == 'parentStory') + { + if(empty($items)) + { + unset($this->lang->kanban->type['parentStory']); + continue; + } + $fromKanbanID = $kanbanID; + } + elseif($lane['type'] == 'story') + { + foreach($items as $colGroup) + { + foreach($colGroup as $cards) + { + foreach($cards as $card) + { + $regionData['links'][] = array('fromKanban' => $fromKanbanID, 'toKanban' => $kanbanID, 'from' => $card['parent'], 'to' => $card['id']); + } + } + } + } + /* 计算各个列上的卡片数量。 */ $columnCount = array(); $parentCols = array(); @@ -463,7 +490,7 @@ class kanbanTao extends kanbanModel $laneCount += count($lanes); $groupData['id'] = $group->id; - $groupData['key'] = "group{$group->id}"; + $groupData['key'] = $kanbanID; $groupData['data']['lanes'] = $lanes; $groupData['data']['cols'] = $cols; $groupData['data']['items'] = $items; @@ -499,6 +526,7 @@ class kanbanTao extends kanbanModel $item['pri'] = $card->pri; $item['color'] = $card->color; $item['assignedTo'] = $card->assignedTo; + $item['parent'] = !empty($card->originParent) ? $card->originParent : 0; $item['progress'] = !empty($card->progress) ? $card->progress : 0; $item['group'] = !empty($card->group) ? $card->group : ''; $item['region'] = !empty($card->region) ? $card->region : ''; @@ -597,6 +625,7 @@ class kanbanTao extends kanbanModel $cardList = array(); $avatarPairs = $this->loadModel('user')->getAvatarPairs(); $users = $this->loadModel('user')->getPairs('noletter'); + $module = $browseType == 'parentStory' ? 'story' : $browseType; foreach($lanes as $laneID => $lane) { $laneData = array(); @@ -604,7 +633,7 @@ class kanbanTao extends kanbanModel $laneData['type'] = $browseType; $laneData['name'] = $laneData['id']; $laneData['region'] = $lane->execution; - $laneData['title'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$browseType->$groupBy . ':' . $lane->name : $lane->name; + $laneData['title'] = (($groupBy == 'pri' or $groupBy == 'severity') and $laneID) ? $this->lang->$module->$groupBy . ':' . $lane->name : $lane->name; $laneData['color'] = $lane->color; $laneData['order'] = $lane->order; @@ -739,6 +768,7 @@ class kanbanTao extends kanbanModel */ protected function getObjectPairs(string $groupBy, array $groupByList, string $browseType, string $orderBy): array { + if($browseType == 'parentStory') $browseType = 'story'; $objectPairs = array(); if(in_array($groupBy, array('module', 'story', 'assignedTo'))) { @@ -770,6 +800,41 @@ class kanbanTao extends kanbanModel return $objectPairs; } + /** + * 更新专业研发看板中的用需、业需、父需求泳道上的卡片。 + * Update card of feature, epic, parent story lane in RD kanban. + * + * @param array $cardPairs + * @param int $executionID + * @param string $otherCardList + * @param string $laneType + * @access public + * @return array + */ + protected function refreshERURCards(array $cardPairs, int $executionID, string $otherCardList, $laneType = 'story'): array + { + $storyType = $laneType == 'parentStory' ? 'story' : $laneType; + $stories = $this->loadModel('story')->getExecutionStories($executionID, 0, 't1.`order`_desc', 'allStory', '0', $storyType, $otherCardList); + foreach($stories as $storyID => $story) + { + if($laneType == 'parentStory' && $story->isParent != '1') continue; + foreach($this->lang->kanban->ERURColumn as $stage => $langItem) + { + if($story->stage != $stage and strpos($cardPairs[$stage], ",$storyID,") !== false) + { + $cardPairs[$stage] = str_replace(",$storyID,", ',', $cardPairs[$stage]); + } + + if($story->stage == $stage and strpos($cardPairs[$stage], ",$storyID,") === false) + { + $cardPairs[$stage] = empty($cardPairs[$stage]) ? ",$storyID," : ",$storyID" . $cardPairs[$stage]; + } + } + } + + return $cardPairs; + } + /** * 更新专业研发看板中的需求泳道上的卡片。 * Update card of story lane in RD kanban. @@ -787,12 +852,13 @@ class kanbanTao extends kanbanModel { foreach($this->config->kanban->storyColumnStageList as $colType => $stage) { + if(!isset($cardPairs[$colType])) continue; if($story->stage != $stage and strpos($cardPairs[$colType], ",$storyID,") !== false) { $cardPairs[$colType] = str_replace(",$storyID,", ',', $cardPairs[$colType]); } - if(strpos(',ready,backlog,develop,test,', $colType) !== false) continue; + if(strpos(',ready,backlog,design,develop,test,', $colType) !== false) continue; if($story->stage == $stage and strpos($cardPairs[$colType], ",$storyID,") === false) { @@ -899,6 +965,38 @@ class kanbanTao extends kanbanModel return $cardPairs; } + /** + * 获取父需求类型的卡片操作菜单。 + * Get menu of story card. + * + * @param int $executionID + * @param array $objects + * @access public + * @return array + */ + protected function getERURCardMenu(int $executionID, array $objects): array + { + $execution = $this->loadModel('execution')->getByID($executionID); + + $menus = array(); + $objects = $this->loadModel('story')->mergeReviewer($objects); + foreach($objects as $story) + { + $menu = array(); + + if(common::hasPriv($story->type, 'edit') and $this->story->isClickable($story, 'edit')) $menu[] = array('label' => $this->lang->story->edit, 'icon' => 'edit', 'url' => helper::createLink($story->type, 'edit', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'change') and $this->story->isClickable($story, 'change')) $menu[] = array('label' => $this->lang->story->change, 'icon' => 'alter', 'url' => helper::createLink($story->type, 'change', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'review') and $this->story->isClickable($story, 'review')) $menu[] = array('label' => $this->lang->story->review, 'icon' => 'search', 'url' => helper::createLink($story->type, 'review', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv($story->type, 'activate') and $this->story->isClickable($story, 'activate')) $menu[] = array('label' => $this->lang->story->activate, 'icon' => 'magic', 'url' => helper::createLink($story->type, 'activate', "storyID=$story->id"), 'modal' => true, 'size' => 'lg'); + if(common::hasPriv('execution', 'unlinkStory') && $execution->hasProduct) $menu[] = array('label' => $this->lang->execution->unlinkStory, 'icon' => 'unlink', 'url' => helper::createLink('execution', 'unlinkStory', "executionID=$executionID&storyID=$story->story&confirm=no&from=taskkanban")); + if(common::hasPriv($story->type, 'delete')) $menu[] = array('label' => $this->lang->story->delete, 'icon' => 'trash', 'url' => helper::createLink($story->type, 'delete', "storyID=$story->id&confirm=no&from=taskkanban")); + + $menus[$story->id] = $menu; + } + + return $menus; + } + /** * 获取需求类型的卡片操作菜单。 * Get menu of story card. diff --git a/module/kanban/test/lib/kanban.unittest.class.php b/module/kanban/test/lib/kanban.unittest.class.php index 3be2d716ec..282f4d3ad7 100644 --- a/module/kanban/test/lib/kanban.unittest.class.php +++ b/module/kanban/test/lib/kanban.unittest.class.php @@ -583,7 +583,7 @@ class kanbanTest if(empty($objects)) { $this->objectModel->createExecutionLane($executionID, $browseType, $groupBy); - $objects = $this->objectModel->getExecutionKanban($executionID, $browseType, $groupBy); + list($objects, $links) = $this->objectModel->getExecutionKanban($executionID, $browseType, $groupBy); } if(dao::isError()) return dao::getError(); diff --git a/module/kanban/test/model/getexecutionkanban.php b/module/kanban/test/model/getexecutionkanban.php index 5505826c36..24d7e941df 100755 --- a/module/kanban/test/model/getexecutionkanban.php +++ b/module/kanban/test/model/getexecutionkanban.php @@ -20,23 +20,23 @@ title=测试 kanbanModel->getExecutionKanban(); timeout=0 cid=1 -- 测试获取execution2的执行看板信息 @columns:2, lanes:2, cards:65 +- 测试获取execution2的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution2 story的执行看板信息 @columns:1, lanes:1, cards:0 +- 测试获取execution2 story的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution2 task的执行看板信息 @columns:1, lanes:1, cards:65 +- 测试获取execution2 task的执行看板信息 @columns:0, lanes:0, cards:0 - 测试获取execution2 bug的执行看板信息 @columns:10, lanes:1, cards:0 -- 测试获取execution3的执行看板信息 @columns:2, lanes:2, cards:53 +- 测试获取execution3的执行看板信息 @columns:0, lanes:0, cards:0 - 测试获取execution3 story pri的执行看板信息 @columns:0, lanes:1, cards:0 - 测试获取execution3 story category的执行看板信息 @columns:0, lanes:1, cards:0 -- 测试获取execution4的执行看板信息 @columns:2, lanes:2, cards:53 +- 测试获取execution4的执行看板信息 @columns:0, lanes:0, cards:0 -- 测试获取execution4 task module的执行看板信息 @columns:2, lanes:1, cards:107 +- 测试获取execution4 task module的执行看板信息 @columns:2, lanes:1, cards:109 - 测试获取execution4 story source的执行看板信息 @columns:0, lanes:1, cards:0 @@ -48,13 +48,13 @@ $groupByList = array('pri', 'category', 'module', 'source', 'assignedTo', 's $kanban = new kanbanTest(); -r($kanban->getExecutionKanbanTest($executionIDList[0])) && p() && e('columns:2, lanes:2, cards:65'); // 测试获取execution2的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[0])) && p() && e('columns:1, lanes:1, cards:0'); // 测试获取execution2 story的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[1])) && p() && e('columns:1, lanes:1, cards:65'); // 测试获取execution2 task的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[2])) && p() && e('columns:10, lanes:1, cards:0'); // 测试获取execution2 bug的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[1])) && p() && e('columns:2, lanes:2, cards:53'); // 测试获取execution3的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[0])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2 story的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[1])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution2 task的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[0], $browseTypeList[2])) && p() && e('columns:10, lanes:1, cards:0'); // 测试获取execution2 bug的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[1])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution3的执行看板信息 r($kanban->getExecutionKanbanTest($executionIDList[1], $browseTypeList[0], $groupByList[0])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution3 story pri的执行看板信息 r($kanban->getExecutionKanbanTest($executionIDList[1], $browseTypeList[0], $groupByList[1])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution3 story category的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2])) && p() && e('columns:2, lanes:2, cards:53'); // 测试获取execution4的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[1], $groupByList[2])) && p() && e('columns:2, lanes:1, cards:107'); // 测试获取execution4 task module的执行看板信息 -r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[0], $groupByList[3])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution4 story source的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2])) && p() && e('columns:0, lanes:0, cards:0'); // 测试获取execution4的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[1], $groupByList[2])) && p() && e('columns:2, lanes:1, cards:109'); // 测试获取execution4 task module的执行看板信息 +r($kanban->getExecutionKanbanTest($executionIDList[2], $browseTypeList[0], $groupByList[3])) && p() && e('columns:0, lanes:1, cards:0'); // 测试获取execution4 story source的执行看板信息 \ No newline at end of file diff --git a/module/mail/model.php b/module/mail/model.php index a34d3c8c37..2a95c7069a 100644 --- a/module/mail/model.php +++ b/module/mail/model.php @@ -668,6 +668,8 @@ class mailModel extends model if($objectType == 'story' or $objectType == 'bug') $suffix = empty($object->product) ? '' : ' - ' . $this->loadModel('product')->getById($object->product)->name; if($objectType == 'task') $suffix = empty($object->execution) ? '' : ' - ' . $this->loadModel('execution')->getById($object->execution)->name; + if($objectType == 'story') $objectType = $object->type; // 业务需求和用户需求的邮件标题不同。 + return strtoupper($objectType) . ' #' . $object->id . ' ' . $title . $suffix; } diff --git a/module/message/config.php b/module/message/config.php index dd619503f1..57715d4634 100644 --- a/module/message/config.php +++ b/module/message/config.php @@ -1,6 +1,8 @@ message->objectTypes = array(); $config->message->objectTypes['product'] = array('opened', 'edited', 'closed', 'undeleted'); +if($config->enableER) $config->message->objectTypes['epic'] = array('opened', 'edited', 'commented', 'changed', 'reviewed', 'closed', 'activated', 'assigned'); +if($config->URAndSR) $config->message->objectTypes['requirement'] = array('opened', 'edited', 'commented', 'changed', 'reviewed', 'closed', 'activated', 'assigned'); $config->message->objectTypes['story'] = array('opened', 'edited', 'commented', 'frombug', 'changed', 'reviewed', 'closed', 'activated', 'assigned'); $config->message->objectTypes['productplan'] = array('opened', 'edited'); $config->message->objectTypes['project'] = array('opened', 'edited', 'started', 'delayed', 'suspended', 'closed', 'activated', 'undeleted'); @@ -13,22 +15,26 @@ $config->message->objectTypes['doc'] = array('releaseddoc', 'edited'); $config->message->objectTypes['kanbancard'] = array('created', 'edited', 'finished', 'activated', 'archived', 'restore', 'deleted', 'moved'); $config->message->available = array(); -$config->message->available['mail']['story'] = $config->message->objectTypes['story']; -$config->message->available['mail']['task'] = $config->message->objectTypes['task']; -$config->message->available['mail']['bug'] = $config->message->objectTypes['bug']; -$config->message->available['mail']['testtask'] = array('opened', 'edited', 'closed'); -$config->message->available['mail']['doc'] = $config->message->objectTypes['doc']; -$config->message->available['mail']['kanbancard'] = $config->message->objectTypes['kanbancard']; +if($config->enableER) $config->message->available['mail']['epic'] = $config->message->objectTypes['epic']; +if($config->URAndSR) $config->message->available['mail']['requirement'] = $config->message->objectTypes['requirement']; +$config->message->available['mail']['story'] = $config->message->objectTypes['story']; +$config->message->available['mail']['task'] = $config->message->objectTypes['task']; +$config->message->available['mail']['bug'] = $config->message->objectTypes['bug']; +$config->message->available['mail']['testtask'] = array('opened', 'edited', 'closed'); +$config->message->available['mail']['doc'] = $config->message->objectTypes['doc']; +$config->message->available['mail']['kanbancard'] = $config->message->objectTypes['kanbancard']; $config->message->available['webhook'] = $config->message->objectTypes; -$config->message->available['message']['bug'] = $config->message->objectTypes['bug']; -$config->message->available['message']['story'] = $config->message->objectTypes['story']; -$config->message->available['message']['task'] = $config->message->objectTypes['task']; -$config->message->available['message']['testtask'] = $config->message->objectTypes['testtask']; -$config->message->available['message']['todo'] = $config->message->objectTypes['todo']; -$config->message->available['message']['doc'] = $config->message->objectTypes['doc']; -$config->message->available['message']['kanbancard'] = $config->message->objectTypes['kanbancard']; +$config->message->available['message']['bug'] = $config->message->objectTypes['bug']; +if($config->enableER) $config->message->available['message']['epic'] = $config->message->objectTypes['epic']; +if($config->URAndSR) $config->message->available['message']['requirement'] = $config->message->objectTypes['requirement']; +$config->message->available['message']['story'] = $config->message->objectTypes['story']; +$config->message->available['message']['task'] = $config->message->objectTypes['task']; +$config->message->available['message']['testtask'] = $config->message->objectTypes['testtask']; +$config->message->available['message']['todo'] = $config->message->objectTypes['todo']; +$config->message->available['message']['doc'] = $config->message->objectTypes['doc']; +$config->message->available['message']['kanbancard'] = $config->message->objectTypes['kanbancard']; $config->message->typeLink = array(); $config->message->typeLink['mail'] = 'mail|index'; diff --git a/module/message/model.php b/module/message/model.php index 0221443740..d2b83b166d 100755 --- a/module/message/model.php +++ b/module/message/model.php @@ -82,6 +82,13 @@ class messageModel extends model $messageSetting = $this->config->message->setting; if(is_string($messageSetting)) $messageSetting = json_decode($messageSetting, true); + /* 如果是业需和用需,则使用它们的发信配置。*/ + if($objectType == 'story') + { + $story = $this->loadModel('story')->fetchByID($objectID); + $objectType = $story->type; + } + if(isset($messageSetting['mail'])) { $actions = $messageSetting['mail']['setting']; @@ -212,11 +219,12 @@ class messageModel extends model } if($toList == 'closed') $toList = ''; - if($objectType == 'story' && $actionID) + if($objectType == 'feedback' && $object->status == 'replied') $toList = ',' . $object->openedBy . ','; + if(in_array($objectType, array('story', 'epic', 'requirement')) && $actionID) { $action = $this->loadModel('action')->getById($actionID); list($toList, $ccList) = $this->loadModel($objectType)->getToAndCcList($object, $action->action); - $toList .= ',' . $ccList; + $toList = $toList . $ccList; } if($objectType == 'testtask') diff --git a/module/metric/calc/execution/scale/count_of_developed_story_in_execution.php b/module/metric/calc/execution/scale/count_of_developed_story_in_execution.php index 5d1e1fc3a0..860c061f6e 100755 --- a/module/metric/calc/execution/scale/count_of_developed_story_in_execution.php +++ b/module/metric/calc/execution/scale/count_of_developed_story_in_execution.php @@ -20,7 +20,7 @@ */ class count_of_developed_story_in_execution extends baseCalc { - public $dataset = 'getDevStoriesWithExecution'; + public $dataset = 'getAllStoriesWithExecution'; public $fieldList = array('t3.project', 't1.stage', 't1.closedReason'); diff --git a/module/metric/calc/execution/scale/count_of_story_in_execution.php b/module/metric/calc/execution/scale/count_of_story_in_execution.php index 04a2b1b7d0..9d6e2f3d30 100755 --- a/module/metric/calc/execution/scale/count_of_story_in_execution.php +++ b/module/metric/calc/execution/scale/count_of_story_in_execution.php @@ -20,7 +20,7 @@ */ class count_of_story_in_execution extends baseCalc { - public $dataset = 'getDevStoriesWithExecution'; + public $dataset = 'getAllStoriesWithExecution'; public $fieldList = array('t3.project'); diff --git a/module/metric/calc/project/scale/count_of_closed_epic_in_project.php b/module/metric/calc/project/scale/count_of_closed_epic_in_project.php new file mode 100644 index 0000000000..60f65dec1c --- /dev/null +++ b/module/metric/calc/project/scale/count_of_closed_epic_in_project.php @@ -0,0 +1,45 @@ + + * @package + * @uses func + * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) + * @Link https://www.zentao.net + */ +class count_of_closed_epic_in_project extends baseCalc +{ + public $dataset = 'getEpicWithProject'; + + public $fieldList = array('t2.project', 't1.status'); + + public $result = array(); + + public function calculate($row) + { + if($row->status == 'closed') + { + if(!isset($this->result[$row->project])) $this->result[$row->project] = 0; + $this->result[$row->project] += 1; + } + } + + public function getResult($options = array()) + { + $records = $this->getRecords(array('project', 'value')); + return $this->filterByOptions($records, $options); + } +} + + diff --git a/module/metric/calc/project/scale/count_of_epic_in_project.php b/module/metric/calc/project/scale/count_of_epic_in_project.php new file mode 100644 index 0000000000..cde5a5396f --- /dev/null +++ b/module/metric/calc/project/scale/count_of_epic_in_project.php @@ -0,0 +1,41 @@ + + * @package + * @uses func + * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) + * @Link https://www.zentao.net + */ +class count_of_epic_in_project extends baseCalc +{ + public $dataset = 'getEpicWithProject'; + + public $fieldList = array('t2.project'); + + public $result = array(); + + public function calculate($row) + { + if(!isset($this->result[$row->project])) $this->result[$row->project] = 0; + $this->result[$row->project] += 1; + } + + public function getResult($options = array()) + { + $records = $this->getRecords(array('project', 'value')); + return $this->filterByOptions($records, $options); + } +} + diff --git a/module/metric/calc/project/scale/count_of_finished_epic_in_project.php b/module/metric/calc/project/scale/count_of_finished_epic_in_project.php new file mode 100644 index 0000000000..fa4a5b2365 --- /dev/null +++ b/module/metric/calc/project/scale/count_of_finished_epic_in_project.php @@ -0,0 +1,46 @@ + + * @package + * @uses func + * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) + * @Link https://www.zentao.net + */ +class count_of_finished_epic_in_project extends baseCalc +{ + public $dataset = 'getEpicWithProject'; + + public $fieldList = array('t2.project', 't1.status', 't1.closedReason'); + + public $result = array(); + + public function calculate($row) + { + if($row->status == 'closed' && $row->closedReason = 'done') + { + if(!isset($this->result[$row->project])) $this->result[$row->project] = 0; + $this->result[$row->project] += 1; + } + } + + public function getResult($options = array()) + { + $records = $this->getRecords(array('project', 'value')); + return $this->filterByOptions($records, $options); + } +} + + + diff --git a/module/metric/calc/project/scale/count_of_finished_story_in_project.php b/module/metric/calc/project/scale/count_of_finished_story_in_project.php index f1548b35f3..7afb9f298d 100755 --- a/module/metric/calc/project/scale/count_of_finished_story_in_project.php +++ b/module/metric/calc/project/scale/count_of_finished_story_in_project.php @@ -20,7 +20,7 @@ */ class count_of_finished_story_in_project extends baseCalc { - public $dataset = 'getDevStoriesWithProject'; + public $dataset = 'getAllStoriesWithProject'; public $fieldList = array('t3.project', 't1.closedReason'); diff --git a/module/metric/calc/project/scale/count_of_unclosed_epic_in_project.php b/module/metric/calc/project/scale/count_of_unclosed_epic_in_project.php new file mode 100644 index 0000000000..949ef43000 --- /dev/null +++ b/module/metric/calc/project/scale/count_of_unclosed_epic_in_project.php @@ -0,0 +1,46 @@ + + * @package + * @uses func + * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) + * @Link https://www.zentao.net + */ +class count_of_unclosed_epic_in_project extends baseCalc +{ + public $dataset = 'getEpicWithProject'; + + public $fieldList = array('t2.project', 't1.status'); + + public $result = array(); + + public function calculate($row) + { + if($row->status != 'closed') + { + if(!isset($this->result[$row->project])) $this->result[$row->project] = 0; + $this->result[$row->project] += 1; + } + } + + public function getResult($options = array()) + { + $records = $this->getRecords(array('project', 'value')); + return $this->filterByOptions($records, $options); + } +} + + + diff --git a/module/metric/calc/project/scale/count_of_unclosed_story_in_project.php b/module/metric/calc/project/scale/count_of_unclosed_story_in_project.php index d9ee879761..229f17df3a 100755 --- a/module/metric/calc/project/scale/count_of_unclosed_story_in_project.php +++ b/module/metric/calc/project/scale/count_of_unclosed_story_in_project.php @@ -20,7 +20,7 @@ */ class count_of_unclosed_story_in_project extends baseCalc { - public $dataset = 'getDevStoriesWithProject'; + public $dataset = 'getAllStoriesWithProject'; public $fieldList = array('t3.project', 't1.status'); diff --git a/module/metric/calc/project/scale/scale_of_story_in_project.php b/module/metric/calc/project/scale/scale_of_story_in_project.php index 3135f04177..6d52c5bd07 100755 --- a/module/metric/calc/project/scale/scale_of_story_in_project.php +++ b/module/metric/calc/project/scale/scale_of_story_in_project.php @@ -20,9 +20,9 @@ */ class scale_of_story_in_project extends baseCalc { - public $dataset = 'getDevStoriesWithProject'; + public $dataset = 'getAllStoriesWithProject'; - public $fieldList = array('t3.project', 't1.estimate', 't1.parent'); + public $fieldList = array('t3.project', 't1.estimate', 't1.isParent'); public $result = array(); @@ -30,9 +30,9 @@ class scale_of_story_in_project extends baseCalc { $project = $row->project; $estimate = $row->estimate; - $parent = $row->parent; + $isParent = $row->isParent; - if($parent == '-1') return false; + if($isParent == '1') return false; if(!isset($this->result[$project])) $this->result[$project] = 0; $this->result[$project] += $estimate; diff --git a/module/metric/calc/system/scale/count_of_daily_created_story.php b/module/metric/calc/system/scale/count_of_daily_created_story.php index 7f1270192b..5d69d9b323 100755 --- a/module/metric/calc/system/scale/count_of_daily_created_story.php +++ b/module/metric/calc/system/scale/count_of_daily_created_story.php @@ -1,15 +1,15 @@ @@ -20,7 +20,7 @@ */ class count_of_daily_created_story extends baseCalc { - public $dataset = 'getAllDevStories'; + public $dataset = 'getAllStories'; public $fieldList = array('t1.openedDate'); diff --git a/module/metric/calc/user/scale/count_of_assigned_epic_in_user.php b/module/metric/calc/user/scale/count_of_assigned_epic_in_user.php new file mode 100755 index 0000000000..d540d08568 --- /dev/null +++ b/module/metric/calc/user/scale/count_of_assigned_epic_in_user.php @@ -0,0 +1,44 @@ + + * @package + * @uses func + * @license ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html) + * @Link https://www.zentao.net + */ +class count_of_assigned_epic_in_user extends baseCalc +{ + public $dataset = 'getAllEpics'; + + public $fieldList = array('assignedTo'); + + public $result = array(); + + public function calculate($row) + { + $assignedTo = $row->assignedTo; + + if(empty($assignedTo) || $assignedTo == 'closed') return false; + + if(!isset($this->result[$assignedTo])) $this->result[$assignedTo] = 0; + $this->result[$assignedTo] += 1; + } + + public function getResult($options = array()) + { + $records = $this->getRecords(array('user', 'value')); + return $this->filterByOptions($records, $options); + } +} diff --git a/module/metric/config.php b/module/metric/config.php index c04720fce9..b15d718973 100644 --- a/module/metric/config.php +++ b/module/metric/config.php @@ -1,7 +1,7 @@ metric = new stdclass(); $config->metric->scopeList = array('program', 'project', 'product', 'execution', 'user', 'system'); -$config->metric->objectList = array('program', 'line', 'product', 'project', 'productplan', 'execution', 'release', 'story', 'requirement', 'task', 'bug', 'case', 'user', 'effort', 'doc', 'feedback', 'ticket', 'risk', 'issue', 'review', 'qa'); +$config->metric->objectList = array('program', 'line', 'product', 'project', 'productplan', 'execution', 'release', 'epic', 'story', 'requirement', 'task', 'bug', 'case', 'user', 'effort', 'doc', 'feedback', 'ticket', 'risk', 'issue', 'review', 'qa'); $config->metric->purposeList = array('scale', 'qc', 'hour', 'cost', 'rate', 'time'); $config->metric->dateList = array('year', 'month', 'week', 'day'); $config->metric->excludeGlobal = array('program', 'project', 'product', 'execution', 'user'); diff --git a/module/metric/dataset.php b/module/metric/dataset.php index 08622c43fe..1c47c425fa 100644 --- a/module/metric/dataset.php +++ b/module/metric/dataset.php @@ -318,6 +318,24 @@ class dataset return $this->defaultWhere($stmt, 't1'); } + /** + * 获取所有需求数据,不区分类型。 + * Get all story list. + * + * @param string $fieldList + * @access public + * @return PDOStatement + */ + public function getAllStories($fieldList) + { + $stmt = $this->dao->select($fieldList)->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') + ->where('t1.deleted')->eq(0) + ->andWhere('t2.deleted')->eq(0); + + return $this->defaultWhere($stmt, 't1'); + } + /** * 获取执行的研发需求数据。 * Get story list, with execution and type is story. @@ -346,6 +364,33 @@ class dataset return $this->defaultWhere($stmt, 't1'); } + /** + * 获取执行的所有需求数据。 + * Get story list, with execution story. + * + * @param string $fieldList + * @access public + * @return PDOStatement + */ + public function getAllStoriesWithExecution($fieldList) + { + $stmt = $this->dao->select($fieldList) + ->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') + ->leftJoin(TABLE_PROJECTSTORY)->alias('t3')->on('t1.id=t3.story') + ->leftJoin(TABLE_PROJECT)->alias('t4')->on('t3.project=t4.id') + ->leftJoin(TABLE_PROJECT)->alias('t5')->on('t4.project=t5.id') + ->where('t1.deleted')->eq(0) + ->andWhere('t2.deleted')->eq(0) + ->andWhere('t2.shadow')->eq(0) + ->andWhere('t4.deleted')->eq(0) // 已删除的执行 + ->andWhere('t4.type')->in('sprint,stage,kanban') + ->andWhere('t4.multiple')->eq('1') + ->andWhere('t5.deleted')->eq(0); // 已删除的项目 + + return $this->defaultWhere($stmt, 't1'); + } + /** * 获取项目的研发需求数据。 * Get story list, with project and type is story. @@ -355,6 +400,52 @@ class dataset * @return PDOStatement */ public function getDevStoriesWithProject($fieldList) + { + $stmt = $this->dao->select($fieldList)->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') + ->leftJoin(TABLE_PROJECTSTORY)->alias('t3')->on('t1.id=t3.story') + ->leftJoin(TABLE_PROJECT)->alias('t4')->on('t3.project=t4.id') + ->where('t1.deleted')->eq('0') + ->andWhere('t2.deleted')->eq('0') + ->andWhere('t1.type')->eq('story') + ->andWhere('t4.deleted')->eq('0') + ->andWhere('t1.isParent')->eq('0') + ->andWhere('t4.type')->eq('project'); + + return $this->defaultWhere($stmt, 't1'); + } + + /** + * 获取项目的业务需求数据。 + * Get epic list with project. + * + * @param string $fieldList + * @access public + * @return PDOStatement + */ + public function getEpicWithProject($fieldList) + { + $stmt = $this->dao->select($fieldList) + ->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story') + ->leftJoin(TABLE_PROJECT)->alias('t3')->on('t2.project=t3.id') + ->where('t1.deleted')->eq(0) + ->andWhere('t3.deleted')->eq(0) + ->andWhere('t1.type')->eq('epic') + ->andWhere('t3.type')->eq('project'); + + return $this->defaultWhere($stmt, 't1'); + } + + /** + * 获取项目的所有需求数据。 + * Get story list, with project story. + * + * @param string $fieldList + * @access public + * @return PDOStatement + */ + public function getAllStoriesWithProject($fieldList) { $stmt = $this->dao->select($fieldList) ->from(TABLE_STORY)->alias('t1') @@ -363,7 +454,6 @@ class dataset ->leftJoin(TABLE_PROJECT)->alias('t4')->on('t3.project=t4.id') ->where('t1.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) - ->andWhere('t1.type')->eq('story') ->andWhere('t4.deleted')->eq(0) ->andWhere('t4.type')->eq('project'); @@ -380,19 +470,17 @@ class dataset */ public function getDevStories($fieldList) { - $caseQuery = $this->dao->select('story, count(DISTINCT id) as case_count') - ->from(TABLE_CASE) - ->groupBy('story') - ->get(); + $caseQuery = $this->dao->select('story, COUNT(DISTINCT id) AS case_count')->from(TABLE_CASE)->groupBy('story')->get(); $stmt = $this->dao->select($fieldList) ->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') ->leftJoin("($caseQuery)")->alias('t3')->on('t1.id=t3.story') - ->where('t1.deleted')->eq(0) - ->andWhere('t2.deleted')->eq(0) + ->where('t1.deleted')->eq('0') + ->andWhere('t2.deleted')->eq('0') ->andWhere('t1.type')->eq('story') - ->andWhere('t2.shadow')->eq(0); + ->andWhere('t1.isParent')->eq('0') + ->andWhere('t2.shadow')->eq('0'); return $this->defaultWhere($stmt, 't1'); } @@ -407,12 +495,32 @@ class dataset */ public function getAllDevStories($fieldList) { - $stmt = $this->dao->select($fieldList) + $stmt = $this->dao->select($fieldList)->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') + ->where('t1.deleted')->eq('0') + ->andWhere('t2.deleted')->eq('0') + ->andWhere('t1.type')->eq('story') + ->andWhere('t1.isParent')->eq('0'); + + return $this->defaultWhere($stmt, 't1'); + } + + /** + * 获取所有业务需求数据,不过滤影子产品。 + * Get all epics, don't filter shadow product. + * + * @param string $fieldList + * @access public + * @return PDOStatement + */ + public function getAllEpics($fieldList) + { + $stmt = $this->dao->select($fieldList) ->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product=t2.id') ->where('t1.deleted')->eq(0) ->andWhere('t2.deleted')->eq(0) - ->andWhere('t1.type')->eq('story'); + ->andWhere('t1.type')->eq('epic'); return $this->defaultWhere($stmt, 't1'); } diff --git a/module/metric/lang/de.php b/module/metric/lang/de.php index ec3c733e07..5180a83244 100755 --- a/module/metric/lang/de.php +++ b/module/metric/lang/de.php @@ -257,6 +257,7 @@ $lang->metric->objectList['project'] = "Project"; $lang->metric->objectList['productplan'] = "Plan"; $lang->metric->objectList['execution'] = "Execution"; $lang->metric->objectList['release'] = "Release"; +$lang->metric->objectList['epic'] = $lang->ERCommon; $lang->metric->objectList['story'] = $lang->SRCommon; $lang->metric->objectList['requirement'] = $lang->URCommon; $lang->metric->objectList['task'] = "Task"; diff --git a/module/metric/lang/en.php b/module/metric/lang/en.php index ec3c733e07..5180a83244 100755 --- a/module/metric/lang/en.php +++ b/module/metric/lang/en.php @@ -257,6 +257,7 @@ $lang->metric->objectList['project'] = "Project"; $lang->metric->objectList['productplan'] = "Plan"; $lang->metric->objectList['execution'] = "Execution"; $lang->metric->objectList['release'] = "Release"; +$lang->metric->objectList['epic'] = $lang->ERCommon; $lang->metric->objectList['story'] = $lang->SRCommon; $lang->metric->objectList['requirement'] = $lang->URCommon; $lang->metric->objectList['task'] = "Task"; diff --git a/module/metric/lang/fr.php b/module/metric/lang/fr.php index ec3c733e07..5180a83244 100755 --- a/module/metric/lang/fr.php +++ b/module/metric/lang/fr.php @@ -257,6 +257,7 @@ $lang->metric->objectList['project'] = "Project"; $lang->metric->objectList['productplan'] = "Plan"; $lang->metric->objectList['execution'] = "Execution"; $lang->metric->objectList['release'] = "Release"; +$lang->metric->objectList['epic'] = $lang->ERCommon; $lang->metric->objectList['story'] = $lang->SRCommon; $lang->metric->objectList['requirement'] = $lang->URCommon; $lang->metric->objectList['task'] = "Task"; diff --git a/module/metric/lang/zh-cn.php b/module/metric/lang/zh-cn.php index 8f1eea17dd..58fe06ac19 100755 --- a/module/metric/lang/zh-cn.php +++ b/module/metric/lang/zh-cn.php @@ -257,6 +257,7 @@ $lang->metric->objectList['project'] = "项目"; $lang->metric->objectList['productplan'] = "计划"; $lang->metric->objectList['execution'] = "执行"; $lang->metric->objectList['release'] = "发布"; +$lang->metric->objectList['epic'] = $lang->ERCommon; $lang->metric->objectList['story'] = $lang->SRCommon; $lang->metric->objectList['requirement'] = $lang->URCommon; $lang->metric->objectList['task'] = "任务"; diff --git a/module/my/config/table.php b/module/my/config/table.php index 1ef7c89759..824323b269 100644 --- a/module/my/config/table.php +++ b/module/my/config/table.php @@ -327,37 +327,37 @@ $config->my->requirement->actionList = array(); $config->my->requirement->actionList['change']['icon'] = 'alter'; $config->my->requirement->actionList['change']['text'] = $lang->story->change; $config->my->requirement->actionList['change']['hint'] = $lang->story->change; -$config->my->requirement->actionList['change']['url'] = array('module' => 'story', 'method' => 'change', 'params' => 'storyID={id}&from=&storyType=requirement'); +$config->my->requirement->actionList['change']['url'] = array('module' => 'requirement', 'method' => 'change', 'params' => 'storyID={id}&from=&storyType=requirement'); $config->my->requirement->actionList['change']['data-toggle'] = 'modal'; $config->my->requirement->actionList['submitReview']['icon'] = 'confirm'; $config->my->requirement->actionList['submitReview']['text'] = $lang->story->submitReview; $config->my->requirement->actionList['submitReview']['hint'] = $lang->story->submitReview; -$config->my->requirement->actionList['submitReview']['url'] = array('module' => 'story', 'method' => 'submitReview', 'params' => 'storyID={id}&storyType=requirement'); +$config->my->requirement->actionList['submitReview']['url'] = array('module' => 'requirement', 'method' => 'submitReview', 'params' => 'storyID={id}&storyType=requirement'); $config->my->requirement->actionList['submitReview']['data-toggle'] = 'modal'; $config->my->requirement->actionList['review']['icon'] = 'search'; $config->my->requirement->actionList['review']['text'] = $lang->story->review; $config->my->requirement->actionList['review']['hint'] = $lang->story->review; -$config->my->requirement->actionList['review']['url'] = array('module' => 'story', 'method' => 'review', 'params' => 'storyID={id}&from=product&storyType=requirement'); +$config->my->requirement->actionList['review']['url'] = array('module' => 'requirement', 'method' => 'review', 'params' => 'storyID={id}&from=product&storyType=requirement'); $config->my->requirement->actionList['review']['data-toggle'] = 'modal'; $config->my->requirement->actionList['recall']['icon'] = 'undo'; $config->my->requirement->actionList['recall']['text'] = $lang->story->recall; $config->my->requirement->actionList['recall']['hint'] = $lang->story->recall; -$config->my->requirement->actionList['recall']['url'] = array('module' => 'story', 'method' => 'recall', 'params' => 'storyID={id}&from=list&confirm=no&storyType=requirement'); +$config->my->requirement->actionList['recall']['url'] = array('module' => 'requirement', 'method' => 'recall', 'params' => 'storyID={id}&from=list&confirm=no&storyType=requirement'); $config->my->requirement->actionList['recall']['className'] = 'ajax-submit'; $config->my->requirement->actionList['edit']['icon'] = 'edit'; $config->my->requirement->actionList['edit']['text'] = $lang->story->edit; $config->my->requirement->actionList['edit']['hint'] = $lang->story->edit; -$config->my->requirement->actionList['edit']['url'] = array('module' => 'story', 'method' => 'edit', 'params' => 'storyID={id}&from=default&storyType=requirement'); +$config->my->requirement->actionList['edit']['url'] = array('module' => 'requirement', 'method' => 'edit', 'params' => 'storyID={id}&from=default&storyType=requirement'); $config->my->requirement->actionList['edit']['data-toggle'] = 'modal'; $config->my->requirement->actionList['close']['icon'] = 'off'; $config->my->requirement->actionList['close']['text'] = $lang->story->close; $config->my->requirement->actionList['close']['hint'] = $lang->story->close; -$config->my->requirement->actionList['close']['url'] = array('module' => 'story', 'method' => 'close', 'params' => 'storyID={id}&from=&storyType=requirement'); +$config->my->requirement->actionList['close']['url'] = array('module' => 'requirement', 'method' => 'close', 'params' => 'storyID={id}&from=&storyType=requirement'); $config->my->requirement->actionList['close']['data-toggle'] = 'modal'; $config->my->requirement->dtable = new stdclass(); @@ -366,12 +366,13 @@ $config->my->requirement->dtable->fieldList['id']['title'] = $lang->idAB; $config->my->requirement->dtable->fieldList['id']['type'] = 'id'; $config->my->requirement->dtable->fieldList['id']['sortType'] = true; -$config->my->requirement->dtable->fieldList['title']['name'] = 'title'; -$config->my->requirement->dtable->fieldList['title']['title'] = common::checkNotCN() ? $lang->URCommon . ' ' . $lang->my->name : $lang->URCommon . $lang->my->name; -$config->my->requirement->dtable->fieldList['title']['type'] = 'title'; -$config->my->requirement->dtable->fieldList['title']['link'] = array('module' => 'story', 'method' => 'view', 'params' => 'id={id}&version=0¶m=0&storyType=requirement'); -$config->my->requirement->dtable->fieldList['title']['fixed'] = 'left'; -$config->my->requirement->dtable->fieldList['title']['sortType'] = true; +$config->my->requirement->dtable->fieldList['title']['name'] = 'title'; +$config->my->requirement->dtable->fieldList['title']['title'] = common::checkNotCN() ? $lang->URCommon . ' ' . $lang->my->name : $lang->URCommon . $lang->my->name; +$config->my->requirement->dtable->fieldList['title']['type'] = 'title'; +$config->my->requirement->dtable->fieldList['title']['link'] = array('module' => 'requirement', 'method' => 'view', 'params' => 'id={id}&version=0¶m=0&storyType=requirement'); +$config->my->requirement->dtable->fieldList['title']['fixed'] = 'left'; +$config->my->requirement->dtable->fieldList['title']['sortType'] = true; +$config->my->requirement->dtable->fieldList['title']['nestedToggle'] = true; $config->my->requirement->dtable->fieldList['pri']['name'] = 'pri'; $config->my->requirement->dtable->fieldList['pri']['title'] = $lang->priAB; @@ -411,6 +412,96 @@ $config->my->requirement->dtable->fieldList['actions']['sortType'] = false; $config->my->requirement->dtable->fieldList['actions']['list'] = $config->my->requirement->actionList; $config->my->requirement->dtable->fieldList['actions']['menu'] = array('change', 'review|submitReview', 'recall', 'edit', 'close'); +$config->my->epic = new stdclass(); +$config->my->epic->actionList = array(); +$config->my->epic->actionList['change']['icon'] = 'alter'; +$config->my->epic->actionList['change']['text'] = $lang->story->change; +$config->my->epic->actionList['change']['hint'] = $lang->story->change; +$config->my->epic->actionList['change']['url'] = array('module' => 'epic', 'method' => 'change', 'params' => 'storyID={id}&from=&storyType=epic'); +$config->my->epic->actionList['change']['data-toggle'] = 'modal'; + +$config->my->epic->actionList['submitReview']['icon'] = 'confirm'; +$config->my->epic->actionList['submitReview']['text'] = $lang->story->submitReview; +$config->my->epic->actionList['submitReview']['hint'] = $lang->story->submitReview; +$config->my->epic->actionList['submitReview']['url'] = array('module' => 'epic', 'method' => 'submitReview', 'params' => 'storyID={id}&storyType=epic'); +$config->my->epic->actionList['submitReview']['data-toggle'] = 'modal'; + +$config->my->epic->actionList['review']['icon'] = 'search'; +$config->my->epic->actionList['review']['text'] = $lang->story->review; +$config->my->epic->actionList['review']['hint'] = $lang->story->review; +$config->my->epic->actionList['review']['url'] = array('module' => 'epic', 'method' => 'review', 'params' => 'storyID={id}&from=product&storyType=epic'); +$config->my->epic->actionList['review']['data-toggle'] = 'modal'; + +$config->my->epic->actionList['recall']['icon'] = 'undo'; +$config->my->epic->actionList['recall']['text'] = $lang->story->recall; +$config->my->epic->actionList['recall']['hint'] = $lang->story->recall; +$config->my->epic->actionList['recall']['url'] = array('module' => 'epic', 'method' => 'recall', 'params' => 'storyID={id}&from=list&confirm=no&storyType=epic'); +$config->my->epic->actionList['recall']['className'] = 'ajax-submit'; + +$config->my->epic->actionList['edit']['icon'] = 'edit'; +$config->my->epic->actionList['edit']['text'] = $lang->story->edit; +$config->my->epic->actionList['edit']['hint'] = $lang->story->edit; +$config->my->epic->actionList['edit']['url'] = array('module' => 'epic', 'method' => 'edit', 'params' => 'storyID={id}&from=default&storyType=epic'); +$config->my->epic->actionList['edit']['data-toggle'] = 'modal'; + +$config->my->epic->actionList['close']['icon'] = 'off'; +$config->my->epic->actionList['close']['text'] = $lang->story->close; +$config->my->epic->actionList['close']['hint'] = $lang->story->close; +$config->my->epic->actionList['close']['url'] = array('module' => 'epic', 'method' => 'close', 'params' => 'storyID={id}&from=&storyType=epic'); +$config->my->epic->actionList['close']['data-toggle'] = 'modal'; + +$config->my->epic->dtable = new stdclass(); +$config->my->epic->dtable->fieldList['id']['name'] = 'id'; +$config->my->epic->dtable->fieldList['id']['title'] = $lang->idAB; +$config->my->epic->dtable->fieldList['id']['type'] = 'id'; +$config->my->epic->dtable->fieldList['id']['sortType'] = true; + +$config->my->epic->dtable->fieldList['title']['name'] = 'title'; +$config->my->epic->dtable->fieldList['title']['title'] = $lang->ERCommon . $lang->my->name; +$config->my->epic->dtable->fieldList['title']['type'] = 'title'; +$config->my->epic->dtable->fieldList['title']['link'] = array('module' => 'epic', 'method' => 'view', 'params' => 'id={id}&version=0¶m=0&storyType=epic'); +$config->my->epic->dtable->fieldList['title']['fixed'] = 'left'; +$config->my->epic->dtable->fieldList['title']['sortType'] = true; +$config->my->epic->dtable->fieldList['title']['nestedToggle'] = true; + +$config->my->epic->dtable->fieldList['pri']['name'] = 'pri'; +$config->my->epic->dtable->fieldList['pri']['title'] = $lang->priAB; +$config->my->epic->dtable->fieldList['pri']['type'] = 'pri'; +$config->my->epic->dtable->fieldList['pri']['group'] = 'pri'; +$config->my->epic->dtable->fieldList['pri']['sortType'] = true; + +$config->my->epic->dtable->fieldList['product']['name'] = 'productTitle'; +$config->my->epic->dtable->fieldList['product']['title'] = $lang->story->product; +$config->my->epic->dtable->fieldList['product']['type'] = 'text'; +$config->my->epic->dtable->fieldList['product']['group'] = 'pri'; +$config->my->epic->dtable->fieldList['product']['sortType'] = true; + +$config->my->epic->dtable->fieldList['status']['name'] = 'status'; +$config->my->epic->dtable->fieldList['status']['title'] = $lang->statusAB; +$config->my->epic->dtable->fieldList['status']['type'] = 'status'; +$config->my->epic->dtable->fieldList['status']['statusMap'] = $lang->story->statusList; +$config->my->epic->dtable->fieldList['status']['group'] = 'pri'; +$config->my->epic->dtable->fieldList['status']['sortType'] = true; + +$config->my->epic->dtable->fieldList['openedBy']['name'] = 'openedBy'; +$config->my->epic->dtable->fieldList['openedBy']['title'] = $lang->story->openedByAB; +$config->my->epic->dtable->fieldList['openedBy']['type'] = 'user'; +$config->my->epic->dtable->fieldList['openedBy']['group'] = 'openedBy'; +$config->my->epic->dtable->fieldList['openedBy']['sortType'] = true; + +$config->my->epic->dtable->fieldList['estimate']['name'] = 'estimate'; +$config->my->epic->dtable->fieldList['estimate']['title'] = $lang->story->estimateAB; +$config->my->epic->dtable->fieldList['estimate']['type'] = 'count'; +$config->my->epic->dtable->fieldList['estimate']['group'] = 'openedBy'; +$config->my->epic->dtable->fieldList['estimate']['sortType'] = true; + +$config->my->epic->dtable->fieldList['actions']['name'] = 'actions'; +$config->my->epic->dtable->fieldList['actions']['title'] = $lang->actions; +$config->my->epic->dtable->fieldList['actions']['type'] = 'actions'; +$config->my->epic->dtable->fieldList['actions']['sortType'] = false; +$config->my->epic->dtable->fieldList['actions']['list'] = $config->my->epic->actionList; +$config->my->epic->dtable->fieldList['actions']['menu'] = array('change', 'review|submitReview', 'recall', 'edit', 'close'); + $config->my->story = new stdclass(); $config->my->story->actionList = array(); $config->my->story->actionList['change']['icon'] = 'alter'; diff --git a/module/my/control.php b/module/my/control.php index 1e2f3c5a7b..3a3080f359 100755 --- a/module/my/control.php +++ b/module/my/control.php @@ -238,14 +238,84 @@ class my extends control $this->myZen->showWorkCount($recTotal, $recPerPage, $pageID); /* Assign. */ - $this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->story; - $this->view->stories = $stories; - $this->view->users = $this->user->getPairs('noletter'); - $this->view->type = $type; - $this->view->param = $param; - $this->view->mode = 'story'; - $this->view->pager = $pager; - $this->view->orderBy = $orderBy; + $this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->story; + $this->view->stories = $stories; + $this->view->users = $this->user->getPairs('noletter'); + $this->view->type = $type; + $this->view->param = $param; + $this->view->mode = 'story'; + $this->view->pager = $pager; + $this->view->orderBy = $orderBy; + $this->view->gradeGroup = $this->story->getGradeGroup(); + $this->view->showGrade = $this->config->edition == 'ipd'; + $this->display(); + } + + /** + * 业务需求列表。 + * My epics. + * + * @param string $type + * @param int $param + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID + * @access public + * @return void + */ + public function epic(string $type = 'assignedTo', int $param = 0, string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) + { + /* Save session. */ + if($this->app->viewType != 'json') $this->session->set('storyList', $this->app->getURI(true), 'my'); + + /* Load pager. */ + $this->app->loadClass('pager', true); + if($this->app->getViewType() == 'mhtml') $recPerPage = 10; + $pager = pager::init($recTotal, $recPerPage, $pageID); + + /* Append id for second sort. */ + $sort = common::appendOrder($orderBy); + if(strpos($sort, 'productTitle') !== false) $sort = str_replace('productTitle', 'product', $sort); + if(strpos($sort, 'pri_') !== false) $sort = str_replace('pri_', 'priOrder_', $sort); + $queryID = ($type == 'bysearch') ? (int)$param : 0; + + $this->loadModel('story'); + $this->loadModel('epic'); + if($type == 'assignedBy') + { + $stories = $this->my->getAssignedByMe($this->app->user->account, $pager, $sort, 'epic'); + } + elseif($type == 'bysearch') + { + $stories = $this->my->getEpicsBySearch($queryID, $this->app->rawMethod, $sort, $pager); + } + else + { + $stories = $this->story->getUserStories($this->app->user->account, $type, $sort, $pager, 'epic', false, 'all'); + } + if(!empty($stories)) $stories = $this->story->mergeReviewer($stories); + + foreach($stories as $story) $story->estimate = $story->estimate . $this->config->hourUnit; + + /* Build the search form. */ + $currentMethod = $this->app->rawMethod; + $actionURL = $this->createLink('my', $currentMethod, "mode=epic&type=bysearch¶m=myQueryID&orderBy={$orderBy}&recTotal={$recTotal}&recPerPage={$recPerPage}&pageID={$pageID}"); + $this->my->buildEpicSearchForm($queryID, $actionURL, $currentMethod); + + $this->myZen->showWorkCount($recTotal, $recPerPage, $pageID); + + /* Assign. */ + $this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->story; + $this->view->stories = $stories; + $this->view->users = $this->user->getPairs('noletter'); + $this->view->type = $type; + $this->view->param = $param; + $this->view->mode = 'epic'; + $this->view->pager = $pager; + $this->view->orderBy = $orderBy; + $this->view->gradeGroup = $this->story->getGradeGroup(); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->display(); } @@ -279,6 +349,7 @@ class my extends control $queryID = ($type == 'bysearch') ? (int)$param : 0; $this->loadModel('story'); + $this->loadModel('requirement'); if($type == 'assignedBy') { $stories = $this->my->getAssignedByMe($this->app->user->account, $pager, $sort, 'requirement'); @@ -303,14 +374,16 @@ class my extends control $this->myZen->showWorkCount($recTotal, $recPerPage, $pageID); /* Assign. */ - $this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->story; - $this->view->stories = $stories; - $this->view->users = $this->user->getPairs('noletter'); - $this->view->type = $type; - $this->view->param = $param; - $this->view->mode = 'requirement'; - $this->view->pager = $pager; - $this->view->orderBy = $orderBy; + $this->view->title = $this->lang->my->common . $this->lang->hyphen . $this->lang->my->story; + $this->view->stories = $stories; + $this->view->users = $this->user->getPairs('noletter'); + $this->view->type = $type; + $this->view->param = $param; + $this->view->mode = 'requirement'; + $this->view->pager = $pager; + $this->view->orderBy = $orderBy; + $this->view->gradeGroup = $this->story->getGradeGroup(); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->display(); } @@ -818,6 +891,8 @@ class my extends control */ public function audit(string $browseType = 'all', int $param = 0, string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { + $this->loadModel('epic'); + $this->loadModel('requirement'); $this->session->set('storyList', $this->app->getURI(true), 'my'); $this->app->loadClass('pager', true); diff --git a/module/my/js/audit.ui.js b/module/my/js/audit.ui.js index 44ccc6422d..2f9bb7702b 100644 --- a/module/my/js/audit.ui.js +++ b/module/my/js/audit.ui.js @@ -20,15 +20,17 @@ window.onRenderCell = function(result, {row, col}) } else { - result[0].props.items[0]['disabled'] = true; - delete result[0].props.items[0]['data-toggle']; - delete result[0].props.items[0]['url']; - delete result[0].props.items[0]['href']; + delete result[0].props.items[0]; } } - if(result && col.name == 'title' && row.data.module == 'review') + if(result && col.name == 'title') { - result[0].props['data-toggle'] = ''; + if(row.data.module == 'review') result[0].props['data-toggle'] = ''; + if(!viewPrivs[row.data.module]) + { + result[0].props['data-toggle'] = ''; + result[0].props['href'] = '#'; + } } return result; } diff --git a/module/my/js/epic.ui.js b/module/my/js/epic.ui.js new file mode 100644 index 0000000000..6274d8414e --- /dev/null +++ b/module/my/js/epic.ui.js @@ -0,0 +1,52 @@ +$(document).off('click', '.batch-btn').on('click', '.batch-btn', function() +{ + const dtable = zui.DTable.query($(this).target); + const checkedList = dtable.$.getChecks(); + if(!checkedList.length) return; + + const url = $(this).data('url'); + const form = new FormData(); + checkedList.forEach((id) => form.append('storyIdList[]', id)); + + if($(this).hasClass('ajax-btn')) + { + $.ajaxSubmit({url, data: form}); + } + else + { + postAndLoadPage(url, form); + } +}); + +/** + * 对标题列进行重定义。 + * Redefine the title column. + * + * @param array result + * @param array info + * @access public + * @return string|array + */ +window.renderCell = function(result, info) +{ + if(info.col.name == 'title' && result[0]) + { + const story = info.row.data; + if(story.shadow == 1) result[0].props.href += '#app=project'; + + let html = ''; + let gradeLabel = ''; + if(showGrade || story.grade >= 2) gradeLabel = gradeGroup[story.type][story.grade]?.name; + if(gradeLabel) html += "" + gradeLabel + " "; + if(story.color) result[0].props.style = 'color: ' + story.color; + if(html) result.unshift({html}); + } + return result; +} + +$(document).off('click', '.switchButton').on('click', '.switchButton', function() +{ + var storyViewType = $(this).attr('data-type'); + $.cookie.set('storyViewType', storyViewType, {expires:config.cookieLife, path:config.webRoot}); + loadCurrentPage(); +}); diff --git a/module/my/js/requirement.ui.js b/module/my/js/requirement.ui.js index 4d7a4b70a8..8419dbb23a 100644 --- a/module/my/js/requirement.ui.js +++ b/module/my/js/requirement.ui.js @@ -19,16 +19,29 @@ $(document).off('click', '.batch-btn').on('click', '.batch-btn', function() }); /** - * 根据状态变更操作按钮的语言项。 - * Change recall btn lang of object status. + * 对标题列进行重定义。 + * Redefine the title column. * - * @param object result - * @param object info + * @param array result + * @param array info * @access public - * @return object + * @return string|array */ -window.onRenderCell = function(result, {row, col}) +window.renderCell = function(result, {row, col}) { + if(col.name == 'title' && result[0]) + { + const story = row.data; + if(story.shadow == 1) result[0].props.href += '#app=project'; + + let html = ''; + let gradeLabel = ''; + if(showGrade || story.grade >= 2) gradeLabel = gradeGroup[story.type][story.grade]?.name; + if(gradeLabel) html += "" + gradeLabel + " "; + if(story.color) result[0].props.style = 'color: ' + story.color; + if(html) result.unshift({html}); + } + if(col.name == 'actions') { for(index in row.data.actions) @@ -38,3 +51,10 @@ window.onRenderCell = function(result, {row, col}) } return result; } + +$(document).off('click', '.switchButton').on('click', '.switchButton', function() +{ + var storyViewType = $(this).attr('data-type'); + $.cookie.set('storyViewType', storyViewType, {expires:config.cookieLife, path:config.webRoot}); + loadCurrentPage(); +}); diff --git a/module/my/js/story.ui.js b/module/my/js/story.ui.js index a2e32c15e3..7e4057db26 100644 --- a/module/my/js/story.ui.js +++ b/module/my/js/story.ui.js @@ -33,11 +33,13 @@ window.renderCell = function(result, info) { const story = info.row.data; if(story.shadow == 1) result[0].props.href += '#app=project'; - if(story.parent > 0) - { - let html = "" + childrenAB + ""; - result.unshift({html}); - } + + let html = ''; + let gradeLabel = ''; + if(showGrade || story.grade >= 2) gradeLabel = gradeGroup[story.type][story.grade]?.name; + if(gradeLabel) html += "" + gradeLabel + " "; + if(story.color) result[0].props.style = 'color: ' + story.color; + if(html) result.unshift({html}); } if(info.col.name == 'status' && result) { @@ -45,3 +47,10 @@ window.renderCell = function(result, info) } return result; } + +$(document).off('click', '.switchButton').on('click', '.switchButton', function() +{ + var storyViewType = $(this).attr('data-type'); + $.cookie.set('storyViewType', storyViewType, {expires:config.cookieLife, path:config.webRoot}); + loadCurrentPage(); +}); diff --git a/module/my/lang/de.php b/module/my/lang/de.php index b37b1558a1..fffa23b747 100644 --- a/module/my/lang/de.php +++ b/module/my/lang/de.php @@ -41,6 +41,7 @@ $lang->my->noData = 'No %s yet. '; $lang->my->storyChanged = "Story Changed"; $lang->my->hours = "Hours/day"; $lang->my->uploadAvatar = 'Upload Avatar'; +$lang->my->epic = "My {$lang->ERCommon}"; $lang->my->requirement = "My {$lang->URCommon}"; $lang->my->testtask = 'My Test Task'; $lang->my->testcase = 'My Case'; @@ -150,10 +151,12 @@ $lang->my->moreSelects['todo']['more']['thisYear'] = 'This Year'; $lang->my->moreSelects['todo']['more']['assignedToOther'] = 'Assigned To Other'; $lang->my->moreSelects['todo']['more']['cycle'] = 'Recurrence'; -$lang->my->featureBar['audit']['all'] = 'All'; -$lang->my->featureBar['audit']['demand'] = 'Demand'; -$lang->my->featureBar['audit']['story'] = 'Story'; -$lang->my->featureBar['audit']['testcase'] = 'Test case'; +$lang->my->featureBar['audit']['all'] = 'All'; +$lang->my->featureBar['audit']['demand'] = 'Demand'; +$lang->my->featureBar['audit']['story'] = $lang->SRCommon; +$lang->my->featureBar['audit']['requirement'] = $lang->URCommon; +$lang->my->featureBar['audit']['epic'] = $lang->ERCommon; +$lang->my->featureBar['audit']['testcase'] = 'Test case'; if(in_array($config->edition, array('max', 'ipd')) and (helper::hasFeature('waterfall') or helper::hasFeature('waterfallplus'))) $lang->my->featureBar['audit']['project'] = $lang->projectCommon; if($config->edition != 'open') $lang->my->featureBar['audit']['feedback'] = 'Feedback'; if($config->edition != 'open' and helper::hasFeature('OA')) $lang->my->featureBar['audit']['oa'] = 'OA'; @@ -179,6 +182,9 @@ $lang->my->featureBar['work']['task']['assignedTo'] = $lang->my->assignedToM $lang->my->featureBar['work']['testcase']['assigntome'] = $lang->my->assignedToMe; $lang->my->featureBar['work']['testtask']['assignedTo'] = 'Test task'; +$lang->my->featureBar['work']['epic'] = $lang->my->featureBar['work']['task']; +$lang->my->featureBar['work']['epic']['reviewBy'] = 'ReviewByMe'; + $lang->my->featureBar['work']['requirement'] = $lang->my->featureBar['work']['task']; $lang->my->featureBar['work']['requirement']['reviewBy'] = 'ReviewByMe'; @@ -191,6 +197,11 @@ $lang->my->featureBar['contribute']['task']['closedBy'] = 'ClosedByMe'; $lang->my->featureBar['contribute']['task']['canceledBy'] = 'CancelledByMe'; $lang->my->featureBar['contribute']['task']['assignedBy'] = 'AssignedByMe'; +$lang->my->featureBar['contribute']['epic']['openedBy'] = 'CreatedByMe'; +$lang->my->featureBar['contribute']['epic']['reviewedBy'] = 'ReviewedByMe'; +$lang->my->featureBar['contribute']['epic']['closedBy'] = 'ClosedByMe'; +$lang->my->featureBar['contribute']['epic']['assignedBy'] = 'AssignedByMe'; + $lang->my->featureBar['contribute']['requirement']['openedBy'] = 'CreatedByMe'; $lang->my->featureBar['contribute']['requirement']['reviewedBy'] = 'ReviewedByMe'; $lang->my->featureBar['contribute']['requirement']['closedBy'] = 'ClosedByMe'; diff --git a/module/my/lang/en.php b/module/my/lang/en.php index b37b1558a1..fffa23b747 100644 --- a/module/my/lang/en.php +++ b/module/my/lang/en.php @@ -41,6 +41,7 @@ $lang->my->noData = 'No %s yet. '; $lang->my->storyChanged = "Story Changed"; $lang->my->hours = "Hours/day"; $lang->my->uploadAvatar = 'Upload Avatar'; +$lang->my->epic = "My {$lang->ERCommon}"; $lang->my->requirement = "My {$lang->URCommon}"; $lang->my->testtask = 'My Test Task'; $lang->my->testcase = 'My Case'; @@ -150,10 +151,12 @@ $lang->my->moreSelects['todo']['more']['thisYear'] = 'This Year'; $lang->my->moreSelects['todo']['more']['assignedToOther'] = 'Assigned To Other'; $lang->my->moreSelects['todo']['more']['cycle'] = 'Recurrence'; -$lang->my->featureBar['audit']['all'] = 'All'; -$lang->my->featureBar['audit']['demand'] = 'Demand'; -$lang->my->featureBar['audit']['story'] = 'Story'; -$lang->my->featureBar['audit']['testcase'] = 'Test case'; +$lang->my->featureBar['audit']['all'] = 'All'; +$lang->my->featureBar['audit']['demand'] = 'Demand'; +$lang->my->featureBar['audit']['story'] = $lang->SRCommon; +$lang->my->featureBar['audit']['requirement'] = $lang->URCommon; +$lang->my->featureBar['audit']['epic'] = $lang->ERCommon; +$lang->my->featureBar['audit']['testcase'] = 'Test case'; if(in_array($config->edition, array('max', 'ipd')) and (helper::hasFeature('waterfall') or helper::hasFeature('waterfallplus'))) $lang->my->featureBar['audit']['project'] = $lang->projectCommon; if($config->edition != 'open') $lang->my->featureBar['audit']['feedback'] = 'Feedback'; if($config->edition != 'open' and helper::hasFeature('OA')) $lang->my->featureBar['audit']['oa'] = 'OA'; @@ -179,6 +182,9 @@ $lang->my->featureBar['work']['task']['assignedTo'] = $lang->my->assignedToM $lang->my->featureBar['work']['testcase']['assigntome'] = $lang->my->assignedToMe; $lang->my->featureBar['work']['testtask']['assignedTo'] = 'Test task'; +$lang->my->featureBar['work']['epic'] = $lang->my->featureBar['work']['task']; +$lang->my->featureBar['work']['epic']['reviewBy'] = 'ReviewByMe'; + $lang->my->featureBar['work']['requirement'] = $lang->my->featureBar['work']['task']; $lang->my->featureBar['work']['requirement']['reviewBy'] = 'ReviewByMe'; @@ -191,6 +197,11 @@ $lang->my->featureBar['contribute']['task']['closedBy'] = 'ClosedByMe'; $lang->my->featureBar['contribute']['task']['canceledBy'] = 'CancelledByMe'; $lang->my->featureBar['contribute']['task']['assignedBy'] = 'AssignedByMe'; +$lang->my->featureBar['contribute']['epic']['openedBy'] = 'CreatedByMe'; +$lang->my->featureBar['contribute']['epic']['reviewedBy'] = 'ReviewedByMe'; +$lang->my->featureBar['contribute']['epic']['closedBy'] = 'ClosedByMe'; +$lang->my->featureBar['contribute']['epic']['assignedBy'] = 'AssignedByMe'; + $lang->my->featureBar['contribute']['requirement']['openedBy'] = 'CreatedByMe'; $lang->my->featureBar['contribute']['requirement']['reviewedBy'] = 'ReviewedByMe'; $lang->my->featureBar['contribute']['requirement']['closedBy'] = 'ClosedByMe'; diff --git a/module/my/lang/fr.php b/module/my/lang/fr.php index b37b1558a1..fffa23b747 100644 --- a/module/my/lang/fr.php +++ b/module/my/lang/fr.php @@ -41,6 +41,7 @@ $lang->my->noData = 'No %s yet. '; $lang->my->storyChanged = "Story Changed"; $lang->my->hours = "Hours/day"; $lang->my->uploadAvatar = 'Upload Avatar'; +$lang->my->epic = "My {$lang->ERCommon}"; $lang->my->requirement = "My {$lang->URCommon}"; $lang->my->testtask = 'My Test Task'; $lang->my->testcase = 'My Case'; @@ -150,10 +151,12 @@ $lang->my->moreSelects['todo']['more']['thisYear'] = 'This Year'; $lang->my->moreSelects['todo']['more']['assignedToOther'] = 'Assigned To Other'; $lang->my->moreSelects['todo']['more']['cycle'] = 'Recurrence'; -$lang->my->featureBar['audit']['all'] = 'All'; -$lang->my->featureBar['audit']['demand'] = 'Demand'; -$lang->my->featureBar['audit']['story'] = 'Story'; -$lang->my->featureBar['audit']['testcase'] = 'Test case'; +$lang->my->featureBar['audit']['all'] = 'All'; +$lang->my->featureBar['audit']['demand'] = 'Demand'; +$lang->my->featureBar['audit']['story'] = $lang->SRCommon; +$lang->my->featureBar['audit']['requirement'] = $lang->URCommon; +$lang->my->featureBar['audit']['epic'] = $lang->ERCommon; +$lang->my->featureBar['audit']['testcase'] = 'Test case'; if(in_array($config->edition, array('max', 'ipd')) and (helper::hasFeature('waterfall') or helper::hasFeature('waterfallplus'))) $lang->my->featureBar['audit']['project'] = $lang->projectCommon; if($config->edition != 'open') $lang->my->featureBar['audit']['feedback'] = 'Feedback'; if($config->edition != 'open' and helper::hasFeature('OA')) $lang->my->featureBar['audit']['oa'] = 'OA'; @@ -179,6 +182,9 @@ $lang->my->featureBar['work']['task']['assignedTo'] = $lang->my->assignedToM $lang->my->featureBar['work']['testcase']['assigntome'] = $lang->my->assignedToMe; $lang->my->featureBar['work']['testtask']['assignedTo'] = 'Test task'; +$lang->my->featureBar['work']['epic'] = $lang->my->featureBar['work']['task']; +$lang->my->featureBar['work']['epic']['reviewBy'] = 'ReviewByMe'; + $lang->my->featureBar['work']['requirement'] = $lang->my->featureBar['work']['task']; $lang->my->featureBar['work']['requirement']['reviewBy'] = 'ReviewByMe'; @@ -191,6 +197,11 @@ $lang->my->featureBar['contribute']['task']['closedBy'] = 'ClosedByMe'; $lang->my->featureBar['contribute']['task']['canceledBy'] = 'CancelledByMe'; $lang->my->featureBar['contribute']['task']['assignedBy'] = 'AssignedByMe'; +$lang->my->featureBar['contribute']['epic']['openedBy'] = 'CreatedByMe'; +$lang->my->featureBar['contribute']['epic']['reviewedBy'] = 'ReviewedByMe'; +$lang->my->featureBar['contribute']['epic']['closedBy'] = 'ClosedByMe'; +$lang->my->featureBar['contribute']['epic']['assignedBy'] = 'AssignedByMe'; + $lang->my->featureBar['contribute']['requirement']['openedBy'] = 'CreatedByMe'; $lang->my->featureBar['contribute']['requirement']['reviewedBy'] = 'ReviewedByMe'; $lang->my->featureBar['contribute']['requirement']['closedBy'] = 'ClosedByMe'; diff --git a/module/my/lang/zh-cn.php b/module/my/lang/zh-cn.php index b4420909f9..98382a2e8f 100644 --- a/module/my/lang/zh-cn.php +++ b/module/my/lang/zh-cn.php @@ -41,6 +41,7 @@ $lang->my->noData = "暂时没有%s。"; $lang->my->storyChanged = "需求变更"; $lang->my->hours = '工时/天'; $lang->my->uploadAvatar = '更换头像'; +$lang->my->epic = "我的{$lang->ERCommon}"; $lang->my->requirement = "我的{$lang->URCommon}"; $lang->my->testtask = '我的测试单'; $lang->my->testcase = '我的用例'; @@ -150,10 +151,13 @@ $lang->my->moreSelects['todo']['more']['thisYear'] = '本年'; $lang->my->moreSelects['todo']['more']['assignedToOther'] = '指派他人'; $lang->my->moreSelects['todo']['more']['cycle'] = '周期'; -$lang->my->featureBar['audit']['all'] = '全部'; -$lang->my->featureBar['audit']['demand'] = '需求池需求'; -$lang->my->featureBar['audit']['story'] = '需求'; -$lang->my->featureBar['audit']['testcase'] = '用例'; +$lang->my->featureBar['audit']['all'] = '全部'; +$lang->my->featureBar['audit']['demand'] = '需求池需求'; +$lang->my->featureBar['audit']['story'] = $lang->SRCommon; +$lang->my->featureBar['audit']['requirement'] = $lang->URCommon; +$lang->my->featureBar['audit']['epic'] = $lang->ERCommon; +$lang->my->featureBar['audit']['story'] = '需求'; +$lang->my->featureBar['audit']['testcase'] = '用例'; if(in_array($config->edition, array('max', 'ipd')) and (helper::hasFeature('waterfall') or helper::hasFeature('waterfallplus'))) $lang->my->featureBar['audit']['project'] = $lang->projectCommon; if($config->edition != 'open') $lang->my->featureBar['audit']['feedback'] = '反馈'; if($config->edition != 'open' and helper::hasFeature('OA')) $lang->my->featureBar['audit']['oa'] = '办公'; @@ -179,6 +183,9 @@ $lang->my->featureBar['work']['task']['assignedTo'] = $lang->my->assignedToM $lang->my->featureBar['work']['testcase']['assigntome'] = $lang->my->assignedToMe; $lang->my->featureBar['work']['testtask']['assignedTo'] = '由我负责'; +$lang->my->featureBar['work']['epic'] = $lang->my->featureBar['work']['task']; +$lang->my->featureBar['work']['epic']['reviewBy'] = '待我评审'; + $lang->my->featureBar['work']['requirement'] = $lang->my->featureBar['work']['task']; $lang->my->featureBar['work']['requirement']['reviewBy'] = '待我评审'; @@ -191,6 +198,11 @@ $lang->my->featureBar['contribute']['task']['closedBy'] = '由我关闭'; $lang->my->featureBar['contribute']['task']['canceledBy'] = '由我取消'; $lang->my->featureBar['contribute']['task']['assignedBy'] = '由我指派'; +$lang->my->featureBar['contribute']['epic']['openedBy'] = '由我创建'; +$lang->my->featureBar['contribute']['epic']['reviewedBy'] = '我评审过'; +$lang->my->featureBar['contribute']['epic']['closedBy'] = '由我关闭'; +$lang->my->featureBar['contribute']['epic']['assignedBy'] = '由我指派'; + $lang->my->featureBar['contribute']['requirement']['openedBy'] = '由我创建'; $lang->my->featureBar['contribute']['requirement']['reviewedBy'] = '我评审过'; $lang->my->featureBar['contribute']['requirement']['closedBy'] = '由我关闭'; diff --git a/module/my/model.php b/module/my/model.php index c4f217f6c5..6307cc1daa 100644 --- a/module/my/model.php +++ b/module/my/model.php @@ -710,6 +710,44 @@ class myModel extends model return $this->myTao->fetchStoriesBySearch($myStoryQuery, $type, $orderBy, $pager, $type == 'contribute' ? $this->getAssignedByMe($this->app->user->account, null, $orderBy, 'story') : array()); } + /** + * 构建业务需求搜索表单。 + * Build epic search form. + * + * @param int $queryID + * @param string $actionURL + * @access public + * @return void + */ + public function buildEpicSearchForm(int $queryID, string $actionURL): void + { + $products = $this->dao->select('id,name')->from(TABLE_PRODUCT) + ->where('deleted')->eq(0) + ->beginIF(!$this->app->user->admin)->andWhere('id')->in($this->app->user->view->products)->fi() + ->orderBy('order_asc') + ->fetchPairs(); + + $productIdList = array_keys($products); + $queryName = $this->app->rawMethod . 'Epic'; + $this->app->loadConfig('product'); + $this->config->product->search['module'] = $queryName; + $this->config->product->search['queryID'] = $queryID; + $this->config->product->search['actionURL'] = $actionURL; + $this->config->product->search['params']['product']['values'] = $products; + $this->config->product->search['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList); + + $this->lang->story->title = str_replace($this->lang->SRCommon, $this->lang->ERCommon, $this->lang->story->title); + $this->lang->story->create = str_replace($this->lang->SRCommon, $this->lang->ERCommon, $this->lang->story->create); + $this->config->product->search['fields']['title'] = $this->lang->story->title; + + unset($this->config->product->search['fields']['plan']); + unset($this->config->product->search['fields']['stage']); + unset($this->config->product->search['fields']['module']); + unset($this->config->product->search['fields']['branch']); + + $this->loadModel('search')->setSearchParams($this->config->product->search); + } + /** * 构建用户需求搜索表单。 * Build Requirement search form. @@ -777,6 +815,48 @@ class myModel extends model return true; } + /** + * 通过搜索获取业务需求。 + * Get epics by search. + * + * @param int $queryID + * @param string $type + * @param string $orderBy + * @param object $pager + * @access public + * @return array + */ + public function getEpicsBySearch(int $queryID, string $type, string $orderBy, object $pager = null): array + { + $queryName = $type == 'contribute' ? 'contributeEpicQuery' : 'workEpicQuery'; + $queryForm = $type == 'contribute' ? 'contributeEpicForm' : 'workEpicForm'; + if($queryID) + { + $query = $this->loadModel('search')->getQuery($queryID); + if($query) + { + $this->session->set($queryName, $query->sql); + $this->session->set($queryForm, $query->form); + } + else + { + $this->session->set($queryName, ' 1 = 1'); + } + } + else + { + if($this->session->{$queryName} == false) $this->session->set($queryName, ' 1 = 1'); + } + + $myEpicQuery = $this->session->{$queryName}; + $myEpicQuery = preg_replace('/`(\w+)`/', 't1.`$1`', $myEpicQuery); + + $epicsAssignedByMe = $type == 'contribute' ? $this->getAssignedByMe($this->app->user->account, null, $orderBy, 'epic') : array(); + $epicIdList = array_keys($epicsAssignedByMe); + + return $this->myTao->fetchEpicsBySearch($myEpicQuery, $type, $orderBy, $pager, $epicIdList, 'epic'); + } + /** * 通过搜索获取用户需求。 * Get requirements by search. @@ -830,7 +910,9 @@ class myModel extends model { $typeList = array(); if($this->config->edition == 'ipd' && $this->getReviewingDemands('id_desc', true)) $typeList[] = 'demand'; - if($this->getReviewingStories('id_desc', true)) $typeList[] = 'story'; + if($this->getReviewingStories('id_desc', true, 'story')) $typeList[] = 'story'; + if($this->getReviewingStories('id_desc', true, 'epic')) $typeList[] = 'epic'; + if($this->getReviewingStories('id_desc', true, 'requirement')) $typeList[] = 'requirement'; if($this->config->vision != 'or' && $this->getReviewingCases('id_desc', true)) $typeList[] = 'testcase'; if($this->config->vision != 'or' && $this->getReviewingApprovals('id_desc', true)) $typeList[] = 'project'; if($this->getReviewingFeedbacks('id_desc', true)) $typeList[] = 'feedback'; @@ -865,6 +947,8 @@ class myModel extends model $reviewList = array(); if($browseType == 'all' || $browseType == 'demand') $reviewList = array_merge($reviewList, $this->getReviewingDemands()); if($browseType == 'all' || $browseType == 'story') $reviewList = array_merge($reviewList, $this->getReviewingStories()); + if($browseType == 'all' || $browseType == 'epic') $reviewList = array_merge($reviewList, $this->getReviewingStories('id_desc', false, 'epic')); + if($browseType == 'all' || $browseType == 'requirement') $reviewList = array_merge($reviewList, $this->getReviewingStories('id_desc', false, 'requirement')); if($vision != 'or' && ($browseType == 'all' || $browseType == 'testcase')) $reviewList = array_merge($reviewList, $this->getReviewingCases()); if($vision != 'or' && ($browseType == 'all' || $browseType == 'project')) $reviewList = array_merge($reviewList, $this->getReviewingApprovals()); if($browseType == 'all' || $browseType == 'feedback') $reviewList = array_merge($reviewList, $this->getReviewingFeedbacks()); @@ -954,7 +1038,7 @@ class myModel extends model * @access public * @return array|bool */ - public function getReviewingStories(string $orderBy = 'id_desc', bool $checkExists = false): array|bool + public function getReviewingStories(string $orderBy = 'id_desc', bool $checkExists = false, $type = 'story'): array|bool { if(!common::hasPriv('story', 'review')) return array(); @@ -965,6 +1049,7 @@ class myModel extends model ->beginIF(!$this->app->user->admin)->andWhere('t1.product')->in($this->app->user->view->products)->fi() ->andWhere('t2.reviewer')->eq($this->app->user->account) ->andWhere('t2.result')->eq('') + ->andWhere('t1.type')->eq($type) ->andWhere('t1.vision')->eq($this->config->vision) ->andWhere('t1.status')->eq('reviewing') ->orderBy($orderBy) diff --git a/module/my/tao.php b/module/my/tao.php index 06a95070f9..edcfc775b3 100644 --- a/module/my/tao.php +++ b/module/my/tao.php @@ -170,6 +170,61 @@ class myTao extends myModel return $stories; } + /** + * 通过搜索查找业务需求。 + * Fetch epics by search. + * + * @param string $myEpicQuery + * @param string $type + * @param string $orderBy + * @param object $pager + * @param array $epicIDList + * @access protected + * @return array + */ + protected function fetchEpicsBySearch(string $myEpicQuery, string $type, string $orderBy, object $pager = null, array $epicIDList = array()): array + { + if($type == 'contribute') + { + $epics = $this->dao->select("distinct t1.*, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder, t2.name as productTitle")->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') + ->leftJoin(TABLE_STORYREVIEW)->alias('t3')->on('t1.id = t3.story') + ->where($myEpicQuery) + ->andWhere('t1.type')->eq('epic') + ->andWhere('t1.openedBy',1)->eq($this->app->user->account) + ->orWhere('t1.closedBy')->eq($this->app->user->account) + ->orWhere('(t3.reviewer')->eq($this->app->user->account) + ->andWhere('t3.result')->eq('') + ->andWhere('t1.status')->eq('reviewing') + ->markRight(1) + ->orWhere('t1.id')->in($epicIDList) + ->markRight(1) + ->andWhere('t1.deleted')->eq(0) + ->orderBy($orderBy) + ->page($pager, 't1.id') + ->fetchAll('id'); + } + else + { + $epics = $this->dao->select("distinct t1.*, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder, t2.name as productTitle")->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') + ->leftJoin(TABLE_STORYREVIEW)->alias('t3')->on('t1.id = t3.story') + ->where($myEpicQuery) + ->andWhere('t1.type')->eq('epic') + ->andWhere('t1.assignedTo',1)->eq($this->app->user->account) + ->orWhere('(t3.reviewer')->eq($this->app->user->account) + ->andWhere('t3.result')->eq('') + ->andWhere('t1.status')->eq('reviewing') + ->markRight(1) + ->markRight(1) + ->andWhere('t1.deleted')->eq(0) + ->orderBy($orderBy) + ->page($pager, 't1.id') + ->fetchAll('id'); + } + return $epics; + } + /** * 通过搜索查找用户需求。 * Fetch requirements by search. diff --git a/module/my/test/lib/my.unittest.class.php b/module/my/test/lib/my.unittest.class.php index 307ae06ee0..6ffea2872d 100644 --- a/module/my/test/lib/my.unittest.class.php +++ b/module/my/test/lib/my.unittest.class.php @@ -164,6 +164,25 @@ class myTest return implode(',', array_keys($objects)); } + /** + * 测试通过搜索获取业务需求。 + * Test get requirements by search. + * + * @param int $queryID + * @param string $type + * @param string $orderBy + * @access public + * @return string|array + */ + public function getEpicsBySearchTest(int $queryID, string $type, string $orderBy): string|array + { + $objects = $this->objectModel->getEpicsBySearch($queryID, $type, $orderBy); + + if(dao::isError()) return dao::getError(); + + return implode(',', array_keys($objects)); + } + /** * 测试通过搜索获取用户需求。 * Test get requirements by search. diff --git a/module/my/test/model/getepicsbysearch.php b/module/my/test/model/getepicsbysearch.php new file mode 100755 index 0000000000..517174d8de --- /dev/null +++ b/module/my/test/model/getepicsbysearch.php @@ -0,0 +1,37 @@ +#!/usr/bin/env php +loadYaml('epic')->gen('20'); +zenData('storyreview')->gen('0'); +zenData('product')->gen('10'); +zenData('productplan')->gen('15'); +zenData('planstory')->gen('20'); +zenData('userquery')->loadYaml('userquery')->gen('2'); +zenData('user')->gen('1'); +zenData('action')->gen('0'); + +su('admin'); + +/** + +title=测试 myModel->getEpicsBySearch(); +cid=1 +pid=1 + +*/ + +$_SESSION['contributeEpicQuery'] = ''; +$_SESSION['workEpicQuery'] = ''; + +$my = new myTest(); +$queryID = array(0, 2); +$typeList = array('contribute', 'other'); +$orderBy = array('id_desc', 'id_asc'); +r($my->getEpicsBySearchTest($queryID[0], $typeList[1], $orderBy[0])) && p() && e('15,13,11'); // 测试获取 queryID 0 类型 other 排序 id_desc 的需求。 +r($my->getEpicsBySearchTest($queryID[0], $typeList[1], $orderBy[1])) && p() && e('11,13,15'); // 测试获取 queryID 0 类型 other 排序 id_asc 的需求。 +r($my->getEpicsBySearchTest($queryID[1], $typeList[0], $orderBy[0])) && p() && e('13'); // 测试获取 queryID 1 类型 contribute 排序 id_desc 的需求。 +r($my->getEpicsBySearchTest($queryID[1], $typeList[0], $orderBy[1])) && p() && e('13'); // 测试获取 queryID 1 类型 contribute 排序 id_asc 的需求。 +r($my->getEpicsBySearchTest($queryID[1], $typeList[1], $orderBy[0])) && p() && e('15,13,11'); // 测试获取 queryID 1 类型 other 排序 id_desc 的需求。 +r($my->getEpicsBySearchTest($queryID[1], $typeList[1], $orderBy[1])) && p() && e('11,13,15'); // 测试获取 queryID 1 类型 other 排序 id_asc 的需求。 diff --git a/module/my/test/yaml/epic.yaml b/module/my/test/yaml/epic.yaml new file mode 100644 index 0000000000..c549523317 --- /dev/null +++ b/module/my/test/yaml/epic.yaml @@ -0,0 +1,9 @@ +title: zt_story +desc: epic data. +author: Sun Guangming +version: "1.0" +fields: + - field: assignedTo + range: admin,user1 + - field: type + range: story{5},requirement{5},epic{5} diff --git a/module/my/ui/audit.html.php b/module/my/ui/audit.html.php index 9fa551924d..1abf8545a9 100644 --- a/module/my/ui/audit.html.php +++ b/module/my/ui/audit.html.php @@ -18,7 +18,6 @@ foreach(array_keys($lang->my->featureBar['audit']) as $type) $reviewPrivs[$type] = $priv; } -jsVar('viewLink', createLink('{module}', 'view', 'id={id}')); jsVar('reviewLink', createLink('{module}', 'review', 'id={id}')); jsVar('flowReviewLink', createLink('{module}', 'approvalreview', 'id={id}')); jsVar('reviewPrivs', $reviewPrivs); @@ -50,9 +49,20 @@ foreach($reviewList as $review) $review->module = $type; if(isset($lang->{$review->type}->common)) $typeName = $lang->{$review->type}->common; - if($type == 'story') $typeName = $review->storyType == 'story' ? $lang->SRCommon : $lang->URCommon; if($review->type == 'projectreview') $typeName = $lang->project->common; if(isset($flows[$review->type])) $typeName = $flows[$review->type]; + if($type == 'story') + { + $typeName = $lang->SRCommon; + if($review->storyType == 'epic') + { + $typeName = $lang->ERCommon; + } + elseif($review->storyType == 'requirement') + { + $typeName = $lang->ERCommon; + } + } $statusList = array(); if(isset($lang->$type->statusList)) $statusList = $lang->$type->statusList; @@ -80,6 +90,8 @@ foreach($reviewList as $review) $review->result = zget($reviewResultList, $review->result); } + + $review->module = isset($review->storyType) ? $review->storyType : $review->module; } $reviewList = initTableData($reviewList, $config->my->audit->dtable->fieldList, $this->my); diff --git a/module/my/ui/epic.html.php b/module/my/ui/epic.html.php new file mode 100644 index 0000000000..65150a2302 --- /dev/null +++ b/module/my/ui/epic.html.php @@ -0,0 +1,117 @@ + + * @package my + * @link https://www.zentao.net + */ +namespace zin; + +include 'header.html.php'; +jsVar('window.globalSearchType', 'epic'); +jsVar('showGrade', $showGrade); +jsVar('gradeGroup', $gradeGroup); + +featureBar +( + set::current($type), + set::linkParams("mode=epic&type={key}¶m={$param}"), + li(searchToggle(set::module($this->app->rawMethod . 'Epic'), set::open($type == 'bysearch'))) +); + +$viewType = $this->cookie->storyViewType ? $this->cookie->storyViewType : 'tree'; +toolbar +( + item(set(array + ( + 'type' => 'btnGroup', + 'items' => array(array + ( + 'icon' => 'list', + 'class' => 'btn-icon switchButton' . ($viewType == 'tiled' ? ' text-primary' : ''), + 'data-type' => 'tiled', + 'hint' => $lang->story->viewTypeList['tiled'] + ), array + ( + 'icon' => 'treeview', + 'class' => 'switchButton btn-icon' . ($viewType == 'tree' ? ' text-primary' : ''), + 'data-type' => 'tree', + 'hint' => $lang->story->viewTypeList['tree'] + )) + ))) +); + +$canBatchEdit = common::hasPriv('epic', 'batchEdit'); +$canBatchReview = common::hasPriv('epic', 'batchReview'); +$canBatchAssignTo = common::hasPriv('epic', 'batchAssignTo'); +$canBatchClose = common::hasPriv('epic', 'batchClose'); +$canBatchAction = $canBatchEdit || $canBatchReview || $canBatchAssignTo || $canBatchClose; + +$reviewItems = array(); +if($canBatchReview) +{ + $rejectItems = array(); + foreach($lang->story->reasonList as $key => $reason) + { + if(!$key || $key == 'subdivided' || $key == 'duplicate') continue; + $rejectItems[] = array('text' => $reason, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('epic', 'batchReview', "result=reject&reason={$key}&storyType=epic")); + } + + foreach($lang->story->reviewResultList as $key => $result) + { + if(!$key || $key == 'revert') continue; + if($key == 'reject') + { + $reviewItems[] = array('text' => $result, 'class' => 'not-hide-menu', 'items' => $rejectItems); + } + else + { + $reviewItems[] = array('text' => $result, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('epic', 'batchReview', "result={$key}&reason=&storyType=epic")); + } + } +} + +$assignedToItems = array(); +if($canBatchAssignTo) +{ + foreach($users as $key => $value) + { + if(empty($key) || $key == 'closed') continue; + $assignedToItems[] = array('text' => $value, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('epic', 'batchAssignTo', "storyType=epic&assignedTo={$key}")); + } +} + +$footToolbar = array('items' => array +( + $canBatchEdit ? array('text' => $lang->edit, 'className' => 'batch-btn', 'data-url' => helper::createLink('epic', 'batchEdit', "productID=0&executionID=0&branch=0&storyType=epic&from={$app->rawMethod}")) : null, + $canBatchReview ? array('caret' => 'up', 'text' => $lang->story->review, 'type' => 'dropdown', 'items' => $reviewItems, 'data-placement' => 'top-start') : null, + $canBatchAssignTo ? array('caret' => 'up', 'text' => $lang->story->assignedTo, 'type' => 'dropdown', 'items' => $assignedToItems, 'data-placement' => 'top-start') : null, + $canBatchClose ? array('text' => $lang->story->close, 'className' => 'batch-btn', 'data-url' => helper::createLink('epic', 'batchClose', "productID=0&executionID=0&storyType=epic&from={$app->rawMethod}")) : null +), 'btnProps' => array('size' => 'sm', 'btnType' => 'secondary')); + +if($canBatchAction) $config->my->epic->dtable->fieldList['id']['type'] = 'checkID'; + +$stories = initTableData($stories, $config->my->epic->dtable->fieldList, $this->story); + +if($viewType == 'tiled') $config->my->epic->dtable->fieldList['title']['nestedToggle'] = false; +$cols = array_values($config->my->epic->dtable->fieldList); +$data = array_values($stories); +dtable +( + set::cols($cols), + set::data($data), + set::userMap($users), + set::fixedLeftWidth('44%'), + set::checkable($canBatchAction ? true : false), + set::onRenderCell(jsRaw('window.renderCell')), + set::orderBy($orderBy), + set::sortLink(createLink('my', $app->rawMethod, "mode={$mode}&type={$type}¶m={$param}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), + set::footToolbar($footToolbar), + set::footPager(usePager()), + set::emptyTip(sprintf($lang->my->noData, $lang->ERCommon)) +); + +render(); diff --git a/module/my/ui/header.html.php b/module/my/ui/header.html.php index 66172de53a..2a9defd62e 100644 --- a/module/my/ui/header.html.php +++ b/module/my/ui/header.html.php @@ -10,21 +10,13 @@ declare(strict_types=1); */ namespace zin; -$badgesOptions = array(); -$badgesOptions['mode'] = $mode; -$badgesOptions['todoCount'] = $todoCount; -$badgesOptions['isIPD'] = $isIPD; -$badgesOptions['isMax'] = $isMax; -$badgesOptions['isBiz'] = $isBiz; -$badgesOptions['isOpenedURAndSR'] = $isOpenedURAndSR; -$badgesOptions['rawMethod'] = $app->rawMethod; - if($app->rawMethod == 'work') { $badgeMap = array(); $nameMap = array('task' =>'task', 'story' =>'story', 'bug' =>'bug', 'testcase' =>'case', 'testtask' =>'testtask'); if($isOpenedURAndSR !== 0) $nameMap = array_merge($nameMap, array('requirement' => 'requirement')); + if($config->enableER) $nameMap = array_merge($nameMap, array('epic' => 'epic')); if($isBiz !== 0 || $isMax !== 0 || $isIPD !== 0) $nameMap = array_merge($nameMap, array('feedback' => 'feedback', 'ticket' => 'ticket')); if($isMax !== 0 || $isIPD !== 0) $nameMap = array_merge($nameMap, array('issue' => 'issue', 'risk' => 'risk', 'nc' => 'qa', 'myMeeting' => 'meeting')); if($isIPD !== 0) $nameMap = array_merge($nameMap, array('demand' => 'demand')); diff --git a/module/my/ui/requirement.html.php b/module/my/ui/requirement.html.php index 437b922f57..aa9bde261e 100644 --- a/module/my/ui/requirement.html.php +++ b/module/my/ui/requirement.html.php @@ -11,6 +11,9 @@ declare(strict_types=1); namespace zin; include 'header.html.php'; +jsVar('window.globalSearchType', 'requirement'); +jsVar('showGrade', $showGrade); +jsVar('gradeGroup', $gradeGroup); jsVar('recall', $lang->story->recall); jsVar('recallChange', $lang->story->recallChange); @@ -22,10 +25,32 @@ featureBar li(searchToggle(set::module($this->app->rawMethod . 'Requirement'), set::open($type == 'bysearch'))) ); -$canBatchEdit = common::hasPriv('story', 'batchEdit'); -$canBatchReview = common::hasPriv('story', 'batchReview'); -$canBatchAssignTo = common::hasPriv('story', 'batchAssignTo'); -$canBatchClose = common::hasPriv('story', 'batchClose'); +$viewType = $this->cookie->storyViewType ? $this->cookie->storyViewType : 'tree'; +toolbar +( + item(set(array + ( + 'type' => 'btnGroup', + 'items' => array(array + ( + 'icon' => 'list', + 'class' => 'btn-icon switchButton' . ($viewType == 'tiled' ? ' text-primary' : ''), + 'data-type' => 'tiled', + 'hint' => $lang->story->viewTypeList['tiled'] + ), array + ( + 'icon' => 'treeview', + 'class' => 'switchButton btn-icon' . ($viewType == 'tree' ? ' text-primary' : ''), + 'data-type' => 'tree', + 'hint' => $lang->story->viewTypeList['tree'] + )) + ))) +); + +$canBatchEdit = common::hasPriv('requirement', 'batchEdit'); +$canBatchReview = common::hasPriv('requirement', 'batchReview'); +$canBatchAssignTo = common::hasPriv('requirement', 'batchAssignTo'); +$canBatchClose = common::hasPriv('requirement', 'batchClose'); $canBatchAction = $canBatchEdit || $canBatchReview || $canBatchAssignTo || $canBatchClose; $reviewItems = array(); @@ -35,7 +60,7 @@ if($canBatchReview) foreach($lang->story->reasonList as $key => $reason) { if(!$key || $key == 'subdivided' || $key == 'duplicate') continue; - $rejectItems[] = array('text' => $reason, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('story', 'batchReview', "result=reject&reason={$key}&storyType=requirement")); + $rejectItems[] = array('text' => $reason, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('requirement', 'batchReview', "result=reject&reason={$key}&storyType=requirement")); } foreach($lang->story->reviewResultList as $key => $result) @@ -47,7 +72,7 @@ if($canBatchReview) } else { - $reviewItems[] = array('text' => $result, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('story', 'batchReview', "result={$key}&reason=&storyType=requirement")); + $reviewItems[] = array('text' => $result, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('requirement', 'batchReview', "result={$key}&reason=&storyType=requirement")); } } } @@ -58,23 +83,25 @@ if($canBatchAssignTo) foreach($users as $key => $value) { if(empty($key) || $key == 'closed') continue; - $assignedToItems[] = array('text' => $value, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('story', 'batchAssignTo', "storyType=requirement&assignedTo={$key}")); + $assignedToItems[] = array('text' => $value, 'innerClass' => 'batch-btn ajax-btn not-open-url', 'data-url' => helper::createLink('requirement', 'batchAssignTo', "storyType=requirement&assignedTo={$key}")); } } $footToolbar = array('items' => array ( - $canBatchEdit ? array('text' => $lang->edit, 'className' => 'batch-btn', 'data-url' => helper::createLink('story', 'batchEdit', "productID=0&executionID=0&branch=0&storyType=requirement&from={$app->rawMethod}")) : null, + $canBatchEdit ? array('text' => $lang->edit, 'className' => 'batch-btn', 'data-url' => helper::createLink('requirement', 'batchEdit', "productID=0&executionID=0&branch=0&storyType=requirement&from={$app->rawMethod}")) : null, $canBatchReview ? array('caret' => 'up', 'text' => $lang->story->review, 'type' => 'dropdown', 'items' => $reviewItems, 'data-placement' => 'top-start') : null, $canBatchAssignTo ? array('caret' => 'up', 'text' => $lang->story->assignedTo, 'type' => 'dropdown', 'items' => $assignedToItems, 'data-placement' => 'top-start') : null, - $canBatchClose ? array('text' => $lang->story->close, 'className' => 'batch-btn', 'data-url' => helper::createLink('story', 'batchClose', "productID=0&executionID=0&storyType=requirement&from={$app->rawMethod}")) : null + $canBatchClose ? array('text' => $lang->story->close, 'className' => 'batch-btn', 'data-url' => helper::createLink('requirement', 'batchClose', "productID=0&executionID=0&storyType=requirement&from={$app->rawMethod}")) : null ), 'btnProps' => array('size' => 'sm', 'btnType' => 'secondary')); if($canBatchAction) $config->my->requirement->dtable->fieldList['id']['type'] = 'checkID'; $stories = initTableData($stories, $config->my->requirement->dtable->fieldList, $this->story); -$cols = array_values($config->my->requirement->dtable->fieldList); -$data = array_values($stories); + +if($viewType == 'tiled') $config->my->requirement->dtable->fieldList['title']['nestedToggle'] = false; +$cols = array_values($config->my->requirement->dtable->fieldList); +$data = array_values($stories); dtable ( set::cols($cols), @@ -82,7 +109,7 @@ dtable set::userMap($users), set::fixedLeftWidth('44%'), set::checkable($canBatchAction ? true : false), - set::onRenderCell(jsRaw('window.onRenderCell')), + set::onRenderCell(jsRaw('window.renderCell')), set::orderBy($orderBy), set::sortLink(createLink('my', $app->rawMethod, "mode={$mode}&type={$type}¶m={$param}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}")), set::footToolbar($footToolbar), diff --git a/module/my/ui/story.html.php b/module/my/ui/story.html.php index bfe775a2ef..b8d8a23af6 100644 --- a/module/my/ui/story.html.php +++ b/module/my/ui/story.html.php @@ -14,7 +14,9 @@ include 'header.html.php'; jsVar('children', $lang->story->children); jsVar('childrenAB', $lang->story->childrenAB); -jsVar('URChanged', $lang->story->URChanged); +jsVar('showGrade', $showGrade); +jsVar('gradeGroup', $gradeGroup); +jsVar('window.globalSearchType', 'story'); featureBar ( @@ -23,6 +25,28 @@ featureBar li(searchToggle(set::module($this->app->rawMethod . 'Story'), set::open($type == 'bysearch'))) ); +$viewType = $this->cookie->storyViewType ? $this->cookie->storyViewType : 'tree'; +toolbar +( + item(set(array + ( + 'type' => 'btnGroup', + 'items' => array(array + ( + 'icon' => 'list', + 'class' => 'btn-icon switchButton' . ($viewType == 'tiled' ? ' text-primary' : ''), + 'data-type' => 'tiled', + 'hint' => $lang->story->viewTypeList['tiled'] + ), array + ( + 'icon' => 'treeview', + 'class' => 'switchButton btn-icon' . ($viewType == 'tree' ? ' text-primary' : ''), + 'data-type' => 'tree', + 'hint' => $lang->story->viewTypeList['tree'] + )) + ))) +); + $canBatchEdit = common::hasPriv('story', 'batchEdit'); $canBatchReview = common::hasPriv('story', 'batchReview'); $canBatchAssignTo = common::hasPriv('story', 'batchAssignTo'); @@ -74,8 +98,10 @@ $footToolbar = array('items' => array if($canBatchAction) $config->my->story->dtable->fieldList['id']['type'] = 'checkID'; $stories = initTableData($stories, $config->my->story->dtable->fieldList, $this->story); -$cols = array_values($config->my->story->dtable->fieldList); -$data = array_values($stories); + +if($viewType == 'tiled') $config->my->story->dtable->fieldList['title']['nestedToggle'] = false; +$cols = array_values($config->my->story->dtable->fieldList); +$data = array_values($stories); dtable ( set::cols($cols), diff --git a/module/my/zen.php b/module/my/zen.php index 4ba3fa1b8f..babdfa5e21 100644 --- a/module/my/zen.php +++ b/module/my/zen.php @@ -192,6 +192,15 @@ class myZen extends my $count['requirement'] = $assignedRequirementCount + $reviewByRequirementCount; } + if($this->config->enableER) + { + $this->story->getUserStories($this->app->user->account, 'assignedTo', 'id_desc', $pager, 'epic', false, 'all'); + $assignedEpicCount = $pager->recTotal; + $this->story->getUserStories($this->app->user->account, 'reviewBy', 'id_desc', $pager, 'epic', false, 'all'); + $reviewByEpicCount = $pager->recTotal; + $count['epic'] = $assignedEpicCount + $reviewByEpicCount; + } + /* Get the number of bugs assigned to me. */ $this->loadModel('bug')->getUserBugs($this->app->user->account, 'assignedTo', 'id_desc', 0, $pager); $count['bug'] = $pager->recTotal; diff --git a/module/product/config.php b/module/product/config.php index afdad0e2b1..1f37f87128 100644 --- a/module/product/config.php +++ b/module/product/config.php @@ -17,7 +17,7 @@ if($config->systemMode == 'ALM' || $config->systemMode == 'PLM') $config->produc /* Export fields of product list page. */ $config->product->list = new stdclass(); -$config->product->list->exportFields = 'id,program,line,name,manager,draftStories,activeStories,changedStories,reviewingStories,closedStories,storyCompleteRate,unResolvedBugs,bugFixedRate,plans,releases'; +$config->product->list->exportFields = 'id,program,line,name,manager,draftEpics,activeEpics,changedEpics,reviewingEpics,closedEpics,epicCompleteRate,draftRequirements,activeRequirements,changedRequirements,reviewingRequirements,closedRequirements,requirementCompleteRate,draftStories,activeStories,changedStories,reviewingStories,closedStories,storyCompleteRate,unResolvedBugs,bugFixedRate,plans,releases'; $config->product->list->customCreateFields = 'PO,RD,acl'; $config->product->list->customBatchEditFields = 'PO,QD,RD,status,type,acl'; if($config->systemMode == 'ALM' || $config->systemMode == 'PLM') $config->product->list->customBatchEditFields .= ',program'; diff --git a/module/product/config/dtable.php b/module/product/config/dtable.php index 9fa9609f98..b84929f586 100644 --- a/module/product/config/dtable.php +++ b/module/product/config/dtable.php @@ -195,53 +195,173 @@ $config->product->all->dtable->fieldList['createdDate']['type'] = 'date'; $config->product->all->dtable->fieldList['createdDate']['sortType'] = true; $config->product->all->dtable->fieldList['createdDate']['group'] = '1'; -$config->product->all->dtable->fieldList['draftStories']['name'] = 'draftStories'; -$config->product->all->dtable->fieldList['draftStories']['title'] = $lang->product->draftStory; -$config->product->all->dtable->fieldList['draftStories']['width'] = 64; -$config->product->all->dtable->fieldList['draftStories']['type'] = 'number'; -$config->product->all->dtable->fieldList['draftStories']['show'] = true; -$config->product->all->dtable->fieldList['draftStories']['sortType'] = true; -$config->product->all->dtable->fieldList['draftStories']['group'] = 'g4'; +if($config->enableER) +{ + $config->product->all->dtable->fieldList['draftEpics']['name'] = 'draftEpics'; + $config->product->all->dtable->fieldList['draftEpics']['title'] = $lang->product->draftStory; + $config->product->all->dtable->fieldList['draftEpics']['width'] = 64; + $config->product->all->dtable->fieldList['draftEpics']['type'] = 'number'; + $config->product->all->dtable->fieldList['draftEpics']['show'] = true; + $config->product->all->dtable->fieldList['draftEpics']['sortType'] = true; + $config->product->all->dtable->fieldList['draftEpics']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['draftEpics']['headerGroup'] = $lang->ERCommon; -$config->product->all->dtable->fieldList['activeStories']['name'] = 'activeStories'; -$config->product->all->dtable->fieldList['activeStories']['title'] = $lang->product->activeStory; -$config->product->all->dtable->fieldList['activeStories']['width'] = 64; -$config->product->all->dtable->fieldList['activeStories']['type'] = 'number'; -$config->product->all->dtable->fieldList['activeStories']['show'] = true; -$config->product->all->dtable->fieldList['activeStories']['sortType'] = true; -$config->product->all->dtable->fieldList['activeStories']['group'] = 'g4'; + $config->product->all->dtable->fieldList['activeEpics']['name'] = 'activeEpics'; + $config->product->all->dtable->fieldList['activeEpics']['title'] = $lang->product->activeStory; + $config->product->all->dtable->fieldList['activeEpics']['width'] = 64; + $config->product->all->dtable->fieldList['activeEpics']['type'] = 'number'; + $config->product->all->dtable->fieldList['activeEpics']['show'] = true; + $config->product->all->dtable->fieldList['activeEpics']['sortType'] = true; + $config->product->all->dtable->fieldList['activeEpics']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['activeEpics']['headerGroup'] = $lang->ERCommon; -$config->product->all->dtable->fieldList['changingStories']['name'] = 'changingStories'; -$config->product->all->dtable->fieldList['changingStories']['title'] = $lang->product->changingStory; -$config->product->all->dtable->fieldList['changingStories']['width'] = 64; -$config->product->all->dtable->fieldList['changingStories']['type'] = 'number'; -$config->product->all->dtable->fieldList['changingStories']['show'] = true; -$config->product->all->dtable->fieldList['changingStories']['sortType'] = true; -$config->product->all->dtable->fieldList['changingStories']['group'] = 'g4'; + $config->product->all->dtable->fieldList['changingEpics']['name'] = 'changingEpics'; + $config->product->all->dtable->fieldList['changingEpics']['title'] = $lang->product->changingStory; + $config->product->all->dtable->fieldList['changingEpics']['width'] = 64; + $config->product->all->dtable->fieldList['changingEpics']['type'] = 'number'; + $config->product->all->dtable->fieldList['changingEpics']['show'] = true; + $config->product->all->dtable->fieldList['changingEpics']['sortType'] = true; + $config->product->all->dtable->fieldList['changingEpics']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['changingEpics']['headerGroup'] = $lang->ERCommon; -$config->product->all->dtable->fieldList['reviewingStories']['name'] = 'reviewingStories'; -$config->product->all->dtable->fieldList['reviewingStories']['title'] = $lang->product->reviewingStory; -$config->product->all->dtable->fieldList['reviewingStories']['width'] = 64; -$config->product->all->dtable->fieldList['reviewingStories']['type'] = 'number'; -$config->product->all->dtable->fieldList['reviewingStories']['show'] = true; -$config->product->all->dtable->fieldList['reviewingStories']['sortType'] = true; -$config->product->all->dtable->fieldList['reviewingStories']['group'] = 'g4'; + $config->product->all->dtable->fieldList['reviewingEpics']['name'] = 'reviewingEpics'; + $config->product->all->dtable->fieldList['reviewingEpics']['title'] = $lang->product->reviewingStory; + $config->product->all->dtable->fieldList['reviewingEpics']['width'] = 64; + $config->product->all->dtable->fieldList['reviewingEpics']['type'] = 'number'; + $config->product->all->dtable->fieldList['reviewingEpics']['show'] = true; + $config->product->all->dtable->fieldList['reviewingEpics']['sortType'] = true; + $config->product->all->dtable->fieldList['reviewingEpics']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['reviewingEpics']['headerGroup'] = $lang->ERCommon; -$config->product->all->dtable->fieldList['totalStories']['name'] = 'totalStories'; -$config->product->all->dtable->fieldList['totalStories']['title'] = $lang->product->totalStories; -$config->product->all->dtable->fieldList['totalStories']['width'] = 92; -$config->product->all->dtable->fieldList['totalStories']['minWidth'] = 100; -$config->product->all->dtable->fieldList['totalStories']['type'] = 'number'; -$config->product->all->dtable->fieldList['totalStories']['sortType'] = true; -$config->product->all->dtable->fieldList['totalStories']['group'] = 'g4'; + $config->product->all->dtable->fieldList['totalEpics']['name'] = 'totalEpics'; + $config->product->all->dtable->fieldList['totalEpics']['title'] = $lang->product->totalStories; + $config->product->all->dtable->fieldList['totalEpics']['width'] = 92; + $config->product->all->dtable->fieldList['totalEpics']['minWidth'] = 100; + $config->product->all->dtable->fieldList['totalEpics']['type'] = 'number'; + $config->product->all->dtable->fieldList['totalEpics']['sortType'] = true; + $config->product->all->dtable->fieldList['totalEpics']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['totalEpics']['headerGroup'] = $lang->ERCommon; -$config->product->all->dtable->fieldList['storyCompleteRate']['name'] = 'storyCompleteRate'; -$config->product->all->dtable->fieldList['storyCompleteRate']['title'] = $lang->product->completeRate; -$config->product->all->dtable->fieldList['storyCompleteRate']['width'] = 64; -$config->product->all->dtable->fieldList['storyCompleteRate']['type'] = 'progress'; -$config->product->all->dtable->fieldList['storyCompleteRate']['show'] = true; -$config->product->all->dtable->fieldList['storyCompleteRate']['sortType'] = false; -$config->product->all->dtable->fieldList['storyCompleteRate']['group'] = 'g4'; + $config->product->all->dtable->fieldList['epicCompleteRate']['name'] = 'epicCompleteRate'; + $config->product->all->dtable->fieldList['epicCompleteRate']['title'] = $lang->product->completeRate; + $config->product->all->dtable->fieldList['epicCompleteRate']['width'] = 64; + $config->product->all->dtable->fieldList['epicCompleteRate']['type'] = 'progress'; + $config->product->all->dtable->fieldList['epicCompleteRate']['show'] = true; + $config->product->all->dtable->fieldList['epicCompleteRate']['sortType'] = false; + $config->product->all->dtable->fieldList['epicCompleteRate']['group'] = 'epicCount'; + $config->product->all->dtable->fieldList['epicCompleteRate']['headerGroup'] = $lang->ERCommon; +} + +if($config->URAndSR) +{ + $config->product->all->dtable->fieldList['draftRequirements']['name'] = 'draftRequirements'; + $config->product->all->dtable->fieldList['draftRequirements']['title'] = $lang->product->draftStory; + $config->product->all->dtable->fieldList['draftRequirements']['width'] = 64; + $config->product->all->dtable->fieldList['draftRequirements']['type'] = 'number'; + $config->product->all->dtable->fieldList['draftRequirements']['show'] = true; + $config->product->all->dtable->fieldList['draftRequirements']['sortType'] = true; + $config->product->all->dtable->fieldList['draftRequirements']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['draftRequirements']['headerGroup'] = $lang->URCommon; + + $config->product->all->dtable->fieldList['activeRequirements']['name'] = 'activeRequirements'; + $config->product->all->dtable->fieldList['activeRequirements']['title'] = $lang->product->activeStory; + $config->product->all->dtable->fieldList['activeRequirements']['width'] = 64; + $config->product->all->dtable->fieldList['activeRequirements']['type'] = 'number'; + $config->product->all->dtable->fieldList['activeRequirements']['show'] = true; + $config->product->all->dtable->fieldList['activeRequirements']['sortType'] = true; + $config->product->all->dtable->fieldList['activeRequirements']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['activeRequirements']['headerGroup'] = $lang->URCommon; + + $config->product->all->dtable->fieldList['changingRequirements']['name'] = 'changingRequirements'; + $config->product->all->dtable->fieldList['changingRequirements']['title'] = $lang->product->changingStory; + $config->product->all->dtable->fieldList['changingRequirements']['width'] = 64; + $config->product->all->dtable->fieldList['changingRequirements']['type'] = 'number'; + $config->product->all->dtable->fieldList['changingRequirements']['show'] = true; + $config->product->all->dtable->fieldList['changingRequirements']['sortType'] = true; + $config->product->all->dtable->fieldList['changingRequirements']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['changingRequirements']['headerGroup'] = $lang->URCommon; + + $config->product->all->dtable->fieldList['reviewingRequirements']['name'] = 'reviewingRequirements'; + $config->product->all->dtable->fieldList['reviewingRequirements']['title'] = $lang->product->reviewingStory; + $config->product->all->dtable->fieldList['reviewingRequirements']['width'] = 64; + $config->product->all->dtable->fieldList['reviewingRequirements']['type'] = 'number'; + $config->product->all->dtable->fieldList['reviewingRequirements']['show'] = true; + $config->product->all->dtable->fieldList['reviewingRequirements']['sortType'] = true; + $config->product->all->dtable->fieldList['reviewingRequirements']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['reviewingRequirements']['headerGroup'] = $lang->URCommon; + + $config->product->all->dtable->fieldList['totalRequirements']['name'] = 'totalRequirements'; + $config->product->all->dtable->fieldList['totalRequirements']['title'] = $lang->product->totalStories; + $config->product->all->dtable->fieldList['totalRequirements']['width'] = 92; + $config->product->all->dtable->fieldList['totalRequirements']['minWidth'] = 100; + $config->product->all->dtable->fieldList['totalRequirements']['type'] = 'number'; + $config->product->all->dtable->fieldList['totalRequirements']['sortType'] = true; + $config->product->all->dtable->fieldList['totalRequirements']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['totalRequirements']['headerGroup'] = $lang->URCommon; + + $config->product->all->dtable->fieldList['requirementCompleteRate']['name'] = 'requirementCompleteRate'; + $config->product->all->dtable->fieldList['requirementCompleteRate']['title'] = $lang->product->completeRate; + $config->product->all->dtable->fieldList['requirementCompleteRate']['width'] = 64; + $config->product->all->dtable->fieldList['requirementCompleteRate']['type'] = 'progress'; + $config->product->all->dtable->fieldList['requirementCompleteRate']['show'] = true; + $config->product->all->dtable->fieldList['requirementCompleteRate']['sortType'] = false; + $config->product->all->dtable->fieldList['requirementCompleteRate']['group'] = 'requirementCount'; + $config->product->all->dtable->fieldList['requirementCompleteRate']['headerGroup'] = $lang->URCommon; +} + +$config->product->all->dtable->fieldList['draftStories']['name'] = 'draftStories'; +$config->product->all->dtable->fieldList['draftStories']['title'] = $lang->product->draftStory; +$config->product->all->dtable->fieldList['draftStories']['width'] = 64; +$config->product->all->dtable->fieldList['draftStories']['type'] = 'number'; +$config->product->all->dtable->fieldList['draftStories']['show'] = true; +$config->product->all->dtable->fieldList['draftStories']['sortType'] = true; +$config->product->all->dtable->fieldList['draftStories']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['draftStories']['headerGroup'] = $lang->SRCommon; + +$config->product->all->dtable->fieldList['activeStories']['name'] = 'activeStories'; +$config->product->all->dtable->fieldList['activeStories']['title'] = $lang->product->activeStory; +$config->product->all->dtable->fieldList['activeStories']['width'] = 64; +$config->product->all->dtable->fieldList['activeStories']['type'] = 'number'; +$config->product->all->dtable->fieldList['activeStories']['show'] = true; +$config->product->all->dtable->fieldList['activeStories']['sortType'] = true; +$config->product->all->dtable->fieldList['activeStories']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['activeStories']['headerGroup'] = $lang->SRCommon; + +$config->product->all->dtable->fieldList['changingStories']['name'] = 'changingStories'; +$config->product->all->dtable->fieldList['changingStories']['title'] = $lang->product->changingStory; +$config->product->all->dtable->fieldList['changingStories']['width'] = 64; +$config->product->all->dtable->fieldList['changingStories']['type'] = 'number'; +$config->product->all->dtable->fieldList['changingStories']['show'] = true; +$config->product->all->dtable->fieldList['changingStories']['sortType'] = true; +$config->product->all->dtable->fieldList['changingStories']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['changingStories']['headerGroup'] = $lang->SRCommon; + +$config->product->all->dtable->fieldList['reviewingStories']['name'] = 'reviewingStories'; +$config->product->all->dtable->fieldList['reviewingStories']['title'] = $lang->product->reviewingStory; +$config->product->all->dtable->fieldList['reviewingStories']['width'] = 64; +$config->product->all->dtable->fieldList['reviewingStories']['type'] = 'number'; +$config->product->all->dtable->fieldList['reviewingStories']['show'] = true; +$config->product->all->dtable->fieldList['reviewingStories']['sortType'] = true; +$config->product->all->dtable->fieldList['reviewingStories']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['reviewingStories']['headerGroup'] = $lang->SRCommon; + +$config->product->all->dtable->fieldList['totalStories']['name'] = 'totalStories'; +$config->product->all->dtable->fieldList['totalStories']['title'] = $lang->product->totalStories; +$config->product->all->dtable->fieldList['totalStories']['width'] = 92; +$config->product->all->dtable->fieldList['totalStories']['minWidth'] = 100; +$config->product->all->dtable->fieldList['totalStories']['type'] = 'number'; +$config->product->all->dtable->fieldList['totalStories']['sortType'] = true; +$config->product->all->dtable->fieldList['totalStories']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['totalStories']['headerGroup'] = $lang->SRCommon; + +$config->product->all->dtable->fieldList['storyCompleteRate']['name'] = 'storyCompleteRate'; +$config->product->all->dtable->fieldList['storyCompleteRate']['title'] = $lang->product->completeRate; +$config->product->all->dtable->fieldList['storyCompleteRate']['width'] = 64; +$config->product->all->dtable->fieldList['storyCompleteRate']['type'] = 'progress'; +$config->product->all->dtable->fieldList['storyCompleteRate']['show'] = true; +$config->product->all->dtable->fieldList['storyCompleteRate']['sortType'] = false; +$config->product->all->dtable->fieldList['storyCompleteRate']['group'] = 'storyCount'; +$config->product->all->dtable->fieldList['storyCompleteRate']['headerGroup'] = $lang->SRCommon; $config->product->all->dtable->fieldList['plans']['name'] = 'plans'; $config->product->all->dtable->fieldList['plans']['title'] = $lang->product->plan; diff --git a/module/product/config/search.php b/module/product/config/search.php index 251f231e97..37f4c60869 100644 --- a/module/product/config/search.php +++ b/module/product/config/search.php @@ -2,7 +2,7 @@ global $app; $app->loadLang('story'); $config->product->search['module'] = 'story'; -$config->product->search['fields']['title'] = $lang->story->title; +$config->product->search['fields']['title'] = $lang->story->name; $config->product->search['fields']['id'] = $lang->story->id; $config->product->search['fields']['keywords'] = $lang->story->keywords; $config->product->search['fields']['status'] = $lang->story->status; @@ -13,6 +13,7 @@ if($config->edition == 'ipd') $config->product->search['fields']['roadmap'] = $ $config->product->search['fields']['stage'] = $lang->story->stage; $config->product->search['fields']['product'] = $lang->story->product; $config->product->search['fields']['branch'] = ''; +$config->product->search['fields']['grade'] = $lang->story->grade; $config->product->search['fields']['plan'] = $lang->story->plan; $config->product->search['fields']['estimate'] = $lang->story->estimate; @@ -48,6 +49,7 @@ $config->product->search['params']['pri'] = array('operator' => '=', $config->product->search['params']['product'] = array('operator' => '=', 'control' => 'select', 'values' => ''); $config->product->search['params']['branch'] = array('operator' => '=', 'control' => 'select', 'values' => ''); +$config->product->search['params']['grade'] = array('operator' => '=', 'control' => 'select', 'values' => ''); $config->product->search['params']['module'] = array('operator' => 'belong', 'control' => 'select', 'values' => ''); $config->product->search['params']['roadmap'] = array('operator' => '=', 'control' => 'select', 'values' => ''); $config->product->search['params']['plan'] = array('operator' => '=', 'control' => 'select', 'values' => ''); diff --git a/module/product/control.php b/module/product/control.php index 504ca9b26e..a0b0f663a9 100755 --- a/module/product/control.php +++ b/module/product/control.php @@ -129,6 +129,8 @@ class product extends control $browseType = strtolower($browseType); /* Pre process. */ + $this->loadModel('requirement'); + $this->loadModel('epic'); $this->loadModel('tree'); $isProjectStory = $this->app->rawModule == 'projectstory'; $cookieOrderBy = $this->cookie->productStoryOrder ? $this->cookie->productStoryOrder : 'id_desc'; @@ -161,7 +163,10 @@ class product extends control $stories = $this->productZen->getStories($projectID, $productID, $branchID, $moduleID, $param, $storyType, $browseType, $orderBy, $pager); /* Process the sql, get the condition partition, save it to session. */ - $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', (strpos('bysearch,reviewbyme,bymodule', $browseType) === false && !$isProjectStory)); + $this->loadModel('common')->saveQueryCondition($this->dao->get(), $storyType, (strpos('bysearch,reviewbyme,bymodule', $browseType) === false && !$isProjectStory)); + + /* Append children. */ + if($storyType != 'story') $stories = $this->loadModel('story')->appendChildren($productID, $stories, $storyType); /* Save session. */ $this->productZen->saveSession4Browse($product, $browseType); @@ -786,31 +791,67 @@ class product extends control * @param int $productID * @param string $branch * @param int $projectID + * @param string $browseType + * @param int $param + * @param string $storyType + * @param string $orderBy * @param int $recTotal * @param int $recPerPage * @param int $pageID * @access public * @return void */ - public function track(int $productID, string $branch = '', int $projectID = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) + public function track(int $productID, string $branch = '', int $projectID = 0, string $browseType = 'allstory', int $param = 0, string $storyType = '', string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { - $branch = ($this->cookie->preBranch !== '' and $branch === '') ? $this->cookie->preBranch : $branch; + $browseType = strtolower($browseType); + $branch = ($this->cookie->preBranch !== '' and $branch === '') ? $this->cookie->preBranch : $branch; if(is_bool($branch)) $branch = (string)(int)$branch; /* Set menu. The projectstory module does not execute. */ $this->productZen->setTrackMenu($productID, $branch, $projectID); + /* Init storyType param. */ + $storyTypeList = $this->productZen->getActiveStoryTypeForTrack($projectID, $productID); + if(empty($storyType)) $storyType = key($storyTypeList); + /* Load pager. */ $this->app->loadClass('pager', true); - $pager = new pager($recTotal, $recPerPage, $pageID); + $pager = new pager($recTotal, $recPerPage, $pageID); + $stories = $this->productZen->getStories($projectID, $productID, $branch, 0, (int)$param, ($browseType == 'bysearch' ? $storyType : 'all'), $browseType, $orderBy, $pager); + $tracks = $this->loadModel('story')->getTracksByStories($stories, $storyType); + + $customFields = $this->productZen->getCustomFieldsForTrack($storyType); + $mergeCells = $this->story->getMergeTrackCells($tracks, $customFields['show']); + if(isset($tracks['cols'])) + { + $showFields = $customFields['show']; + $cols = array(); + foreach($tracks['cols'] as $col) + { + if(in_array($col['name'], $showFields) || (isset($col['parentName']) && in_array($col['parentName'], $showFields))) $cols[] = $col; + } + $tracks['cols'] = $cols; + } + + /* Build search form. */ + $this->productZen->buildSearchFormForTrack($productID, $branch, $projectID, $browseType, $param, $storyType); $this->view->title = $this->lang->story->track; - $this->view->tracks = $this->loadModel('story')->getTracks($productID, $branch, $projectID, $pager); + $this->view->tracks = $tracks; + $this->view->storyIdList = array_keys($stories); $this->view->pager = $pager; $this->view->productID = $productID; - $this->view->projectProducts = $this->product->getProductPairsByProject($projectID); $this->view->branch = $branch; $this->view->projectID = $projectID; + $this->view->browseType = $browseType; + $this->view->param = $param; + $this->view->storyType = $storyType; + $this->view->orderBy = $orderBy; + $this->view->customFields = $customFields; + $this->view->mergeCells = $mergeCells; + $this->view->storyTypeList = $storyTypeList; + $this->view->users = $this->loadModel('user')->getPairs('noletter|nodeleted'); + $this->view->projectProducts = $this->product->getProductPairsByProject($projectID); $this->display(); } @@ -827,6 +868,20 @@ class product extends control $this->loadModel('setting')->updateItem("{$this->app->user->account}.product.showAllProjects", $this->post->showAllProjects); } + /** + * 设置需求列表展示的需求等级。 + * Ajax set show grades. + * + * @param string $module + * @param string $showGrades + * @access public + * @return void + */ + public function ajaxSetShowGrades(string $module, string $showGrades) + { + if($showGrades) $this->loadModel('setting')->updateItem("{$this->app->user->account}.$module.showGrades", $showGrades); + } + /** * 获取关联某个执行的产品下拉数据。 * Ajax get products. diff --git a/module/product/css/all.ui.css b/module/product/css/all.ui.css index e69de29bb2..c64f4656de 100644 --- a/module/product/css/all.ui.css +++ b/module/product/css/all.ui.css @@ -0,0 +1 @@ +.dtable-header-group { border-left: 1px solid #efefef} diff --git a/module/product/css/track.ui.css b/module/product/css/track.ui.css index 8742dae229..1a70d147df 100644 --- a/module/product/css/track.ui.css +++ b/module/product/css/track.ui.css @@ -1,9 +1,33 @@ -#mainContent .table-empty-tip {padding: 180px 60px; text-align: center;} -#mainContent .table {table-layout: fixed; background-color: #fff; cursor: pointer; border: 1px solid #ddd;} -#mainContent .table td {white-space:nowrap; text-overflow:clip; overflow:hidden; position:unset !important; border-bottom-color:#ddd !important; border: 1px solid #ddd;} -#mainContent .table .requirement{background: #fff} -#mainContent .main-table tbody > tr:hover { background-color: #fff;} -#mainContent .main-table tbody > tr:nth-child(odd):hover { background-color: #f5f5f5;} -#mainContent .main-table thead > tr > th {overflow: hidden; border: 1px solid #ddd;} -#mainContent .table-footer {background: #fff; padding: 5px; margin-bottom: 10px;} -#mainContent td.empty-tip {padding: 80px;} +.switchBtn {background: transparent; box-shadow: none;} +.orderByIcon.with-popover-show{color: var(--color-primary-500);} + +.card {min-height:80px;} +.card .card-heading{color:var(--color-gray-900)} +.card .card-heading a{color:var(--color-gray-900)} +.card .card-title{height:40px; width:100%;} +.card .card-content{flex-direction: row;justify-content: space-between;color:var(--color-gray-800)} + +.kanban-item.parentTask { padding-bottom: 1px;} +.kanban-item.childTask { padding-top:0px; padding-bottom: 1px;} +.kanban-item.childTask .card {background-color: #f9f9f9;} + +.kanban-header-title .as-title{font-size:14px;} +.kanban-header-lane-name, .kanban-lane-name {display:none;} +.kanban-lane-cols, .kanban-header-cols, .kanban-header-sub-cols { gap: calc(var(--kanban-cols-gap) / 2); } +.lane-col-shrink-with-above { position: relative; } +.lane-col-shrink-with-above:before { + background-color: inherit; + left: 0; + position: absolute; + right: 0; + --tw-content: ""; + content: var(--tw-content); + height: calc(var(--kanban-col-gap-top, var(--kanban-lanes-gap)) - 2px); + top: calc(2px - var(--kanban-col-gap-top, var(--kanban-lanes-gap))); +} + +.kanban-header-col-wrapper{ + --tw-bg-opacity-60: 0.6; + background-color: rgba(var(--color-gray-100-rgb), var(--tw-bg-opacity-60)); +} +.kanban-lane-col{background-color: rgba(var(--color-gray-100-rgb), var(--tw-bg-opacity));} diff --git a/module/product/js/browse.ui.js b/module/product/js/browse.ui.js index fb421b679a..b071321671 100644 --- a/module/product/js/browse.ui.js +++ b/module/product/js/browse.ui.js @@ -18,6 +18,29 @@ $(document).off('click', '[data-formaction]').on('click', '[data-formaction]', f $.ajaxSubmit({"url": $this.data('formaction'), "data": postData}); } }); +$(document).off('click', '.batchImportToLibBtn').on('click', '.batchImportToLibBtn', function() +{ + const dtable = zui.DTable.query($('#stories')); + const checkedList = dtable.$.getChecks(); + if(!checkedList.length) return; + + $("[name='storyIdList'").val(checkedList.join(',')); +}); + +$(document).off('click', '.batchChangeParentBtn').on('click', '.batchChangeParentBtn', function(e) +{ + const dtable = zui.DTable.query($('#stories')); + const checkedList = dtable.$.getChecks(); + + $.cookie.set('checkedItem', checkedList.join(','), {expires:config.cookieLife, path:config.webRoot}); +}); + +$(document).off('click', '.switchButton').on('click', '.switchButton', function() +{ + var storyViewType = $(this).attr('data-type'); + $.cookie.set('storyViewType', storyViewType, {expires:config.cookieLife, path:config.webRoot}); + loadCurrentPage(); +}); $(document).off('click','#linkStoryByPlan button[type="submit"]').on('click', '#linkStoryByPlan button[type="submit"]', function() { @@ -32,7 +55,6 @@ $(document).off('click','#linkStoryByPlan button[type="submit"]').on('click', '# $(document).off('click', '.batchUnlinkStory').on('click', '.batchUnlinkStory', function(e) { - const $this = $(this); const dtable = zui.DTable.query($('#stories')); const checkedList = dtable.$.getChecks(); if(!checkedList.length) return; @@ -72,13 +94,16 @@ window.renderCell = function(result, info) let html = ''; if(story.parentName != undefined && story.parent > 0 && $.cookie.get('tab') == 'project' && projectHasProduct && vision != 'lite') html += story.parentName + ' / '; if(typeof modulePairs[story.rawModule] != 'undefined') html += "" + modulePairs[story.rawModule] + " "; - if(story.parent > 0) html += "" + (storyType == 'requirement' ? 'SR' : childrenAB) + " "; + + let gradeLabel = ''; + if(showGrade || story.grade >= 2) gradeLabel = gradeGroup[story.type][story.grade]; + if(gradeLabel) html += "" + gradeLabel + " "; if(story.color) result[0].props.style = 'color: ' + story.color; if(html) result.unshift({html}); } if(info.col.name == 'status' && result) { - if(info.row.data.URChanged == '1') result[0] = {html: "" + URChanged + ""}; + result[0] = {html: `` + info.row.data.status + ""}; } if(info.col.name == 'assignedTo' && info.row.data.status == 'closed') { @@ -86,6 +111,10 @@ window.renderCell = function(result, info) delete result[0]['props']['href']; result[0]['props']['className'] += ' disabled'; } + if(info.col.name == 'childItem') + { + result[1]['attrs']['title'] = info.row.data?.childItemTitle; + } return result; }; @@ -94,7 +123,6 @@ window.setStatistics = function(element, checkedIdList, pageSummary) if(checkedIdList == undefined || checkedIdList.length == 0) return {html: pageSummary}; let total = 0; - let SRTotal = 0; let estimate = 0; let rate = '0%'; let hasCase = 0; @@ -105,26 +133,39 @@ window.setStatistics = function(element, checkedIdList, pageSummary) if(checkedIdList.includes(row.id)) { const story = row.data; - estimate += parseFloat(story.estimate); + if(story.type != storyType && tab == 'product') return; + if(story?.needSummaryEstimate) estimate += parseFloat(story.estimate); if(story.caseCount > 0) { hasCase += 1; } - else if(typeof story.isParent != 'undefined' && story.isParent) + else if(typeof story.isParent != 'undefined' && story.isParent == '1') { rateCount -= 1; } - if(storyType == story.type) total += 1; - if(storyType == 'requirement' && story.type == 'story') SRTotal += 1; + total += 1; } }) if(rateCount) rate = Math.round(hasCase / rateCount * 10000 / 100) + '' + '%'; - return {html: checkedSummary.replace('%total%', total).replace('%estimate%', estimate).replace('%rate%', rate).replace('%SRTotal%', SRTotal)}; + return {html: checkedSummary.replace('%total%', total).replace('%estimate%', estimate).replace('%rate%', rate)}; }; +window.setShowGrades = function() +{ + const showGrades = $('[name^=showGrades]').zui('picker').$.state.value; + if(oldShowGrades == showGrades) return; + + const module = tab == 'product' ? storyType : tab; + const link = $.createLink('product', 'ajaxSetShowGrades', 'module=' + module + '&showGrades=' + showGrades); + $.get(link, function() + { + loadCurrentPage(); + }); +} + window.importToLib = function() { const dtable = zui.DTable.query($('#stories')); diff --git a/module/product/js/track.ui.js b/module/product/js/track.ui.js new file mode 100644 index 0000000000..723cfd114f --- /dev/null +++ b/module/product/js/track.ui.js @@ -0,0 +1,165 @@ +window.getItem = function(info) +{ + const col = info.col; + const color = info.item.color ? " style='color:" + info.item.color + "'" : ''; + const title = info.item.title; + + let titleHtml = `${title}`; + info.item.content = []; + if(col.indexOf('epic') != -1 || col.indexOf('requirement') != -1 || col.indexOf('story') != -1) + { + let storyPriList = langStoryPriList[info.item.storyType]; + let storyStatusList = langStoryStatusList[info.item.storyType]; + let storyStageList = langStoryStageList[info.item.storyType]; + + if(privs[info.item.storyType]) titleHtml = "" + title + ""; + info.item.title = {html: `
${storyPriList[info.item.pri]} ${titleHtml}
`} + info.item.titleAttrs = {'title' : title}; + + info.item.content.push({html: `
${storyStatusList[info.item.status]}
`}); + info.item.content.push({html: `
${storyStageList[info.item.stage]}
`}) + } + else if(col == 'project' || col == 'execution') + { + let delayHtml = ''; + if(info.item.delay > 0) delayHtml = `${langProjectStatusList['delay']}`; + + titleHtml = `${title}`; + if(col == 'project' && privs['project']) titleHtml = "" + title + ""; + if(col == 'execution' && privs['execution']) titleHtml = "" + title + ""; + info.item.title = {html: `
${titleHtml}${delayHtml}
`} + info.item.titleAttrs = {'title' : title}; + + info.item.content.push({html: `
${langProjectStatusList[info.item.status]}
`}); + info.item.content.push({component: 'ProgressCircle', props: {percent: info.item.progress, size: 24}}); + } + else if(col == 'task') + { + if(privs['task']) titleHtml = "" + title + ""; + info.item.title = {html: `
${langTaskPriList[info.item.pri]} ${titleHtml}
`} + info.item.titleAttrs = {'title' : title}; + + if(info.item.parent == '-1') info.item.content.push({html: ``}); + info.item.content.push({html: `
${langTaskStatusList[info.item.status]}
`}); + if(info.item.assignedTo) info.item.content.push({html: " " + (users[info.item.assignedTo] ? users[info.item.assignedTo] : info.item.assignedTo)}); + info.item.content.push({component: 'ProgressCircle', props: {percent: info.item.progress, size: 24}}); + } + else if(col == 'bug') + { + if(privs['bug']) titleHtml = "" + title + ""; + info.item.title = {html: `
${langBugPriList[info.item.pri]} ${titleHtml}
`} + info.item.titleAttrs = {'title' : title}; + + severity = info.item.severity; + severityHtml = `
`; + if(!langBugSeverityList[severity] || langBugSeverityList[severity] != severity) severityHtml = `
${severity}
`; + + info.item.content.push({html: severityHtml}); + if(info.item.assignedTo) info.item.content.push({html: " " + (users[info.item.assignedTo] ? users[info.item.assignedTo] : info.item.assignedTo)}); + } + else if(col == 'case') + { + if(privs['case']) titleHtml = "" + title + ""; + info.item.title = {html: `
${langCasePriList[info.item.pri]} ${titleHtml}
`} + info.item.titleAttrs = {'title' : title}; + + info.item.content.push({html: "
" + (langCaseResultList[info.item.lastRunResult] ? langCaseResultList[info.item.lastRunResult] : langUnexecuted) + "
"}); + info.item.content.push({html: (users[info.item.lastRunner] ? users[info.item.lastRunner] : info.item.lastRunner)}); + } + else if(col == 'design') + { + info.item.titleAttrs = {'class': 'line-clamp-3', 'title' : title}; + if(privs['design']) + { + info.item.titleUrl = $.createLink('design', 'view', `id=${info.item.id}`); + info.item.titleAttrs = {'class': 'line-clamp-3', 'title' : title, 'data-toggle': 'modal', 'data-size': 'lg'}; + } + } + else if(col == 'commit') + { + if(privs['commit']) info.item.titleUrl = $.createLink('repo', 'revision', `repoID=${info.item.repo}&objectID=0&revision=${info.item.revision}`); + info.item.titleAttrs = {'class': 'line-clamp-3', 'title' : title}; + } +} + +window.getLaneCol = function(lane, col) +{ + if(mergeCells[lane.name]) + { + if(mergeCells[lane.name][col.name]) return {laneColClass: 'lane-col-join-with-above'}; + return {laneColClass: 'lane-col-shrink-with-above'}; + } +} + +window.getCol = function(col) +{ + if(col.name == storyType) col.subtitle = {html: ``}; +} + +window.itemRender = function(info) +{ + const col = info.col; + if(col == 'task') + { + if(info.item.parent > '0') info.item.className.push('hidden childTask parent-' + info.item.parent); + if(info.item.parent == '-1') info.item.className.push('parentTask'); + } + if(col == 'project' || col == 'execution') + { + $delayed = $('.kanban-lane-col[z-lane="' + info.lane + '"][z-col="' + col + '"] .kanban-item[z-key="' + info.item.id + '"] .delayed'); + if($delayed.length > 0) + { + let $relative = $delayed.closest('.relative'); + if($relative.find('.line-clamp-2').height() < $relative.find('.title').height()) $delayed.addClass('absolute bottom-0 right-0'); + } + } + + if(config.rawModule == 'projectstory' && (col.indexOf('epic') != -1 || col.indexOf('requirement') != -1 || col.indexOf('story') != -1)) + { + if(!storyIdList.includes(parseInt(info.item.id))) info.item.className.push('hidden'); + } +} + +window.afterRender = function() +{ + $orderByIcon = $('.orderByIcon'); + if($orderByIcon.length > 0 && !$orderByIcon.hasClass('dropdownInited')) + { + new zui.Dropdown($('.orderByIcon'), {menu: {items: orderByItems}}); + $orderByIcon.addClass('dropdownInited'); + } + + $hasSubCols = $('.kanban-header-cols .kanban-header-col.has-subs'); + if($hasSubCols.length > 0) + { + $hasSubCols.each(function() + { + $subCols = $(this).find(".kanban-header-sub-cols .kanban-header-col.is-sub"); + if($subCols.length == 1) + { + style = $subCols.attr('style'); + $subCols.parent().addClass('hidden'); + $(this).removeClass('has-subs').attr('style', style); + } + }); + } +} + +window.toggleChildren = function(obj, parentID) +{ + if($(obj).hasClass('is-expanded')) + { + $(obj).removeClass('is-expanded').addClass('is-collapsed'); + $('.parent-' + parentID).addClass('hidden') + } + else + { + $(obj).removeClass('is-collapsed').addClass('is-expanded'); + $('.parent-' + parentID).removeClass('hidden') + } +} + +window.changeProduct = function(e) +{ + if(config.rawModule == 'projectstory') loadPage($.createLink(config.rawModule, config.rawMethod, "projectID=" + projectID + "&productID=" + $(e.target).val())); +} diff --git a/module/product/lang/de.php b/module/product/lang/de.php index c88acd0d5a..9a1027375b 100644 --- a/module/product/lang/de.php +++ b/module/product/lang/de.php @@ -12,6 +12,7 @@ $lang->product->index = $lang->productCommon . 'Home'; $lang->product->browse = 'Story Liste'; $lang->product->requirement = 'Requirement Liste'; +$lang->product->epic = "{$lang->ERCommon} Liste"; $lang->product->dynamic = 'Verlauf'; $lang->product->view = 'Übersicht'; $lang->product->edit = "{$lang->productCommon} bearbeiten"; @@ -69,32 +70,39 @@ $lang->product->moreProduct = "More {$lang->productCommon}"; $lang->product->projectInfo = "My {$lang->projectCommon}s that are linked to this {$lang->productCommon} are listed below."; $lang->product->progress = "Progress"; -$lang->product->currentExecution = "Aktuelle Execution"; -$lang->product->activeStories = 'Aktivierte [S]'; -$lang->product->activeStoriesTitle = 'Active Stories'; -$lang->product->changedStories = 'Geänderte [S]'; -$lang->product->changedStoriesTitle = 'Changed Stories'; -$lang->product->draftStories = 'Entwurf [S]'; -$lang->product->draftStoriesTitle = 'Draft Stories'; -$lang->product->reviewingStories = "Reviewing [S]"; -$lang->product->reviewingStoriesTitle = "Reviewing Stories"; -$lang->product->closedStories = 'Geschlossene [S]'; -$lang->product->closedStoriesTitle = 'Closed Stories'; -$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; -$lang->product->activeRequirements = "Active {$lang->URCommon}"; -$lang->product->changedRequirements = "Changed {$lang->URCommon}"; -$lang->product->draftRequirements = "Draft {$lang->URCommon}"; -$lang->product->closedRequirements = "Closed {$lang->URCommon}"; -$lang->product->requireCompleteRate = "{$lang->URCommon} Completion rate"; -$lang->product->unResolvedBugs = 'Ungelöste [B]'; -$lang->product->unResolvedBugsTitle = 'Active Bugs'; -$lang->product->assignToNullBugs = 'Nicht zugewiesene [B]'; -$lang->product->assignToNullBugsTitle = 'Unassigned Bugs'; -$lang->product->closedBugs = 'Closed Bug'; -$lang->product->bugFixedRate = 'Repair Rate'; -$lang->product->unfoldClosed = 'Unfold Closed'; -$lang->product->storyDeliveryRate = "Story Delivery Rate"; -$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; +$lang->product->currentExecution = "Aktuelle Execution"; +$lang->product->activeStories = 'Aktivierte [S]'; +$lang->product->activeStoriesTitle = 'Active Stories'; +$lang->product->changedStories = 'Geänderte [S]'; +$lang->product->changedStoriesTitle = 'Changed Stories'; +$lang->product->draftStories = 'Entwurf [S]'; +$lang->product->draftStoriesTitle = 'Draft Stories'; +$lang->product->reviewingStories = "Reviewing [S]"; +$lang->product->reviewingStoriesTitle = "Reviewing Stories"; +$lang->product->closedStories = 'Geschlossene [S]'; +$lang->product->closedStoriesTitle = 'Closed Stories'; +$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; +$lang->product->activeRequirements = "Active {$lang->URCommon}"; +$lang->product->changedRequirements = "Changed {$lang->URCommon}"; +$lang->product->draftRequirements = "Draft {$lang->URCommon}"; +$lang->product->reviewingRequirements = "Reviewing {$lang->URCommon}"; +$lang->product->closedRequirements = "Closed {$lang->URCommon}"; +$lang->product->requirementCompleteRate = "{$lang->URCommon} Completion rate"; +$lang->product->activeEpics = "Active {$lang->ERCommon}"; +$lang->product->changedEpics = "Changed {$lang->ERCommon}"; +$lang->product->draftEpics = "Draft {$lang->ERCommon}"; +$lang->product->reviewingEpics = "Reviewing {$lang->ERCommon}"; +$lang->product->closedEpics = "Closed {$lang->ERCommon}"; +$lang->product->epicCompleteRate = "{$lang->ERCommon} Completion rate"; +$lang->product->unResolvedBugs = 'Ungelöste [B]'; +$lang->product->unResolvedBugsTitle = 'Active Bugs'; +$lang->product->assignToNullBugs = 'Nicht zugewiesene [B]'; +$lang->product->assignToNullBugsTitle = 'Unassigned Bugs'; +$lang->product->closedBugs = 'Closed Bug'; +$lang->product->bugFixedRate = 'Repair Rate'; +$lang->product->unfoldClosed = 'Unfold Closed'; +$lang->product->storyDeliveryRate = "Story Delivery Rate"; +$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; $lang->product->confirmDelete = " Möchten Sie {$lang->productCommon} löschen?"; $lang->product->errorNoProduct = "Kein {$lang->productCommon} erstellt!"; @@ -219,10 +227,14 @@ $lang->product->abbr->aclList['open'] = 'Default'; $lang->product->aclTips['open'] = "Benutzer mit Rechten für {$lang->productCommon} können zugreifen."; $lang->product->aclTips['private'] = "{$lang->executionCommon} Nur Teammitglieder"; -$lang->product->storySummary = " %s {$lang->SRCommon}, %s Stunde(n) geplant, Fallabdeckung ist %s auf dieser Seite."; -$lang->product->checkedSRSummary = " %total% {$lang->SRCommon}, %estimate% Stunde(n) geplant, Fallabdeckung ist %rate%."; -$lang->product->requirementSummary = " %s {$lang->URCommon}, %s {$lang->SRCommon}, %s Stunde(n) geplant."; -$lang->product->checkedURSummary = " %total% {$lang->URCommon}, %SRTotal% {$lang->SRCommon}, %estimate% Stunde(n) geplant."; +$lang->product->allSummary = "Total %s requirements on this page. Estimates %s ({$lang->hourCommon}), and Case Coverage: %rate%."; +$lang->product->checkedAllSummary = "%total% requirements selected, Esitmates: %estimate% ({$lang->hourCommon}), and Case Coverage: %rate%."; +$lang->product->storySummary = "Total %s {$lang->SRCommon}, %s Stunde(n) geplant, Fallabdeckung ist %s auf dieser Seite."; +$lang->product->checkedSRSummary = "Total %total% {$lang->SRCommon}, %estimate% Stunde(n) geplant, Fallabdeckung ist %rate%."; +$lang->product->requirementSummary = "Total %s {$lang->URCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedURSummary = "%total% {$lang->URCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; +$lang->product->epicSummary = "Total %s {$lang->ERCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedERSummary = "%total% {$lang->ERCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; $lang->product->noModule = '
Kein Modul
Jetzt verwalten
'; $lang->product->noProduct = 'Kein Produkt. '; $lang->product->noMatched = '"%s" kann nicht gefunden werden.' . $lang->productCommon; diff --git a/module/product/lang/en.php b/module/product/lang/en.php index 91c491e0f9..4a25fba0aa 100644 --- a/module/product/lang/en.php +++ b/module/product/lang/en.php @@ -12,6 +12,7 @@ $lang->product->index = $lang->productCommon . ' Home'; $lang->product->browse = 'Story List'; $lang->product->requirement = 'Requirement List'; +$lang->product->epic = "{$lang->ERCommon} List"; $lang->product->dynamic = 'Dynamics'; $lang->product->view = "{$lang->productCommon} Detail"; $lang->product->edit = "Edit {$lang->productCommon}"; @@ -69,32 +70,39 @@ $lang->product->moreProduct = "More {$lang->productCommon}"; $lang->product->projectInfo = "My {$lang->projectCommon}s that are linked to this {$lang->productCommon} are listed below."; $lang->product->progress = "Progress"; -$lang->product->currentExecution = "Current Execution"; -$lang->product->activeStories = 'Active [S]'; -$lang->product->activeStoriesTitle = 'Active Stories'; -$lang->product->changedStories = 'Changed [S]'; -$lang->product->changedStoriesTitle = 'Changed Stories'; -$lang->product->draftStories = 'Draft [S]'; -$lang->product->draftStoriesTitle = 'Draft Stories'; -$lang->product->reviewingStories = "Reviewing [S]"; -$lang->product->reviewingStoriesTitle = "Reviewing Stories"; -$lang->product->closedStories = 'Closed [S]'; -$lang->product->closedStoriesTitle = 'Closed Stories'; -$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; -$lang->product->activeRequirements = "Active {$lang->URCommon}"; -$lang->product->changedRequirements = "Changed {$lang->URCommon}"; -$lang->product->draftRequirements = "Draft {$lang->URCommon}"; -$lang->product->closedRequirements = "Closed {$lang->URCommon}"; -$lang->product->requireCompleteRate = "{$lang->URCommon} Completion rate"; -$lang->product->unResolvedBugs = 'Active [B]'; -$lang->product->unResolvedBugsTitle = 'Active Bugs'; -$lang->product->assignToNullBugs = 'Unassigned [B]'; -$lang->product->assignToNullBugsTitle = 'Unassigned Bugs'; -$lang->product->closedBugs = 'Closed Bug'; -$lang->product->bugFixedRate = 'Repair Rate'; -$lang->product->unfoldClosed = 'Unfold Closed'; -$lang->product->storyDeliveryRate = "Story Delivery Rate"; -$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; +$lang->product->currentExecution = "Current Execution"; +$lang->product->activeStories = 'Active [S]'; +$lang->product->activeStoriesTitle = 'Active Stories'; +$lang->product->changedStories = 'Changed [S]'; +$lang->product->changedStoriesTitle = 'Changed Stories'; +$lang->product->draftStories = 'Draft [S]'; +$lang->product->draftStoriesTitle = 'Draft Stories'; +$lang->product->reviewingStories = "Reviewing [S]"; +$lang->product->reviewingStoriesTitle = "Reviewing Stories"; +$lang->product->closedStories = 'Closed [S]'; +$lang->product->closedStoriesTitle = 'Closed Stories'; +$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; +$lang->product->activeRequirements = "Active {$lang->URCommon}"; +$lang->product->changedRequirements = "Changed {$lang->URCommon}"; +$lang->product->draftRequirements = "Draft {$lang->URCommon}"; +$lang->product->reviewingRequirements = "Reviewing {$lang->URCommon}"; +$lang->product->closedRequirements = "Closed {$lang->URCommon}"; +$lang->product->requirementCompleteRate = "{$lang->URCommon} Completion rate"; +$lang->product->activeEpics = "Active {$lang->ERCommon}"; +$lang->product->changedEpics = "Changed {$lang->ERCommon}"; +$lang->product->draftEpics = "Draft {$lang->ERCommon}"; +$lang->product->reviewingEpics = "Reviewing {$lang->ERCommon}"; +$lang->product->closedEpics = "Closed {$lang->ERCommon}"; +$lang->product->epicCompleteRate = "{$lang->ERCommon} Completion rate"; +$lang->product->unResolvedBugs = 'Active [B]'; +$lang->product->unResolvedBugsTitle = 'Active Bugs'; +$lang->product->assignToNullBugs = 'Unassigned [B]'; +$lang->product->assignToNullBugsTitle = 'Unassigned Bugs'; +$lang->product->closedBugs = 'Closed Bug'; +$lang->product->bugFixedRate = 'Repair Rate'; +$lang->product->unfoldClosed = 'Unfold Closed'; +$lang->product->storyDeliveryRate = "Story Delivery Rate"; +$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; $lang->product->confirmDelete = " Do you want to delete the {$lang->productCommon}?"; $lang->product->errorNoProduct = "No {$lang->productCommon} is created yet!"; @@ -219,10 +227,14 @@ $lang->product->abbr->aclList['open'] = 'Default'; $lang->product->aclTips['open'] = "Users with privileges to {$lang->productCommon} can access it."; $lang->product->aclTips['private'] = "{$lang->executionCommon} team members only"; +$lang->product->allSummary = "Total %s requirements on this page. Estimates %s ({$lang->hourCommon}), and Case Coverage: %rate%."; +$lang->product->checkedAllSummary = "%total% requirements selected, Esitmates: %estimate% ({$lang->hourCommon}), and Case Coverage: %rate%."; $lang->product->storySummary = "Total %s {$lang->SRCommon} on this page. Estimates: %s ({$lang->hourCommon}), and Case Coverage: %s."; $lang->product->checkedSRSummary = "%total% {$lang->SRCommon} selected, Esitmates: %estimate% ({$lang->hourCommon}), and Case Coverage: %rate%."; -$lang->product->requirementSummary = "Total %s {$lang->URCommon}, %s {$lang->SRCommon} on this page. Estimates: %s ({$lang->hourCommon})."; -$lang->product->checkedURSummary = "%total% {$lang->URCommon}, %SRTotal% {$lang->SRCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; +$lang->product->requirementSummary = "Total %s {$lang->URCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedURSummary = "%total% {$lang->URCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; +$lang->product->epicSummary = "Total %s {$lang->ERCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedERSummary = "%total% {$lang->ERCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; $lang->product->noModule = '
You have no modules.
Manage Now
'; $lang->product->noProduct = "No {$lang->productCommon} yet. "; $lang->product->noMatched = '"%s" cannot be found.' . $lang->productCommon; diff --git a/module/product/lang/fr.php b/module/product/lang/fr.php index 6854c73d4c..5a74831268 100644 --- a/module/product/lang/fr.php +++ b/module/product/lang/fr.php @@ -12,6 +12,7 @@ $lang->product->index = 'Accueil ' . $lang->productCommon; $lang->product->browse = 'Liste Stories'; $lang->product->requirement = 'Liste Requirements'; +$lang->product->epic = "{$lang->ERCommon} List"; $lang->product->dynamic = 'Historique'; $lang->product->view = "{$lang->productCommon} Détail"; $lang->product->edit = "Editer {$lang->productCommon}"; @@ -69,32 +70,39 @@ $lang->product->moreProduct = "More {$lang->productCommon}"; $lang->product->projectInfo = "My {$lang->projectCommon}s that are linked to this {$lang->productCommon} are listed below."; $lang->product->progress = "Progress"; -$lang->product->currentExecution = "Current Execution"; -$lang->product->activeStories = 'Actives [S]'; -$lang->product->activeStoriesTitle = 'Stories Actives'; -$lang->product->changedStories = 'Changées [S]'; -$lang->product->changedStoriesTitle = 'Stories Modifiées'; -$lang->product->draftStories = 'Brouillon [S]'; -$lang->product->draftStoriesTitle = 'Stories en Analyse'; -$lang->product->reviewingStories = "Reviewing [S]"; -$lang->product->reviewingStoriesTitle = "Reviewing Stories"; -$lang->product->closedStories = 'Fermées [S]'; -$lang->product->closedStoriesTitle = 'Stories Fermées'; -$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; -$lang->product->activeRequirements = "Active {$lang->URCommon}"; -$lang->product->changedRequirements = "Changed {$lang->URCommon}"; -$lang->product->draftRequirements = "Draft {$lang->URCommon}"; -$lang->product->closedRequirements = "Closed {$lang->URCommon}"; -$lang->product->requireCompleteRate = "{$lang->URCommon} Completion rate"; -$lang->product->unResolvedBugs = 'Ouverts [B]'; -$lang->product->unResolvedBugsTitle = 'Bugs Ouverts'; -$lang->product->assignToNullBugs = 'Orphelins [B]'; -$lang->product->assignToNullBugsTitle = 'Bugs non assignés'; -$lang->product->closedBugs = 'Closed Bug'; -$lang->product->bugFixedRate = 'Repair Rate'; -$lang->product->unfoldClosed = 'Unfold Closed'; -$lang->product->storyDeliveryRate = "Story Delivery Rate"; -$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; +$lang->product->currentExecution = "Current Execution"; +$lang->product->activeStories = 'Actives [S]'; +$lang->product->activeStoriesTitle = 'Stories Actives'; +$lang->product->changedStories = 'Changées [S]'; +$lang->product->changedStoriesTitle = 'Stories Modifiées'; +$lang->product->draftStories = 'Brouillon [S]'; +$lang->product->draftStoriesTitle = 'Stories en Analyse'; +$lang->product->reviewingStories = "Reviewing [S]"; +$lang->product->reviewingStoriesTitle = "Reviewing Stories"; +$lang->product->closedStories = 'Fermées [S]'; +$lang->product->closedStoriesTitle = 'Stories Fermées'; +$lang->product->storyCompleteRate = "{$lang->SRCommon} Completion rate"; +$lang->product->activeRequirements = "Active {$lang->URCommon}"; +$lang->product->changedRequirements = "Changed {$lang->URCommon}"; +$lang->product->draftRequirements = "Draft {$lang->URCommon}"; +$lang->product->reviewingRequirements = "Reviewing {$lang->URCommon}"; +$lang->product->closedRequirements = "Closed {$lang->URCommon}"; +$lang->product->requirementCompleteRate = "{$lang->URCommon} Completion rate"; +$lang->product->activeEpics = "Active {$lang->ERCommon}"; +$lang->product->changedEpics = "Changed {$lang->ERCommon}"; +$lang->product->draftEpics = "Draft {$lang->ERCommon}"; +$lang->product->reviewingEpics = "Reviewing {$lang->ERCommon}"; +$lang->product->closedEpics = "Closed {$lang->ERCommon}"; +$lang->product->epicCompleteRate = "{$lang->ERCommon} Completion rate"; +$lang->product->unResolvedBugs = 'Ouverts [B]'; +$lang->product->unResolvedBugsTitle = 'Bugs Ouverts'; +$lang->product->assignToNullBugs = 'Orphelins [B]'; +$lang->product->assignToNullBugsTitle = 'Bugs non assignés'; +$lang->product->closedBugs = 'Closed Bug'; +$lang->product->bugFixedRate = 'Repair Rate'; +$lang->product->unfoldClosed = 'Unfold Closed'; +$lang->product->storyDeliveryRate = "Story Delivery Rate"; +$lang->product->storyDeliveryRateTip = "Story Delivery Rate = The released or done stories / (Total stories - Closed reason is not done)* 100%"; $lang->product->confirmDelete = "Voulez-vous vraiment supprimer le {$lang->productCommon} ?"; $lang->product->errorNoProduct = "Aucun {$lang->productCommon} n'est créé pour l'instant !"; @@ -219,10 +227,14 @@ $lang->product->abbr->aclList['open'] = 'Défaut'; $lang->product->aclTips['open'] = "Les utilisateurs ayant des droits sur {$lang->productCommon} peuvent accéder à ce {$lang->productCommon}."; $lang->product->aclTips['private'] = "les membres de l'équipe et les membres de la Liste blanche peuvent y accéder."; +$lang->product->allSummary = "Total %s requirements on this page. Estimates %s ({$lang->hourCommon}), and Case Coverage: %rate%."; +$lang->product->checkedAllSummary = "%total% requirements selected, Esitmates: %estimate% ({$lang->hourCommon}), and Case Coverage: %rate%."; $lang->product->storySummary = "Total de %s {$lang->SRCommon} sur cette page. Estimé: %s (h), et couverture de la recette: %s."; $lang->product->checkedSRSummary = "%total% {$lang->SRCommon} sélectionnées, Estimé: %estimate%, et couverture de la recette: %rate%."; -$lang->product->requirementSummary = "Total de %s {$lang->URCommon}, %s {$lang->SRCommon} sur cette page. Estimé: %s (h),."; -$lang->product->checkedURSummary = "%total% {$lang->URCommon}, %SRTotal% {$lang->SRCommon} sélectionnées, Estimé: %estimate%,."; +$lang->product->requirementSummary = "Total %s {$lang->URCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedURSummary = "%total% {$lang->URCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; +$lang->product->epicSummary = "Total %s {$lang->ERCommon} on this page. Estimates: %s ({$lang->hourCommon})."; +$lang->product->checkedERSummary = "%total% {$lang->ERCommon} selected, Esitmates: %estimate% ({$lang->hourCommon})."; $lang->product->noModule = "
Vous n'avez aucun modules.
Gérer Maintenant
"; $lang->product->noProduct = "No {$lang->productCommon} à ce jour. "; $lang->product->noMatched = '"%s" cannot be found.' . $lang->productCommon; diff --git a/module/product/lang/zh-cn.php b/module/product/lang/zh-cn.php index 7a2a0493c8..cacf7b9e2a 100644 --- a/module/product/lang/zh-cn.php +++ b/module/product/lang/zh-cn.php @@ -12,6 +12,7 @@ $lang->product->index = $lang->productCommon . '主页'; $lang->product->browse = "{$lang->SRCommon}列表"; $lang->product->requirement = "{$lang->URCommon}列表"; +$lang->product->epic = "{$lang->ERCommon}列表"; $lang->product->dynamic = $lang->productCommon . '动态'; $lang->product->view = "{$lang->productCommon}概况"; $lang->product->edit = "编辑{$lang->productCommon}"; @@ -69,32 +70,39 @@ $lang->product->moreProduct = "更多{$lang->productCommon}"; $lang->product->projectInfo = "所有与此{$lang->productCommon}关联的我参与的{$lang->projectCommon}"; $lang->product->progress = "{$lang->productCommon}完成度"; -$lang->product->currentExecution = "当前执行"; -$lang->product->activeStories = "激活{$lang->SRCommon}"; -$lang->product->activeStoriesTitle = "激活{$lang->SRCommon}"; -$lang->product->changedStories = "已变更{$lang->SRCommon}"; -$lang->product->changedStoriesTitle = "已变更{$lang->SRCommon}"; -$lang->product->draftStories = "草稿{$lang->SRCommon}"; -$lang->product->draftStoriesTitle = "草稿{$lang->SRCommon}"; -$lang->product->reviewingStories = "评审中{$lang->SRCommon}"; -$lang->product->reviewingStoriesTitle = "评审中{$lang->SRCommon}"; -$lang->product->closedStories = "已关闭{$lang->SRCommon}"; -$lang->product->closedStoriesTitle = "已关闭{$lang->SRCommon}"; -$lang->product->storyCompleteRate = "{$lang->SRCommon}完成率"; -$lang->product->activeRequirements = "激活{$lang->URCommon}"; -$lang->product->changedRequirements = "已变更{$lang->URCommon}"; -$lang->product->draftRequirements = "草稿{$lang->URCommon}"; -$lang->product->closedRequirements = "已关闭{$lang->URCommon}"; -$lang->product->requireCompleteRate = "{$lang->URCommon}完成率"; -$lang->product->unResolvedBugs = '未解决Bug'; -$lang->product->unResolvedBugsTitle = '未解决Bug'; -$lang->product->assignToNullBugs = '未指派Bug'; -$lang->product->assignToNullBugsTitle = '未指派Bug'; -$lang->product->closedBugs = '关闭Bug'; -$lang->product->bugFixedRate = '修复率'; -$lang->product->unfoldClosed = '展开已关闭'; -$lang->product->storyDeliveryRate = "需求交付率"; -$lang->product->storyDeliveryRateTip = "需求交付率 = 阶段为已发布或关闭原因是已完成需求数 /(研发需求总数 - 关闭原因不是已完成的需求数)* 100%"; +$lang->product->currentExecution = "当前执行"; +$lang->product->activeStories = "激活{$lang->SRCommon}"; +$lang->product->activeStoriesTitle = "激活{$lang->SRCommon}"; +$lang->product->changedStories = "已变更{$lang->SRCommon}"; +$lang->product->changedStoriesTitle = "已变更{$lang->SRCommon}"; +$lang->product->draftStories = "草稿{$lang->SRCommon}"; +$lang->product->draftStoriesTitle = "草稿{$lang->SRCommon}"; +$lang->product->reviewingStories = "评审中{$lang->SRCommon}"; +$lang->product->reviewingStoriesTitle = "评审中{$lang->SRCommon}"; +$lang->product->closedStories = "已关闭{$lang->SRCommon}"; +$lang->product->closedStoriesTitle = "已关闭{$lang->SRCommon}"; +$lang->product->storyCompleteRate = "{$lang->SRCommon}完成率"; +$lang->product->activeRequirements = "激活{$lang->URCommon}"; +$lang->product->changedRequirements = "已变更{$lang->URCommon}"; +$lang->product->draftRequirements = "草稿{$lang->URCommon}"; +$lang->product->reviewingRequirements = "评审中{$lang->URCommon}"; +$lang->product->closedRequirements = "已关闭{$lang->URCommon}"; +$lang->product->requirementCompleteRate = "{$lang->URCommon}完成率"; +$lang->product->activeEpics = "激活{$lang->ERCommon}"; +$lang->product->changedEpics = "已变更{$lang->ERCommon}"; +$lang->product->draftEpics = "草稿{$lang->ERCommon}"; +$lang->product->reviewingEpics = "评审中{$lang->ERCommon}"; +$lang->product->closedEpics = "已关闭{$lang->ERCommon}"; +$lang->product->epicCompleteRate = "{$lang->ERCommon}完成率"; +$lang->product->unResolvedBugs = '未解决Bug'; +$lang->product->unResolvedBugsTitle = '未解决Bug'; +$lang->product->assignToNullBugs = '未指派Bug'; +$lang->product->assignToNullBugsTitle = '未指派Bug'; +$lang->product->closedBugs = '关闭Bug'; +$lang->product->bugFixedRate = '修复率'; +$lang->product->unfoldClosed = '展开已关闭'; +$lang->product->storyDeliveryRate = "需求交付率"; +$lang->product->storyDeliveryRateTip = "需求交付率 = 阶段为已发布或关闭原因是已完成需求数 /(研发需求总数 - 关闭原因不是已完成的需求数)* 100%"; $lang->product->confirmDelete = " 您确定删除该{$lang->productCommon}吗?"; $lang->product->errorNoProduct = "还没有创建{$lang->productCommon}!"; @@ -219,10 +227,14 @@ $lang->product->abbr->aclList['open'] = '公开'; $lang->product->aclTips['open'] = "有{$lang->productCommon}视图权限,即可访问"; $lang->product->aclTips['private'] = "{$lang->productCommon}相关负责人、所属项目集的干系人、相关联{$lang->projectCommon}的团队成员和干系人可访问"; +$lang->product->allSummary = "本页共 %s 个需求,预计 %s 个{$lang->hourCommon},用例覆盖率 %s。"; +$lang->product->checkedAllSummary = "选中 %total% 个需求,预计 %estimate% 个{$lang->hourCommon},用例覆盖率 %rate%。"; $lang->product->storySummary = "本页共 %s 个{$lang->SRCommon},预计 %s 个{$lang->hourCommon},用例覆盖率 %s。"; $lang->product->checkedSRSummary = "选中 %total% 个{$lang->SRCommon},预计 %estimate% 个{$lang->hourCommon},用例覆盖率 %rate%。"; -$lang->product->requirementSummary = "本页共 %s 个{$lang->URCommon},%s个{$lang->SRCommon},预计 %s 个{$lang->hourCommon}。"; -$lang->product->checkedURSummary = "选中 %total% 个{$lang->URCommon},%SRTotal%个{$lang->SRCommon},预计 %estimate% 个{$lang->hourCommon}。"; +$lang->product->requirementSummary = "本页共 %s 个{$lang->URCommon},预计 %s 个{$lang->hourCommon}。"; +$lang->product->checkedURSummary = "选中 %total% 个{$lang->URCommon},预计 %estimate% 个{$lang->hourCommon}。"; +$lang->product->epicSummary = "本页共 %s 个{$lang->ERCommon},预计 %s 个{$lang->hourCommon}。"; +$lang->product->checkedERSummary = "选中 %total% 个{$lang->ERCommon},预计 %estimate% 个{$lang->hourCommon}。"; $lang->product->noModule = "
您现在还没有模块信息
请维护模块
"; $lang->product->noProduct = "暂时没有{$lang->productCommon}。"; $lang->product->noMatched = '找不到包含"%s"的' . $lang->productCommon; diff --git a/module/product/model.php b/module/product/model.php index f08a26cea9..c56238eceb 100755 --- a/module/product/model.php +++ b/module/product/model.php @@ -715,28 +715,41 @@ class productModel extends model /* Get module data. */ $projectID = ($this->app->tab == 'project' && empty($projectID)) ? $this->session->project : $projectID; - $searchConfig['params']['module']['values'] = $this->productTao->getModulesForSearchForm($productID, $products, $branch, $projectID); + $searchConfig['params']['module']['values'] = $this->productTao->getModulesForSearchForm($productID, $products, $branch, (int)$projectID); - if($storyType == 'requirement') + $gradePairs = $this->loadModel('story')->getGradePairs($storyType, 'all'); + + if($projectID || $storyType == 'all') { - /* Change for requirement story title. */ - $this->lang->story->title = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->title); - $this->lang->story->create = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->create); + $project = $this->loadModel('project')->fetchById($projectID); + $gradePairs = array(); + $gradeList = $this->loadModel('story')->getGradeList(''); + $storyTypes = isset($project->storyType) ? $project->storyType : 'epic,story,requirement'; + 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->lang->story->title = $this->lang->story->name; + } + elseif($storyType != 'story') + { + $replacement = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon; + + $this->lang->story->title = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->title); + $this->lang->story->create = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->create); $searchConfig['fields']['title'] = $this->lang->story->title; - unset($searchConfig['fields']['plan']); - unset($searchConfig['params']['plan']); - unset($searchConfig['fields']['stage']); - unset($searchConfig['params']['stage']); + } - if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID); - } - else - { - /* Get product plan data. */ - $productIdList = ($this->app->tab == 'project' && empty($productID)) ? array_keys($products) : array($productID); - $branchParam = ($this->app->tab == 'project' && empty($productID)) ? '' : $branch; - $searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList, (empty($branchParam) || $branchParam == 'all') ? '' : $branchParam); - } + $searchConfig['params']['grade']['values'] = $gradePairs; + if($this->config->edition == 'ipd') $searchConfig['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID); + + /* Get product plan data. */ + $productIdList = ($this->app->tab == 'project' && empty($productID)) ? array_keys($products) : array($productID); + $branchParam = ($this->app->tab == 'project' && empty($productID)) ? '' : $branch; + $searchConfig['params']['plan']['values'] = $this->loadModel('productplan')->getPairs($productIdList, (empty($branchParam) || $branchParam == 'all') ? '' : $branchParam); /* Get branch data. */ if($productID) @@ -1187,44 +1200,48 @@ class productModel extends model $storyIdList = array(); $rateCount = 0; - $allCount = 0; + $total = 0; $SRTotal = 0; + $URTotal = 0; + $ERTotal = 0; + + $idList = array(); + foreach($stories as $story) $idList[] = $story->id; + $childTypes = $this->dao->select('parent, type')->from(TABLE_STORY)->where('parent')->in($idList)->andWhere('deleted')->eq('0')->fetchGroup('parent', 'type'); foreach($stories as $story) { - if($storyType == 'requirement' && $story->type == 'story') $SRTotal += 1; - if(!empty($story->type) && $story->type != $storyType) continue; + $total ++; + if($story->type == 'story') $SRTotal += 1; + if($story->type == 'requirement') $URTotal += 1; + if($story->type == 'epic') $ERTotal += 1; - $totalEstimate += $story->estimate; - $allCount ++; + /* 产品软需列表仅统计叶子需求的规模。 + 如果是产品用需、业需列表,则追加统计子需求类型和父需求不一样的父需求规模。 + 如果是项目需求列表,则只统计叶子需求的规模。 + */ + $isLeafStory = !isset($childTypes[$story->id]) || (isset($childTypes[$story->id]) && !isset($childTypes[$story->id][$story->type])); + if(($storyType == $story->type && $isLeafStory) || ($storyType == 'all' && $story->isParent == '0')) + { + $totalEstimate += $story->estimate; + $story->needSummaryEstimate = true; + } - if($story->parent >= 0 && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed')))) + if($story->type != 'story') continue; + if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed')))) { $storyIdList[] = $story->id; $rateCount ++; } - - /* When the status is not closed or closedReason is done or postponed then add cases rate..*/ - if(empty($story->children)) continue; - foreach($story->children as $child) - { - if($storyType == 'requirement' && $child->type == 'story') $SRTotal += 1; - if($child->type != $storyType) continue; - - $allCount ++; - if($child->status != 'closed' || in_array($child->closedReason, array('done', 'postponed'))) - { - $storyIdList[] = $child->id; - $rateCount ++; - } - } } $casesCount = count($this->productTao->filterNoCasesStory($storyIdList)); $rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 4); - if($storyType == 'story') return sprintf($this->lang->product->storySummary, $allCount, $totalEstimate, $rate * 100 . "%"); - if($this->config->vision == 'or') return sprintf($this->lang->product->requirementSummary, $allCount, $totalEstimate); - return sprintf($this->lang->product->requirementSummary, $allCount, $SRTotal, $totalEstimate); + if($storyType == 'all') return sprintf($this->lang->product->allSummary, $total, $totalEstimate, $rate * 100 . "%"); + if($storyType == 'epic') return sprintf($this->lang->product->epicSummary, $ERTotal, $totalEstimate); + if($storyType == 'requirement') return sprintf($this->lang->product->requirementSummary, $URTotal, $totalEstimate); + + return sprintf($this->lang->product->storySummary, $SRTotal, $totalEstimate, $rate * 100 . "%"); } /** @@ -1477,9 +1494,11 @@ class productModel extends model if($this->config->vision == 'or') return $product; - $product->testCaseCoverage = $product->coverage; - $product->storyCompleteRate = $product->totalStories == 0 ? 0 : round($product->finishedStories / $product->totalStories, 3) * 100; - $product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100; + $product->testCaseCoverage = $product->coverage; + $product->epicCompleteRate = $product->totalEpics == 0 ? 0 : round($product->finishedEpics / $product->totalEpics, 3) * 100; + $product->requirementCompleteRate = $product->totalRequirements == 0 ? 0 : round($product->finishedRequirements / $product->totalRequirements, 3) * 100; + $product->storyCompleteRate = $product->totalStories == 0 ? 0 : round($product->finishedStories / $product->totalStories, 3) * 100; + $product->bugFixedRate = ($product->unresolvedBugs + $product->fixedBugs) == 0 ? 0 : round($product->fixedBugs / ($product->unresolvedBugs + $product->fixedBugs), 3) * 100; return $product; } diff --git a/module/product/tao.php b/module/product/tao.php index fa35518b06..9e05b950e4 100644 --- a/module/product/tao.php +++ b/module/product/tao.php @@ -856,13 +856,29 @@ class productTao extends productModel { $product = new stdclass(); - $product->draftStories = isset($productStories[$productID]) ? $productStories[$productID]['draft'] : 0; - $product->activeStories = isset($productStories[$productID]) ? $productStories[$productID]['active'] : 0; - $product->changingStories = isset($productStories[$productID]) ? $productStories[$productID]['changing'] : 0; - $product->reviewingStories = isset($productStories[$productID]) ? $productStories[$productID]['reviewing'] : 0; - $product->closedStories = isset($productStories[$productID]) ? $productStories[$productID]['closed'] : 0; - $product->finishedStories = isset($productStories[$productID]) ? $productStories[$productID]['finished'] : 0; - $product->totalStories = isset($productStories[$productID]) ? $productStories[$productID]['total'] : 0; + $product->draftEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['draft'] : 0; + $product->activeEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['active'] : 0; + $product->changingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['changing'] : 0; + $product->reviewingEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['reviewing'] : 0; + $product->closedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['closed'] : 0; + $product->finishedEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['finished'] : 0; + $product->totalEpics = isset($productStories[$productID]['epic']) ? $productStories[$productID]['epic']['total'] : 0; + + $product->draftRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['draft'] : 0; + $product->activeRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['active'] : 0; + $product->changingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['changing'] : 0; + $product->reviewingRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['reviewing'] : 0; + $product->closedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['closed'] : 0; + $product->finishedRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['finished'] : 0; + $product->totalRequirements = isset($productStories[$productID]['requirement']) ? $productStories[$productID]['requirement']['total'] : 0; + + $product->draftStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['draft'] : 0; + $product->activeStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['active'] : 0; + $product->changingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['changing'] : 0; + $product->reviewingStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['reviewing'] : 0; + $product->closedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['closed'] : 0; + $product->finishedStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['finished'] : 0; + $product->totalStories = isset($productStories[$productID]['story']) ? $productStories[$productID]['story']['total'] : 0; $product->unresolvedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['unresolved'] : 0; $product->closedBugs = isset($productBugs[$productID]) ? $productBugs[$productID]['closed'] : 0; @@ -888,26 +904,25 @@ class productTao extends productModel */ protected function getStoryStats(array $productIdList): array { - $stories = $this->dao->select('product, status, `closedReason`, count(id) AS c') + $stories = $this->dao->select('product, status, `closedReason`, count(id) AS c, type') ->from(TABLE_STORY) ->where('deleted')->eq(0) ->beginIF(!empty($productIdList))->andWhere('product')->in($productIdList)->fi() - ->andWhere('type')->eq('story') - ->groupBy('product, status, `closedReason`') + ->groupBy('product, status, `closedReason`, type') ->fetchAll(); $productStories = array(); foreach($stories as $story) { - if(!isset($productStories[$story->product])) $productStories[$story->product] = array('draft' => 0, 'active' => 0, 'changing' => 0, 'reviewing' => 0, 'finished' => 0, 'closed' => 0, 'total' => 0); - if($story->status == 'draft') $productStories[$story->product]['draft'] += $story->c; - if($story->status == 'active') $productStories[$story->product]['active'] += $story->c; - if($story->status == 'changing') $productStories[$story->product]['changing'] += $story->c; - if($story->status == 'reviewing') $productStories[$story->product]['reviewing'] += $story->c; - if($story->status == 'closed') $productStories[$story->product]['closed'] += $story->c; - $productStories[$story->product]['total'] += $story->c; + if(!isset($productStories[$story->product][$story->type])) $productStories[$story->product][$story->type] = array('draft' => 0, 'active' => 0, 'changing' => 0, 'reviewing' => 0, 'finished' => 0, 'closed' => 0, 'total' => 0); + if($story->status == 'draft') $productStories[$story->product][$story->type]['draft'] += $story->c; + if($story->status == 'active') $productStories[$story->product][$story->type]['active'] += $story->c; + if($story->status == 'changing') $productStories[$story->product][$story->type]['changing'] += $story->c; + if($story->status == 'reviewing') $productStories[$story->product][$story->type]['reviewing'] += $story->c; + if($story->status == 'closed') $productStories[$story->product][$story->type]['closed'] += $story->c; + $productStories[$story->product][$story->type]['total'] += $story->c; - if($story->status == 'closed' && $story->closedReason == 'done') $productStories[$story->product]['finished'] += $story->c; + if($story->status == 'closed' && $story->closedReason == 'done') $productStories[$story->product][$story->type]['finished'] += $story->c; } return $productStories; diff --git a/module/product/ui/all.html.php b/module/product/ui/all.html.php index a78b529805..6c4df709f6 100644 --- a/module/product/ui/all.html.php +++ b/module/product/ui/all.html.php @@ -140,6 +140,7 @@ dtable set::checkable($canBatchEdit), set::sortLink(createLink('product', 'all', "browseType={$browseType}&orderBy={name}_{sortType}&recTotal={$recTotal}&recPerPage={$recPerPage}")), set::orderBy($orderBy), + set::plugins(array('header-group')), $canBatchEdit ? set::footToolbar ( item diff --git a/module/product/ui/browse.html.php b/module/product/ui/browse.html.php index 2d7718d8eb..d25e458060 100644 --- a/module/product/ui/browse.html.php +++ b/module/product/ui/browse.html.php @@ -15,8 +15,15 @@ namespace zin; data('storyType', $storyType); data('activeMenuID', $storyType); jsVar('URChanged', $this->lang->story->URChanged); +jsVar('gradeGroup', $gradeGroup); +jsVar('showGrade', $showGrade); +jsVar('oldShowGrades', $showGrades); +jsVar('storyType', $storyType); +jsVar('tab', $app->tab); jsVar('vision', $config->vision); +jsVar('window.globalSearchType', $storyType); +$viewType = $this->cookie->storyViewType ? $this->cookie->storyViewType : 'tree'; $storyCommon = $storyType == 'requirement' ? $lang->URCommon : $lang->SRCommon; $isProjectStory = $this->app->rawModule == 'projectstory'; $projectHasProduct = $isProjectStory && !empty($project->hasProduct); @@ -54,13 +61,13 @@ $fnGenerateSideBar = function() use ($moduleTree, $moduleID, $productID, $branch }; /* Build create story button. */ -$fnBuildCreateStoryButton = function() use ($lang, $product, $isProjectStory, $storyType, $productID, $branch, $moduleID, $projectID, $projectProducts) +$fnBuildCreateStoryButton = function() use ($lang, $product, $isProjectStory, $storyType, $productID, $branch, $moduleID, $projectID, $project, $projectProducts) { if(!common::canModify('product', $product)) return null; global $app; - $createLink = createLink('story', 'create', "product=" . (empty($productID) ? current(array_keys($projectProducts)) : $productID) . "&branch=$branch&moduleID=$moduleID&storyID=0&projectID=$projectID&bugID=0&planID=0&todoID=0&extra=&storyType=$storyType") . ($isProjectStory ? '#app=project' : ''); - $batchCreateLink = createLink('story', 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=0&project=$projectID&plan=0&storyType=$storyType"). ($isProjectStory ? '#app=project' : ''); + $createLink = createLink($storyType, 'create', "product=" . (empty($productID) ? current(array_keys($projectProducts)) : $productID) . "&branch=$branch&moduleID=$moduleID&storyID=0&projectID=$projectID&bugID=0&planID=0&todoID=0&extra=&storyType=$storyType") . ($isProjectStory ? '#app=project' : ''); + $batchCreateLink = createLink($storyType, 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=0&project=$projectID&plan=0&storyType=$storyType"). ($isProjectStory ? '#app=project' : ''); $createBtnLink = ''; $createBtnTitle = ''; @@ -91,12 +98,32 @@ $fnBuildCreateStoryButton = function() use ($lang, $product, $isProjectStory, $s $link = $this->createLink('tutorial', 'wizard', "module=story&method=create¶ms=$wizardParams"); $items[] = array('text' => $lang->story->createCommon, 'url' => $link); } - else + elseif(!$isProjectStory) { $items[] = array('text' => $lang->story->create, 'url' => $createLink); } - $items[] = array('text' => $lang->story->batchCreate, 'url' => $batchCreateLink); + if($isProjectStory) + { + $batchItems[] = array('text' => $lang->SRCommon, 'url' => $batchCreateLink); + if(str_contains($project->storyType, 'requirement')) + { + if(common::hasPriv('requirement', 'create')) $items[] = array('text' => $lang->requirement->create, 'url' => createLink('requirement', 'create', "product=$productID&branch=$branch&moduleID=$moduleID&requirementID=0&projectID=$projectID") . '#app=project'); + if(common::hasPriv('requirement', 'batchCreate')) $batchItems[] = array('text' => $lang->URCommon, 'url' => createLink('requirement', 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&requirementID=0&project=$projectID") . '#app=project'); + } + + if(str_contains($project->storyType, 'epic')) + { + if(common::hasPriv('epic', 'create')) $items[] = array('text' => $lang->epic->create, 'url' => createLink('epic', 'create', "product=$productID&branch=$branch&moduleID=$moduleID&epicID=0&projectID=$projectID") . '#app=project'); + if(common::hasPriv('epic', 'batchCreate')) $batchItems[] = array('text' => $lang->ERCommon, 'url' => createLink('epic', 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&epicID=0&project=$projectID") . '#app=project'); + } + + $items[] = array('text' => $lang->story->batchCreate, 'items' => $batchItems); + } + else + { + $items[] = array('text' => $lang->story->batchCreate, 'url' => $batchCreateLink); + } return btnGroup ( @@ -144,8 +171,6 @@ $fnBuildLinkStoryButton = function() use($lang, $app, $product, $projectHasProdu ))); } - if($storyType == 'requirement') $lang->execution->linkStory = str_replace($lang->SRCommon, $lang->URCommon, $lang->execution->linkStory); - $canLinkStory = common::hasPriv('projectstory', 'linkStory'); $canlinkPlanStory = !empty($product) && common::hasPriv('projectstory', 'importPlanStories') && $storyType == 'story' && !$project->charter; $linkStoryUrl = $this->createLink('projectstory', 'linkStory', "project=$project->id&browseType=¶m=0&orderBy=id_desc&recPerPage=50&pageID=1&extra=&storyType=$storyType"); @@ -181,8 +206,9 @@ $config->story->dtable->fieldList['title']['title'] = $lang->story->title; if($app->rawModule == 'projectstory') $config->story->dtable->fieldList['title']['link'] = array('url' => helper::createLink('projectstory', 'view', 'storyID={id}&projectID={project}')); $setting = $this->loadModel('datatable')->getSetting('product', 'browse', false, $storyType); -if($storyType == 'requirement') unset($setting['plan'], $setting['stage'], $setting['taskCount'], $setting['bugCount'], $setting['caseCount']); +if($storyType != 'story') unset($setting['taskCount'], $setting['bugCount'], $setting['caseCount']); if($storyType == 'story' && $config->edition == 'ipd') unset($setting['roadmap']); +if($viewType == 'tiled') $setting['title']['nestedToggle'] = false; $cols = array_values($setting); /* DataTable data. */ @@ -195,39 +221,39 @@ foreach($stories as $story) $story->rawModule = $story->module; $story->from = $app->tab; $options['branches'] = zget($branchOptions, $story->product, array()); - - $data[] = $this->story->formatStoryForList($story, $options, $storyType); - if(!isset($story->children)) continue; - - /* Children. */ - if($config->vision != 'or') - { - foreach($story->children as $key => $child) - { - if($app->rawModule == 'projectstory' && $child->project != $story->project) continue; - $child->rawModule = $child->module; - $data[] = $this->story->formatStoryForList($child, $options, $storyType); - } - } + $data[] = $this->story->formatStoryForList($story, $options, $storyType, $maxGradeGroup); } /* Generate toolbar of DataTable footer. */ -$fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, $storyType, $browseType, $isProjectStory, $projectHasProduct, $storyProductID, $projectID, $branch, $users, $branchTagOption, $modules, $plans, $branchID, $config, $noclosedRoadmaps) +$fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, $storyType, $browseType, $isProjectStory, $projectHasProduct, $storyProductID, $projectID, $branch, $users, $branchTagOption, $modules, $plans, $branchID, $gradePairs, $config,$noclosedRoadmaps) { /* Flag variables of permissions. */ - $canBeChanged = common::canModify('product', $product); - $canBatchEdit = $canBeChanged && hasPriv($storyType, 'batchEdit'); - $canBatchClose = hasPriv($storyType, 'batchClose') && strtolower($browseType) != 'closedbyme' && strtolower($browseType) != 'closedstory'; - $canBatchReview = $canBeChanged && hasPriv($storyType, 'batchReview'); - $canBatchChangeStage = $canBeChanged && hasPriv('story', 'batchChangeStage') && $storyType == 'story'; - $canBatchChangeBranch = $canBeChanged && hasPriv($storyType, 'batchChangeBranch') && $product && $product->type != 'normal' && $productID; - $canBatchChangeModule = $canBeChanged && hasPriv($storyType, 'batchChangeModule') && $productID && (($product->type != 'normal' && $branchID != 'all') || $product->type == 'normal') && !$isProjectStory; - $canBatchChangePlan = $canBeChanged && hasPriv('story', 'batchChangePlan') && $storyType == 'story' && (!$isProjectStory || $projectHasProduct || ($isProjectStory && isset($project->model) && $project->model == 'scrum')) && $productID && $product && (($product->type != 'normal' && $branchID != 'all') || $product->type == 'normal'); - $canBatchChangeRoadmap = $canBeChanged && hasPriv('story', 'batchChangeRoadmap') && $config->vision == 'or'; - $canBatchAssignTo = $canBeChanged && hasPriv($storyType, 'batchAssignTo'); - $canBatchUnlink = $canBeChanged && $projectHasProduct && hasPriv('projectstory', 'batchUnlinkStory'); - $canBatchImportToLib = $canBeChanged && $isProjectStory && in_array($this->config->edition, array('max', 'ipd')) && hasPriv('story', 'batchImportToLib') && helper::hasFeature('storylib'); - $canBatchAction = $canBatchEdit || $canBatchClose || $canBatchReview || $canBatchChangeStage || $canBatchChangeModule || $canBatchChangePlan || $canBatchAssignTo || $canBatchUnlink || $canBatchImportToLib || $canBatchChangeBranch || $canBatchChangeRoadmap; + $canBeChanged = common::canModify('product', $product); + if($isProjectStory) + { + $canBatchClose = hasPriv('projectstory', 'batchClose') && strtolower($browseType) != 'closedbyme'; + $canBatchEdit = hasPriv('projectstory', 'batchEdit'); + $canBatchReview = hasPriv('projectstory', 'batchReview'); + $canBatchAssignTo = hasPriv('projectstory', 'batchAssignTo'); + $canBatchChangePlan = hasPriv('projectstory', 'batchChangePlan') && $productID && $product; + } + else + { + $canBatchEdit = $canBeChanged && hasPriv($storyType, 'batchEdit'); + $canBatchClose = hasPriv($storyType, 'batchClose') && strtolower($browseType) != 'closedbyme' && strtolower($browseType) != 'closedstory'; + $canBatchReview = $canBeChanged && hasPriv($storyType, 'batchReview'); + $canBatchAssignTo = $canBeChanged && hasPriv($storyType, 'batchAssignTo'); + $canBatchChangePlan = $canBeChanged && hasPriv($storyType, 'batchChangePlan') && $productID && $product && (($product->type != 'normal' && $branchID != 'all') || $product->type == 'normal'); + } + + $canBatchChangeGrade = $canBeChanged && hasPriv($storyType, 'batchChangeGrade'); + $canBatchChangeStage = $canBeChanged && hasPriv('story', 'batchChangeStage') && $storyType == 'story'; + $canBatchChangeBranch = $canBeChanged && hasPriv($storyType, 'batchChangeBranch') && $product && $product->type != 'normal' && $productID; + $canBatchChangeModule = $canBeChanged && hasPriv($storyType, 'batchChangeModule') && $productID && (($product->type != 'normal' && $branchID != 'all') || $product->type == 'normal') && !$isProjectStory; + $canBatchChangeParent = $canBeChanged && hasPriv($storyType, 'batchChangeParent'); + $canBatchUnlink = $canBeChanged && $projectHasProduct && hasPriv('projectstory', 'batchUnlinkStory'); + $canBatchImportToLib = $canBeChanged && $isProjectStory && in_array($this->config->edition, array('max', 'ipd')) && hasPriv('story', 'batchImportToLib') && helper::hasFeature('storylib'); + $canBatchAction = $canBatchEdit || $canBatchClose || $canBatchReview || $canBatchChangeGrade || $canBatchChangeStage || $canBatchChangeModule || $canBatchChangePlan || $canBatchChangeParent || $canBatchAssignTo || $canBatchUnlink || $canBatchImportToLib || $canBatchChangeBranch; /* Remove empty data from data list. */ unset($lang->story->reviewResultList[''], $lang->story->reviewResultList['revert']); @@ -235,31 +261,34 @@ $fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, unset($plans[''], $lang->story->stageList[''], $users['']); /* Generate dropdown menu items for the DataTable footer toolbar.*/ - $planItems = $planItems ?? array(); - foreach($lang->story->reviewResultList as $key => $result) $reviewResultItems[$key] = array('text' => $result, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchReview', "result=$key")); - foreach($lang->story->reasonList as $key => $reason) $reviewRejectItems[] = array('text' => $reason, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchReview', "result=reject&reason=$key")); - foreach($branchTagOption as $branchID => $branchName) $branchItems[] = array('text' => $branchName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchChangeBranch', "branchID=$branchID")); - foreach($modules as $moduleID => $moduleName) $moduleItems[] = array('text' => $moduleName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchChangeModule', "moduleID=$moduleID")); - foreach($plans as $planID => $planName) $planItems[] = array('text' => $planName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchChangePlan', "planID=$planID")); + $planItems = $planItems ?? array(); + $gradeItems = array(); + foreach($lang->story->reviewResultList as $key => $result) $reviewResultItems[$key] = array('text' => $result, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchReview', "result=$key")); + foreach($gradePairs as $key => $result) $gradeItems[] = array('text' => $result, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchChangeGrade', "result=$key")); + foreach($lang->story->reasonList as $key => $reason) $reviewRejectItems[] = array('text' => $reason, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchReview', "result=reject&reason=$key")); + foreach($branchTagOption as $branchID => $branchName) $branchItems[] = array('text' => $branchName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchChangeBranch', "branchID=$branchID")); + foreach($modules as $moduleID => $moduleName) $moduleItems[] = array('text' => $moduleName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchChangeModule', "moduleID=$moduleID")); + foreach($plans as $planID => $planName) $planItems[] = array('text' => $planName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($isProjectStory ? 'projectstory' : $storyType, 'batchChangePlan', "planID=$planID")); foreach($noclosedRoadmaps as $roadmapID => $roadmapName) $roadmapItems[] = array('text' => empty($roadmapName) ? $lang->null : $roadmapName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchChangeRoadmap', "roadmapID=$roadmapID")); foreach($lang->story->stageList as $key => $stageName) { - if(!str_contains('|tested|verified|released|closed|', "|$key|")) continue; + if(!str_contains('|tested|verified|rejected|pending|released|closed|', "|$key|")) continue; $stageItems[] = array('text' => $stageName, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchChangeStage', "stage=$key")); } foreach($users as $account => $realname) { if($account == 'closed') continue; - $assignItems[] = array('text' => $realname, 'class' => 'batch-btn', 'data-formaction' => $this->createLink('story', 'batchAssignTo', "productID={$productID}"), 'data-account' => $account); + $assignItems[] = array('text' => $realname, 'class' => 'batch-btn', 'data-formaction' => $this->createLink($storyType, 'batchAssignTo', "productID={$productID}"), 'data-account' => $account); } if(isset($reviewResultItems['reject'])) $reviewResultItems['reject'] = array('class' => 'not-hide-menu', 'text' => $lang->story->reviewResultList['reject'], 'items' => $reviewRejectItems); $reviewResultItems = array_values($reviewResultItems); $navActionItems = array(); - if($canBatchClose) $navActionItems[] = array('class' => 'batch-btn batchClostBtn', 'text' => $lang->close, 'data-page' => 'batch', 'data-formaction' => helper::createLink('story', 'batchClose', "productID={$productID}&executionID=0&storyType={$storyType}")); - if($canBatchReview) $navActionItems[] = array('class' => 'not-hide-menu batchReviewBtn', 'text' => $lang->story->review, 'items' => $reviewResultItems); + if($canBatchClose) $navActionItems[] = array('class' => 'batch-btn batchClostBtn', 'text' => $lang->close, 'data-page' => 'batch', 'data-formaction' => helper::createLink($storyType, 'batchClose', "productID={$productID}&executionID=0")); + if($canBatchChangeGrade) $navActionItems[] = array('class' => 'not-hide-menu batchGradeBtn', 'text' => $lang->story->grade, 'items' => $gradeItems); + if($canBatchReview) $navActionItems[] = array('class' => 'not-hide-menu batchReviewBtn', 'text' => $lang->story->review, 'items' => $reviewResultItems); if($canBatchChangeStage) $navActionItems[] = array('class' => 'not-hide-menu batchChangeStageBtn', 'text' => $lang->story->stageAB, 'items' => $stageItems); if(!$canBatchAction) return array(); @@ -275,7 +304,7 @@ $fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, 'className' => 'secondary batch-btn', 'disabled' => ($canBatchEdit ? '': 'disabled'), 'data-page' => 'batch', - 'data-formaction' => $this->createLink('story', 'batchEdit', "productID=$storyProductID&projectID=$projectID&branch=$branch&storyType=$storyType") + 'data-formaction' => $this->createLink($storyType, 'batchEdit', "productID=$storyProductID&projectID=$projectID&branch=$branch&type=$storyType") ), /* Popup menu trigger icon. */ array('caret' => 'up', 'className' => 'size-sm secondary', 'items' => $navActionItems, 'data-toggle' => 'dropdown', 'data-placement' => 'top-start') @@ -295,6 +324,8 @@ $fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, ($canBatchChangeBranch && $product->type != 'normal') ? array('caret' => 'up', 'text' => $lang->product->branchName[$product->type], 'className' => 'batchChangeBranchBtn', 'items' => $branchItems, 'type' => 'dropdown', 'data-placement' => 'top-start', 'data-menu' => array('searchBox' => true)) : null, /* AssignedTo button. */ array('caret' => 'up', 'text' => $lang->story->assignedTo, 'className' => ($canBatchAssignTo ? 'secondary batchAssignToBtn' : 'hidden'), 'items' => $assignItems, 'type' => 'dropdown', 'data-placement' => 'top-start', 'data-menu' => array('searchBox' => true)), + /* Change parent button. */ + array('text' => $lang->story->changeParent, 'className' => $canBatchChangeParent ? 'secondary batchChangeParentBtn' : 'hidden', 'data-toggle' => 'modal', 'url' => createLink($storyType, 'batchChangeParent', "productID=$productID&storyType=$storyType")), /* Batch import to lib button .*/ $canBatchImportToLib && $storyType != 'requirement' ? array('text' => $lang->story->importToLib, 'className' => 'btn secondary batchImportToLibBtn', 'id' => 'importToLib', 'data-toggle' => 'modal', 'url' => '#batchImportToLib', 'data-on' => 'click', 'data-call' => 'importToLib') : null ); @@ -309,6 +340,10 @@ $fnGenerateFootToolbar = function() use ($lang, $product, $productID, $project, /* Layout. */ global $app; if($app->rawModule == 'projectstory' && !empty($project)) dropmenu(set::text($project->name)); +$checkedSummary = $lang->product->checkedSRSummary; +if($storyType == 'requirement') $checkedSummary = $lang->product->checkedURSummary; +if($storyType == 'epic') $checkedSummary = $lang->product->checkedERSummary; +if($isProjectStory) $checkedSummary = $lang->product->checkedAllSummary; data('storyBrowseType', $storyBrowseType); @@ -316,27 +351,59 @@ jsVar('childrenAB', $lang->story->childrenAB); jsVar('projectID', $projectID); jsVar('modulePairs', $modulePairs); jsVar('storyType', $storyType); -jsVar('checkedSummary', $storyType == 'story' ? $lang->product->checkedSRSummary : $lang->product->checkedURSummary); - -if($isProjectStory and $storyType == 'requirement') -{ - unset($lang->projectstory->featureBar['story']['linkedexecution']); - unset($lang->projectstory->featureBar['story']['unlinkedexecution']); -} +jsVar('checkedSummary', $checkedSummary); $queryMenuLink = createLink($app->rawModule, $app->rawMethod, $projectIDParam . "productID=$productID&branch=$branch&browseType=bySearch¶m={queryID}"); featureBar ( + ($showGrade && ($storyType != 'story' || $isProjectStory)) ? to::leading + ( + picker + ( + set::tree(true), + set::name('showGrades'), + set::items($gradeMenu), + set::search(false), + set::multiple(true), + set::width('150px'), + setStyle('justify-content', 'center'), + set::display($lang->story->viewAllGrades), + set::menu(array('checkbox' => true)), + set::value($showGrades), + set::onPopHidden(jsRaw('setShowGrades')) + ) + ) : null, set::current($storyBrowseType), set::link(createLink($app->rawModule, $app->rawMethod, $projectIDParam . "productID=$productID&branch=$branch&browseType={key}¶m=$param&storyType=$storyType&orderBy=$orderBy&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&projectID=$projectID")), set::queryMenuLinkCallback(array(fn($key) => str_replace('{queryID}', (string)$key, $queryMenuLink))), li(searchToggle(set::open($browseType == 'bysearch' || $storyBrowseType == 'bysearch'), set::module($config->product->search['module']))) ); +$canExport = $isProjectStory ? hasPriv('projectstory', 'export') : hasPriv($storyType, 'export'); +$canReport = $isProjectStory ? hasPriv('projectstory', 'report') : hasPriv($storyType, 'report'); +$reportUrl = $isProjectStory ? helper::createLink('projectstory', 'report', "productID=$productID&branchID=$branch&storyType=$storyType&browseType=$browseType&moduleID=$moduleID&chartType=pie&projectID=$projectID") : helper::createLink($storyType, 'report', "productID=$productID&branchID=$branch&storyType=$storyType&browseType=$browseType&moduleID=$moduleID"); +$exportUrl = $isProjectStory ? helper::createLink('projectstory', 'export', "productID=$productID&orderBy=$orderBy&executionID=$projectID&browseType=$browseType") : helper::createLink($storyType, 'export', "productID=$productID&orderBy=$orderBy&executionID=$projectID&browseType=$browseType"); toolbar ( - (!hasPriv('story', 'report') || !$productID) ? null : item(set(array('id' => 'reportBtn', 'icon' => 'bar-chart', 'class' => 'ghost', 'url' => helper::createLink('story', 'report', "productID=$productID&branchID=$branch&storyType=$storyType&browseType=$browseType&moduleID=$moduleID&chartType=pie&projectID=$projectID") . ($app->tab == 'project' ? '#app=project' : '')))), - !hasPriv('story', 'export') ? null : item(set(array('id' => 'exportBtn', 'icon' => 'export', 'class' => 'ghost', 'url' => helper::createLink('story', 'export', "productID=$productID&orderBy=$orderBy&executionID=$projectID&browseType=$browseType&storyType=$storyType"), 'data-toggle' => 'modal'))), + item(set(array + ( + 'type' => 'btnGroup', + 'items' => array(array + ( + 'icon' => 'list', + 'class' => 'btn-icon switchButton' . ($viewType == 'tiled' ? ' text-primary' : ''), + 'data-type' => 'tiled', + 'hint' => $lang->story->viewTypeList['tiled'] + ), array + ( + 'icon' => 'treeview', + 'class' => 'switchButton btn-icon' . ($viewType == 'tree' ? ' text-primary' : ''), + 'data-type' => 'tree', + 'hint' => $lang->story->viewTypeList['tree'] + )) + ))), + (!$canReport || !$productID) ? null : item(set(array('id' => 'reportBtn', 'icon' => 'bar-chart', 'class' => 'ghost', 'url' => $reportUrl))), + !$canExport ? null : item(set(array('id' => 'exportBtn', 'icon' => 'export', 'class' => 'ghost', 'url' => $exportUrl, 'data-toggle' => 'modal'))), $fnBuildCreateStoryButton(), $fnBuildLinkStoryButton() ); @@ -346,10 +413,13 @@ $fnGenerateSideBar(); $footToolbar = $fnGenerateFootToolbar(); $sortLink = createLink('product', 'browse', "productID={$productID}&branch={$branch}&browseType={$browseType}¶m={$param}&storyType={$storyType}&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}&projectID=$projectID"); if($this->app->rawModule == 'projectstory') $sortLink = createLink('projectstory', 'story', "projectID={$projectID}&productID={$productID}&branch=$branch&browseType=$browseType¶m=$param&storyType=$storyType&orderBy={name}_{sortType}&recTotal={$pager->recTotal}&recPerPage={$pager->recPerPage}&pageID={$pager->pageID}"); + +$emptyTip = $lang->story->noStory; +if($storyType == 'requirement') $emptyTip = $lang->story->noRequirement; +if($storyType == 'epic') $emptyTip = $lang->story->noEpic; dtable ( set::id('stories'), - set::rowKey('uniqueID'), set::userMap($users), set::customCols(array('url' => createLink('datatable', 'ajaxcustom', "module={$app->moduleName}&method={$app->methodName}&extra={$storyType}"), 'globalUrl' => createLink('datatable', 'ajaxsaveglobal', "module={$app->moduleName}&method={$app->methodName}&extra={$storyType}"))), set::checkable(!empty($footToolbar)), // The user can do batch action if this parameter is not false(true, null). @@ -361,9 +431,9 @@ dtable set::checkInfo(jsRaw("function(checkedIdList){return window.setStatistics(this, checkedIdList, '{$summary}');}")), set::footPager(usePager()), set::footToolbar($footToolbar), - set::emptyTip($storyType == 'story' ? $lang->story->noStory : $lang->story->noRequirement), + set::emptyTip($emptyTip), set::createTip($lang->story->create), - set::createLink(hasPriv($storyType, 'create') ? createLink('story', 'create', "product=$productID&branch=$branch&moduleID=$moduleID&storyID=0&projectID=$projectID&bugID=0&planID=0&todoID=0&extra=&storyType=$storyType") . ($isProjectStory ? '#app=project' : '') : '') + set::createLink(hasPriv($storyType, 'create') ? createLink($storyType, 'create', "product=$productID&branch=$branch&moduleID=$moduleID&storyID=0&projectID=$projectID&bugID=0&planID=0&todoID=0&extra=&storyType=$storyType") . ($isProjectStory ? '#app=project' : '') : '') ); modal(set::id('#batchUnlinkStoryBox')); @@ -372,6 +442,41 @@ $linkStoryByPlanTips = $lang->execution->linkNormalStoryByPlanTips; if($product && $product->type != 'normal') $linkStoryByPlanTips = sprintf($lang->execution->linkBranchStoryByPlanTips, $lang->product->branchName[$product->type]); if($isProjectStory) $linkStoryByPlanTips = str_replace($lang->execution->common, $lang->projectCommon, $linkStoryByPlanTips); +if(in_array($this->config->edition, array('max', 'ipd'))) +{ + modal + ( + setID('batchImportToLib'), + set::title($lang->story->importToLib), + form + ( + set::action($this->createLink('story', 'batchImportToLib')), + formGroup + ( + set::label($lang->story->lib), + picker + ( + set::name('lib'), + set::items($libs), + set::required(true) + ) + ), + (!hasPriv('assetlib', 'approveStory') && !hasPriv('assetlib', 'batchApproveStory')) ? formGroup + ( + set::label($lang->story->approver), + picker + ( + set::name('assignedTo'), + set::items($approvers) + ) + ) : null, + set::submitBtnText($lang->import), + formHidden('storyIdList', ''), + set::actions(array('submit')) + ) + ); +} + modal ( setID('linkStoryByPlan'), diff --git a/module/product/ui/track.html.php b/module/product/ui/track.html.php index 9a3e591631..e25390087b 100644 --- a/module/product/ui/track.html.php +++ b/module/product/ui/track.html.php @@ -12,232 +12,129 @@ declare(strict_types=1); namespace zin; -if($config->URAndSR && $this->app->rawModule == 'projectstory' && $this->session->hasProduct) +$viewByTypePairs = array(); +foreach($storyTypeList as $type => $typeName) $viewByTypePairs[$type] = sprintf($lang->story->viewByType, $typeName); + +$storyTypeLang = $storyTypeList[$storyType]; +$paramTemplate = "productID={$productID}&branch={$branch}&projectID={$projectID}&browseType=allstory¶m=0&storyType=%s&orderBy=%s"; +if($app->rawModule == 'projectstory') $paramTemplate = "projectID={$projectID}&productID={$productID}&branch={$branch}&browseType=allstory¶m=0&storyType=%s&orderBy=%s"; + +$orderByItems = array(); +$orderByTitle = ''; +if($storyType != 'story') unset($lang->story->trackOrderByList['stage']); +foreach($lang->story->trackOrderByList as $orderByType => $orderByName) { - $productItems = array(); - foreach($projectProducts as $id => $name) + $item = array(); + $item['text'] = $orderByName; + $item['selected'] = strpos($orderBy, $orderByType) === 0; + $item['items'] = array(); + $item['items'][] = array('text' => $lang->story->trackSortList['asc'], 'selected' => $orderBy == "{$orderByType}_asc", 'url' => createLink($app->rawModule, 'track', sprintf($paramTemplate, $storyType, "{$orderByType}_asc"))); + $item['items'][] = array('text' => $lang->story->trackSortList['desc'], 'selected' => $orderBy == "{$orderByType}_desc", 'url' => createLink($app->rawModule, 'track', sprintf($paramTemplate, $storyType, "{$orderByType}_desc"))); + $orderByItems[] = $item; + + if($item['selected']) { - $productItems[] = array('text' => $name, 'url' => createLink('projectstory', 'track', "projectID={$this->session->project}&productID={$id}"), 'active' => $productID == $id); + $orderByTitle = $orderByName; + if($orderBy == "{$orderByType}_asc") $orderByTitle .= $lang->story->trackSortList['asc']; + if($orderBy == "{$orderByType}_desc") $orderByTitle .= $lang->story->trackSortList['desc']; } } -$getRequirements = function($tracks) -{ - global $app, $config, $lang; - $requirementItems = array(); - $tab = $app->rawModule == 'projectstory' ? 'project' : 'product'; - $module = $app->rawModule == 'projectstory' ? 'projectstory' : 'story'; - foreach($tracks as $key => $requirement) - { - $track = ($key == 'noRequirement') ? $requirement : $requirement->track; - $rowspan = count($track); - $title = $lang->story->noRequirement; - if($config->URAndSR && $key != 'noRequirement') $title = common::hasPriv($requirement->type, 'view') ? a( - set('href', createLink('story', 'view', "storyID={$requirement->id}")), - set('title', $requirement->title), - set('data-app', $tab), - $requirement->title - ) : $requirement->title; +$dropdownItems = array(); +foreach($viewByTypePairs as $type => $typeName) $dropdownItems[] = array('text' => $typeName, 'selected' => $storyType == $type, 'url' => createLink($app->rawModule, 'track', sprintf($paramTemplate, $type, ($type != 'story' && strpos($orderBy, 'stage') === 0) ? 'id_desc' : $orderBy))); - $requirementItems[] = h::tr( - $config->URAndSR ? h::td( - $rowspan != 0 ? set('rowspan', $rowspan) : null, - setClass('requirement'), - $key != 'noRequirement' ? label( - setClass('primary-pale ring-primary mr-1'), - zget($lang->story->statusList, $requirement->status) - ) : null, - set('title', $key != 'noRequirement' ? $requirement->title : $lang->story->noRequirement), - $title - ) : null, - count($track) != 0 ? getStoryTrack($track, $tab, $module) : null - ); - } - return $requirementItems; -}; - -function getStoryTrack($track, $tab, $module) -{ - $i = 0; - $storyItems = array(); - foreach($track as $storyID => $story) - { - if($i > 0) - { - $storyItems[] = h::tr(getTrackTd($storyID, $story, $tab, $module)); - } - else - { - $storyItems[] = getTrackTd($storyID, $story, $tab, $module); - } - $i ++; - } - return $storyItems; -}; - -function getTrackTd($storyID, $story, $tab, $module) -{ - global $lang, $config; - $trackItem = array(); - - /* Story. */ - $trackItem[] = h::td( - isset($story->parent) && $story->parent > 0 ? label( - setClass('rounded-full light mr-1'), - set('title', $lang->story->children), - $lang->story->childrenAB - ) : null, - a( - set('href', createLink($module, 'view', "storyID={$storyID}")), - set('title', $story->title), - set('data-app', $tab), - $story->title - ) - ); - - $trackItem[] = h::td(getTaskTd($story->tasks)); // Task - - if(in_array($config->edition, array('max', 'ipd'))) $trackItem[] = h::td(getDesignTd($story->designs)); // Design - - $trackItem[] = h::td(getCaseTd($story->cases)); // Case - - if(in_array($config->edition, array('max', 'ipd')) && helper::hasFeature('devops')) $trackItem[] = h::td(getRevisionTd($story->revisions)); // Revision - - $trackItem[] = h::td(getBugTd($story->bugs)); // Bug - - return $trackItem; -}; - -function getTaskTd($tasks) -{ - $taskItems = array(); - foreach($tasks as $task) - { - $taskItems[] = a( - set('href', createLink('task', 'view', "taskID={$task->id}")), - set('title', $task->name), - $task->name - ); - $taskItems[] = br(); - } - return $taskItems; -}; - -function getDesignTd($designs) -{ - $designItems = array(); - foreach($designs as $design) - { - $designItems[] = a( - set('href', createLink('design', 'view', "designID={$design->id}")), - set('title', $design->name), - $design->name - ); - $designItems[] = br(); - } - return $designItems; -}; - -function getCaseTd($cases) -{ - $caseItems = array(); - foreach($cases as $case) - { - $caseItems[] = a( - set('href', createLink('testcase', 'view', "caseID={$case->id}")), - set('title', $case->title), - $case->title - ); - $caseItems[] = br(); - } - return $caseItems; -}; - -function getRevisionTd($revisions) -{ - $revisionItems = array(); - foreach($revisions as $revision => $repoComment) - { - $revisionItems[] = a( - set('href', createLink('design', 'revision', "revisionID={$revision}")), - set('title', $repoComment), - '#'. $revision . '-' . $repoComment - ); - $revisionItems[] = br(); - } - return $revisionItems; -}; - -function getBugTd($bugs) -{ - $bugItems = array(); - foreach($bugs as $bug) - { - $bugItems[] = a( - set('href', createLink('bug', 'view', "bugID={$bug->id}")), - set('title', $bug->title), - $bug->title - ); - $bugItems[] = br(); - } - return $bugItems; -}; - -$colspan = 4; -if($config->URAndSR) $colspan ++; -if(in_array($config->edition, array('max', 'ipd'))) $colspan ++; -if(in_array($config->edition, array('max', 'ipd')) && helper::hasFeature('devops')) $colspan ++; -div +featureBar ( - setClass('main-col'), - div( - setClass('main-table'), - h::table + to::leading + ( + ($app->rawModule == 'projectstory' && count($projectProducts) > 1) ? picker ( - setID('trackList'), - setClass('table table-bordered'), - h::thead - ( - $config->URAndSR ? h::th( - !empty($productItems) ? dropdown( - btn( - setClass('ghost btn square btn-default'), - set::icon('product'), - $projectProducts[$productID] - ), - set::items($productItems), - set::placement('bottom-start') - ) : $lang->story->requirement - ) : null, - h::th($lang->story->story), - h::th($lang->story->tasks), - in_array($config->edition, array('max', 'ipd')) ? h::th($lang->story->design) : null, - h::th($lang->story->case), - in_array($config->edition, array('max', 'ipd')) && helper::hasFeature('devops') ? h::th($lang->story->repoCommit) : null, - h::th($lang->story->bug) - ), - h::tbody - ( - !empty($tracks) ? $getRequirements($tracks) : h::tr - ( - h::td - ( - setClass('text-gray text-center empty-tip'), - set::colspan($colspan), - $lang->product->noData - ) - ) - ) - ), - !empty($tracks) ? div + setID('switchProduct'), + set::name('switchProduct'), + set::items(array(0 => $this->lang->product->all) + $projectProducts), + set::value($productID), + set::required(true), + on::change('changeProduct'), + set::width(145) + ) : null, + count($viewByTypePairs) > 1 ? dropdown ( - setClass('table-footer'), - pager( - set::_className('flex justify-end items-center'), - set::linkCreator(createLink($app->rawModule, $app->rawMethod, "productID={$productID}&branch={$branch}&projectID={$projectID}&recTotal={$pager->recTotal}&recPerPage={recPerPage}&pagerID={page}")) - ) + to('trigger', btn(setClass('switchBtn'), $viewByTypePairs[$storyType])), + set::items($dropdownItems), ) : null + ), + li(searchToggle(set::open($browseType == 'bysearch'), set::module($config->product->search['module']), set::text($lang->searchAB . $storyTypeLang))) +); + +toolbar +( + formSettingBtn + ( + set::customFields($customFields), + set::noCancel(true), + set::canGlobal(commonModel::hasPriv('datatable', 'setGlobal')), + set::urlParams("module=product§ion=trackFields&key={$storyType}"), + set::submitCallback("loadCurrentPage"), + set::restoreCallback("loadCurrentPage"), + set::text($lang->settings) ) ); -render(); +$privs['epic'] = commonModel::hasPriv('epic', 'view'); +$privs['requirement'] = commonModel::hasPriv('requirement', 'view'); +$privs['story'] = commonModel::hasPriv('story', 'view'); +$privs['project'] = commonModel::hasPriv('project', 'view'); +$privs['execution'] = commonModel::hasPriv('execution', 'task'); +$privs['task'] = commonModel::hasPriv('task', 'view'); +$privs['bug'] = commonModel::hasPriv('bug', 'view'); +$privs['case'] = commonModel::hasPriv('testcase', 'view'); +$privs['design'] = commonModel::hasPriv('design', 'view'); +$privs['commit'] = commonModel::hasPriv('repo', 'revision'); + +$app->loadLang('epic'); +$app->loadLang('requirement'); +$app->loadLang('project'); +$app->loadLang('task'); +$app->loadLang('bug'); +$app->loadLang('testcase'); +jsVar('langStoryPriList', array('epic' => $lang->epic->priList, 'requirement' => $lang->requirement->priList, 'story' => $lang->story->priList)); +jsVar('langStoryStatusList', array('epic' => $lang->epic->statusList, 'requirement' => $lang->requirement->statusList, 'story' => $lang->story->statusList)); +jsVar('langStoryStageList', array('epic' => $lang->epic->stageList, 'requirement' => $lang->requirement->stageList, 'story' => $lang->story->stageList)); +jsVar('langProjectStatusList', $lang->project->statusList); +jsVar('langTaskPriList', $lang->task->priList); +jsVar('langTaskStatusList', $lang->task->statusList); +jsVar('langChildren', $lang->task->childrenAB); +jsVar('langBugPriList', $lang->bug->priList); +jsVar('langBugSeverityList', $lang->bug->severityList); +jsVar('langCasePriList', $lang->testcase->priList); +jsVar('langCaseResultList', $lang->testcase->resultList); +jsVar('langUnexecuted', $lang->testcase->unexecuted); + +jsVar('storyIdList', $storyIdList); +jsVar('projectID', $projectID); +jsVar('mergeCells', $mergeCells); +jsVar('orderByItems', $orderByItems); +jsVar('orderByTitle', $orderByTitle); +jsVar('storyType', $storyType); +jsVar('users', $users); +jsVar('privs', $privs); + +empty($tracks) ? div(setClass('dtable-empty-tip bg-white shadow'), span(setClass('text-gray'), $lang->noData)) : div +( + set::id('track'), + zui::kanbanList + ( + set::key('kanban'), + set::items(array(array( + 'data' => $tracks, + 'getLaneCol' => jsRaw('window.getLaneCol'), + 'getCol' => jsRaw('window.getCol'), + 'getItem' => jsRaw('window.getItem'), + 'itemRender' => jsRaw('window.itemRender'), + 'afterRender' => jsRaw('window.afterRender'), + 'draggable' => false + ))), + set::height('calc(100vh - 130px)') + ), + pager(setClass('justify-end')) +); diff --git a/module/product/zen.php b/module/product/zen.php index 920aea9ba1..0d6186c2fb 100644 --- a/module/product/zen.php +++ b/module/product/zen.php @@ -378,6 +378,12 @@ class productZen extends product $extendFieldList = $this->product->getFlowExtendFields(); foreach($extendFieldList as $field => $name) { + if(str_contains(strtolower($fieldName), 'epic') && !$this->config->enableER) continue; + if(str_contains(strtolower($fieldName), 'requirement') && !$this->config->URAndSR) continue; + + $fieldName = trim($fieldName); + $fieldPairs[$fieldName] = zget($productLang, $fieldName); + $extCol = $config->product->dtable->extendField; $extCol['name'] = $field; $extCol['title'] = $name; @@ -895,7 +901,7 @@ class productZen extends product * @access protected * @return array */ - protected function getStories(int $projectID, int $productID, string $branchID, int $moduleID, int $param, string $storyType, string $browseType, string $orderBy, object $pager): array + public function getStories(int $projectID, int $productID, string $branchID = '', int $moduleID = 0, int $param = 0, string $storyType = 'all', string $browseType = 'allstory', string $orderBy = 'id_desc', object $pager = null): array { /* Append id for second sort. */ $sort = common::appendOrder($orderBy); @@ -910,7 +916,7 @@ class productZen extends product $this->products = $this->product->getProducts($projectID, 'all', '', false); if($browseType == 'bybranch') $param = $branchID; - $stories = $this->story->getExecutionStories($projectID, $productID, $sort, $browseType, (string)$param, $storyType, '', $pager); + $stories = $this->story->getExecutionStories($projectID, $productID, $sort, $browseType, (string)$param, 'all', '', $pager); } else { @@ -1135,6 +1141,36 @@ class productZen extends product $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $storyType, $branch, $projectID); } + /** + * 构建矩阵搜索表单。 + * Build search form for track. + * + * @param int $productID + * @param string $branch + * @param int $projectID + * @param string $browseType + * @param int $param + * @param string $storyType + * @access protected + * @return void + */ + protected function buildSearchFormForTrack(int &$productID, string $branch, int $projectID, string $browseType, int $param, string $storyType): void + { + if($this->config->edition == 'ipd' && $storyType == 'story') unset($this->config->product->search['fields']['roadmap']); + if($this->config->edition == 'ipd' && $storyType == 'requirement') + { + $this->config->product->search['params']['roadmap']['values'] = $this->loadModel('roadmap')->getPairs($productID); + } + + /* Build search form. */ + $actionURL = $this->createLink('product', 'track', "productID={$productID}&branch={$branch}&projectID={$projectID}&browseType=bySearch¶m=myQueryID&storyType=$storyType"); + if($this->app->rawModule == 'projectstory') $actionURL = $this->createLink('projectstory', 'track', "projectID={$projectID}&productID={$productID}&branch={$branch}&browseType=bySearch¶m=myQueryID&storyType=$storyType"); + + $this->config->product->search['module'] = $this->app->rawModule . 'Track'; + $queryID = ($browseType == 'bysearch') ? (int)$param : 0; + $this->product->buildSearchForm($productID, $this->products, $queryID, $actionURL, $storyType, $branch, $projectID); + } + /** * 获取需求的ID列表。 * Get ID list of stories. @@ -1350,6 +1386,10 @@ class productZen extends product $projectProducts = $this->getProjectProductList($projectID, $isProjectStory); list($branchOpt, $branchTagOpt) = $this->getBranchAndTagOption($projectID, $product, $isProjectStory); + $gradeList = $this->loadModel('story')->getGradeList(''); + $gradeGroup = array(); + foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; + /* Set show module by config. */ $showModule = empty($this->config->product->browse->showModule) ? 0 : $this->config->product->browse->showModule; if($isProjectStory) $showModule = empty($this->config->projectstory->story->showModule) ? 0 : $this->config->projectstory->story->showModule; @@ -1360,6 +1400,11 @@ class productZen extends product $this->view->projectID = $projectID; $this->view->project = $project; $this->view->stories = $stories; + $this->view->gradeGroup = $gradeGroup; + $this->view->showGrade = $this->config->edition == 'ipd'; + $this->view->gradeMenu = $this->loadModel('story')->getGradeMenu($storyType, $project); + $this->view->gradePairs = $this->story->getGradePairs($storyType); + $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); $this->view->storyType = $storyType; $this->view->browseType = $browseType; $this->view->isProjectStory = $isProjectStory; @@ -1370,6 +1415,10 @@ class productZen extends product $this->view->branchTagOption = $branchTagOpt; $this->view->projectProducts = $projectProducts; + $module = $this->app->tab == 'product' ? $storyType : $this->app->tab; + $this->view->showGrades = isset($this->config->{$module}->showGrades) ? $this->config->{$module}->showGrades : $this->story->getDefaultShowGrades($this->view->gradeMenu); + + $storyType = $isProjectStory ? 'all' : $storyType; $this->view->summary = $this->product->summary($stories, $storyType); $this->view->plans = $this->loadModel('productplan')->getPairs($productID, isset($projectProducts[$productID]) ? array(BRANCH_MAIN) + $projectProducts[$productID]->branches : (($branch === 'all' || empty($branch)) ? '' : $branch), 'unexpired,noclosed', true); $this->view->users = $this->loadModel('user')->getPairs('noletter|pofirst|nodeleted'); @@ -1420,4 +1469,61 @@ class productZen extends product return array_values($projectList); } + + /** + * 为跟踪矩阵设置自定义列。 + * Get custom fields for track. + * + * @param string $storyType epic|requirement|story + * @access protected + * @return array + */ + public function getCustomFieldsForTrack(string $storyType): array + { + $listFields = array(); + $listFields['requirement'] = $this->lang->URCommon; + $listFields['story'] = $this->lang->SRCommon; + $listFields['project'] = $this->lang->story->project; + $listFields['execution'] = $this->lang->story->execution; + $listFields['design'] = $this->lang->story->design; + $listFields['commit'] = $this->lang->story->repoCommit; + $listFields['task'] = $this->lang->story->tasks; + $listFields['bug'] = $this->lang->story->bugs; + $listFields['case'] = $this->lang->story->cases; + + if($storyType == 'requirement' || $storyType == 'story') unset($listFields['requirement']); + if($storyType == 'story') unset($listFields['story']); + + $showFields = !isset($this->config->product->trackFields->{$storyType}) ? array_keys($listFields) : explode(',', $this->config->product->trackFields->{$storyType}); + return array('list' => $listFields, 'show' => array_merge(array($storyType), $showFields)); + } + + /** + * 获取根据矩阵可用的需求类型。 + * Get active storyType list for track. + * + * @param int $projectID + * @param int $productID + * @access protected + * @return array + */ + public function getActiveStoryTypeForTrack(int $projectID = 0, int $productID = 0): array + { + $storyTypeList = $this->lang->story->typeList; + + /* Check story type by linked story's type. */ + if($this->app->rawModule == 'projectstory' && $projectID) + { + $stories = $this->getStories($projectID, $productID); + if($stories) + { + $havingType = array(); + foreach($stories as $story) $havingType[$story->type] = $story->type; + if(!isset($havingType['epic'])) unset($storyTypeList['epic']); + if(!isset($havingType['epic']) && !isset($havingType['requirement'])) unset($storyTypeList['requirement']); + } + } + + return $storyTypeList; + } } diff --git a/module/productplan/control.php b/module/productplan/control.php index 7a3ef1af46..61fbb392f4 100644 --- a/module/productplan/control.php +++ b/module/productplan/control.php @@ -352,6 +352,8 @@ class productplan extends control */ public function view(int $planID = 0, string $type = 'story', string $orderBy = 'order_desc', string $link = 'false', string $param = '', int $recTotal = 0, int $recPerPage = 100, int $pageID = 1) { + $this->app->loadLang('requirement'); + $this->app->loadLang('epic'); $plan = $this->productplan->getByID($planID, true); if(!$plan) { @@ -389,7 +391,7 @@ class productplan extends control $this->view->modulePairs = $modulePairs; $this->view->planStories = $planStories; $this->view->planBugs = $this->loadModel('bug')->getPlanBugs($planID, 'all', $type == 'bug' ? $sort : 'id_desc', $bugPager); - $this->view->summary = $this->product->summary($planStories); + $this->view->summary = $this->productplanZen->buildViewSummary($planStories); $this->view->type = $type; $this->view->orderBy = $orderBy; $this->view->link = $link; @@ -578,11 +580,11 @@ class productplan extends control $planStories = $this->loadModel('story')->getPlanStories($planID); if($browseType == 'bySearch') { - $allStories = $this->story->getBySearch($plan->product, "0,{$plan->branch}", (int)$param, 'id', 0, 'story', array_keys($planStories), '', $pager); + $allStories = $this->story->getBySearch($plan->product, "0,{$plan->branch}", (int)$param, 'id_desc', 0, 'all', array_keys($planStories), '', $pager); } else { - $allStories = $this->story->getProductStories($this->view->product->id, $plan->branch ? "0,{$plan->branch}" : 0, '0', 'draft,reviewing,active,changing', 'story', 'id_desc', $hasParent = false, array_keys($planStories), $pager); + $allStories = $this->story->getProductStories($this->view->product->id, $plan->branch ? "0,{$plan->branch}" : 0, '0', 'draft,reviewing,active,changing,launched', 'all', 'id_desc', true, array_keys($planStories), $pager); } $modules = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); diff --git a/module/productplan/js/view.ui.js b/module/productplan/js/view.ui.js index 6f09ac30ae..d16f579e53 100644 --- a/module/productplan/js/view.ui.js +++ b/module/productplan/js/view.ui.js @@ -12,7 +12,8 @@ window.renderStoryCell = function(result, info) if(info.col.name == 'title' && result) { let html = ''; - if(story.parent > 0) html += "" + childrenAB + ""; + let gradeLabel = gradeGroup[story.type][story.grade]; + if(gradeLabel) html += "" + gradeLabel + " "; if(html) result.unshift({html}); } @@ -60,27 +61,30 @@ window.onSearchLinks = function(type, result) * @access public * @return object */ -window.setStatistics = function(element, checkedIdList) +window.setStatistics = function(element, checkedIdList, pageSummary) { - const checkedTotal = checkedIdList.length; - if(checkedTotal == 0) return {html: summary}; + if(checkedIdList == undefined || checkedIdList.length == 0) return {html: pageSummary}; let checkedEstimate = 0; let checkedCase = 0; let total = 0; - checkedIdList.forEach((rowID) => { - const story = element.getRowInfo(rowID); - total += 1; - if(story) + const rows = element.layout.allRows; + rows.forEach((row) => { + if(checkedIdList.includes(row.id)) { - checkedEstimate += parseFloat(story.data.estimate); - if(cases[rowID]) checkedCase += 1; + const story = element.getRowInfo(row.id); + if(story.data.type == 'story') + { + total += 1; + checkedEstimate += parseFloat(story.data.estimate); + if(cases[row.id]) checkedCase += 1; + } } - }) + }); - const rate = Math.round(checkedCase / checkedTotal * 10000) / 100 + '' + '%'; - return {html: checkedSummary.replace('%total%', checkedTotal) + const rate = Math.round(checkedCase / total * 10000) / 100 + '' + '%'; + return {html: checkedSummary.replace('%total%', total) .replace('%estimate%', checkedEstimate.toFixed(1)) .replace('%rate%', rate)}; } diff --git a/module/productplan/lang/de.php b/module/productplan/lang/de.php index 5fdf439758..d5ecda6004 100644 --- a/module/productplan/lang/de.php +++ b/module/productplan/lang/de.php @@ -78,8 +78,9 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt $lang->productplan->nextStep = "Next step"; $lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s."; $lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%."; -$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; -$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->storySummary = "Total: %s {$lang->ERCommon}s, %s {$lang->URCommon}s, %s {$lang->SRCommon}s on this page. Estimates %s h, and Case Coverage: %s."; +$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->id = 'ID'; @@ -104,6 +105,7 @@ $lang->productplan->order = "Rank"; $lang->productplan->deleted = "Deleted"; $lang->productplan->mailto = "Mailto"; $lang->productplan->planStatus = "Status"; +$lang->productplan->storyTitle = "Title"; $lang->productplan->createdBy = 'Created By'; $lang->productplan->createdDate = 'Created Date'; diff --git a/module/productplan/lang/en.php b/module/productplan/lang/en.php index 3f2ad92823..7c5347ebed 100644 --- a/module/productplan/lang/en.php +++ b/module/productplan/lang/en.php @@ -78,8 +78,9 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt $lang->productplan->nextStep = "Next step"; $lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s."; $lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%."; -$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; -$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->storySummary = "Total: %s {$lang->ERCommon}s, %s {$lang->URCommon}s, %s {$lang->SRCommon}s on this page. Estimates %s h, and Case Coverage: %s."; +$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->id = 'ID'; @@ -104,6 +105,7 @@ $lang->productplan->order = "Order"; $lang->productplan->deleted = "Deleted"; $lang->productplan->mailto = "Mailto"; $lang->productplan->planStatus = "Status"; +$lang->productplan->storyTitle = "Title"; $lang->productplan->createdBy = 'Created By'; $lang->productplan->createdDate = 'Created Date'; diff --git a/module/productplan/lang/fr.php b/module/productplan/lang/fr.php index 6ec80877fe..c6ecc1161f 100644 --- a/module/productplan/lang/fr.php +++ b/module/productplan/lang/fr.php @@ -78,8 +78,9 @@ $lang->productplan->projectNotEmpty = $lang->projectCommon . ' cannot be empt $lang->productplan->nextStep = "Next step"; $lang->productplan->summary = "Total: %s, Parents: %s, Children: %s,Independent: %s."; $lang->productplan->checkedSummary = "Seleted: %total%, Parents: %parent%, Children: %child%, Independent: %independent%."; -$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; -$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s {$lang->SRCommon} under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->storySummary = "Total: %s {$lang->ERCommon}s, %s {$lang->URCommon}s, %s {$lang->SRCommon}s on this page. Estimates %s h, and Case Coverage: %s."; +$lang->productplan->confirmChangePlan = "After the branch of『%s』is unlinked, %s story and %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; +$lang->productplan->confirmRemoveStory = "After the branch of『%s』is unlinked, %s story under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->confirmRemoveBug = "After the branch of『%s』is unlinked, %s bugs under the branch will be removed from the plan at the same time, so still want to unassociate?"; $lang->productplan->id = 'ID'; @@ -104,6 +105,7 @@ $lang->productplan->order = "Order"; $lang->productplan->deleted = "Deleted"; $lang->productplan->mailto = "Mailto"; $lang->productplan->planStatus = "Status"; +$lang->productplan->storyTitle = "Title"; $lang->productplan->createdBy = 'Created By'; $lang->productplan->createdDate = 'Created Date'; diff --git a/module/productplan/lang/zh-cn.php b/module/productplan/lang/zh-cn.php index a9fff12c47..cf28bacec0 100644 --- a/module/productplan/lang/zh-cn.php +++ b/module/productplan/lang/zh-cn.php @@ -40,12 +40,12 @@ $lang->productplan->batchUnlink = "批量移除"; $lang->productplan->batchClose = "批量关闭"; $lang->productplan->batchChangeStatus = "批量修改状态"; $lang->productplan->unlinkAB = "移除"; -$lang->productplan->linkStory = "关联{$lang->SRCommon}"; -$lang->productplan->unlinkStory = "移除{$lang->SRCommon}"; +$lang->productplan->linkStory = "关联需求"; +$lang->productplan->unlinkStory = "移除需求"; $lang->productplan->unlinkStoryAB = "移除"; -$lang->productplan->batchUnlinkStory = "批量移除{$lang->SRCommon}"; -$lang->productplan->linkedStories = $lang->SRCommon; -$lang->productplan->unlinkedStories = "未关联{$lang->SRCommon}"; +$lang->productplan->batchUnlinkStory = "批量移除需求"; +$lang->productplan->linkedStories = '需求'; +$lang->productplan->unlinkedStories = "未关联需求"; $lang->productplan->updateOrder = '排序'; $lang->productplan->createChildren = "创建子计划"; $lang->productplan->createExecution = "创建{$lang->execution->common}"; @@ -65,7 +65,7 @@ $lang->productplan->expired = "已过期"; $lang->productplan->closedReason = "关闭原因"; $lang->productplan->confirmDelete = "您确认删除该计划吗?"; -$lang->productplan->confirmUnlinkStory = "您确认移除该{$lang->SRCommon}吗?"; +$lang->productplan->confirmUnlinkStory = "您确认移除该需求吗?"; $lang->productplan->confirmUnlinkBug = "您确认移除该Bug吗?"; $lang->productplan->confirmStart = "您确认开始该计划吗?"; $lang->productplan->confirmFinish = "您确认完成该计划吗?"; @@ -78,8 +78,9 @@ $lang->productplan->projectNotEmpty = "所属{$lang->projectCommon}不能为 $lang->productplan->nextStep = "下一步"; $lang->productplan->summary = "本页共 %s 个计划,父计划 %s,子计划 %s,独立计划 %s。"; $lang->productplan->checkedSummary = "共选中 %total% 个计划,父计划 %parent%,子计划 %child%,独立计划 %independent%。"; -$lang->productplan->confirmChangePlan = "分支『%s』解除关联后,分支下的%s个{$lang->SRCommon}和%s个Bug将同步从计划中移除,是否解除?"; -$lang->productplan->confirmRemoveStory = "分支『%s』解除关联后,分支下的%s个{$lang->SRCommon}将同步从计划中移除,是否解除?"; +$lang->productplan->storySummary = "本页共 %s 个{$lang->ERCommon},%s 个{$lang->URCommon}, %s 个{$lang->SRCommon},预计 %s 个工时, 用例覆盖率 %s 。"; +$lang->productplan->confirmChangePlan = "分支『%s』解除关联后,分支下的%s个需求和%s个Bug将同步从计划中移除,是否解除?"; +$lang->productplan->confirmRemoveStory = "分支『%s』解除关联后,分支下的%s个需求将同步从计划中移除,是否解除?"; $lang->productplan->confirmRemoveBug = "分支『%s』解除关联后,分支下的%s个Bug将同步从计划中移除,是否解除?"; $lang->productplan->id = '编号'; @@ -90,20 +91,21 @@ $lang->productplan->desc = '描述'; $lang->productplan->begin = '开始日期'; $lang->productplan->end = '结束日期'; $lang->productplan->status = '状态'; -$lang->productplan->last = '上次计划'; +$lang->productplan->last = "上次计划"; $lang->productplan->future = '待定'; -$lang->productplan->stories = "{$lang->SRCommon}数"; +$lang->productplan->stories = "需求数"; $lang->productplan->bugs = 'Bug数'; $lang->productplan->hour = $lang->hourCommon; $lang->productplan->execution = $lang->execution->common; -$lang->productplan->parent = '父计划'; -$lang->productplan->parentAB = '父'; -$lang->productplan->children = '子计划'; -$lang->productplan->childrenAB = '子'; -$lang->productplan->order = '排序'; -$lang->productplan->deleted = '已删除'; -$lang->productplan->mailto = '抄送给'; -$lang->productplan->planStatus = '状态'; +$lang->productplan->parent = "父计划"; +$lang->productplan->parentAB = "父"; +$lang->productplan->children = "子计划"; +$lang->productplan->childrenAB = "子"; +$lang->productplan->order = "排序"; +$lang->productplan->deleted = "已删除"; +$lang->productplan->mailto = "抄送给"; +$lang->productplan->planStatus = "状态"; +$lang->productplan->storyTitle = "需求名称"; $lang->productplan->createdBy = '由谁创建'; $lang->productplan->createdDate = '创建时间'; diff --git a/module/productplan/model.php b/module/productplan/model.php index 029d3ed7cf..59b38418ab 100644 --- a/module/productplan/model.php +++ b/module/productplan/model.php @@ -133,7 +133,7 @@ class productplanModel extends model $bugCount = isset($bugs[$plan->id]) ? count($bugs[$plan->id]) : 0; $plan->bugs = zget($plan, 'bugs', 0) + $bugCount; - $plan->hour = zget($plan, 'hour', 0) + array_sum($storyPairs); + $plan->hour = zget($plan, 'hour', 0) + round(array_sum($storyPairs), 1); $plan->stories = zget($plan, 'stories', 0) + count($storyPairs); $plan->projects = zget($planProjects, $plan->id, ''); $plan->expired = $plan->end < helper::today(); @@ -860,10 +860,12 @@ class productplanModel extends model if(strpos(",$story->plan,", ",{$planID},") !== false) continue; /* Update the plan linked with the story and the order of the story in the plan. */ - $storyID = (int)$storyID; - $this->dao->update(TABLE_STORY)->set('plan')->eq($planID)->where('id')->eq($storyID)->exec(); + $storyID = (int)$storyID; + $newPlanID = $story->type == 'story' ? $planID : trim($story->plan, ',') . ',' . $planID; + $this->dao->update(TABLE_STORY)->set('plan')->eq($newPlanID)->where('id')->eq($storyID)->exec(); - $this->story->updateStoryOrderOfPlan($storyID, (string)$planID, $story->plan); + $oldPlanID = $story->type == 'story' ? $story->plan : ''; + $this->story->updateStoryOrderOfPlan($storyID, (string)$planID, $oldPlanID); $this->action->create('story', $storyID, 'linked2plan', '', $planID); $this->story->setStage($storyID); diff --git a/module/productplan/ui/linkstory.html.php b/module/productplan/ui/linkstory.html.php index e722649677..d4fff89c2d 100644 --- a/module/productplan/ui/linkstory.html.php +++ b/module/productplan/ui/linkstory.html.php @@ -13,8 +13,9 @@ namespace zin; $cols = array(); foreach($config->productplan->defaultFields['linkStory'] as $field) $cols[$field] = zget($config->story->dtable->fieldList, $field, array()); $cols = array_map(function($col){$col['show'] = true; return $col;}, $cols); -$cols['title']['link'] = $this->createLink('story', 'view', "storyID={id}"); -$cols['title']['nestedToggle'] = false; +$cols['title']['link'] = $this->createLink('story', 'storyView', "storyID={id}"); +$cols['title']['title'] = $lang->productplan->storyTitle; +$cols['title']['nestedToggle'] = true; $cols['title']['data-toggle'] = 'modal'; $cols['title']['data-size'] = 'lg'; $cols['plan']['name'] = 'planTitle'; @@ -26,6 +27,7 @@ foreach($cols as $colKey => $colConfig) $cols[$colKey]['sort'] = true; foreach($allStories as $story) $story->estimate = $story->estimate . $config->hourUnit; +$config->product->search['fields']['title'] = $lang->productplan->storyTitle; searchForm ( set::module('story'), diff --git a/module/productplan/ui/view.html.php b/module/productplan/ui/view.html.php index ccb6b7d8bd..4d061c270a 100644 --- a/module/productplan/ui/view.html.php +++ b/module/productplan/ui/view.html.php @@ -37,6 +37,7 @@ jsVar('summary', $summary); jsVar('checkedSummary', $lang->product->checkedSRSummary); jsVar('storyPageID', $storyPager->pageID); jsVar('storyRecPerPage', $storyPager->recPerPage); +jsVar('gradeGroup', $gradeGroup); $bugCols = array(); $storyCols = array(); @@ -56,8 +57,8 @@ foreach($config->productplan->defaultFields['story'] as $field) if(isset($storyCols['branch'])) $storyCols['branch']['map'] = $branchOption; foreach($config->productplan->defaultFields['bug'] as $field) $bugCols[$field] = zget($config->bug->dtable->fieldList, $field, array()); -$storyCols['title']['link'] = $this->createLink('story', 'view', "storyID={id}"); -$storyCols['title']['nestedToggle'] = false; +$storyCols['title']['link'] = $this->createLink('story', 'storyView', "storyID={id}"); +$storyCols['title']['title'] = $lang->productplan->storyTitle; $storyCols['assignedTo']['type'] = 'user'; $storyCols['module']['type'] = 'text'; $storyCols['module']['map'] = $modulePairs; @@ -153,8 +154,12 @@ $planStories = initTableData($planStories, $storyCols, $this->productplan); $planBugs = initTableData($planBugs, $bugCols, $this->productplan); foreach($planStories as $story) $story->estimate = $story->estimate . $config->hourUnit; -$createStoryLink = common::hasPriv('story', 'create') ? $this->createLink('story', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null; -$batchCreateStoryLink = common::hasPriv('story', 'batchCreate') ? $this->createLink('story', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null; +$createStoryLink = common::hasPriv('story', 'create') ? $this->createLink('story', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null; +$batchCreateStoryLink = common::hasPriv('story', 'batchCreate') ? $this->createLink('story', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null; +$createRequirementLink = common::hasPriv('requirement', 'create') ? $this->createLink('requirement', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null; +$batchCreateRequirementLink = common::hasPriv('requirement', 'batchCreate') ? $this->createLink('requirement', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null; +$createEpicLink = common::hasPriv('epic', 'create') ? $this->createLink('epic', 'create', "productID=$plan->product&branch=$plan->branch&moduleID=0&storyID=0&projectID=$projectID&bugID=0&planID=$plan->id") : null; +$batchCreateEpicLink = common::hasPriv('epic', 'batchCreate') ? $this->createLink('epic', 'batchCreate', "productID=$plan->product&branch=$plan->branch&moduleID=0&story=0&project=$projectID&plan={$plan->id}") : null; $branchNames = ''; if($product->type != 'normal') @@ -236,7 +241,15 @@ detailBody set::caret(true), set::url($createStoryLink) ), - set::items(array(array('text' => $lang->story->batchCreate, 'url' => $batchCreateStoryLink, 'class' => empty($batchCreateStoryLink) ? 'disabled' : ''))), + set::items(array( + array('text' => $lang->requirement->create, 'url' => $createRequirementLink, 'class' => empty($createRequirementLink) ? 'disabled' : ''), + array('text' => $lang->epic->create, 'url' => $createEpicLink, 'class' => empty($createEpicLink) ? 'disabled' : ''), + array('text' => $lang->story->batchCreate, 'items' => array( + array('text' => $lang->SRCommon, 'url' => $batchCreateStoryLink, 'class' => empty($batchCreateStoryLink) ? 'disabled' : ''), + array('text' => $lang->URCommon, 'url' => $batchCreateRequirementLink, 'class' => empty($batchCreateRequirementLink) ? 'disabled' : ''), + array('text' => $lang->ERCommon, 'url' => $batchCreateEpicLink, 'class' => empty($batchCreateEpicLink) ? 'disabled' : '') + )) + )), set::trigger('hover'), set::placement('bottom-end') ), @@ -265,7 +278,7 @@ detailBody set::sortLink(createLink('productplan', 'view', "planID={$plan->id}&type=story&orderBy={name}_{sortType}&link=false¶m={$param}&recTotal={$storyPager->recTotal}&recPerPage={$storyPager->recPerPage}&page={$storyPager->pageID}")), set::orderBy($orderBy), set::extraHeight('+144'), - set::checkInfo(jsRaw('function(checkedIDList){return window.setStatistics(this, checkedIDList);}')), + set::checkInfo(jsRaw("function(checkedIDList){return window.setStatistics(this, checkedIDList, '{$summary}');}")), set::footPager ( usePager('storyPager', '', array( diff --git a/module/productplan/zen.php b/module/productplan/zen.php index 2378f7dacd..bdbcb7b942 100644 --- a/module/productplan/zen.php +++ b/module/productplan/zen.php @@ -235,11 +235,16 @@ class productplanZen extends productplan if($plan->parent > 0) $this->view->parentPlan = $this->productplan->getById($plan->parent); if($plan->parent == '-1') $this->view->childrenPlans = $this->productplan->getChildren($plan->id); - $this->view->plan = $plan; - $this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id); - $this->view->users = $this->loadModel('user')->getPairs('noletter'); - $this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true); - $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product); + $gradeList = $this->loadModel('story')->getGradeList(''); + $gradeGroup = array(); + foreach($gradeList as $grade) $gradeGroup[$grade->type][$grade->grade] = $grade->name; + + $this->view->plan = $plan; + $this->view->gradeGroup = $gradeGroup; + $this->view->actions = $this->loadModel('action')->getList('productplan', $plan->id); + $this->view->users = $this->loadModel('user')->getPairs('noletter'); + $this->view->plans = $this->productplan->getPairs($plan->product, $plan->branch, '', true); + $this->view->modules = $this->loadModel('tree')->getOptionMenu($plan->product); if($this->app->getViewType() == 'json') { @@ -267,6 +272,7 @@ class productplanZen extends productplan $this->config->product->search['actionURL'] = $this->createLink('productplan', 'view', "planID=$plan->id&type=story&orderBy=$orderBy&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; $this->config->product->search['style'] = 'simple'; + $this->config->product->search['fields']['title'] = $this->lang->productplan->storyTitle; $this->config->product->search['params']['product']['values'] = $products + array('all' => $this->lang->product->allProductsOfProject); $this->config->product->search['params']['plan']['values'] = $this->productplan->getPairs($plan->product, $plan->branch, 'withMainPlan', true); $this->config->product->search['params']['module']['values'] = $this->loadModel('tree')->getOptionMenu($plan->product, 'story', 0, 'all'); @@ -288,6 +294,8 @@ class productplanZen extends productplan $this->config->product->search['params']['branch']['values'] = array('' => '', BRANCH_MAIN => $this->lang->branch->main) + $branches; } + unset($this->config->product->search['fields']['grade']); + unset($this->config->product->search['params']['grade']); unset($this->config->product->search['fields']['product']); $this->loadModel('search')->setSearchParams($this->config->product->search); } @@ -333,4 +341,76 @@ class productplanZen extends productplan } $this->loadModel('search')->setSearchParams($this->config->bug->search); } + + /** + * 构造需求列表的摘要信息。 + * Get the summary of product's stories. + * + * @param array $stories + * @access public + * @return string + */ + public function buildViewSummary(array $stories): string + { + $totalEstimate = 0.0; + $storyIdList = array(); + + $rateCount = 0; + $SRTotal = 0; + $URTotal = 0; + $ERTotal = 0; + + foreach($stories as $story) + { + if($story->type == 'story') $SRTotal += 1; + if($story->type == 'requirement') $URTotal += 1; + if($story->type == 'epic') $ERTotal += 1; + + if($story->isParent == '0') $totalEstimate += $story->estimate; + + if($story->type != 'story') continue; + if($story->isParent == '0' && ($story->status != 'closed' || in_array($story->closedReason, array('done', 'postponed')))) + { + $storyIdList[] = $story->id; + $rateCount ++; + } + } + + $casesCount = count($this->loadModel('product')->filterNoCasesStory($storyIdList)); + $rate = empty($stories) || $rateCount == 0 ? 0 : round($casesCount / $rateCount, 2); + + return sprintf($this->lang->productplan->storySummary, $ERTotal, $URTotal, $SRTotal, $totalEstimate, $rate * 100 . "%"); + } + + /** + * 构造计划详情页面的操作菜单。 + * Build operate menu for plan detail page. + * + * @param object $plan + * @access public + * @return array + */ + public function buildViewActions(object $plan): array + { + $params = "planID=$plan->id"; + + $canEdit = common::hasPriv('productplan', 'edit'); + $canStart = common::hasPriv('productplan', 'start') && $this->productplan->isClickable($plan, 'start'); + $canFinish = common::hasPriv('productplan', 'finish') && $this->productplan->isClickable($plan, 'finish'); + $canClose = common::hasPriv('productplan', 'close') && $this->productplan->isClickable($plan, 'close'); + $canActivate = common::hasPriv('productplan', 'activate') && $this->productplan->isClickable($plan, 'activate'); + $canCreateChild = common::hasPriv('productplan', 'create') && $this->productplan->isClickable($plan, 'create'); + $canDelete = common::hasPriv('productplan', 'delete') && $this->productplan->isClickable($plan, 'delete'); + + $menu = array(); + if($canStart) $menu[] = array('icon' => 'play text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->startAB, 'data-url' => helper::createLink('productplan', 'start', $params), 'data-action' => 'start', 'onclick' => 'ajaxConfirmLoad(this)'); + if($canFinish) $menu[] = array('icon' => 'checked text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->finishAB, 'data-url' => helper::createLink('productplan', 'finish', $params), 'data-action' => 'finish', 'onclick' => 'ajaxConfirmLoad(this)'); + if($canClose) $menu[] = array('icon' => 'off text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->closeAB, 'url' => helper::createLink('productplan', 'close', $params, '', true), 'data-toggle' => 'modal'); + if($canActivate) $menu[] = array('icon' => 'magic text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->activateAB, 'data-url' => helper::createLink('productplan', 'activate', $params), 'data-action' => 'activate', 'onclick' => 'ajaxConfirmLoad(this)'); + if($canCreateChild) $menu[] = array('icon' => 'split text-primary', 'class' => 'ghost', 'text' => $this->lang->productplan->children, 'url' => helper::createLink('productplan', 'create', "product={$plan->product}&branch={$plan->branch}&parent={$plan->id}")); + if($canEdit) $menu[] = array('icon' => 'edit text-primary', 'class' => 'ghost', 'text' => $this->lang->edit, 'url' => helper::createLink('productplan', 'edit', $params)); + if($canDelete) $menu[] = array('icon' => 'trash text-primary', 'class' => 'ghost', 'text' => $this->lang->delete, 'data-url' => helper::createLink('productplan', 'delete', $params), 'data-action' => 'delete', 'onclick' => 'ajaxConfirmLoad(this)'); + + return $menu; + } } diff --git a/module/project/config.php b/module/project/config.php index e3568e75d3..74b9e768be 100755 --- a/module/project/config.php +++ b/module/project/config.php @@ -108,23 +108,25 @@ $config->project->noSprintPriv['story'] = array('create', 'batchCreate', 'e $config->project->noSprintPriv['bug'] = array('create', 'confirm', 'view', 'edit', 'assignTo', 'batchAssignTo', 'resolve', 'activate', 'close', 'export', 'confirmStoryChange', 'delete', 'linkBugs', 'import', 'showImport', 'exportTemplate'); $config->project->noSprintPriv['testcase'] = array('groupCase', 'create', 'batchCreate', 'createBug', 'view', 'edit', 'delete', 'export', 'confirmChange', 'confirmStoryChange', 'batchEdit', 'batchDelete', 'linkCases', 'bugs', 'review', 'batchReview', 'batchConfirmStoryChange', 'importFromLib', 'batchChangeType', 'exportTemplate', 'import', 'showImport', 'confirmLibcaseChange', 'ignoreLibcaseChange', 'submit'); $config->project->noSprintPriv['testtask'] = array('create', 'cases', 'groupCase', 'edit', 'delete', 'batchAssign', 'linkcase', 'unlinkcase', 'runcase', 'results', 'batchUnlinkCases', 'report', 'browseUnits', 'unitCases', 'importUnitResult', 'batchRun', 'runDeployCase', 'deployCaseResults'); -$config->project->includedPriv['doc'] = array('createLib', 'editLib', 'deleteLib', 'create', 'edit', 'view', 'delete', 'deleteFile', 'collect', 'projectSpace', 'showFiles', 'addCatalog', 'editCatalog', 'deleteCatalog', 'displaySetting', 'diff', 'importToPracticeLib', 'importToComponentLib'); +$config->project->noSprintPriv['doc'] = array('createLib', 'editLib', 'deleteLib', 'create', 'edit', 'view', 'delete', 'deleteFile', 'collect', 'projectSpace', 'showFiles', 'addCatalog', 'editCatalog', 'deleteCatalog', 'displaySetting', 'diff', 'importToPracticeLib', 'importToComponentLib'); $config->project->noSprintPriv['repo'] = array('create', 'showSyncCommit', 'browse', 'view', 'diff', 'log', 'revision', 'blame', 'download', 'apiGetRepoByUrl', 'review', 'addBug', 'editBug', 'deleteBug', 'addComment', 'editComment', 'deleteComment'); $config->project->noSprintPriv['testreport'] = array('create', 'view', 'delete', 'edit', 'export'); $config->project->noSprintPriv['auditplan'] = array('browse', 'create', 'edit', 'batchCreate', 'batchCheck', 'check', 'nc', 'result', 'assignTo'); $config->project->includedPriv = $config->project->noSprintPriv; -$config->project->includedPriv['project'][] = 'execution'; -$config->project->includedPriv['task'] = array('create'); -$config->project->includedPriv['story'] = array('create', 'batchCreate', 'edit', 'export', 'delete', 'view', 'change', 'review', 'batchReview', 'recall', 'close', 'batchClose', 'batchChangePlan', 'batchChangeStage', 'assignTo', 'batchAssignTo', 'activate', 'zeroCase', 'batchEdit', 'import', 'showImport', 'exportTemplate', 'importToLib', 'batchImportToLib', 'relation', 'browse'); -$config->project->includedPriv['bug'] = array('create', 'confirm', 'view', 'edit', 'assignTo', 'batchAssignTo', 'resolve', 'activate', 'close', 'export', 'confirmStoryChange', 'delete', 'linkBugs', 'import', 'showImport', 'exportTemplate'); -$config->project->includedPriv['testcase'] = array('groupCase', 'create', 'batchCreate', 'createBug', 'view', 'edit', 'delete', 'export', 'confirmChange', 'confirmStoryChange', 'batchEdit', 'batchDelete', 'linkCases', 'bugs', 'review', 'batchReview', 'batchConfirmStoryChange', 'importFromLib', 'batchChangeType', 'exportTemplate', 'import', 'showImport', 'confirmLibcaseChange', 'ignoreLibcaseChange', 'submit'); -$config->project->includedPriv['testtask'] = array('create', 'cases', 'groupCase', 'edit', 'delete', 'batchAssign', 'linkcase', 'unlinkcase', 'runcase', 'results', 'batchUnlinkCases', 'report', 'browseUnits', 'unitCases', 'importUnitResult', 'batchRun', 'runDeployCase', 'deployCaseResults'); -$config->project->includedPriv['doc'] = array('createLib', 'editLib', 'deleteLib', 'create', 'edit', 'view', 'delete', 'deleteFile', 'collect', 'projectSpace', 'showFiles', 'addCatalog', 'editCatalog', 'deleteCatalog', 'displaySetting', 'diff', 'importToPracticeLib', 'importToComponentLib'); -$config->project->includedPriv['repo'] = array('create', 'showSyncCommit', 'browse', 'view', 'diff', 'log', 'revision', 'blame', 'download', 'apiGetRepoByUrl', 'review', 'addBug', 'deleteBug', 'addComment', 'editComment', 'deleteComment'); -$config->project->includedPriv['testreport'] = array('create', 'view', 'delete', 'edit', 'export'); -$config->project->includedPriv['auditplan'] = array('browse', 'create', 'edit', 'batchCreate', 'batchCheck', 'check', 'nc', 'result', 'assignTo'); -$config->project->includedPriv['execution'] = array('create', 'start', 'delete', 'calendar', 'effortCalendar', 'effort', 'taskEffort', 'computeTaskEffort', 'deleterelation', 'maintainrelation', 'relation', 'gantt'); +$config->project->includedPriv['project'][] = 'execution'; +$config->project->includedPriv['task'] = array('create'); +$config->project->includedPriv['story'] = array('create', 'batchCreate', 'edit', 'delete', 'view', 'change', 'review', 'recall', 'close', 'batchChangeStage', 'assignTo', 'activate', 'zeroCase', 'importToLib', 'batchImportToLib'); +$config->project->includedPriv['requirement'] = array('create', 'batchCreate', 'edit', 'delete', 'view', 'change', 'review', 'recall', 'close', 'assignTo', 'activate'); +$config->project->includedPriv['epic'] = array('create', 'batchCreate', 'edit', 'delete', 'view', 'change', 'review', 'recall', 'close', 'assignTo', 'activate'); +$config->project->includedPriv['bug'] = array('create', 'confirm', 'view', 'edit', 'assignTo', 'batchAssignTo', 'resolve', 'activate', 'close', 'export', 'confirmStoryChange', 'delete', 'linkBugs', 'import', 'showImport', 'exportTemplate'); +$config->project->includedPriv['testcase'] = array('groupCase', 'create', 'batchCreate', 'createBug', 'view', 'edit', 'delete', 'export', 'confirmChange', 'confirmStoryChange', 'batchEdit', 'batchDelete', 'linkCases', 'bugs', 'review', 'batchReview', 'batchConfirmStoryChange', 'importFromLib', 'batchChangeType', 'exportTemplate', 'import', 'showImport', 'confirmLibcaseChange', 'ignoreLibcaseChange', 'submit'); +$config->project->includedPriv['testtask'] = array('create', 'cases', 'groupCase', 'edit', 'delete', 'batchAssign', 'linkcase', 'unlinkcase', 'runcase', 'results', 'batchUnlinkCases', 'report', 'browseUnits', 'unitCases', 'importUnitResult', 'batchRun', 'runDeployCase', 'deployCaseResults'); +$config->project->includedPriv['doc'] = array('createLib', 'editLib', 'deleteLib', 'create', 'edit', 'view', 'delete', 'deleteFile', 'collect', 'projectSpace', 'showFiles', 'addCatalog', 'editCatalog', 'deleteCatalog', 'displaySetting', 'diff', 'importToPracticeLib', 'importToComponentLib'); +$config->project->includedPriv['repo'] = array('create', 'showSyncCommit', 'browse', 'view', 'diff', 'log', 'revision', 'blame', 'download', 'apiGetRepoByUrl', 'review', 'addBug', 'deleteBug', 'addComment', 'editComment', 'deleteComment'); +$config->project->includedPriv['testreport'] = array('create', 'view', 'delete', 'edit', 'export'); +$config->project->includedPriv['auditplan'] = array('browse', 'create', 'edit', 'batchCreate', 'batchCheck', 'check', 'nc', 'result', 'assignTo'); +$config->project->includedPriv['execution'] = array('create', 'start', 'delete', 'calendar', 'effortCalendar', 'effort', 'taskEffort', 'computeTaskEffort', 'deleterelation', 'maintainrelation', 'relation', 'gantt'); if($config->edition != 'max') $config->project->includedPriv['stakeholder'] = array('browse', 'create', 'batchCreate', 'edit', 'delete', 'view', 'communicate', 'expect', 'expectation', 'deleteExpect', 'createExpect', 'editExpect', 'viewExpect'); $config->project->browseTable = new stdClass(); diff --git a/module/project/config/form.php b/module/project/config/form.php index ed47a35864..cb47b1eeda 100755 --- a/module/project/config/form.php +++ b/module/project/config/form.php @@ -26,6 +26,7 @@ $config->project->form->create['desc'] = array('type' => 'string', 'requir $config->project->form->create['acl'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->project->form->create['whitelist'] = array('type' => 'array', 'required' => false, 'default' => ''); $config->project->form->create['auth'] = array('type' => 'array', 'required' => false, 'default' => ''); +$config->project->form->create['storyType'] = array('type' => 'array', 'required' => false, 'default' => ''); $config->project->form->create['model'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->project->form->create['vision'] = array('type' => 'string', 'required' => false, 'default' => $config->vision); if(isset($this->config->setCode) && $this->config->setCode == 1) $config->project->form->create['code'] = array('type' => 'string', 'required' => false, 'filter' => 'trim'); diff --git a/module/project/control.php b/module/project/control.php index 62bf71279f..bda15e8e3e 100755 --- a/module/project/control.php +++ b/module/project/control.php @@ -1604,6 +1604,27 @@ class project extends control } } + /** + * 判断项目下是否有对应概念的需求。 + * AJAX: check if the project has the concept of story. + * + * @param string $projectID + * @param string $storyType + * @access public + * @return bool + */ + public function ajaxGetStoryByType($projectID, $storyType) + { + $story = $this->dao->select('t1.id')->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story') + ->where('t1.deleted')->eq(0) + ->andWhere('t2.project')->eq($projectID) + ->andWhere('t1.type')->eq($storyType) + ->fetch('id'); + + return print(!empty($story)); + } + /** * Ajax get product drop menu. * diff --git a/module/project/js/common.ui.js b/module/project/js/common.ui.js index f55237ff4d..64ad7a64bb 100644 --- a/module/project/js/common.ui.js +++ b/module/project/js/common.ui.js @@ -19,10 +19,12 @@ function changeType() { $('.productsBox').addClass('hidden'); $('.stageByBox').addClass('hidden'); + $('[data-name=storyType]').addClass('hidden'); } else { $('.productsBox').removeClass('hidden'); + $('[data-name=storyType]').removeClass('hidden'); } } diff --git a/module/project/js/create.ui.js b/module/project/js/create.ui.js index 202addad40..8e8235ab8d 100644 --- a/module/project/js/create.ui.js +++ b/module/project/js/create.ui.js @@ -49,3 +49,15 @@ $(document).on('keyup', '#projectName', function() if($(this).text().includes(name) || $(this).data('pinyin').includes(name)) $(this).show(); }); }); + +window.toggleStoryType = function(e) +{ + if(e.target.value == 'requirement' && !e.target.checked) + { + $('input[value=epic]').prop('checked', false); + } + else if(e.target.value == 'epic' && e.target.checked) + { + $('input[value=requirement]').prop('checked', true); + } +} diff --git a/module/project/js/edit.ui.js b/module/project/js/edit.ui.js index 535b476f13..8ac2d3158b 100644 --- a/module/project/js/edit.ui.js +++ b/module/project/js/edit.ui.js @@ -16,3 +16,35 @@ $(document).on('click', '.model-drop', function() $('#project-model').addClass(btnClass); $('#model').val(model); }) + +window.toggleStoryType = function(e) +{ + if(!e.target.checked) + { + const link = $.createLink('project', 'ajaxGetStoryByType', 'projectID=' + currentProject + '&storyType=' + e.target.value) + $.get(link, function(data) + { + if(data) + { + zui.Modal.confirm(confirmDisableStoryType).then((res) => { + if(!res) + { + if(storyType.includes('epic')) $('input[value=epic]').prop('checked', true); + if(storyType.includes('requirement')) $('input[value=requirement]').prop('checked', true); + return false; + } + }) + } + }); + + if(e.target.value == 'requirement') + { + $('input[value=epic]').prop('checked', false); + } + } + + if(e.target.value == 'epic' && e.target.checked) + { + $('input[value=requirement]').prop('checked', true); + } +} diff --git a/module/project/lang/de.php b/module/project/lang/de.php index 6b21e81caa..fc7c053fa1 100644 --- a/module/project/lang/de.php +++ b/module/project/lang/de.php @@ -77,6 +77,7 @@ $lang->project->manageRepo = 'Manage Repo'; $lang->project->linkedRepo = 'Link Repo'; $lang->project->unlinkedRepo = 'Unlink Repo'; $lang->project->executionCount = 'Total Executions'; +$lang->project->storyType = 'Link Story Concept'; $lang->project->storyCount = 'Story Count'; $lang->project->storyPoints = 'Story Points'; $lang->project->invested = 'Invested'; @@ -425,15 +426,16 @@ $lang->project->programTitle['0'] = 'Hidden'; $lang->project->programTitle['base'] = "Base-level {$lang->projectCommon} only"; $lang->project->programTitle['end'] = "End-level {$lang->projectCommon} only"; -$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; -$lang->project->chooseProgramType = 'Select management type'; -$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; -$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; -$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; -$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; -$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; -$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; -$lang->project->parentBudget = "The budget of the parent program: "; +$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; +$lang->project->chooseProgramType = 'Select management type'; +$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; +$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; +$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; +$lang->project->confirmDisableStoryType = "After cancellation, all corresponding conceptual requirements that have been associated with the project and execution will be removed, and the operation is irreversible."; +$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; +$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; +$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; +$lang->project->parentBudget = "The budget of the parent program: "; $lang->project->beginLessThanParent = "The start date of the {$lang->projectCommon} is < the start date of the parent program: %s. "; $lang->project->endGreatThanParent = "The finish date of the {$lang->projectCommon} is > the finish date of the parent program: %s. "; diff --git a/module/project/lang/en.php b/module/project/lang/en.php index 6b21e81caa..fc7c053fa1 100644 --- a/module/project/lang/en.php +++ b/module/project/lang/en.php @@ -77,6 +77,7 @@ $lang->project->manageRepo = 'Manage Repo'; $lang->project->linkedRepo = 'Link Repo'; $lang->project->unlinkedRepo = 'Unlink Repo'; $lang->project->executionCount = 'Total Executions'; +$lang->project->storyType = 'Link Story Concept'; $lang->project->storyCount = 'Story Count'; $lang->project->storyPoints = 'Story Points'; $lang->project->invested = 'Invested'; @@ -425,15 +426,16 @@ $lang->project->programTitle['0'] = 'Hidden'; $lang->project->programTitle['base'] = "Base-level {$lang->projectCommon} only"; $lang->project->programTitle['end'] = "End-level {$lang->projectCommon} only"; -$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; -$lang->project->chooseProgramType = 'Select management type'; -$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; -$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; -$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; -$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; -$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; -$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; -$lang->project->parentBudget = "The budget of the parent program: "; +$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; +$lang->project->chooseProgramType = 'Select management type'; +$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; +$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; +$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; +$lang->project->confirmDisableStoryType = "After cancellation, all corresponding conceptual requirements that have been associated with the project and execution will be removed, and the operation is irreversible."; +$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; +$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; +$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; +$lang->project->parentBudget = "The budget of the parent program: "; $lang->project->beginLessThanParent = "The start date of the {$lang->projectCommon} is < the start date of the parent program: %s. "; $lang->project->endGreatThanParent = "The finish date of the {$lang->projectCommon} is > the finish date of the parent program: %s. "; diff --git a/module/project/lang/fr.php b/module/project/lang/fr.php index 6b21e81caa..fc7c053fa1 100644 --- a/module/project/lang/fr.php +++ b/module/project/lang/fr.php @@ -77,6 +77,7 @@ $lang->project->manageRepo = 'Manage Repo'; $lang->project->linkedRepo = 'Link Repo'; $lang->project->unlinkedRepo = 'Unlink Repo'; $lang->project->executionCount = 'Total Executions'; +$lang->project->storyType = 'Link Story Concept'; $lang->project->storyCount = 'Story Count'; $lang->project->storyPoints = 'Story Points'; $lang->project->invested = 'Invested'; @@ -425,15 +426,16 @@ $lang->project->programTitle['0'] = 'Hidden'; $lang->project->programTitle['base'] = "Base-level {$lang->projectCommon} only"; $lang->project->programTitle['end'] = "End-level {$lang->projectCommon} only"; -$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; -$lang->project->chooseProgramType = 'Select management type'; -$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; -$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; -$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; -$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; -$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; -$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; -$lang->project->parentBudget = "The budget of the parent program: "; +$lang->project->accessDenied = "Access denied to this {$lang->projectCommon}"; +$lang->project->chooseProgramType = 'Select management type'; +$lang->project->cannotCreateChild = "The {$lang->projectCommon} has contents, so you cannot add a child {$lang->projectCommon}. You can create a parent project for this one and then add a child {$lang->projectCommon} for the parent {$lang->projectCommon}."; +$lang->project->hasChildren = "This {$lang->projectCommon} has a child {$lang->projectCommon}, so it cannot be deleted."; +$lang->project->confirmDelete = 'Do you want to delete \"%s\"?'; +$lang->project->confirmDisableStoryType = "After cancellation, all corresponding conceptual requirements that have been associated with the project and execution will be removed, and the operation is irreversible."; +$lang->project->cannotChangeToCat = "The {$lang->projectCommon} has contents, so you cannot it to a parent {$lang->projectCommon}."; +$lang->project->cannotCancelCat = "There are child {$lang->projectCommon}s of this {$lang->projectCommon}. You cannot cancel the parent {$lang->projectCommon} mark."; +$lang->project->parentBeginEnd = "The begin and end date of the parent {$lang->projectCommon}: %s ~ %s"; +$lang->project->parentBudget = "The budget of the parent program: "; $lang->project->beginLessThanParent = "The start date of the {$lang->projectCommon} is < the start date of the parent program: %s. "; $lang->project->endGreatThanParent = "The finish date of the {$lang->projectCommon} is > the finish date of the parent program: %s. "; diff --git a/module/project/lang/zh-cn.php b/module/project/lang/zh-cn.php index abdbbdb57f..280751adf6 100644 --- a/module/project/lang/zh-cn.php +++ b/module/project/lang/zh-cn.php @@ -77,6 +77,7 @@ $lang->project->manageRepo = '关联代码库'; $lang->project->linkedRepo = '已关联代码库'; $lang->project->unlinkedRepo = '未关联代码库'; $lang->project->executionCount = '执行数'; +$lang->project->storyType = '关联需求概念'; $lang->project->storyCount = '需求条目数'; $lang->project->storyPoints = '需求规模'; $lang->project->invested = '已投入'; @@ -425,15 +426,16 @@ $lang->project->programTitle['0'] = '不显示'; $lang->project->programTitle['base'] = '只显示一级项目集'; $lang->project->programTitle['end'] = '只显示最后一级项目集'; -$lang->project->accessDenied = "您无权访问该{$lang->projectCommon}!"; -$lang->project->chooseProgramType = "选择{$lang->projectCommon}管理方式"; -$lang->project->cannotCreateChild = "该{$lang->projectCommon}已经有实际的内容,无法直接添加子{$lang->projectCommon}。您可以为当前{$lang->projectCommon}创建一个父{$lang->projectCommon},然后在新的父{$lang->projectCommon}下面添加子{$lang->projectCommon}。"; -$lang->project->hasChildren = "该{$lang->projectCommon}有子{$lang->projectCommon}存在,不能删除。"; -$lang->project->confirmDelete = "您确定删除{$lang->projectCommon}“%s”吗?"; -$lang->project->cannotChangeToCat = "该{$lang->projectCommon}已经有实际的内容,无法修改为父{$lang->projectCommon}"; -$lang->project->cannotCancelCat = "该{$lang->projectCommon}下已经有子{$lang->projectCommon},无法取消父{$lang->projectCommon}标记"; -$lang->project->parentBeginEnd = "父{$lang->projectCommon}起止时间:%s ~ %s"; -$lang->project->parentBudget = "父项目集预算:"; +$lang->project->accessDenied = "您无权访问该{$lang->projectCommon}!"; +$lang->project->chooseProgramType = "选择{$lang->projectCommon}管理方式"; +$lang->project->cannotCreateChild = "该{$lang->projectCommon}已经有实际的内容,无法直接添加子{$lang->projectCommon}。您可以为当前{$lang->projectCommon}创建一个父{$lang->projectCommon},然后在新的父{$lang->projectCommon}下面添加子{$lang->projectCommon}。"; +$lang->project->hasChildren = "该{$lang->projectCommon}有子{$lang->projectCommon}存在,不能删除。"; +$lang->project->confirmDelete = "您确定删除{$lang->projectCommon}“%s”吗?"; +$lang->project->confirmDisableStoryType = "取消后会将项目和执行内已经关联的相应概念需求全部移除,操作不可逆。"; +$lang->project->cannotChangeToCat = "该{$lang->projectCommon}已经有实际的内容,无法修改为父{$lang->projectCommon}"; +$lang->project->cannotCancelCat = "该{$lang->projectCommon}下已经有子{$lang->projectCommon},无法取消父{$lang->projectCommon}标记"; +$lang->project->parentBeginEnd = "父{$lang->projectCommon}起止时间:%s ~ %s"; +$lang->project->parentBudget = "父项目集预算:"; $lang->project->beginLessThanParent = "{$lang->projectCommon}的开始日期小于了父项目集的开始日期:%s。"; $lang->project->endGreatThanParent = "{$lang->projectCommon}的完成日期大于了父项目集的完成日期:%s。"; diff --git a/module/project/model.php b/module/project/model.php index 788a5732fd..98e83abb29 100755 --- a/module/project/model.php +++ b/module/project/model.php @@ -529,15 +529,11 @@ class projectModel extends model public function getStatData($projectID) { $executions = $this->loadModel('execution')->getPairs($projectID); - $storyTypeCount = $this->dao->select('count(t2.story) as storyCount,t1.type')->from(TABLE_STORY)->alias('t1') + $storyCount = $this->dao->select('count(t2.story) as storyCount')->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id = t2.story') ->where('t2.project')->eq($projectID) ->andWhere('t1.deleted')->eq(0) - ->groupBy('t1.type') - ->fetchPairs('type', 'storyCount'); - - $storyCount = isset($storyTypeCount['story']) ? $storyTypeCount['story'] : 0; - $requirementCount = isset($storyTypeCount['requirement']) ? $storyTypeCount['requirement'] : 0; + ->fetch('storyCount'); $bugCount = $this->dao->select('count(id) as bugCount')->from(TABLE_BUG) ->where('project')->in($projectID) @@ -561,14 +557,13 @@ class projectModel extends model ->fetch('count'); $statData = new stdclass(); - $statData->storyCount = $storyCount; - $statData->requirementCount = $requirementCount; - $statData->bugCount = $bugCount; - $statData->taskCount = $taskCount->count; - $statData->waitCount = $taskCount->waitCount; - $statData->doingCount = $taskCount->doingCount; - $statData->finishedCount = $taskCount->finishedCount; - $statData->delayedCount = $delayedCount; + $statData->storyCount = $storyCount; + $statData->bugCount = $bugCount; + $statData->taskCount = $taskCount->count; + $statData->waitCount = $taskCount->waitCount; + $statData->doingCount = $taskCount->doingCount; + $statData->finishedCount = $taskCount->finishedCount; + $statData->delayedCount = $delayedCount; return $statData; } @@ -1354,6 +1349,12 @@ class projectModel extends model $this->file->updateObjectID((string)$this->post->uid, $projectID, 'project'); // 通过uid更新文件id。 if($oldProject->parent != $project->parent) $this->loadModel('program')->processNode($projectID, $project->parent, $oldProject->path, $oldProject->grade); // 更新项目从属路径。 + if($oldProject->storyType != $project->storyType) + { + /* 编辑项目时如果取消关联需求类型,则把对应类型的需求移除。 */ + $unlinkType = array_diff(explode(',', $oldProject->storyType), explode(',', $project->storyType)); + if($unlinkType) $this->unlinkStoryByType($projectID, $unlinkType); + } if(empty($oldProject->multiple) and $oldProject->model != 'waterfall') $this->loadModel('execution')->syncNoMultipleSprint($projectID); // 无迭代的非瀑布项目需要更新。 if(dao::isError()) return false; @@ -1652,6 +1653,33 @@ class projectModel extends model return !dao::isError(); } + /** + * 删除项目和迭代下对应需求概念的关联关系。 + * Unlink the relationship between the project and the story concept under the iteration and the project. + * + * @param int $projectID + * @param string $storyType + * @access public + * @return void + */ + public function unlinkStoryByType($projectID, $storyType) + { + $idList = $this->dao->select('id')->from(TABLE_PROJECT) + ->where('deleted')->eq(0) + ->andWhere('project')->eq($projectID) + ->fetchPairs(); + + $idList[$projectID] = $projectID; + + $storyIdList = $this->dao->select('t1.story')->from(TABLE_PROJECTSTORY)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') + ->where('t1.project')->in($idList) + ->andWhere('t2.type')->in($storyType) + ->fetchPairs(); + + if($storyIdList) $this->dao->delete()->from(TABLE_PROJECTSTORY)->where('story')->in($storyIdList)->andWhere('project')->in($idList)->exec(); + } + /** * 移除项目成员。 * Unlink a member. diff --git a/module/project/tao.php b/module/project/tao.php index 16334ddb20..613eb1488c 100755 --- a/module/project/tao.php +++ b/module/project/tao.php @@ -850,7 +850,6 @@ class projectTao extends projectModel return $this->dao->select("t1.project, count(t2.id) as allStories, count(if(t2.status = 'active' or t2.status = 'changing', 1, null)) as leftStories, count(if(t2.status = 'closed' and t2.closedReason = 'done', 1, null)) as doneStories")->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story=t2.id') ->where('t1.project')->in($projectIdList) - ->andWhere('t2.type')->eq('story') ->andWhere('deleted')->eq('0') ->groupBy('project') ->fetchAll('project'); @@ -1021,19 +1020,7 @@ class projectTao extends projectModel if(!$hasProduct && in_array($model, $this->config->project->scrumList)) $lang->project->menu->projectplan['link'] = sprintf($lang->project->menu->projectplan['link'], $projectProduct); - /* Init story dropdown for project secondary meun. */ - if($model !== 'kanban' && !empty($this->config->URAndSR) && isset($lang->project->menu->storyGroup)) - { - $lang->project->menu->story = $lang->project->menu->storyGroup; - $lang->project->menu->story['link'] = sprintf($lang->project->menu->storyGroup['link'], '%s', $projectProduct); - $lang->project->menu->story['dropMenu']->story['link'] = sprintf($lang->project->menu->storyGroup['dropMenu']->story['link'], '%s', $projectProduct); - $lang->project->menu->story['dropMenu']->requirement['link'] = sprintf($lang->project->menu->storyGroup['dropMenu']->requirement['link'], '%s', $projectProduct); - if(isset($this->app->params['storyType']) && $this->app->params['storyType'] == 'story') $lang->project->menu->story['dropMenu']->story['subModule'] .= ',projectstory,story'; - if(isset($this->app->params['storyType']) && $this->app->params['storyType'] == 'requirement') $lang->project->menu->story['dropMenu']->requirement['subModule'] .= ',projectstory,story'; - } - if(!$hasProduct) unset($lang->project->menu->settings['subMenu']->products); - if(isset($lang->project->menu->storyGroup)) unset($lang->project->menu->storyGroup); if(!in_array($model, $this->config->project->scrumList)) unset($lang->project->menu->projectplan); return true; } @@ -1051,20 +1038,6 @@ class projectTao extends projectModel protected function setNavGroupMenu(string $navGroup, int $executionID, object $project): bool { global $lang; - /* Single execution and has no product project menu. */ - if(!$project->multiple && !empty($this->config->URAndSR) && isset($lang->$navGroup->menu->storyGroup)) - { - $lang->$navGroup->menu->story = $lang->$navGroup->menu->storyGroup; - $lang->$navGroup->menu->story['link'] = sprintf($lang->$navGroup->menu->storyGroup['link'], '%s', $project->id); - - $lang->$navGroup->menu->story['dropMenu']->story['link'] = sprintf($lang->$navGroup->menu->storyGroup['dropMenu']->story['link'], '%s', $project->id); - $lang->$navGroup->menu->story['dropMenu']->requirement['link'] = sprintf($lang->$navGroup->menu->storyGroup['dropMenu']->requirement['link'], '%s', $project->id); - - if(isset($this->app->params['storyType']) && $this->app->params['storyType'] == 'story') $lang->$navGroup->menu->story['dropMenu']->story['subModule'] = 'story,execution'; - if(isset($this->app->params['storyType']) && $this->app->params['storyType'] == 'requirement') $lang->$navGroup->menu->story['dropMenu']->requirement['subModule'] = 'story,execution'; - } - - if(isset($lang->$navGroup->menu->storyGroup)) unset($lang->$navGroup->menu->storyGroup); foreach($lang->$navGroup->menu as $label => $menu) { $objectID = 0; diff --git a/module/project/ui/common.field.php b/module/project/ui/common.field.php index 3e47503c34..7df1dde350 100644 --- a/module/project/ui/common.field.php +++ b/module/project/ui/common.field.php @@ -130,3 +130,12 @@ $fields->field('auth') ->wrapBefore() ->control('radioList') ->items($lang->project->authList); + +$storyTypeList = array(); +foreach($lang->story->typeList as $key => $text) +{ + $disabled = $key == 'story' ? true : false; + $storyTypeList[] = array('text' => $text, 'value' => $key, 'disabled' => $disabled); +} + +$fields->field('storyType')->control(array('control' => 'checkBox', 'items' => $storyTypeList, 'name' => 'storyType[]')); diff --git a/module/project/ui/create.field.php b/module/project/ui/create.field.php index e05826a2bd..24aa3d5281 100644 --- a/module/project/ui/create.field.php +++ b/module/project/ui/create.field.php @@ -58,3 +58,6 @@ $fields->field('acl') ->control(array('control' => 'aclBox', 'aclItems' => data('programID') ? $lang->project->subAclList : $lang->project->aclList, 'aclValue' => $copyProject ? data('copyProject.acl') : 'open', 'whitelistLabel' => $lang->project->whitelist, 'userValue' => data('copyProjectID') ? data('copyProject.whitelist') : '')); $fields->field('auth')->foldable()->value($copyProject ? data('copyProject.auth') : 'extend'); + +$storyType = in_array($model, array('waterfall', 'waterfallplus', 'ipd')) ? 'story,requirement' : 'story'; +$fields->field('storyType')->foldable()->value($storyType); diff --git a/module/project/ui/create.html.php b/module/project/ui/create.html.php index ed34f9b787..4605d5399a 100644 --- a/module/project/ui/create.html.php +++ b/module/project/ui/create.html.php @@ -81,6 +81,7 @@ formGridPanel ), divider(setClass('h-4 mr-4 ml-2 self-center')) ), + formHidden('storyType[]', 'story'), on::click('[name=name], [name=code], [data-name=begin] .pick *, [name=days], [data-name="parent"] .pick *', 'removeTips'), on::click('[type=submit]', 'removeAllTips'), on::click('[name=multiple]', $toggleLongTime), @@ -90,6 +91,7 @@ formGridPanel on::change('[name=begin], [name=end]', 'computeWorkDays'), on::change('[name^=products]', 'toggleStageBy'), on::change('[name=parent], [name=budget]', 'checkBudget'), + on::change('[name^=storyType]', 'toggleStoryType'), on::init()->do('setTimeout(() => $element.find("[name=longTime]").trigger("change"), 500)'), set::title($lang->project->create), set::fullModeOrders(array('begin,days,PM,budget', !empty($config->setCode) ? 'parent,hasProduct,name,code,begin' : 'parent,name,hasProduct,begin')), diff --git a/module/project/ui/edit.field.php b/module/project/ui/edit.field.php index 4a3d90a39b..fc99bc5c90 100644 --- a/module/project/ui/edit.field.php +++ b/module/project/ui/edit.field.php @@ -21,3 +21,4 @@ if(strpos($config->project->edit->requiredFields, 'budget') === false) $fields-> $fields->field('budget')->value(data('project.budget') !== null && data('project.budget') == 0 ? '' : data('project.budget')); $fields->field('acl')->control(array('control' => 'aclBox', 'aclItems' => data('programID') ? $lang->project->subAclList : $lang->project->aclList, 'aclValue' => data('project.acl'), 'whitelistLabel' => $lang->project->whitelist, 'userValue' => data('project.whitelist'))); +$fields->field('storyType')->foldable()->value(data('project.storyType')); diff --git a/module/project/ui/edit.html.php b/module/project/ui/edit.html.php index 809dcdc460..f0fc6802e0 100644 --- a/module/project/ui/edit.html.php +++ b/module/project/ui/edit.html.php @@ -23,6 +23,7 @@ jsVar('currencySymbol', $lang->project->currencySymbol); jsVar('parentBudget', $lang->project->parentBudget); jsVar('budgetUnitLabel', $lang->project->tenThousandYuan); jsVar('unLinkProductTip', $lang->project->unLinkProductTip); +jsVar('confirmDisableStoryType', $lang->project->confirmDisableStoryType); jsVar('weekend', $config->execution->weekend); jsVar('allProducts', $allProducts); jsVar('branchGroups', $branchGroups); @@ -31,6 +32,7 @@ jsVar('currentProject', $project->id); jsVar('from', $from); jsVar('programID', $programID); jsVar('LONG_TIME', LONG_TIME); +jsVar('storyType', $project->storyType); unset($lang->project->endList['999']); jsVar('endList', $lang->project->endList); @@ -61,12 +63,14 @@ formGridPanel set::items($projectModelItems) ) ), + formHidden('storyType[]', 'story'), on::change('[name=hasProduct]', 'changeType'), on::change('[name=longTime]')->do('const $endPicker = $("[name=end]").zui("datePicker"); $endPicker.render({disabled: $(target).prop("checked")}); if($(target).prop("checked")){ $endPicker.$.setValue(""); $("[name=days]").attr("disabled", "disabled");} else{ $("[name=days]").removeAttr("disabled");}'), on::change('[name=future]', 'toggleBudget'), on::change('[name=begin], [name=end]', 'computeWorkDays'), on::change('[name=parent], [name=budget]', 'checkBudget'), on::change('[name^=products]', 'toggleStageBy'), + on::change('[name^=storyType]', 'toggleStoryType'), set::modeSwitcher(false), set::defaultMode('full'), set::title($lang->project->edit), @@ -74,4 +78,3 @@ formGridPanel ); render(); - diff --git a/module/project/ui/view.html.php b/module/project/ui/view.html.php index 616b764061..6ee6f1d988 100644 --- a/module/project/ui/view.html.php +++ b/module/project/ui/view.html.php @@ -203,7 +203,7 @@ row ( setClass('w-1/3 storyCount'), div(setClass('text-md font-bold'), $statData->storyCount), - span(setClass('text-gray'), $lang->story->common) + span(setClass('text-gray'), $lang->common->story) ), div ( diff --git a/module/project/view/view.html.php b/module/project/view/view.html.php index 3a77502a06..4684743ede 100644 --- a/module/project/view/view.html.php +++ b/module/project/view/view.html.php @@ -247,7 +247,7 @@
- hasProduct) and !empty($config->URAndSR) and $project->model !== 'kanban' and isset($lang->project->menu->storyGroup)):?> + hasProduct) and !empty($config->URAndSR) and $project->model !== 'kanban'):?> diff --git a/module/project/zen.php b/module/project/zen.php index 08ebc446f5..d8f8e6bd4f 100755 --- a/module/project/zen.php +++ b/module/project/zen.php @@ -1,5 +1,6 @@ setIF($this->post->multiple != 'on', 'multiple', '0') ->setIF($this->post->multiple == 'on' || !in_array($this->post->model, array('scrum', 'kanban')) || $this->config->vision == 'lite', 'multiple', '1') ->setIF($this->post->model == 'ipd', 'stageBy', 'project') + ->setIF($this->post->hasProduct == '0', 'storyType', 'story,epic,requirement') ->setDefault('openedBy', $this->app->user->account) ->setDefault('openedDate', helper::now()) ->setDefault('team', $this->post->name) @@ -37,6 +39,7 @@ class projectZen extends project ->add('type', 'project') ->join('whitelist', ',') ->join('auth', ',') + ->join('storyType', ',') ->stripTags($this->config->project->editor->create['id'], $this->config->allowedTags) ->get(); if($this->post->newProduct == 'on') $project->stageBy = 'project'; @@ -98,6 +101,7 @@ class projectZen extends project ->setIF($this->post->budget != 0, 'budget', round((float)$this->post->budget, 2)) ->join('whitelist', ',') ->join('auth', ',') + ->join('storyType', ',') ->stripTags($this->config->project->editor->edit['id'], $this->config->allowedTags) ->remove('products,plans,branch') ->get(); @@ -720,7 +724,6 @@ class projectZen extends project { if($project->hasProduct) { - if($this->config->URAndSR) unset($this->lang->resource->requirement); unset($this->lang->resource->productplan); unset($this->lang->resource->tree); } @@ -736,8 +739,6 @@ class projectZen extends project unset($this->lang->resource->projectstory->unlinkStory); unset($this->lang->resource->projectstory->batchUnlinkStory); unset($this->lang->resource->story->view); - if($this->config->URAndSR) unset($this->lang->resource->requirement->view); - if($this->config->URAndSR) unset($this->lang->resource->requirement->batchChangeBranch); unset($this->lang->resource->tree->browseTask); unset($this->lang->resource->tree->browsehost); unset($this->lang->resource->tree->editHost); diff --git a/module/projectstory/control.php b/module/projectstory/control.php index 0bfe8067a7..9a9b810a7f 100644 --- a/module/projectstory/control.php +++ b/module/projectstory/control.php @@ -78,23 +78,26 @@ class projectStory extends control /** * Obtain the tracking matrix through the product. * - * @param int $productID - * @param int $browseType - * @param int $recTotal - * @param int $recPerPage - * @param int $pageID + * @param int $projectID + * @param int $productID + * @param string $branch + * @param string $browseType allstory|bysearch + * @param int $param + * @param string $storyType + * @param string $orderBy + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID * @access public * @return void */ - public function track(int $projectID = 0, int $productID = 0, int $branch = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) + public function track(int $projectID = 0, int $productID = 0, string $branch = '', string $browseType = 'allstory', int $param = 0, string $storyType = '', string $orderBy = 'id_desc', int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { - $products = $this->loadModel('product')->getProductPairsByProject($projectID); - if(empty($productID)) $productID = key($products); - $project = $this->loadModel('project')->getByID($projectID); $this->session->set('hasProduct', $project->hasProduct); + $this->config->project->showGrades = null; - echo $this->fetch('product', 'track', "productID=$productID&branch=$branch&projectID=$projectID&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); + echo $this->fetch('product', 'track', "productID=$productID&branch=$branch&projectID=$projectID&browseType=$browseType¶m=$param&storyType=$storyType&orderBy=$orderBy&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); } /** @@ -111,7 +114,7 @@ class projectStory extends control $this->session->set('productList', $this->app->getURI(true), 'product'); $story = $this->loadModel('story')->getByID((int)$storyID); - echo $this->fetch('story', 'view', "storyID=$storyID&version=$story->version¶m=" . $this->session->project); + echo $this->fetch($story->type, 'view', "storyID=$storyID&version=$story->version¶m=" . $this->session->project); } /** @@ -197,5 +200,188 @@ class projectStory extends control { echo $this->fetch('execution', 'importPlanStories', "projectID=$projectID&planID=$planID&productID=$productID"); } -} + /** + * 查看需求的报告。 + * 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 = 'story', string $browseType = 'unclosed', int $moduleID = 0, string $chartType = 'pie', int $projectID = 0) + { + echo $this->fetch('story', 'report', "productID=$productID&branchID=$branchID&storyType=$storyType&browseType=$browseType&moduleID=$moduleID&chartType=$chartType&projectID=$projectID"); + } + + /** + * 导出需求数据。 + * Get the data of the stories to export. + * + * @param int $productID + * @param string $orderBy + * @param int $executionID + * @param string $browseType + * @param string $storyType requirement|story + * @access public + * @return void + */ + public function export(int $productID, string $orderBy, int $executionID = 0, string $browseType = '', string $storyType = 'story') + { + echo $this->fetch('story', 'export', "productID=$productID&orderBy=$orderBy&executionID=$executionID&browseType=$browseType&storyType=$storyType"); + } + + /** + * Batch review stories. + * + * @param string $result + * @param string $reason + * @param string $storyType story|requirement + * @access public + * @return void + */ + public function batchReview(string $result, string $reason = '', string $storyType = 'story') + { + echo $this->fetch('story', 'batchReview', "result=$result&reason=$reason&storyType=$storyType"); + } + + /** + * 批量关闭需求。 + * Batch close the stories. + * + * @param int $productID + * @param int $executionID + * @param string $storyType story|requirement + * @param string $from contribute|work + * @access public + * @return void + */ + public function batchClose(int $productID = 0, int $executionID = 0, string $storyType = 'story', string $from = '') + { + echo $this->fetch('story', 'batchClose', "productID=$productID&executionID=$executionID&storyType=$storyType&from=$from"); + } + + + /** + * Batch change the plan of story. + * + * @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"); + } + + /** + * Batch assign to. + * + * @param string $storyType story|requirement + * @access public + * @return void + */ + public function batchAssignTo(string $storyType = 'story', string $assignedTo = '') + { + echo $this->fetch('story', 'batchAssignTo', "storyType=$storyType&assignedTo=$assignedTo"); + } + + + /** + * Batch edit story. + * + * @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 = 'story', string $from = '') + { + echo $this->fetch('story', 'batchEdit', "productID=$productID&executionID=$executionID&branch=$branch&storyType=$storyType&from=$from"); + } + + /** + * 导入需求数据。 + * 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 = 'story', $projectID = 0) + { + echo $this->fetch('story', 'import', "productID=$productID&branch=$branch&storyType=$storyType&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 = 'story', $projectID = 0, $pagerID = 1, $maxImport = 0, $insert = '') + { + echo $this->fetch('story', 'showImport', "productID=$productID&branch=$branch&type=$type&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 = 'story') + { + echo $this->fetch('story', 'exportTemplate', "productID=$productID&branch=$branch&storyType=$storyType"); + } + + /** + * 批量导入需求到需求库。 + * Batch import the data of the requiremens to the library. + * + * @access public + * @return void + */ + public function batchImportToLib() + { + echo $this->fetch('story', 'batchImportToLib'); + } + + /** + * 导入需求到需求库。 + * Import the data of the requiremens to the library. + * + * @param int $storyID + * @access public + * @return void + */ + public function importToLib($storyID) + { + echo $this->fetch('story', 'importToLib', "storyID=$storyID"); + } +} diff --git a/module/projectstory/lang/de.php b/module/projectstory/lang/de.php index fa7153d0d5..b32ecc8288 100644 --- a/module/projectstory/lang/de.php +++ b/module/projectstory/lang/de.php @@ -2,7 +2,7 @@ /* Field. */ $lang->projectstory->project = "{$lang->projectCommon} ID"; $lang->projectstory->product = "{$lang->productCommon} ID"; -$lang->projectstory->story = "{$lang->SRCommon} List"; +$lang->projectstory->story = "Requirement List"; $lang->projectstory->version = "Version"; $lang->projectstory->order = "Order"; @@ -10,15 +10,26 @@ $lang->projectstory->storyCommon = $lang->projectCommon . ' Story'; $lang->projectstory->storyList = $lang->projectCommon . ' Story List'; $lang->projectstory->storyView = $lang->projectCommon . ' Story Detail'; -$lang->projectstory->common = "{$lang->projectCommon} {$lang->SRCommon}"; -$lang->projectstory->index = "{$lang->SRCommon} Home"; -$lang->projectstory->view = "{$lang->SRCommon} Detail"; -$lang->projectstory->story = "{$lang->SRCommon} List"; +$lang->projectstory->common = "{$lang->projectCommon} Requirement"; +$lang->projectstory->index = "Requirement Home"; +$lang->projectstory->view = "Requirement Detail"; +$lang->projectstory->story = "Requirement List"; $lang->projectstory->track = 'Tracking Matrix'; -$lang->projectstory->linkStory = 'Linked' . $lang->SRCommon; -$lang->projectstory->unlinkStory = 'Unlinked' . $lang->SRCommon; -$lang->projectstory->batchUnlinkStory = 'Batch Unlink ' . $lang->SRCommon; -$lang->projectstory->importplanstories = 'Linked by plan' . $lang->SRCommon; +$lang->projectstory->linkStory = 'Linked Requirement'; +$lang->projectstory->unlinkStory = 'Unlinked Requirement'; +$lang->projectstory->report = 'Requirement Report'; +$lang->projectstory->export = 'Export Requirement'; +$lang->projectstory->batchReview = 'Batch Review Requirement'; +$lang->projectstory->batchClose = 'Batch Close Requirement'; +$lang->projectstory->batchChangePlan = 'Batch Change Plan'; +$lang->projectstory->batchAssignTo = 'Batch Assign Requirement'; +$lang->projectstory->batchEdit = 'Batch Edit Requirement'; +$lang->projectstory->importToLib = 'Import To Library'; +$lang->projectstory->batchImportToLib = 'Batch Import To Library'; +$lang->projectstory->importCase = 'Import Requirement'; +$lang->projectstory->exportTemplate = 'Export Template'; +$lang->projectstory->batchUnlinkStory = 'Batch Unlink Requirement'; +$lang->projectstory->importplanstories = 'Linked by plan Requirement'; $lang->projectstory->trackAction = 'Matrix'; $lang->projectstory->confirm = 'Confirm'; @@ -26,11 +37,13 @@ $lang->projectstory->confirm = 'Confirm'; $lang->projectstory->whyNoStories = "No story can be linked. Please check whether there is any story in {$lang->projectCommon} which is linked to {$lang->productCommon} and make sure it has been reviewed."; $lang->projectstory->batchUnlinkTip = "Other requirements are removed. The following requirements are linked to the execution of this {$lang->projectCommon}. Please remove them from the execution first."; -$lang->projectstory->featureBar['story']['allstory'] = 'All'; -$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; -$lang->projectstory->featureBar['story']['draft'] = 'Draft'; -$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; -$lang->projectstory->featureBar['story']['changing'] = 'Changing'; -$lang->projectstory->featureBar['story']['closed'] = 'Closed'; -$lang->projectstory->featureBar['story']['linkedexecution'] = 'Linked ' . $lang->execution->common; -$lang->projectstory->featureBar['story']['unlinkedexecution'] = 'Unlinked ' . $lang->execution->common; +$lang->projectstory->featureBar['story']['allstory'] = 'All'; +$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; +$lang->projectstory->featureBar['story']['draft'] = 'Draft'; +$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; +$lang->projectstory->featureBar['story']['changing'] = 'Changing'; +$lang->projectstory->featureBar['story']['more'] = $lang->more; + +$lang->projectstory->moreSelects['story']['more']['closed'] = 'Closed'; +$lang->projectstory->moreSelects['story']['more']['linkedexecution'] = 'Linked' . $lang->execution->common; +$lang->projectstory->moreSelects['story']['more']['unlinkedexecution'] = 'Unlinked' . $lang->execution->common; diff --git a/module/projectstory/lang/en.php b/module/projectstory/lang/en.php index 27abbcb4e5..dd93c8d558 100644 --- a/module/projectstory/lang/en.php +++ b/module/projectstory/lang/en.php @@ -10,15 +10,26 @@ $lang->projectstory->storyCommon = $lang->projectCommon . ' Story'; $lang->projectstory->storyList = $lang->projectCommon . ' Story List'; $lang->projectstory->storyView = $lang->projectCommon . ' Story Detail'; -$lang->projectstory->common = "{$lang->projectCommon} {$lang->SRCommon}"; -$lang->projectstory->index = "{$lang->SRCommon} Home"; -$lang->projectstory->view = "{$lang->SRCommon} Detail"; -$lang->projectstory->story = "{$lang->SRCommon} List"; +$lang->projectstory->common = "{$lang->projectCommon} Requirement"; +$lang->projectstory->index = "Requirement Home"; +$lang->projectstory->view = "Requirement Detail"; +$lang->projectstory->story = "Requirement List"; $lang->projectstory->track = 'Tracking Matrix'; -$lang->projectstory->linkStory = 'Linked' . $lang->SRCommon; -$lang->projectstory->unlinkStory = 'Unlinked' . $lang->SRCommon; -$lang->projectstory->batchUnlinkStory = 'Batch Unlink ' . $lang->SRCommon; -$lang->projectstory->importplanstories = 'Linked by plan' . $lang->SRCommon; +$lang->projectstory->linkStory = 'Linked Requirement'; +$lang->projectstory->unlinkStory = 'Unlinked Requirement'; +$lang->projectstory->report = 'Requirement Report'; +$lang->projectstory->export = 'Export Requirement'; +$lang->projectstory->batchReview = 'Batch Review Requirement'; +$lang->projectstory->batchClose = 'Batch Close Requirement'; +$lang->projectstory->batchChangePlan = 'Batch Change Plan'; +$lang->projectstory->batchAssignTo = 'Batch Assign Requirement'; +$lang->projectstory->batchEdit = 'Batch Edit Requirement'; +$lang->projectstory->importToLib = 'Import To Library'; +$lang->projectstory->batchImportToLib = 'Batch Import To Library'; +$lang->projectstory->importCase = 'Import Requirement'; +$lang->projectstory->exportTemplate = 'Export Template'; +$lang->projectstory->batchUnlinkStory = 'Batch Unlink Requirement'; +$lang->projectstory->importplanstories = 'Import requirements by plan'; $lang->projectstory->trackAction = 'Matrix'; $lang->projectstory->confirm = 'Confirm'; @@ -26,11 +37,13 @@ $lang->projectstory->confirm = 'Confirm'; $lang->projectstory->whyNoStories = "No story can be linked. Please check whether there is any story in {$lang->projectCommon} which is linked to {$lang->productCommon} and make sure it has been reviewed."; $lang->projectstory->batchUnlinkTip = "Other requirements are removed. The following requirements are linked to the execution of this {$lang->projectCommon}. Please remove them from the execution first."; -$lang->projectstory->featureBar['story']['allstory'] = 'All'; -$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; -$lang->projectstory->featureBar['story']['draft'] = 'Draft'; -$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; -$lang->projectstory->featureBar['story']['changing'] = 'Changing'; -$lang->projectstory->featureBar['story']['closed'] = 'Closed'; -$lang->projectstory->featureBar['story']['linkedexecution'] = 'Linked ' . $lang->execution->common; -$lang->projectstory->featureBar['story']['unlinkedexecution'] = 'Unlinked ' . $lang->execution->common; +$lang->projectstory->featureBar['story']['allstory'] = 'All'; +$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; +$lang->projectstory->featureBar['story']['draft'] = 'Draft'; +$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; +$lang->projectstory->featureBar['story']['changing'] = 'Changing'; +$lang->projectstory->featureBar['story']['more'] = $lang->more; + +$lang->projectstory->moreSelects['story']['more']['closed'] = 'Closed'; +$lang->projectstory->moreSelects['story']['more']['linkedexecution'] = 'Linked' . $lang->execution->common; +$lang->projectstory->moreSelects['story']['more']['unlinkedexecution'] = 'Unlinked' . $lang->execution->common; diff --git a/module/projectstory/lang/fr.php b/module/projectstory/lang/fr.php index fa7153d0d5..bdfa32c532 100644 --- a/module/projectstory/lang/fr.php +++ b/module/projectstory/lang/fr.php @@ -10,13 +10,24 @@ $lang->projectstory->storyCommon = $lang->projectCommon . ' Story'; $lang->projectstory->storyList = $lang->projectCommon . ' Story List'; $lang->projectstory->storyView = $lang->projectCommon . ' Story Detail'; -$lang->projectstory->common = "{$lang->projectCommon} {$lang->SRCommon}"; +$lang->projectstory->common = "{$lang->projectCommon} Requirement"; $lang->projectstory->index = "{$lang->SRCommon} Home"; $lang->projectstory->view = "{$lang->SRCommon} Detail"; $lang->projectstory->story = "{$lang->SRCommon} List"; $lang->projectstory->track = 'Tracking Matrix'; $lang->projectstory->linkStory = 'Linked' . $lang->SRCommon; $lang->projectstory->unlinkStory = 'Unlinked' . $lang->SRCommon; +$lang->projectstory->report = 'Requirement Report'; +$lang->projectstory->export = 'Export Requirement'; +$lang->projectstory->batchReview = 'Batch Review Requirement'; +$lang->projectstory->batchClose = 'Batch Close Requirement'; +$lang->projectstory->batchChangePlan = 'Batch Change Plan'; +$lang->projectstory->batchAssignTo = 'Batch Assign Requirement'; +$lang->projectstory->batchEdit = 'Batch Edit Requirement'; +$lang->projectstory->importToLib = 'Import To Library'; +$lang->projectstory->batchImportToLib = 'Batch Import To Library'; +$lang->projectstory->importCase = 'Import Requirement'; +$lang->projectstory->exportTemplate = 'Export Template'; $lang->projectstory->batchUnlinkStory = 'Batch Unlink ' . $lang->SRCommon; $lang->projectstory->importplanstories = 'Linked by plan' . $lang->SRCommon; $lang->projectstory->trackAction = 'Matrix'; @@ -26,11 +37,13 @@ $lang->projectstory->confirm = 'Confirm'; $lang->projectstory->whyNoStories = "No story can be linked. Please check whether there is any story in {$lang->projectCommon} which is linked to {$lang->productCommon} and make sure it has been reviewed."; $lang->projectstory->batchUnlinkTip = "Other requirements are removed. The following requirements are linked to the execution of this {$lang->projectCommon}. Please remove them from the execution first."; -$lang->projectstory->featureBar['story']['allstory'] = 'All'; -$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; -$lang->projectstory->featureBar['story']['draft'] = 'Draft'; -$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; -$lang->projectstory->featureBar['story']['changing'] = 'Changing'; -$lang->projectstory->featureBar['story']['closed'] = 'Closed'; -$lang->projectstory->featureBar['story']['linkedexecution'] = 'Linked ' . $lang->execution->common; -$lang->projectstory->featureBar['story']['unlinkedexecution'] = 'Unlinked ' . $lang->execution->common; +$lang->projectstory->featureBar['story']['allstory'] = 'All'; +$lang->projectstory->featureBar['story']['unclosed'] = 'Open'; +$lang->projectstory->featureBar['story']['draft'] = 'Draft'; +$lang->projectstory->featureBar['story']['reviewing'] = 'Reviewing'; +$lang->projectstory->featureBar['story']['changing'] = 'Changing'; +$lang->projectstory->featureBar['story']['more'] = $lang->more; + +$lang->projectstory->moreSelects['story']['more']['closed'] = 'Closed'; +$lang->projectstory->moreSelects['story']['more']['linkedexecution'] = 'Linked' . $lang->execution->common; +$lang->projectstory->moreSelects['story']['more']['unlinkedexecution'] = 'Unlinked' . $lang->execution->common; diff --git a/module/projectstory/lang/zh-cn.php b/module/projectstory/lang/zh-cn.php index 90b6474979..27f9c00952 100644 --- a/module/projectstory/lang/zh-cn.php +++ b/module/projectstory/lang/zh-cn.php @@ -10,27 +10,40 @@ $lang->projectstory->storyCommon = $lang->projectCommon . '需求'; $lang->projectstory->storyList = $lang->projectCommon . '需求列表'; $lang->projectstory->storyView = $lang->projectCommon . '需求详情'; -$lang->projectstory->common = "{$lang->projectCommon}{$lang->SRCommon}"; -$lang->projectstory->index = "{$lang->SRCommon}主页"; -$lang->projectstory->view = "{$lang->SRCommon}详情"; -$lang->projectstory->story = "{$lang->SRCommon}列表"; +$lang->projectstory->common = "{$lang->projectCommon}需求"; +$lang->projectstory->index = "需求主页"; +$lang->projectstory->view = "需求详情"; +$lang->projectstory->story = "需求列表"; $lang->projectstory->track = '矩阵'; -$lang->projectstory->linkStory = '关联' . $lang->SRCommon; -$lang->projectstory->unlinkStory = '移除' . $lang->SRCommon; -$lang->projectstory->batchUnlinkStory = '批量移除' . $lang->SRCommon; -$lang->projectstory->importplanstories = '按计划关联' . $lang->SRCommon; +$lang->projectstory->linkStory = '关联需求'; +$lang->projectstory->unlinkStory = '移除需求'; +$lang->projectstory->report = '统计报表'; +$lang->projectstory->export = '导出需求'; +$lang->projectstory->batchReview = '批量评审需求'; +$lang->projectstory->batchClose = '批量关闭需求'; +$lang->projectstory->batchChangePlan = '批量修改计划'; +$lang->projectstory->batchAssignTo = '批量指派需求'; +$lang->projectstory->batchEdit = '批量编辑需求'; +$lang->projectstory->importToLib = '导入需求库'; +$lang->projectstory->batchImportToLib = '批量导入需求库'; +$lang->projectstory->importCase = '导入需求'; +$lang->projectstory->exportTemplate = '导出模板'; +$lang->projectstory->batchUnlinkStory = '批量移除需求'; +$lang->projectstory->importplanstories = '按计划关联需求'; $lang->projectstory->trackAction = '跟踪矩阵'; $lang->projectstory->confirm = '确定'; /* Notice. */ -$lang->projectstory->whyNoStories = "看起来没有{$lang->SRCommon}可以关联。请检查下{$lang->projectCommon}关联的{$lang->productCommon}中有没有{$lang->SRCommon},而且要确保它们已经审核通过。"; +$lang->projectstory->whyNoStories = "看起来没有需求可以关联。请检查下{$lang->projectCommon}关联的{$lang->productCommon}中有没有需求,而且要确保它们已经审核通过。"; $lang->projectstory->batchUnlinkTip = "其他需求已经移除,如下需求已与该{$lang->projectCommon}下执行相关联,请从执行中移除后再操作。"; -$lang->projectstory->featureBar['story']['allstory'] = '全部'; -$lang->projectstory->featureBar['story']['unclosed'] = '未关闭'; -$lang->projectstory->featureBar['story']['draft'] = '草稿'; -$lang->projectstory->featureBar['story']['reviewing'] = '评审中'; -$lang->projectstory->featureBar['story']['changing'] = '变更中'; -$lang->projectstory->featureBar['story']['closed'] = '已关闭'; -$lang->projectstory->featureBar['story']['linkedexecution'] = '已关联' . $lang->execution->common; -$lang->projectstory->featureBar['story']['unlinkedexecution'] = '未关联' . $lang->execution->common; +$lang->projectstory->featureBar['story']['allstory'] = '全部'; +$lang->projectstory->featureBar['story']['unclosed'] = '未关闭'; +$lang->projectstory->featureBar['story']['draft'] = '草稿'; +$lang->projectstory->featureBar['story']['reviewing'] = '评审中'; +$lang->projectstory->featureBar['story']['changing'] = '变更中'; +$lang->projectstory->featureBar['story']['more'] = $lang->more; + +$lang->projectstory->moreSelects['story']['more']['closed'] = '已关闭'; +$lang->projectstory->moreSelects['story']['more']['linkedexecution'] = '已关联' . $lang->execution->common; +$lang->projectstory->moreSelects['story']['more']['unlinkedexecution'] = '未关联' . $lang->execution->common; diff --git a/module/release/control.php b/module/release/control.php index 51b4f3748e..e4dd452e75 100644 --- a/module/release/control.php +++ b/module/release/control.php @@ -372,13 +372,14 @@ class release extends control /* Load pager. */ $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); + $excludeStoryIdList = $this->releaseZen->getExcludeStoryIdList($release); if($browseType == 'bySearch') { - $allStories = $this->story->getBySearch($release->product, $release->branch, $queryID, 'id_desc', $executionIdList, 'story', $release->stories, 'draft,reviewing,changing', $pager); + $allStories = $this->story->getBySearch($release->product, $release->branch, $queryID, 'id_desc', $executionIdList, 'story', $excludeStoryIdList, 'draft,reviewing,changing', $pager); } else { - $allStories = $this->story->batchGetExecutionStories(implode(',', $executionIdList), $release->product, 't1.`order`_desc', 'byBranch', $release->branch, 'story', $release->stories, $pager); + $allStories = $this->story->batchGetExecutionStories(implode(',', $executionIdList), $release->product, 't1.`order`_desc', 'byBranch', $release->branch, 'story', $excludeStoryIdList, $pager); } $this->view->allStories = $allStories; @@ -386,6 +387,8 @@ class release extends control $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->view->browseType = $browseType; $this->view->param = $param; + $this->view->grades = $this->loadModel('story')->getGradePairs('story', 'all'); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->pager = $pager; if($this->app->tab == 'project') $this->view->projectID = $release->project; diff --git a/module/release/js/linkstory.ui.js b/module/release/js/linkstory.ui.js new file mode 100644 index 0000000000..92ab722fc3 --- /dev/null +++ b/module/release/js/linkstory.ui.js @@ -0,0 +1,13 @@ +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/release/js/view.ui.js b/module/release/js/view.ui.js index 23d834cf6a..fe97589dcb 100644 --- a/module/release/js/view.ui.js +++ b/module/release/js/view.ui.js @@ -132,3 +132,17 @@ window.onSearchLinks = function(type, result) showLink(type, params ? atob(params[1]) : null, true); return false; }; + +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/release/ui/linkstory.html.php b/module/release/ui/linkstory.html.php index d0e39cda70..70eda62157 100644 --- a/module/release/ui/linkstory.html.php +++ b/module/release/ui/linkstory.html.php @@ -10,6 +10,9 @@ declare(strict_types=1); */ namespace zin; +jsVar('showGrade', $showGrade); +jsVar('grades', $grades); + $buildModule = $app->rawModule == 'projectrelease' ? 'projectrelease' : 'release'; $cols = array(); @@ -36,6 +39,7 @@ dtable set::userMap($users), set::cols($cols), set::data($allStories), + set::onRenderCell(jsRaw('window.renderStoryCell')), set::updateCheckedRows($checkedRows), set::loadPartial(true), set::footToolbar(array diff --git a/module/release/ui/view.html.php b/module/release/ui/view.html.php index 3dd32b5535..429a81bd69 100644 --- a/module/release/ui/view.html.php +++ b/module/release/ui/view.html.php @@ -60,6 +60,8 @@ detailHeader ); jsVar('releaseID', $release->id); +jsVar('showGrade', $showGrade); +jsVar('grades', $grades); /* Table data and setting for finished stories tab. */ jsVar('summary', $summary); @@ -221,6 +223,7 @@ detailBody set::checkable($canBatchUnlinkStory || $canBatchCloseStory), set::sortLink(createLink($releaseModule, 'view', "releaseID={$release->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('recPerPage' => $storyPager->recPerPage, 'recTotal' => $storyPager->recTotal, 'linkCreator' => createLink($releaseModule, 'view', "releaseID={$release->id}&type=story&link={$link}¶m={$param}&orderBy={$orderBy}&recTotal={$storyPager->recTotal}&recPerPage={recPerPage}&page={page}")))), diff --git a/module/release/zen.php b/module/release/zen.php index e0a5b118af..20f6510e91 100644 --- a/module/release/zen.php +++ b/module/release/zen.php @@ -155,6 +155,32 @@ class releaseZen extends release return $releaseQuery; } + /** + * 获取发布不可关联的需求ID列表。 + * Get the story id list that can not be linked to the release. + * + * @param object $release + * @access protected + * @return array + */ + public function getExcludeStoryIdList(object $release): array + { + $parentIdList = $this->dao->select('id')->from(TABLE_STORY) + ->where('product')->eq($release->product) + ->andWhere('type')->eq('story') + ->andWhere('isParent')->eq('1') + ->andWhere('status')->notIN('draft,reviewing,changing') + ->fetchPairs(); + + foreach(explode(',', $release->stories) as $storyID) + { + if(!$storyID) continue; + if(!isset($parentIdList[$storyID])) $parentIdList[$storyID] = $storyID; + } + + return $parentIdList; + } + /** * 生成的发布详情页面的需求数据。 * Generate the story data for the release view page. @@ -195,6 +221,8 @@ class releaseZen extends release $this->view->orderBy = $orderBy; $this->view->type = $type; $this->view->link = $link; + $this->view->grades = $this->loadModel('story')->getGradePairs('story', 'all'); + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->param = $param; $this->view->storyCases = $this->loadModel('testcase')->getStoryCaseCounts(array_keys($stories)); $this->view->summary = $this->product->summary($stories); @@ -228,8 +256,10 @@ class releaseZen extends release unset($this->config->product->search['fields']['product']); unset($this->config->product->search['fields']['project']); + unset($this->config->product->search['fields']['grade']); unset($this->config->product->search['params']['product']); unset($this->config->product->search['params']['project']); + unset($this->config->product->search['params']['grade']); $this->config->product->search['actionURL'] = $this->createLink($this->app->rawModule, 'view', "releaseID={$release->id}&type=story&link=true¶m=" . helper::safe64Encode('&browseType=bySearch&queryID=myQueryID')); $this->config->product->search['queryID'] = $queryID; diff --git a/module/repo/zen.php b/module/repo/zen.php index 3a13fe99b2..092a3e6929 100644 --- a/module/repo/zen.php +++ b/module/repo/zen.php @@ -1011,6 +1011,8 @@ class repoZen extends repo $productStories = $this->loadModel('story')->getBySearch($productID, 0, $queryID, $orderBy, 0, 'story', array_keys($linkedStories)); $allStories = array_merge($allStories, $productStories); } + + $allStories = array_filter($allStories, function($story) { return $story->isParent == '0'; }); } else { diff --git a/module/requirement/config.php b/module/requirement/config.php new file mode 100644 index 0000000000..83c6648ea1 --- /dev/null +++ b/module/requirement/config.php @@ -0,0 +1,4 @@ +loadConfig('story'); +$config->requirement = clone $config->story; diff --git a/module/requirement/control.php b/module/requirement/control.php new file mode 100644 index 0000000000..020eddd668 --- /dev/null +++ b/module/requirement/control.php @@ -0,0 +1,433 @@ +fetch('story', 'create', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=$storyID&objectID=$objectID&bugID=$bugID&planID=$planID&todoID=$todoID&extra=$extra&storyType=requirement"); + } + + /** + * 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 = 'requirement', string $extra = '') + { + echo $this->fetch('story', 'batchCreate', "productID=$productID&branch=$branch&moduleID=$moduleID&storyID=$storyID&executionID=$executionID&plan=$plan&storyType=requirement&extra=$extra"); + } + + /** + * View a requirement. + * + * @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=requirement"); + } + + /** + * Edit a requirement. + * + * @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=requirement"); + } + + /** + * Batch edit requirement. + * + * @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 = 'requirement', string $from = '') + { + echo $this->fetch('story', 'batchEdit', "productID=$productID&executionID=$executionID&branch=$branch&storyType=requirement&from=$from"); + } + + /** + * 用户需求详情页,关联软件需求,将用户需求关联到软件需求。 + * Link story and requirement. + * + * @param int $storyID + * @param string $type linkStories|linkRelateUR|linkRelateSR + * @param int $linkedStoryID + * @param string $browseType ''|bySearch + * @param int $queryID + * @access public + * @return void + */ + public function linkStory(int $storyID, string $type = 'linkStories', int $linkedStoryID = 0, string $browseType = '', int $queryID = 0) + { + echo $this->fetch('story', 'linkStory', "storyID=$storyID&type=$type&linkedStoryID=$linkedStoryID&browseType=$browseType&queryID=$queryID&storyType=requirement"); + } + + /** + * 导出需求数据。 + * 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=requirement"); + } + + /** + * 导入需求数据。 + * 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 = 'requirement', $projectID = 0) + { + echo $this->fetch('story', 'import', "productID=$productID&branch=$branch&storyType=requirement&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 = 'requirement', $projectID = 0, $pagerID = 1, $maxImport = 0, $insert = '') + { + echo $this->fetch('story', 'showImport', "productID=$productID&branch=$branch&type=requirement&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 = 'requirement') + { + echo $this->fetch('story', 'exportTemplate', "productID=$productID&branch=$branch&storyType=requirement"); + } + + /** + * Delete a requirement. + * + * @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=requirement"); + } + + /** + * Change a requirement. + * + * @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=requirement"); + } + + /** + * Review a requirement. + * + * @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=requirement"); + } + + /** + * Submit review. + * + * @param int $storyID + * @access public + * @return void + */ + public function submitReview(int $storyID) + { + echo $this->fetch('story', 'submitReview', "storyID=$storyID&storyType=requirement"); + } + + /** + * Batch review requirements. + * + * @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=requirement"); + } + + /** + * Recall the requirement review or requirement 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=requirement"); + } + + /** + * 需求的指派给页面。 + * Assign the requirement to a user. + * + * @param int $storyID + * @access public + * @return void + */ + public function assignTo(int $storyID) + { + echo $this->fetch('story', 'assignTo', "storyID=$storyID"); + } + + /** + * 关闭需求。 + * Close the requirement. + * + * @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=requirement"); + } + + /** + * 批量关闭需求。 + * 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 = 'requirement', string $from = '') + { + echo $this->fetch('story', 'batchClose', "productID=$productID&executionID=$executionID&storyType=requirement&from=$from"); + } + + /** + * 激活需求。 + * Activate a requirement. + * + * @param int $storyID + * @access public + * @return void + */ + public function activate(int $storyID) + { + echo $this->fetch('story', 'activate', "storyID=$storyID&storyType=requirement"); + } + + /** + * 查看需求的报告。 + * 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 = 'requirement', string $browseType = 'unclosed', int $moduleID = 0, string $chartType = 'pie', int $projectID = 0) + { + echo $this->fetch('story', 'report', "productID=$productID&branchID=$branchID&storyType=requirement&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=requirement"); + } + + /** + * Batch change parent. + * + * @param int $productID + * @param string $storyType + * @access public + * @return void + */ + public function batchChangeParent(int $productID = 0, string $storyType = 'requirement') + { + echo $this->fetch('story', 'batchChangeParent', "productID=$productID&storyType=requirement"); + } + + /** + * 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 assign to. + * + * @param string $storyType story|requirement + * @access public + * @return void + */ + public function batchAssignTo(string $storyType = 'requirement', string $assignedTo = '') + { + echo $this->fetch('story', 'batchAssignTo', "storyType=requirement&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=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"); + } + + /** + * 关联用户需求。 + * 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"); + } + + /** + * 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 = 'requirement') + { + echo $this->fetch('story', 'ajaxGetUserStories', "userID=$userID&id=$id&appendID=$appendID&storyType=requirement"); + } +} diff --git a/module/requirement/lang/de.php b/module/requirement/lang/de.php new file mode 100644 index 0000000000..dcd95a6c84 --- /dev/null +++ b/module/requirement/lang/de.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->requirement = clone $lang->story; + +foreach($lang->requirement as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->requirement->$key = str_replace($lang->SRCommon, $lang->URCommon, $value); +} + +$lang->requirement->common = $lang->URCommon; + +$lang->requirement->stageList = array(); +$lang->requirement->stageList[''] = ''; +$lang->requirement->stageList['defining'] = 'Defining'; +$lang->requirement->stageList['planning'] = 'Planning'; +$lang->requirement->stageList['developing'] = 'Developing'; +$lang->requirement->stageList['delivering'] = 'Delivering'; +$lang->requirement->stageList['closed'] = 'Closed'; diff --git a/module/requirement/lang/en.php b/module/requirement/lang/en.php new file mode 100644 index 0000000000..dcd95a6c84 --- /dev/null +++ b/module/requirement/lang/en.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->requirement = clone $lang->story; + +foreach($lang->requirement as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->requirement->$key = str_replace($lang->SRCommon, $lang->URCommon, $value); +} + +$lang->requirement->common = $lang->URCommon; + +$lang->requirement->stageList = array(); +$lang->requirement->stageList[''] = ''; +$lang->requirement->stageList['defining'] = 'Defining'; +$lang->requirement->stageList['planning'] = 'Planning'; +$lang->requirement->stageList['developing'] = 'Developing'; +$lang->requirement->stageList['delivering'] = 'Delivering'; +$lang->requirement->stageList['closed'] = 'Closed'; diff --git a/module/requirement/lang/fr.php b/module/requirement/lang/fr.php new file mode 100644 index 0000000000..dcd95a6c84 --- /dev/null +++ b/module/requirement/lang/fr.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->requirement = clone $lang->story; + +foreach($lang->requirement as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->requirement->$key = str_replace($lang->SRCommon, $lang->URCommon, $value); +} + +$lang->requirement->common = $lang->URCommon; + +$lang->requirement->stageList = array(); +$lang->requirement->stageList[''] = ''; +$lang->requirement->stageList['defining'] = 'Defining'; +$lang->requirement->stageList['planning'] = 'Planning'; +$lang->requirement->stageList['developing'] = 'Developing'; +$lang->requirement->stageList['delivering'] = 'Delivering'; +$lang->requirement->stageList['closed'] = 'Closed'; diff --git a/module/requirement/lang/zh-cn.php b/module/requirement/lang/zh-cn.php new file mode 100644 index 0000000000..2f7e11156a --- /dev/null +++ b/module/requirement/lang/zh-cn.php @@ -0,0 +1,20 @@ +loadLang('story'); +$lang->requirement = clone $lang->story; + +foreach($lang->requirement as $key => $value) +{ + if(!is_string($value)) continue; + if(strpos($value, $lang->SRCommon) !== false) $lang->requirement->$key = str_replace($lang->SRCommon, $lang->URCommon, $value); +} + +$lang->requirement->common = $lang->URCommon; + +$lang->requirement->stageList = array(); +$lang->requirement->stageList[''] = ''; +$lang->requirement->stageList['defining'] = '定义中'; +$lang->requirement->stageList['planning'] = '规划中'; +$lang->requirement->stageList['developing'] = '研发中'; +$lang->requirement->stageList['delivering'] = '交付中'; +$lang->requirement->stageList['closed'] = '已关闭'; diff --git a/module/requirement/model.php b/module/requirement/model.php new file mode 100644 index 0000000000..569db25608 --- /dev/null +++ b/module/requirement/model.php @@ -0,0 +1,10 @@ +control->loadModel('story'); + return call_user_func_array(array('storyModel', 'isClickable'), array($data, $action)); + } +} diff --git a/module/search/config.php b/module/search/config.php index 11c1d145af..6d1b7686ff 100644 --- a/module/search/config.php +++ b/module/search/config.php @@ -82,6 +82,13 @@ $config->search->fields->requirement->content = 'keywords,spec,verify'; $config->search->fields->requirement->addedDate = 'openedDate'; $config->search->fields->requirement->editedDate = 'lastEditedDate'; +$config->search->fields->epic = new stdclass(); +$config->search->fields->epic->id = 'id'; +$config->search->fields->epic->title = 'title'; +$config->search->fields->epic->content = 'keywords,spec,verify'; +$config->search->fields->epic->addedDate = 'openedDate'; +$config->search->fields->epic->editedDate = 'lastEditedDate'; + $config->search->fields->task = new stdclass(); $config->search->fields->task->id = 'id'; $config->search->fields->task->title = 'name'; diff --git a/module/search/lang/de.php b/module/search/lang/de.php index e0f6a4fe07..b23ed3a6f0 100644 --- a/module/search/lang/de.php +++ b/module/search/lang/de.php @@ -101,10 +101,12 @@ $lang->search->modules['project'] = $lang->projectCommon; $lang->search->modules['execution'] = $lang->execution->common; $lang->search->modules['story'] = $lang->SRCommon; $lang->search->modules['requirement'] = $lang->URCommon; +$lang->search->modules['epic'] = $lang->ERCommon; $lang->search->modules['aiapp'] = 'AI'; $lang->search->objectTypeList['story'] = $lang->SRCommon; $lang->search->objectTypeList['requirement'] = $lang->URCommon; +$lang->search->objectTypeList['epic'] = $lang->ERCommon; $lang->search->objectTypeList['stage'] = 'stage'; $lang->search->objectTypeList['sprint'] = $lang->execution->common; $lang->search->objectTypeList['kanban'] = 'kanban'; diff --git a/module/search/lang/en.php b/module/search/lang/en.php index 9a4c6341a4..8cc71b5245 100644 --- a/module/search/lang/en.php +++ b/module/search/lang/en.php @@ -101,10 +101,12 @@ $lang->search->modules['project'] = $lang->projectCommon; $lang->search->modules['execution'] = $lang->execution->common; $lang->search->modules['story'] = $lang->SRCommon; $lang->search->modules['requirement'] = $lang->URCommon; +$lang->search->modules['epic'] = $lang->ERCommon; $lang->search->modules['aiapp'] = 'AI'; $lang->search->objectTypeList['story'] = $lang->SRCommon; $lang->search->objectTypeList['requirement'] = $lang->URCommon; +$lang->search->objectTypeList['epic'] = $lang->ERCommon; $lang->search->objectTypeList['stage'] = 'stage'; $lang->search->objectTypeList['sprint'] = $lang->execution->common; $lang->search->objectTypeList['kanban'] = 'kanban'; diff --git a/module/search/lang/fr.php b/module/search/lang/fr.php index ac7c0e58a9..cac84edc01 100644 --- a/module/search/lang/fr.php +++ b/module/search/lang/fr.php @@ -101,10 +101,12 @@ $lang->search->modules['project'] = $lang->projectCommon; $lang->search->modules['execution'] = $lang->execution->common; $lang->search->modules['story'] = $lang->SRCommon; $lang->search->modules['requirement'] = $lang->URCommon; +$lang->search->modules['epic'] = $lang->ERCommon; $lang->search->modules['aiapp'] = 'AI'; $lang->search->objectTypeList['story'] = $lang->SRCommon; $lang->search->objectTypeList['requirement'] = $lang->URCommon; +$lang->search->objectTypeList['epic'] = $lang->ERCommon; $lang->search->objectTypeList['stage'] = 'stage'; $lang->search->objectTypeList['sprint'] = $lang->execution->common; $lang->search->objectTypeList['kanban'] = 'kanban'; diff --git a/module/search/lang/zh-cn.php b/module/search/lang/zh-cn.php index 6dc43fe290..243bb4115f 100644 --- a/module/search/lang/zh-cn.php +++ b/module/search/lang/zh-cn.php @@ -101,10 +101,12 @@ $lang->search->modules['project'] = $lang->projectCommon; $lang->search->modules['execution'] = $lang->execution->common; $lang->search->modules['story'] = $lang->SRCommon; $lang->search->modules['requirement'] = $lang->URCommon; +$lang->search->modules['epic'] = $lang->ERCommon; $lang->search->modules['aiapp'] = 'AI'; $lang->search->objectTypeList['story'] = $lang->SRCommon; $lang->search->objectTypeList['requirement'] = $lang->URCommon; +$lang->search->objectTypeList['epic'] = $lang->ERCommon; $lang->search->objectTypeList['stage'] = '阶段'; $lang->search->objectTypeList['sprint'] = $lang->execution->common; $lang->search->objectTypeList['kanban'] = '看板'; diff --git a/module/search/tao.php b/module/search/tao.php index 8cab6878fa..b826ee0524 100644 --- a/module/search/tao.php +++ b/module/search/tao.php @@ -769,7 +769,7 @@ class searchTao extends searchModel if($module == 'issue') $fields = ($this->config->edition == 'max' || $this->config->edition == 'ipd') ? 'id,project,owner,lib' : 'id,project,owner'; if($module == 'project') $fields = 'id,model'; if($module == 'execution') $fields = 'id,type,project'; - if($module == 'story' || $module == 'requirement') $fields = ($this->config->edition == 'max' || $this->config->edition == 'ipd') ? 'id,type,lib' : 'id,type'; + if(in_array($module, array('story', 'requirement', 'epic'))) $fields = ($this->config->edition == 'max' || $this->config->edition == 'ipd') ? 'id,type,lib' : 'id,type'; if(($module == 'risk' || $module == 'opportunity') && ($this->config->edition == 'max' || $this->config->edition == 'ipd')) $fields = 'id,lib'; if($module == 'doc' && ($this->config->edition == 'max' || $this->config->edition == 'ipd')) $fields = 'id,assetLib,assetLibType'; @@ -963,7 +963,7 @@ class searchTao extends searchModel if($module == 'issue') return $this->processIssueRecord($record, $objectList); if($module == 'project') return $this->processProjectRecord($record, $objectList); if($module == 'execution') return $this->processExecutionRecord($record, $objectList); - if($module == 'story' || $module == 'requirement') return $this->processStoryRecord($record, $module, $objectList); + if(in_array($module, array('story', 'requirement', 'epic'))) return $this->processStoryRecord($record, $module, $objectList); if(($module == 'risk' || $module == 'opportunity') && ($this->config->edition == 'max' || $this->config->edition == 'ipd')) return $this->processRiskRecord($record, $module, $objectList); if($module == 'doc' && ($this->config->edition == 'max' || $this->config->edition == 'ipd')) return $this->processDocRecord($record, $objectList); @@ -1046,7 +1046,7 @@ class searchTao extends searchModel } $module = 'story'; - $method = 'view'; + $method = 'storyView'; if(!empty($story->lib)) { $module = 'assetlib'; diff --git a/module/story/config.php b/module/story/config.php index 9be5cce06b..5c46866582 100644 --- a/module/story/config.php +++ b/module/story/config.php @@ -33,16 +33,16 @@ $config->story->editor->assignto = array('id' => 'comment', 'tools' => 'simpleTo $config->story->list = new stdclass(); $config->story->exportFields = ' - id, product, branch, module, plan, source, sourceNote, title, spec, verify, keywords, + id, product, branch, module, plan, source, sourceNote, title, spec, verify, grade, parent, keywords, pri, estimate, status, stage, category, taskCountAB, bugCountAB, caseCountAB, openedBy, openedDate, assignedTo, assignedDate, mailto, reviewedBy, reviewedDate, closedBy, closedDate, closedReason, lastEditedBy, lastEditedDate, - childStories, linkStories, duplicateStory, files'; + duplicateStory, files'; $config->story->list->customCreateFields = ''; -$config->story->list->customBatchCreateFields = 'plan,assignedTo,spec,source,verify,pri,estimate,URS,parent,keywords,mailto'; +$config->story->list->customBatchCreateFields = 'plan,assignedTo,spec,source,verify,pri,estimate,keywords,mailto'; $config->story->list->customBatchEditFields = 'branch,plan,estimate,pri,assignedTo,source,stage,closedBy,closedReason,keywords'; $config->story->list->actionsOperatedParentStory = ',edit,batchcreate,change,review,recall,submitreview,processstorychange,'; diff --git a/module/story/config/dtable.php b/module/story/config/dtable.php index 2843f7ec9c..309cb018f4 100644 --- a/module/story/config/dtable.php +++ b/module/story/config/dtable.php @@ -26,7 +26,7 @@ if($app->tab == 'execution') $config->story->dtable->fieldList['title']['name'] = 'title'; $config->story->dtable->fieldList['title']['title'] = $lang->story->title; $config->story->dtable->fieldList['title']['type'] = 'title'; -$config->story->dtable->fieldList['title']['link'] = array('url' => helper::createLink('story', 'view', 'storyID={id}')); +$config->story->dtable->fieldList['title']['link'] = array('url' => helper::createLink('{type}', 'view', 'storyID={id}')); $config->story->dtable->fieldList['title']['fixed'] = 'left'; $config->story->dtable->fieldList['title']['sortType'] = true; $config->story->dtable->fieldList['title']['minWidth'] = '342'; @@ -58,6 +58,7 @@ $config->story->dtable->fieldList['plan']['title'] = $lang->story->planAB; $config->story->dtable->fieldList['plan']['sortType'] = true; $config->story->dtable->fieldList['plan']['width'] = '136'; $config->story->dtable->fieldList['plan']['show'] = true; +$config->story->dtable->fieldList['plan']['control'] = 'multiple'; $config->story->dtable->fieldList['plan']['group'] = 4; $config->story->dtable->fieldList['plan']['dataSource'] = array('module' => 'productplan', 'method' => 'getPairs', 'params' => '$productID'); @@ -201,23 +202,11 @@ $config->story->dtable->fieldList['caseCount']['link'] = "RAWJSstory->dtable->fieldList['caseCount']['data-toggle'] = 'modal'; $config->story->dtable->fieldList['caseCount']['group'] = 7; -$config->story->dtable->fieldList['URS']['name'] = 'URS'; -$config->story->dtable->fieldList['URS']['title'] = 'UR'; -$config->story->dtable->fieldList['URS']['sortType'] = false; -$config->story->dtable->fieldList['URS']['width'] = '50'; -$config->story->dtable->fieldList['URS']['type'] = 'text'; -$config->story->dtable->fieldList['URS']['link'] = $config->edition != 'open' && common::hasPriv('story', 'relation') ? helper::createLink('story', 'relation', 'storyID={id}&storyType={type}') : ''; -$config->story->dtable->fieldList['URS']['data-toggle'] = 'modal'; -$config->story->dtable->fieldList['URS']['group'] = 7; - -$config->story->dtable->fieldList['SRS']['name'] = 'SRS'; -$config->story->dtable->fieldList['SRS']['title'] = 'SR'; -$config->story->dtable->fieldList['SRS']['sortType'] = false; -$config->story->dtable->fieldList['SRS']['width'] = '50'; -$config->story->dtable->fieldList['SRS']['type'] = 'text'; -$config->story->dtable->fieldList['SRS']['link'] = $config->edition != 'open' && common::hasPriv('story', 'relation') ? helper::createLink('story', 'relation', 'storyID={id}&storyType={type}') : ''; -$config->story->dtable->fieldList['SRS']['data-toggle'] = 'modal'; -$config->story->dtable->fieldList['SRS']['group'] = 7; +$config->story->dtable->fieldList['childItem']['name'] = 'childItem'; +$config->story->dtable->fieldList['childItem']['title'] = $lang->story->childItem; +$config->story->dtable->fieldList['childItem']['sortType'] = false; +$config->story->dtable->fieldList['childItem']['type'] = 'text'; +$config->story->dtable->fieldList['childItem']['group'] = 6; $config->story->dtable->fieldList['closedBy']['name'] = 'closedBy'; $config->story->dtable->fieldList['closedBy']['title'] = $lang->story->closedBy; @@ -303,7 +292,7 @@ $config->story->dtable->fieldList['actions']['title'] = $lang->actions; $config->story->dtable->fieldList['actions']['fixed'] = 'right'; $config->story->dtable->fieldList['actions']['required'] = true; $config->story->dtable->fieldList['actions']['width'] = 'auto'; -$config->story->dtable->fieldList['actions']['minWidth'] = $app->tab == 'project' ? 250 : 200; +$config->story->dtable->fieldList['actions']['minWidth'] = $app->tab != 'product' ? 240 : 200; $config->story->dtable->fieldList['actions']['type'] = 'actions'; $config->story->dtable->fieldList['actions']['actionsMap']['assigned']['icon'] = 'hand-right'; @@ -345,7 +334,7 @@ $config->story->dtable->fieldList['actions']['actionsMap']['subdivide']['hint'] $config->story->dtable->fieldList['actions']['actionsMap']['subdivide']['data-app'] = $app->tab; $config->story->dtable->fieldList['actions']['actionsMap']['processStoryChange']['icon'] = 'ok'; -$config->story->dtable->fieldList['actions']['actionsMap']['processStoryChange']['hint'] = $lang->confirm; +$config->story->dtable->fieldList['actions']['actionsMap']['processStoryChange']['hint'] = $lang->story->processStoryChange; $config->story->dtable->fieldList['actions']['actionsMap']['batchCreate']['icon'] = 'split'; $config->story->dtable->fieldList['actions']['actionsMap']['batchCreate']['hint'] = $lang->story->subdivide; diff --git a/module/story/config/form.php b/module/story/config/form.php index b787fe525e..43b6e5d988 100644 --- a/module/story/config/form.php +++ b/module/story/config/form.php @@ -8,15 +8,15 @@ $config->story->form->create = array(); $config->story->form->create['product'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->create['branch'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->create['module'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); -$config->story->form->create['plan'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); +$config->story->form->create['plan'] = array('type' => 'array', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join'); $config->story->form->create['assignedTo'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => 'users'); $config->story->form->create['source'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->sourceList); $config->story->form->create['sourceNote'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->create['feedbackBy'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->create['notifyEmail'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->create['reviewer'] = array('type' => 'array', 'control' => 'multi-select', 'required' => false, 'default' => '', 'options' => 'users'); -$config->story->form->create['URS'] = array('type' => 'array', 'control' => 'multi-select', 'required' => false, 'default' => '', 'options' => array()); $config->story->form->create['parent'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); +$config->story->form->create['grade'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 1, 'options' => array()); $config->story->form->create['region'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->create['lane'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->create['title'] = array('type' => 'string', 'control' => 'text', 'required' => true, 'filter' => 'trim'); @@ -49,11 +49,12 @@ $config->story->form->edit['product'] = array('type' => 'int', 'contr $config->story->form->edit['branch'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->edit['module'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->edit['parent'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); +$config->story->form->edit['grade'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 1, 'options' => array()); $config->story->form->edit['title'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->edit['color'] = array('type' => 'string', 'control' => 'color', 'required' => false, 'default' => ''); $config->story->form->edit['spec'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => ''); $config->story->form->edit['verify'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => ''); -$config->story->form->edit['plan'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array()); +$config->story->form->edit['plan'] = array('type' => 'array', 'control' => 'select', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join'); $config->story->form->edit['source'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->sourceList); $config->story->form->edit['sourceNote'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->edit['stage'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->stageList); @@ -70,12 +71,13 @@ $config->story->form->edit['assignedTo'] = array('type' => 'string', 'contr $config->story->form->edit['closedBy'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => 'users'); $config->story->form->edit['closedReason'] = array('type' => 'string', 'control' => 'select', 'required' => false, 'default' => '', 'options' => $lang->story->reasonList); $config->story->form->edit['duplicateStory'] = array('type' => 'int', 'control' => 'select', 'required' => false, 'default' => '', 'options' => array()); -$config->story->form->edit['childStories'] = array('type' => 'array', 'control' => 'select', 'required' => false, 'default' => '', 'options' => array(), 'filter' => 'join'); $config->story->form->batchCreate = array(); $config->story->form->batchCreate['branch'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array()); $config->story->form->batchCreate['module'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array()); $config->story->form->batchCreate['plan'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array()); +$config->story->form->batchCreate['parent'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => 0, 'options' => array()); +$config->story->form->batchCreate['grade'] = array('ditto' => true, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => 1, 'options' => array()); $config->story->form->batchCreate['assignedTo'] = array('ditto' => false, 'type' => 'string', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => '', 'options' => 'users'); $config->story->form->batchCreate['region'] = array('ditto' => false, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => 0, 'options' => array()); $config->story->form->batchCreate['lane'] = array('ditto' => false, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => 0, 'options' => array()); @@ -89,8 +91,6 @@ $config->story->form->batchCreate['category'] = array('ditto' => true, 'type' $config->story->form->batchCreate['pri'] = array('ditto' => false, 'type' => 'int', 'control' => 'priPicker', 'required' => false, 'width' => '92px', 'default' => $config->story->defaultPriority, 'options' => $lang->story->priList); $config->story->form->batchCreate['estimate'] = array('ditto' => false, 'type' => 'float', 'control' => 'text', 'required' => false, 'width' => '76px', 'default' => 0); $config->story->form->batchCreate['reviewer'] = array('ditto' => true, 'type' => 'array', 'control' => 'select', 'required' => false, 'width' => '136px', 'default' => '', 'multiple' => true, 'options' => array()); -$config->story->form->batchCreate['URS'] = array('ditto' => false, 'type' => 'array', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => '', 'multiple' => true); -$config->story->form->batchCreate['parent'] = array('ditto' => false, 'type' => 'int', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => ''); $config->story->form->batchCreate['keywords'] = array('ditto' => false, 'type' => 'string', 'control' => 'text', 'required' => false, 'width' => '200px', 'default' => ''); $config->story->form->batchCreate['mailto'] = array('ditto' => false, 'type' => 'array', 'control' => 'select', 'required' => false, 'width' => '200px', 'default' => array(''), 'multiple' => true, 'options' => 'users', 'filter' => 'join'); @@ -98,7 +98,7 @@ $config->story->form->batchEdit = array(); $config->story->form->batchEdit['branch'] = array('type' => 'string', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => '', 'options' => array()); $config->story->form->batchEdit['module'] = array('type' => 'int', 'width' => '200px', 'control' => array('control' => 'picker', 'required' => true), 'required' => false, 'default' => 0, 'options' => array()); $config->story->form->batchEdit['roadmap'] = array('type' => 'int', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array()); -$config->story->form->batchEdit['plan'] = array('type' => 'int', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array()); +$config->story->form->batchEdit['plan'] = array('type' => 'array', 'width' => '200px', 'control' => 'picker', 'required' => false, 'default' => 0, 'options' => array(), 'filter' => 'join'); $config->story->form->batchEdit['title'] = array('type' => 'string', 'width' => '240px', 'control' => array('control' => 'colorInput', 'inputClass' => 'filter-none'), 'required' => true, 'filter' => 'trim', 'base' => true); $config->story->form->batchEdit['color'] = array('type' => 'string', 'control' => 'color', 'required' => false, 'default' => '', 'filter' => 'trim'); $config->story->form->batchEdit['estimate'] = array('type' => 'float', 'width' => '76px', 'control' => 'text', 'required' => false, 'default' => '0'); @@ -142,7 +142,6 @@ $config->story->form->review['closedReason'] = array('type' => 'string', 'cont $config->story->form->review['pri'] = array('type' => 'int', 'control' => 'priPicker', 'required' => false, 'default' => '', 'options' => $lang->story->priList); $config->story->form->review['estimate'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => ''); $config->story->form->review['duplicateStory'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => ''); -$config->story->form->review['childStories'] = array('type' => 'string', 'control' => 'text', 'required' => false, 'default' => ''); $config->story->form->review['status'] = array('type' => 'string', 'control' => 'hidden', 'required' => false, 'default' => ''); $config->story->form->activate = array(); @@ -155,7 +154,6 @@ $config->story->form->activate['closedReason'] = array('type' => 'string', $config->story->form->activate['closedDate'] = array('type' => 'datetime', 'required' => false, 'default' => null); $config->story->form->activate['assignedDate'] = array('type' => 'datetime', 'required' => false, 'default' => $now); $config->story->form->activate['duplicateStory'] = array('type' => 'int', 'required' => false, 'default' => 0); -$config->story->form->activate['childStories'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->story->form->activate['retractedBy'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->story->form->activate['retractedReason'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->story->form->activate['retractedDate'] = array('type' => 'datetime', 'required' => false, 'default' => null); @@ -170,7 +168,6 @@ $config->story->form->close['closedReason'] = array('type' => 'string', 'req $config->story->form->close['closedDate'] = array('type' => 'datetime', 'required' => false, 'default' => $now); $config->story->form->close['assignedDate'] = array('type' => 'datetime', 'required' => false, 'default' => $now); $config->story->form->close['duplicateStory'] = array('type' => 'int', 'required' => false, 'default' => 0); -$config->story->form->close['childStories'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->story->form->close['closeSync'] = array('type' => 'string', 'required' => false, 'default' => ''); $config->story->form->submitReview = array(); diff --git a/module/story/control.php b/module/story/control.php index f6bc838168..8b1d37f328 100755 --- a/module/story/control.php +++ b/module/story/control.php @@ -29,6 +29,8 @@ class story extends control $this->loadModel('tree'); $this->loadModel('user'); $this->loadModel('action'); + $this->loadModel('requirement'); + $this->loadModel('epic'); if($this->app->rawModule == 'projectstory') $this->app->tab = 'project'; } @@ -65,7 +67,7 @@ class story extends control helper::setcookie('lastStoryModule', $moduleID, $this->config->cookieLife, $this->config->webRoot, '', $this->config->cookieSecure, false); /* Get story data from post. */ - $storyData = $this->storyZen->buildStoryForCreate($objectID, $bugID); + $storyData = $this->storyZen->buildStoryForCreate($objectID, $bugID, $storyType); if(!$storyData) return $this->send(array('result' => 'fail', 'message' => dao::getError())); $response = $this->storyZen->checkRepeatStory($storyData, $objectID); @@ -101,8 +103,8 @@ class story extends control $initStory = $this->storyZen->initStoryForCreate($planID, $copyStoryID, $bugID, $todoID, $extra); /* Get form fields. */ - $this->storyZen->setViewVarsForKanban($objectID, $this->story->parseExtra($extra)); - $fields = $this->storyZen->getFormFieldsForCreate($productID, $branch, $objectID, $initStory); + $this->storyZen->setViewVarsForKanban($objectID, $this->story->parseExtra($extra), $storyType); + $fields = $this->storyZen->getFormFieldsForCreate($productID, $branch, $objectID, $initStory, $storyType); $fields = $this->storyZen->setModuleField($fields, $moduleID); $fields = $this->storyZen->removeFormFieldsForCreate($fields, $storyType); @@ -141,7 +143,7 @@ class story extends control if(empty($stories)) return $this->sendError($this->lang->error->noData, true); if(dao::isError()) return $this->sendError(dao::getError(), true); - $storyIdList = $this->story->batchCreate($stories, $productID, $branch, $storyType, $storyID); + $storyIdList = $this->story->batchCreate($stories); if(dao::isError()) return $this->sendError(dao::getError(), true); /* Project or execution linked stories. */ @@ -156,7 +158,7 @@ class story extends control } if($this->session->project && $executionID != $this->session->project) $this->execution->linkStory($this->session->project, $storyIdList); - $this->execution->linkStory($executionID, $storyIdList, $extra, $lanes); + $this->execution->linkStory($executionID, $storyIdList, $extra, $lanes, $storyType); } if($this->viewType == 'json') return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'idList' => $stories)); @@ -166,7 +168,7 @@ class story extends control return $this->send(array('result' => 'success', 'message' => $this->lang->saveSuccess, 'locate' => $locateLink)); } - $this->storyZen->setMenuForBatchCreate($productID, $branch, $executionID, $extra); + $this->storyZen->setMenuForBatchCreate($productID, $branch, $executionID, $extra, $storyType); if($productID == 0 && $executionID != 0) return $this->sendError($this->lang->execution->errorNoLinkedProducts, $this->createLink('execution', 'manageproducts', "executionID=$executionID")); @@ -183,28 +185,40 @@ class story extends control if($storyID) { $story = $this->story->getByID($storyID); - if($story) unset($customFields['parent'], $fields['parent']); + if($story) + { + if(isset($fields['parent'])) $fields['parent']['default'] = $storyID; + $gradeOptions = $this->story->getGradeOptions($story, $storyType); + $fields['grade']['options'] = $gradeOptions; + $fields['grade']['default'] = current($gradeOptions); + } + + $this->view->story = $story; $this->view->storyTitle = isset($story->title) ? $story->title : ''; /* Check can subdivide or not. */ - if(!$this->story->checkCanSubdivide($story, !empty($product->shadow))) return $this->send(array('result' => 'fail', 'message' => $this->lang->story->errorNotSubdivide, 'load' => array('alert' => $this->lang->story->errorNotSubdivide))); + if(!$this->story->checkCanSubdivide($story, !empty($product->shadow))) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorNotSubdivide))); + + /* Check can split requirement/story or not. */ + if($storyType != $story->type && !$this->story->checkCanSplit($story)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->errorCannotSplit))); } - $this->view->title = $product->name . $this->lang->hyphen . ($storyID ? $this->lang->story->subdivide : $this->lang->story->batchCreate); - $this->view->customFields = $customFields; - $this->view->showFields = $showFields; - $this->view->product = $product; - $this->view->branch = $branch; - $this->view->productID = $productID; - $this->view->storyID = $storyID; - $this->view->moduleID = $moduleID; - $this->view->executionID = $executionID; - $this->view->type = $storyType; - $this->view->fields = $fields; - $this->view->planID = $plan; - $this->view->stories = $this->storyZen->getDataFromUploadImages($productID, $moduleID, $plan); - $this->view->storyTitle = isset($story->title) ? $story->title : ''; - $this->view->forceReview = $this->story->checkForceReview(); + $this->view->title = $product->name . $this->lang->hyphen . ($storyID ? $this->lang->story->subdivide : $this->lang->story->batchCreate); + $this->view->customFields = $customFields; + $this->view->showFields = $showFields; + $this->view->product = $product; + $this->view->branch = $branch; + $this->view->productID = $productID; + $this->view->storyID = $storyID; + $this->view->moduleID = $moduleID; + $this->view->executionID = $executionID; + $this->view->type = $storyType; + $this->view->fields = $fields; + $this->view->planID = $plan; + $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); + $this->view->stories = $this->storyZen->getDataFromUploadImages($productID, $moduleID, $plan); + $this->view->storyTitle = isset($story->title) ? $story->title : ''; + $this->view->forceReview = $this->story->checkForceReview($storyType); $this->display(); } @@ -280,6 +294,7 @@ class story extends control $this->loadModel('file'); $this->app->loadLang('bug'); $this->commonAction($storyID); + $this->story->getAllChildId($storyID); if(!empty($_POST)) { @@ -310,6 +325,7 @@ class story extends control $this->view->title = $this->lang->story->edit . "STORY" . $this->lang->hyphen . $this->view->story->title; $this->view->story = $story; + $this->view->showGrade = $this->config->edition == 'ipd'; $this->view->twins = empty($story->twins) ? array() : $this->story->getByList($story->twins); $this->view->fields = $fields; $this->view->branches = $this->view->product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($story->product); @@ -354,17 +370,11 @@ class story extends control /* Set Custom*/ foreach(explode(',', $this->config->story->list->customBatchEditFields) as $field) $customFields[$field] = $this->lang->story->$field; - $showFields = $this->config->story->custom->batchEditFields; - if($storyType == 'requirement') - { - unset($customFields['plan'], $customFields['stage']); - $showFields = str_replace(array('plan', 'stage'), '', $showFields); - } $product = $this->product->getByID($productID); if($product && $product->type == 'normal') unset($customFields['branch']); $this->view->customFields = $customFields; - $this->view->showFields = $showFields; + $this->view->showFields = $this->config->story->custom->batchEditFields; $this->storyZen->setFormOptionsForBatchEdit($productID, $executionID, $stories); @@ -502,7 +512,7 @@ class story extends control if($tab == 'product' and !empty($product->shadow)) return $this->send(array('result' => 'success', 'open' => array('url' => $uri, 'app' => 'project'))); - if(!$story) + if(!$story || (isset($story->type) && $story->type != $storyType)) { $locateModule = $this->config->vision == 'lite' ? 'project' : 'product'; $locateMethod = ($this->config->edition == 'ipd' && $this->config->vision == 'or') ? 'browse' : 'index'; @@ -528,7 +538,7 @@ class story extends control if($this->app->tab == 'project') { $projectID = $param ? $param : $this->session->project; - $project = $this->loadModel('project')->fetchByID($projectID); + $project = $this->loadModel('project')->fetchByID((int)$projectID); $this->view->projectID = $projectID; $this->view->project = $project; if(!$project->multiple) @@ -558,22 +568,54 @@ class story extends control $this->product->setMenu($story->product, $story->branch); } - $this->view->title = "STORY #$story->id $story->title - $product->name"; - $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id); - $this->view->users = $this->user->getPairs('noletter'); - $this->view->executions = $this->execution->getPairs(0, 'all', 'nocode'); - $this->view->version = $version; - $this->view->preAndNext = $this->loadModel('common')->getPreAndNextObject('story', $storyID); - $this->view->builds = $this->loadModel('build')->getStoryBuilds($storyID); - $this->view->releases = $this->loadModel('release')->getStoryReleases($storyID); - $this->view->story = $story; - $this->view->product = $product; - $this->view->actions = $this->action->getList('story', $storyID); - $this->view->branch = $story->branch; + $this->view->title = "STORY #$story->id $story->title - $product->name"; + $this->view->branches = $product->type == 'normal' ? array() : $this->loadModel('branch')->getPairs($product->id); + $this->view->users = $this->user->getPairs('noletter'); + $this->view->executions = $this->execution->getPairs(0, 'all', 'nocode'); + $this->view->version = $version; + $this->view->preAndNext = $this->loadModel('common')->getPreAndNextObject('story', $storyID); + $this->view->builds = $this->loadModel('build')->getStoryBuilds($storyID); + $this->view->releases = $this->loadModel('release')->getStoryReleases($storyID); + $this->view->story = $story; + $this->view->product = $product; + $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); + $this->view->gradePairs = $this->story->getGradePairs($story->type, 'all'); + $this->view->showGrade = $this->config->edition == 'ipd'; + $this->view->actions = $this->action->getList('story', $storyID); + $this->view->branch = $story->branch; $this->display(); } + /** + * 需求详情,延后鉴权。 + * View a story, delay auth. + * + * @param int $storyID + * @access public + * @return void + */ + public function storyView(int $storyID) + { + $story = $this->story->fetchByID($storyID); + if(common::hasPriv($story->type, 'view')) + { + echo $this->fetch($story->type, 'view', "storyID=$storyID"); + } + else + { + $vars = "module={$story->type}&method=view"; + if(isset($this->server->http_referer)) + { + $referer = helper::safe64Encode($this->server->http_referer); + $vars .= "&referer=$referer"; + } + $denyLink = helper::createLink('user', 'deny', $vars); + + return $this->send(array('result' => 'success', 'load' => $denyLink)); + } + } + /** * Delete a story. * @@ -591,7 +633,11 @@ class story extends control if($confirm == 'no') { - if($story->type == 'requirement') $this->lang->story->confirmDelete = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->confirmDelete); + if($story->type != 'story') + { + $replacement = $story->type == 'requirement' ? $this->lang->URCommon : $this->lang->ERCommon; + $this->lang->story->confirmDelete = str_replace($this->lang->SRCommon, $replacement, $this->lang->story->confirmDelete); + } $confirmURL = $this->createLink('story', 'delete', "story=$storyID&confirm=yes&from=$from&storyType=$storyType"); return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.confirm({message: '{$this->lang->story->confirmDelete}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => {if(res) $.ajaxSubmit({url: '$confirmURL'});});")); } @@ -789,7 +835,7 @@ class story extends control $this->view->actions = $this->action->getList('story', $storyID); $this->view->reviewers = $this->user->getPairs('noclosed|nodeleted', '', 0, $reviewers); $this->view->users = $this->user->getPairs('noclosed|noletter'); - $this->view->needReview = (($this->app->user->account == $product->PO or $this->config->story->needReview == 0 or !$this->story->checkForceReview()) and empty($story->reviewer)) ? "checked='checked'" : ""; + $this->view->needReview = (($this->app->user->account == $product->PO or $this->config->{$storyType}->needReview == 0 or !$this->story->checkForceReview($storyType)) and empty($story->reviewer)) ? "checked='checked'" : ""; $this->view->lastReviewer = $this->story->getLastReviewer($story->id); $this->display(); @@ -800,8 +846,8 @@ class story extends control * Close the story. * * @param int $storyID - * @param string $from taskkanban - * @param string $storyType story|requirement + * @param string $from taskkanban + * @param string $storyType story|requirement * @access public * @return void */ @@ -815,15 +861,14 @@ class story extends control $postData = form::data($this->config->story->form->close) ->stripTags($this->config->story->editor->close['id'], $this->config->allowedTags) ->removeIF($this->post->closedReason != 'duplicate', 'duplicateStory') - ->removeIF($this->post->closedReason != 'subdivided', 'childStories') ->get(); + + if(strpos($this->config->{$story->type}->change->requiredFields, 'comment') !== false and !$this->post->comment) $this->send(array('result' => 'fail', 'message' => array('comment' => sprintf($this->lang->error->notempty, $this->lang->comment)))); $postData = $this->loadModel('file')->processImgURL($postData, $this->config->story->editor->close['id'], $this->post->uid); $changes = $this->story->close($storyID, $postData); if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError())); - $this->story->closeParentRequirement($storyID, $postData); - if($changes) { $preStatus = $story->status; @@ -864,7 +909,7 @@ class story extends control $this->story->replaceURLang($story->type); /* Set the closed reason options and remove subdivided options. */ - $reasonList = $this->lang->story->reasonList; + $reasonList = $this->lang->{$storyType}->reasonList; if($story->status == 'draft') unset($reasonList['cancel']); unset($reasonList['subdivided']); @@ -918,7 +963,6 @@ class story extends control /* Get the skipped and already closed stories, and the count of stories which have a twin. */ $stories = $this->story->getByList($storyIdList); $closedStory = array(); - $skippedStory = array(); $ignoreTwins = array(); $twinsCount = array(); $storyCount = count($stories); @@ -930,9 +974,11 @@ class story extends control continue; } - if($story->parent == -1) $skippedStory[] = '#' . $story->id; - if($story->status == 'closed') $closedStory[] = '#' . $story->id; - if($story->parent == -1 || $story->status == 'closed') unset($stories[$story->id]); + if($story->status == 'closed') + { + $closedStory[] = $story->id; + unset($stories[$story->id]); + } if(!empty($story->twins)) { @@ -948,9 +994,7 @@ class story extends control if($storyCount == count($closedStory)) return $this->send(array('result' => 'fail', 'load' => array('alert' => $this->lang->story->notice->closed, 'locate' => $this->session->storyList))); $errorTips = ''; - if($closedStory) $errorTips .= sprintf($this->lang->story->closedStory, implode(' ', $closedStory)); - if($errorTips) $errorTips .= '\n'; - if($skippedStory) $errorTips .= sprintf($this->lang->story->skipStory, implode(' ', $skippedStory)); + if($closedStory) $errorTips .= sprintf($this->lang->story->closedStory, implode(',', $closedStory)); $this->view->productID = $productID; $this->view->stories = $stories; @@ -1018,9 +1062,21 @@ class story extends control $stories = $this->story->getByList($_POST['storyIdList']); $activeStories = array(); $storyPairs = array(); + $hasParent = false; + $hasERUR = false; foreach($stories as $story) { if(str_contains(',draft,reviewing,changing,closed,', ",{$story->status},")) continue; + if($story->type != 'story') + { + $hasERUR = true; + continue; + } + if($story->isParent == '1') + { + $hasParent = true; + continue; + } $activeStories[$story->id] = $story; $storyPairs[$story->id] = $story->title; @@ -1035,6 +1091,8 @@ class story extends control $this->view->taskType = empty($_POST['type']) ? '' : $_POST['type']; $this->view->stories = $activeStories; $this->view->storyPairs = $storyPairs; + $this->view->hasParent = $hasParent; + $this->view->hasERUR = $hasERUR; $this->view->modules = $this->loadModel('tree')->getTaskOptionMenu($executionID, 0, 'allModule'); $this->view->members = $this->loadModel('user')->getTeamMemberPairs($executionID, 'execution', 'nodeleted'); $this->view->storyTasks = $this->loadModel('task')->getStoryTaskCounts(array_keys($stories), $executionID); @@ -1133,6 +1191,28 @@ class story extends control return $this->send(array('result' => 'success', 'load' => true)); } + /** + * Batch change the grade of story. + * + * @param int $grade + * @access public + * @return void + */ + public function batchChangeGrade(int $grade) + { + if(empty($_POST['storyIdList'])) return $this->send(array('result' => 'success', 'load' => true)); + + $storyIdList = array_unique($this->post->storyIdList); + $message = $this->story->batchChangeGrade($storyIdList, $grade); + + $response = array(); + $response['result'] = 'success'; + $response['load'] = false; + if($message) $response['callback'] = "zui.Modal.alert('{$message}').then((res) => {loadCurrentPage()});"; + if(empty($message)) $response['load'] = true; + return $this->send($response); + } + /** * Batch change the stage of story. * @@ -1155,7 +1235,30 @@ class story extends control if($message) $response['callback'] = "zui.Modal.alert({icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x', message: '{$message}'}).then((res) => {loadCurrentPage()});"; if(empty($message)) $response['load'] = true; return $this->send($response); + } + /** + * 批量修改需求的父需求。 + * Batch change the parent of story. + * + * @param int $productID + * @param string $storyType story|requirement + * @access public + * @return void + */ + public function batchChangeParent(int $productID = 0, string $storyType = 'story') + { + if($_POST) + { + $storyIdList = $this->cookie->checkedItem; + $result = $this->story->batchChangeParent($storyIdList, (int)$this->post->parent, $storyType); + if($result) return $this->send(array('result' => 'fail', 'callback' => "zui.Modal.alert('$result');", 'closeModal' => true, 'load' => true)); + + return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true)); + } + + $this->view->parents = $this->story->getParentStoryPairs($productID, '', $storyType); + $this->display(); } /** @@ -1312,29 +1415,29 @@ class story extends control } /** - * 关联需求。 - * Link story and requirement. - * - * 引用1:软件需求列表页,关联用户需求,将软件需求关联到用户需求。 - * 引用2:用户需求列表页,关联软件需求,将用户需求关联到软件需求。 + * 需求详情页关联需求。 + * Link story in view. * * @param int $storyID - * @param string $type linkStories|linkRelateUR|linkRelateSR + * @param string $type link|remove * @param int $linkedStoryID * @param string $browseType ''|bySearch * @param int $queryID 0| - * @param string $storyType story|requirement + * @param int $recTotal + * @param int $recPerPage + * @param int $pageID * @access public * @return void */ - public function linkStory(int $storyID, string $type = 'linkStories', int $linkedStoryID = 0, string $browseType = '', int $queryID = 0, string $storyType = 'story') + public function linkStory(int $storyID, string $type = 'link', int $linkedStoryID = 0, string $browseType = '', int $queryID = 0, int $recTotal = 0, int $recPerPage = 20, int $pageID = 1) { $this->commonAction($storyID); + $story = $this->story->getById($storyID); if($type == 'remove') { $this->story->unlinkStory($storyID, $linkedStoryID); - helper::end(); + return $this->send(array('result' => 'success', 'load' => true)); } if($_POST) @@ -1346,120 +1449,45 @@ class story extends control } /* Get story, product, products, and queryID. */ - $story = $this->story->getById($storyID); $products = $this->product->getPairs('', 0, '', 'all'); - /* Change for requirement story title. */ - if($story->type == 'requirement') - { - $this->lang->story->title = str_replace($this->lang->URCommon, $this->lang->SRCommon, $this->lang->story->title); - $this->lang->story->linkStory = str_replace($this->lang->URCommon, $this->lang->SRCommon, $this->lang->story->linkStory); - } - /* Build search form. */ - $actionURL = $this->createLink('story', 'linkStory', "storyID=$storyID&type=$type&linkedStoryID=$linkedStoryID&browseType=bySearch&queryID=myQueryID&storyType=$storyType"); - $this->product->buildSearchForm($story->product, $products, $queryID, $actionURL, $story->type == 'story' ? 'requirement' : 'story', (string)$story->branch); + $actionURL = $this->createLink('story', 'linkStory', "storyID=$storyID&type=$type&linkedStoryID=$linkedStoryID&browseType=bySearch&queryID=myQueryID&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); + $this->product->buildSearchForm($story->product, $products, $queryID, $actionURL, 'all', (string)$story->branch); - /* Get stories to link. */ - $stories2Link = $this->story->getStories2Link($storyID, $type, $browseType, $queryID, $story->type); - - /* Assign. */ - $this->view->title = $this->lang->story->linkStory . "STORY" . $this->lang->hyphen .$this->lang->story->linkStory; - $this->view->type = $type; - $this->view->stories2Link = $stories2Link; - $this->view->users = $this->loadModel('user')->getPairs('noletter'); - - $this->display(); - } - - /** - * 关联软件需求。 - * Link related stories. - * - * @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) - { /* Load pager. */ $this->app->loadClass('pager', true); $pager = new pager($recTotal, $recPerPage, $pageID); - /* Get story, product, products, and queryID. */ - $story = $this->story->getById($storyID); - $products = $this->product->getPairs('', 0, '', 'all'); - $queryID = ($browseType == 'bySearch') ? $param : 0; - $type = $story->type == 'story' ? 'linkRelateSR' : 'linkRelateUR'; - $method = $story->type == 'story' ? 'linkStories' : 'linkRequirements'; + /* Get stories to link. */ + $stories2Link = $this->story->getStories2Link($storyID, $browseType, $queryID, $pager); - /* Build search form. */ - $actionURL = $this->createLink('story', $method, "storyID=$storyID&browseType=bySearch&excludeStories=$excludeStories&queryID=myQueryID"); - $this->product->buildSearchForm($story->product, $products, $queryID, $actionURL, $story->type); - - $this->view->story = $story; - $this->view->stories2Link = $this->story->getStories2Link($storyID, $type, $browseType, $queryID, $story->type, $pager, $excludeStories); - $this->view->products = $products; - $this->view->users = $this->user->getPairs('noletter'); - $this->view->pager = $pager; + /* Assign. */ + $this->view->title = $this->lang->story->linkStory . "STORY" . $this->lang->hyphen .$this->lang->story->linkStory; + $this->view->type = $type; + $this->view->pager = $pager; + $this->view->stories2Link = $stories2Link; + $this->view->maxGradeGroup = $this->story->getMaxGradeGroup(); + $this->view->users = $this->loadModel('user')->getPairs('noletter'); $this->display(); } /** - * 关联用户需求。 - * Link related requirements. + * 需求的父需求变更时,子需求确认变更。 + * Confirm the change of the parent story. * * @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) + public function processStoryChange(int $storyID) { - $this->lang->story->title = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->title); - $this->config->product->search['fields']['title'] = $this->lang->story->title; - unset($this->config->product->search['fields']['plan']); - unset($this->config->product->search['fields']['stage']); + $story = $this->story->fetchByID($storyID); + $parent = $this->story->fetchByID($story->parent); - echo $this->fetch('story', 'linkStories', "storyID=$storyID&browseType=$browseType&excludeStories=$excludeStories¶m=$param&recTotal=$recTotal&recPerPage=$recPerPage&pageID=$pageID"); - } - - /** - * 当软件需求关联的用户需求有变更时,让用户决定是否变更软件需求。 - * Ask user to decide to whether change related story after the user requirement changed. - * - * @param int $storyID - * @param string $result yes|no - * @access public - * @return void - */ - public function processStoryChange(int $storyID, string $result = 'yes') - { - $this->commonAction($storyID); - $story = $this->story->getByID($storyID); - - if($result == 'no') - { - $this->dao->update(TABLE_STORY)->set('URChanged')->eq(0)->where('id')->eq($storyID)->exec(); - return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true)); - } - - $this->view->changedStories = $this->story->getChangedStories($story); - $this->view->users = $this->loadModel('user')->getPairs('noletter'); - $this->view->storyID = $storyID; - $this->display(); + $this->dao->update(TABLE_STORY)->set('parentVersion')->eq($parent->version)->where('id')->eq($storyID)->exec(); + return $this->send(array('result' => 'success', 'load' => true, 'closeModal' => true)); } /** @@ -1485,7 +1513,7 @@ class story extends control $moduleID = $this->tree->getAllChildID($moduleID); } - $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch, $moduleID, $type, $status); + $stories = $this->story->getExecutionStoryPairs($executionID, $productID, $branch, $moduleID, $type, $status, 'story', false); if($this->app->getViewType() === 'json') { @@ -1503,8 +1531,7 @@ class story extends control } else { - $items = array(); - foreach($stories as $storyID => $storyName) $items[] = array('text' => $storyName, 'value' => $storyID, 'keys' => $storyName); + $items = $this->story->addGradeLabel($stories); return print(json_encode($items)); } } @@ -1557,8 +1584,7 @@ class story extends control return $this->send($storyList); } - $items = array(); - foreach($stories as $storyID => $storyName) $items[] = array('text' => $storyName, 'value' => $storyID, 'keys' => $storyName); + $items = $this->story->addGradeLabel($stories); return print(json_encode($items)); } @@ -1623,6 +1649,50 @@ class story extends control $this->display(); } + /** + * 获取需求层级下拉列表及默认值。 + * AJAX: get the grade of a story. + * + * @param int $storyID + * @param string $type + * @param int $grade + * @access public + * @return void + */ + public function ajaxGetGrade(int $storyID, string $type = 'story', int $grade = 0) + { + $story = $this->story->fetchByID($storyID); + $gradeOptions = $this->story->getGradeOptions($story, $type, (array)$grade); + + $items = array(); + foreach($gradeOptions as $grade => $name) $items[] = array('text' => $name, 'value' => $grade); + + return $this->send(array('items' => array_values($items), 'default' => key($gradeOptions))); + } + + /** + * 检查需求的等级是否超出系统设置。 + * AJAX: check the grade of a story. + * + * @param int $storyID + * @param int $newGrade + * @access public + * @return bool + */ + public function ajaxCheckGrade(int $storyID, int $newGrade) + { + $oldStory = $this->story->fetchByID($storyID); + $story = clone $oldStory; + $story->grade = $newGrade; + + if($story->grade > $oldStory->grade) + { + if(!$this->story->checkGrade($story, $oldStory)) return print(json_encode(array('result' => false, 'message' => dao::getError()))); + } + + return print(json_encode(array('result' => true))); + } + /** * 获取需求的信息。 * AJAX: get module of a story. @@ -1653,13 +1723,21 @@ class story extends control * AJAX: get the parent story. * * @param int $productID - * @param string $labelName + * @param int $storyID * @access public * @return string */ - public function ajaxGetParentStory(int $productID, string $labelName = '') + public function ajaxGetParentStory(int $productID, int $storyID = 0) { - $stories = $this->story->getParentStoryPairs($productID); + if($storyID) + { + $story = $this->story->fetchByID($storyID); + $stories = $this->story->getParentStoryPairs($story->product, $story->parent, $story->type, $storyID); + } + else + { + $stories = $this->story->getParentStoryPairs($productID); + } $items = array(); foreach($stories as $storyID => $storyTitle) @@ -1668,7 +1746,7 @@ class story extends control $items[] = array('text' => $storyTitle, 'value' => $storyID); } - return print(json_encode(array('items' => $items, 'name' => $labelName))); + return $this->send($items); } /** @@ -1696,7 +1774,7 @@ class story extends control foreach($this->post->charts as $chart) { $chartFunc = 'getDataOf' . $chart; - $chartData = $this->story->$chartFunc(); + $chartData = $this->story->$chartFunc($storyType); $chartOption = $this->lang->story->report->$chart; if(!empty($chartType)) $chartOption->type = $chartType; $this->story->mergeChartOption($chart); @@ -1736,12 +1814,6 @@ class story extends control $this->product->setMenu($productID, $branchID); } - if($storyType != 'story') - { - unset($this->lang->story->report->charts['storiesPerPlan']); - unset($this->lang->story->report->charts['storiesPerStage']); - } - $this->view->title = $product->name . $this->lang->hyphen . $this->lang->story->reportChart; $this->view->productID = $productID; $this->view->branchID = $branchID; @@ -1774,7 +1846,7 @@ class story extends control $this->loadModel('transfer'); $postData = form::data($this->config->transfer->form->export)->get(); - $this->session->set('storyTransferParams', array('productID' => $productID, 'executionID' => $executionID)); + $this->session->set("{$storyType}TransferParams", array('productID' => $productID, 'executionID' => $executionID)); /* 给评审过的人员添加下拉选择,方便再次导入时转换成待评审人员。*/ /* Add a drop-down selection to the reviewer to facilitate the conversion to the reviewer during import. */ @@ -1798,20 +1870,23 @@ class story extends control } $this->post->set('rows', $this->story->getExportStories($orderBy, $storyType, $postData)); - $this->fetch('transfer', 'export', 'model=story'); + $this->fetch('transfer', 'export', "model=$storyType"); } $this->story->replaceURLang($storyType); - $fileName = $storyType == 'requirement' ? $this->lang->URCommon : $this->lang->SRCommon; - $project = null; + $fileName = $this->lang->SRCommon; + if($storyType == 'requirement') $fileName = $this->lang->URCommon; + if($storyType == 'epic') $fileName = $this->lang->ERCommon; + $project = null; $hasBranch = false; if($executionID) { $execution = $this->loadModel('execution')->getByID($executionID); - $fileName = $execution->name . $this->lang->dash . $fileName; + $fileName = $execution->name . $this->lang->dash . $this->lang->common->story; $project = $execution; if($execution->type == 'execution') $project = $this->project->getById($execution->project); + $this->lang->story->title = $this->lang->story->name; $products = $this->loadModel('product')->getProducts($executionID); foreach($products as $product) @@ -1867,59 +1942,23 @@ class story extends control * @param int $userID * @param string $id the id of the select control. * @param int $appendID + * @param string $storyType * @access public * @return string */ - public function ajaxGetUserStories(int $userID = 0, string $id = '', int $appendID = 0) + public function ajaxGetUserStories(int $userID = 0, string $id = '', int $appendID = 0, $storyType = 'story') { if(empty($userID)) $userID = $this->app->user->id; $user = $this->loadModel('user')->getById($userID, 'id'); - $stories = $this->story->getUserStoryPairs($user->account, 10, 'story', '', $appendID); + $stories = $this->story->getUserStoryPairs($user->account, 10, $storyType, '', $appendID); $items = array(); foreach($stories as $storyID => $storyTitle) $items[] = array('text' => $storyTitle, 'value' => $storyID); - $fieldName = $id ? "stories[$id]" : 'story'; + $fieldName = $id ? "stories[$id]" : $storyType; return print(json_encode(array('name' => $fieldName, 'items' => $items))); } - /** - * 通过AJAX方式获取需求的状态。 - * AJAX: get story status. - * - * @param string $method - * @param string $params - * @access public - * @return void - */ - public function ajaxGetStatus(string $method, string $params = '') - { - parse_str(str_replace(',', '&', $params), $params); - $status = ''; - if($method == 'create') - { - $status = 'draft'; - if(!empty($params['needNotReview'])) $status = 'active'; - if(!empty($params['project'])) $status = 'active'; - if($this->story->checkForceReview()) $status = 'draft'; - } - elseif($method == 'change') - { - $oldStory = $this->dao->findById((int)$params['storyID'])->from(TABLE_STORY)->fetch(); - $status = $oldStory->status; - if($params['changing'] and $oldStory->status == 'active' and empty($params['needNotReview'])) $status = 'changing'; - if($params['changing'] and $oldStory->status == 'active' and $this->story->checkForceReview()) $status = 'changing'; - if($params['changing'] and $oldStory->status == 'draft' and $params['needNotReview']) $status = 'active'; - } - elseif($method == 'review') - { - $oldStory = $this->dao->findById((int)$params['storyID'])->from(TABLE_STORY)->fetch(); - $status = $oldStory->status; - if($params['result'] == 'revert') $status = 'active'; - } - echo $status; - } - /** * 用AJAX方式获取需求的指派给。 * AJAX: get story assignee. @@ -1943,10 +1982,11 @@ class story extends control if($type == 'review') { + $moduleName = $this->app->rawModule; $story = $this->story->getByID($storyID); $reviewers = $this->story->getReviewerPairs($storyID, $story->version); $isChanged = $story->changedBy ? true : false; - $superReviewers = trim(zget($this->config->story, 'superReviewers', ''), ','); + $superReviewers = trim(zget($this->config->{$moduleName}, 'superReviewers', ''), ','); $isSuperReviewer = strpos(",{$superReviewers},", ",{$this->app->user->account},") !== false; if(count($reviewers) == 1) @@ -1986,32 +2026,6 @@ class story extends control } - /** - * AJAX: Get user requirements. - * - * @param int $productID - * @param int $branchID - * @param int $moduleID - * @param string $requirementList - * @access public - * @return void - */ - public function ajaxGetURS(int $productID, string $branchID = '0', int $moduleID = 0, string $requirementList = '0') - { - $moduleIdList = $this->loadModel('tree')->getAllChildId($moduleID); - - $requirements = $this->story->getProductStoryPairs($productID, $branchID, $moduleIdList, 'active,launched', 'id_desc', 0, '', 'requirement'); - - $items = array(); - foreach($requirements as $requirementID => $requirementTitle) - { - if(empty($requirementID)) continue; - $items[] = array('text' => $requirementTitle, 'value' => $requirementID); - } - - return print(json_encode(array('name' => 'URS[]', 'multiple' => true, 'defaultValue' => $requirementList, 'items' => $items))); - } - /** * 移除需求的孪生需求。 * AJAX: Deleted story twin. diff --git a/module/story/css/create.ui.css b/module/story/css/create.ui.css index c6f0e2f5c0..ed02ab7a74 100644 --- a/module/story/css/create.ui.css +++ b/module/story/css/create.ui.css @@ -1,4 +1,3 @@ -.input-group .URSBox{width: 100%; padding-right: 1px;} .c-actions .btn-link{padding:0 5px;} #uploadFiles .upload {width: 100%;} .no-background .input-group-addon{background: none;} diff --git a/module/story/js/batchcreate.ui.js b/module/story/js/batchcreate.ui.js index 95e8ae83dc..73f049481b 100644 --- a/module/story/js/batchcreate.ui.js +++ b/module/story/js/batchcreate.ui.js @@ -1,3 +1,70 @@ +$('input[name=typeSwitcher]').closest('.panel-actions').addClass('row-reverse justify-between w-11/12'); + +window.setModuleAndPlanByBranch = function(e) +{ + const $branch = $(e.target); + const branchID = $branch.val(); + let $row = $branch.closest('tr'); + + var moduleLink = $.createLink('tree', 'ajaxGetOptionMenu', 'productID=' + productID + '&viewtype=story&branch=' + branchID + '&rootModuleID=0&returnType=html&fieldID=&extra=nodeleted'); + + while($row.length) + { + const $modulePicker = $row.find('[name^=module]').zui('picker'); + const moduleID = $row.find('[name^=module]').val(); + $.getJSON(moduleLink, function(data) + { + $modulePicker.render({items: data.items}) + $modulePicker.$.setValue(moduleID); + }); + + $row = $row.next('tr'); + if(!$row.find('td[data-name="module"][data-ditto="on"]').length) break; + } + + var planLink = $.createLink('productPlan', 'ajaxGetProductPlans', 'productID=' + productID + '&branch=' + branchID); + let $rows = $branch.closest('tr'); + while($rows.length) + { + const $planPicker = $rows.find('[name^=plan]').zui('picker'); + const planID = $rows.find('[name^=plan]').val(); + $.getJSON(planLink, function(data) + { + $planPicker.render({items: data}) + $planPicker.$.setValue(planID); + }); + + $rows = $rows.next('tr'); + if(!$rows.find('td[data-name="plan"][data-ditto="on"]').length) break; + } +} + +window.setGrade = function(e) +{ + const parent = e.target.value; + const link = $.createLink('story', 'ajaxGetGrade', 'parent=' + parent + '&type=' + storyType); + $.get(link, function(data) + { + data = JSON.parse(data); + const currentIndex = $(e.target).closest('tr').attr('data-index'); + $(e.target).closest('tbody').find('[name^=grade]').each(function(index) + { + if(index < currentIndex) return; + let $grade = $(this).zui('picker'); + $grade.render({items: data.items}); + $grade.$.setValue(data.default); + }) + }) +} + +window.switchType = function(e) +{ + const type = e.target.value; + const link = $.createLink(type, 'batchCreate', `productID=${productID}&branchID=${branch}&module=0&storyID=${storyID}&executionID=${executionID}`); + + loadPage(link); +} + window.changeRegion = function(e) { const $region = $(e.target); @@ -9,5 +76,4 @@ window.changeRegion = function(e) $region.closest('tr').find('[name^=lane]').zui('picker').render({items: data.items}); $region.closest('tr').find('[name^=lane]').zui('picker').$.setValue(laneID); }); - } diff --git a/module/story/js/batchedit.ui.js b/module/story/js/batchedit.ui.js index b5f283a981..c83dba4530 100644 --- a/module/story/js/batchedit.ui.js +++ b/module/story/js/batchedit.ui.js @@ -14,6 +14,8 @@ window.renderRowData = function($row, index, story) $picker.render(options); }); + $row.addClass('story' + story.id); + let $closedReasonTD = $row.find('[data-name="closedReason"]'); $closedReasonTD.find('.picker-box').on('inited', function(e, info) { @@ -33,9 +35,6 @@ window.renderRowData = function($row, index, story) let appendStoryHtml = ""; appendStoryHtml += "
"; - appendStoryHtml += ""; - appendStoryHtml += ""; - appendStoryHtml += ''; $closedReasonTD.find('.input-group').append(appendStoryHtml); items = []; @@ -56,9 +55,37 @@ window.renderRowData = function($row, index, story) let $module = $row.find('.form-batch-control[data-name="module"]'); let $plan = $row.find('.form-batch-control[data-name="plan"]'); let $branch = $row.find('.form-batch-control[data-name="branch"]'); + let $stage = $row.find('.form-batch-control[data-name="stage"]'); let $estimate = $row.find('.form-batch-control[data-name="estimate"]'); let $roadmap = $row.find('.form-batch-control[data-name="roadmap"]'); + if($estimate.length > 0 && story.isParent == '1') $estimate.find('input.form-control').attr('readonly', 'readonly'); + + if($stage.length > 0) + { + $stage.find('.picker-box').on('inited', function(e, info) + { + let $picker = info[0]; + let options = $picker.options; + let items = options.items; + + if(story.type == 'story' && story.isParent == '0') + { + /* 叶子需求删除父需求的阶段。*/ + items.splice(0, 1); + items.splice(1, 1); + items.splice(11, 1); + options.items = items; + } + else if(story.type != 'story' || story.isParent == '1') + { + options.disabled = true; + } + + $picker.render(options); + }) + } + $title.attr('disabled', 'disabled').attr('title', story.title).after(""); $row.find('.form-control-static[data-name="status"]').addClass('status-' + story.rawStatus); if($branch.length > 0) @@ -118,11 +145,13 @@ window.renderRowData = function($row, index, story) items.push({text: plans[plan], value: plan}); } options.items = items; - options.disabled = story.parent < 0 || story.type == 'requirement'; options.defaultValue = story.plan; + options.multiple = story.type != 'story' ? true : false; $picker.render(options); }); + + $row.attr('type', story.type); } if($estimate.length > 0) @@ -202,17 +231,10 @@ window.setDuplicateAndChild = function(resolution, storyID) { if(resolution == 'duplicate') { - $('#childStoryBox' + storyID).addClass('hidden'); $('#duplicateStoryBox' + storyID).removeClass('hidden'); } - else if(resolution == 'subdivided') - { - $('#duplicateStoryBox' + storyID).addClass('hidden'); - $('#childStoryBox' + storyID).removeClass('hidden'); - } else { $('#duplicateStoryBox' + storyID).addClass('hidden'); - $('#childStoryBox' + storyID).addClass('hidden'); } }; diff --git a/module/story/js/batchtotask.ui.js b/module/story/js/batchtotask.ui.js index 1bc9ede5ba..095bba5ce5 100644 --- a/module/story/js/batchtotask.ui.js +++ b/module/story/js/batchtotask.ui.js @@ -67,3 +67,12 @@ function copyStoryTitle(event) $($story.closest('tr').find('[name^=name]')).val(storyTitle); } + +if(hasERUR) +{ + zui.Modal.alert(errorERURSplitTask); +} +else if(hasParent) +{ + zui.Modal.alert(errorParentSplitTask); +} diff --git a/module/story/js/common.ui.js b/module/story/js/common.ui.js index 89b2300ede..86e19d0b66 100644 --- a/module/story/js/common.ui.js +++ b/module/story/js/common.ui.js @@ -44,43 +44,6 @@ window.toggleFeedback = function(obj) $('.feedbackBox').toggleClass('hidden', !feedbackSource.includes(source)); } -window.loadURS = function(e) -{ - const eventType = typeof e == 'undefined' ? '' : e.type; - const productID = $('[name=product]').val(); - - let requirementList = $('[name=URS]').val(); - requirementList = requirementList ? encodeURIComponent(requirementList.join(',')) : ''; - - let moduleID = 0; - if(eventType == 'change') moduleID = $('[name=module]').val(); - if(eventType == 'click' && !$(e.target).prop('checked')) moduleID = $('[name=module]').val(); - if($('#loadURS').prop('checked')) moduleID = 0; - - let branchID = 0; - let $branches = $('input[name^=branches]'); - if($branches.length > 0) - { - branchIdList = []; - $branches.each(function() - { - var currentBranch = $(this).val(); - if(currentBranch == '') currentBranch = 0; - if(!branchIdList.includes(currentBranch)) branchIdList.push(currentBranch); - }) - branchID = branchIdList.join(','); - } - - var link = $.createLink('story', 'ajaxGetURS', 'productID=' + productID + '&branchID=' + branchID + '&moduleID=' + moduleID + '&requirementList=' + requirementList); - $.get(link, function(data) - { - data = JSON.parse(data); - $URS = $('#URS').zui('picker'); - $URS.render({items: data.items}); - $URS.$.setValue($URS.$.value); - }) -}; - window.loadBranchModule = function(productID) { const branch = $('[name=branch]').val(); @@ -132,7 +95,6 @@ window.loadBranch = function() window.loadProductPlans(productID, branch); window.loadBranchModule(productID); - window.loadURS(); }; window.setModuleAndPlanByBranch = function(e) diff --git a/module/story/js/create.ui.js b/module/story/js/create.ui.js index 0ada4ca70b..1d9fd90ff0 100644 --- a/module/story/js/create.ui.js +++ b/module/story/js/create.ui.js @@ -98,3 +98,15 @@ $(document).on('click', 'form.form-setting-form .row label.state', function() }) formSettingLabelClicked = false; }); + +window.loadGrade = function(e) +{ + const parent = e.target.value; + const link = $.createLink('story', 'ajaxGetGrade', 'parent=' + parent + '&type=' + storyType); + $.getJSON(link, function(data) + { + const $grade = $('[name=grade]').zui('picker'); + $grade.render({items: data.items}); + $grade.$.setValue(data.default); + }) +} diff --git a/module/story/js/edit.ui.js b/module/story/js/edit.ui.js index a539deb35d..c8312906fc 100644 --- a/module/story/js/edit.ui.js +++ b/module/story/js/edit.ui.js @@ -13,29 +13,20 @@ window.loadProduct = function() } } - if(parentStory) + if(isParent == '1') { - confirmLoadProduct = confirm(moveChildrenTips); - if(!confirmLoadProduct) - { - $product.$.setValue(oldProductID.toString()); - return false; - } + zui.Modal.confirm(moveChildrenTips).then((result) => { + if(!result) + { + $product.$.setValue(oldProductID.toString(), true); + loadProductBranches(oldProductID); + return false; + } + }); } loadProductBranches(productID); loadProductReviewers(productID); - loadURS(); - - if(storyType == 'story') - { - var storyLink = $.createLink('story', 'ajaxGetParentStory', 'productID=' + productID + '&labelName=parent'); - var $parent = $('#parent').zui('picker'); - $.get(storyLink, function(data) - { - $parent.render(JSON.parse(data)); - }); - } } window.linkStories = function(e) @@ -116,7 +107,7 @@ function loadProductBranches(productID) if(data && data != '[]') { $branchBox.html("
").removeClass('hidden'); - $branch = new zui.Picker('.branchIdBox #branch', {items: JSON.parse(data), name: 'branch'}); + $branch = new zui.Picker('.branchIdBox #branch', {items: JSON.parse(data), name: 'branch', defaultValue: 0}); branch = $branch.$.value; } }); @@ -145,3 +136,33 @@ function loadProductReviewers(productID) if(needNotReview) $('.reviewerBox #reviewer').attr('disabled', 'disabled'); }); } + +window.loadGrade = function(e) +{ + let parent = e.target.value; + let link = $.createLink('story', 'ajaxGetGrade', 'parent=' + parent + '&type=' + storyType); + $.getJSON(link, function(options){ + const checkLink = $.createLink('story', 'ajaxCheckGrade', 'id=' + storyID + '&grade=' + options.default); + $.getJSON(checkLink, function(data){ + if(data.result) + { + const $grade = $('[name=grade]').zui('picker'); + $grade.render({items: options.items}); + $grade.$.setValue(options.default); + } + else + { + zui.Modal.alert(data.message.grade); + const $parent = $('[name=parent]').zui('picker'); + $parent.$.setValue(oldParent, true); + + let link = $.createLink('story', 'ajaxGetGrade', 'parent=' + oldParent + '&type=' + storyType); + $.getJSON(link, function(options){ + const $grade = $('[name=grade]').zui('picker'); + $grade.render({items: options.items}); + $grade.$.setValue(oldGrade); + }); + } + }); + }) +} diff --git a/module/story/js/report.ui.js b/module/story/js/report.ui.js index ff9d1e0ecb..5a160c62d1 100644 --- a/module/story/js/report.ui.js +++ b/module/story/js/report.ui.js @@ -33,5 +33,5 @@ function initReport() { if($(this).prop('checked')) form.append('charts[]', $(this).val()); }) - postAndLoadPage($.createLink('story', 'report', params + '&chartType=' + chartType + '&projectID=' + projectID), form, '#report'); + postAndLoadPage($.createLink(storyType, 'report', params + '&chartType=' + chartType + '&projectID=' + projectID), form, '#report'); } diff --git a/module/story/js/review.ui.js b/module/story/js/review.ui.js index 0a22acb36e..da1a8b16df 100644 --- a/module/story/js/review.ui.js +++ b/module/story/js/review.ui.js @@ -13,14 +13,12 @@ window.switchShow = function(obj) { $('#rejectedReasonBox').addClass('hidden'); $('#duplicateStoryBox').addClass('hidden'); - $('#childStoriesBox').addClass('hidden'); if(isLastOne) $('#assignedToBox').removeClass('hidden'); } else if(result == 'clarify') { $('#rejectedReasonBox').addClass('hidden'); $('#duplicateStoryBox').addClass('hidden'); - $('#childStoriesBox').addClass('hidden'); $('#rejectedReasonBox').addClass('hidden'); if(isLastOne) $('#assignedToBox').removeClass('hidden'); } @@ -28,7 +26,6 @@ window.switchShow = function(obj) { $('#rejectedReasonBox').addClass('hidden'); $('#duplicateStoryBox').addClass('hidden'); - $('#childStoriesBox').addClass('hidden'); $('#rejectedReasonBox').addClass('hidden'); if(isLastOne) $('#assignedToBox').removeClass('hidden'); if(result == 'pass') @@ -43,5 +40,4 @@ window.setStory = function(obj) { var reason = $(obj).val(); $('#duplicateStoryBox').toggleClass('hidden', reason != 'duplicate'); - $('#childStoriesBox').toggleClass('hidden', reason != 'subdivided'); } diff --git a/module/story/js/view.ui.js b/module/story/js/view.ui.js index cad7658538..a415ffc44f 100644 --- a/module/story/js/view.ui.js +++ b/module/story/js/view.ui.js @@ -1,3 +1,15 @@ +$(document).on('mouseenter', '.detail-side .tab-pane ul li', function(e) +{ + $(this).find('.unlink').removeClass('hidden'); + e.stopPropagation(); +}); + +$(document).on('mouseleave', '.detail-side .tab-pane ul li', function(e) +{ + $(this).find('.unlink').addClass('hidden'); + e.stopPropagation(); +}); + window.ajaxDelete = function(storyID) { zui.Modal.confirm({message: confirmDeleteTip, icon:'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'}).then((res) => diff --git a/module/story/lang/de.php b/module/story/lang/de.php index c67b4670c4..08ba0ad708 100644 --- a/module/story/lang/de.php +++ b/module/story/lang/de.php @@ -40,7 +40,7 @@ $lang->story->unlink = 'Unlink'; $lang->story->track = 'Track'; $lang->story->trackAB = 'Track'; $lang->story->processStoryChange = 'Confirm Story Change'; -$lang->story->splitRequirent = 'Decompose'; +$lang->story->split = 'Decompose'; $lang->story->close = 'Schließen'; $lang->story->batchClose = 'Mehere schließen'; $lang->story->activate = 'Aktivieren'; @@ -56,8 +56,8 @@ $lang->story->caseCount = 'Fälle'; $lang->story->taskCountAB = 'T'; $lang->story->bugCountAB = 'B'; $lang->story->caseCountAB = 'C'; -$lang->story->linkStory = "Link {$lang->SRCommon}"; -$lang->story->unlinkStory = "Verknüpfung aufheben {$lang->SRCommon}"; +$lang->story->linkStory = "Link Story"; +$lang->story->unlinkStory = "Verknüpfung aufheben story"; $lang->story->linkStoriesAB = "Link {$lang->SRCommon}"; $lang->story->linkRequirementsAB = "Link {$lang->URCommon}"; $lang->story->export = "Daten exportieren"; @@ -70,6 +70,9 @@ $lang->story->batchChangeBranch = "Mehere Branches ändern"; $lang->story->batchChangeStage = "Mehere Phasen ändern"; $lang->story->batchAssignTo = "Mehere zuordnen"; $lang->story->batchChangeModule = "Mehere Module ändern"; +$lang->story->batchChangeParent = "Batch Change Parent"; +$lang->story->batchChangeGrade = "Batch Change Grade"; +$lang->story->changeParent = "Change Parent"; $lang->story->viewAll = "Alle"; $lang->story->toTask = 'Convert to Task'; $lang->story->batchToTask = 'Batch Convert to Task'; @@ -84,11 +87,13 @@ $lang->story->relieved = 'Relieved'; $lang->story->relievedTwins = 'Relieved Twins'; $lang->story->loadAllStories = 'All'; $lang->story->hasDividedTask = 'has divided task'; +$lang->story->viewAllGrades = 'View All Grades'; $lang->story->codeBranch = 'Code branch'; $lang->story->unlinkBranch = 'Unlink code branch'; $lang->story->branchName = 'Branch Name'; $lang->story->branchFrom = 'Create from'; $lang->story->codeRepo = 'Code Library'; +$lang->story->viewByType = "View By %s"; $lang->story->editAction = "Edit {$lang->SRCommon}"; $lang->story->changeAction = "Change Story"; @@ -101,7 +106,6 @@ $lang->story->deleteAction = "Delete Story"; $lang->story->exportAction = "Export Story"; $lang->story->reportAction = "Story Report"; -$lang->story->skipStory = "{$lang->SRCommon} %s is parent stories, and this operation has been filtered."; $lang->story->closedStory = "{$lang->SRCommon} %s is closed, and this operation has been filtered."; $lang->story->batchToTaskTips = "Only active {$lang->SRCommon} can be converted into tasks."; $lang->story->successToTask = "Converted to task."; @@ -120,8 +124,13 @@ $lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and th $lang->story->id = 'ID'; $lang->story->parent = 'Parent'; +$lang->story->isParent = 'Is Parent'; +$lang->story->grade = 'Grade'; +$lang->story->gradeName = 'Grade Name'; +$lang->story->path = 'Path'; $lang->story->product = $lang->productCommon; $lang->story->project = $lang->projectCommon; +$lang->story->execution = "Execution"; $lang->story->branch = "Branch/Platform"; $lang->story->module = 'Module'; $lang->story->moduleAB = 'Module'; @@ -130,6 +139,7 @@ $lang->story->source = 'Von'; $lang->story->sourceNote = 'Hinweis'; $lang->story->fromBug = 'Von Bug'; $lang->story->title = 'Titel'; +$lang->story->name = "Name"; $lang->story->type = "Story/Requirement"; $lang->story->category = 'Category'; $lang->story->color = 'Color'; @@ -176,6 +186,7 @@ $lang->story->plan = 'Plan'; $lang->story->planAB = 'Plan'; $lang->story->comment = 'Kommentar'; $lang->story->children = "Child {$lang->SRCommon}"; +$lang->story->childItem = "Child Item"; $lang->story->childrenAB = "C"; $lang->story->linkStories = 'Story verknüpfen'; $lang->story->linkRequirements = "Linked {$lang->URCommon}"; @@ -212,6 +223,13 @@ $lang->story->estimateUnit = "(Unit: {$lang->story->hour})"; $lang->story->ditto = 'Dito'; $lang->story->dittoNotice = "This story is not linked to the same {$lang->productCommon} as the last one is!"; +$lang->story->viewTypeList['tiled'] = 'Tiled'; +$lang->story->viewTypeList['tree'] = 'Tree'; + +if($config->enableER) $lang->story->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->story->typeList['requirement'] = $lang->URCommon; +$lang->story->typeList['story'] = $lang->SRCommon; + $lang->story->needNotReviewList[0] = 'Need Review'; $lang->story->needNotReviewList[1] = 'Need Not Review'; @@ -232,14 +250,21 @@ if($config->systemMode == 'PLM') } $lang->story->stageList[''] = ''; +$lang->story->stageList['defining'] = 'Defining'; $lang->story->stageList['wait'] = 'Wartend'; +$lang->story->stageList['planning'] = 'Planning'; $lang->story->stageList['planned'] = 'Geplant'; $lang->story->stageList['projected'] = 'Projektiert'; +$lang->story->stageList['designing'] = 'Designing'; +$lang->story->stageList['designed'] = 'Designed'; $lang->story->stageList['developing'] = 'Entwicklung'; $lang->story->stageList['developed'] = 'Entwickelt'; $lang->story->stageList['testing'] = 'Testen'; $lang->story->stageList['tested'] = 'Getestet'; $lang->story->stageList['verified'] = 'Akzepiert'; +$lang->story->stageList['rejected'] = 'Verify Rejected'; +$lang->story->stageList['delivering'] = 'Delivering'; +$lang->story->stageList['pending'] = 'Release Pending'; $lang->story->stageList['released'] = 'Freigegeben'; $lang->story->stageList['closed'] = 'Geschlossen'; @@ -312,37 +337,51 @@ $lang->story->lblActivate = 'Aktivieren'; $lang->story->lblClose = 'Close'; $lang->story->lblTBC = 'Task/Bug/Case'; -$lang->story->checkAffection = 'Impact'; -$lang->story->affectedProjects = "{$lang->project->common}s/{$lang->execution->common}s"; -$lang->story->affectedBugs = 'Bug'; -$lang->story->affectedCases = 'Fall'; -$lang->story->affectedTwins = 'Zwillinge'; +$lang->story->checkAffection = 'Impact'; +$lang->story->affectedProjects = "{$lang->project->common}s/{$lang->execution->common}s"; +$lang->story->affectedBugs = 'Bug'; +$lang->story->affectedCases = 'Fall'; +$lang->story->affectedTwins = 'Zwillinge'; -$lang->story->specTemplate = "Als ein < type of user >, möchte ich < some goal > dass < some reason >."; -$lang->story->needNotReview = 'Keine Prüfung'; -$lang->story->successSaved = "Story wurde gespeichrt!"; -$lang->story->confirmDelete = "Möchten Sie diese Story löschen?"; -$lang->story->confirmRecall = "Do you want to recall this story?"; -$lang->story->errorEmptyChildStory = '『Unterteilte Story』 darf nicht leer sein.'; -$lang->story->errorNotSubdivide = "If the status is not active, or the stage is not wait, or a sub story, it cannot be subdivided."; -$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』darf nicht leer sein."; -$lang->story->mustChooseResult = 'Ergebnis wählen'; -$lang->story->mustChoosePreVersion = 'Version wählen um es umzukhren.'; -$lang->story->noStory = 'Keine Stories. '; -$lang->story->noRequirement = 'No Requirements'; -$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; -$lang->story->cannotDeleteParent = "Can not delete parent {$lang->SRCommon}"; -$lang->story->moveChildrenTips = "Its Child {$lang->SRCommon} will be moved to the selected {$lang->productCommon} when editing the linked {$lang->productCommon} of Parent {$lang->SRCommon}."; -$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; -$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; -$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; -$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; -$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; -$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; -$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; -$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; -$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; -$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->specTemplate = "Als ein < type of user >, möchte ich < some goal > dass < some reason >."; +$lang->story->needNotReview = 'Keine Prüfung'; +$lang->story->childStoryTitle = 'Contains %s sub-requirements, of which %s have been finished'; +$lang->story->childTaskTitle = 'Contains %s tasks, of which %s have been finished'; +$lang->story->successSaved = "Story wurde gespeichrt!"; +$lang->story->confirmDelete = "Möchten Sie diese Story löschen?"; +$lang->story->confirmRecall = "Do you want to recall this story?"; +$lang->story->errorEmptyChildStory = '『Unterteilte Story』 darf nicht leer sein.'; +$lang->story->errorNotSubdivide = "If the status is not active, or the stage is not wait, or a sub story, it cannot be subdivided."; +$lang->story->errorMaxGradeSubdivide = "The current story's grade exceeds the system setting, so it cannot be subdivided same type story."; +$lang->story->errorStepwiseSubdivide = "This requirement type does not allow cross-system splitting. This setting can be changed in the admin."; +$lang->story->errorCannotSplit = "This requirement has been split into sub-requirements of this type and cannot be split into requirements of other types."; +$lang->story->errorParentSplitTask = "Parent requirements cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorERURSplitTask = "Parent requirements,{$lang->ERCommon} and {$lang->URCommon}cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』darf nicht leer sein."; +$lang->story->mustChooseResult = 'Ergebnis wählen'; +$lang->story->mustChoosePreVersion = 'Version wählen um es umzukhren.'; +$lang->story->noEpic = "No Epics."; +$lang->story->noStory = 'Keine Stories. '; +$lang->story->noRequirement = 'No Requirements.'; +$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; +$lang->story->cannotDeleteParent = "Can not delete parent {$lang->SRCommon}"; +$lang->story->moveChildrenTips = "Are you sure to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; +$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; +$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; +$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; +$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; +$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; +$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; +$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; +$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; +$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->changeProductTips = "Are you sure you want to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->gradeOverflow = "The system detects that the maximum level of sub-requirements under this requirement is %s. After synchronization modification, it is %s. It exceeds the level range set by the system and cannot be modified."; +$lang->story->batchGradeOverflow = "After the parent requirement of %s is modified, the level of its child requirement will exceed the level range set by the system, and this modification has ignored it."; +$lang->story->batchGradeSameRoot = 'The requirements %s has a parent-child relationship and the requirement grade will not be modified.'; +$lang->story->batchGradeGtParent = 'The grade of %s requirements cannot be higher than its parent requirement, and it has been ignored in this modification.'; +$lang->story->batchParentError = "The parent requirement of %s requirement cannot be itself or its child requirement, which has been ignored in this modification."; $lang->story->form = new stdclass(); $lang->story->form->area = 'Story Bereich'; @@ -356,6 +395,7 @@ $lang->story->action->rejectreviewed = array('main' => '$date, geprüft v $lang->story->action->recalled = array('main' => '$date, recalled by $actor.'); $lang->story->action->closed = array('main' => '$date, geschlossen von $actor. Der Grund ist $extra $appendLink.', 'extra' => 'reasonList'); $lang->story->action->closedbysystem = array('main' => '$date, The system determines that the parent story is automatically closed because all child stories are closed.'); +$lang->story->action->closedbyparent = array('main' => '$date, The system determines that the child story is automatically closed because its parent story is closed.'); $lang->story->action->reviewpassed = array('main' => '$date, determined by the System. The result is Pass.'); $lang->story->action->reviewrejected = array('main' => '$date, closed by System. The reason is Rejection.'); $lang->story->action->reviewclarified = array('main' => '$date, determined by the System. The result is To Be Clarified. Please re-initiate the review after edit.'); @@ -379,29 +419,31 @@ $lang->story->action->unlinkrelatedstory = array('main' => '$date, Verknüpfu $lang->story->action->unlinkchildstory = array('main' => '$date, Verknüpfung aufgelöst durch $actor Story $extra.'); $lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.'); $lang->story->action->synctwins = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList'); +$lang->story->action->syncgrade = array('main' => "\$date, the system judges that this story's parent grade changed,this story grade synchronously changed to \$extra."); $lang->story->action->linked2roadmap = array('main' => '$date, linked by $actor to Roadmap $extra'); $lang->story->action->unlinkedfromroadmap = array('main' => '$date, unlinked by $actor from Roadmap $extra.'); $lang->story->action->changedbycharter = array('main' => '$date, launched by $actor for charter $extra, Synchronously adjust the story status to launched.'); /* Statistical statement. */ $lang->story->report = new stdclass(); -$lang->story->report->common = 'Bericht'; -$lang->story->report->select = 'Stories gruppieren nach'; -$lang->story->report->create = 'Erzeugen'; -$lang->story->report->value = 'Story Anzahl'; +$lang->story->report->common = 'Report'; +$lang->story->report->select = 'Select Report Type'; +$lang->story->report->create = 'Create Report'; +$lang->story->report->value = 'Reports'; -$lang->story->report->charts['storiesPerProduct'] = 'nach ' . $lang->productCommon; -$lang->story->report->charts['storiesPerModule'] = 'nach Modul'; -$lang->story->report->charts['storiesPerSource'] = 'nach Source'; -$lang->story->report->charts['storiesPerPlan'] = 'nach Plan'; -$lang->story->report->charts['storiesPerStatus'] = 'nach Status'; -$lang->story->report->charts['storiesPerStage'] = 'nach Phase'; -$lang->story->report->charts['storiesPerPri'] = 'nach Priorität'; -$lang->story->report->charts['storiesPerEstimate'] = 'nach Stunden'; -$lang->story->report->charts['storiesPerOpenedBy'] = 'nach Ersteller'; -$lang->story->report->charts['storiesPerAssignedTo'] = 'nach Zuständiger'; -$lang->story->report->charts['storiesPerClosedReason'] = 'nach Grund'; -$lang->story->report->charts['storiesPerChange'] = 'nach Änderungszeit'; +$lang->story->report->charts['storiesPerProduct'] = 'Group by ' . $lang->productCommon . ' Story'; +$lang->story->report->charts['storiesPerModule'] = 'Group by Module Story'; +$lang->story->report->charts['storiesPerSource'] = 'Group by Story Source'; +$lang->story->report->charts['storiesPerPlan'] = 'Group by Plan'; +$lang->story->report->charts['storiesPerStatus'] = 'Group by Status'; +$lang->story->report->charts['storiesPerStage'] = 'Group by Phase'; +$lang->story->report->charts['storiesPerPri'] = 'Group by Priority'; +$lang->story->report->charts['storiesPerEstimate'] = 'Group by Estimates'; +$lang->story->report->charts['storiesPerOpenedBy'] = 'Group by CreatedBy'; +$lang->story->report->charts['storiesPerAssignedTo'] = 'Group by AssignedTo'; +$lang->story->report->charts['storiesPerClosedReason'] = 'Group by Closed Reason'; +$lang->story->report->charts['storiesPerChange'] = 'Group by Changed Story'; +$lang->story->report->charts['storiesPerGrade'] = 'Group by Story Grade'; $lang->story->report->options = new stdclass(); $lang->story->report->options->graph = new stdclass(); @@ -421,19 +463,21 @@ $lang->story->report->storiesPerAssignedTo = new stdclass(); $lang->story->report->storiesPerClosedReason = new stdclass(); $lang->story->report->storiesPerEstimate = new stdclass(); $lang->story->report->storiesPerChange = new stdclass(); +$lang->story->report->storiesPerGrade = new stdclass(); $lang->story->report->storiesPerProduct->item = $lang->productCommon; -$lang->story->report->storiesPerModule->item = 'Modul'; +$lang->story->report->storiesPerModule->item = 'Module'; $lang->story->report->storiesPerSource->item = 'Source'; $lang->story->report->storiesPerPlan->item = 'Plan'; $lang->story->report->storiesPerStatus->item = 'Status'; $lang->story->report->storiesPerStage->item = 'Phase'; -$lang->story->report->storiesPerPri->item = 'Priorität'; -$lang->story->report->storiesPerOpenedBy->item = 'Konto'; -$lang->story->report->storiesPerAssignedTo->item = 'Benutzer'; -$lang->story->report->storiesPerClosedReason->item = 'Grund'; -$lang->story->report->storiesPerEstimate->item = 'Stunden'; -$lang->story->report->storiesPerChange->item = 'Änderung'; +$lang->story->report->storiesPerPri->item = 'Priority'; +$lang->story->report->storiesPerOpenedBy->item = 'OpenedBy'; +$lang->story->report->storiesPerAssignedTo->item = 'AssignedTo'; +$lang->story->report->storiesPerClosedReason->item = 'Reason'; +$lang->story->report->storiesPerEstimate->item = 'Estimates'; +$lang->story->report->storiesPerChange->item = 'Changed Story'; +$lang->story->report->storiesPerGrade->item = 'Grade'; $lang->story->report->storiesPerProduct->graph = new stdclass(); $lang->story->report->storiesPerModule->graph = new stdclass(); @@ -447,19 +491,21 @@ $lang->story->report->storiesPerAssignedTo->graph = new stdclass(); $lang->story->report->storiesPerClosedReason->graph = new stdclass(); $lang->story->report->storiesPerEstimate->graph = new stdclass(); $lang->story->report->storiesPerChange->graph = new stdclass(); +$lang->story->report->storiesPerGrade->graph = new stdclass(); $lang->story->report->storiesPerProduct->graph->xAxisName = $lang->productCommon; -$lang->story->report->storiesPerModule->graph->xAxisName = 'Modul'; +$lang->story->report->storiesPerModule->graph->xAxisName = 'Module'; $lang->story->report->storiesPerSource->graph->xAxisName = 'Source'; $lang->story->report->storiesPerPlan->graph->xAxisName = 'Plan'; $lang->story->report->storiesPerStatus->graph->xAxisName = 'Status'; $lang->story->report->storiesPerStage->graph->xAxisName = 'Phase'; -$lang->story->report->storiesPerPri->graph->xAxisName = 'Priorität'; -$lang->story->report->storiesPerOpenedBy->graph->xAxisName = 'Ersteller'; -$lang->story->report->storiesPerAssignedTo->graph->xAxisName = 'Zuständiger'; -$lang->story->report->storiesPerClosedReason->graph->xAxisName = 'Grund'; -$lang->story->report->storiesPerEstimate->graph->xAxisName = 'Stunden '; -$lang->story->report->storiesPerChange->graph->xAxisName = 'Änderung'; +$lang->story->report->storiesPerPri->graph->xAxisName = 'Priority'; +$lang->story->report->storiesPerOpenedBy->graph->xAxisName = 'CreatedBy'; +$lang->story->report->storiesPerAssignedTo->graph->xAxisName = 'AssignedTo'; +$lang->story->report->storiesPerClosedReason->graph->xAxisName = 'Close Reason'; +$lang->story->report->storiesPerEstimate->graph->xAxisName = 'Estimates '; +$lang->story->report->storiesPerChange->graph->xAxisName = 'Change Times'; +$lang->story->report->storiesPerGrade->graph->xAxisName = 'Change Times'; $lang->story->placeholder = new stdclass(); $lang->story->placeholder->estimate = $lang->story->hour; @@ -499,9 +545,8 @@ $lang->story->recallTip = array(); $lang->story->recallTip['actived'] = 'The Story has not initiated a review process and no undo action is required.'; $lang->story->subDivideTip = array(); -$lang->story->subDivideTip['subStory'] = 'The Sub-stories cannot be subdivided.'; $lang->story->subDivideTip['notWait'] = 'The Story has been %s and cannot be subdivided.'; -$lang->story->subDivideTip['notActive'] = "The %s is not active and cannot be subdivided."; +$lang->story->subDivideTip['notActive'] = "The story is not active and cannot be subdivided."; $lang->story->subDivideTip['twinsSplit'] = 'The Twins Story cannot be subdivided.'; $lang->story->featureBar['browse']['all'] = $lang->all; @@ -520,31 +565,6 @@ $lang->story->operateList['submitreview'] = 'submit review'; $lang->story->operateList['recalledchange'] = 'recalled change'; $lang->story->operateList['recalled'] = 'recalled review'; -$lang->requirement->common = $lang->URCommon; -$lang->requirement->create = 'Create Requirement'; -$lang->requirement->batchCreate = "Batch Create"; -$lang->requirement->editAction = "Edit {$lang->URCommon}"; -$lang->requirement->changeAction = "Change {$lang->URCommon}"; -$lang->requirement->assignAction = "Assign {$lang->URCommon}"; -$lang->requirement->reviewAction = "Review {$lang->URCommon}"; -$lang->requirement->subdivideAction = "Subdivide {$lang->URCommon}"; -$lang->requirement->closeAction = "Close {$lang->URCommon}"; -$lang->requirement->activateAction = "Activate {$lang->URCommon}"; -$lang->requirement->deleteAction = "Delete {$lang->URCommon}"; -$lang->requirement->exportAction = "Export {$lang->URCommon}"; -$lang->requirement->reportAction = "Report"; -$lang->requirement->recall = $lang->story->recallAction; -$lang->requirement->batchReview = 'Batch Review'; -$lang->requirement->batchEdit = "Batch Edit"; -$lang->requirement->batchClose = 'Batch Close'; -$lang->requirement->view = 'Requirement Detail'; -$lang->requirement->linkRequirementsAB = "Link {$lang->URCommon}"; -$lang->requirement->batchChangeBranch = "Batch Change Branches"; -$lang->requirement->batchAssignTo = "Batch Assign"; -$lang->requirement->batchChangeModule = "Batch Change Modules"; -$lang->requirement->submitReview = $lang->story->submitReview; -$lang->requirement->linkStory = 'Link Story'; - $lang->story->addBranch = 'Add %s'; $lang->story->deleteBranch = 'Delete %s'; $lang->story->notice->branch = 'Each branch will establish a requirement. The requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later'; @@ -552,3 +572,12 @@ $lang->story->notice->branch = 'Each branch will establish a requirement. The re $lang->story->relievedTwinsRelation = 'Relieved twins relationship'; $lang->story->relievedTwinsRelationTips = 'After the twins relationship is terminated, it cannot be restored and the shutdown of the requirement is no longer synchronized.'; $lang->story->changeRelievedTwinsTips = 'After the twins relationship is terminated, the twin stories are no longer synchronized.'; + +$lang->story->trackOrderByList['id'] = 'Order by ID'; +$lang->story->trackOrderByList['pri'] = 'Order by priority'; +$lang->story->trackOrderByList['status'] = 'Order by status'; +$lang->story->trackOrderByList['stage'] = 'Order by stage'; +$lang->story->trackOrderByList['category'] = 'Order by category'; + +$lang->story->trackSortList['asc'] = ' Ascending'; +$lang->story->trackSortList['desc'] = ' Descending'; diff --git a/module/story/lang/en.php b/module/story/lang/en.php index 93d678a561..0a152462d5 100644 --- a/module/story/lang/en.php +++ b/module/story/lang/en.php @@ -40,7 +40,7 @@ $lang->story->unlink = 'Unlink'; $lang->story->track = 'Track'; $lang->story->trackAB = 'Track'; $lang->story->processStoryChange = 'Confirm Story Change'; -$lang->story->splitRequirent = 'Decompose'; +$lang->story->split = 'Decompose'; $lang->story->close = 'Close'; $lang->story->batchClose = 'Batch Close'; $lang->story->activate = 'Activate'; @@ -56,8 +56,8 @@ $lang->story->caseCount = 'Cases'; $lang->story->taskCountAB = 'T'; $lang->story->bugCountAB = 'B'; $lang->story->caseCountAB = 'C'; -$lang->story->linkStory = "Link {$lang->SRCommon}"; -$lang->story->unlinkStory = "UnLinked {$lang->SRCommon}"; +$lang->story->linkStory = "Link Story"; +$lang->story->unlinkStory = "Do you confirm unlink this story?"; $lang->story->linkStoriesAB = "Link {$lang->SRCommon}"; $lang->story->linkRequirementsAB = "Link {$lang->URCommon}"; $lang->story->export = "Export Data"; @@ -70,6 +70,9 @@ $lang->story->batchChangeBranch = "Batch Change Branches"; $lang->story->batchChangeStage = "Batch Change Phases"; $lang->story->batchAssignTo = "Batch Assign"; $lang->story->batchChangeModule = "Batch Change Modules"; +$lang->story->batchChangeParent = "Batch Change Parent"; +$lang->story->batchChangeGrade = "Batch Change Grade"; +$lang->story->changeParent = "Change Parent"; $lang->story->viewAll = "See All"; $lang->story->toTask = 'Convert to Task'; $lang->story->batchToTask = 'Batch Convert to Task'; @@ -84,11 +87,13 @@ $lang->story->relieved = 'Relieved'; $lang->story->relievedTwins = 'Relieved Twins'; $lang->story->loadAllStories = 'All'; $lang->story->hasDividedTask = 'has divided task'; +$lang->story->viewAllGrades = 'View All Grades'; $lang->story->codeBranch = 'Code branch'; $lang->story->unlinkBranch = 'Unlink code branch'; $lang->story->branchName = 'Branch Name'; $lang->story->branchFrom = 'Create from'; $lang->story->codeRepo = 'Code Library'; +$lang->story->viewByType = "View By %s"; $lang->story->editAction = "Edit {$lang->SRCommon}"; $lang->story->changeAction = "Change {$lang->SRCommon}"; @@ -101,7 +106,6 @@ $lang->story->deleteAction = "Delete {$lang->SRCommon}"; $lang->story->exportAction = "Export {$lang->SRCommon}"; $lang->story->reportAction = "Report"; -$lang->story->skipStory = "{$lang->SRCommon} %s is parent stories, and this operation has been filtered."; $lang->story->closedStory = "{$lang->SRCommon} %s is closed, and this operation has been filtered."; $lang->story->batchToTaskTips = "Only active {$lang->SRCommon} can be converted into tasks."; $lang->story->successToTask = "Converted to task."; @@ -120,8 +124,13 @@ $lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and th $lang->story->id = 'ID'; $lang->story->parent = 'Parent'; +$lang->story->isParent = 'Is Parent'; +$lang->story->grade = 'Grade'; +$lang->story->gradeName = 'Grade Name'; +$lang->story->path = 'Path'; $lang->story->product = $lang->productCommon; $lang->story->project = $lang->projectCommon; +$lang->story->execution = "Execution"; $lang->story->branch = "Branch/Platform"; $lang->story->module = 'Module'; $lang->story->moduleAB = 'Module'; @@ -130,6 +139,7 @@ $lang->story->source = 'From'; $lang->story->sourceNote = 'Note'; $lang->story->fromBug = 'From Bug'; $lang->story->title = 'Title'; +$lang->story->name = "Name"; $lang->story->type = "Story/Requirement"; $lang->story->category = 'Category'; $lang->story->color = 'Color'; @@ -176,6 +186,7 @@ $lang->story->plan = 'Linked Plan'; $lang->story->planAB = 'Plan'; $lang->story->comment = 'Comment'; $lang->story->children = "Child {$lang->SRCommon}"; +$lang->story->childItem = "Child Item"; $lang->story->childrenAB = "C"; $lang->story->linkStories = 'Linked Story'; $lang->story->linkRequirements = "Linked {$lang->URCommon}"; @@ -212,6 +223,13 @@ $lang->story->estimateUnit = "(Unit: {$lang->story->hour})"; $lang->story->ditto = 'Ditto'; $lang->story->dittoNotice = "This story is not linked to the same {$lang->productCommon} as the last one is!"; +$lang->story->viewTypeList['tiled'] = 'Tiled'; +$lang->story->viewTypeList['tree'] = 'Tree'; + +if($config->enableER) $lang->story->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->story->typeList['requirement'] = $lang->URCommon; +$lang->story->typeList['story'] = $lang->SRCommon; + $lang->story->needNotReviewList[0] = 'Need Review'; $lang->story->needNotReviewList[1] = 'No Review'; @@ -232,14 +250,21 @@ if($config->systemMode == 'PLM') } $lang->story->stageList[''] = ''; +$lang->story->stageList['defining'] = 'Defining'; $lang->story->stageList['wait'] = 'Waiting'; +$lang->story->stageList['planning'] = 'Planning'; $lang->story->stageList['planned'] = 'Planned'; $lang->story->stageList['projected'] = 'Projected'; +$lang->story->stageList['designing'] = 'Designing'; +$lang->story->stageList['designed'] = 'Designed'; $lang->story->stageList['developing'] = 'Developing'; $lang->story->stageList['developed'] = 'Developed'; $lang->story->stageList['testing'] = 'Testing'; $lang->story->stageList['tested'] = 'Tested'; $lang->story->stageList['verified'] = 'Accepted'; +$lang->story->stageList['rejected'] = 'Verify Rejected'; +$lang->story->stageList['delivering'] = 'Delivering'; +$lang->story->stageList['pending'] = 'Release Pending'; $lang->story->stageList['released'] = 'Released'; $lang->story->stageList['closed'] = 'Closed'; @@ -318,31 +343,45 @@ $lang->story->affectedBugs = 'Bugs'; $lang->story->affectedCases = 'Cases'; $lang->story->affectedTwins = 'Twins'; -$lang->story->specTemplate = "As a < type of user >, I want < some goal > so that < some reason >."; -$lang->story->needNotReview = 'No Review'; -$lang->story->successSaved = "Story is saved!"; -$lang->story->confirmDelete = "Do you want to delete this story?"; -$lang->story->confirmRecall = "Do you want to recall this story?"; -$lang->story->errorEmptyChildStory = '『Decomposed Stories』canot be blank.'; -$lang->story->errorNotSubdivide = "If the status is not active, or the stage is not wait, or a sub story, it cannot be subdivided."; -$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』canot be blank."; -$lang->story->mustChooseResult = 'Select Result'; -$lang->story->mustChoosePreVersion = 'Select a version to revert to.'; -$lang->story->noStory = 'No stories yet. '; -$lang->story->noRequirement = 'No requirements yet. '; -$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; -$lang->story->cannotDeleteParent = "Can not delete parent {$lang->SRCommon}"; -$lang->story->moveChildrenTips = "Its Child {$lang->SRCommon} will be moved to the selected {$lang->productCommon} when editing the linked {$lang->productCommon} of Parent {$lang->SRCommon}."; -$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; -$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; -$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; -$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; -$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; -$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; -$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; -$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; -$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; -$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->specTemplate = "As a < type of user >, I want < some goal > so that < some reason >."; +$lang->story->needNotReview = 'No Review'; +$lang->story->childStoryTitle = 'Contains %s sub-requirements, of which %s have been finished'; +$lang->story->childTaskTitle = 'Contains %s tasks, of which %s have been finished'; +$lang->story->successSaved = "Story is saved!"; +$lang->story->confirmDelete = "Do you want to delete this story?"; +$lang->story->confirmRecall = "Do you want to recall this story?"; +$lang->story->errorEmptyChildStory = '『Decomposed Stories』canot be blank.'; +$lang->story->errorNotSubdivide = "If the status is not active, or the stage is not wait, or a sub story, it cannot be subdivided."; +$lang->story->errorMaxGradeSubdivide = "The current story's grade exceeds the system setting, so it cannot be subdivided same type story."; +$lang->story->errorStepwiseSubdivide = "This requirement type does not allow cross-system splitting. This setting can be changed in the admin."; +$lang->story->errorCannotSplit = "This requirement has been split into sub-requirements of this type and cannot be split into requirements of other types."; +$lang->story->errorParentSplitTask = "Parent requirements cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorERURSplitTask = "Parent requirements,{$lang->ERCommon} and {$lang->URCommon}cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』canot be blank."; +$lang->story->mustChooseResult = 'Select Result'; +$lang->story->mustChoosePreVersion = 'Select a version to revert to.'; +$lang->story->noEpic = "No Epics."; +$lang->story->noStory = 'No stories yet. '; +$lang->story->noRequirement = 'No requirements yet. '; +$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; +$lang->story->cannotDeleteParent = "Can not delete parent {$lang->SRCommon}"; +$lang->story->moveChildrenTips = "Are you sure to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; +$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; +$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; +$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; +$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; +$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; +$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; +$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; +$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; +$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->changeProductTips = "Are you sure you want to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->gradeOverflow = "The system detects that the maximum level of sub-requirements under this requirement is %s. After synchronization modification, it is %s. It exceeds the level range set by the system and cannot be modified."; +$lang->story->batchGradeOverflow = "After the parent requirement of %s is modified, the level of its child requirement will exceed the level range set by the system, and this modification has ignored it."; +$lang->story->batchGradeSameRoot = 'The requirements %s has a parent-child relationship and the requirement grade will not be modified.'; +$lang->story->batchGradeGtParent = 'The grade of %s requirements cannot be higher than its parent requirement, and it has been ignored in this modification.'; +$lang->story->batchParentError = "The parent requirement of %s requirement cannot be itself or its child requirement, which has been ignored in this modification."; $lang->story->form = new stdclass(); $lang->story->form->area = 'Scope'; @@ -356,6 +395,7 @@ $lang->story->action->rejectreviewed = array('main' => '$date, recorded b $lang->story->action->recalled = array('main' => '$date, recalled by $actor.'); $lang->story->action->closed = array('main' => '$date, closed by $actor. The reason is $extra $appendLink.', 'extra' => 'reasonList'); $lang->story->action->closedbysystem = array('main' => '$date, The system determines that the parent story is automatically closed because all child stories are closed.'); +$lang->story->action->closedbyparent = array('main' => '$date, The system determines that the child story is automatically closed because its parent story is closed.'); $lang->story->action->reviewpassed = array('main' => '$date, determined by the System. The result is Pass.'); $lang->story->action->reviewrejected = array('main' => '$date, closed by System. The reason is Rejection.'); $lang->story->action->reviewclarified = array('main' => '$date, determined by the System. The result is To Be Clarified. Please re-initiate the review after edit.'); @@ -379,6 +419,7 @@ $lang->story->action->unlinkrelatedstory = array('main' => '$date, unlinked b $lang->story->action->unlinkchildstory = array('main' => '$date, unlinked by $actor Decomposed Story $extra.'); $lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.'); $lang->story->action->synctwins = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList'); +$lang->story->action->syncgrade = array('main' => "\$date, the system judges that this story's parent grade changed,this story grade synchronously changed to \$extra."); $lang->story->action->linked2roadmap = array('main' => '$date, linked by $actor to Roadmap $extra'); $lang->story->action->unlinkedfromroadmap = array('main' => '$date, unlinked by $actor from Roadmap $extra.'); $lang->story->action->changedbycharter = array('main' => '$date, launched by $actor for charter $extra, Synchronously adjust the story status to launched.'); @@ -402,6 +443,7 @@ $lang->story->report->charts['storiesPerOpenedBy'] = 'Group by CreatedBy'; $lang->story->report->charts['storiesPerAssignedTo'] = 'Group by AssignedTo'; $lang->story->report->charts['storiesPerClosedReason'] = 'Group by Closed Reason'; $lang->story->report->charts['storiesPerChange'] = 'Group by Changed Story'; +$lang->story->report->charts['storiesPerGrade'] = 'Group by Story Grade'; $lang->story->report->options = new stdclass(); $lang->story->report->options->graph = new stdclass(); @@ -421,6 +463,7 @@ $lang->story->report->storiesPerAssignedTo = new stdclass(); $lang->story->report->storiesPerClosedReason = new stdclass(); $lang->story->report->storiesPerEstimate = new stdclass(); $lang->story->report->storiesPerChange = new stdclass(); +$lang->story->report->storiesPerGrade = new stdclass(); $lang->story->report->storiesPerProduct->item = $lang->productCommon; $lang->story->report->storiesPerModule->item = 'Module'; @@ -434,6 +477,7 @@ $lang->story->report->storiesPerAssignedTo->item = 'AssignedTo'; $lang->story->report->storiesPerClosedReason->item = 'Reason'; $lang->story->report->storiesPerEstimate->item = 'Estimates'; $lang->story->report->storiesPerChange->item = 'Changed Story'; +$lang->story->report->storiesPerGrade->item = 'Grade'; $lang->story->report->storiesPerProduct->graph = new stdclass(); $lang->story->report->storiesPerModule->graph = new stdclass(); @@ -447,6 +491,7 @@ $lang->story->report->storiesPerAssignedTo->graph = new stdclass(); $lang->story->report->storiesPerClosedReason->graph = new stdclass(); $lang->story->report->storiesPerEstimate->graph = new stdclass(); $lang->story->report->storiesPerChange->graph = new stdclass(); +$lang->story->report->storiesPerGrade->graph = new stdclass(); $lang->story->report->storiesPerProduct->graph->xAxisName = $lang->productCommon; $lang->story->report->storiesPerModule->graph->xAxisName = 'Module'; @@ -460,6 +505,7 @@ $lang->story->report->storiesPerAssignedTo->graph->xAxisName = 'AssignedTo'; $lang->story->report->storiesPerClosedReason->graph->xAxisName = 'Close Reason'; $lang->story->report->storiesPerEstimate->graph->xAxisName = 'Estimates '; $lang->story->report->storiesPerChange->graph->xAxisName = 'Change Times'; +$lang->story->report->storiesPerGrade->graph->xAxisName = 'Change Times'; $lang->story->placeholder = new stdclass(); $lang->story->placeholder->estimate = $lang->story->hour; @@ -499,9 +545,8 @@ $lang->story->recallTip = array(); $lang->story->recallTip['actived'] = 'The Story has not initiated a review process and no undo action is required.'; $lang->story->subDivideTip = array(); -$lang->story->subDivideTip['subStory'] = 'The Sub-stories cannot be subdivided.'; $lang->story->subDivideTip['notWait'] = 'The Story has been %s and cannot be subdivided.'; -$lang->story->subDivideTip['notActive'] = "The %s is not active and cannot be subdivided."; +$lang->story->subDivideTip['notActive'] = "The story is not active and cannot be subdivided."; $lang->story->subDivideTip['twinsSplit'] = 'The Twins Story cannot be subdivided.'; $lang->story->featureBar['browse']['all'] = $lang->all; @@ -520,31 +565,6 @@ $lang->story->operateList['submitreview'] = 'submit review'; $lang->story->operateList['recalledchange'] = 'recalled change'; $lang->story->operateList['recalled'] = 'recalled review'; -$lang->requirement->common = $lang->URCommon; -$lang->requirement->create = 'Create Requirement'; -$lang->requirement->batchCreate = "Batch Create"; -$lang->requirement->editAction = "Edit {$lang->URCommon}"; -$lang->requirement->changeAction = "Change {$lang->URCommon}"; -$lang->requirement->assignAction = "Assign {$lang->URCommon}"; -$lang->requirement->reviewAction = "Review {$lang->URCommon}"; -$lang->requirement->subdivideAction = "Subdivide {$lang->URCommon}"; -$lang->requirement->closeAction = "Close {$lang->URCommon}"; -$lang->requirement->activateAction = "Activate {$lang->URCommon}"; -$lang->requirement->deleteAction = "Delete {$lang->URCommon}"; -$lang->requirement->exportAction = "Export {$lang->URCommon}"; -$lang->requirement->reportAction = "Report"; -$lang->requirement->recall = $lang->story->recallAction; -$lang->requirement->batchReview = 'Batch Review'; -$lang->requirement->batchEdit = "Batch Edit"; -$lang->requirement->batchClose = 'Batch Close'; -$lang->requirement->view = 'Requirement Detail'; -$lang->requirement->linkRequirementsAB = "Link {$lang->URCommon}"; -$lang->requirement->batchChangeBranch = "Batch Change Branches"; -$lang->requirement->batchAssignTo = "Batch Assign"; -$lang->requirement->batchChangeModule = "Batch Change Modules"; -$lang->requirement->submitReview = $lang->story->submitReview; -$lang->requirement->linkStory = 'Link Story'; - $lang->story->addBranch = 'Add %s'; $lang->story->deleteBranch = 'Delete %s'; $lang->story->notice->branch = 'Each branch will establish a requirement. The requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later'; @@ -552,3 +572,12 @@ $lang->story->notice->branch = 'Each branch will establish a requirement. The re $lang->story->relievedTwinsRelation = 'Relieved twins relationship'; $lang->story->relievedTwinsRelationTips = 'After the twins relationship is terminated, it cannot be restored and the shutdown of the requirement is no longer synchronized.'; $lang->story->changeRelievedTwinsTips = 'After the twins relationship is terminated, the twin stories are no longer synchronized.'; + +$lang->story->trackOrderByList['id'] = 'Order by ID'; +$lang->story->trackOrderByList['pri'] = 'Order by priority'; +$lang->story->trackOrderByList['status'] = 'Order by status'; +$lang->story->trackOrderByList['stage'] = 'Order by stage'; +$lang->story->trackOrderByList['category'] = 'Order by category'; + +$lang->story->trackSortList['asc'] = ' Ascending'; +$lang->story->trackSortList['desc'] = ' Descending'; diff --git a/module/story/lang/fr.php b/module/story/lang/fr.php index 9482fb8221..000d914de1 100644 --- a/module/story/lang/fr.php +++ b/module/story/lang/fr.php @@ -40,7 +40,7 @@ $lang->story->unlink = 'Unlink'; $lang->story->track = 'Track'; $lang->story->trackAB = 'Track'; $lang->story->processStoryChange = 'Confirm Story Change'; -$lang->story->splitRequirent = 'Décompose'; +$lang->story->split = 'Décompose'; $lang->story->close = 'Fermer'; $lang->story->batchClose = 'Fermer par Lot'; $lang->story->activate = 'Activer'; @@ -56,8 +56,8 @@ $lang->story->caseCount = 'CasTests'; $lang->story->taskCountAB = 'T'; $lang->story->bugCountAB = 'B'; $lang->story->caseCountAB = 'C'; -$lang->story->linkStory = "Lier {$lang->SRCommon}"; -$lang->story->unlinkStory = "Dissocier {$lang->SRCommon}"; +$lang->story->linkStory = "Lier Story"; +$lang->story->unlinkStory = "Do you confirm unlink this story?"; $lang->story->linkStoriesAB = "Lier {$lang->SRCommon}"; $lang->story->linkRequirementsAB = "Lier {$lang->URCommon}"; $lang->story->export = "Exporter Données"; @@ -70,6 +70,9 @@ $lang->story->batchChangeBranch = "Changer Branches par lot"; $lang->story->batchChangeStage = "Changer Phases par lot"; $lang->story->batchAssignTo = "Affecter par lot"; $lang->story->batchChangeModule = "Changer Modules par lot"; +$lang->story->batchChangeParent = "Batch Change Parent"; +$lang->story->batchChangeGrade = "Batch Change Grade"; +$lang->story->changeParent = "Change Parent"; $lang->story->viewAll = "Voir Tout"; $lang->story->toTask = 'Convert to Task'; $lang->story->batchToTask = 'Batch Convert to Task'; @@ -84,11 +87,13 @@ $lang->story->relieved = 'Relieved'; $lang->story->relievedTwins = 'Relieved Twins'; $lang->story->loadAllStories = 'All'; $lang->story->hasDividedTask = 'has divided task'; +$lang->story->viewAllGrades = 'View All Grades'; $lang->story->codeBranch = 'Code branch'; $lang->story->unlinkBranch = 'Unlink code branch'; $lang->story->branchName = 'Branch Name'; $lang->story->branchFrom = 'Create from'; $lang->story->codeRepo = 'Code Library'; +$lang->story->viewByType = "View By %s"; $lang->story->editAction = "Edit {$lang->SRCommon}"; $lang->story->changeAction = "Changer Story"; @@ -101,7 +106,6 @@ $lang->story->deleteAction = "Supprimer Story"; $lang->story->exportAction = "Exporter Story"; $lang->story->reportAction = "Rapport de Story"; -$lang->story->skipStory = "{$lang->SRCommon} %s is parent stories, and this operation has been filtered."; $lang->story->closedStory = "{$lang->SRCommon} %s is closed, and this operation has been filtered."; $lang->story->batchToTaskTips = "Only active {$lang->SRCommon} can be converted into tasks."; $lang->story->successToTask = "Converted to task."; @@ -120,8 +124,13 @@ $lang->story->batchEditTip = "{$lang->SRCommon} %sis twin stories, and th $lang->story->id = 'ID'; $lang->story->parent = 'Parent'; +$lang->story->isParent = 'Is Parent'; +$lang->story->grade = 'Grade'; +$lang->story->gradeName = 'Grade Name'; +$lang->story->path = 'Path'; $lang->story->product = $lang->productCommon; $lang->story->project = $lang->projectCommon; +$lang->story->execution = "Execution"; $lang->story->branch = "Branche/Plateforme"; $lang->story->module = 'Module'; $lang->story->moduleAB = 'Module'; @@ -130,6 +139,7 @@ $lang->story->source = 'De'; $lang->story->sourceNote = 'Note'; $lang->story->fromBug = 'Depuis Bug'; $lang->story->title = 'Titre'; +$lang->story->name = "Name"; $lang->story->type = "Story/Requirement"; $lang->story->category = 'Category'; $lang->story->color = 'Couleur'; @@ -176,6 +186,7 @@ $lang->story->plan = 'Intégrée Plans'; $lang->story->planAB = 'Plan'; $lang->story->comment = 'Commentaire'; $lang->story->children = "Enfant {$lang->SRCommon}"; +$lang->story->childItem = "Child Item"; $lang->story->childrenAB = "C"; $lang->story->linkStories = 'Stories Liées'; $lang->story->linkRequirements = "{$lang->URCommon} Lié es"; @@ -212,6 +223,13 @@ $lang->story->estimateUnit = "(Unit: {$lang->story->hour})"; $lang->story->ditto = 'Idem'; $lang->story->dittoNotice = "This story is not linked to the same {$lang->productCommon} as the last one is!"; +$lang->story->viewTypeList['tiled'] = 'Tiled'; +$lang->story->viewTypeList['tree'] = 'Tree'; + +if($config->enableER) $lang->story->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->story->typeList['requirement'] = $lang->URCommon; +$lang->story->typeList['story'] = $lang->SRCommon; + $lang->story->needNotReviewList[0] = 'Need Review'; $lang->story->needNotReviewList[1] = 'Need Not Review'; @@ -232,14 +250,21 @@ if($config->systemMode == 'PLM') } $lang->story->stageList[''] = ''; +$lang->story->stageList['defining'] = 'Defining'; $lang->story->stageList['wait'] = 'En Attente'; +$lang->story->stageList['planning'] = 'Planning'; $lang->story->stageList['planned'] = 'Plannifée'; $lang->story->stageList['projected'] = 'Projetée'; +$lang->story->stageList['designing'] = 'Designing'; +$lang->story->stageList['designed'] = 'Designed'; $lang->story->stageList['developing'] = 'En Dév.'; $lang->story->stageList['developed'] = 'Développée'; $lang->story->stageList['testing'] = 'En Test'; $lang->story->stageList['tested'] = 'Testée'; $lang->story->stageList['verified'] = 'Acceptée'; +$lang->story->stageList['rejected'] = 'Verify Rejected'; +$lang->story->stageList['delivering'] = 'Delivering'; +$lang->story->stageList['pending'] = 'Release Pending'; $lang->story->stageList['released'] = 'Versionnée'; $lang->story->stageList['closed'] = 'Fermée'; @@ -318,31 +343,45 @@ $lang->story->affectedBugs = 'Bugs'; $lang->story->affectedCases = 'CasTests'; $lang->story->affectedTwins = 'des jumeaux'; -$lang->story->specTemplate = "En tant que < type utilisateur >, je veux < différents objectifs > pour < différentes raisons >."; -$lang->story->needNotReview = 'Aucune Validation Requise'; -$lang->story->successSaved = "Story est sauvegardée !"; -$lang->story->confirmDelete = "Voulez-vous vraiment supprimer cette story ?"; -$lang->story->confirmRecall = "Do you want to recall this story?"; -$lang->story->errorEmptyChildStory = '『Decomposed Stories』ne peuvent être vides.'; -$lang->story->errorNotSubdivide = "Si le statut n'est pas actif, ou l'étape n'est pas en attente, ou si c'est une sous-story, elle ne peut pas être subdivisée."; -$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』canot be blank."; -$lang->story->mustChooseResult = 'Sélect Résultat'; -$lang->story->mustChoosePreVersion = 'Sélect une version pour revenir en arrière.'; -$lang->story->noStory = "Aucune story pour l'instant. "; -$lang->story->noRequirement = 'No Requirements'; -$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; -$lang->story->cannotDeleteParent = "Impossible de supprimer {$lang->SRCommon} parent"; -$lang->story->moveChildrenTips = "Its Child {$lang->SRCommon} will be moved to the selected {$lang->productCommon} when editing the linked {$lang->productCommon} of Parent {$lang->SRCommon}."; -$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; -$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; -$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; -$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; -$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; -$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; -$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; -$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; -$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; -$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->specTemplate = "En tant que < type utilisateur >, je veux < différents objectifs > pour < différentes raisons >."; +$lang->story->needNotReview = 'Aucune Validation Requise'; +$lang->story->childStoryTitle = 'Contains %s sub-requirements, of which %s have been finished'; +$lang->story->childTaskTitle = 'Contains %s tasks, of which %s have been finished'; +$lang->story->successSaved = "Story est sauvegardée !"; +$lang->story->confirmDelete = "Voulez-vous vraiment supprimer cette story ?"; +$lang->story->confirmRecall = "Do you want to recall this story?"; +$lang->story->errorEmptyChildStory = '『Decomposed Stories』ne peuvent être vides.'; +$lang->story->errorNotSubdivide = "Si le statut n'est pas actif, ou l'étape n'est pas en attente, ou si c'est une sous-story, elle ne peut pas être subdivisée."; +$lang->story->errorMaxGradeSubdivide = "The current story's grade exceeds the system setting, so it cannot be subdivided same type story."; +$lang->story->errorStepwiseSubdivide = "This requirement type does not allow cross-system splitting. This setting can be changed in the admin."; +$lang->story->errorCannotSplit = "This requirement has been split into sub-requirements of this type and cannot be split into requirements of other types."; +$lang->story->errorParentSplitTask = "Parent requirements cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorERURSplitTask = "Parent requirements,{$lang->ERCommon} and {$lang->URCommon}cannot be transferred to tasks, this operation has been filtered."; +$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』canot be blank."; +$lang->story->mustChooseResult = 'Sélect Résultat'; +$lang->story->mustChoosePreVersion = 'Sélect une version pour revenir en arrière.'; +$lang->story->noEpic = "No Epics."; +$lang->story->noStory = "Aucune story pour l'instant. "; +$lang->story->noRequirement = 'No Requirements'; +$lang->story->ignoreChangeStage = 'The status of %s is Draft or Closed. This operation has been filtered.'; +$lang->story->cannotDeleteParent = "Impossible de supprimer {$lang->SRCommon} parent"; +$lang->story->moveChildrenTips = "Are you sure to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->changeTips = 'The story associated with the requirements to change, click "Cancel" ignore this change, click "Confirm" to change the story.'; +$lang->story->estimateMustBeNumber = 'Estimate value must be number.'; +$lang->story->estimateMustBePlus = 'Estimated value cannot be negative'; +$lang->story->confirmChangeBranch = $lang->SRCommon . ' %s is linked to the plan of its linked branch. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan of its linked branch. Do you want to continue edit ' . $lang->SRCommon . '?'; +$lang->story->confirmChangePlan = $lang->SRCommon . ' %s is linked to the branch of its plan. If the branch is edited, ' . $lang->SRCommon . ' will be removed from the plan. Do you want to continue edit branch ?'; +$lang->story->errorDuplicateStory = $lang->SRCommon . '%s not exist'; +$lang->story->confirmRecallChange = "After undo the change, the story content will revert to the version before the change. Are you sure you want to undo?"; +$lang->story->confirmRecallReview = "Are you sure you want to withdraw the review?"; +$lang->story->noStoryToTask = "Only the activated {$lang->SRCommon} can be converted into a task!"; +$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s status is closed, and the operation has been filtered."; +$lang->story->changeProductTips = "Are you sure you want to modify the product? After modification, all sub-level requirements of the requirement will also be changed accordingly."; +$lang->story->gradeOverflow = "The system detects that the maximum level of sub-requirements under this requirement is %s. After synchronization modification, it is %s. It exceeds the level range set by the system and cannot be modified."; +$lang->story->batchGradeOverflow = "After the parent requirement of %s is modified, the level of its child requirement will exceed the level range set by the system, and this modification has ignored it."; +$lang->story->batchGradeSameRoot = 'The requirements %s has a parent-child relationship and the requirement grade will not be modified.'; +$lang->story->batchGradeGtParent = 'The grade of %s requirements cannot be higher than its parent requirement, and it has been ignored in this modification.'; +$lang->story->batchParentError = "The parent requirement of %s requirement cannot be itself or its child requirement, which has been ignored in this modification."; $lang->story->form = new stdclass(); $lang->story->form->area = 'Périmètre'; @@ -356,6 +395,7 @@ $lang->story->action->rejectreviewed = array('main' => '$date, validée p $lang->story->action->recalled = array('main' => '$date, recalled by $actor.'); $lang->story->action->closed = array('main' => '$date, Fermée par $actor. La raison est $extra $appendLink.', 'extra' => 'reasonList'); $lang->story->action->closedbysystem = array('main' => '$date, The system determines that the parent story is automatically closed because all child stories are closed.'); +$lang->story->action->closedbyparent = array('main' => '$date, The system determines that the child story is automatically closed because its parent story is closed.'); $lang->story->action->reviewpassed = array('main' => '$date, determined by the System. The result is Pass.'); $lang->story->action->reviewrejected = array('main' => '$date, closed by System. The reason is Rejection.'); $lang->story->action->reviewclarified = array('main' => '$date, determined by the System. The result is To Be Clarified. Please re-initiate the review after edit.'); @@ -379,29 +419,31 @@ $lang->story->action->unlinkrelatedstory = array('main' => '$date, dissociée $lang->story->action->unlinkchildstory = array('main' => '$date, dissociée par $actor de la sous-Story $extra.'); $lang->story->action->recalledchange = array('main' => '$date, Undo changes by \$actor.'); $lang->story->action->synctwins = array('main' => "\$date, the system judges that this story is adjusted synchronously due to the \$operate of twin story \$extra.", 'operate' => 'operateList'); +$lang->story->action->syncgrade = array('main' => "\$date, the system judges that this story's parent grade changed,this story grade synchronously changed to \$extra."); $lang->story->action->linked2roadmap = array('main' => '$date, linked by $actor to Roadmap $extra'); $lang->story->action->unlinkedfromroadmap = array('main' => '$date, unlinked by $actor from Roadmap $extra.'); $lang->story->action->changedbycharter = array('main' => '$date, launched by $actor for charter $extra, Synchronously adjust the story status to launched.'); /* Statistical statement. */ $lang->story->report = new stdclass(); -$lang->story->report->common = 'Rapport'; -$lang->story->report->select = 'Choix Type de Rapport'; -$lang->story->report->create = 'Créer Rapport'; -$lang->story->report->value = 'Rapports'; +$lang->story->report->common = 'Report'; +$lang->story->report->select = 'Select Report Type'; +$lang->story->report->create = 'Create Report'; +$lang->story->report->value = 'Reports'; -$lang->story->report->charts['storiesPerProduct'] = 'Regroupé par ' . $lang->productCommon . ' Story'; -$lang->story->report->charts['storiesPerModule'] = 'Regroupé par Module Story'; -$lang->story->report->charts['storiesPerSource'] = 'Regroupé par Source'; -$lang->story->report->charts['storiesPerPlan'] = 'Regroupé par Plan'; -$lang->story->report->charts['storiesPerStatus'] = 'Regroupé par Statut'; -$lang->story->report->charts['storiesPerStage'] = 'Regroupé par Phase'; -$lang->story->report->charts['storiesPerPri'] = 'Regroupé par Priorité'; -$lang->story->report->charts['storiesPerEstimate'] = 'Regroupé par Estimés'; -$lang->story->report->charts['storiesPerOpenedBy'] = 'Regroupé par Créateur'; -$lang->story->report->charts['storiesPerAssignedTo'] = 'Regroupé par Détenteur'; -$lang->story->report->charts['storiesPerClosedReason'] = 'Par Raison de Fermeture'; -$lang->story->report->charts['storiesPerChange'] = 'Regroupé par Modif de Story'; +$lang->story->report->charts['storiesPerProduct'] = 'Group by ' . $lang->productCommon . ' Story'; +$lang->story->report->charts['storiesPerModule'] = 'Group by Module Story'; +$lang->story->report->charts['storiesPerSource'] = 'Group by Story Source'; +$lang->story->report->charts['storiesPerPlan'] = 'Group by Plan'; +$lang->story->report->charts['storiesPerStatus'] = 'Group by Status'; +$lang->story->report->charts['storiesPerStage'] = 'Group by Phase'; +$lang->story->report->charts['storiesPerPri'] = 'Group by Priority'; +$lang->story->report->charts['storiesPerEstimate'] = 'Group by Estimates'; +$lang->story->report->charts['storiesPerOpenedBy'] = 'Group by CreatedBy'; +$lang->story->report->charts['storiesPerAssignedTo'] = 'Group by AssignedTo'; +$lang->story->report->charts['storiesPerClosedReason'] = 'Group by Closed Reason'; +$lang->story->report->charts['storiesPerChange'] = 'Group by Changed Story'; +$lang->story->report->charts['storiesPerGrade'] = 'Group by Story Grade'; $lang->story->report->options = new stdclass(); $lang->story->report->options->graph = new stdclass(); @@ -421,19 +463,21 @@ $lang->story->report->storiesPerAssignedTo = new stdclass(); $lang->story->report->storiesPerClosedReason = new stdclass(); $lang->story->report->storiesPerEstimate = new stdclass(); $lang->story->report->storiesPerChange = new stdclass(); +$lang->story->report->storiesPerGrade = new stdclass(); $lang->story->report->storiesPerProduct->item = $lang->productCommon; $lang->story->report->storiesPerModule->item = 'Module'; $lang->story->report->storiesPerSource->item = 'Source'; $lang->story->report->storiesPerPlan->item = 'Plan'; -$lang->story->report->storiesPerStatus->item = 'Statut'; +$lang->story->report->storiesPerStatus->item = 'Status'; $lang->story->report->storiesPerStage->item = 'Phase'; -$lang->story->report->storiesPerPri->item = 'Priorité'; -$lang->story->report->storiesPerOpenedBy->item = 'Créateur'; -$lang->story->report->storiesPerAssignedTo->item = 'Détenteur'; -$lang->story->report->storiesPerClosedReason->item = 'Raison'; -$lang->story->report->storiesPerEstimate->item = 'Estimation'; -$lang->story->report->storiesPerChange->item = 'Chang. Story'; +$lang->story->report->storiesPerPri->item = 'Priority'; +$lang->story->report->storiesPerOpenedBy->item = 'OpenedBy'; +$lang->story->report->storiesPerAssignedTo->item = 'AssignedTo'; +$lang->story->report->storiesPerClosedReason->item = 'Reason'; +$lang->story->report->storiesPerEstimate->item = 'Estimates'; +$lang->story->report->storiesPerChange->item = 'Changed Story'; +$lang->story->report->storiesPerGrade->item = 'Grade'; $lang->story->report->storiesPerProduct->graph = new stdclass(); $lang->story->report->storiesPerModule->graph = new stdclass(); @@ -447,19 +491,21 @@ $lang->story->report->storiesPerAssignedTo->graph = new stdclass(); $lang->story->report->storiesPerClosedReason->graph = new stdclass(); $lang->story->report->storiesPerEstimate->graph = new stdclass(); $lang->story->report->storiesPerChange->graph = new stdclass(); +$lang->story->report->storiesPerGrade->graph = new stdclass(); $lang->story->report->storiesPerProduct->graph->xAxisName = $lang->productCommon; $lang->story->report->storiesPerModule->graph->xAxisName = 'Module'; $lang->story->report->storiesPerSource->graph->xAxisName = 'Source'; $lang->story->report->storiesPerPlan->graph->xAxisName = 'Plan'; -$lang->story->report->storiesPerStatus->graph->xAxisName = 'Statut'; +$lang->story->report->storiesPerStatus->graph->xAxisName = 'Status'; $lang->story->report->storiesPerStage->graph->xAxisName = 'Phase'; -$lang->story->report->storiesPerPri->graph->xAxisName = 'Priorité'; -$lang->story->report->storiesPerOpenedBy->graph->xAxisName = 'Créateur'; -$lang->story->report->storiesPerAssignedTo->graph->xAxisName = 'Détenteur'; -$lang->story->report->storiesPerClosedReason->graph->xAxisName = 'Raison Fermeture'; -$lang->story->report->storiesPerEstimate->graph->xAxisName = 'Estimattion '; +$lang->story->report->storiesPerPri->graph->xAxisName = 'Priority'; +$lang->story->report->storiesPerOpenedBy->graph->xAxisName = 'CreatedBy'; +$lang->story->report->storiesPerAssignedTo->graph->xAxisName = 'AssignedTo'; +$lang->story->report->storiesPerClosedReason->graph->xAxisName = 'Close Reason'; +$lang->story->report->storiesPerEstimate->graph->xAxisName = 'Estimates '; $lang->story->report->storiesPerChange->graph->xAxisName = 'Change Times'; +$lang->story->report->storiesPerGrade->graph->xAxisName = 'Change Times'; $lang->story->placeholder = new stdclass(); $lang->story->placeholder->estimate = $lang->story->hour; @@ -499,9 +545,8 @@ $lang->story->recallTip = array(); $lang->story->recallTip['actived'] = "Aucun processus de révision n'a été lancé pour cette Story, et il n'est pas nécessaire de révoquer l'opération."; $lang->story->subDivideTip = array(); -$lang->story->subDivideTip['subStory'] = 'Les sous-stories ne peuvent pas être décomposées.'; $lang->story->subDivideTip['notWait'] = "L'exigence %s ne peut pas être subdivisée."; -$lang->story->subDivideTip['notActive'] = "The %s is not active and cannot be subdivided."; +$lang->story->subDivideTip['notActive'] = "The story is not active and cannot be subdivided."; $lang->story->subDivideTip['twinsSplit'] = 'The Twins Story cannot be subdivided.'; $lang->story->featureBar['browse']['all'] = $lang->all; @@ -520,31 +565,6 @@ $lang->story->operateList['submitreview'] = 'submit review'; $lang->story->operateList['recalledchange'] = 'recalled change'; $lang->story->operateList['recalled'] = 'recalled review'; -$lang->requirement->common = $lang->URCommon; -$lang->requirement->create = 'Create Requirement'; -$lang->requirement->batchCreate = "Batch Create"; -$lang->requirement->editAction = "Edit {$lang->URCommon}"; -$lang->requirement->changeAction = "Change {$lang->URCommon}"; -$lang->requirement->assignAction = "Assign {$lang->URCommon}"; -$lang->requirement->reviewAction = "Review {$lang->URCommon}"; -$lang->requirement->subdivideAction = "Subdivide {$lang->URCommon}"; -$lang->requirement->closeAction = "Close {$lang->URCommon}"; -$lang->requirement->activateAction = "Activate {$lang->URCommon}"; -$lang->requirement->deleteAction = "Delete {$lang->URCommon}"; -$lang->requirement->exportAction = "Export {$lang->URCommon}"; -$lang->requirement->reportAction = "Report"; -$lang->requirement->recall = $lang->story->recallAction; -$lang->requirement->batchReview = 'Batch Review'; -$lang->requirement->batchEdit = "Batch Edit"; -$lang->requirement->batchClose = 'Batch Close'; -$lang->requirement->view = 'Requirement Detail'; -$lang->requirement->linkRequirementsAB = "Link {$lang->URCommon}"; -$lang->requirement->batchChangeBranch = "Batch Change Branches"; -$lang->requirement->batchAssignTo = "Batch Assign"; -$lang->requirement->batchChangeModule = "Batch Change Modules"; -$lang->requirement->submitReview = $lang->story->submitReview; -$lang->requirement->linkStory = 'Lier Story'; - $lang->story->addBranch = 'Add %s'; $lang->story->deleteBranch = 'Delete %s'; $lang->story->notice->branch = 'Each branch will establish a requirement. The requirements are twins. The twins requirements are synchronized except for the product, branch, module, plan, and stage fields. You can manually remove the twins relationship later'; @@ -552,3 +572,12 @@ $lang->story->notice->branch = 'Each branch will establish a requirement. The re $lang->story->relievedTwinsRelation = 'Relieved twins relationship'; $lang->story->relievedTwinsRelationTips = 'After the twins relationship is terminated, it cannot be restored and the shutdown of the requirement is no longer synchronized.'; $lang->story->changeRelievedTwinsTips = 'After the twins relationship is terminated, the twin stories are no longer synchronized.'; + +$lang->story->trackOrderByList['id'] = 'Order by ID'; +$lang->story->trackOrderByList['pri'] = 'Order by priority'; +$lang->story->trackOrderByList['status'] = 'Order by status'; +$lang->story->trackOrderByList['stage'] = 'Order by stage'; +$lang->story->trackOrderByList['category'] = 'Order by category'; + +$lang->story->trackSortList['asc'] = ' Ascending'; +$lang->story->trackSortList['desc'] = ' Descending'; diff --git a/module/story/lang/zh-cn.php b/module/story/lang/zh-cn.php index 2c88efac62..745c1b5cdb 100644 --- a/module/story/lang/zh-cn.php +++ b/module/story/lang/zh-cn.php @@ -33,14 +33,14 @@ $lang->story->batchReview = '批量评审'; $lang->story->edit = "编辑"; $lang->story->editDraft = "编辑草稿"; $lang->story->batchEdit = "批量编辑"; -$lang->story->subdivide = '细分'; -$lang->story->subdivideSR = $lang->SRCommon . '细分'; +$lang->story->subdivide = '拆分'; +$lang->story->subdivideSR = $lang->SRCommon . '拆分'; $lang->story->link = '关联'; $lang->story->unlink = '移除'; $lang->story->track = '跟踪矩阵'; $lang->story->trackAB = '矩阵'; -$lang->story->processStoryChange = '确认需求变动'; -$lang->story->splitRequirent = '拆分'; +$lang->story->processStoryChange = '确认父需求变更'; +$lang->story->split = '拆分'; $lang->story->close = '关闭'; $lang->story->batchClose = '批量关闭'; $lang->story->activate = '激活'; @@ -56,8 +56,8 @@ $lang->story->caseCount = '用例数'; $lang->story->taskCountAB = 'T'; $lang->story->bugCountAB = 'B'; $lang->story->caseCountAB = 'C'; -$lang->story->linkStory = "关联{$lang->SRCommon}"; -$lang->story->unlinkStory = "移除相关{$lang->SRCommon}"; +$lang->story->linkStory = "关联需求"; +$lang->story->unlinkStory = "您确认移除相关需求吗?"; $lang->story->linkStoriesAB = "关联相关{$lang->SRCommon}"; $lang->story->linkRequirementsAB = "关联相关{$lang->URCommon}"; $lang->story->export = "导出数据"; @@ -70,6 +70,9 @@ $lang->story->batchChangeBranch = "批量修改分支"; $lang->story->batchChangeStage = "批量修改阶段"; $lang->story->batchAssignTo = "批量指派"; $lang->story->batchChangeModule = "批量修改模块"; +$lang->story->batchChangeParent = "批量修改父需求"; +$lang->story->batchChangeGrade = "批量修改层级"; +$lang->story->changeParent = "修改父需求"; $lang->story->viewAll = '查看全部'; $lang->story->toTask = '转任务'; $lang->story->batchToTask = '批量转任务'; @@ -84,25 +87,26 @@ $lang->story->relieved = '解除'; $lang->story->relievedTwins = '解除孪生需求'; $lang->story->loadAllStories = '所有'; $lang->story->hasDividedTask = '已经分解任务'; +$lang->story->viewAllGrades = '查看所有层级'; $lang->story->codeBranch = '代码分支'; $lang->story->unlinkBranch = '解除关联代码分支'; $lang->story->branchName = '分支名称'; $lang->story->branchFrom = '创建自'; $lang->story->codeRepo = '仓库名称'; +$lang->story->viewByType = "按照%s查看"; $lang->story->editAction = "编辑{$lang->SRCommon}"; $lang->story->changeAction = "变更{$lang->SRCommon}"; $lang->story->assignAction = "指派{$lang->SRCommon}"; $lang->story->reviewAction = "评审{$lang->SRCommon}"; -$lang->story->subdivideAction = "细分{$lang->SRCommon}"; +$lang->story->subdivideAction = "拆分{$lang->SRCommon}"; $lang->story->closeAction = "关闭{$lang->SRCommon}"; $lang->story->activateAction = "激活{$lang->SRCommon}"; $lang->story->deleteAction = "删除{$lang->SRCommon}"; $lang->story->exportAction = "导出{$lang->SRCommon}"; $lang->story->reportAction = "统计报表"; -$lang->story->skipStory = "{$lang->SRCommon} %s 为父需求,本次操作已被过滤。"; -$lang->story->closedStory = "{$lang->SRCommon} %s 已关闭,本次操作已被过滤。"; +$lang->story->closedStory = '需求:%s 已关闭,将不会被关闭。'; $lang->story->batchToTaskTips = "只有激活状态的{$lang->SRCommon}才能转为任务。"; $lang->story->successToTask = '批量转任务成功'; $lang->story->storyRound = '第 %s 轮估算'; @@ -120,8 +124,13 @@ $lang->story->batchEditTip = "{$lang->SRCommon} %s为孪生需求,本 $lang->story->id = '编号'; $lang->story->parent = '父需求'; +$lang->story->isParent = '是父需求'; +$lang->story->grade = '需求层级'; +$lang->story->gradeName = '层级名称'; +$lang->story->path = '路径'; $lang->story->product = "所属{$lang->productCommon}"; $lang->story->project = "所属{$lang->projectCommon}"; +$lang->story->execution = "所属执行"; $lang->story->branch = "平台/分支"; $lang->story->module = '所属模块'; $lang->story->moduleAB = '模块'; @@ -130,6 +139,7 @@ $lang->story->source = "来源"; $lang->story->sourceNote = '来源备注'; $lang->story->fromBug = '来源Bug'; $lang->story->title = "{$lang->SRCommon}名称"; +$lang->story->name = "需求名称"; $lang->story->type = "需求类型"; $lang->story->category = "类别"; $lang->story->color = '标题颜色'; @@ -176,11 +186,12 @@ $lang->story->plan = "所属计划"; $lang->story->planAB = '计划'; $lang->story->comment = '备注'; $lang->story->children = "子需求"; +$lang->story->childItem = "子项"; $lang->story->childrenAB = "子"; -$lang->story->linkStories = "相关{$lang->SRCommon}"; +$lang->story->linkStories = "相关需求"; $lang->story->linkRequirements = "相关{$lang->URCommon}"; -$lang->story->childStories = "细分{$lang->SRCommon}"; -$lang->story->duplicateStory = "重复{$lang->SRCommon}"; +$lang->story->childStories = "拆分{$lang->SRCommon}"; +$lang->story->duplicateStory = "重复需求"; $lang->story->reviewResult = '评审意见'; $lang->story->reviewResultAB = '评审结果'; $lang->story->preVersion = '之前版本'; @@ -188,7 +199,7 @@ $lang->story->keywords = '关键词'; $lang->story->newStory = "继续添加{$lang->SRCommon}"; $lang->story->colorTag = '颜色标签'; $lang->story->files = '附件'; -$lang->story->copy = "复制{$lang->SRCommon}"; +$lang->story->copy = "复制需求"; $lang->story->total = "总{$lang->SRCommon}"; $lang->story->draft = '草稿'; $lang->story->unclosed = '未关闭'; @@ -212,6 +223,13 @@ $lang->story->estimateUnit = "(单位:{$lang->story->hour})"; $lang->story->ditto = '同上'; $lang->story->dittoNotice = "该{$lang->SRCommon}与上一{$lang->SRCommon}不属于同一{$lang->productCommon}!"; +$lang->story->viewTypeList['tiled'] = '平铺'; +$lang->story->viewTypeList['tree'] = '树状'; + +if($config->enableER) $lang->story->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->story->typeList['requirement'] = $lang->URCommon; +$lang->story->typeList['story'] = $lang->SRCommon; + $lang->story->needNotReviewList[0] = '需要评审'; $lang->story->needNotReviewList[1] = '不需要评审'; @@ -232,20 +250,27 @@ if($config->systemMode == 'PLM') } $lang->story->stageList[''] = ''; +$lang->story->stageList['defining'] = '定义中'; $lang->story->stageList['wait'] = '未开始'; +$lang->story->stageList['planning'] = '规划中'; $lang->story->stageList['planned'] = "已计划"; $lang->story->stageList['projected'] = '已立项'; +$lang->story->stageList['designing'] = '设计中'; +$lang->story->stageList['designed'] = '设计完毕'; $lang->story->stageList['developing'] = '研发中'; $lang->story->stageList['developed'] = '研发完毕'; $lang->story->stageList['testing'] = '测试中'; $lang->story->stageList['tested'] = '测试完毕'; $lang->story->stageList['verified'] = '已验收'; +$lang->story->stageList['rejected'] = '验收失败'; +$lang->story->stageList['delivering'] = '交付中'; +$lang->story->stageList['pending'] = '待发布'; $lang->story->stageList['released'] = '已发布'; $lang->story->stageList['closed'] = '已关闭'; $lang->story->reasonList[''] = ''; $lang->story->reasonList['done'] = '已完成'; -$lang->story->reasonList['subdivided'] = '已细分'; +$lang->story->reasonList['subdivided'] = '已拆分'; $lang->story->reasonList['duplicate'] = '重复'; $lang->story->reasonList['postponed'] = '延期'; $lang->story->reasonList['willnotdo'] = '不做'; @@ -300,7 +325,7 @@ $lang->story->legendCases = '相关用例'; $lang->story->legendBuilds = '相关版本'; $lang->story->legendReleases = '相关发布'; $lang->story->legendLinkStories = "相关{$lang->SRCommon}"; -$lang->story->legendChildStories = "细分{$lang->SRCommon}"; +$lang->story->legendChildStories = "拆分{$lang->SRCommon}"; $lang->story->legendSpec = "需求描述"; $lang->story->legendVerify = '验收标准'; $lang->story->legendMisc = '其他相关'; @@ -318,31 +343,45 @@ $lang->story->affectedBugs = '影响的Bug'; $lang->story->affectedCases = '影响的用例'; $lang->story->affectedTwins = '影响的孪生需求'; -$lang->story->specTemplate = "建议参考的模板:作为一名<某种类型的用户>,我希望<达成某些目的>,这样可以<开发的价值>。"; -$lang->story->needNotReview = '不需要评审'; -$lang->story->successSaved = "{$lang->SRCommon}成功添加,"; -$lang->story->confirmDelete = "您确认删除该{$lang->SRCommon}吗?"; -$lang->story->confirmRecall = "您确认撤销该{$lang->SRCommon}吗?"; -$lang->story->errorEmptyChildStory = "『细分{$lang->SRCommon}』不能为空。"; -$lang->story->errorNotSubdivide = "状态不是激活,或者阶段不是未开始的{$lang->SRCommon},或者是子需求,则不能细分。"; -$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』不能为空。"; -$lang->story->mustChooseResult = '必须选择评审意见'; -$lang->story->mustChoosePreVersion = '必须选择回溯的版本'; -$lang->story->noStory = "暂时没有{$lang->SRCommon}。"; -$lang->story->noRequirement = "暂时没有{$lang->URCommon}。"; -$lang->story->ignoreChangeStage = "{$lang->SRCommon} %s 状态为草稿或已关闭,本次操作已被过滤。"; -$lang->story->cannotDeleteParent = "不能删除父{$lang->SRCommon}"; -$lang->story->moveChildrenTips = "修改父{$lang->SRCommon}的所属{$lang->productCommon}会将其下的子{$lang->SRCommon}也移动到所选{$lang->productCommon}下。"; -$lang->story->changeTips = '该软件需求关联的用户需求有变更,点击“不变更”忽略此条变更,点击“变更”来进行该软件需求的变更。'; -$lang->story->estimateMustBeNumber = '估算值必须是数字'; -$lang->story->estimateMustBePlus = '估算值不能是负数'; -$lang->story->confirmChangeBranch = $lang->SRCommon . '%s已关联在之前所属分支的计划中,调整分支后,' . $lang->SRCommon . '将从之前所属分支的计划中移除,请确认是否继续修改上述' . $lang->SRCommon . '的分支。'; -$lang->story->confirmChangePlan = $lang->SRCommon . '%s已关联在之前计划的所属分支中,调整分支后,' . $lang->SRCommon . '将会从计划中移除,请确认是否继续修改计划的所属分支。'; -$lang->story->errorDuplicateStory = $lang->SRCommon . '%s不存在'; -$lang->story->confirmRecallChange = "撤销变更后,需求内容会回退至变更前的版本,您确定要撤销吗?"; -$lang->story->confirmRecallReview = "您确定要撤回评审吗?"; -$lang->story->noStoryToTask = "只有激活的{$lang->SRCommon}才能转为任务!"; -$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s 状态为已关闭,本次操作已被过滤。"; +$lang->story->specTemplate = "建议参考的模板:作为一名<某种类型的用户>,我希望<达成某些目的>,这样可以<开发的价值>。"; +$lang->story->needNotReview = '不需要评审'; +$lang->story->childStoryTitle = '包含%s个子需求,其中%s个已完成'; +$lang->story->childTaskTitle = '包含%s个子任务,其中%s个已完成'; +$lang->story->successSaved = "{$lang->SRCommon}成功添加,"; +$lang->story->confirmDelete = "您确认删除该{$lang->SRCommon}吗?"; +$lang->story->confirmRecall = "您确认撤销该{$lang->SRCommon}吗?"; +$lang->story->errorEmptyChildStory = "『拆分{$lang->SRCommon}』不能为空。"; +$lang->story->errorNotSubdivide = "状态不是激活,或者阶段不是未开始的{$lang->SRCommon},或者是子需求,则不能拆分。"; +$lang->story->errorMaxGradeSubdivide = "该需求的层级已经达到系统设置的最大层级,不能拆分同类型需求。"; +$lang->story->errorStepwiseSubdivide = "该需求类型不允许跨层级拆分,后台可以更改此设置。"; +$lang->story->errorCannotSplit = "该需求已拆分此类型的子需求,不能拆分为其他类型需求。"; +$lang->story->errorParentSplitTask = "父需求不能转任务,本次操作已过滤。"; +$lang->story->errorERURSplitTask = "父需求、{$lang->ERCommon}、{$lang->URCommon}不能转任务,本次操作已过滤。"; +$lang->story->errorEmptyReviewedBy = "『{$lang->story->reviewers}』不能为空。"; +$lang->story->mustChooseResult = '必须选择评审意见'; +$lang->story->mustChoosePreVersion = '必须选择回溯的版本'; +$lang->story->noEpic = "暂时没有{$lang->ERCommon}。"; +$lang->story->noStory = "暂时没有{$lang->SRCommon}。"; +$lang->story->noRequirement = "暂时没有{$lang->URCommon}。"; +$lang->story->ignoreChangeStage = "{$lang->SRCommon} %s 状态为草稿或已关闭,本次操作已被过滤。"; +$lang->story->cannotDeleteParent = "不能删除父{$lang->SRCommon}"; +$lang->story->moveChildrenTips = "确认修改所属产品吗,修改后,需求的所有子级需求也会跟随变更。"; +$lang->story->changeTips = '该软件需求关联的用户需求有变更,点击“不变更”忽略此条变更,点击“变更”来进行该软件需求的变更。'; +$lang->story->estimateMustBeNumber = '估算值必须是数字'; +$lang->story->estimateMustBePlus = '估算值不能是负数'; +$lang->story->confirmChangeBranch = $lang->SRCommon . '%s已关联在之前所属分支的计划中,调整分支后,' . $lang->SRCommon . '将从之前所属分支的计划中移除,请确认是否继续修改上述' . $lang->SRCommon . '的分支。'; +$lang->story->confirmChangePlan = $lang->SRCommon . '%s已关联在之前计划的所属分支中,调整分支后,' . $lang->SRCommon . '将会从计划中移除,请确认是否继续修改计划的所属分支。'; +$lang->story->errorDuplicateStory = $lang->SRCommon . '%s不存在'; +$lang->story->confirmRecallChange = "撤销变更后,需求内容会回退至变更前的版本,您确定要撤销吗?"; +$lang->story->confirmRecallReview = "您确定要撤回评审吗?"; +$lang->story->noStoryToTask = "只有激活的{$lang->SRCommon}才能转为任务!"; +$lang->story->ignoreClosedStory = "{$lang->SRCommon} %s 状态为已关闭,本次操作已被过滤。"; +$lang->story->changeProductTips = "确认修改所属产品吗,修改后,需求的所有子级需求也会跟随变更。"; +$lang->story->gradeOverflow = "系统检测该需求下子需求的最大层级为%s,同步修改后为%s,超出系统设置的层级范围,无法修改。"; +$lang->story->batchGradeOverflow = "%s需求的父需求修改后,其子需求的层级会超出系统设置的层级范围,本次修改已将其忽略。"; +$lang->story->batchGradeSameRoot = '%s需求存在父子关系,将不会被修改需求层级。'; +$lang->story->batchGradeGtParent = '%s需求的层级不能高于其父需求,本次修改已将其忽略。'; +$lang->story->batchParentError = "%s需求的父需求不能为其本身或其子需求,本次修改已将其忽略。"; $lang->story->form = new stdclass(); $lang->story->form->area = "该{$lang->SRCommon}所属范围"; @@ -356,6 +395,7 @@ $lang->story->action->rejectreviewed = array('main' => '$date, 由 story->action->recalled = array('main' => '$date, 由 $actor 撤销评审。'); $lang->story->action->closed = array('main' => '$date, 由 $actor 关闭,原因为 $extra $appendLink。', 'extra' => 'reasonList'); $lang->story->action->closedbysystem = array('main' => '$date, 系统判定由于关闭了所有子需求,自动关闭父需求。'); +$lang->story->action->closedbyparent = array('main' => '$date, 系统判定由于关闭了父需求,自动关闭子需求。'); $lang->story->action->reviewpassed = array('main' => '$date, 由 系统 判定,结果为 确认通过。'); $lang->story->action->reviewrejected = array('main' => '$date, 由 系统 关闭,原因为 拒绝。'); $lang->story->action->reviewclarified = array('main' => '$date, 由 系统 判定,结果为 有待明确,请编辑后重新发起评审。'); @@ -373,12 +413,13 @@ $lang->story->action->linked2release = array('main' => '$date, 由 story->action->unlinkedfromrelease = array('main' => '$date, 由 $actor 从发布 $extra 移除。'); $lang->story->action->linked2revision = array('main' => '$date, 由 $actor 关联到代码提交 $extra'); $lang->story->action->unlinkedfromrevision = array('main' => '$date, 由 $actor 取消关联到代码提交 $extra'); -$lang->story->action->linkrelatedstory = array('main' => "\$date, 由 \$actor 关联相关{$lang->SRCommon} \$extra。"); -$lang->story->action->subdividestory = array('main' => "\$date, 由 \$actor 细分为{$lang->SRCommon} \$extra。"); -$lang->story->action->unlinkrelatedstory = array('main' => "\$date, 由 \$actor 移除相关{$lang->SRCommon} \$extra。"); -$lang->story->action->unlinkchildstory = array('main' => "\$date, 由 \$actor 移除细分{$lang->SRCommon} \$extra。"); +$lang->story->action->linkrelatedstory = array('main' => "\$date, 由 \$actor 关联相关需求 \$extra。"); +$lang->story->action->subdividestory = array('main' => "\$date, 由 \$actor 拆分为{$lang->SRCommon} \$extra。"); +$lang->story->action->unlinkrelatedstory = array('main' => "\$date, 由 \$actor 移除相关需求 \$extra。"); +$lang->story->action->unlinkchildstory = array('main' => "\$date, 由 \$actor 移除拆分{$lang->SRCommon} \$extra。"); $lang->story->action->recalledchange = array('main' => "\$date, 由 \$actor 撤销变更。"); $lang->story->action->synctwins = array('main' => "\$date, 系统判断由于孪生需求 \$extra \$operate,本需求同步调整。", 'operate' => 'operateList'); +$lang->story->action->syncgrade = array('main' => "\$date, 系统判断由于父需求层级变动,本需求层级同步修改为 \$extra。"); $lang->story->action->linked2roadmap = array('main' => '$date, 由 $actor 关联到路标 $extra。'); $lang->story->action->unlinkedfromroadmap = array('main' => '$date, 由 $actor 从路标 $extra 移除。'); $lang->story->action->changedbycharter = array('main' => '$date, 由 $actor 通过立项申请 $extra ,需求状态同步调整为已立项。'); @@ -390,18 +431,19 @@ $lang->story->report->select = '请选择报表类型'; $lang->story->report->create = '生成报表'; $lang->story->report->value = "需求数"; -$lang->story->report->charts['storiesPerProduct'] = $lang->productCommon . "{$lang->SRCommon}数量"; -$lang->story->report->charts['storiesPerModule'] = "模块{$lang->SRCommon}数量"; -$lang->story->report->charts['storiesPerSource'] = "按{$lang->SRCommon}来源统计"; -$lang->story->report->charts['storiesPerPlan'] = "按计划进行统计"; -$lang->story->report->charts['storiesPerStatus'] = '按状态进行统计'; -$lang->story->report->charts['storiesPerStage'] = '按所处阶段进行统计'; -$lang->story->report->charts['storiesPerPri'] = '按优先级进行统计'; -$lang->story->report->charts['storiesPerEstimate'] = "按预计{$lang->hourCommon}进行统计"; -$lang->story->report->charts['storiesPerOpenedBy'] = '按由谁创建来进行统计'; -$lang->story->report->charts['storiesPerAssignedTo'] = '按当前指派来进行统计'; -$lang->story->report->charts['storiesPerClosedReason'] = '按关闭原因来进行统计'; -$lang->story->report->charts['storiesPerChange'] = '按变更次数来进行统计'; +$lang->story->report->charts['storiesPerProduct'] = $lang->productCommon . "{$lang->SRCommon}数量"; +$lang->story->report->charts['storiesPerModule'] = "模块{$lang->SRCommon}数量"; +$lang->story->report->charts['storiesPerSource'] = "按{$lang->SRCommon}来源统计"; +$lang->story->report->charts['storiesPerPlan'] = "按计划进行统计"; +$lang->story->report->charts['storiesPerStatus'] = '按状态进行统计'; +$lang->story->report->charts['storiesPerStage'] = '按所处阶段进行统计'; +$lang->story->report->charts['storiesPerPri'] = '按优先级进行统计'; +$lang->story->report->charts['storiesPerEstimate'] = "按预计{$lang->hourCommon}进行统计"; +$lang->story->report->charts['storiesPerOpenedBy'] = '按由谁创建来进行统计'; +$lang->story->report->charts['storiesPerAssignedTo'] = '按当前指派来进行统计'; +$lang->story->report->charts['storiesPerClosedReason'] = '按关闭原因来进行统计'; +$lang->story->report->charts['storiesPerChange'] = '按变更次数来进行统计'; +$lang->story->report->charts['storiesPerGrade'] = '按层级来进行统计'; $lang->story->report->options = new stdclass(); $lang->story->report->options->graph = new stdclass(); @@ -421,6 +463,7 @@ $lang->story->report->storiesPerAssignedTo = new stdclass(); $lang->story->report->storiesPerClosedReason = new stdclass(); $lang->story->report->storiesPerEstimate = new stdclass(); $lang->story->report->storiesPerChange = new stdclass(); +$lang->story->report->storiesPerGrade = new stdclass(); $lang->story->report->storiesPerProduct->item = $lang->productCommon; $lang->story->report->storiesPerModule->item = '模块'; @@ -434,6 +477,7 @@ $lang->story->report->storiesPerAssignedTo->item = '指派给'; $lang->story->report->storiesPerClosedReason->item = '原因'; $lang->story->report->storiesPerEstimate->item = "预计{$lang->hourCommon}"; $lang->story->report->storiesPerChange->item = '变更次数'; +$lang->story->report->storiesPerGrade->item = '层级'; $lang->story->report->storiesPerProduct->graph = new stdclass(); $lang->story->report->storiesPerModule->graph = new stdclass(); @@ -447,6 +491,7 @@ $lang->story->report->storiesPerAssignedTo->graph = new stdclass(); $lang->story->report->storiesPerClosedReason->graph = new stdclass(); $lang->story->report->storiesPerEstimate->graph = new stdclass(); $lang->story->report->storiesPerChange->graph = new stdclass(); +$lang->story->report->storiesPerGrade->graph = new stdclass(); $lang->story->report->storiesPerProduct->graph->xAxisName = $lang->productCommon; $lang->story->report->storiesPerModule->graph->xAxisName = '模块'; @@ -460,6 +505,7 @@ $lang->story->report->storiesPerAssignedTo->graph->xAxisName = '当前指派'; $lang->story->report->storiesPerClosedReason->graph->xAxisName = '关闭原因'; $lang->story->report->storiesPerEstimate->graph->xAxisName = '预计时间'; $lang->story->report->storiesPerChange->graph->xAxisName = '变更次数'; +$lang->story->report->storiesPerGrade->graph->xAxisName = '层级'; $lang->story->placeholder = new stdclass(); $lang->story->placeholder->estimate = $lang->story->hour; @@ -488,21 +534,20 @@ $lang->story->categoryList['experience'] = '体验'; $lang->story->categoryList['improve'] = '改进'; $lang->story->categoryList['other'] = '其他'; -$lang->story->changeTip = "只有激活状态的{$lang->SRCommon},才能进行变更"; +$lang->story->changeTip = "只有激活状态的需求,才能进行变更"; $lang->story->reviewTip = array(); -$lang->story->reviewTip['active'] = "该{$lang->SRCommon}已是激活状态,无需评审"; -$lang->story->reviewTip['notReviewer'] = "您不是该{$lang->SRCommon}的评审人员,无法进行评审操作"; +$lang->story->reviewTip['active'] = "该需求已是激活状态,无需评审"; +$lang->story->reviewTip['notReviewer'] = "您不是该需求的评审人员,无法进行评审操作"; $lang->story->reviewTip['reviewed'] = '您已评审'; $lang->story->recallTip = array(); -$lang->story->recallTip['actived'] = "该{$lang->SRCommon}未发起评审流程,无需撤销操作"; +$lang->story->recallTip['actived'] = "该需求未发起评审流程,无需撤销操作"; $lang->story->subDivideTip = array(); -$lang->story->subDivideTip['subStory'] = "子{$lang->SRCommon}无法细分"; -$lang->story->subDivideTip['notWait'] = "该{$lang->SRCommon}%s,无法进行细分操作"; -$lang->story->subDivideTip['notActive'] = "%s不是激活状态,无法进行细分操作"; -$lang->story->subDivideTip['twinsSplit'] = '孪生需求不可细分'; +$lang->story->subDivideTip['notWait'] = "该需求%s,无法进行拆分操作"; +$lang->story->subDivideTip['notActive'] = "需求不是激活状态,无法进行拆分操作"; +$lang->story->subDivideTip['twinsSplit'] = '孪生需求不可拆分'; $lang->story->featureBar['browse']['all'] = '全部'; $lang->story->featureBar['browse']['unclosed'] = $lang->story->unclosed; @@ -520,31 +565,6 @@ $lang->story->operateList['submitreview'] = '提交评审'; $lang->story->operateList['recalledchange'] = '撤销变更'; $lang->story->operateList['recalled'] = '撤销评审'; -$lang->requirement->common = $lang->URCommon; -$lang->requirement->create = "提{$lang->URCommon}"; -$lang->requirement->batchCreate = "批量创建"; -$lang->requirement->editAction = "编辑{$lang->URCommon}"; -$lang->requirement->changeAction = "变更{$lang->URCommon}"; -$lang->requirement->assignAction = "指派{$lang->URCommon}"; -$lang->requirement->reviewAction = "评审{$lang->URCommon}"; -$lang->requirement->subdivideAction = "细分{$lang->URCommon}"; -$lang->requirement->closeAction = "关闭{$lang->URCommon}"; -$lang->requirement->activateAction = "激活{$lang->URCommon}"; -$lang->requirement->deleteAction = "删除{$lang->URCommon}"; -$lang->requirement->exportAction = "导出{$lang->URCommon}"; -$lang->requirement->reportAction = "统计报表"; -$lang->requirement->recall = $lang->story->recall; -$lang->requirement->batchReview = '批量评审'; -$lang->requirement->batchEdit = "批量编辑"; -$lang->requirement->batchClose = '批量关闭'; -$lang->requirement->view = "{$lang->URCommon}详情"; -$lang->requirement->linkRequirementsAB = "关联相关{$lang->URCommon}"; -$lang->requirement->batchChangeBranch = "批量修改分支"; -$lang->requirement->batchAssignTo = "批量指派"; -$lang->requirement->batchChangeModule = "批量修改模块"; -$lang->requirement->submitReview = $lang->story->submitReview; -$lang->requirement->linkStory = "关联{$lang->SRCommon}"; - $lang->story->addBranch = '添加%s'; $lang->story->deleteBranch = '删除%s'; $lang->story->notice->branch = "每个分支会建立一个需求,需求间互为孪生关系。孪生需求间除{$lang->productCommon}、分支、模块、计划、阶段字段外均保持同步,后期您可以手动解除孪生关系。"; @@ -552,3 +572,12 @@ $lang->story->notice->branch = "每个分支会建立一个需求,需求间互 $lang->story->relievedTwinsRelation = '解除孪生关系'; $lang->story->relievedTwinsRelationTips = '孪生关系解除后无法恢复,需求的关闭将不再同步。'; $lang->story->changeRelievedTwinsTips = '孪生关系解除后无法恢复,孪生需求间内容不再同步。'; + +$lang->story->trackOrderByList['id'] = '按ID'; +$lang->story->trackOrderByList['pri'] = '按优先级'; +$lang->story->trackOrderByList['status'] = '按状态'; +$lang->story->trackOrderByList['stage'] = '按阶段'; +$lang->story->trackOrderByList['category'] = '按类别'; + +$lang->story->trackSortList['asc'] = '升序'; +$lang->story->trackSortList['desc'] = '降序'; diff --git a/module/story/model.php b/module/story/model.php index 84a2d3a444..81249fd17d 100644 --- a/module/story/model.php +++ b/module/story/model.php @@ -44,10 +44,7 @@ class storyModel extends model if($setImgSize) $story->spec = $this->file->setImgSize($story->spec); if($setImgSize) $story->verify = $this->file->setImgSize($story->verify); - /* Get relation story ID.*/ - $story->relationStoryID = $this->getRelationStoryID($story->id, $story->type); - - $storyIdList = $storyID . ($story->relationStoryID ? "," . trim($story->relationStoryID, ',') : '') . ($story->twins ? "," . trim($story->twins, ',') : ''); + $storyIdList = $storyID . ($story->twins ? "," . trim($story->twins, ',') : ''); $story->executions = $this->dao->select('t2.project, t2.id, t2.name, t2.status, t2.type, t2.multiple')->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_EXECUTION)->alias('t2')->on('t1.project = t2.id') ->where('t2.type')->in('sprint,stage,kanban') @@ -58,10 +55,17 @@ class storyModel extends model $story->tasks = $this->dao->select('id,name,assignedTo,execution,project,status,consumed,`left`,type')->from(TABLE_TASK)->where('deleted')->eq(0)->andWhere('story')->in($storyIdList)->orderBy('id DESC')->fetchGroup('execution'); if($this->config->vision == 'lite' && $story->tasks) $story->executions += $this->dao->select('project,id,name,status,type,multiple')->from(TABLE_EXECUTION)->where('id')->in(array_keys($story->tasks))->orderBy('`order` DESC')->fetchAll('id'); - if($story->toBug) $story->toBugTitle = $this->dao->findById($story->toBug)->from(TABLE_BUG)->fetch('title'); - if($story->parent > 0) $story->parentName = $this->dao->findById($story->parent)->from(TABLE_STORY)->fetch('title'); - if($story->fromStory) $story->sourceName = $this->dao->select('title')->from(TABLE_STORY)->where('id')->eq($story->fromStory)->fetch('title'); - if($story->parent == '-1') $story->children = $this->dao->select('*')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq(0)->fetchAll('id'); + if($story->parent > 0) + { + $parent = $this->dao->findById($story->parent)->from(TABLE_STORY)->fetch(); + $story->parentName = $parent->title; + $story->parentType = $parent->type; + $story->parentChanged = $story->parentVersion > 0 && $parent->version > $story->parentVersion && $parent->status == 'active'; + } + + if($story->toBug) $story->toBugTitle = $this->dao->findById($story->toBug)->from(TABLE_BUG)->fetch('title'); + if($story->fromStory) $story->sourceName = $this->dao->select('title')->from(TABLE_STORY)->where('id')->eq($story->fromStory)->fetch('title'); + if($story->isParent == '1') $story->children = $this->dao->select('*')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq(0)->orderBy('id_desc')->fetchAll('id'); if($story->plan) { $plans = $this->dao->select('id,title,branch')->from(TABLE_PRODUCTPLAN)->where('id')->in($story->plan)->fetchAll('id'); @@ -72,29 +76,11 @@ class storyModel extends model } } - $extraStories = array(); - if($story->duplicateStory) $extraStories = array($story->duplicateStory); - if($story->linkStories) $extraStories = array_merge($extraStories, explode(',', $story->linkStories)); - if($story->childStories) $extraStories = array_merge($extraStories, explode(',', $story->childStories)); - - $extraStories = array_unique($extraStories); + $extraStories = $story->duplicateStory ? array($story->duplicateStory) : array(); if(!empty($extraStories)) $story->extraStories = $this->dao->select('id,title')->from(TABLE_STORY)->where('id')->in($extraStories)->fetchPairs(); - $linkStoryField = $story->type == 'story' ? 'linkStories' : 'linkRequirements'; - if($story->{$linkStoryField}) - { - $multiBranches = $this->loadModel('branch')->getAllPairs('noproductname'); - $multiProducts = $this->loadModel('product')->getMultiBranchPairs(); - $linkStoryList = $this->dao->select('id,title,product,branch,status')->from(TABLE_STORY)->where('id')->in($story->{$linkStoryField})->fetchAll('id'); - $linkStoryTitles = array(); - foreach($linkStoryList as $linkStory) - { - $linkStoryTitles[$linkStory->id] = $linkStory->title; - if(isset($multiProducts[$linkStory->product]) && isset($multiBranches[$linkStory->branch])) $linkStory->branchName = $multiBranches[$linkStory->branch]; - } - $story->linkStoryList = $linkStoryList; - $story->linkStoryTitles = $linkStoryTitles; - } + $story->hasOtherTypeChild = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($story->id)->andWhere('type')->ne($story->type)->andWhere('deleted')->eq('0')->fetch('id'); + $story->hasSameTypeChild = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($story->id)->andWhere('type')->eq($story->type)->andWhere('deleted')->eq('0')->fetch('id'); $story->openedDate = helper::isZeroDate($story->openedDate) ? '' : substr($story->openedDate, 0, 19); $story->assignedDate = helper::isZeroDate($story->assignedDate) ? '' : substr($story->assignedDate, 0, 19); @@ -206,32 +192,21 @@ class storyModel extends model { $users = $this->loadModel('user')->getPairs('pofirst|nodeleted', "{$story->lastEditedBy},{$story->openedBy},{$story->assignedTo}"); - $story = $this->storyTao->getAffectedProjects($story, $users); - $story = $this->storyTao->getAffectedBugs($story, $users); - $story = $this->storyTao->getAffectedCases($story, $users); - $story = $this->storyTao->getAffectedTwins($story, $users); + if($story->type == 'story') + { + $story = $this->storyTao->getAffectedProjects($story, $users); + $story = $this->storyTao->getAffectedBugs($story, $users); + $story = $this->storyTao->getAffectedCases($story, $users); + $story = $this->storyTao->getAffectedTwins($story, $users); + } + else + { + $story = $this->storyTao->getAffectedChildren($story, $users); + } return $story; } - /** - * 获取用户需求细分的软件需求。 - * Get relation story ID. - * - * @param int $storyID - * @param string $storyType - * @access public - * @return string - */ - public function getRelationStoryID(int $storyID, string $storyType = 'requirement'): string - { - $relationStoryIdList = array(); - $relationStoryList = $this->getStoryRelation($storyID, $storyType); - foreach($relationStoryList as $relationStory) $relationStoryIdList[$relationStory->id] = $relationStory->id; - - return implode(',', $relationStoryIdList); - } - /** * Get requirements for story. * @@ -255,7 +230,7 @@ class storyModel extends model * @param int $executionID * @param int $productID * @param string $orderBy - * @param string $type + * @param string $browseType * @param int|string $param * @param string $storyType * @param array|string $excludeStories @@ -263,44 +238,66 @@ class storyModel extends model * @access public * @return array */ - public function getExecutionStories(int $executionID = 0, int $productID = 0, string $orderBy = 't1.`order`_desc', string $type = 'byModule', string $param = '0', string $storyType = 'story', array|string $excludeStories = '', object|null $pager = null): array + public function getExecutionStories(int $executionID = 0, int $productID = 0, string $orderBy = 't1.`order`_desc', string $browseType = 'byModule', string $param = '0', string $storyType = 'story', array|string $excludeStories = '', object|null $pager = null): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStories(); if(empty($executionID)) return array(); + $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch(); + + $sqlCondition = ''; + $module = $execution->type == 'project' ? 'project' : 'execution'; + $showGrades = isset($this->config->{$module}->showGrades) ? $this->config->{$module}->showGrades : null; + if($showGrades) + { + $items = explode(',', $showGrades); + $conditions = array(); + foreach($items as $item) + { + preg_match('/^([a-zA-Z]+)(\d+)$/', $item, $matches); + $type = $matches[1]; + $grade = $matches[2]; + $conditions[$type][] = $grade; + } + + $sqlCondition = '('; + foreach($conditions as $type => $grades) $sqlCondition .= "(t2.type = '$type' AND t2.grade " . helper::dbIN($grades) . ") OR "; + $sqlCondition = rtrim($sqlCondition, 'OR '); + $sqlCondition .= ')'; + } /* 格式化参数。 */ - $orderBy = str_replace('branch_', 't2.branch_', $orderBy); - $type = strtolower($type); + $orderBy = str_replace('branch_', 't2.branch_', $orderBy); + $browseType = strtolower($browseType); if(is_string($excludeStories)) $excludeStories = explode(',', $excludeStories); /* 获取需求。 */ - if($type == 'bysearch') $stories = $this->storyTao->getExecutionStoriesBySearch($executionID, (int)$param, $productID, $orderBy, $storyType, $excludeStories, $pager); - if($type != 'bysearch') + if($browseType == 'bysearch') $stories = $this->storyTao->getExecutionStoriesBySearch($executionID, (int)$param, $productID, $orderBy, $storyType, $sqlCondition, $excludeStories, $pager); + if($browseType != 'bysearch') { /* 根据请求类型和参数,获取查询要用到的条件。 */ - $execution = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($executionID)->fetch(); - $modules = $this->storyTao->getModules4ExecutionStories($type, $param); - $storyIdList = $this->storyTao->getIdListOfExecutionsByProjectID($type, $executionID); - $productParam = ($type == 'byproduct' and $param) ? $param : $productID; - $branchParam = ($type == 'bybranch' and $param !== '') ? $param : (string)$this->cookie->storyBranchParam; + $modules = $this->storyTao->getModules4ExecutionStories($browseType, $param); + $storyIdList = $this->storyTao->getIdListOfExecutionsByProjectID($browseType, $executionID); + $productParam = ($browseType == 'byproduct' and $param) ? $param : $productID; + $branchParam = ($browseType == 'bybranch' and $param !== '') ? $param : (string)$this->cookie->storyBranchParam; /* 设置查询需求的公共 DAO 变量。 */ - $type = (strpos('bymodule|byproduct', $type) !== false and $this->session->storyBrowseType) ? $this->session->storyBrowseType : $type; + $browseType = (strpos('bymodule|byproduct', $browseType) !== false and $this->session->storyBrowseType) ? $this->session->storyBrowseType : $browseType; $storyDAO = $this->dao->select("DISTINCT t1.*, t2.*, IF(t2.`pri` = 0, {$this->config->maxPriValue}, t2.`pri`) as priOrder, t3.type as productType, t2.version as version")->from(TABLE_PROJECTSTORY)->alias('t1') ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t2.product = t3.id') ->where('t1.project')->eq($executionID) - ->andWhere('t2.type')->eq($storyType) ->andWhere('t2.deleted')->eq(0) ->andWhere('t3.deleted')->eq(0) + ->beginIF($storyType != 'all')->andWhere('t2.type')->in($storyType)->fi() + ->beginIF($sqlCondition)->andWhere($sqlCondition)->fi() ->beginIF($excludeStories)->andWhere('t2.id')->notIN($excludeStories)->fi() ->beginIF($this->session->storyBrowseType and strpos('changing|', $this->session->storyBrowseType) !== false)->andWhere('t2.status')->in($this->storyTao->getUnclosedStatusKeys())->fi() ->beginIF($modules)->andWhere('t2.module')->in($modules)->fi(); /* 根据传入的 ID 是项目还是执行分别查询需求。 */ - if($execution->type == 'project') $stories = $this->storyTao->fetchProjectStories($storyDAO, $productID, $type, $branchParam, $storyIdList, $orderBy, $pager); - if($execution->type != 'project') $stories = $this->storyTao->fetchExecutionStories($storyDAO, (int)$productParam, $type, $branchParam, $orderBy, $pager); + if($execution->type == 'project') $stories = $this->storyTao->fetchProjectStories($storyDAO, $productID, $browseType, $branchParam, $storyIdList, $orderBy, $pager); + if($execution->type != 'project') $stories = $this->storyTao->fetchExecutionStories($storyDAO, (int)$productParam, $browseType, $branchParam, $orderBy, $pager); } $stories = $this->storyTao->fixBranchStoryStage($stories); @@ -375,11 +372,12 @@ class storyModel extends model * @param array|string|int $moduleIdList * @param string $type full|short * @param string $status all|noclosed|changing|active|draft|closed|reviewing - * @param string $storyType story|requirement + * @param string $storyType story|epic|requirement + * @param bool $hasParent * @access public * @return array */ - public function getExecutionStoryPairs(int $executionID = 0, int $productID = 0, string|int $branch = 'all', array|string|int $moduleIdList = '', string $type = 'full', string $status = 'all', string $storyType = 'story'): array + public function getExecutionStoryPairs(int $executionID = 0, int $productID = 0, string|int $branch = 'all', array|string|int $moduleIdList = '', string $type = 'full', string $status = 'all', string $storyType = '', bool $hasParent = true): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getExecutionStoryPairs(); @@ -388,8 +386,9 @@ class storyModel extends model ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->leftJoin(TABLE_PRODUCT)->alias('t3')->on('t1.product = t3.id') ->where('t1.project')->eq($executionID) + ->beginIF($storyType)->andWhere('t2.type')->in($storyType)->fi() + ->beginIF(!$hasParent)->andWhere('t2.isParent')->eq('0')->fi() ->andWhere('t2.deleted')->eq('0') - ->andWhere('t2.type')->eq($storyType) ->beginIF($productID)->andWhere('t2.product')->eq($productID)->fi() ->beginIF($branch !== 'all')->andWhere('t2.branch')->in("0,$branch")->fi() ->beginIF($moduleIdList)->andWhere('t2.module')->in($moduleIdList)->fi() @@ -442,6 +441,18 @@ class storyModel extends model $this->loadModel('common')->saveQueryCondition($this->dao->get(), 'story', false); + /* Add parent list for display. */ + foreach($stories as $story) + { + $story->parent = array(); + foreach(explode(',', trim($story->path, ',')) as $parentID) + { + if(!$parentID) continue; + if($parentID == $story->id) continue; + $story->parent[] = (int)$parentID; + } + } + return $stories; } @@ -517,12 +528,27 @@ class storyModel extends model if($executionID) $this->storyTao->linkToExecutionForCreate($executionID, $storyID, $story, $extra); if($bugID) $this->storyTao->closeBugWhenToStory($bugID, $storyID); if(!empty($story->reviewer)) $this->storyTao->doCreateReviewer($storyID, $story->reviewer); - if(!empty($story->URS)) $this->storyTao->doCreateURRelations($storyID, $story->URS); - if(!empty($story->parent)) $this->subdivide($story->parent, array($storyID)); + if(!empty($story->parent)) + { + $this->subdivide($story->parent, array($storyID)); + $this->updateParentStatus($storyID, $story->parent, false); + } + else + { + $this->dao->update(TABLE_STORY) + ->set('root')->eq($storyID) + ->set('path')->eq(",{$storyID},") + ->where('id')->eq($storyID) + ->exec(); + } if(!empty($story->plan)) { $this->updateStoryOrderOfPlan($storyID, (string)$story->plan); // Set story order in this plan. - $this->action->create('productplan', (int)$story->plan, 'linkstory', '', $storyID); + foreach(explode(',', $story->plan) as $planID) + { + if(!$planID) continue; + $this->action->create('productplan', (int)$planID, 'linkstory', '', $storyID); + } } $this->setStage($storyID); @@ -642,14 +668,10 @@ class storyModel extends model * Batch create stories. * * @param array $stories - * @param int $productID - * @param string $branch - * @param string $type - * @param int $URID * @access public * @return array */ - public function batchCreate(array $stories, int $productID = 0, string $branch = '', string $type = 'story', int $URID = 0): array + public function batchCreate(array $stories): array { $this->loadModel('action'); $storyIdList = array(); @@ -660,17 +682,28 @@ class storyModel extends model if(!$storyID) return array(); $this->storyTao->doCreateSpec($storyID, $story); + if(!empty($story->parent)) + { + $this->subdivide($story->parent, array($storyID)); + $this->updateParentStatus($storyID, $story->parent, false); + } + else + { + $this->dao->update(TABLE_STORY)->set('root')->eq($storyID)->set('path')->eq(",{$storyID},")->where('id')->eq($storyID)->exec(); + } /* Update product plan stories order. */ if(!empty($story->reviewer)) $this->storyTao->doCreateReviewer($storyID, $story->reviewer); if($story->plan) { $this->updateStoryOrderOfPlan($storyID, (string)$story->plan); - $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},$storyID"; + foreach(explode(',', (string)$story->plan) as $planID) + { + if(!$planID) continue; + $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID"; + } } - if(!empty($story->URS)) $this->storyTao->doCreateURRelations($storyID, $story->URS); - $this->setStage($storyID); $this->executeHooks($storyID); @@ -691,8 +724,6 @@ class storyModel extends model $this->loadModel('score')->create('story', 'create',$storyID); $this->score->create('ajax', 'batchCreate'); - - if($URID && $storyIdList) $this->subdivide($URID, $storyIdList); foreach($link2Plans as $planID => $stories) $this->action->create('productplan', $planID, 'linkstory', '', $stories); } @@ -712,7 +743,7 @@ class storyModel extends model $oldStory = $this->getById($storyID); $this->dao->update(TABLE_STORY)->data($story, 'spec,verify,deleteFiles,relievedTwins,reviewer,reviewerHasChanged') ->autoCheck() - ->batchCheck($this->config->story->change->requiredFields, 'notempty') + ->batchCheck($this->config->{$oldStory->type}->change->requiredFields, 'notempty') ->checkFlow() ->where('id')->eq($storyID)->exec(); if(dao::isError()) return false; @@ -749,12 +780,6 @@ class storyModel extends model $this->updateStoryVersion($newStory); } - if($oldStory->type == 'requirement' && $story->status == 'active') - { - $relations = $this->storyTao->getRelation($storyID, 'requirement'); - $this->dao->update(TABLE_STORY)->set('URChanged')->eq(1)->where('id')->in($relations)->exec(); - } - if($story->reviewerHasChanged) { $oldStoryReviewers = $this->getReviewerPairs($storyID, $oldStory->version); @@ -795,9 +820,10 @@ class storyModel extends model $oldStory->twins = ''; } + $moduleName = $this->app->rawModule; $this->dao->update(TABLE_STORY)->data($story, 'reviewer,spec,verify,deleteFiles,finalResult') ->autoCheck() - ->batchCheck($this->config->story->edit->requiredFields, 'notempty') + ->batchCheck($this->config->{$moduleName}->edit->requiredFields, 'notempty') ->checkIF(!empty($story->closedBy), 'closedReason', 'notempty') ->checkIF(isset($story->closedReason) and $story->closedReason == 'done', 'stage', 'notempty') ->checkIF(isset($story->closedReason) and $story->closedReason == 'duplicate', 'duplicateStory', 'notempty') @@ -812,13 +838,23 @@ class storyModel extends model $this->storyTao->doUpdateSpec($storyID, $story, $oldStory, $addedFiles); $this->storyTao->doUpdateLinkStories($storyID, $story, $oldStory); - $changed = $story->parent != $oldStory->parent; - if($changed) $this->doChangeParent($storyID, $story, $oldStory->parent); - if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent, !$changed); + if($story->product != $oldStory->product) + { + $childStories = $this->getAllChildId($storyID, false); + $story->id = $storyID; + foreach($childStories as $childStoryID) + { + $this->updateStoryProduct($childStoryID, $story, $story->product); + } + } + if($story->grade != $oldStory->grade) $this->syncGrade($oldStory, $story); + $parentChanged = $story->parent != $oldStory->parent; + if($parentChanged) $this->doChangeParent($storyID, $story, $oldStory); + if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent, !$parentChanged); if($story->parent > 0) { - $this->dao->update(TABLE_STORY)->set('parent')->eq('-1')->where('id')->eq($story->parent)->exec(); - $this->updateParentStatus($storyID, $story->parent, !$changed); + $this->updateParentStatus($storyID, $story->parent, !$parentChanged); + $this->storyTao->computeParentStage($story); } /* Set new stage and update story sort of plan when story plan has changed. */ @@ -846,7 +882,7 @@ class storyModel extends model ->fetchPairs('project', 'project'); $this->loadModel('kanban'); - foreach($executionIdList as $executionID) $this->kanban->updateLane($executionID, 'story', $storyID); + foreach($executionIdList as $executionID) $this->kanban->updateLane($executionID, $oldStory->type, $storyID); } unset($oldStory->parent, $story->parent); @@ -870,13 +906,30 @@ class storyModel extends model * Update story product. * * @param int $storyID + * @param object $parentID * @param int $productID * @access public * @return void */ - public function updateStoryProduct(int $storyID, int $productID): void + public function updateStoryProduct(int $storyID, object $parent, int $productID): void { - $this->dao->update(TABLE_STORY)->set('product')->eq($productID)->where('id')->eq($storyID)->exec(); + if($parent->id != $storyID) + { + $childPath = $this->dao->select('path')->from(TABLE_STORY)->where('id')->eq($storyID)->fetch('path'); + $childPath = strstr($childPath, ",{$parent->id},"); + $this->dao->update(TABLE_STORY) + ->set('product')->eq($parent->product) + ->set('module')->eq(0) + ->set('root')->eq($parent->root) + ->set('path')->eq($childPath) + ->where('id')->eq($storyID) + ->exec(); + } + else + { + $this->dao->update(TABLE_STORY)->set('path')->eq(",{$storyID},")->where('id')->eq($storyID)->exec(); + } + $this->dao->update(TABLE_PROJECTSTORY)->set('product')->eq($productID)->where('story')->eq($storyID)->exec(); $storyProjects = $this->dao->select('project')->from(TABLE_PROJECTSTORY)->where('story')->eq($storyID)->orderBy('project')->fetchPairs('project', 'project'); $linkedProjects = $this->dao->select('project')->from(TABLE_PROJECTPRODUCT)->where('project')->in($storyProjects)->andWhere('product')->eq($productID)->orderBy('project')->fetchPairs('project','project'); @@ -906,11 +959,7 @@ class storyModel extends model if($parentID <= 0) return true; $childrenStatus = $this->dao->select('id,status')->from(TABLE_STORY)->where('parent')->eq($parentID)->andWhere('deleted')->eq(0)->fetchPairs('status', 'status'); - if(empty($childrenStatus)) - { - $this->dao->update(TABLE_STORY)->set('parent')->eq('0')->where('id')->eq($parentID)->exec(); - return true; - } + if(empty($childrenStatus)) return true; $oldParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($parentID)->andWhere('deleted')->eq(0)->fetch(); if(empty($oldParentStory)) @@ -918,12 +967,13 @@ class storyModel extends model $this->dao->update(TABLE_STORY)->set('parent')->eq('0')->where('id')->eq($storyID)->exec(); return true; } - if($oldParentStory->parent != '-1') $this->dao->update(TABLE_STORY)->set('parent')->eq('-1')->where('id')->eq($parentID)->exec(); $this->computeEstimate($parentID); + if(isset($oldParentStory->type) && $childStory->type != $oldParentStory->type) return false; + $status = $oldParentStory->status; if(count($childrenStatus) == 1 and current($childrenStatus) == 'closed') $status = current($childrenStatus); // Close parent story. - if($oldParentStory->status == 'closed') $status = $this->getActivateStatus($parentID); // Activate parent story. + if($oldParentStory->status == 'closed' && $childStory->status == 'active') $status = $this->getActivateStatus($parentID); // Activate parent story. $action = ''; $preStatus = ''; @@ -958,35 +1008,11 @@ class storyModel extends model $actionID = $this->loadModel('action')->create('story', $parentID, $action, '', $preStatus, '', false); $this->action->logHistory($actionID, $changes); } + + if($newParentStory->parent) return $this->updateParentStatus($parentID, $newParentStory->parent, true); return true; } - /** - * 如果软件需求变更了,则更新软件需求和用户需求关系表中的版本。 - * If the story changed, update the version filed of the requirement in relation table. - * - * @param object $story - * @access public - * @return void - */ - public function updateStoryVersion(object $story): void - { - $changedStories = $this->getChangedStories($story); - if(empty($changedStories)) return; - - foreach($changedStories as $changedStory) - { - $this->dao->update(TABLE_RELATION) - ->set('AVersion')->eq($changedStory->version) - ->where('AType')->eq('requirement') - ->andWhere('BType')->eq('story') - ->andWhere('relation')->eq('subdivideinto') - ->andWhere('AID')->eq($changedStory->id) - ->andWhere('BID')->eq($story->id) - ->exec(); - } - } - /** * Update the story order of plan. * @@ -1063,8 +1089,22 @@ class storyModel extends model $oldStory = $oldStories[$storyID]; if($story->plan != $oldStory->plan) { - if(!empty($oldStory->plan)) $unlinkPlans[$oldStory->plan] = empty($unlinkPlans[$oldStory->plan]) ? $storyID : "{$unlinkPlans[$oldStory->plan]},{$storyID}"; - if(!empty($story->plan)) $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},{$storyID}"; + if(!empty($oldStory->plan)) + { + foreach(explode(',', (string)$oldStory->plan) as $planID) + { + if(!$planID) continue; + $unlinkPlans[$planID] = empty($unlinkPlans[$planID]) ? $storyID : "{$unlinkPlans[$planID]},$storyID"; + } + } + if(!empty($story->plan)) + { + foreach(explode(',', (string)$story->plan) as $planID) + { + if(!$planID) continue; + $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID"; + } + } } } @@ -1083,7 +1123,8 @@ class storyModel extends model if(dao::isError()) return false; /* Update story sort of plan when story plan has changed. */ - if($oldStory->plan != $story->plan) $this->updateStoryOrderOfPlan($storyID, $story->plan, $oldStory->plan); + if($oldStory->plan != $story->plan) $this->updateStoryOrderOfPlan($storyID, (string)$story->plan, $oldStory->plan); + if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent); $this->executeHooks($storyID); if($oldStory->type == 'story' && $story->stage != $oldStory->stage) $this->batchChangeStage(array($storyID), $story->stage); @@ -1104,8 +1145,8 @@ class storyModel extends model } $this->loadModel('score')->create('ajax', 'batchEdit'); - foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', $planID, 'unlinkstory', '', $stories); - foreach($link2Plans as $planID => $stories) $this->action->create('productplan', $planID, 'linkstory', '', $stories); + foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'unlinkstory', '', $stories); + foreach($link2Plans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'linkstory', '', $stories); return true; } @@ -1157,13 +1198,6 @@ class storyModel extends model if($actionID) $this->action->logHistory($actionID, $changes); } - if($story->result == 'pass' && $oldStory->type == 'requirement') - { - /* IF is requirement changed, notify its relation. */ - $relations = $this->storyTao->getRelation($storyID, 'requirement'); - $this->dao->update(TABLE_STORY)->set('URChanged')->eq(1)->where('id')->in($relations)->exec(); - } - if(!empty($oldStory->twins)) $this->syncTwins($oldStory->id, $oldStory->twins, $changes, 'Reviewed'); return true; @@ -1315,13 +1349,6 @@ class storyModel extends model $this->dao->delete()->from(TABLE_STORYSPEC)->where('story')->in($twinsIdList)->andWHere('version')->eq($oldStory->version)->exec(); $this->dao->delete()->from(TABLE_STORYREVIEW)->where('story')->in($twinsIdList)->andWhere('version')->eq($oldStory->version)->exec(); - /* If is requirement recall change, revert its relation. */ - if($oldStory->type == 'requirement') - { - $relations = $this->storyTao->getRelation($storyID, 'requirement'); - $this->dao->update(TABLE_STORY)->set('URChanged')->eq(0)->where('id')->in($relations)->exec(); - } - $changes = common::createChanges($oldStory, $story); if(!empty($oldStory->twins)) $this->syncTwins($storyID, $oldStory->twins, $changes, 'recalledChange'); } @@ -1374,28 +1401,30 @@ class storyModel extends model { $now = helper::now(); $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); - /* 如果该需求是用户需求,则为细分操作,记录用户需求和软件需求的关联关系。 */ - if($oldStory->type == 'requirement') - { - foreach($SRList as $SRID) $this->storyTao->doCreateURRelations($SRID, array($storyID)); - return; - } - /* 如果该需求是软件需求,则为分解子需求操作。 */ /* Set parent to child story. */ - $this->dao->update(TABLE_STORY)->set('parent')->eq($storyID)->where('id')->in($SRList)->exec(); + foreach($SRList as $childStoryID) + { + $path = rtrim($oldStory->path, ',') . ",$childStoryID,"; + $this->dao->update(TABLE_STORY) + ->set('parent')->eq($storyID) + ->set('parentVersion')->eq($oldStory->version) + ->set('root')->eq($oldStory->root) + ->set('path')->eq($path) + ->where('id')->eq($childStoryID) + ->exec(); + } $this->computeEstimate($storyID); /* Set childStories. */ - $childStories = implode(',', $SRList); $newStory = new stdClass(); - $newStory->parent = '-1'; + $newStory->isParent = '1'; $newStory->plan = ''; $newStory->lastEditedBy = $this->app->user->account; $newStory->lastEditedDate = $now; - $newStory->childStories = trim($oldStory->childStories . ',' . $childStories, ','); $this->dao->update(TABLE_STORY)->data($newStory)->autoCheck()->where('id')->eq($storyID)->exec(); + $childStories = implode(',', $SRList); $actionID = $this->loadModel('action')->create('story', $storyID, 'createChildrenStory', '', $childStories); $this->action->logHistory($actionID, common::createChanges($oldStory, $newStory)); } @@ -1404,8 +1433,8 @@ class storyModel extends model * 关闭需求。 * Close the story. * - * @param int $storyID - * @param object $postData + * @param int $storyID + * @param object $postData * @access public * @return array|false */ @@ -1430,12 +1459,12 @@ class storyModel extends model return false; } - if(strpos($this->config->story->close->requiredFields, 'comment') !== false and !$this->post->comment) dao::$errors['comment'][] = sprintf($this->lang->error->notempty, $this->lang->comment); + if(strpos($this->config->{$oldStory->type}->close->requiredFields, 'comment') !== false and !$this->post->comment) dao::$errors['comment'][] = sprintf($this->lang->error->notempty, $this->lang->comment); $this->lang->story->comment = $this->lang->comment; $this->dao->update(TABLE_STORY)->data($story, 'comment,closeSync') ->autoCheck() - ->batchCheck($this->config->story->close->requiredFields, 'notempty') + ->batchCheck($this->config->{$oldStory->type}->close->requiredFields, 'notempty') ->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty') ->checkFlow() ->where('id')->eq($storyID) @@ -1443,11 +1472,7 @@ class storyModel extends model if(dao::isError()) return false; /* Update parent story status and stage. */ - if($oldStory->parent > 0) - { - $this->updateParentStatus($storyID, $oldStory->parent); - $this->setStage($oldStory->parent); - } + if($oldStory->parent > 0) $this->updateParentStatus($storyID, $oldStory->parent); if(!dao::isError()) { $this->setStage($storyID); @@ -1484,6 +1509,7 @@ class storyModel extends model if(empty($story->closedReason)) continue; $oldStory = $oldStories[$storyID]; + if(isset($stories[$oldStory->parent])) continue; $this->dao->update(TABLE_STORY)->data($story, 'comment')->autoCheck() ->checkIF($story->closedReason == 'duplicate', 'duplicateStory', 'notempty') @@ -1590,8 +1616,7 @@ class storyModel extends model if(empty($oldStory)) continue; if($oldStory->branch != BRANCH_MAIN and $plan->branch != BRANCH_MAIN and !in_array($oldStory->branch, explode(',', $plan->branch))) continue; - /* Ignore parent story, closed story and story linked to this plan already. */ - if($oldStory->parent < 0) continue; + /* Ignore closed story and story linked to this plan already. */ if($oldStory->status == 'closed') continue; if(strpos(",{$oldStory->plan},", ",$planID,") !== false) continue; @@ -1604,21 +1629,28 @@ class storyModel extends model if($oldPlanID) $story->plan = trim(str_replace(",$oldPlanID,", ',', ",$oldStory->plan,"), ','); /* Update the order of the story in the plan. */ - $this->updateStoryOrderOfPlan((int)$storyID, (string)$planID, $oldStory->plan); + $oldStoryPlan = $oldStory->type == 'story' ? $oldStory->plan : ''; + $this->updateStoryOrderOfPlan((int)$storyID, (string)$planID, $oldStoryPlan); - /* Replace plan field if product is normal or not linked to plan or story linked to a branch. */ + /* 用需、业需追加计划,软需替换计划。 */ $productType = $products[$oldStory->product]->type; - if($productType == 'normal') $story->plan = $planID; - if(empty($oldPlanID)) $story->plan = $planID; - if($oldStory->branch) $story->plan = $planID; + if($oldStory->type != 'story') + { + $story->plan = trim("{$oldStory->plan},{$planID}", ','); + } + elseif($productType == 'normal' || empty($oldStory->plan) || $oldStory->branch) + { + $story->plan = $planID; + } /* Change stage. */ if($planID) { - if($oldStory->stage == 'wait') $story->stage = 'planned'; + if($oldStory->stage == 'wait') $story->stage = 'planned'; + if($oldStory->stage == 'defining') $story->stage = 'planning'; if($productType != 'normal' and $oldStory->branch == 0) { - if(!empty($oldPlanID)) $story->plan = trim("{$story->plan},{$planID}", ','); + if(!empty($oldPlanID) && $oldStory->type == 'story') $story->plan = trim("{$story->plan},{$planID}", ','); foreach(explode(',', $plan->branch) as $planBranch) { if(isset($oldStoryStages[$storyID][$planBranch])) continue; @@ -1636,25 +1668,124 @@ class storyModel extends model /* Update story and recompute stage. */ $this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq($storyID)->exec(); - if(!$planID) $this->setStage($storyID); + if(!$planID) $this->setStage((int)$storyID); + if(isset($story->stage) && $story->stage != $oldStory->stage && $oldStory->parent > 0) + { + $story->parent = $oldStory->parent; + $this->storyTao->computeParentStage($story); + } if(!dao::isError()) { $allChanges[$storyID] = common::createChanges($oldStory, $story); - if($story->plan != $oldStory->plan and !empty($oldStory->plan) and strpos((string)$oldStory->plan, ',') === false) $unlinkPlans[$oldStory->plan] = empty($unlinkPlans[$oldStory->plan]) ? $storyID : "{$unlinkPlans[$oldStory->plan]},$storyID"; - if($story->plan != $oldStory->plan and !empty($story->plan) and strpos((string)$story->plan, ',') === false) $link2Plans[$story->plan] = empty($link2Plans[$story->plan]) ? $storyID : "{$link2Plans[$story->plan]},$storyID"; + if($story->plan != $oldStory->plan and !empty($oldStory->plan) and $oldStory->type == 'story') + { + foreach(explode(',', (string)$oldStory->plan) as $planID) + { + if(!$planID) continue; + $unlinkPlans[$planID] = empty($unlinkPlans[$planID]) ? $storyID : "{$unlinkPlans[$planID]},$storyID"; + } + } + if($story->plan != $oldStory->plan and !empty($story->plan)) + { + foreach(explode(',', (string)$story->plan) as $planID) + { + if(!$planID) continue; + $link2Plans[$planID] = empty($link2Plans[$planID]) ? $storyID : "{$link2Plans[$planID]},$storyID"; + } + } } } if(!dao::isError()) { $this->loadModel('action'); - foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', $planID, 'unlinkstory', '', $stories); - foreach($link2Plans as $planID => $stories) $this->action->create('productplan', $planID, 'linkstory', '', $stories); + foreach($unlinkPlans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'unlinkstory', '', $stories); + foreach($link2Plans as $planID => $stories) $this->action->create('productplan', (int)$planID, 'linkstory', '', $stories); } return $allChanges; } + /** + * 批量修改需求的父需求。 + * Batch change the parent of story. + * + * @param string $storyIdList + * @param int $parentID + * @param string $storyType + * @access public + * @return bool|string + */ + public function batchChangeParent(string $storyIdList, int $parentID, string $storyType = 'story') + { + if(empty($storyIdList)) return; + + $stories = $this->getByList($storyIdList); + $parent = $this->fetchById($parentID); + $gradePairs = $this->getGradePairs($storyType); + $gradePairs = array_keys($gradePairs); + + $gradeErrorStories = array(); + $parentErrorStories = array(); + + $this->loadModel('action'); + foreach($stories as $storyID => $story) + { + if($story->parent == $parentID) continue; + if($story->id == $parentID) + { + $parentErrorStories[] = '#' . $storyID; + continue; + } + if($parent && $story->root == $parent->root && $story->type == $parent->type && $story->grade < $parent->grade) + { + $parentErrorStories[] = '#' . $storyID; + continue; + } + + $oldStory = clone $story; + if($parent && $parent->type == $story->type) + { + $parentGradeIndex = array_search($parent->grade, $gradePairs); + $story->grade = $gradePairs[$parentGradeIndex + 1]; + } + else + { + $story->grade = current($gradePairs); + } + + if($story->grade > $oldStory->grade) + { + if(!$this->checkGrade($story, $oldStory, 'batch')) + { + $gradeErrorStories[] = '#' . $storyID; + continue; + } + } + + if($story->grade != $oldStory->grade) $this->syncGrade($oldStory, $story); + + $this->dao->update(TABLE_STORY) + ->set('grade')->eq($story->grade) + ->set('parent')->eq($parentID) + ->autoCheck() + ->where('id')->eq($story->id) + ->exec(); + + $this->action->create('story', $story->id, 'syncGrade', '', $story->grade); + + $story->parent = $parentID; + $this->doChangeParent($story->id, $story, $oldStory); + $this->setStage($story->id); + } + + $notice = ''; + if($gradeErrorStories) $notice = sprintf($this->lang->story->batchGradeOverflow, implode(',', $gradeErrorStories)); + if($parentErrorStories) $notice = sprintf($this->lang->story->batchParentError, implode(',', $parentErrorStories)); + + return $notice; + } + /** * Batch change branch. * @@ -1716,6 +1847,82 @@ class storyModel extends model return $allChanges; } + /** + * Batch change the grade of story. + * + * @param array $storyIdList + * @param int $grade + * + * @access public + * @return string|null + */ + public function batchChangeGrade(array $storyIdList, int $grade): string|null + { + $now = helper::now(); + $account = $this->app->user->account; + $oldStories = $this->getByList($storyIdList); + $this->loadModel('action'); + + $rootGroup = array(); + $parentIdList = array(); + foreach($oldStories as $oldStory) + { + $rootGroup[$oldStory->root] = isset($rootGroup[$oldStory->root]) ? $rootGroup[$oldStory->root] + 1 : 1; + if($oldStory->parent > 0) $parentIdList[] = $oldStory->parent; + } + + $parents = $this->dao->select('id, grade, type')->from(TABLE_STORY)->where('id')->in($parentIdList)->fetchAll('id'); + + $sameRootList = ''; + $gradeGtParentList = ''; + $gradeOverflowList = ''; + foreach($storyIdList as $storyID) + { + $oldStory = $oldStories[$storyID]; + if($grade == $oldStory->grade) continue; + + if($rootGroup[$oldStory->root] > 1) + { + $sameRootList .= "#{$storyID} "; + continue; + } + + if($oldStory->parent > 0 && $grade < $parents[$oldStory->parent]->grade && $oldStory->type == $parents[$oldStory->parent]->type) + { + $gradeGtParentList .= "#{$storyID} "; + continue; + } + + $story = new stdclass(); + $story->lastEditedBy = $account; + $story->lastEditedDate = $now; + $story->grade = $grade; + + if($story->grade > $oldStory->grade) + { + if(!$this->checkGrade($story, $oldStory)) + { + $gradeOverflowList .= "#{$storyID} "; + continue; + } + } + + if($story->grade != $oldStory->grade) $this->syncGrade($oldStory, $story); + $this->dao->update(TABLE_STORY)->data($story)->autoCheck()->where('id')->eq((int)$storyID)->exec(); + if(!dao::isError()) + { + $changes = common::createChanges($oldStory, $story); + $actionID = $this->action->create('story', (int)$storyID, 'Edited'); + $this->action->logHistory($actionID, $changes); + } + } + + if($gradeOverflowList) return sprintf($this->lang->story->batchGradeOverflow, $gradeOverflowList); + if($sameRootList) return sprintf($this->lang->story->batchGradeSameRoot, $sameRootList); + if($gradeGtParentList) return sprintf($this->lang->story->batchGradeGtParent, $gradeGtParentList); + return null; + } + /** * Batch change the stage of story. * @@ -1735,6 +1942,8 @@ class storyModel extends model foreach($storyIdList as $storyID) { $oldStory = $oldStories[$storyID]; + if($oldStory->type != 'story' || $oldStory->isParent == '1') continue; + if($oldStory->status == 'draft' or $oldStory->status == 'closed') { $ignoreStories .= "#{$storyID} "; @@ -1920,9 +2129,29 @@ class storyModel extends model */ public function setStage(int $storyID): bool { - $story = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); + $story = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); if(empty($story)) return false; + /* 父需求主动关联计划、项目的情况。 */ + if($story->type != 'story' || $story->isParent == '1') + { + list($linkedBranches, $linkedProjects) = $this->storyTao->getLinkedBranchesAndProjects($storyID); + if($story->stage == 'defining') + { + if(!empty($linkedProjects) || !empty($story->plan)) $this->dao->update(TABLE_STORY)->set('stage')->eq('planning')->where('id')->eq($storyID)->exec(); + } + elseif($story->stage == 'planning') + { + $child = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq('0')->fetch('id'); + if(empty($linkedBranches) && empty($story->plan) && empty($child)) $this->dao->update(TABLE_STORY)->set('stage')->eq('defining')->where('id')->eq($storyID)->exec(); + } + elseif($story->stage == 'closed') + { + return $this->storyTao->setStageToClosed($storyID, $linkedBranches, $linkedProjects); + } + return false; + } + /* 获取已经存在的分支阶段. */ $oldStages = $this->dao->select('*')->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->fetchAll('branch'); $this->dao->delete()->from(TABLE_STORYSTAGE)->where('story')->eq($storyID)->exec(); @@ -1959,47 +2188,24 @@ class storyModel extends model * Get the stories to link. * * @param int $storyID - * @param string $type linkStories|linkRelateSR|linkRelateUR - * @param string $browseType bySearch + * @param string $browseType bySearch * @param int $queryID - * @param string $storyType story|requirement * @param object $pager - * @param string $excludeStories * @access public * @return array */ - public function getStories2Link(int $storyID, string $type = 'linkStories', string $browseType = 'bySearch', int $queryID = 0, string $storyType = 'story', object $pager = null, string $excludeStories = ''): array + public function getStories2Link(int $storyID, string $browseType = 'bySearch', int $queryID = 0, object $pager = null): array { - $story = $this->getById($storyID); - $tmpStoryType = $storyType == 'story' ? 'requirement' : 'story'; - $stories2Link = array(); - if($type == 'linkRelateSR' or $type == 'linkRelateUR') - { - $tmpStoryType = $story->type; - $linkStoryField = $story->type == 'story' ? 'linkStories' : 'linkRequirements'; - $storyIDList = $story->id . ',' . $excludeStories . ',' . $story->{$linkStoryField}; - } - else - { - $storyIDList = $this->storyTao->getRelation($storyID, $story->type); - } + $story = $this->getById($storyID); + $excludes = $this->storyTao->getRelation($storyID, $story->type); + /* 自身不能关联自身。 */ + $excludes[$storyID] = $storyID; + + $stories2Link = array(); if($browseType == 'bySearch') { - $stories2Link = $this->getBySearch($story->product, $story->branch, $queryID, 'id_desc', 0, $tmpStoryType, $storyIDList, '', $pager); - } - elseif($type != 'linkRelateSR' and $type != 'linkRelateUR') - { - $status = $storyType == 'story' ? 'active' : 'all'; - $stories2Link = $this->getProductStories($story->product, $story->branch, '', $status, $tmpStoryType, 'id_desc', true, $storyIDList, $pager); - } - - if($type != 'linkRelateSR' and $type != 'linkRelateUR') - { - foreach($stories2Link as $id => $story) - { - if($storyType == 'story' and $story->status != 'active') unset($stories2Link[$id]); - } + $stories2Link = $this->getBySearch($story->product, $story->branch, $queryID, 'id_desc', 0, 'all', $excludes, '', $pager); } return $stories2Link; @@ -2024,17 +2230,25 @@ class storyModel extends model public function getProductStories(string|int $productID = 0, array|string|int $branch = 0, array|string $moduleIdList = '', array|string $status = 'all', string $type = 'story', string $orderBy = 'id_desc', bool $hasParent = true, array|string $excludeStories = '', object|null $pager = null): array { if(commonModel::isTutorialMode()) return $this->loadModel('tutorial')->getStories(); + $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; + if($showGrades) + { + $pattern = "/{$type}(\d+)/"; + preg_match_all($pattern, $showGrades, $matches); + $showGrades = isset($matches[1]) ? $matches[1] : null; + } $productQuery = $this->storyTao->buildProductsCondition($productID, $branch); $stories = $this->dao->select("*, IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY) ->where('deleted')->eq(0) ->andWhere($productQuery) - ->beginIF(!$hasParent)->andWhere("parent")->ge(0)->fi() + ->beginIF(!$hasParent)->andWhere("isParent")->eq('0')->fi() ->beginIF(!empty($moduleIdList))->andWhere('module')->in($moduleIdList)->fi() ->beginIF(!empty($excludeStories))->andWhere('id')->notIN($excludeStories)->fi() ->beginIF($status and $status != 'all')->andWhere('status')->in($status)->fi() ->andWhere("FIND_IN_SET('{$this->config->vision}', vision)") - ->andWhere('type')->eq($type) + ->beginIF($type != 'all')->andWhere('type')->in($type)->fi() + ->beginIF($showGrades)->andWhere('grade')->in($showGrades)->fi() ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); @@ -2060,15 +2274,17 @@ class storyModel extends model */ public function getProductStoryPairs(int|array $productIdList = 0, string|int $branch = 'all', array|string|int $moduleIdList = '', string|array $status = 'all', string $order = 'id_desc', int $limit = 0, string $type = 'full', string $storyType = 'story', bool|string $hasParent = true): array { + if($hasParent === 'false') $hasParent = false; + $stories = $this->dao->select('t1.id, t1.title, t1.module, t1.pri, t1.estimate, t2.name AS product') ->from(TABLE_STORY)->alias('t1')->leftJoin(TABLE_PRODUCT)->alias('t2')->on('t1.product = t2.id') ->where('1=1') ->beginIF($productIdList)->andWhere('t1.product')->in($productIdList)->fi() ->beginIF($moduleIdList)->andWhere('t1.module')->in($moduleIdList)->fi() ->beginIF($branch !== 'all')->andWhere('t1.branch')->in("0,$branch")->fi() - ->beginIF(!$hasParent or $hasParent == 'false')->andWhere('t1.parent')->ge(0)->fi() + ->beginIF(!$hasParent)->andWhere('t1.isParent')->eq('0')->fi() ->beginIF($status and $status != 'all')->andWhere('t1.status')->in($status)->fi() - ->beginIF($type != 'full' || $type != 'all')->andWhere('t1.type')->eq($storyType)->fi() + ->beginIF($type != 'full' && $type != 'all')->andWhere('t1.type')->eq($storyType)->fi() ->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)") ->andWhere('t1.deleted')->eq('0') ->orderBy($order) @@ -2078,6 +2294,34 @@ class storyModel extends model return $this->formatStories($stories, $type, $limit); } + /** + * 获取需求的所有子需求ID。 + * Get all child stories of a story. + * + * @param int $storyID + * @param bool $includeSelf + * @param bool $sameType true|false + * @access public + * @return array + */ + public function getAllChildId(int $storyID, bool $includeSelf = true, bool $sameType = false): array + { + if($storyID == 0) return array(); + + $story = $this->fetchByID($storyID); + if(empty($story)) return array(); + + $children = $this->dao->select('id')->from(TABLE_STORY) + ->where('path')->like($story->path . '%') + ->andWhere('deleted')->eq(0) + ->beginIF(!$includeSelf)->andWhere('id')->ne($storyID)->fi() + ->beginIF($sameType)->andWhere('type')->eq($story->type)->fi() + ->fetchPairs(); + + return array_keys($children); + } + + /** * Get stories by assignedTo. * @@ -2229,10 +2473,10 @@ class storyModel extends model * @param string|array $modules * @param string $fieldName * @param string $fieldValue - * @param string $type requirement|story + * @param string $type requirement|story * @param string $orderBy * @param object $pager - * @param string $operator equal|include + * @param string $operator equal|include * @access public * @return array */ @@ -2246,10 +2490,19 @@ class storyModel extends model $sql = $this->dao->select("t1.*, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder")->from(TABLE_STORY)->alias('t1'); if($fieldName == 'reviewBy') $sql = $sql->leftJoin(TABLE_STORYREVIEW)->alias('t2')->on('t1.id = t2.story and t1.version = t2.version'); + $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; + if($showGrades) + { + $pattern = "/{$type}(\d+)/"; + preg_match_all($pattern, $showGrades, $matches); + $showGrades = isset($matches[1]) ? $matches[1] : null; + } + $stories = $sql->where('t1.product')->in($productID) ->andWhere('t1.deleted')->eq(0) ->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)") ->andWhere('t1.type')->eq($type) + ->beginIF($showGrades)->andWhere('t1.grade')->in($showGrades)->fi() ->beginIF($branch != 'all')->andWhere("t1.branch")->eq($branch)->fi() ->beginIF($modules)->andWhere("t1.module")->in($modules)->fi() ->beginIF($operator == 'equal' and $fieldName != 'reviewBy' and $fieldName != 'assignedBy')->andWhere('t1.' . $fieldName)->eq($fieldValue)->fi() @@ -2280,9 +2533,18 @@ class storyModel extends model */ public function get2BeClosed(int $productID, int|string $branch, string|array $modules, string $type = 'story', string $orderBy = '', object|null $pager = null): array { + $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; + if($showGrades) + { + $pattern = "/{$type}(\d+)/"; + preg_match_all($pattern, $showGrades, $matches); + $showGrades = isset($matches[1]) ? $matches[1] : null; + } + $stories = $this->dao->select("*,IF(`pri` = 0, {$this->config->maxPriValue}, `pri`) as priOrder")->from(TABLE_STORY) ->where('product')->in($productID) ->andWhere('type')->eq($type) + ->beginIF($showGrades)->andWhere('grade')->in($showGrades)->fi() ->beginIF($branch and $branch != 'all')->andWhere("branch")->eq($branch)->fi() ->beginIF($modules)->andWhere("module")->in($modules)->fi() ->andWhere('deleted')->eq(0) @@ -2404,6 +2666,7 @@ class storyModel extends model if($branch and strpos($storyQuery, '`branch` =') === false) $storyQuery .= " AND `branch` " . helper::dbIN((string)$branch); } $storyQuery = preg_replace("/`plan` +LIKE +'%([0-9]+)%'/i", "CONCAT(',', `plan`, ',') LIKE '%,$1,%'", $storyQuery); + $storyQuery = preg_replace_callback("/AND `grade` (=|!=) '(\w+)(\d+)'/", function($matches){return "AND `grade` {$matches[1]} '" . $matches[3] . "' AND `type` = '" . $matches[2] . "'";}, $storyQuery); return $this->getBySQL($queryProductID, $storyQuery, $orderBy, $pager, $type); } @@ -2441,6 +2704,14 @@ class storyModel extends model } } + $showGrades = isset($this->config->{$type}->showGrades) ? $this->config->{$type}->showGrades : null; + if($showGrades) + { + $pattern = "/{$type}(\d+)/"; + preg_match_all($pattern, $showGrades, $matches); + $showGrades = isset($matches[1]) ? $matches[1] : null; + } + $tmpStories = $this->dao->select("DISTINCT t1.*, IF(t1.`pri` = 0, {$this->config->maxPriValue}, t1.`pri`) as priOrder")->from(TABLE_STORY)->alias('t1') ->leftJoin(TABLE_PROJECTSTORY)->alias('t2')->on('t1.id=t2.story') ->beginIF(strpos($sql, 'result') !== false)->leftJoin(TABLE_STORYREVIEW)->alias('t3')->on('t1.id = t3.story and t1.version = t3.version')->fi() @@ -2448,7 +2719,8 @@ class storyModel extends model ->beginIF($productID != 'all' && $productID != '' && $productID != 0)->andWhere('t1.`product`')->eq((int)$productID)->fi() ->andWhere('t1.deleted')->eq(0) ->andWhere("FIND_IN_SET('{$this->config->vision}', t1.vision)") - ->andWhere('t1.type')->eq($type) + ->beginIF($type != 'all')->andWhere('t1.type')->in($type)->fi() + ->beginIF($showGrades)->andWhere('t1.grade')->in($showGrades)->fi() ->orderBy($orderBy) ->page($pager, 't1.id') ->fetchAll('id'); @@ -2473,46 +2745,155 @@ class storyModel extends model * * @param int $productID * @param string|int $appendedStories + * @param string $storyType + * @param int $storyID * @access public * @return array */ - public function getParentStoryPairs(int $productID, string|int $appendedStories = ''): array + public function getParentStoryPairs(int $productID, string|int $appendedStories = '', string $storyType = 'story', int $storyID = 0): array { - $stories = $this->dao->select('id, title')->from(TABLE_STORY) - ->where('deleted')->eq('0') - ->andWhere('product')->eq($productID) - ->andWhere('parent')->le(0) - ->andWhere('type')->eq('story') - ->andWhere('stage')->eq('wait') - ->andWhere('status')->eq('active') - ->andWhere('plan')->in('0,') - ->andWhere('twins')->eq('') - ->beginIF(!empty($appendedStories))->orWhere('id')->in($appendedStories)->fi() - ->fetchPairs(); - return array(0 => '') + $stories; + if($storyType == 'story') + { + $maxGradeGroup = $this->getMaxGradeGroup(); + $lastGrade = $this->dao->select('grade')->from(TABLE_STORYGRADE)->where('type')->eq($storyType)->andWhere('status')->eq('enable')->orderBy('grade_desc')->limit(1)->fetch('grade'); + $SRGradePairs = $this->getGradePairs('story'); + $URGradePairs = $this->getGradePairs('requirement'); + $requirements = $this->dao->select('id, parent, grade, title, status')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('requirement') + ->beginIF($this->config->requirement->gradeRule == 'stepwise')->andWhere('grade')->eq($maxGradeGroup['requirement'])->fi() + ->fetchAll('id'); + + $parents = array(); + foreach($requirements as $requirement) $parents[$requirement->parent] = $requirement->parent; + + foreach($requirements as $id => $requirement) + { + if(isset($parents[$requirement->id])) unset($requirements[$id]); + if(!isset($URGradePairs[$requirement->grade])) unset($requirements[$id]); + if($requirement->status != 'active' && $requirement->status != 'launched') unset($requirements[$id]); + } + + $childIdList = $this->getAllChildId($storyID); + $stories = $this->dao->select('id, grade, title')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('story') + ->andWhere('status')->eq('active') + ->andWhere('twins')->eq('') + ->andWhere('grade')->ne($lastGrade) + ->andWhere('grade')->in(array_keys($SRGradePairs)) + ->beginIF($childIdList)->andWhere('id')->notIN($childIdList)->fi() + ->beginIF(!empty($appendedStories))->orWhere('id')->in($appendedStories)->fi() + ->fetchAll('id'); + + return array_merge($this->addGradeLabel($requirements), $this->addGradeLabel($stories)); + } + elseif($storyType == 'requirement') + { + return $this->getRequirementParents($productID, $appendedStories, $storyType, $storyID); + } + elseif($storyType == 'epic') + { + return $this->getEpicParents($productID, $appendedStories, $storyType, $storyID); + } } /** - * 关闭用户需求,如果所有细分的软件需求已被关闭。 - * Close the parent user requirement if all divided stories has been closed. + * 获取用户需求的父需求键值对。 + * Get requirement parents. * - * @param int $storyID - * @param object $postData + * @param int $productID + * @param string|int $appendedStories + * @param string $storyType + * @param int $storyID * @access public - * @return void + * @return array */ - public function closeParentRequirement(int $storyID, object $postData): void + public function getRequirementParents(int $productID, string|int $appendedStories = '', string $storyType = 'requirement', int $storyID = 0): array { - $parentID = $this->dao->select('BID')->from(TABLE_RELATION)->where('AID')->eq($storyID)->fetch(); - if(empty($parentID)) return; + $maxGradeGroup = $this->getMaxGradeGroup(); + $lastGrade = $this->dao->select('grade')->from(TABLE_STORYGRADE)->where('type')->eq($storyType)->andWhere('status')->eq('enable')->orderBy('grade_desc')->limit(1)->fetch('grade'); + $ERGradePairs = $this->getGradePairs('epic'); + $URGradePairs = $this->getGradePairs('requirement'); - $stories = $this->dao->select('t2.id, t2.status')->from(TABLE_RELATION)->alias('t1') - ->leftJoin(TABLE_STORY)->alias('t2')->on('t2.id=t1.AID') - ->where('t1.BType')->eq('requirement') - ->andWhere('t2.status')->ne('closed') - ->andWhere('t2.type')->eq('story') + $epics = $this->dao->select('id, parent, grade, title, status')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('epic') + ->beginIF($this->config->epic->gradeRule == 'stepwise')->andWhere('grade')->eq($maxGradeGroup['epic'])->fi() + ->fetchAll('id'); + + $parents = array(); + foreach($epics as $epic) $parents[$epic->parent] = $epic->parent; + + foreach($epics as $id => $epic) + { + if(isset($parents[$epic->id])) unset($epics[$id]); + if(!isset($ERGradePairs[$epic->grade])) unset($epics[$id]); + if($epic->status != 'active') unset($epics[$id]); + } + + $childIdList = $this->getAllChildId($storyID); + $allStoryParents = $this->dao->select('parent')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('story') + ->andWhere('parent')->gt('0') ->fetchPairs(); - if(empty($stories)) $this->close($parentID->BID, $postData); + + $requirements = $this->dao->select('id, parent, grade, title')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('requirement') + ->andWhere('status')->in('active,launched') + ->andWhere('grade')->in(array_keys($URGradePairs)) + ->andWhere('grade')->ne($lastGrade) + ->andWhere('id')->notIN($allStoryParents) + ->beginIF($childIdList)->andWhere('id')->notIN($childIdList)->fi() + ->beginIF(!empty($appendedStories))->orWhere('id')->in($appendedStories)->fi() + ->fetchAll('id'); + + return array_merge($this->addGradeLabel($epics), $this->addGradeLabel($requirements)); + } + + /** + * 获取业务需求的父需求键值对。 + * Get epic parents. + * + * @param int $productID + * @param string|int $appendedStories + * @param string $storyType + * @param int $storyID + * @access public + * @return array + */ + public function getEpicParents(int $productID, string|int $appendedStories = '', string $storyType = 'epic', int $storyID = 0): array + { + $lastGrade = $this->dao->select('grade')->from(TABLE_STORYGRADE)->where('type')->eq($storyType)->andWhere('status')->eq('enable')->orderBy('grade_desc')->limit(1)->fetch('grade'); + $ERGradePairs = $this->getGradePairs('epic'); + $childIdList = $this->getAllChildId($storyID); + + $allRequirementParents = $this->dao->select('parent')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('requirement') + ->andWhere('parent')->gt('0') + ->fetchPairs(); + $epics = $this->dao->select('id, parent, grade, title')->from(TABLE_STORY) + ->where('deleted')->eq('0') + ->andWhere('product')->eq($productID) + ->andWhere('type')->eq('epic') + ->andWhere('status')->eq('active') + ->andWhere('grade')->in(array_keys($ERGradePairs)) + ->andWhere('grade')->ne($lastGrade) + ->andWhere('id')->notIN($allRequirementParents) + ->beginIF($childIdList)->andWhere('id')->notIN($childIdList)->fi() + ->beginIF(!empty($appendedStories))->orWhere('id')->in($appendedStories)->fi() + ->fetchAll('id'); + + return $this->addGradeLabel($epics); } /** @@ -2681,37 +3062,6 @@ class storyModel extends model return $allStories; } - /** - * 根据变更了的软件需求查找对应的用户需求。 - * Get the requirements by the changed stories. - * - * @param object $story - * @access public - * @return array - */ - public function getChangedStories(object $story): array - { - if($story->type == 'requirement') return array(); - - $relations = $this->dao->select('*')->from(TABLE_RELATION) - ->where('AType')->eq('requirement') - ->andWhere('BType')->eq('story') - ->andWhere('relation')->eq('subdivideinto') - ->andWhere('BID')->eq($story->id) - ->fetchAll('AID'); - - if(empty($relations)) return array(); - - $stories = $this->getByList(array_keys($relations)); - foreach($stories as $id => $story) - { - $version = $relations[$story->id]->AVersion; - if($version > $story->version) unset($stories[$id]); - } - - return $stories; - } - /** * Batch get story stage. * @@ -2854,13 +3204,14 @@ class storyModel extends model * 获取产品软件需求数量的数据。 * Get report data of stories per product. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerProduct(): array + public function getDataOfStoriesPerProduct(string $storyType = 'story'): array { $datas = $this->dao->select('product as name, count(product) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('product')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); @@ -2873,14 +3224,15 @@ class storyModel extends model * 获取按模块软件需求数量的统计数据。 * Get report data of stories per module. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerModule(): array + public function getDataOfStoriesPerModule(string $storyType = 'story'): array { $datas = $this->dao->select('module as name, count(module) as value, product, branch') ->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('module,product,branch') ->orderBy('value DESC') ->fetchAll('name'); @@ -2914,18 +3266,20 @@ class storyModel extends model * 获取按需求来源统计的数据。 * Get report data of stories per source. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerSource(): array + public function getDataOfStoriesPerSource(string $storyType = 'story'): array { $datas = $this->dao->select('source as name, count(source) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('source')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); - $this->lang->story->sourceList[''] = $this->lang->report->undefined; - foreach($datas as $key => $data) $data->name = isset($this->lang->story->sourceList[$key]) ? $this->lang->story->sourceList[$key] : $this->lang->report->undefined; + + $this->lang->{$storyType}->sourceList[''] = $this->lang->report->undefined; + foreach($datas as $key => $data) $data->name = isset($this->lang->{$storyType}->sourceList[$key]) ? $this->lang->{$storyType}->sourceList[$key] : $this->lang->report->undefined; return $datas; } @@ -2933,13 +3287,14 @@ class storyModel extends model * 获取按计划进行统计的数据。 * Get report data of stories per plan. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerPlan(): array + public function getDataOfStoriesPerPlan(string $storyType = 'story'): array { $datas = $this->dao->select('plan as name, count(plan) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('plan')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); @@ -2986,17 +3341,19 @@ class storyModel extends model * 获取按状态进行统计的数据。 * Get report data of stories per status. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerStatus(): array + public function getDataOfStoriesPerStatus(string $storyType = 'story'): array { $datas = $this->dao->select('status as name, count(status) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('status')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); - foreach($datas as $status => $data) if(isset($this->lang->story->statusList[$status])) $data->name = $this->lang->story->statusList[$status]; + + foreach($datas as $status => $data) if(isset($this->lang->{$storyType}->statusList[$status])) $data->name = $this->lang->{$storyType}->statusList[$status]; return $datas; } @@ -3004,13 +3361,14 @@ class storyModel extends model * 获取按所处阶段进行统计的数据。 * Get report data of stories per stage. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerStage(): array + public function getDataOfStoriesPerStage(string $storyType = 'story'): array { $datas = $this->dao->select('stage as name, count(stage) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('stage')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); @@ -3022,20 +3380,22 @@ class storyModel extends model * 获取按优先级进行统计的数据。 * Get report data of stories per pri. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerPri(): array + public function getDataOfStoriesPerPri(string $storyType = 'story'): array { $datas = $this->dao->select('pri as name, count(pri) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('pri')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); + foreach($datas as $pri => $data) { - if(isset($this->lang->story->priList[$pri]) && $this->lang->story->priList[$pri] != '') - $data->name = $this->lang->story->priList[$pri]; + if(isset($this->lang->{$storyType}->priList[$pri]) && $this->lang->{$storyType}->priList[$pri] != '') + $data->name = $this->lang->{$storyType}->priList[$pri]; else $data->name = $this->lang->report->undefined; } @@ -3046,13 +3406,14 @@ class storyModel extends model * 获取按照预计工时进行统计的数据。 * Get report data of stories per estimate. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerEstimate(): array + public function getDataOfStoriesPerEstimate(string $storyType = 'story'): array { return $this->dao->select('estimate as name, count(estimate) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('estimate')->orderBy('value') ->fetchAll(); } @@ -3061,13 +3422,14 @@ class storyModel extends model * 获取按由谁创建来进行统计的数据。 * Get report data of stories per openedBy. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerOpenedBy(): array + public function getDataOfStoriesPerOpenedBy(string $storyType = 'story'): array { $datas = $this->dao->select('openedBy as name, count(openedBy) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('openedBy')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); @@ -3080,13 +3442,14 @@ class storyModel extends model * 获取按当前指派给来进行统计的数据。 * Get report data of stories per assignedTo. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerAssignedTo(): array + public function getDataOfStoriesPerAssignedTo(string $storyType = 'story'): array { $datas = $this->dao->select('assignedTo as name, count(assignedTo) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('assignedTo')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); @@ -3099,17 +3462,19 @@ class storyModel extends model * 获取按关闭原因来进行统计的数据。 * Get report data of stories per closedReason. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerClosedReason(): array + public function getDataOfStoriesPerClosedReason(string $storyType = 'story'): array { $datas = $this->dao->select('closedReason as name, count(closedReason) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('closedReason')->orderBy('value DESC') ->fetchAll('name'); if(!$datas) return array(); - foreach($datas as $reason => $data) $data->name = $this->lang->story->reasonList[$reason] != '' ? $this->lang->story->reasonList[$reason] : $this->lang->report->undefined; + + foreach($datas as $reason => $data) $data->name = $this->lang->{$storyType}->reasonList[$reason] != '' ? $this->lang->{$storyType}->reasonList[$reason] : $this->lang->report->undefined; return $datas; } @@ -3117,17 +3482,51 @@ class storyModel extends model * 获取按变更次数来进行统计的数据。 * Get report data of stories per change. * + * @param string $storyType * @access public * @return array */ - public function getDataOfStoriesPerChange(): array + public function getDataOfStoriesPerChange(string $storyType = 'story'): array { return $this->dao->select('(version-1) as name, count(*) as value')->from(TABLE_STORY) - ->where($this->reportCondition()) + ->where($this->reportCondition($storyType)) ->groupBy('version')->orderBy('value') ->fetchAll(); } + /** + * 获取按需求层级来进行统计的数据。 + * Get report data of stories per grade. + * + * @param string $storyType + * @access public + * @return array + */ + public function getDataOfStoriesPerGrade(string $storyType = 'story'): array + { + $datas = $this->dao->select('type, grade as name, count(*) as count')->from(TABLE_STORY) + ->where($this->reportCondition($storyType)) + ->groupBy('type,name') + ->fetchGroup('type', 'name'); + + $gradeGroup = $this->getGradeGroup(); + $summary = array(); + foreach($datas as $type => $gradeCount) + { + $gradePairs = zget($gradeGroup, $type, array()); + foreach($gradeCount as $gradeValue => $count) + { + $grade = zget($gradePairs, $gradeValue); + + $data = new stdclass(); + $data->name = isset($grade->name) ? $grade->name : $this->lang->report->undefined; + $data->value = $count->count; + $summary[] = $data; + } + } + return $summary; + } + /** * Get kanban group data. * @@ -3188,7 +3587,7 @@ class storyModel extends model $ccList = isset($story->mailto) ? str_replace(' ', '', trim($story->mailto, ',')) : ''; /* If the action is changed or reviewed, mail to the project or execution team. */ - if(strtolower($actionType) == 'changed' or strtolower($actionType) == 'reviewed') + if((strtolower($actionType) == 'changed' or strtolower($actionType) == 'reviewed') && $story->type == 'story') { $teamMembers = $this->getTeamMembers($story->id, $actionType); if($teamMembers) @@ -3262,7 +3661,7 @@ class storyModel extends model global $dao; $project = $dao->findByID($_SESSION['project'])->from(TABLE_PROJECT)->fetch(); } - return ($app->tab == 'execution' || (!empty($project) && $project->multiple == '0')) && $story->status == 'active' && $story->type == 'story'; + return ($app->tab == 'execution' || (!empty($project) && $project->multiple == '0')) && $story->status == 'active' && $story->type == 'story' && $story->isParent == '0'; } $disabledFeatures = ",{$config->disabledFeatures},"; @@ -3294,25 +3693,22 @@ class storyModel extends model if($action == 'batchcreate') { if($config->vision == 'or') return false; - if($config->vision == 'lite' && $story->parent <= 0 && $story->status == 'active' && in_array($story->stage, array('wait', 'projected'))) return true; + if(!empty($story->twins)) return false; + if(strpos('active,launched', $story->status) === false) return false; + if($config->vision == 'lite' && ($story->status == 'active' && in_array($story->stage, array('wait', 'projected')))) return true; - if($story->parent > 0 || !empty($story->twins)) return false; - if($story->type == 'requirement' && strpos('draft,reviewing,changing', $story->status) !== false) return false; - - if(strpos('active,launched,developing', $story->status) === false || !empty($story->plan)) return false; - if(isset($shadowProducts[$story->product]) && (!empty($taskGroups[$story->id]) || $story->stage != 'projected')) return false; - if(!isset($shadowProducts[$story->product]) && $story->stage != 'wait' && $story->type == 'story') return false; + if(isset($shadowProducts[$story->product]) && (!empty($taskGroups[$story->id]))) return false; + if(!isset($shadowProducts[$story->product]) && !in_array($story->stage, array('wait', 'planned', 'projected')) && $story->type == 'story' && $story->isParent == '0') return false; } $story->reviewer = isset($story->reviewer) ? $story->reviewer : array(); $story->notReview = isset($story->notReview) ? $story->notReview : array(); - $isSuperReviewer = strpos(',' . trim(zget($config->story, 'superReviewers', ''), ',') . ',', ',' . $app->user->account . ','); + $storyType = $app->methodName == 'audit' ? $story->storyType : $story->type; + $isSuperReviewer = isset($config->{$storyType}) ? strpos(',' . trim(zget($config->{$storyType}, 'superReviewers', ''), ',') . ',', ',' . $app->user->account . ',') : false; - if($action == 'change') return (($isSuperReviewer !== false || count($story->reviewer) == 0 || count($story->notReview) == 0) && in_array($story->status, array('active', 'launched'))); - if($action == 'review') return (($isSuperReviewer !== false || in_array($app->user->account, $story->notReview)) && $story->status == 'reviewing'); - - if($action == 'createbranch') return $story->type == 'story'; - if($action == 'processstorychange') return (bool)$story->URChanged; + if($action == 'change') return (($isSuperReviewer !== false or count($story->reviewer) == 0 or count($story->notReview) == 0) and $story->status == 'active'); + if($action == 'review') return (($isSuperReviewer !== false or in_array($app->user->account, $story->notReview)) and $story->status == 'reviewing'); + if($action == 'createbranch') return $story->type == 'story'; return true; } @@ -3386,23 +3782,82 @@ class storyModel extends model return $stories; } + /** + * 向业务需求和用户需求追加子需求。 + * Append children to story. + * + * @param int $productID + * @param array $stories + * @param string $storyType + * @access public + * @return array|object + */ + public function appendChildren(int $productID, array $stories, string $storyType): array + { + $storyGrades = array(); + $requirementGrades = array(); + + $showGrades = isset($this->config->{$storyType}->showGrades); + if($showGrades) + { + foreach(explode(',', $this->config->{$storyType}->showGrades) as $grade) + { + if(!$grade) continue; + if(strpos($grade, 'requirement') !== false) $requirementGrades[] = str_replace('requirement', '', $grade); + if(strpos($grade, 'story') !== false) $storyGrades[] = str_replace('story', '', $grade); + } + } + + $rootIdList = array(); + foreach($stories as $story) $rootIdList[$story->root] = $story->root; + $children = $this->dao->select('*')->from(TABLE_STORY) + ->where('root')->in($rootIdList) + ->andWhere('product')->eq($productID) + ->andWhere('deleted')->eq('0') + ->beginIF($storyType == 'requirement') + ->andWhere('type')->eq('story') + ->beginIF($showGrades && isset($storyGrades))->andWhere('grade')->in($storyGrades)->fi() + ->fi() + ->beginIF($storyType == 'epic') + ->andWhere('((type')->eq('requirement') + ->beginIF($showGrades && isset($requirementGrades))->andWhere('grade')->in($requirementGrades)->fi() + ->markRight(1) + ->orWhere('(type')->eq('story') + ->beginIF($showGrades && isset($storyGrades))->andWhere('grade')->in($storyGrades)->fi() + ->markRight(1) + ->markRight(1) + ->fi() + ->fetchAll('id'); + + if($children) + { + $children = $this->storyTao->mergePlanTitleAndChildren($productID, $children, $storyType); + return array_merge($stories, $children); + } + + return $stories; + } + /** * Set report condition. * + * @param string $storyType * @access public * @return string */ - public function reportCondition(): string + public function reportCondition(string $storyType): string { - if(isset($_SESSION['storyQueryCondition'])) + $queryCondition = $storyType . 'QueryCondition'; + $onlyCondition = $storyType . 'OnlyCondition'; + if(isset($_SESSION[$queryCondition])) { - if(!$this->session->storyOnlyCondition) + if(!$this->session->{$onlyCondition}) { - preg_match_all('/[`"]' . trim(TABLE_STORY, '`') .'[`"] AS ([\w]+) /', $this->session->storyQueryCondition, $matches); + preg_match_all('/[`"]' . trim(TABLE_STORY, '`') .'[`"] AS ([\w]+) /', $this->session->{$queryCondition}, $matches); $tableAlias = isset($matches[1][0]) ? $matches[1][0] . '.' : ''; - return 'id in (' . preg_replace('/SELECT .* FROM/', "SELECT $tableAlias" . "id FROM", $this->session->storyQueryCondition) . ')'; + return 'id in (' . preg_replace('/SELECT .* FROM/', "SELECT $tableAlias" . "id FROM", $this->session->{$queryCondition}) . ')'; } - return $this->session->storyQueryCondition; + return $this->session->{$queryCondition}; } return '1=1'; } @@ -3410,19 +3865,20 @@ class storyModel extends model /** * Check force review for user. * + * @param string $storyType * @access public * @return bool */ - public function checkForceReview(): bool + public function checkForceReview(string $storyType = 'story'): bool { $forceReview = false; - $forceField = $this->config->story->needReview == 0 ? 'forceReview' : 'forceNotReview'; - $forceReviewRoles = !empty($this->config->story->{$forceField . 'Roles'}) ? $this->config->story->{$forceField . 'Roles'} : ''; - $forceReviewDepts = !empty($this->config->story->{$forceField . 'Depts'}) ? $this->config->story->{$forceField . 'Depts'} : ''; + $forceField = $this->config->{$storyType}->needReview == 0 ? 'forceReview' : 'forceNotReview'; + $forceReviewRoles = !empty($this->config->{$storyType}->{$forceField . 'Roles'}) ? $this->config->{$storyType}->{$forceField . 'Roles'} : ''; + $forceReviewDepts = !empty($this->config->{$storyType}->{$forceField . 'Depts'}) ? $this->config->{$storyType}->{$forceField . 'Depts'} : ''; $forceUsers = ''; - if(!empty($this->config->story->{$forceField})) $forceUsers = $this->config->story->{$forceField}; + if(!empty($this->config->{$storyType}->{$forceField})) $forceUsers = $this->config->{$storyType}->{$forceField}; if(!empty($forceReviewRoles) or !empty($forceReviewDepts)) { @@ -3441,7 +3897,7 @@ class storyModel extends model $forceUsers .= "," . implode(',', array_keys($users)); } - $forceReview = $this->config->story->needReview == 0 ? strpos(",{$forceUsers},", ",{$this->app->user->account},") !== false : strpos(",{$forceUsers},", ",{$this->app->user->account},") === false; + $forceReview = $this->config->{$storyType}->needReview == 0 ? strpos(",{$forceUsers},", ",{$this->app->user->account},") !== false : strpos(",{$forceUsers},", ",{$this->app->user->account},") === false; return $forceReview; } @@ -3495,6 +3951,85 @@ class storyModel extends model return $tracks; } + /** + * 根据传入的需求获取跟踪矩阵,返回看板格式数据。 + * Get track by stories, return kanban format. + * + * @param array $stories + * @param string $storyType epic|requirement|story + * @access public + * @return array + */ + public function getTracksByStories(array $stories, string $storyType): array + { + if(empty($stories)) return array(); + + $rootIdList = array_unique(array_column($stories, 'root')); + $allStories = $this->dao->select('id,parent,color,isParent,root,path,grade,product,pri,type,status,stage,title,estimate')->from(TABLE_STORY)->where('root')->in($rootIdList)->andWhere('deleted')->eq(0)->orderBy('type,grade,parent')->fetchAll('id'); + $leafNodes = $this->storyTao->getLeafNodes($stories, array_keys($allStories)); + + $tracks = array(); + $lanes = $this->storyTao->buildTrackLanes($leafNodes, $storyType); + $cols = $this->storyTao->buildTrackCols($storyType); + $items = $this->storyTao->buildTrackItems($allStories, $leafNodes, $storyType); + + return array('lanes' => $lanes, 'cols' => $cols, 'items' => $items, 'leafNodes' => $leafNodes); + } + + /** + * 获取矩阵需要合并的单元格。 + * Get need merge cells for track. + * + * @param array $tracks + * @param array $showCols + * @access public + * @return array + */ + public function getMergeTrackCells(array $tracks, array $showCols): array + { + $storyCols = array(); + foreach($showCols as $colType) + { + if(!in_array($colType, array('epic', 'requirement', 'story'))) continue; + foreach($tracks['cols'] as $col) + { + $colName = $col['name']; + if($col['parent'] == -1) continue; + if(str_contains($colName, $colType)) $storyCols[] = $col; + } + } + + $firstCol = zget(reset($storyCols), 'name', ''); + $leafNodes = $tracks['leafNodes']; + $mergeCells = array(); + $preRoot = 0; + foreach($tracks['lanes'] as $lane) + { + $laneName = $lane['name']; + + $storyID = substr($laneName, 5); + $story = zget($leafNodes, $storyID, ''); + if(empty($story)) continue; + + if($preRoot != $story->root) + { + $preRoot = $story->root; + continue; + } + if(!empty($tracks['items'][$laneName][$firstCol])) continue; + + foreach($storyCols as $col) + { + $colName = $col['name']; + if(!empty($tracks['items'][$laneName][$colName])) break; + + $mergeCells[$laneName][$colName] = true; + } + } + + return $mergeCells; + } + /** * 获取产品或项目关联的用户需求跟踪矩阵。 * Get requirements track. @@ -3547,56 +4082,25 @@ class storyModel extends model } /** - * 获取单个用户需求的跟踪矩阵 - * Get track by id. - * - * @param int $storyID - * @access public - * @return array - */ - public function getTrackByID(int $storyID): array - { - $requirement = $this->getByID($storyID); - - /* 获取该用户需求细分的研发需求,并构造跟踪矩阵信息。 */ - $storyIdList = $this->storyTao->getRelation($requirement->id, 'requirement'); - $stories = $this->dao->select('id,title,parent')->from(TABLE_STORY)->where('id')->in($storyIdList)->andWhere('deleted')->eq(0)->fetchAll('id'); - $track = array(); - foreach($stories as $id => $story) $track[$id] = $this->storyTao->buildStoryTrack($story); - - return $track; - } - - /** - * 通过关系表查询UR关联的SR或SR关联的UR。 - * Get SR or UR with the relationship between UR and SR. + * 获取需求关联的需求列表。 + * Get story relation. * * @param int $storyID * @param string $storyType story|requirement - * @param array $fields * @access public * @return array */ - public function getStoryRelation(int $storyID, string $storyType = 'story', array $fields = array()): array + public function getStoryRelation(int $storyID, string $storyType = 'story'): array { - $conditionField = $storyType == 'story' ? 'BID' : 'AID'; - $storyType = $storyType == 'story' ? 'AID' : 'BID'; - - $relations = $this->dao->select($storyType)->from(TABLE_RELATION) - ->where('AType')->eq('requirement') - ->andWhere('BType')->eq('story') - ->andWhere('relation')->eq('subdivideinto') - ->andWhere($conditionField)->eq($storyID) - ->fetchPairs(); + $relations = $this->storyTao->getRelation($storyID, $storyType); if(empty($relations)) return array(); - $fields = empty($fields) ? '*' : implode(',', $fields); - return $this->dao->select($fields)->from(TABLE_STORY) + return $this->dao->select('*')->from(TABLE_STORY) ->where('id')->in($relations) ->andWhere('deleted')->eq('0') ->orderBy('id_desc') - ->fetchAll(); + ->fetchGroup('type', 'id'); } /** @@ -3623,8 +4127,8 @@ class storyModel extends model } /** - * 关联软件需求和用户需求。 - * Create a relationship between the story and the requirement. + * 需求关联需求。 + * Story link story. * * @param int $storyID * @param array|object storyList postedData @@ -3633,36 +4137,39 @@ class storyModel extends model */ public function linkStories(int $storyID, array|object $storyList = array()): void { - $story = $this->getByID($storyID); - $stories = empty($storyList) ? $this->post->stories : $storyList; - $isStory = ($story->type == 'story'); + $story = $this->getByID($storyID); + $linkStories = $this->getByList($storyList); - foreach($stories as $id) + $this->loadModel('action'); + foreach($linkStories as $id => $linkStory) { - $requirement = $this->getByID((int)$id); $data = new stdclass(); - $data->AType = 'requirement'; - $data->BType = 'story'; + $data->AType = $story->type; + $data->BType = $linkStory->type; $data->product = $story->product; - $data->relation = 'subdivideinto'; - $data->AID = $isStory ? $id : $storyID; - $data->BID = $isStory ? $storyID : $id; - $data->AVersion = $isStory ? $requirement->version : $story->version; - $data->BVersion = $isStory ? $story->version : $requirement->version; + $data->relation = 'linkedto'; + $data->AID = $storyID; + $data->BID = $id; + $data->AVersion = $story->version; + $data->BVersion = $linkStory->version; $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec(); - $data->AType = 'story'; - $data->BType = 'requirement'; - $data->relation = 'subdividedfrom'; - $data->product = $story->product; - $data->AID = $isStory ? $storyID : $id; - $data->BID = $isStory ? $id : $storyID; - $data->AVersion = $isStory ? $story->version : $requirement->version; - $data->BVersion = $isStory ? $requirement->version : $story->version; + $data->AType = $linkStory->type; + $data->BType = $story->type; + $data->product = $linkStory->product; + $data->relation = 'linkedfrom'; + $data->AID = $id; + $data->BID = $storyID; + $data->AVersion = $linkStory->version; + $data->BVersion = $story->version; $this->dao->insert(TABLE_RELATION)->data($data)->autoCheck()->exec(); + + $this->action->create('story', $id, 'linkrelatedstory', '', $storyID); } + + $this->action->create('story', $storyID, 'linkrelatedstory', '', implode(',', array_keys($linkStories))); } /** @@ -3678,13 +4185,16 @@ class storyModel extends model $idList = "$storyID,$linkedStoryID"; $this->dao->delete()->from(TABLE_RELATION) - ->where('AType')->in('story,requirement') - ->andWhere('BType')->in('story,requirement') - ->andWhere('relation')->in('subdivideinto,subdividedfrom') + ->where('AType')->in('story,requirement,epic') + ->andWhere('BType')->in('story,requirement,epic') + ->andWhere('relation')->in('linkedto,linkedfrom') ->andWhere('AID')->in($idList) ->andWhere('BID')->in($idList) ->exec(); + $this->loadModel('action')->create('story', $storyID, 'unlinkrelatedstory', '', $linkedStoryID); + $this->loadModel('action')->create('story', $linkedStoryID, 'unlinkrelatedstory', '', $storyID); + return !dao::isError(); } @@ -3822,51 +4332,63 @@ class storyModel extends model */ public function replaceURLang(string $type): void { - if($type != 'requirement') return; - $storyLang = $this->lang->story; $SRCommon = $this->lang->SRCommon; - $URCommon = $this->lang->URCommon; + if($this->app->tab != 'product' && $this->app->rawMethod == 'report') + { + $replacement = $this->lang->common->story; + $storyLang->report->charts['storiesPerProduct'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerProduct']); + $storyLang->report->charts['storiesPerModule'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerModule']); + $storyLang->report->charts['storiesPerSource'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerSource']); + } + else + { + if($type == 'story') return; + $replacement = $type == 'requirement' ? $this->lang->URCommon : $this->lang->epic->common; - $storyLang->create = str_replace($SRCommon, $URCommon, $storyLang->create); - $storyLang->changeAction = str_replace($SRCommon, $URCommon, $storyLang->changeAction); - $storyLang->changed = str_replace($SRCommon, $URCommon, $storyLang->changed); - $storyLang->assignAction = str_replace($SRCommon, $URCommon, $storyLang->assignAction); - $storyLang->reviewAction = str_replace($SRCommon, $URCommon, $storyLang->reviewAction); - $storyLang->subdivideAction = str_replace($SRCommon, $URCommon, $storyLang->subdivideAction); - $storyLang->closeAction = str_replace($SRCommon, $URCommon, $storyLang->closeAction); - $storyLang->activateAction = str_replace($SRCommon, $URCommon, $storyLang->activateAction); - $storyLang->deleteAction = str_replace($SRCommon, $URCommon, $storyLang->deleteAction); - $storyLang->view = str_replace($SRCommon, $URCommon, $storyLang->view); - $storyLang->linkStory = str_replace($SRCommon, $URCommon, $storyLang->linkStory); - $storyLang->exportAction = str_replace($SRCommon, $URCommon, $storyLang->exportAction); - $storyLang->zeroCase = str_replace($SRCommon, $URCommon, $storyLang->zeroCase); - $storyLang->zeroTask = str_replace($SRCommon, $URCommon, $storyLang->zeroTask); - $storyLang->copyTitle = str_replace($SRCommon, $URCommon, $storyLang->copyTitle); - $storyLang->common = str_replace($SRCommon, $URCommon, $storyLang->common); - $storyLang->title = str_replace($SRCommon, $URCommon, $storyLang->title); - $storyLang->spec = str_replace($SRCommon, $URCommon, $storyLang->spec); - $storyLang->children = str_replace($SRCommon, $URCommon, $storyLang->children); - $storyLang->linkStories = str_replace($SRCommon, $URCommon, $storyLang->linkStories); - $storyLang->childStories = str_replace($SRCommon, $URCommon, $storyLang->childStories); - $storyLang->duplicateStory = str_replace($SRCommon, $URCommon, $storyLang->duplicateStory); - $storyLang->newStory = str_replace($SRCommon, $URCommon, $storyLang->newStory); - $storyLang->copy = str_replace($SRCommon, $URCommon, $storyLang->copy); - $storyLang->total = str_replace($SRCommon, $URCommon, $storyLang->total); - $storyLang->released = str_replace($SRCommon, $URCommon, $storyLang->released); - $storyLang->legendLifeTime = str_replace($SRCommon, $URCommon, $storyLang->legendLifeTime); - $storyLang->legendLinkStories = str_replace($SRCommon, $URCommon, $storyLang->legendLinkStories); - $storyLang->legendChildStories = str_replace($SRCommon, $URCommon, $storyLang->legendChildStories); - $storyLang->legendSpec = str_replace($SRCommon, $URCommon, $storyLang->legendSpec); + $storyLang->create = str_replace($SRCommon, $replacement, $storyLang->create); + $storyLang->changeAction = str_replace($SRCommon, $replacement, $storyLang->changeAction); + $storyLang->changed = str_replace($SRCommon, $replacement, $storyLang->changed); + $storyLang->assignAction = str_replace($SRCommon, $replacement, $storyLang->assignAction); + $storyLang->reviewAction = str_replace($SRCommon, $replacement, $storyLang->reviewAction); + $storyLang->subdivideAction = str_replace($SRCommon, $replacement, $storyLang->subdivideAction); + $storyLang->closeAction = str_replace($SRCommon, $replacement, $storyLang->closeAction); + $storyLang->activateAction = str_replace($SRCommon, $replacement, $storyLang->activateAction); + $storyLang->deleteAction = str_replace($SRCommon, $replacement, $storyLang->deleteAction); + $storyLang->view = str_replace($SRCommon, $replacement, $storyLang->view); + $storyLang->linkStory = str_replace($SRCommon, $replacement, $storyLang->linkStory); + $storyLang->exportAction = str_replace($SRCommon, $replacement, $storyLang->exportAction); + $storyLang->zeroCase = str_replace($SRCommon, $replacement, $storyLang->zeroCase); + $storyLang->zeroTask = str_replace($SRCommon, $replacement, $storyLang->zeroTask); + $storyLang->copyTitle = str_replace($SRCommon, $replacement, $storyLang->copyTitle); + $storyLang->title = str_replace($SRCommon, $replacement, $storyLang->title); + $storyLang->spec = str_replace($SRCommon, $replacement, $storyLang->spec); + $storyLang->children = str_replace($SRCommon, $replacement, $storyLang->children); + $storyLang->linkStories = str_replace($SRCommon, $replacement, $storyLang->linkStories); + $storyLang->childStories = str_replace($SRCommon, $replacement, $storyLang->childStories); + $storyLang->duplicateStory = str_replace($SRCommon, $replacement, $storyLang->duplicateStory); + $storyLang->newStory = str_replace($SRCommon, $replacement, $storyLang->newStory); + $storyLang->copy = str_replace($SRCommon, $replacement, $storyLang->copy); + $storyLang->total = str_replace($SRCommon, $replacement, $storyLang->total); + $storyLang->released = str_replace($SRCommon, $replacement, $storyLang->released); + $storyLang->legendLifeTime = str_replace($SRCommon, $replacement, $storyLang->legendLifeTime); + $storyLang->legendLinkStories = str_replace($SRCommon, $replacement, $storyLang->legendLinkStories); + $storyLang->legendChildStories = str_replace($SRCommon, $replacement, $storyLang->legendChildStories); + $storyLang->legendSpec = str_replace($SRCommon, $replacement, $storyLang->legendSpec); + $storyLang->unlinkStory = str_replace($SRCommon, $replacement, $storyLang->unlinkStory); + $storyLang->legendLifeTime = str_replace($SRCommon, $replacement, $storyLang->legendLifeTime); + $storyLang->legendLinkStories = str_replace($SRCommon, $replacement, $storyLang->legendLinkStories); + $storyLang->legendChildStories = str_replace($SRCommon, $replacement, $storyLang->legendChildStories); + $storyLang->legendSpec = str_replace($SRCommon, $replacement, $storyLang->legendSpec); - $storyLang->notice->closed = str_replace($SRCommon, $URCommon, $storyLang->notice->closed); - $storyLang->notice->reviewerNotEmpty = str_replace($SRCommon, $URCommon, $storyLang->notice->reviewerNotEmpty); - $storyLang->skipStory = str_replace($SRCommon, $URCommon, $storyLang->skipStory); - $storyLang->closedStory = str_replace($SRCommon, $URCommon, $storyLang->closedStory); + $storyLang->notice->closed = str_replace($SRCommon, $replacement, $storyLang->notice->closed); + $storyLang->notice->reviewerNotEmpty = str_replace($SRCommon, $replacement, $storyLang->notice->reviewerNotEmpty); + $storyLang->closedStory = str_replace($SRCommon, $replacement, $storyLang->closedStory); - $storyLang->report->charts['storiesPerProduct'] = str_replace($SRCommon, $URCommon, $storyLang->report->charts['storiesPerProduct']); - $storyLang->report->charts['storiesPerModule'] = str_replace($SRCommon, $URCommon, $storyLang->report->charts['storiesPerModule']); - $storyLang->report->charts['storiesPerSource'] = str_replace($SRCommon, $URCommon, $storyLang->report->charts['storiesPerSource']); + $storyLang->report->charts['storiesPerProduct'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerProduct']); + $storyLang->report->charts['storiesPerModule'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerModule']); + $storyLang->report->charts['storiesPerSource'] = str_replace($SRCommon, $replacement, $storyLang->report->charts['storiesPerSource']); + } } /** @@ -3897,7 +4419,8 @@ class storyModel extends model $rejectCount = 0; $revertCount = 0; $clarifyCount = 0; - $reviewRule = !empty($this->config->story->reviewRules) ? $this->config->story->reviewRules : 'allpass'; + $moduleName = $this->app->rawModule; + $reviewRule = !empty($this->config->{$moduleName}->reviewRules) ? $this->config->{$moduleName}->reviewRules : 'allpass'; foreach($reviewerList as $result) { $passCount = $result == 'pass' ? $passCount + 1 : $passCount; @@ -4166,15 +4689,17 @@ class storyModel extends model /* Get stories. */ $stories = array(); $selectedIDList = $this->cookie->checkedItem ? $this->cookie->checkedItem : '0'; - if($this->session->storyOnlyCondition) + $queryCondition = $storyType . 'QueryCondition'; + $onlyCondition = $storyType . 'OnlyCondition'; + if($this->session->$onlyCondition) { - $queryCondition = $postData->exportType == 'selected' ? ' `id` ' . helper::dbIN($selectedIDList) : str_replace('`story`', '`id`', $this->session->storyQueryCondition); - $stories = $this->dao->select('id,title,linkStories,childStories,parent,mailto,reviewedBy')->from(TABLE_STORY)->where($queryCondition)->orderBy($orderBy)->fetchAll('id'); + $queryCondition = $postData->exportType == 'selected' ? ' `id` ' . helper::dbIN($selectedIDList) : str_replace('`story`', '`id`', $this->session->$queryCondition); + $stories = $this->dao->select('id,title,type,grade,linkStories,parent,mailto,reviewedBy')->from(TABLE_STORY)->where($queryCondition)->orderBy($orderBy)->fetchAll('id'); } else { $orderBy = " ORDER BY " . helper::wrapSqlAfterOrderBy($orderBy); - $querySQL = $this->session->storyQueryCondition . $orderBy; + $querySQL = $this->session->$queryCondition . $orderBy; if($postData->exportType == 'selected') $querySQL = "SELECT * FROM " . TABLE_STORY . "WHERE `id` IN({$selectedIDList})" . $orderBy; $stmt = $this->app->dbQuery($querySQL); @@ -4183,14 +4708,18 @@ class storyModel extends model if(empty($stories)) return $stories; - $storyIdList = array_keys($stories); - $children = array(); + $storyIdList = array_keys($stories); + $children = array(); + $parentIdList = array(); foreach($stories as $story) { - if($story->parent > 0 and isset($stories[$story->parent])) + if($story->parent > 0) { - $children[$story->parent][$story->id] = $story; - unset($stories[$story->id]); + $parentIdList[] = $story->parent; + if(isset($stories[$story->parent])) + { + $children[$story->parent][$story->id] = $story; + } } } @@ -4212,6 +4741,7 @@ class storyModel extends model $storyTasks = $this->loadModel('task')->getStoryTaskCounts($storyIdList); $storyBugs = $this->loadModel('bug')->getStoryBugCounts($storyIdList); $storyCases = $this->loadModel('testcase')->getStoryCaseCounts($storyIdList); + $parents = $this->dao->select('id, title')->from(TABLE_STORY)->where('id')->in($parentIdList)->fetchPairs('id', 'title'); /* Get related objects title or names. */ $relatedSpecs = $this->dao->select('*')->from(TABLE_STORYSPEC)->where('`story`')->in($storyIdList)->orderBy('version desc')->fetchGroup('story'); @@ -4223,9 +4753,10 @@ class storyModel extends model if(!empty($relatedSpec[0]->files)) $fileIdList[] = $relatedSpec[0]->files; } $fileIdList = array_unique($fileIdList); - $relatedFiles = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('objectType')->eq('story')->andWhere('objectID')->in($storyIdList)->andWhere('extra')->ne('editor')->fetchGroup('objectID'); + $relatedFiles = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('objectType')->eq($storyType)->andWhere('objectID')->in($storyIdList)->andWhere('extra')->ne('editor')->fetchGroup('objectID'); $filesInfo = $this->dao->select('id, objectID, pathname, title')->from(TABLE_FILE)->where('id')->in($fileIdList)->andWhere('extra')->ne('editor')->fetchAll('id'); + $gradeGroup = $this->getGradeGroup(); foreach($stories as $story) { $story->spec = ''; @@ -4266,14 +4797,6 @@ class storyModel extends model $story->linkStories = implode("; \n", array_filter($tmpLinkStories)); } - if($story->childStories) - { - $tmpChildStories = array(); - $childStoriesIdList = explode(',', $story->childStories); - foreach($childStoriesIdList as $childStoryID) $tmpChildStories[] = zget($relatedStories, trim($childStoryID), ''); - $story->childStories = implode("; \n", array_filter($tmpChildStories)); - } - /* Set related files. */ $story->files = ''; if(isset($relatedFiles[$story->id])) @@ -4292,6 +4815,11 @@ class storyModel extends model $reviewedByList = array_filter(explode(',', $story->reviewedBy)); foreach($reviewedByList as $i => $reviewedBy) $reviewedByList[$i] = zget($users, trim($reviewedBy)); $story->reviewedBy = implode(',', array_filter($reviewedByList)); + + $gradePairs = zget($gradeGroup, $story->type, array()); + $grade = zget($gradePairs, $story->grade, ''); + $story->grade = $grade->name; + $story->parent = zget($parents, $story->parent, ''); } return $stories; @@ -4440,15 +4968,15 @@ class storyModel extends model * @param string $type * @param object $execution * @param string $storyType story|requirement + * @param array $maxGradeGroup * @access public * @return array */ - public function buildActionButtonList(object $story, $type = 'browse', object|null $execution = null, $storyType = 'story'): array + public function buildActionButtonList(object $story, $type = 'browse', object|null $execution = null, $storyType = 'story', array $maxGradeGroup = array()): array { - $menu = ''; $params = "storyID=$story->id"; - if($type == 'browse') return $this->storyTao->buildBrowseActionBtnList($story, $params, $storyType, $execution); + if($type == 'browse') return $this->storyTao->buildBrowseActionBtnList($story, $params, $storyType, $execution, $maxGradeGroup); return array(); } @@ -4459,12 +4987,13 @@ class storyModel extends model * @param object $story * @param array $options * @param string $storyType sting|requirement + * @param array $maxGradeGroup * @access public * @return object */ - public function formatStoryForList(object $story, array $options = array(), string $storyType = 'story'): object + public function formatStoryForList(object $story, array $options = array(), string $storyType = 'story', array $maxGradeGroup = array()): object { - $story->actions = $this->buildActionButtonList($story, 'browse', zget($options, 'execution', null), $storyType); + $story->actions = $this->buildActionButtonList($story, 'browse', zget($options, 'execution', null), $storyType, $maxGradeGroup); $story->estimate = $story->estimate . $this->config->hourUnit; $story->taskCount = zget(zget($options, 'storyTasks', array()), $story->id, 0); @@ -4476,9 +5005,10 @@ class storyModel extends model $story->roadmap = zget(zget($options, 'roadmaps', array()), $story->roadmap, 0); $story->sourceNote = $story->source == 'researchreport' ? zget(zget($options, 'reports', array()), $story->sourceNote, '') : $story->sourceNote; - $story->source = zget($this->lang->story->sourceList, $story->source, ''); - $story->category = zget($this->lang->story->categoryList, $story->category); - $story->closedReason = zget($this->lang->story->reasonList, $story->closedReason); + $story->pri = zget($this->lang->{$storyType}->priList, $story->pri); + $story->source = zget($this->lang->{$storyType}->sourceList, $story->source); + $story->category = zget($this->lang->{$storyType}->categoryList, $story->category); + $story->closedReason = zget($this->lang->{$storyType}->reasonList, $story->closedReason); /* Set rowKey to uniqueID in dTable. */ $story->uniqueID = $story->parent > 0 ? $story->parent . '-' . $story->id : $story->id; @@ -4510,6 +5040,17 @@ class storyModel extends model $story->actions = $actions; } + /* Parent story has changed. */ + if(!empty($story->parentChanged)) + { + $story->rawStatus = 'changing'; + $story->status = $this->lang->story->parent . $this->lang->story->change; + } + else + { + $story->rawStatus = $story->status; + $story->status = zget($this->lang->{$story->type}->statusList, $story->status); + } return $story; } @@ -4546,7 +5087,6 @@ class storyModel extends model ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.story = t2.id') ->where('t1.project')->eq($projectID) ->andWhere('t2.deleted')->eq(0) - ->beginIF($this->config->vision != 'lite')->andWhere('t2.type')->eq('story')->fi() ->orderBy('t1.`order`_desc') ->fetchAll(); } @@ -4591,4 +5131,260 @@ class storyModel extends model return $storyStatus; } + + /** + * 需求下拉列表增加层级标签。 + * Add grade label to story options. + * + * @param array $stories + * @access public + * @return array + */ + public function addGradeLabel(array $stories): array + { + $gradeGroup = $this->getGradeGroup(); + + $storyList = $this->getByList(array_keys($stories)); + + $options = array(); + foreach($storyList as $story) + { + $gradePairs = zget($gradeGroup, $story->type, array()); + $storyTitle = is_string($stories[$story->id]) ? $stories[$story->id] : $story->title; + if(isset($gradePairs[$story->grade])) + { + $options[] = array('text' => array('html' => "{$gradePairs[$story->grade]->name} {$storyTitle}"), 'value' => $story->id, 'keys' => $storyTitle); + } + else + { + $options[] = array('text' => $stories[$story->id], 'value' => $story->id); + } + } + + return $options; + } + + /** + * Get story grade list. + * + * @param string $type story|requirement|epic + * @access public + * @return array + */ + public function getGradeList(string $type = 'story'): array + { + return $this->dao->select('*')->from(TABLE_STORYGRADE) + ->where('1=1') + ->beginIF($type)->andWhere('type')->eq($type)->fi() + ->orderBy('grade_asc') + ->fetchAll(); + } + + /** + * Get story grade group. + * + * @access public + * @return array + */ + public function getGradeGroup(): array + { + return $this->dao->select('type, grade, name')->from(TABLE_STORYGRADE)->orderBy('type,grade')->fetchGroup('type', 'grade'); + } + + /** + * Get story grade pairs. + * + * @param string $type story|requirement|epic + * @param string $status enable|disable + * @param array $appendList + * @access public + * @return array + */ + public function getGradePairs(string $type = 'story', string $status = 'enable', array $appendList = array()): array + { + return $this->dao->select("grade, name")->from(TABLE_STORYGRADE) + ->where('type')->eq($type) + ->beginIF($status == 'enable')->andWhere('status')->eq('enable')->fi() + ->beginIF($appendList) + ->orWhere('(grade')->in($appendList) + ->andWhere('type')->eq($type) + ->markRight(1) + ->fi() + ->orderBy('grade_asc') + ->fetchPairs(); + } + + /** + * 获取需求列表层级设置菜单。 + * Get grade menu in story list. + * + * @param string $storyType + * @param object $project + * @access public + * @return array + */ + public function getGradeMenu(string $storyType, object $project = null): array + { + $storyTypes = isset($project->storyType) ? explode(',', $project->storyType) : array(); + $showEpic = $storyType == 'epic' || in_array('epic', $storyTypes) || $storyType == 'all'; + $showRequirement = $showEpic || $storyType == 'requirement' || in_array('requirement', $storyTypes) || $storyType == 'all'; + + $menu = array(); + if($showEpic) + { + $items = array(); + $gradePairs = $this->getGradePairs('epic', 'all'); + foreach($gradePairs as $grade => $name) $items[] = array('text' => $name, 'value' => "epic{$grade}"); + $menu[] = array('text' => $this->lang->preview . $this->lang->ERCommon, 'value' => 'epic', 'items' => $items); + } + + if($showRequirement) + { + $items = array(); + $gradePairs = $this->getGradePairs('requirement', 'all'); + foreach($gradePairs as $grade => $name) $items[] = array('text' => $name, 'value' => "requirement{$grade}"); + $menu[] = array('text' => $this->lang->preview . $this->lang->URCommon, 'value' => 'requirement', 'items' => $items); + } + + $items = array(); + $gradePairs = $this->getGradePairs('story', 'all'); + foreach($gradePairs as $grade => $name) $items[] = array('text' => $name, 'value' => "story{$grade}"); + $menu[] = array('text' => $this->lang->preview . $this->lang->SRCommon, 'value' => 'story', 'items' => $items); + + return $menu; + } + + /** + * 获取需求层级下拉列表。 + * Get grade options. + * + * @param object|bool $story + * @param string $storyType + * @param array $appendList + * @access public + * @return array + */ + public function getGradeOptions(object|bool $story, string $storyType, array $appendList = array()): array + { + if(!$story) return $this->getGradePairs($storyType, 'enable', $appendList); + + $gradeOptions = array(); + if($storyType != $story->type) + { + $gradeOptions = $this->getGradePairs($storyType, 'enable', $appendList); + } + else + { + $gradeOptions = $this->dao->select('grade, name')->from(TABLE_STORYGRADE) + ->where('type')->eq($storyType) + ->andWhere('grade')->gt($story->grade) + ->andWhere('status')->eq('enable') + ->beginIF($appendList) + ->orWhere('(grade')->in($appendList) + ->andWhere('type')->eq($storyType) + ->markRight(1) + ->fi() + ->orderBy('grade_asc') + ->fetchPairs(); + } + + return $gradeOptions; + } + + /** + * 按照类型分组,获取需求最大层级。 + * Get max grade group by story type. + * + * @access public + * @return array + */ + public function getMaxGradeGroup() + { + return $this->dao->select('type, max(grade) as maxGrade')->from(TABLE_STORYGRADE) + ->where('status')->eq('enable') + ->groupBy('type') + ->fetchPairs(); + } + + /** + * 获取需求列表默认展示的层级。 + * Get the default show grades. + * + * @param array $gradeMenu + * @access public + * @return string + */ + public function getDefaultShowGrades(array $gradeMenu): string + { + $showGrades = ''; + foreach($gradeMenu as $menu) + { + foreach($menu['items'] as $item) + { + $showGrades .= $item['value'] . ','; + } + } + + return $showGrades; + } + + /** + * 检查需求层级是否合法。 + * Check if the grade is valid. + * + * @param object $story + * @param object $oldStory + * @param strign $mode single|batch + * @access public + * @return bool + */ + public function checkGrade(object $story, object $oldStory, string $mode = 'single') + { + $children = $this->getAllChildId($oldStory->id); + $maxGrade = $this->dao->select('max(grade) as maxGrade')->from(TABLE_STORY) + ->where('id')->in($children) + ->andWhere('deleted')->eq('0') + ->fetch('maxGrade'); + + $systemMaxGrade = $this->dao->select('max(grade) as maxGrade')->from(TABLE_STORYGRADE) + ->where('type')->eq($oldStory->type) + ->andWhere('status')->eq('enable') + ->fetch('maxGrade'); + + $newMaxGrade = (int)$maxGrade + (int)$story->grade - (int)$oldStory->grade; + if($newMaxGrade > $systemMaxGrade) + { + if($mode == 'batch') return false; + dao::$errors['grade'] = sprintf($this->lang->story->gradeOverflow, $systemMaxGrade, $newMaxGrade); + } + + return !dao::isError(); + } + + /** + * 父需求层级变更时,同步子需求层级。 + * Sync child story grade when parent story grade changed. + * + * @param object $oldStory + * @param object $story + * @access public + * @return array + */ + public function syncGrade(object $oldStory, object $story) + { + if($oldStory->isParent != '1') return; + + $childIdList = $this->getAllChildId($oldStory->id, false); + if($childIdList) + { + $this->loadModel('action'); + $children = $this->getByList($childIdList); + foreach($children as $child) + { + $grade = (int)$child->grade + (int)$story->grade - (int)$oldStory->grade; + $this->dao->update(TABLE_STORY)->set('grade')->eq($grade)->where('id')->eq($child->id)->exec(); + $this->action->create('story', $child->id, 'syncGrade', '', $grade); + } + } + } } diff --git a/module/story/tao.php b/module/story/tao.php index ba4fdff06b..6ba3380f11 100644 --- a/module/story/tao.php +++ b/module/story/tao.php @@ -67,29 +67,31 @@ class storyTao extends storyModel } /** - * 获取用户需求细分的研发需求,或者研发需求关联的用户需求。 - * Get associated requirements. + * 获取需求主动关联、被动关联的需求。 + * Get linked stories. * * @param int $storyID * @param string $storyType - * @param array $fields * @access public * @return array */ - public function getRelation(int $storyID, string $storyType, array $fields = array()): array + public function getRelation(int $storyID, string $storyType): array { - /* 初始化查询条件变量。*/ - $BType = $storyType == 'story' ? 'requirement' : 'story'; - $relation = $storyType == 'story' ? 'subdividedfrom' : 'subdivideinto'; - $queryFields = empty($fields) ? 'id,title' : implode(',', $fields); - - /* 获取对应的关联数据。*/ - return $this->dao->select('BID')->from(TABLE_RELATION) + /* 主动关联。*/ + $linkedToList = $this->dao->select('BID')->from(TABLE_RELATION) ->where('AType')->eq($storyType) - ->andWhere('BType')->eq($BType) - ->andWhere('relation')->eq($relation) ->andWhere('AID')->eq($storyID) + ->andWhere('relation')->eq('linkedto') ->fetchPairs(); + + /* 被动关联。*/ + $linkedFromList = $this->dao->select('AID')->from(TABLE_RELATION) + ->where('BType')->eq($storyType) + ->andWhere('BID')->eq($storyID) + ->andWhere('relation')->eq('linkedto') + ->fetchPairs(); + + return $linkedToList + $linkedFromList; } /** @@ -284,59 +286,13 @@ class storyTao extends storyModel return "({$productQuery}) "; } - /** - * 将需求列表中的子需求合并到列表中的父需求下。 - * Merge children to parent. - * - * @param array $stories - * @param string $type - * @access protected - * @return int[] - */ - protected function appendSRToChildren(array $stories): array - { - /* For requirement children. */ - $relationGroups = $this->batchGetRelations(array_keys($stories), 'requirement', array('*')); - if(empty($stories)); - - if($this->app->rawModule == 'projectstory') - { - $projectID = $this->session->project; - /* For story children. */ - $relationIDList = array(); - foreach($relationGroups as $relationGroup) $relationIDList = array_merge($relationIDList, array_keys($relationGroup)); - $projectIDList = $this->dao->select('story,project')->from(TABLE_PROJECTSTORY)->where('story')->in($relationIDList)->andWhere('project')->eq($projectID)->fetchPairs('story', 'project'); - } - - foreach($stories as $story) - { - /* Merge subdivided stories for requirement. */ - if(empty($relationGroups[$story->id])) continue; - - $story->parent = '-1'; - $story->children = array(); - foreach($relationGroups[$story->id] as $SRID => $SRStory) - { - if(empty($SRStory)) continue; - if(!empty($projectID) && !isset($projectIDList[$SRID])) continue; - - $children = clone $SRStory; - $children->parent = $story->id; - $children->project = isset($projectIDList[$SRID]) ? $projectIDList[$SRID] : 0; - $story->children[$SRID] = $children; - } - $story->linkStories = implode(',', array_column($story->children, 'title')); - } - return $stories; - } - /** * 追加需求所属的计划标题和子需求。 * Merge plan title and children. * * @param int|array|string $productID * @param array $stories - * @param string $type story|requirement + * @param string $type story|requirement * * @access protected * @return array @@ -352,40 +308,61 @@ class storyTao extends storyModel $plans = $this->dao->select('id,title')->from(TABLE_PRODUCTPLAN)->Where('deleted')->eq(0)->beginIF($productID)->andWhere('product')->in($productID)->fetchPairs('id', 'title'); $parents = $this->extractParents($stories); - if($parents) + if($parents) $parents = $this->dao->select('id,title,status,version,type')->from(TABLE_STORY)->where('id')->in($parents)->andWhere('deleted')->eq(0)->fetchAll('id'); + + $childItems = $this->getChildItems($stories); + + if($type != 'story') { - $children = $this->dao->select('id,parent,title')->from(TABLE_STORY)->where('parent')->in($parents)->andWhere('deleted')->eq(0)->fetchGroup('parent', 'id'); - $parents = $this->dao->select('id,title')->from(TABLE_STORY)->where('id')->in($parents)->andWhere('deleted')->eq(0)->fetchAll('id'); + $sameTypeChildren = $this->dao->select('distinct t1.parent')->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.parent = t2.id') + ->where('t1.parent')->in(array_keys($stories)) + ->andWhere('t1.type = t2.type') + ->andWhere('t2.deleted')->eq(0) + ->fetchPairs(); + + $otherTypeChildren = $this->dao->select('distinct t1.parent')->from(TABLE_STORY)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.parent = t2.id') + ->where('t1.parent')->in(array_keys($stories)) + ->andWhere('t1.type != t2.type') + ->andWhere('t2.deleted')->eq(0) + ->fetchPairs(); } - $mainID = $type == 'story' ? 'BID' : 'AID'; - $countID = $type == 'story' ? 'AID' : 'BID'; - - $relations = $this->dao->select("$mainID, count($countID) as count")->from(TABLE_RELATION) - ->where('AType')->eq('requirement') - ->andWhere('BType')->eq('story') - ->andWhere('relation')->eq('subdivideinto') - ->groupBy($mainID) - ->fetchAll($mainID); - - if($type == 'requirement') $stories = $this->appendSRToChildren($stories); foreach($stories as $story) { - /* Export story linkstories. */ - if(isset($children[$story->id])) $story->linkStories = implode(',', array_column($children[$story->id], 'title')); + /* Judge parent story has changed. */ + if($story->parent > 0 and isset($parents[$story->parent])) + { + if($parents[$story->parent]->version > $story->parentVersion && $story->parentVersion > 0 && $parents[$story->parent]->status == 'active') $story->parentChanged = true; + } - /* Merge parent story title. */ - if($story->parent > 0 and isset($parents[$story->parent])) $story->parentName = $parents[$story->parent]->title; + /* Judge parent story if has same type child or other type child. */ + if($story->type != 'story' && $story->isParent == '1') + { + if(isset($sameTypeChildren[$story->id])) $story->hasSameTypeChild = true; + if(isset($otherTypeChildren[$story->id])) $story->hasOtherTypeChild = true; + } /* Merge plan title. */ $story->planTitle = ''; - $storyPlans = explode(',', trim($story->plan, ',')); + $storyPlans = explode(',', trim((string)$story->plan, ',')); foreach($storyPlans as $planID) $story->planTitle .= zget($plans, $planID, '') . ' '; - if(isset($relations[$story->id])) + if(isset($childItems[$story->id])) { - if($type == 'story') $story->URS = $relations[$story->id]->count; - if($type == 'requirement') $story->SRS = $relations[$story->id]->count; + $story->childItem = $childItems[$story->id]['finished'] . ' / ' . $childItems[$story->id]['total']; + $story->childItemTitle = $childItems[$story->id]['title']; + } + + $story->originParent = $story->parent; + + $story->parent = array(); + foreach(explode(',', trim((string)$story->path, ',')) as $parentID) + { + if(!$parentID) continue; + if($parentID == $story->id) continue; + $story->parent[] = (int)$parentID; } } @@ -406,13 +383,51 @@ class storyTao extends storyModel { $parent = array_map(function($story) { - if($story->parent == '-1') return $story->id; - if($story->parent > '0') return $story->parent; + if($story->parent > '0') return $story->parent; return false; }, $stories); return array_values(array_unique(array_filter($parent))); } + /** + * 获取需求的子项,统计已完成/总数。 + * Get child items of stories, and count the finished/total. + * + * @param array $stories + * @access protected + * @return array + */ + protected function getChildItems(array $stories): array + { + $childItems = array(); + + $childStories = $this->dao->select('id, parent, status')->from(TABLE_STORY) + ->where('parent')->in(array_keys($stories)) + ->andWhere('deleted')->eq(0) + ->fetchGroup('parent'); + + foreach($childStories as $parentID => $childStory) + { + $childItems[$parentID]['total'] = count($childStory); + $childItems[$parentID]['finished'] = count(array_filter($childStory, function($story){return $story->status == 'closed';})); + $childItems[$parentID]['title'] = sprintf($this->lang->story->childStoryTitle, $childItems[$parentID]['total'], $childItems[$parentID]['finished']); + } + + $childTasks = $this->dao->select('id, story, status')->from(TABLE_TASK) + ->where('story')->in(array_keys($stories)) + ->andWhere('deleted')->eq(0) + ->fetchGroup('story'); + + foreach($childTasks as $parentID => $childTask) + { + $childItems[$parentID]['total'] = count($childTask); + $childItems[$parentID]['finished'] = count(array_filter($childTask, function($task){return in_array($task->status, array('done', 'closed', 'cancel'));})); + $childItems[$parentID]['title'] = sprintf($this->lang->story->childTaskTitle, $childItems[$parentID]['total'], $childItems[$parentID]['finished']); + } + + return $childItems; + } + /** * 通过搜索条件获取关联执行的需求。 * Get execution stories by search. @@ -422,12 +437,13 @@ class storyTao extends storyModel * @param int $productID * @param string $orderBy * @param string $storyType + * @param string $sqlCondition * @param array $excludeStories * @param object|null $pager * @access protected * @return array */ - protected function getExecutionStoriesBySearch(int $executionID, int $queryID, int $productID, string $orderBy, string $storyType = 'story', array $excludeStories = array(), object|null $pager = null): array + protected function getExecutionStoriesBySearch(int $executionID, int $queryID, int $productID, string $orderBy, string $storyType = 'story', string $sqlCondition = '', array $excludeStories = array(), object|null $pager = null): array { /* 获取查询条件。 */ $rawModule = $this->app->rawModule; @@ -439,6 +455,7 @@ class storyTao extends storyModel $storyQuery = $this->replaceAllProductQuery($this->session->executionStoryQuery); $storyQuery = $this->replaceRevertQuery($storyQuery, $productID); $storyQuery = preg_replace('/`(\w+)`/', 't2.`$1`', $storyQuery); + $storyQuery = preg_replace_callback("/t2.`grade` (=|!=) '(\w+)(\d+)'/", function($matches){return "t2.`grade` {$matches[1]} '" . $matches[3] . "' AND t2.`type` = '" . $matches[2] . "'";}, $storyQuery); if(strpos($storyQuery, 'result') !== false) $storyQuery = str_replace('t2.`result`', 't4.`result`', $storyQuery); return $this->dao->select("distinct t1.*, t2.*, IF(t2.`pri` = 0, {$this->config->maxPriValue}, t2.`pri`) as priOrder, t3.type as productType, t2.version as version")->from(TABLE_PROJECTSTORY)->alias('t1') @@ -449,7 +466,8 @@ class storyTao extends storyModel ->andWhere('t1.project')->eq($executionID) ->andWhere('t2.deleted')->eq(0) ->andWhere('t3.deleted')->eq(0) - ->andWhere('t2.type')->eq($storyType) + ->andWhere('t2.type')->in($storyType) + ->beginIF($sqlCondition)->andWhere($sqlCondition)->fi() ->beginIF($excludeStories)->andWhere('t2.id')->notIN($excludeStories)->fi() ->orderBy($orderBy) ->page($pager, 't2.id') @@ -633,7 +651,10 @@ class storyTao extends storyModel */ protected function getUnclosedStatusKeys(): array { - $unclosedStatus = $this->lang->story->statusList; + $moduleName = $this->app->rawModule; + if(!in_array($moduleName, array('story', 'epic', 'requirement'))) $moduleName = 'story'; + + $unclosedStatus = $this->lang->{$moduleName}->statusList; unset($unclosedStatus['closed']); return array_keys($unclosedStatus); } @@ -686,10 +707,10 @@ class storyTao extends storyModel */ protected function doCreateStory(object $story): int|false { - $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify,reviewer,URS,region,lane,branches,plans,modules,uploadImage') + $this->dao->insert(TABLE_STORY)->data($story, 'spec,verify,reviewer,region,lane,branches,plans,modules,uploadImage') ->autoCheck() ->checkIF(!empty($story->notifyEmail), 'notifyEmail', 'email') - ->batchCheck($this->config->story->create->requiredFields, 'notempty') + ->batchCheck($this->config->{$story->type}->create->requiredFields, 'notempty') ->checkFlow() ->exec(); if(dao::isError()) return false; @@ -836,51 +857,71 @@ class storyTao extends storyModel * * @param int $storyID * @param object $story - * @param int $oldStoryParent + * @param object $oldStory * @access protected * @return void */ - protected function doChangeParent(int $storyID, object $story, int $oldStoryParent) + protected function doChangeParent(int $storyID, object $story, object $oldStory) { $this->loadModel('action'); - $this->updateStoryProduct($storyID, $story->product); - if($oldStoryParent == '-1') + if($oldStory->parent > 0) { - $childStories = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($storyID)->andWhere('deleted')->eq(0)->fetchPairs('id'); - foreach($childStories as $childStoryID) $this->updateStoryProduct($childStoryID, $story->product); - } + $oldParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStory->parent)->fetch(); + $oldChildren = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($oldStory->parent)->andWhere('deleted')->eq(0)->fetchPairs('id', 'id'); + if(empty($oldChildren)) + { + $this->dao->update(TABLE_STORY) + ->set('isParent')->eq('0') + ->beginIF($oldParentStory->type == 'story')->set('stage')->eq('wait')->fi() + ->where('id')->eq($oldStory->parent) + ->exec(); + } + $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStory->parent)->fetch(); - if($oldStoryParent > 0) - { - $oldParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStoryParent)->fetch(); - $oldChildren = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($oldStoryParent)->andWhere('deleted')->eq(0)->fetchPairs('id', 'id'); - $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($oldStoryParent)->fetch(); - if(empty($oldChildren)) $this->dao->update(TABLE_STORY)->set('parent')->eq(0)->where('id')->eq($oldStoryParent)->exec(); - $this->dao->update(TABLE_STORY)->set('childStories')->eq(implode(',', $oldChildren))->set('lastEditedBy')->eq($this->app->user->account)->set('lastEditedDate')->eq(helper::now())->where('id')->eq($oldStoryParent)->exec(); - - $this->action->create('story', $storyID, 'unlinkParentStory', '', $oldStoryParent, '', false); - $actionID = $this->action->create('story', $oldStoryParent, 'unLinkChildrenStory', '', $storyID, '', false); + $this->action->create('story', $storyID, 'unlinkParentStory', '', $oldStory->parent, '', false); + $actionID = $this->action->create('story', $oldStory->parent, 'unLinkChildrenStory', '', $storyID, '', false); $changes = common::createChanges($oldParentStory, $newParentStory); if(!empty($changes)) $this->action->logHistory($actionID, $changes); + $this->updateLane($oldParentStory->id, $oldParentStory->type); } if($story->parent > 0) { - $parentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch(); - $children = $this->dao->select('id')->from(TABLE_STORY)->where('parent')->eq($story->parent)->andWhere('deleted')->eq(0)->fetchPairs('id', 'id'); - $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch(); - $this->dao->update(TABLE_STORY)->set('parent')->eq('-1') - ->set('childStories')->eq(implode(',', $children)) + $parentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch(); + $newRoot = $parentStory->root; + $story->path = rtrim($parentStory->path, ',') . ',' . $storyID . ','; + + $this->dao->update(TABLE_STORY)->set('parentVersion')->eq($parentStory->version)->where('id')->eq($storyID)->exec(); + $this->dao->update(TABLE_STORY) + ->set('isParent')->eq('1') ->set('lastEditedBy')->eq($this->app->user->account) ->set('lastEditedDate')->eq(helper::now()) ->where('id')->eq($story->parent) ->exec(); + $newParentStory = $this->dao->select('*')->from(TABLE_STORY)->where('id')->eq($story->parent)->fetch(); $this->action->create('story', $storyID, 'linkParentStory', '', $story->parent, '', false); $actionID = $this->action->create('story', $story->parent, 'linkChildStory', '', $storyID, '', false); $changes = common::createChanges($parentStory, $newParentStory); if(!empty($changes)) $this->action->logHistory($actionID, $changes); + $this->updateLane($parentStory->id, $parentStory->type); } + else + { + $newRoot = $storyID; + $story->path = ",{$storyID},"; + } + + $childIdList = $this->getAllChildId($storyID, false); + $children = $this->getByList($childIdList); + foreach($children as $child) + { + $newChildPath = str_replace($oldStory->path, $story->path, $child->path); + $this->dao->update(TABLE_STORY)->set('path')->eq($newChildPath)->where('id')->eq($child->id)->exec(); + } + + if($childIdList) $this->dao->update(TABLE_STORY)->set('root')->eq($newRoot)->where('id')->in($childIdList)->exec(); + $this->dao->update(TABLE_STORY)->set('root')->eq($newRoot)->set('path')->eq($story->path)->where('id')->eq($storyID)->exec(); } /** @@ -894,6 +935,7 @@ class storyTao extends storyModel */ protected function doUpdateLinkStories(int $storyID, object $story, object $oldStory) { + if($oldStory->type == 'epic') return; $linkStoryField = $oldStory->type == 'story' ? 'linkStories' : 'linkRequirements'; $linkStories = explode(',', $story->{$linkStoryField}); $oldLinkStories = explode(',', $oldStory->{$linkStoryField}); @@ -918,53 +960,6 @@ class storyTao extends storyModel } } - /** - * 创建用户需求和软件需求的关联关系。 - * Do create UR and SR relations. - * - * @param int $storyID - * @param array $URList - * @access protected - * @return void - */ - protected function doCreateURRelations(int $storyID, array $URList): void - { - if(empty($storyID) || empty($URList)) return; - - $requirements = $this->getByList($URList); - $data = new stdclass(); - foreach($URList as $URID) - { - if(!isset($requirements[$URID])) continue; - - $requirement = $requirements[$URID]; - if($requirement->type != 'requirement') continue; - - $requirement = $requirements[$URID]; - $data->product = $requirement->product; - $data->AType = 'requirement'; - $data->BType = 'story'; - $data->relation = 'subdivideinto'; - $data->AID = $URID; - $data->BID = $storyID; - $data->AVersion = $requirement->version; - $data->BVersion = 1; - $data->extra = 1; - - $this->dao->replace(TABLE_RELATION)->data($data)->autoCheck()->exec(); - - $data->AType = 'story'; - $data->BType = 'requirement'; - $data->relation = 'subdividedfrom'; - $data->AID = $storyID; - $data->BID = $URID; - $data->AVersion = 1; - $data->BVersion = $requirement->version; - - $this->dao->replace(TABLE_RELATION)->data($data)->autoCheck()->exec(); - } - } - /** * 在创建需求的时候,将需求关联到项目或执行。 * Link to execution for create story. @@ -1130,11 +1125,32 @@ class storyTao extends storyModel protected function checkCanSubdivide($story, $isShadowProduct): bool { if($this->config->vision == 'lite') return true; + if($story->type != 'story') return true; - if(in_array($story->status, array('launched', 'developing', 'active'))) return true; - if(!$isShadowProduct && $story->type == 'story' && $story->stage != 'wait') return false; - if($isShadowProduct && $story->stage != 'projected') return false; - if($story->parent > 0) return false; + if(!in_array($story->status, array('launched', 'developing', 'active'))) return false; + if($story->isParent == '1') return true; + if(!$isShadowProduct && !in_array($story->stage, array('wait', 'planned', 'projected'))) return false; + if($isShadowProduct && $story->stage != 'projected') return false; + + return true; + } + + /** + * Check whether a story can be split. + * + * @param object $story + * @access protected + * @return bool + */ + protected function checkCanSplit($story): bool + { + $sameTypeChild = $this->dao->select('id')->from(TABLE_STORY) + ->where('parent')->eq($story->id) + ->andWhere('type')->eq($story->type) + ->andWhere('deleted')->eq(0) + ->fetch('id'); + + if($sameTypeChild) return false; return true; } @@ -1227,7 +1243,9 @@ class storyTao extends storyModel { if(!empty($branchID)) $this->dao->replace(TABLE_STORYSTAGE)->set('story')->eq($storyID)->set('branch')->eq((int)$branchID)->set('stage')->eq('closed')->exec(); } + if($story->stage != 'closed') $this->updateLinkedLane($storyID, $linkedProjects); + if($story->parent > 0) $this->computeParentStage($story); return true; } @@ -1244,10 +1262,14 @@ class storyTao extends storyModel */ protected function updateStage(int $storyID, array $stages, array $oldStages = array(), array $linkedProjects = array()): bool { - if(empty($stages) && $oldStages) $stages = array_column($oldStages, 'stage', 'branch'); - if(empty($stages)) return false; - $story = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); + if(empty($stages) && $oldStages) $stages = array_column($oldStages, 'stage', 'branch'); + if(empty($stages)) + { + if($story->parent > 0) $this->computeParentStage($story); + return false; + } + if(empty($story)) return false; $product = $this->dao->findById($story->product)->from(TABLE_PRODUCT)->fetch(); if($product and $product->type != 'normal' and empty($story->branch)) @@ -1280,10 +1302,145 @@ class storyTao extends storyModel $this->dao->update(TABLE_STORY)->set('stage')->eq($stage)->where('id')->eq($storyID)->exec(); } - if($story->stage != $stage) $this->updateLinkedLane($storyID, $linkedProjects); + if($story->stage != $stage) + { + $this->updateLinkedLane($storyID, $linkedProjects); + if($story->parent > 0) $this->computeParentStage($story); + } return true; } + /** + * 通过子需求更新父需求的阶段。 + * 子需求阶段范围:未开始、已计划、已立项、设计中、设计完毕、研发中、研发完毕、测试中、测试完毕、已验收、验收失败、待发布、已发布、已关闭。 + * 父需求阶段范围:定义中、规划中、研发中、交付中、已关闭。 + * + * 父需求阶段由子需求阶段决定,规则如下: + * 1. 定义中: + * 排除已关闭(关闭原因不是已完成)后,所有子需求阶段仅为定义中、未开始。 + * 2. 规划中: + * 排除已关闭(关闭原因不是已完成)后,至少有一个子级需求为规划中、已立项、已计划, 其他子需求阶段仅为定义中、未开始。 + * 3. 研发中: + * 排除已关闭(关闭原因不是已完成)后,至少有一个子级需求为设计中、设计完毕、研发中、研发完毕、测试中、测试完毕、已验收、验收失败, 其他子需求阶段仅为未开始、规划中、已立项、已计划。 + * 4. 交付中: + * 至少有一个子级需求为交付中、待发布、已发布;或者有一个子级需求为已关闭(关闭原因是已完成)。 + * 5. 已关闭: + * 所有子需求都已关闭。 + * + * Update parent stage by children. + * Parent stage is decided by children stage, the rules are as follows: + * 1. Defining: + * All children stages are defining or wait, and no children stage is closed and closedReason is not done. + * 2. Planning: + * At least one child stage is planning, planned or projected, and all other children stages are defining or wait, and no children stage is closed and closedReason is not done. + * 3. Developing: + * At least one child stage is designing, designed, developing, developed, testing, tested, verified or rejected, and all other children stages are defining, planning, planned or projected, and no children stage is closed and closedReason is not done. + * 4. Delivering: + * At least one child stage is delivering, pending or released, or at least one child stage is closed and closedReason is done. + * 5. Closed: + * All children stages are closed. + * + * @param object $story + * @access protected + * @return void + */ + protected function computeParentStage(object $story) + { + $parent = $this->dao->findById($story->parent)->from(TABLE_STORY)->fetch(); + if(empty($parent)) return; + + $children = $this->dao->select('id, stage, closedReason')->from(TABLE_STORY) + ->where('parent')->eq($story->parent) + ->andWhere('deleted')->eq(0) + ->fetchAll('id'); + + $allDefining = true; + $allClosed = true; + foreach($children as $child) + { + if($child->stage == 'closed' && $child->closedReason != 'done') continue; + if(!in_array($child->stage, array('wait', 'defining'))) $allDefining = false; + if($child->stage != 'closed') $allClosed = false; + } + + $parentStage = $parent->stage; + if($allClosed) + { + $parentStage = 'closed'; + } + elseif($allDefining) + { + $parentStage = 'defining'; + } + else + { + $hasPlanning = false; + $allClosedOrDefining = true; + foreach($children as $child) + { + /* Planning. */ + if($child->stage == 'closed' && $child->closedReason != 'done') continue; + if(in_array($child->stage, array('planning', 'planned', 'projected'))) + { + $hasPlanning = true; + } + elseif(!in_array($child->stage, array('wait', 'defining'))) + { + $allClosedOrDefining = false; + } + } + + if($hasPlanning && $allClosedOrDefining) + { + $parentStage = 'planning'; + } + else + { + /* Developing. */ + $hasDeveloping = false; + $allClosedOrDefiningOrPlanning = true; + foreach($children as $child) + { + if($child->stage == 'closed' && $child->closedReason != 'done') continue; + if(in_array($child->stage, array('designing', 'designed', 'developing', 'developed', 'testing', 'tested', 'verified', 'rejected'))) + { + $hasDeveloping = true; + } + elseif(!in_array($child->stage, array('wait', 'defining', 'planning', 'planned', 'projected'))) + { + $allClosedOrDefiningOrPlanning = false; + } + } + + if($hasDeveloping && $allClosedOrDefiningOrPlanning) + { + $parentStage = 'developing'; + } + else + { + /* Delivering. */ + $hasDelivering = false; + foreach($children as $child) + { + if(in_array($child->stage, array('delivering', 'pending', 'released')) || ($child->stage == 'closed' && $child->closedReason == 'done')) + { + $hasDelivering = true; + break; + } + } + + if($hasDelivering) $parentStage = 'delivering'; + } + } + } + + if($parentStage != $parent->stage) + { + $this->dao->update(TABLE_STORY)->set('stage')->eq($parentStage)->where('id')->eq($parent->id)->exec(); + if($parent->parent > 0) $this->computeParentStage($parent); + } + } + /** * 根据计划和关联的项目的分支,获取默认的阶段值。 * Get default stages by plans and linked branches. @@ -1321,7 +1478,7 @@ class storyTao extends storyModel protected function getLinkedTaskStat(int $storyID, array $linkedProjects): array { $tasks = $this->dao->select('type,execution,status')->from(TABLE_TASK)->where('execution')->in(array_keys($linkedProjects)) - ->andWhere('type')->in('devel,test') + ->andWhere('type')->in('devel,test,design') ->andWhere('story')->eq($storyID) ->andWhere('deleted')->eq(0) ->andWhere('status')->ne('cancel') @@ -1330,9 +1487,10 @@ class storyTao extends storyModel if(empty($tasks)) return array(); /* Cycle all tasks, get counts of every type and every status. */ - $branchStatusList = $branchDevelCount = $branchTestCount = array(); - $statusList['devel'] = array('wait' => 0, 'doing' => 0, 'done' => 0, 'pause' => 0); - $statusList['test'] = array('wait' => 0, 'doing' => 0, 'done' => 0, 'pause' => 0); + $branchStatusList = $branchDevelCount = $branchTestCount = $branchDesignCount = array(); + $statusList['design'] = array('wait' => 0, 'doing' => 0, 'done' => 0, 'pause' => 0); + $statusList['devel'] = array('wait' => 0, 'doing' => 0, 'done' => 0, 'pause' => 0); + $statusList['test'] = array('wait' => 0, 'doing' => 0, 'done' => 0, 'pause' => 0); foreach($tasks as $type => $typeTasks) { if(!isset($statusList[$type])) continue; @@ -1348,12 +1506,35 @@ class storyTao extends storyModel if(!isset($branchStatusList[$branch])) $branchStatusList[$branch] = $statusList; $branchStatusList[$branch][$type][$status] ++; - if($type == 'devel') $branchDevelCount[$branch] = !isset($branchDevelCount[$branch]) ? 1 : ($branchDevelCount[$branch] + 1); - if($type == 'test') $branchTestCount[$branch] = !isset($branchTestCount[$branch]) ? 1 : ($branchTestCount[$branch] + 1); + if($type == 'devel') $branchDevelCount[$branch] = !isset($branchDevelCount[$branch]) ? 1 : ($branchDevelCount[$branch] + 1); + if($type == 'test') $branchTestCount[$branch] = !isset($branchTestCount[$branch]) ? 1 : ($branchTestCount[$branch] + 1); + if($type == 'design') $branchDesignCount[$branch] = !isset($branchDesignCount[$branch]) ? 1 : ($branchDesignCount[$branch] + 1); } } } - return array($branchStatusList, $branchDevelCount, $branchTestCount); + return array($branchStatusList, $branchDevelCount, $branchTestCount, $branchDesignCount); + } + + /** + * 更新关联的看板泳道卡片。 + * Update linked lane. + * + * @param int $storyID + * @param string $storyType + * @access protected + * @return int + */ + protected function updateLane(int $storyID, string $storyType) + { + $executionIdList = $this->dao->select('t1.project')->from(TABLE_PROJECTSTORY)->alias('t1') + ->leftJoin(TABLE_PROJECT)->alias('t2')->on('t1.project = t2.id') + ->where('t1.story')->eq($storyID) + ->andWhere('t2.deleted')->eq(0) + ->andWhere('t2.type')->in('sprint,stage,kanban') + ->fetchPairs('project', 'project'); + + $this->loadModel('kanban'); + foreach($executionIdList as $executionID) $this->kanban->updateLane($executionID, $storyType, $storyID); } /** @@ -1400,14 +1581,18 @@ class storyTao extends storyModel } /* 根据任务状态统计信息,计算该需求所处的阶段。 */ - list($branchStatusList, $branchDevelCount, $branchTestCount) = $taskStat; + list($branchStatusList, $branchDevelCount, $branchTestCount, $branchDesignCount) = $taskStat; foreach($branchStatusList as $branch => $statusList) { $branch = (int)$branch; $stage = 'projected'; - $testCount = isset($branchTestCount[$branch]) ? $branchTestCount[$branch] : 0; - $develCount = isset($branchDevelCount[$branch]) ? $branchDevelCount[$branch] : 0; + $desginCount = isset($branchDesignCount[$branch]) ? $branchDesignCount[$branch] : 0; + $testCount = isset($branchTestCount[$branch]) ? $branchTestCount[$branch] : 0; + $develCount = isset($branchDevelCount[$branch]) ? $branchDevelCount[$branch] : 0; + $doingDesignTask = $statusList['design']['wait'] < $desginCount && $statusList['design']['done'] < $desginCount && $desginCount > 0; + $doneDesignTask = $statusList['design']['done'] == $desginCount && $desginCount > 0; + $notStartDevTask = $statusList['devel']['wait'] == $develCount; $doingDevelTask = $statusList['devel']['wait'] < $develCount && $statusList['devel']['done'] < $develCount && $develCount > 0; $doneDevelTask = $statusList['devel']['done'] == $develCount && $develCount > 0; $notStartTestTask = $statusList['test']['wait'] == $testCount; @@ -1416,13 +1601,15 @@ class storyTao extends storyModel $hasDoingTestTask = $statusList['test']['doing'] > 0 || $statusList['test']['pause'] > 0; $notDoingTestTask = $statusList['test']['doing'] == 0; - if($doingDevelTask && $notStartTestTask) $stage = 'developing'; //开发任务没有全部完成,测试任务没有开始,阶段为开发中。 - if($doingDevelTask && $notDoingTestTask) $stage = 'developing'; //开发任务没有全部完成,没有测试中的测试任务,阶段为开发中。 - if($doingDevelTask && $doneTestTask) $stage = 'testing'; //开发任务没有全部完成,测试任务已经完成,阶段为测试中。 - if($doneDevelTask && $notStartTestTask) $stage = 'developed'; //开发任务已经完成,测试任务还没有开始,阶段为开发完成。 - if($doneDevelTask && $doingTestTask) $stage = 'testing'; //开发任务已经完成,测试任务已经开始,阶段为测试中。 - if($hasDoingTestTask) $stage = 'testing'; //有测试任务正在测试,阶段为测试中。 - if($doneDevelTask && $doneTestTask) $stage = 'tested'; //开发任务已经完成,测试任务已经完成,阶段为测试完成。 + if($doingDesignTask && $notStartDevTask) $stage = 'designing'; //设计任务没有全部完成,开发任务还没有开始,阶段为设计中。 + if($doneDesignTask && $notStartDevTask) $stage = 'designed'; //设计任务全部完成,开发任务还没有开始,阶段为设计完成。 + if($doingDevelTask && $notStartTestTask) $stage = 'developing'; //开发任务没有全部完成,测试任务没有开始,阶段为开发中。 + if($doingDevelTask && $notDoingTestTask) $stage = 'developing'; //开发任务没有全部完成,没有测试中的测试任务,阶段为开发中。 + if($doingDevelTask && $doneTestTask) $stage = 'testing'; //开发任务没有全部完成,测试任务已经完成,阶段为测试中。 + if($doneDevelTask && $notStartTestTask) $stage = 'developed'; //开发任务已经完成,测试任务还没有开始,阶段为开发完成。 + if($doneDevelTask && $doingTestTask) $stage = 'testing'; //开发任务已经完成,测试任务已经开始,阶段为测试中。 + if($hasDoingTestTask) $stage = 'testing'; //有测试任务正在测试,阶段为测试中。 + if($doneDevelTask && $doneTestTask) $stage = 'tested'; //开发任务已经完成,测试任务已经完成,阶段为测试完成。 $stages[(int)$branch] = $stage; } @@ -1508,7 +1695,7 @@ class storyTao extends storyModel if(!isset($this->config->story->affect)) $this->config->story->affect = new stdclass(); $this->config->story->affect->bugs = new stdclass(); $this->config->story->affect->bugs->fields[] = array('name' => 'id', 'title' => $this->lang->idAB); - $this->config->story->affect->bugs->fields[] = array('name' => 'title', 'title' => $this->lang->bug->title, 'link' => $this->config->vision != 'or' ? helper::createLink('bug', 'view', 'id={id}') : '', 'type' => 'title', 'fixed' => false, 'sortType' => false, 'flex' => false); + $this->config->story->affect->bugs->fields[] = array('name' => 'title', 'title' => $this->lang->bug->title, 'link' => $this->config->vision != 'or' ? helper::createLink('bug', 'view', 'id={id}') : '', 'data-toggle' => 'modal', 'data-size' => 'lg', 'fixed' => false, 'sortType' => false, 'flex' => false); $this->config->story->affect->bugs->fields[] = array('name' => 'status', 'title' => $this->lang->statusAB); $this->config->story->affect->bugs->fields[] = array('name' => 'openedBy', 'title' => $this->lang->bug->openedBy); $this->config->story->affect->bugs->fields[] = array('name' => 'resolvedBy', 'title' => $this->lang->bug->resolvedBy); @@ -1516,7 +1703,7 @@ class storyTao extends storyModel $this->config->story->affect->bugs->fields[] = array('name' => 'lastEditedBy', 'title' => $this->lang->bug->lastEditedBy); /* Get affected bugs. */ - $storyIdList = $story->id . ($story->relationStoryID ? ',' . trim($story->relationStoryID, ',') : '') . ($story->twins ? ',' . trim($story->twins, ',') : ''); + $storyIdList = $story->id . ($story->twins ? ',' . trim($story->twins, ',') : ''); $story->bugs = $this->dao->select('*')->from(TABLE_BUG)->where('status')->ne('closed') ->andWhere('story')->in($storyIdList) ->andWhere('status')->ne('closed') @@ -1550,13 +1737,13 @@ class storyTao extends storyModel if(!isset($this->config->story->affect)) $this->config->story->affect = new stdclass(); $this->config->story->affect->cases = new stdclass(); $this->config->story->affect->cases->fields[] = array('name' => 'id', 'title' => $this->lang->idAB); - $this->config->story->affect->cases->fields[] = array('name' => 'title', 'title' => $this->lang->testcase->title, 'link' => $this->config->vision != 'or' ? helper::createLink('testcase', 'view', 'id={id}') : '', 'type' => 'title', 'fixed' => false, 'sortType' => false, 'flex' => false); + $this->config->story->affect->cases->fields[] = array('name' => 'title', 'title' => $this->lang->testcase->title, 'link' => $this->config->vision != 'or' ? helper::createLink('testcase', 'view', 'id={id}') : '', 'data-toggle' => 'modal', 'data-size' => 'lg', 'fixed' => false, 'sortType' => false, 'flex' => false); $this->config->story->affect->cases->fields[] = array('name' => 'status', 'title' => $this->lang->statusAB); $this->config->story->affect->cases->fields[] = array('name' => 'openedBy', 'title' => $this->lang->testcase->openedBy); $this->config->story->affect->cases->fields[] = array('name' => 'lastEditedBy', 'title' => $this->lang->testcase->lastEditedBy); /* Get affected cases. */ - $storyIdList = $story->id . ($story->relationStoryID ? ',' . trim($story->relationStoryID, ',') : '') . ($story->twins ? ',' . trim($story->twins, ',') : ''); + $storyIdList = $story->id . ($story->twins ? ',' . trim($story->twins, ',') : ''); $story->cases = $this->dao->select('*')->from(TABLE_CASE)->where('deleted')->eq(0) ->andWhere('story')->in($storyIdList) ->fetchAll(); @@ -1570,6 +1757,36 @@ class storyTao extends storyModel return $story; } + /** + * 获取该需求影响的子需求。 + * Get affected children for this story. + * + * @param object $story + * @param array $users + * @access protected + * @return object + */ + protected function getAffectedChildren(object $story, array $users): object + { + $storyType = $story->type; + foreach($story->children as $child) + { + $child->status = $this->processStatus('story', $child); + $child->openedBy = zget($users, $child->openedBy); + $storyType = $child->type; + } + + if(!isset($this->config->story->affect)) $this->config->story->affect = new stdclass(); + $this->config->story->affect->children = new stdclass(); + $this->config->story->affect->children->fields[] = array('name' => 'id', 'title' => $this->lang->idAB, 'type' => 'id', 'sortType' => false); + $this->config->story->affect->children->fields[] = array('name' => 'title', 'title' => $this->lang->story->name, 'link' => helper::createLink($storyType, 'view', 'id={id}'), 'type' => 'title', 'sortType' => false, 'data-toggle' => 'modal', 'data-size' => 'lg'); + $this->config->story->affect->children->fields[] = array('name' => 'pri', 'title' => $this->lang->priAB, 'type' => 'pri', 'sortType' => false); + $this->config->story->affect->children->fields[] = array('name' => 'status', 'title' => $this->lang->story->status, 'type' => 'status', 'sortType' => false); + $this->config->story->affect->children->fields[] = array('name' => 'openedBy', 'title' => $this->lang->story->openedBy, 'type' => 'user', 'sortType' => false); + + return $story; + } + /** * 获取该需求影响的孪生需求。 * Get affected twins for this story. @@ -1587,7 +1804,7 @@ class storyTao extends storyModel $this->config->story->affect->twins = new stdclass(); $this->config->story->affect->twins->fields[] = array('name' => 'id', 'title' => $this->lang->idAB); $this->config->story->affect->twins->fields[] = array('name' => 'branch', 'title' => $this->lang->story->branch); - $this->config->story->affect->twins->fields[] = array('name' => 'title', 'title' => $this->lang->story->title, 'link' => $this->config->vision != 'or' ? helper::createLink('story', 'view', 'id={id}') : '', 'type' => 'title', 'fixed' => false, 'sortType' => false, 'flex' => false); + $this->config->story->affect->twins->fields[] = array('name' => 'title', 'title' => $this->lang->story->title, 'link' => $this->config->vision != 'or' ? helper::createLink('story', 'view', 'id={id}') : '', 'data-toggle' => 'modal', 'data-size' => 'lg', 'fixed' => false, 'sortType' => false, 'flex' => false); $this->config->story->affect->twins->fields[] = array('name' => 'status', 'title' => $this->lang->statusAB); $this->config->story->affect->twins->fields[] = array('name' => 'stage', 'title' => $this->lang->story->stageAB); $this->config->story->affect->twins->fields[] = array('name' => 'openedBy', 'title' => $this->lang->story->openedBy); @@ -1615,10 +1832,11 @@ class storyTao extends storyModel * @param string $params * @param string $storyType story|requirement * @param object $execution + * @param array $maxGradeGroup * @access protected * @return array */ - protected function buildBrowseActionBtnList(object $story, string $params = '', string $storyType = 'story', object $execution = null): array + protected function buildBrowseActionBtnList(object $story, string $params = '', string $storyType = 'story', object $execution = null, array $maxGradeGroup = array()): array { global $lang; @@ -1634,15 +1852,6 @@ class storyTao extends storyModel } } - if($storyType == 'requirement') - { - $this->lang->story->changeTip = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->changeTip); - $this->lang->story->reviewTip['active'] = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->reviewTip['active']); - $this->lang->story->reviewTip['notReviewer'] = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->reviewTip['notReviewer']); - $this->lang->story->recallTip['actived'] = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->recallTip['actived']); - $this->lang->story->subDivideTip['notWait'] = str_replace($this->lang->SRCommon, $this->lang->URCommon, $this->lang->story->subDivideTip['notWait']); - } - static $taskGroups = array(); $actSubmitreview = array(); @@ -1652,36 +1861,36 @@ class storyTao extends storyModel $executionID = empty($execution) ? 0 : $execution->id; if(!isset($story->from)) $story->from = ''; - $closeLink = helper::createLink('story', 'close', $params . "&from=&storyType=$story->type"); - $processStoryChangeLink = helper::createLink('story', 'processStoryChange', $params); - $changeLink = helper::createLink('story', 'change', $params . "&from=$story->from&storyType=$story->type"); - $submitReviewLink = helper::createLink('story', 'submitReview', "storyID=$story->id&storyType=$story->type"); - $reviewLink = helper::createLink('story', 'review', $params . "&from=$story->from&storyType=$story->type") . ($this->app->tab == 'project' ? '#app=project' : ''); - $recallLink = helper::createLink('story', 'recall', $params . "&from=list&confirm=no&storyType=$story->type"); - $batchCreateStoryLink = helper::createLink('story', 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0&storyType=story"); - $editLink = helper::createLink('story', 'edit', $params . "&kanbanGroup=default&storyType=$story->type") . ($this->app->tab == 'project' ? '#app=project' : ''); - $createCaseLink = helper::createLink('testcase', 'create', "productID=$story->product&branch=$story->branch&module=0&from=¶m=0&$params"); + $closeLink = helper::createLink($story->type, 'close', $params . "&from=$story->from"); + $processStoryChangeLink = helper::createLink('story', 'processStoryChange', $params); + $changeLink = helper::createLink($story->type, 'change', $params . "&from=$story->from"); + $submitReviewLink = helper::createLink($story->type, 'submitReview', "storyID=$story->id"); + $reviewLink = helper::createLink($story->type, 'review', $params . "&from=$story->from") . ($this->app->tab == 'project' ? '#app=project' : ''); + $recallLink = helper::createLink($story->type, 'recall', $params . "&from=list&confirm=no"); + $batchCreateStoryLink = helper::createLink($story->type, 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0"); + $editLink = helper::createLink($story->type, 'edit', $params . "&kanbanGroup=default") . ($this->app->tab == 'project' ? '#app=project' : ''); + $createCaseLink = helper::createLink('testcase', 'create', "productID=$story->product&branch=$story->branch&module=0&from=¶m=0&$params"); /* If the story cannot be changed, render the close button. */ - $canClose = common::hasPriv('story', 'close') && $this->isClickable($story, 'close'); - if(!common::canBeChanged('story', $story)) return array(array('name' => 'close', 'hint' => $lang->close, 'data-toggle' => 'modal', 'url' => $canClose ? $closeLink : null, 'disabled' => !$canClose)); - $canProcess = common::hasPriv('story', 'processStoryChange'); - if($story->URChanged) return array(array('name' => 'processStoryChange', 'data-toggle' => 'modal', 'url' => $canProcess ? $processStoryChangeLink : null, 'disabled' => !$canProcess)); + $canClose = common::hasPriv($story->type, 'close') && $this->isClickable($story, 'close'); + if(!common::canBeChanged($story->type, $story)) return array(array('name' => 'close', 'hint' => $lang->close, 'data-toggle' => 'modal', 'url' => $canClose ? $closeLink : null, 'disabled' => !$canClose)); + $canProcessChange = common::hasPriv('story', 'processStoryChange'); + if(!empty($story->parentChanged)) return array(array('name' => 'processStoryChange', 'url' => $canProcessChange ? $processStoryChangeLink : null, 'disabled' => !$canProcessChange, 'innerClass' => 'ajax-submit')); /* Change button. */ - $canChange = common::hasPriv('story', 'change') && $this->isClickable($story, 'change'); + $canChange = common::hasPriv($story->type, 'change') && $this->isClickable($story, 'change'); $title = $canChange ? $lang->story->change : $this->lang->story->changeTip; $actions[] = array('name' => 'change', 'url' => $canChange ? $changeLink : null, 'hint' => $title, 'disabled' => !$canChange); /* Submitreview, review, recall buttons. */ if(strpos('draft,changing', $story->status) !== false) { - $canSubmitReview = common::hasPriv('story', 'submitReview'); + $canSubmitReview = common::hasPriv($story->type, 'submitReview'); $actSubmitreview = array('name' => 'submitreview', 'data-toggle' => 'modal', 'url' => $canSubmitReview ? $submitReviewLink : null); } else { - $canReview = common::hasPriv('story', 'review') && $this->isClickable($story, 'review'); + $canReview = common::hasPriv($story->type, 'review') && $this->isClickable($story, 'review'); $title = $this->lang->story->review; if(!$canReview && $story->status != 'closed') { @@ -1730,30 +1939,49 @@ class storyTao extends storyModel $actions[] = array('name' => 'divider', 'type'=>'divider'); /* Edit button. */ - $canEdit = common::hasPriv('story', 'edit') && $this->isClickable($story, 'edit'); + $canEdit = common::hasPriv($story->type, 'edit') && $this->isClickable($story, 'edit'); $actions[] = array('name' => 'edit', 'url' => $this->isClickable($story, 'edit') ? $editLink : null, 'disabled' => !$canEdit); /* Create test case button. */ - if($story->type != 'requirement' && $this->config->vision != 'lite') $actions[] = array('name' => 'testcase', 'url' => common::hasPriv('testcase', 'create') && $story->parent >= 0 ? $createCaseLink : null, 'disabled' => $story->parent < 0 || !common::hasPriv('testcase', 'create'), 'data-toggle' => 'modal', 'data-size' => 'lg'); + if($story->type == 'story' && $this->config->vision != 'lite') $actions[] = array('name' => 'testcase', 'url' => common::hasPriv('testcase', 'create') && $story->parent >= 0 ? $createCaseLink : null, 'disabled' => $story->parent < 0 || !common::hasPriv('testcase', 'create'), 'data-toggle' => 'modal', 'data-size' => 'lg'); /* Batch create button. */ $shadow = $this->dao->findByID($story->product)->from(TABLE_PRODUCT)->fetch('shadow'); - $canBatchCreateStory = $this->isClickable($story, 'batchcreate'); - if($this->app->rawModule != 'projectstory' || $story->type == 'requirement' || $this->config->vision == 'lite' || $shadow) + + $canBatchCreateStory = common::hasPriv($story->type, 'batchcreate') && $this->isClickable($story, 'batchcreate') && $story->grade < $maxGradeGroup[$story->type] && empty($story->hasOtherTypeChild); + if(!($this->app->rawModule == 'projectstory' && $this->app->rawMethod == 'story') || $this->config->vision == 'lite' || $shadow) { if($shadow and empty($taskGroups[$story->id])) $taskGroups[$story->id] = $this->dao->select('id')->from(TABLE_TASK)->where('story')->eq($story->id)->fetch('id'); - $title = $story->type == 'story' ? $this->lang->story->subdivideSR : $this->lang->story->subdivide; - if(!$canBatchCreateStory && $story->status != 'closed') + /* + * 需求的拆分按钮分为两种情况: + * 1.拆分成相同类型的子需求。 + * 2.拆分成其他类型的子需求。 + * 这两种情况是互斥的,拆分了一种类型的需求后,另一种类型的需求就不能再拆分了。 + * 默认拆分相同类型的需求,如果不能拆分相同类型的需求,则拆分其他类型的需求。 + */ + if($canBatchCreateStory) { - if(!in_array($story->status, array('active', 'launched', 'developing'))) $title = sprintf($this->lang->story->subDivideTip['notActive'], $story->type == 'story' ? $this->lang->SRCommon : $this->lang->URCommon); + $actions[] = array('name' => 'batchCreate', 'url' => $batchCreateStoryLink, 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif($story->type == 'epic' && common::hasPriv('requirement', 'batchCreate') && $this->isClickable($story, 'batchcreate') && empty($story->hasSameTypeChild) && !($this->config->epic->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['epic'])) + { + $actions[] = array('name' => 'batchCreate', 'url' => helper::createLink('requirement', 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0"), 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif($story->type == 'requirement' && common::hasPriv('story', 'batchCreate') && $this->isClickable($story, 'batchcreate') && empty($story->hasSameTypeChild) && !($this->config->requirement->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['requirement'])) + { + $actions[] = array('name' => 'batchCreate', 'url' => helper::createLink('story', 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0"), 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif(!$canBatchCreateStory && $story->status != 'closed' && common::hasPriv($story->type, 'batchcreate')) + { + $title = $this->lang->story->split; + if($story->status != 'active') $title = $this->lang->story->subDivideTip['notActive']; if($story->status == 'active' && $story->stage != 'wait') $title = sprintf($this->lang->story->subDivideTip['notWait'], zget($this->lang->story->stageList, $story->stage)); if(!empty($story->twins)) $title = $this->lang->story->subDivideTip['twinsSplit']; - if($story->parent > 0) $title = $this->lang->story->subDivideTip['subStory']; if($story->status == 'active' and !empty($taskGroups[$story->id])) $title = sprintf($this->lang->story->subDivideTip['notWait'], $this->lang->story->hasDividedTask); + if($story->grade >= $maxGradeGroup[$story->type]) $title = $this->lang->story->errorMaxGradeSubdivide; + $actions[] = array('name' => 'batchCreate', 'hint' => $title, 'disabled' => true, 'icon' => 'split'); } - - $actions[] = array('name' => 'batchCreate', 'url' => $canBatchCreateStory ? $batchCreateStoryLink : null, 'hint' => $title, 'disabled' => !$canBatchCreateStory); } if(!empty($execution)) @@ -1764,9 +1992,9 @@ class storyTao extends storyModel $batchCreateTaskLink = helper::createLink('task', 'batchCreate', "executionID={$execution->id}&storyID={$story->id}"); $storyEstimateLink = helper::createLink('execution', 'storyEstimate', "executionID={$execution->id}&storyID={$story->id}"); - $canCreateTask = common::hasPriv('task', 'create') && $story->status == 'active'; - $canBatchCreateTask = common::hasPriv('task', 'batchCreate') && $story->status == 'active'; - $canStoryEstimate = common::hasPriv('execution', 'storyEstimate'); + $canCreateTask = common::hasPriv('task', 'create') && $story->status == 'active' && $story->isParent == '0' && $story->type == 'story'; + $canBatchCreateTask = common::hasPriv('task', 'batchCreate') && $story->status == 'active' && $story->isParent == '0' && $story->type == 'story'; + $canStoryEstimate = common::hasPriv('execution', 'storyEstimate') && $story->type == 'story'; $actions[] = array('name' => 'createTask', 'url' => $canCreateTask ? $createTaskLink : null, 'disabled' => !$canCreateTask, 'className' => 'create-task-btn'); $actions[] = array('name' => 'batchCreateTask', 'url' => $canBatchCreateTask ? $batchCreateTaskLink : null, 'disabled' => !$canBatchCreateTask); @@ -1798,6 +2026,30 @@ class storyTao extends storyModel $disabled = true; $unlinkTitle = $this->lang->execution->notAllowedUnlinkStory; } + + $canBatchCreateStory = common::hasPriv($story->type, 'batchcreate') && $this->isClickable($story, 'batchcreate') && $story->grade < $maxGradeGroup[$story->type] && empty($story->hasOtherTypeChild); + if($canBatchCreateStory) + { + $actions[] = array('name' => 'batchCreate', 'url' => $batchCreateStoryLink, 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif($story->type == 'epic' && common::hasPriv('requirement', 'batchCreate') && $this->isClickable($story, 'batchcreate') && empty($story->hasSameTypeChild) && !($this->config->epic->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['epic'])) + { + $actions[] = array('name' => 'batchCreate', 'url' => helper::createLink('requirement', 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0"), 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif($story->type == 'requirement' && common::hasPriv('story', 'batchCreate') && $this->isClickable($story, 'batchcreate') && empty($story->hasSameTypeChild) && !($this->config->requirement->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['requirement'])) + { + $actions[] = array('name' => 'batchCreate', 'url' => helper::createLink('story', 'batchCreate', "productID=$story->product&branch=$story->branch&module=$story->module&$params&executionID=$executionID&plan=0"), 'hint' => $this->lang->story->split, 'icon' => 'split'); + } + elseif(!$canBatchCreateStory && $story->status != 'closed' && common::hasPriv($story->type, 'batchcreate')) + { + $title = $this->lang->story->split; + if($story->status != 'active') $title = $this->lang->story->subDivideTip['notActive']; + if($story->status == 'active' && $story->stage != 'wait') $title = sprintf($this->lang->story->subDivideTip['notWait'], zget($this->lang->story->stageList, $story->stage)); + if(!empty($story->twins)) $title = $this->lang->story->subDivideTip['twinsSplit']; + if($story->status == 'active' and !empty($taskGroups[$story->id])) $title = sprintf($this->lang->story->subDivideTip['notWait'], $this->lang->story->hasDividedTask); + if($story->grade >= $maxGradeGroup[$story->type]) $title = $this->lang->story->errorMaxGradeSubdivide; + $actions[] = array('name' => 'batchCreate', 'hint' => $title, 'disabled' => true, 'icon' => 'split'); + } } if($story->type == 'requirement') $unlinkStoryTip = str_replace($this->lang->SRCommon, $this->lang->URCommon, $unlinkStoryTip); @@ -1818,7 +2070,8 @@ class storyTao extends storyModel */ protected function isSuperReviewer(): bool { - return str_contains(',' . zget($this->config->story, 'superReviewers', '') . ',', ",{$this->app->user->account},"); + $moduleName = $this->app->rawModule; + return str_contains(',' . zget($this->config->{$moduleName}, 'superReviewers', '') . ',', ",{$this->app->user->account},"); } /** @@ -1839,7 +2092,6 @@ class storyTao extends storyModel $now = helper::now(); $story = new stdclass(); $story->status = $status; - $story->stage = 'wait'; if(strpos('launched,active,changing,draft', $status) !== false) { $story->assignedTo = $oldParentStory->openedBy; @@ -1853,19 +2105,294 @@ class storyTao extends storyModel if($status == 'closed') { + $closedReason = $this->dao->select('closedReason')->from(TABLE_STORY) + ->where('parent')->eq($parentID) + ->andWhere('deleted')->eq(0) + ->andWhere('closedReason')->eq('done') + ->fetch('closedReason'); + $story->assignedTo = 'closed'; $story->assignedDate = $now; $story->closedBy = $this->app->user->account; $story->closedDate = $now; - $story->closedReason = 'done'; - $story->closedReason = 'done'; + $story->closedReason = $closedReason ? 'done' : 'willnotdo'; } $story->lastEditedBy = $this->app->user->account; $story->lastEditedDate = $now; - $story->parent = '-1'; $this->dao->update(TABLE_STORY)->data($story)->where('id')->eq($parentID)->exec(); return $story; } + + /** + * 获取传入所有需求的叶子结点,并按照根节点顺序返回 + * Get leaf node by all stories, and sort by base nodes. + * + * @param array $stories + * @param array $allStoryIdList + * @access public + * @return array + */ + public function getLeafNodes(array $stories, array $allStoryIdList): array + { + $parent = array(); + $leafNodes = array(); + $stmt = $this->dao->select('id,parent,root,path,grade,product,pri,type,status,stage,title,estimate')->from(TABLE_STORY)->where('id')->in($allStoryIdList)->andWhere('deleted')->eq(0)->orderBy('type_desc,grade_desc,id_desc')->query(); + while($story = $stmt->fetch()) + { + $parent[$story->parent] = true; + + if(isset($parent[$story->id])) continue; + $leafNodes[$story->root][$story->id] = $story; + } + + $sortLeafNodes = array(); + foreach($stories as $story) + { + if(isset($leafNodes[$story->root])) $sortLeafNodes += $leafNodes[$story->root]; + } + + return $sortLeafNodes; + } + + /** + * 根据叶子结点数据,构建看板泳道数据。 + * Build lanes data by leaf node. + * + * @param array $leafNodes + * @param string $storyType epic|requirement|story + * @access public + * @return array + */ + public function buildTrackLanes(array $leafNodes, string $storyType): array + { + $lanes = array(); + foreach($leafNodes as $story) + { + if(($storyType == 'requirement' || $storyType == 'story') && $story->type == 'epic') continue; + if($storyType == 'story' && $story->type == 'requirement') continue; + $lanes[] = array('name' => "lane_{$story->id}", 'title' => ''); + } + return $lanes; + } + + /** + * 根据需求类型,构建看板列数据。 + * Build cols data by storyType. + * + * @param string $storyType epic|requirement|story + * @access public + * @return array + */ + public function buildTrackCols(string $storyType): array + { + $storyGrade = $this->getGradeGroup(); + + $cols = array(); + if($storyType == 'epic') $cols['epic'] = $this->buildTrackCol('epic', $this->lang->ERCommon, empty($storyGrade['epic']) ? 0 : -1); + if($storyType != 'story') $cols['requirement'] = $this->buildTrackCol('requirement', $this->lang->URCommon, empty($storyGrade['requirement']) ? 0 : -1); + + $cols['story'] = $this->buildTrackCol('story', $this->lang->SRCommon, empty($storyGrade['story']) ? 0 : -1); + $cols['project'] = $this->buildTrackCol('project', $this->lang->story->project); + $cols['execution'] = $this->buildTrackCol('execution', $this->lang->story->execution); + $cols['design'] = $this->buildTrackCol('design', $this->lang->story->design); + $cols['commit'] = $this->buildTrackCol('commit', $this->lang->story->repoCommit); + $cols['task'] = $this->buildTrackCol('task', $this->lang->story->tasks); + $cols['bug'] = $this->buildTrackCol('bug', $this->lang->story->bugs); + $cols['case'] = $this->buildTrackCol('case', $this->lang->story->cases); + + foreach($storyGrade as $type => $grades) + { + if(!isset($cols[$type])) continue; + foreach($grades as $grade) $cols["{$type}_{$grade->grade}"] = $this->buildTrackCol("{$type}_{$grade->grade}", $grade->name, $type); + } + + return array_values($cols); + } + + /** + * 根据单个看板列数据。 + * Build single col data. + * + * @param string $name + * @param string $title + * @param int|string $parent + * @access public + * @return array + */ + public function buildTrackCol(string $name, string $title, int|string $parent = 0): array + { + $col = array('name' => $name, 'title' => $title, 'parent' => $parent); + if($parent != '0' && $parent != -1) $col['parentName'] = $parent; + return $col; + } + + /** + * 构建看板项数据。 + * Build items data by storyType. + * + * @param array $allStories + * @param array $leafNodes + * @param string $storyType epic|requirement|story + * @access public + * @return array + */ + public function buildTrackItems(array $allStories, array $leafNodes, string $storyType): array + { + $storyIdList = array_keys($leafNodes); + $projectGroup = $this->getProjectsForTrack($storyIdList); + $designGroup = $this->getDesignsForTrack($storyIdList); + + $projects = zget($projectGroup, 'project', array()); + $executions = zget($projectGroup, 'execution', array()); + $designs = zget($designGroup, 'design', array()); + $commits = zget($designGroup, 'commit', array()); + $tasks = $this->getTasksForTrack($storyIdList); + $cases = $this->dao->select('id,project,pri,status,color,title,story,lastRunner,lastRunResult')->from(TABLE_CASE)->where('story')->in($storyIdList)->andWhere('deleted')->eq(0)->orderBy('project')->fetchGroup('story', 'id'); + $bugs = $this->dao->select('id,project,pri,status,color,title,story,assignedTo,severity')->from(TABLE_BUG)->where('story')->in($storyIdList)->andWhere('deleted')->eq(0)->orderBy('project')->fetchGroup('story', 'id'); + $storyGrade = $this->getGradeGroup(); + + $items = array(); + foreach($leafNodes as $node) + { + $laneName = "lane_{$node->id}"; + foreach(explode(',', trim($node->path, ',')) as $storyID) + { + if(!isset($allStories[$storyID])) continue; + $story = $allStories[$storyID]; + + unset($allStories[$storyID]); + if($storyType == 'requirement' && $story->type == 'epic') continue; + if($storyType == 'story' && ($story->type == 'requirement' || $story->type == 'epic')) continue; + if(!isset($storyGrade[$story->type][$story->grade])) continue; + + $colName = "{$story->type}_{$story->grade}"; + $story->storyType = $story->type; + unset($story->type); + + $items[$laneName][$colName][] = $story; + } + $items[$laneName]['project'] = array_values(zget($projects, $node->id, array())); + $items[$laneName]['execution'] = array_values(zget($executions, $node->id, array())); + $items[$laneName]['design'] = array_values(zget($designs, $node->id, array())); + $items[$laneName]['commit'] = array_values(zget($commits, $node->id, array())); + $items[$laneName]['task'] = array_values(zget($tasks, $node->id, array())); + $items[$laneName]['bug'] = array_values(zget($bugs, $node->id, array())); + $items[$laneName]['case'] = array_values(zget($cases, $node->id, array())); + } + + return $items; + } + + /** + * 根据需求ID列表获取关联的项目和执行 + * Get linked projects and executions by story id list. + * + * @param array $storyIdList + * @access public + * @return array + */ + public function getProjectsForTrack(array $storyIdList): array + { + $projectStoryList = array(); + $stmt = $this->dao->select('*')->from(TABLE_PROJECTSTORY)->where('story')->in($storyIdList)->query(); + while($projectStory = $stmt->fetch()) $projectStoryList[$projectStory->project][$projectStory->story] = $projectStory->story; + + $projects = array(); + $executions = array(); + $stmt = $this->dao->select('id,type AS projectType,model,parent,path,grade,name as title,hasProduct,begin,end,status,project,progress')->from(TABLE_PROJECT)->where('id')->in(array_keys($projectStoryList))->andWhere('deleted')->eq(0)->orderBy('id')->query(); + $today = helper::today(); + $storyGroup = array(); + while($project = $stmt->fetch()) + { + $delay = 0; + if($project->status != 'done' && $project->status != 'closed' && $project->status != 'suspended') $delay = helper::diffDate($today, $project->end); + + $project->delay = $delay > 0; + $projectType = $project->projectType == 'project' ? 'project' : 'execution'; + foreach($projectStoryList[$project->id] as $storyID) $storyGroup[$projectType][$storyID][$project->id] = $project; + } + + return $storyGroup; + } + + /** + * 根据需求ID列表获取关联的设计和提交 + * Get linked designs and commits by story id list. + * + * @param array $storyIdList + * @access public + * @return array + */ + public function getDesignsForTrack(array $storyIdList): array + { + $storyGroup = array('design' => array(), 'commit' => array()); + $stmt = $this->dao->select('id,project,commit,name as title,status,story,type AS designType')->from(TABLE_DESIGN)->where('story')->in($storyIdList)->andWhere('deleted')->eq(0)->orderBy('project')->query(); + $commitIdList = ''; + $commitGroup = array(); + while($design = $stmt->fetch()) + { + $storyGroup['design'][$design->story][$design->id] = $design; + $commitIdList .= $design->commit ? "{$design->commit}," : ''; + } + + $commits = $this->dao->select('id,repo,revision,committer,comment as title')->from(TABLE_REPOHISTORY)->where('id')->in(array_unique(explode(',', $commitIdList)))->fetchAll('id'); + foreach($storyGroup['design'] as $storyID => $designs) + { + foreach($designs as $designID => $design) + { + if(empty($design->commit)) continue; + foreach(explode(',', $design->commit) as $commitID) + { + $commit = zget($commits, $commitID, ''); + if(empty($commit)) continue; + + $commit->project = $design->project; + $storyGroup['commit'][$storyID][$commitID] = $commit; + } + } + } + + return $storyGroup; + } + + /** + * 根据需求ID列表获取关联的任务 + * Get linked tasks by story id list. + * + * @param array $storyIdList + * @access public + * @return array + */ + public function getTasksForTrack(array $storyIdList): array + { + $stmt = $this->dao->select('id,project,execution,pri,status,color,name as title,assignedTo,story,estimate,consumed,`left`,parent')->from(TABLE_TASK)->where('story')->in($storyIdList)->andWhere('deleted')->eq(0)->orderBy('project,execution')->query(); + $tasks = array(); + while($task = $stmt->fetch()) + { + $task->progress = 0; + if($task->consumed + $task->left) $task->progress = round(($task->consumed / ($task->consumed + $task->left)) * 100, 2); + + if($task->parent > 0 && isset($tasks[$task->parent])) + { + $tasks[$task->parent]->children[$task->id] = $task; + } + else + { + $tasks[$task->id] = $task; + } + } + + $taskGroup = array(); + foreach($tasks as $task) + { + $children = !empty($task->children) ? $task->children : array(); + unset($task->children); + + $taskGroup[$task->story][$task->id] = $task; + foreach($children as $subTask) $taskGroup[$subTask->story][$subTask->id] = $subTask; + } + return $taskGroup; + } } diff --git a/module/story/test/lib/story.unittest.class.php b/module/story/test/lib/story.unittest.class.php index f1b91168bd..a7e217add7 100644 --- a/module/story/test/lib/story.unittest.class.php +++ b/module/story/test/lib/story.unittest.class.php @@ -383,22 +383,15 @@ class storyTest * @access public * @return object|array */ - public function subdivideTest(int $storyID, array $stories, string $type): object|array + public function subdivideTest(int $storyID, array $stories): object|array { - $this->objectModel->dao->delete()->from(TABLE_RELATION)->exec(); $this->objectModel->subdivide($storyID, $stories); if(dao::isError()) return dao::getError(); - global $tester; - if($type == 'requirement') - { - return $tester->dao->select('*')->from(TABLE_RELATION)->fetchAll(); - } - else - { - return $this->objectModel->getById($storyID); - } + $story = $this->objectModel->getById($storyID); + $story->children = $this->objectModel->dao->select('*')->from(TABLE_STORY)->where('parent')->eq($storyID)->fetchAll(); + return $story; } /** @@ -411,6 +404,9 @@ class storyTest */ public function closeTest(int $storyID, object $postData) { + global $tester; + $tester->loadModel('requirement'); + $tester->loadModel('epic'); $this->objectModel->close($storyID, $postData); if(dao::isError()) return dao::getError(); @@ -682,7 +678,7 @@ class storyTest */ public function getExecutionStoriesBySearchTest(int $executionID, int $queryID, int $productID, array $excludeStories = array(), object|null $pager = null): int { - $stories = $this->objectModel->getExecutionStoriesBySearch($executionID, $queryID, $productID, 't2.id_desc', 'story', $excludeStories, $pager); + $stories = $this->objectModel->getExecutionStoriesBySearch($executionID, $queryID, $productID, 't2.id_desc', 'story', '', $excludeStories, $pager); return count($stories); } @@ -764,24 +760,6 @@ class storyTest return array_filter((array)$this->objectModel->dao->select('*')->from(TABLE_ACTION)->orderBy('id_desc')->limit(1)->fetch()); } - /** - * 测试 doCreateURRelations 方法。 - * Test doCreateURRelations method. - * - * @param int $storyID - * @param array $URList - * @access public - * @return array - */ - public function doCreateURRelationsTest(int $storyID, array $URList): array - { - $this->objectModel->dao->delete()->from(TABLE_RELATION)->exec(); - $this->objectModel->doCreateURRelations($storyID, $URList); - - if(dao::isError()) return dao::getError(); - return $this->objectModel->dao->select('*')->from(TABLE_RELATION)->fetchAll(); - } - /** * 测试 doCreateReviewer 方法。 * Test doCreateReviewer method. @@ -978,23 +956,6 @@ class storyTest return $this->objectModel->fetchBaseInfo($storyID); } - /** - * 测试 updateStoryVersion 方法 - * Test updateStoryVersion method - * - * @param int $storyID - * @access public - * @return object - */ - public function updateStoryVersionTest(int $storyID): object - { - $story = $this->objectModel->fetchByID($storyID); - $this->objectModel->dao->update(TABLE_STORY)->set('version')->eq(2)->where('id')->eq(3)->exec(); - $this->objectModel->updateStoryVersion($story); - - return $this->objectModel->dao->select('*')->from(TABLE_RELATION)->where('AType')->eq('requirement')->andWhere('BType')->eq('story')->andWhere('relation')->eq('subdivideinto')->andWhere('AID')->eq(3)->andWhere('BID')->eq($storyID)->fetch(); - } - /** * 测试 syncTwins 方法。 * Test syncTwins method diff --git a/module/story/test/model/addgradelabel.php b/module/story/test/model/addgradelabel.php new file mode 100755 index 0000000000..a8e98f9fc2 --- /dev/null +++ b/module/story/test/model/addgradelabel.php @@ -0,0 +1,28 @@ +#!/usr/bin/env php +addGradeLabel(); +timeout=0 +cid=0 + +- 查看是否有SR2标签第text条的html属性 @SR2 软件需求2 +- 查看是否有SR2标签第text条的html属性 @SR2 软件需求4 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/story.unittest.class.php'; +su('admin'); + +$story = zenData('story'); +$story->version->range(1); +$story->gen(5); +zenData('storyspec')->gen(5); + +global $tester; +$stories = $tester->loadModel('story')->getProductStories(1); +$stories = $tester->story->addGradeLabel($stories); + +r($stories[0]) && p('text:html') && e("SR2 软件需求2"); // 查看是否有SR2标签 +r($stories[1]) && p('text:html') && e("SR2 软件需求4"); // 查看是否有SR2标签 \ No newline at end of file diff --git a/module/story/test/model/batchchangeplan.php b/module/story/test/model/batchchangeplan.php index a6fe32c9ed..fb1ad44615 100755 --- a/module/story/test/model/batchchangeplan.php +++ b/module/story/test/model/batchchangeplan.php @@ -4,22 +4,22 @@ /** title=测试 storyModel->batchChangePlan(); +timeout=0 cid=0 - 批量修改6个需求的计划,查看被修改计划的需求数量 @4 -- 批量修改6个需求的计划,查看被修改计划的需求数量 @4 +- 批量修改6个需求的计划,查看被修改计划的需求数量 @3 - 批量修改6个需求的计划,查看需求1修改后的计划ID第1条的plan属性 @10 - 批量修改6个需求的计划,查看需求2修改后的计划ID第2条的plan属性 @10 - 批量修改6个需求的计划,查看需求4修改后的计划ID第4条的plan属性 @2,10 - 批量修改6个需求的计划,查看需求5修改后的计划ID第5条的plan属性 @10 -- 批量修改6个需求的计划,查看需求1修改后的计划ID第1条的plan属性 @9 +- 批量修改6个需求的计划,查看需求1修改后的计划ID第1条的plan属性 @10,9 - 批量修改6个需求的计划,查看需求2修改后的计划ID第2条的plan属性 @9 -- 批量修改6个需求的计划,查看需求4修改后的计划ID第4条的plan属性 @9 -- 批量修改6个需求的计划,查看需求5修改后的计划ID第5条的plan属性 @9 -- 批量修改6个需求的计划,查看需求1修改后的计划ID第1条的plan属性 @5 +- 批量修改6个需求的计划,查看需求4修改后的计划ID第4条的plan属性 @2,10 +- 批量修改6个需求的计划,查看需求1修改后的计划ID第1条的plan属性 @10,9,5 - 批量修改6个需求的计划,查看需求2修改后的计划ID第2条的plan属性 @5 -- 批量修改6个需求的计划,查看需求4修改后的计划ID第4条的plan属性 @5 -- 批量修改6个需求的计划,查看需求5修改后的计划ID第5条的plan属性 @5 +- 批量修改6个需求的计划,查看需求4修改后的计划ID第4条的plan属性 @2,10,5 +- 批量修改6个需求的计划,查看需求5修改后的计划ID第5条的plan属性 @10,5 */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -50,17 +50,16 @@ $stories1 = $story->batchChangePlanTest($storyIdList, 10, 11); $stories2 = $story->batchChangePlanTest($storyIdList, 9); $stories3 = $story->batchChangePlanTest($storyIdList, 5, 9); -r(count($stories1)) && p() && e('4'); // 批量修改6个需求的计划,查看被修改计划的需求数量 -r(count($stories2)) && p() && e('4'); // 批量修改6个需求的计划,查看被修改计划的需求数量 -r($stories1) && p('1:plan') && e('10'); // 批量修改6个需求的计划,查看需求1修改后的计划ID -r($stories1) && p('2:plan') && e('10'); // 批量修改6个需求的计划,查看需求2修改后的计划ID -r($stories1) && p('4:plan', '|') && e('2,10'); // 批量修改6个需求的计划,查看需求4修改后的计划ID -r($stories1) && p('5:plan') && e('10'); // 批量修改6个需求的计划,查看需求5修改后的计划ID -r($stories2) && p('1:plan') && e('9'); // 批量修改6个需求的计划,查看需求1修改后的计划ID -r($stories2) && p('2:plan') && e('9'); // 批量修改6个需求的计划,查看需求2修改后的计划ID -r($stories2) && p('4:plan') && e('9'); // 批量修改6个需求的计划,查看需求4修改后的计划ID -r($stories2) && p('5:plan') && e('9'); // 批量修改6个需求的计划,查看需求5修改后的计划ID -r($stories3) && p('1:plan') && e('5'); // 批量修改6个需求的计划,查看需求1修改后的计划ID -r($stories3) && p('2:plan') && e('5'); // 批量修改6个需求的计划,查看需求2修改后的计划ID -r($stories3) && p('4:plan') && e('5'); // 批量修改6个需求的计划,查看需求4修改后的计划ID -r($stories3) && p('5:plan') && e('5'); // 批量修改6个需求的计划,查看需求5修改后的计划ID +r(count($stories1)) && p() && e('4'); // 批量修改6个需求的计划,查看被修改计划的需求数量 +r(count($stories2)) && p() && e('3'); // 批量修改6个需求的计划,查看被修改计划的需求数量 +r($stories1) && p('1:plan') && e('10'); // 批量修改6个需求的计划,查看需求1修改后的计划ID +r($stories1) && p('2:plan') && e('10'); // 批量修改6个需求的计划,查看需求2修改后的计划ID +r($stories1) && p('4:plan', '|') && e('2,10'); // 批量修改6个需求的计划,查看需求4修改后的计划ID +r($stories1) && p('5:plan') && e('10'); // 批量修改6个需求的计划,查看需求5修改后的计划ID +r($stories2) && p('1:plan', '|') && e('10,9'); // 批量修改6个需求的计划,查看需求1修改后的计划ID +r($stories2) && p('2:plan') && e('9'); // 批量修改6个需求的计划,查看需求2修改后的计划ID +r($stories2) && p('4:plan', '|') && e('2,10'); // 批量修改6个需求的计划,查看需求4修改后的计划ID +r($stories3) && p('1:plan', '|') && e('10,9,5'); // 批量修改6个需求的计划,查看需求1修改后的计划ID +r($stories3) && p('2:plan') && e('5'); // 批量修改6个需求的计划,查看需求2修改后的计划ID +r($stories3) && p('4:plan', '|') && e('2,10,5'); // 批量修改6个需求的计划,查看需求4修改后的计划ID +r($stories3) && p('5:plan', '|') && e('10,5'); // 批量修改6个需求的计划,查看需求5修改后的计划ID \ No newline at end of file diff --git a/module/story/test/model/batchgetexecutionstories.php b/module/story/test/model/batchgetexecutionstories.php old mode 100644 new mode 100755 index 6089070ebc..4930042b2c --- a/module/story/test/model/batchgetexecutionstories.php +++ b/module/story/test/model/batchgetexecutionstories.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->batchGetExecutionStories(); +timeout=0 cid=0 - 不传入数据。 @0 @@ -12,7 +13,7 @@ cid=0 - 传入有关联需求的项目数据,传入无需求的产品数据。 @0 - 传入有关联需求的项目数据,不传入产品数据。 @25 - 传入有关联需求的项目数据,传入有需求的产品数据。 @25 -- 传入有关联需求的项目数据,传入无需求的产品数据。 @25 +- 传入有关联需求的项目数据,传入无需求的产品数据。 @0 - 传入分页。 @5 */ @@ -42,10 +43,10 @@ r(count($storyModel->batchGetExecutionStories('11,12', 1))) && p() && e('25'); / r(count($storyModel->batchGetExecutionStories('11,12', 2))) && p() && e('0'); //传入有关联需求的项目数据,传入无需求的产品数据。 r(count($storyModel->batchGetExecutionStories('36,37', 0))) && p() && e('25'); //传入有关联需求的项目数据,不传入产品数据。 r(count($storyModel->batchGetExecutionStories('36,37', 1))) && p() && e('25'); //传入有关联需求的项目数据,传入有需求的产品数据。 -r(count($storyModel->batchGetExecutionStories('36,37', 2))) && p() && e('25'); //传入有关联需求的项目数据,传入无需求的产品数据。 +r(count($storyModel->batchGetExecutionStories('36,37', 2))) && p() && e('0'); //传入有关联需求的项目数据,传入无需求的产品数据。 $storyModel->app->loadClass('pager', $static = true); $storyModel->app->moduleName = 'product'; $storyModel->app->methodName = 'track'; $pager = new pager(0, 5, 1); -r(count($storyModel->batchGetExecutionStories('36,37', 1, 't1.`order`_desc', 'byModule', '0', 'story', '', $pager))) && p() && e('5'); //传入分页。 +r(count($storyModel->batchGetExecutionStories('36,37', 1, 't1.`order`_desc', 'byModule', '0', 'story', '', $pager))) && p() && e('5'); //传入分页。 \ No newline at end of file diff --git a/module/story/test/model/batchreview.php b/module/story/test/model/batchreview.php index 224035ce32..00b98e55e8 100755 --- a/module/story/test/model/batchreview.php +++ b/module/story/test/model/batchreview.php @@ -32,7 +32,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/buildactionbuttonlist.php b/module/story/test/model/buildactionbuttonlist.php old mode 100644 new mode 100755 index 95e2d982a4..30a3beab1a --- a/module/story/test/model/buildactionbuttonlist.php +++ b/module/story/test/model/buildactionbuttonlist.php @@ -4,45 +4,45 @@ /** title=测试 storyModel->buildactionbuttonlist(); +timeout=0 cid=0 - 查看用户需求列表操作菜单第一个链接 - 第0条的name属性 @change - - 第0条的hint属性 @只有激活状态的用户需求,才能进行变更 + - 第0条的hint属性 @只有激活状态的需求,才能进行变更 - 第0条的disabled属性 @1 - 查看用户需求列表操作菜单第二个链接 - 第1条的name属性 @submitreview - 第1条的data-toggle属性 @modal - - 第1条的url属性 @/story-submitReview-1-requirement.html + - 第1条的url属性 @/requirement-submitReview-1.html - 查看用户需求列表操作菜单第四个链接 - 第3条的name属性 @close - 第3条的data-toggle属性 @modal - - 第3条的url属性 @/story-close-1--requirement.html + - 第3条的url属性 @/requirement-close-1-.html - 查看用户需求列表操作菜单第六个链接 - 第5条的name属性 @edit - - 第5条的url属性 @/story-edit-1-default-requirement.html + - 第5条的url属性 @/requirement-edit-1-default.html - 查看软件需求详情操作菜单第一个链接 - 第0条的name属性 @change - 第0条的hint属性 @变更 - - 第0条的url属性 @/story-change-2--story.html + - 第0条的url属性 @/story-change-2-.html - 查看软件需求详情操作菜单第二个链接 - 第1条的name属性 @review - - 第1条的hint属性 @该用户需求已是激活状态,无需评审 + - 第1条的hint属性 @该需求已是激活状态,无需评审 - 第1条的disabled属性 @1 - 查看软件需求详情操作菜单第四个链接 - 第3条的name属性 @close - 第3条的data-toggle属性 @modal - - 第3条的url属性 @/story-close-2--story.html + - 第3条的url属性 @/story-close-2-.html - 查看软件需求详情操作菜单第六个链接 - 第5条的name属性 @edit - - 第5条的url属性 @/story-edit-2-default-story.html + - 第5条的url属性 @/story-edit-2-default.html - 查看软件需求详情操作菜单第七个链接 - 第6条的name属性 @testcase - 第6条的url属性 @/testcase-create-1-0-0--0-2.html - 查看软件需求详情操作菜单第九个链接 - - 第8条的name属性 @unlink - - 第8条的className属性 @ajax-submit - - 第8条的url属性 @/execution-unlinkStory-11-2-yes.html + - 第8条的name属性 @batchCreate + - 第8条的className属性 @~~ */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -77,15 +77,15 @@ $execution->multiple = 0; $tester->story->app->tab = 'execution'; $storyOperateMenu2 = $tester->story->buildactionbuttonlist($story, 'browse', $execution, 'story'); -r($requirementOperateMenu1) && p('0:name,hint,disabled') && e('change,只有激活状态的用户需求,才能进行变更,1'); //查看用户需求列表操作菜单第一个链接 -r($requirementOperateMenu1) && p('1:name,data-toggle,url') && e('submitreview,modal,/story-submitReview-1-requirement.html'); //查看用户需求列表操作菜单第二个链接 -r($requirementOperateMenu1) && p('3:name,data-toggle,url') && e('close,modal,/story-close-1--requirement.html'); //查看用户需求列表操作菜单第四个链接 -r($requirementOperateMenu1) && p('5:name,url') && e('edit,/story-edit-1-default-requirement.html'); //查看用户需求列表操作菜单第六个链接 +r($requirementOperateMenu1) && p('0:name,hint,disabled') && e('change,只有激活状态的需求,才能进行变更,1'); //查看用户需求列表操作菜单第一个链接 +r($requirementOperateMenu1) && p('1:name,data-toggle,url') && e('submitreview,modal,/requirement-submitReview-1.html'); //查看用户需求列表操作菜单第二个链接 +r($requirementOperateMenu1) && p('3:name,data-toggle,url') && e('close,modal,/requirement-close-1-.html'); //查看用户需求列表操作菜单第四个链接 +r($requirementOperateMenu1) && p('5:name,url') && e('edit,/requirement-edit-1-default.html'); //查看用户需求列表操作菜单第六个链接 -r($storyOperateMenu1) && p('0:name,hint,url') && e('change,变更,/story-change-2--story.html'); //查看软件需求详情操作菜单第一个链接 -r($storyOperateMenu1) && p('1:name,hint,disabled') && e('review,该用户需求已是激活状态,无需评审,1'); //查看软件需求详情操作菜单第二个链接 -r($storyOperateMenu1) && p('3:name,data-toggle,url') && e('close,modal,/story-close-2--story.html'); //查看软件需求详情操作菜单第四个链接 -r($storyOperateMenu1) && p('5:name,url') && e('edit,/story-edit-2-default-story.html'); //查看软件需求详情操作菜单第六个链接 -r($storyOperateMenu1) && p('6:name,url') && e('testcase,/testcase-create-1-0-0--0-2.html'); //查看软件需求详情操作菜单第七个链接 +r($storyOperateMenu1) && p('0:name,hint,url') && e('change,变更,/story-change-2-.html'); //查看软件需求详情操作菜单第一个链接 +r($storyOperateMenu1) && p('1:name,hint,disabled') && e('review,该需求已是激活状态,无需评审,1'); //查看软件需求详情操作菜单第二个链接 +r($storyOperateMenu1) && p('3:name,data-toggle,url') && e('close,modal,/story-close-2-.html'); //查看软件需求详情操作菜单第四个链接 +r($storyOperateMenu1) && p('5:name,url') && e('edit,/story-edit-2-default.html'); //查看软件需求详情操作菜单第六个链接 +r($storyOperateMenu1) && p('6:name,url') && e('testcase,/testcase-create-1-0-0--0-2.html'); //查看软件需求详情操作菜单第七个链接 -r($storyOperateMenu2) && p('8:name,className,url') && e('unlink,ajax-submit,/execution-unlinkStory-11-2-yes.html'); //查看软件需求详情操作菜单第九个链接 +r($storyOperateMenu2) && p('8:name,className') && e('batchCreate,~~'); //查看软件需求详情操作菜单第九个链接 \ No newline at end of file diff --git a/module/story/test/model/change.php b/module/story/test/model/change.php index 5605c1ac09..d4e8a55fbe 100755 --- a/module/story/test/model/change.php +++ b/module/story/test/model/change.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->change(); +timeout=0 cid=0 - 查看变更后需求数据。 @@ -40,4 +41,4 @@ $result1 = $story->changeTest(1, $story1); $result2 = $story->changeTest(2, $story2); r($result1) && p('title,spec,version') && e('测试需求1变更标题,测试需求1的变更描述,4'); // 查看变更后需求数据。 -r((int)strpos($result2['title'][0], '名称』不能为空') !== false) && p() && e('1'); // 变更时不填写需求名称,给出提示 +r((int)strpos($result2['title'][0], '名称』不能为空') !== false) && p() && e('1'); // 变更时不填写需求名称,给出提示 \ No newline at end of file diff --git a/module/story/test/model/checkforcereview.php b/module/story/test/model/checkforcereview.php old mode 100644 new mode 100755 index cd5103c1c2..74c91f0df6 --- a/module/story/test/model/checkforcereview.php +++ b/module/story/test/model/checkforcereview.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->checkforcereview(); +timeout=0 cid=0 - 执行story模块的checkForceReview方法 @0 @@ -62,4 +63,4 @@ r((int)$tester->story->checkForceReview()) && p() && e('1'); $tester->story->config->story->forceNotReviewRoles = ''; $tester->story->config->story->forceNotReviewDepts = '1'; -r((int)$tester->story->checkForceReview()) && p() && e('1'); +r((int)$tester->story->checkForceReview()) && p() && e('1'); \ No newline at end of file diff --git a/module/story/test/model/close.php b/module/story/test/model/close.php index 51fba3f995..7ea70c804f 100755 --- a/module/story/test/model/close.php +++ b/module/story/test/model/close.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->close(); +timeout=0 cid=0 - 关闭一个用户需求,查看状态 @@ -19,7 +20,7 @@ cid=0 - 关闭一个重复了的需求,但缺少重复的需求的ID,查看状态 - 属性status @~~ - 属性closedReason @~~ - - 属性duplicateStory @『重复研发需求』不能为空。 + - 属性duplicateStory @『重复需求』不能为空。 */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -58,7 +59,7 @@ $story2 = $story->closeTest(2, $postData2); $story3 = $story->closeTest(3, $postData3); $story4 = $story->closeTest(4, $postData4); -r($story1) && p('status,closedReason') && e('closed,done'); // 关闭一个用户需求,查看状态 -r($story2) && p('status,closedReason') && e('closed,willnotdo'); // 关闭一个软件需求,查看状态 -r($story3) && p('status,closedReason,duplicateStory') && e('closed,duplicate,5'); // 关闭一个重复了的需求,查看状态 -r($story4) && p('status,closedReason,duplicateStory') && e('~~,~~,『重复研发需求』不能为空。'); // 关闭一个重复了的需求,但缺少重复的需求的ID,查看状态 +r($story1) && p('status,closedReason') && e('closed,done'); // 关闭一个用户需求,查看状态 +r($story2) && p('status,closedReason') && e('closed,willnotdo'); // 关闭一个软件需求,查看状态 +r($story3) && p('status,closedReason,duplicateStory') && e('closed,duplicate,5'); // 关闭一个重复了的需求,查看状态 +r($story4) && p('status,closedReason,duplicateStory') && e('~~,~~,『重复需求』不能为空。'); // 关闭一个重复了的需求,但缺少重复的需求的ID,查看状态 \ No newline at end of file diff --git a/module/story/test/model/create.php b/module/story/test/model/create.php index 2e0bfc51ce..95ad52d934 100755 --- a/module/story/test/model/create.php +++ b/module/story/test/model/create.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->create(); +timeout=0 cid=0 - 检查创建后的数据。 @@ -39,8 +40,7 @@ $data = new stdclass(); $data->product = 1; $data->module = 0; $data->modules = array(0); -$data->plans = array(0); -$data->plan = 1; +$data->plan = '1'; $data->assignedTo = ''; $data->source = ''; $data->sourceNote = ''; @@ -63,7 +63,6 @@ $data->openedBy = 'admin'; $data->openedDate = date('Y-m-d H:i:s'); $data->mailto = ''; $data->parent = 1; -$data->URS[] = 1; $data->reviewer[] = 'admin'; $story = new storyTest(); @@ -72,4 +71,4 @@ $test2 = $story->createTest($data, 11); $test3 = $story->createTest($data, 0, 1); r((array)$test1) && p('id,title') && e('5,test story'); //检查创建后的数据。 r(count($test2->linkedProjects)) && p() && e('1'); //如果传入执行,检查需求是否已经关联到执行了。 -r($test3->linkedBug->status) && p() && e('closed'); //如果传入Bug,检查Bug是否已经关闭了。 +r($test3->linkedBug->status) && p() && e('closed'); //如果传入Bug,检查Bug是否已经关闭了。 \ No newline at end of file diff --git a/module/story/test/model/createtwins.php b/module/story/test/model/createtwins.php index 27191d34d9..bc27710c4e 100755 --- a/module/story/test/model/createtwins.php +++ b/module/story/test/model/createtwins.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->createTwins(); +timeout=0 cid=0 - 检查创建后的数据。 @@ -39,7 +40,7 @@ $data->module = 0; $data->branches = array(0, 1); $data->modules = array(1, 2); $data->plans = array(0, 0); -$data->plan = 1; +$data->plan = '1'; $data->assignedTo = ''; $data->source = ''; $data->sourceNote = ''; @@ -61,9 +62,8 @@ $data->openedBy = 'admin'; $data->openedDate = date('Y-m-d H:i:s'); $data->mailto = ''; $data->parent = 1; -$data->URS[] = 1; $data->reviewer[] = 'admin'; $story = new storyTest(); $test1 = $story->createTwinsTest($data); -r((array)$test1) && p('id-twins', '-') && e('5-,6,'); //检查创建后的数据。 +r((array)$test1) && p('id-twins', '-') && e('5-,6,'); //检查创建后的数据。 \ No newline at end of file diff --git a/module/story/test/model/getaffectedscope.php b/module/story/test/model/getaffectedscope.php index 9b1b0f8757..856baeb79a 100755 --- a/module/story/test/model/getaffectedscope.php +++ b/module/story/test/model/getaffectedscope.php @@ -27,7 +27,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/getbyid.php b/module/story/test/model/getbyid.php index 4bebaafa17..644f076a61 100755 --- a/module/story/test/model/getbyid.php +++ b/module/story/test/model/getbyid.php @@ -4,19 +4,18 @@ /** title=测试 storyModel->getById(); +timeout=0 cid=0 -- 获取ID为1、版本号为1的需求关联的用户需求。 @用户需求3 - 获取ID为1、版本号为1的需求的名称。属性title @用户需求版本一1 - 获取ID为1、版本号为2的需求的名称。 - 属性title @用户需求版本一2 - 属性spec @这是一个软件需求描述2 - 获取ID为2、版本号为3的需求关联的执行。 @22|30|37 -- 获取ID为2、版本号为3的需求关联的需求。 @4|6|8 +- 获取ID为2、版本号为3的需求关联的需求。 @4 - 获取ID为2、版本号为3的需求,执行ID为21的创建任务数。 @2 - 获取ID为2、版本号为3的需求,执行ID为26的创建任务数。 @2 - 获取ID为2、版本号为3的需求关联的计划。 @1.0 -- 获取ID为2、版本号为3的需求关联需求的标题。 @软件需求6 - 获取ID为2、版本号为3的需求的名称。属性title @用户需求版本一6 - 获取ID为10、版本号为3的需求转化Bug的名称。属性toBugTitle @BUG1 - 获取ID为18、版本号为3的需求子需求的标题。属性title @软件需求20 @@ -34,9 +33,9 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); +$story->isParent->range('0{17},1,0{100}'); $story->twins->range('``{27},30,``,28'); $story->gen(30); @@ -77,19 +76,17 @@ $story18Version3 = $story->getByIdTest(18, 3); $story20Version3 = $story->getByIdTest(20, 3); $story28Version3 = $story->getByIdTest(28, 3); -r($story1Version1->linkStoryTitles[3]) && p() && e('用户需求3'); //获取ID为1、版本号为1的需求关联的用户需求。 r($story1Version1) && p('title') && e('用户需求版本一1'); //获取ID为1、版本号为1的需求的名称。 r($story1Version2) && p('title,spec') && e('用户需求版本一2,这是一个软件需求描述2'); //获取ID为1、版本号为2的需求的名称。 r(implode('|', array_keys($story2Version3->executions))) && p() && e('22|30|37'); //获取ID为2、版本号为3的需求关联的执行。 -r(implode('|', array_keys($story2Version3->extraStories))) && p() && e('4|6|8'); //获取ID为2、版本号为3的需求关联的需求。 +r(implode('|', array_keys($story2Version3->extraStories))) && p() && e('4'); //获取ID为2、版本号为3的需求关联的需求。 r(count($story2Version3->tasks[21])) && p() && e('2'); //获取ID为2、版本号为3的需求,执行ID为21的创建任务数。 r(count($story2Version3->tasks[26])) && p() && e('2'); //获取ID为2、版本号为3的需求,执行ID为26的创建任务数。 r($story2Version3->planTitle[1]) && p() && e('1.0'); //获取ID为2、版本号为3的需求关联的计划。 -r($story2Version3->linkStoryTitles[6]) && p() && e('软件需求6'); //获取ID为2、版本号为3的需求关联需求的标题。 r($story2Version3) && p('title') && e('用户需求版本一6'); //获取ID为2、版本号为3的需求的名称。 r($story10Version3) && p('toBugTitle') && e('BUG1'); //获取ID为10、版本号为3的需求转化Bug的名称。 r($story18Version3->children[20]) && p('title') && e('软件需求20'); //获取ID为18、版本号为3的需求子需求的标题。 r($story20Version3) && p('parentName') && e('软件需求18'); //获取ID为20、版本号为3的需求父需求的标题。 -r(count($story28Version3->tasks[30])) && p() && e('2'); //获取ID为20、版本号为3的需求孪生需求关联执行中的任务数。 +r(count($story28Version3->tasks[30])) && p() && e('2'); //获取ID为20、版本号为3的需求孪生需求关联执行中的任务数。 \ No newline at end of file diff --git a/module/story/test/model/getchangedstories.php b/module/story/test/model/getchangedstories.php deleted file mode 100755 index 1159d2c09c..0000000000 --- a/module/story/test/model/getchangedstories.php +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env php -getChangedStories(); -cid=0 - -- 获取需求12关联的用户需求数量 @1 -- 获取需求12关联的用户需求详情 - - 第3条的title属性 @用户需求3 - - 第3条的type属性 @requirement - -*/ -include dirname(__FILE__, 5) . '/test/lib/init.php'; -su('admin'); - -zenData('story')->gen(20); -zenData('product')->gen(1); -$story = zenData('story'); -$story->product->range(1); -$story->version->range(1); -$story->gen(20); - -zenData('storyspec')->gen(20); -$relation = zenData('relation'); -$relation->AID->range('1,10,2,11,3,12,4,13,5,14,6,15,7,16,8,17'); -$relation->AVersion->range('1'); -$relation->BID->range('10,1,11,2,12,3,13,4,14,5,15,6,16,7,17,8'); -$relation->BVersion->range('1'); -$relation->gen(16); - -global $tester; -$tester->loadModel('story'); -$story = $tester->story->getById(12); -$requirements = $tester->story->getChangedStories($story); - -r(count($requirements)) && p() && e('1'); // 获取需求12关联的用户需求数量 -r($requirements) && p('3:title,type') && e('用户需求3,requirement'); // 获取需求12关联的用户需求详情 diff --git a/module/story/test/model/getdataofstoriesperassignedto.php b/module/story/test/model/getdataofstoriesperassignedto.php index 1d8faae511..548c87c1ef 100755 --- a/module/story/test/model/getdataofstoriesperassignedto.php +++ b/module/story/test/model/getdataofstoriesperassignedto.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerAssignedTo(); +timeout=0 cid=0 - 按照需求指派人分组,获取分组后的需求数量 @4 @@ -28,4 +29,4 @@ $_SESSION['storyQueryCondition'] = "`id` < 20"; $data = $tester->story->getDataOfStoriesPerAssignedTo(); r(count($data)) && p() && e('4'); // 按照需求指派人分组,获取分组后的需求数量 -r($data) && p('admin:name,value') && e('admin,5'); // 按照需求指派人分组,获取各个指派人名下的需求数量,查看admin下的数据 +r($data) && p('admin:name,value') && e('admin,5'); // 按照需求指派人分组,获取各个指派人名下的需求数量,查看admin下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperchange.php b/module/story/test/model/getdataofstoriesperchange.php index ceb4a9604c..7b6282e73f 100755 --- a/module/story/test/model/getdataofstoriesperchange.php +++ b/module/story/test/model/getdataofstoriesperchange.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerChange(); +timeout=0 cid=0 - 按照需求版本号分组,获取分组后的需求数量 @4 @@ -31,4 +32,4 @@ $data = $tester->story->getDataOfStoriesPerChange(); r(count($data)) && p() && e('4'); // 按照需求版本号分组,获取分组后的需求数量 r($data) && p('0:name,value') && e('3,4'); // 按照需求版本号分组,获取各个版本号下的需求数量,查看admin下的数据 -r($data) && p('2:value') && e('5'); // 按照需求版本号分组,获取各个版本号下的需求数量,查看admin下的数据 +r($data) && p('2:value') && e('5'); // 按照需求版本号分组,获取各个版本号下的需求数量,查看admin下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperclosedreason.php b/module/story/test/model/getdataofstoriesperclosedreason.php index 5987cb5a4a..5ce948449c 100755 --- a/module/story/test/model/getdataofstoriesperclosedreason.php +++ b/module/story/test/model/getdataofstoriesperclosedreason.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerClosedReason(); +timeout=0 cid=0 - 按照需求关闭原因分组,获取分组后的需求数量 @7 @@ -38,4 +39,4 @@ $data = $tester->story->getDataOfStoriesPerClosedReason(); r(count($data)) && p() && e('7'); // 按照需求关闭原因分组,获取分组后的需求数量 r($data) && p('willnotdo:name,value') && e('不做,3'); // 按照需求关闭原因分组,获取各个关闭原因下的需求数量,查看willnotdo的数据 r($data) && p('bydesign:name,value') && e('设计如此,2'); // 按照需求关闭原因分组,获取各个关闭原因下的需求数量,查看bydesign的数据 -r($data) && p('cancel:name,value') && e('已取消,2'); // 按照需求关闭原因分组,获取各个关闭原因下的需求数量,查看cancel的数据 +r($data) && p('cancel:name,value') && e('已取消,2'); // 按照需求关闭原因分组,获取各个关闭原因下的需求数量,查看cancel的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperestimate.php b/module/story/test/model/getdataofstoriesperestimate.php index aeb3e52db8..db1d7bc3a9 100755 --- a/module/story/test/model/getdataofstoriesperestimate.php +++ b/module/story/test/model/getdataofstoriesperestimate.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerEstimate(); +timeout=0 cid=0 - 按照需求预计工时分组,获取分组后的需求数量 @4 @@ -31,4 +32,4 @@ $data = $tester->story->getDataOfStoriesPerEstimate(); r(count($data)) && p() && e('4'); // 按照需求预计工时分组,获取分组后的需求数量 r($data) && p('0:name,value') && e('4,4'); // 按照需求预计工时分组,获取各个工时的需求数量,查看工时为19的数据 -r($data) && p('1:value') && e('5'); // 按照需求预计工时分组,获取各个工时的需求数量,查看工时为20的数据 +r($data) && p('1:value') && e('5'); // 按照需求预计工时分组,获取各个工时的需求数量,查看工时为20的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriespermodule.php b/module/story/test/model/getdataofstoriespermodule.php index 5fb3f46c40..0a2e5a7f56 100755 --- a/module/story/test/model/getdataofstoriespermodule.php +++ b/module/story/test/model/getdataofstoriespermodule.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerModule(); +timeout=0 cid=0 - 按照模块分组,获取分组后的需求数量 @6 @@ -37,4 +38,4 @@ $_SESSION['storyQueryCondition'] = "`id` < 20"; $data = $tester->story->getDataOfStoriesPerModule(); r(count($data)) && p() && e('6'); // 按照模块分组,获取分组后的需求数量 -r($data) && p('1:name,value,product') && e('/这是一个模块1,4,1'); // 按照模块分组,获取各个模块下的需求数量,查看模块2150下的数据 +r($data) && p('1:name,value,product') && e('/这是一个模块1,4,1'); // 按照模块分组,获取各个模块下的需求数量,查看模块2150下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperopenedby.php b/module/story/test/model/getdataofstoriesperopenedby.php index f0b4cbc116..4d6b0f4677 100755 --- a/module/story/test/model/getdataofstoriesperopenedby.php +++ b/module/story/test/model/getdataofstoriesperopenedby.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerOpenedBy(); +timeout=0 cid=0 - 按照创建人分组,获取分组后的需求数量 @4 @@ -31,4 +32,4 @@ $_SESSION['storyQueryCondition'] = "`id` < 20"; $data = $tester->story->getDataOfStoriesPerOpenedBy(); r(count($data)) && p() && e('4'); // 按照创建人分组,获取分组后的需求数量 -r($data) && p('user2:name,value') && e('用户2,5'); // 按照创建人分组,获取各个创建人的需求数量,查看用户test3下的数据 +r($data) && p('user2:name,value') && e('用户2,5'); // 按照创建人分组,获取各个创建人的需求数量,查看用户test3下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperplan.php b/module/story/test/model/getdataofstoriesperplan.php index a6004e5a6f..cdd9b4c22f 100755 --- a/module/story/test/model/getdataofstoriesperplan.php +++ b/module/story/test/model/getdataofstoriesperplan.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerPlan(); +timeout=0 cid=0 - 按照计划分组,获取分组后的需求数量 @5 @@ -35,4 +36,4 @@ $data = $tester->story->getDataOfStoriesPerPlan(); r(count($data)) && p() && e('5'); // 按照计划分组,获取分组后的需求数量 r($data) && p('1:name,value') && e('1.0,4'); // 按照计划分组,获取各个计划的需求数量,查看1下的数据 -r($data) && p('4:name,value') && e('1.0,4'); // 按照计划分组,获取各个计划的需求数量,查看4下的数据 +r($data) && p('4:name,value') && e('1.0,4'); // 按照计划分组,获取各个计划的需求数量,查看4下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperpri.php b/module/story/test/model/getdataofstoriesperpri.php index a7257eaaaf..ab2c33dc0e 100755 --- a/module/story/test/model/getdataofstoriesperpri.php +++ b/module/story/test/model/getdataofstoriesperpri.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerPri(); +timeout=0 cid=0 - 按照优先级分组,获取分组后的需求数量 @4 @@ -35,4 +36,4 @@ $data = $tester->story->getDataOfStoriesPerPri(); r(count($data)) && p() && e('4'); // 按照优先级分组,获取分组后的需求数量 r($data) && p('1:name,value') && e('1,5'); // 按照优先级分组,获取各个优先级的需求数量,查看优先级1下的数据 -r($data) && p('4:name,value') && e('4,4'); // 按照优先级分组,获取各个优先级的需求数量,查看优先级4下的数据 +r($data) && p('4:name,value') && e('4,4'); // 按照优先级分组,获取各个优先级的需求数量,查看优先级4下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperproduct.php b/module/story/test/model/getdataofstoriesperproduct.php index 5458526004..4dbebcb67a 100755 --- a/module/story/test/model/getdataofstoriesperproduct.php +++ b/module/story/test/model/getdataofstoriesperproduct.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerProduct(); +timeout=0 cid=0 - 按照产品分组,获取分组后的需求数量 @5 @@ -31,4 +32,4 @@ $_SESSION['storyQueryCondition'] = "`id` < 20"; $data = $tester->story->getDataOfStoriesPerProduct(); r(count($data)) && p() && e('5'); // 按照产品分组,获取分组后的需求数量 -r($data) && p('1:name,value') && e('正常产品1,4'); // 按照产品分组,获取各个产品下的需求数量,查看产品1下的数据 +r($data) && p('1:name,value') && e('正常产品1,4'); // 按照产品分组,获取各个产品下的需求数量,查看产品1下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriespersource.php b/module/story/test/model/getdataofstoriespersource.php index be54ce1b31..be176d8817 100755 --- a/module/story/test/model/getdataofstoriespersource.php +++ b/module/story/test/model/getdataofstoriespersource.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerSource(); +timeout=0 cid=0 - 按照需求来源分组,获取分组后的需求数量 @14 @@ -35,4 +36,4 @@ $data = $tester->story->getDataOfStoriesPerSource(); r(count($data)) && p() && e('14'); // 按照需求来源分组,获取分组后的需求数量 r($data) && p('support:name,value') && e('技术支持,1'); // 按照需求来源分组,获取各个需求来源的需求数量,查看support下的数据 -r($data) && p('market:name,value') && e('市场,2'); // 按照需求来源分组,获取各个需求来源的需求数量,查看market下的数据 +r($data) && p('market:name,value') && e('市场,2'); // 按照需求来源分组,获取各个需求来源的需求数量,查看market下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperstage.php b/module/story/test/model/getdataofstoriesperstage.php index 55bb0d53ac..75dec0a775 100755 --- a/module/story/test/model/getdataofstoriesperstage.php +++ b/module/story/test/model/getdataofstoriesperstage.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerStage(); +timeout=0 cid=0 - 按照需求阶段分组,获取分组后的需求数量 @10 @@ -38,4 +39,4 @@ $data = $tester->story->getDataOfStoriesPerStage(); r(count($data)) && p() && e('10'); // 按照需求阶段分组,获取分组后的需求数量 r($data) && p('wait:name,value') && e('未开始,1'); // 按照需求阶段分组,获取各个需求阶段的需求数量,查看wait下的数据 r($data) && p('planned:name,value') && e('已计划,1'); // 按照需求阶段分组,获取各个需求阶段的需求数量,查看planned下的数据 -r($data) && p('released:name,value') && e('已发布,1'); // 按照需求阶段分组,获取各个需求阶段的需求数量,查看released下的数据 +r($data) && p('released:name,value') && e('已发布,1'); // 按照需求阶段分组,获取各个需求阶段的需求数量,查看released下的数据 \ No newline at end of file diff --git a/module/story/test/model/getdataofstoriesperstatus.php b/module/story/test/model/getdataofstoriesperstatus.php index 9f1dbe366e..abd6efcf89 100755 --- a/module/story/test/model/getdataofstoriesperstatus.php +++ b/module/story/test/model/getdataofstoriesperstatus.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getDataOfStoriesPerStatus(); +timeout=0 cid=0 - 按照需求状态分组,获取分组后的需求数量 @4 @@ -39,4 +40,4 @@ $data = $tester->story->getDataOfStoriesPerStatus(); r(count($data)) && p() && e('4'); // 按照需求状态分组,获取分组后的需求数量 r($data) && p('draft:name,value') && e('草稿,5'); // 按照需求状态分组,获取各个需求状态的需求数量,查看draft下的数据 r($data) && p('active:name,value') && e('激活,5'); // 按照需求状态分组,获取各个需求状态的需求数量,查看active下的数据 -r($data) && p('closed:name,value') && e('已关闭,5'); // 按照需求状态分组,获取各个需求状态的需求数量,查看closed下的数据 +r($data) && p('closed:name,value') && e('已关闭,5'); // 按照需求状态分组,获取各个需求状态的需求数量,查看closed下的数据 \ No newline at end of file diff --git a/module/story/test/model/getexportstories.php b/module/story/test/model/getexportstories.php old mode 100644 new mode 100755 index e6be25458d..203333c328 --- a/module/story/test/model/getexportstories.php +++ b/module/story/test/model/getexportstories.php @@ -4,15 +4,15 @@ /** title=测试 storyModel->getExportStories(); +timeout=0 cid=0 - 查看只保存导出条件,导出选中需求数 @2 - 查看只保存导出条件,导出全部需求数 @9 - 查看保存全部导出SQL,导出选中需求数 @2 - 查看保存全部导出SQL,导出全部需求数 @5 -- 查看需求的id顺序 @9|8|7|6|5|4|2|3|1 +- 查看需求的id顺序 @9|8|7|6|5|4|3|2|1 - 查看需求ID为2的数据信息。 - - 第2条的parent属性 @`-1` - 第2条的title属性 @用户需求版本一2 - 第2条的spec属性 @这是一个软件需求描述2 - 第2条的verify属性 @这是一个需求验收2 @@ -60,5 +60,5 @@ r(count($stories2)) && p() && e('9'); //查看只保存导出条件,导出全 r(count($stories3)) && p() && e('2'); //查看保存全部导出SQL,导出选中需求数 r(count($stories4)) && p() && e('5'); //查看保存全部导出SQL,导出全部需求数 -r(implode('|', array_keys($stories2))) && p() && e('9|8|7|6|5|4|2|3|1'); //查看需求的id顺序 -r($stories2) && p('2:parent,title,spec,verify,bugCountAB') && e('`-1`,用户需求版本一2,这是一个软件需求描述2,这是一个需求验收2,1'); //查看需求ID为2的数据信息。 +r(implode('|', array_keys($stories2))) && p() && e('9|8|7|6|5|4|3|2|1'); //查看需求的id顺序 +r($stories2) && p('2:title,spec,verify,bugCountAB') && e('用户需求版本一2,这是一个软件需求描述2,这是一个需求验收2,1'); //查看需求ID为2的数据信息。 \ No newline at end of file diff --git a/module/story/test/model/getparentstorypairs.php b/module/story/test/model/getparentstorypairs.php index 62bc535770..3418143526 100755 --- a/module/story/test/model/getparentstorypairs.php +++ b/module/story/test/model/getparentstorypairs.php @@ -4,13 +4,12 @@ /** title=测试 storyModel->getParentStoryPairs(); +timeout=0 cid=0 -- 第91个元素和第0个元素的值 - - 属性91 @用户需求91 - - @~~ -- 获取符合条件的需求数 @5 -- 测试附加的需求ID1,需求1以数字1结尾属性1 @~f:1$~ +- 获取ID为3的父需求的标题第3条的keys属性 @用户需求9 +- 获取符合条件的需求数 @3 +- 测试附加的需求ID1,需求1以数字1结尾第0条的keys属性 @软件需求2 */ @@ -31,19 +30,24 @@ $story->status->range('active,draft'); $story->product->range('1{10},2{5},3{5},4{70},5{10}'); $story->parent->range('0,1'); $story->plan->range('0,1'); +$story->version->range('1'); +$story->grade->range('1{10},2{10}'); $story->gen(100); +zenData('storyspec')->gen(100); +zenData('storygrade')->gen(6); + $product = zenData('product'); $product->gen(5); global $tester; $tester->loadModel('story'); -$stories = $tester->story->getParentStoryPairs(5); +$stories = $tester->story->getParentStoryPairs(1); -r($stories) && p('91,0') && e('用户需求91,~~'); // 第91个元素和第0个元素的值 +r($stories) && p('3:keys') && e('用户需求9'); // 获取ID为3的父需求的标题 array_pop($stories); /* remove empty item at array top. */ -r(count($stories)) && p() && e(5); // 获取符合条件的需求数 +r(count($stories)) && p() && e(3); // 获取符合条件的需求数 -$storiesWithAppended = $tester->story->getParentStoryPairs(5, 1); -r($storiesWithAppended) && p('1') && e('~f:1$~'); // 测试附加的需求ID1,需求1以数字1结尾 +$storiesWithAppended = $tester->story->getParentStoryPairs(5, 2); +r($storiesWithAppended) && p('0:keys') && e('软件需求2'); // 测试附加的需求ID1,需求1以数字1结尾 \ No newline at end of file diff --git a/module/story/test/model/getproductstorypairs.php b/module/story/test/model/getproductstorypairs.php index 65b88018ef..8fab8e03b7 100755 --- a/module/story/test/model/getproductstorypairs.php +++ b/module/story/test/model/getproductstorypairs.php @@ -4,9 +4,10 @@ /** title=测试 storyModel->getProductStoryPairs(); +timeout=0 cid=0 -- 获取产品1下的所有软件需求数量 @2 +- 获取产品1下的所有软件需求数量 @4 - 获取产品1下的所有用户需求数量 @1 - 查看通过产品1获取的软件需求的名称字段 - 属性4 @4:软件需求4 (优先级:4,预计工时:4) @@ -29,7 +30,7 @@ $tester->loadModel('story'); $stories1 = $tester->story->getProductStoryPairs(1); $stories2 = $tester->story->getProductStoryPairs(3, 0, 1830, 'all', 'id_asc', 0, 'full', 'story', true); -r(count($stories1)) && p() && e('2'); // 获取产品1下的所有软件需求数量 +r(count($stories1)) && p() && e('4'); // 获取产品1下的所有软件需求数量 r(count($stories2)) && p() && e('1'); // 获取产品1下的所有用户需求数量 r($stories1) && p('4|2', '|') && e('4:软件需求4 (优先级:4,预计工时:4)|2:软件需求2 (优先级:2,预计工时:2)'); // 查看通过产品1获取的软件需求的名称字段 -r($stories2) && p('10', '|') && e('10:软件需求10 (优先级:2,预计工时:2)'); // 查看通过产品3获取的软件需求的键值对详情 +r($stories2) && p('10', '|') && e('10:软件需求10 (优先级:2,预计工时:2)'); // 查看通过产品3获取的软件需求的键值对详情 \ No newline at end of file diff --git a/module/story/test/model/getrelationstoryid.php b/module/story/test/model/getrelationstoryid.php deleted file mode 100755 index 1d62162be5..0000000000 --- a/module/story/test/model/getrelationstoryid.php +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env php -getStoryRelation(); -cid=0 - -*/ -include dirname(__FILE__, 5) . '/test/lib/init.php'; -su('admin'); - -zenData('storyspec')->gen(60); -$story = zenData('story'); -$story->product->range(1); -$story->parent->range('0{18},`-1`,19'); -$story->type->range('requirement{10},story{10}'); -$story->gen(20); - -$relation = zenData('relation'); -$relation->product->range(1); -$relation->AID->range('1,11,1,2,12,2,3,13,3,4,14,4,5,15,5,6,16,6,7,17,7,8,18,8'); -$relation->BID->range('11,1,1,12,2,2,13,3,3,14,4,4,15,5,5,16,6,6,17,7,7,18,8,8'); -$relation->AType->range('requirement,story,design'); -$relation->BType->range('story,requirement,commit'); -$relation->relation->range('subdivideinto,subdividedfrom,completedin'); -$relation->gen(24); - -global $tester; -$relations1 = $tester->loadModel('story')->getRelationStoryID(1, 'requirement'); -$relations2 = $tester->story->getRelationStoryID(11, 'story'); - -r($relations1) && p() && e('11'); // 获取用户需求1关联的软件需求 -r($relations2) && p() && e('1'); // 获取软件需求11关联的用户需求 diff --git a/module/story/test/model/getreviewresult.php b/module/story/test/model/getreviewresult.php index 5edc489888..77aced2261 100755 --- a/module/story/test/model/getreviewresult.php +++ b/module/story/test/model/getreviewresult.php @@ -4,9 +4,10 @@ /** title=测试 storyModel->getReviewResult(); +timeout=0 cid=0 -- 传入空参数 @pass +- 传入空参数 @pass - 设置 allpass,都是传入pass结果 @pass - 设置 allpass,传入 pass 和 reject @reject - 设置 allpass,传入 clarify 和 pass @clarify @@ -15,7 +16,7 @@ cid=0 - 设置 allpass,传入 clarify @clarify - 设置 allpass,传入 revert @revert - 设置 allpass,传入 reject @reject -- 设置 halfpass,传入 pass 和 reject @pass +- 设置 halfpass,传入 pass 和 reject @reject */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -38,4 +39,4 @@ r($storyModel->getReviewResult(array('revert', '', '', ''))) && p() && e('rever r($storyModel->getReviewResult(array('reject', '', '', ''))) && p() && e('reject'); //设置 allpass,传入 reject $storyModel->config->story->reviewRules = 'halfpass'; -r($storyModel->getReviewResult(array('pass', 'pass', 'reject'))) && p() && e('pass'); //设置 halfpass,传入 pass 和 reject +r($storyModel->getReviewResult(array('pass', 'pass', 'reject'))) && p() && e('reject'); //设置 halfpass,传入 pass 和 reject \ No newline at end of file diff --git a/module/story/test/model/getstories2link.php b/module/story/test/model/getstories2link.php index cc3ed7274f..26110b5c16 100755 --- a/module/story/test/model/getstories2link.php +++ b/module/story/test/model/getstories2link.php @@ -33,14 +33,14 @@ $relation->AID->range('1,11,1,2,12,2,3,13,3,4,14,4,5,15,5,6,16,6,7,17,7,8,18,8') $relation->BID->range('11,1,1,12,2,2,13,3,3,14,4,4,15,5,5,16,6,6,17,7,7,18,8,8'); $relation->AType->range('requirement,story,design'); $relation->BType->range('story,requirement,commit'); -$relation->relation->range('subdivideinto,subdividedfrom,completedin'); +$relation->relation->range('linkedto,linkedfrom,completedin'); $relation->gen(24); global $tester; -$stories1 = $tester->loadModel('story')->getStories2Link(1, 'linkStories', 'bySearch', 0, 'requirement'); +$stories1 = $tester->loadModel('story')->getStories2Link(1, 'bySearch', 0); $stories2 = $tester->loadModel('story')->getStories2Link(2); -r(count($stories1)) && p() && e('9'); // 获取需求1可关联的需求数量 -r(count($stories2)) && p() && e('3'); // 获取需求2可关联的需求数量 -r($stories1) && p('12:type,product') && e('story,1'); // 获取需求1可关联的需求id、product -r($stories2) && p('2:type,product') && e('requirement,1'); // 获取需求2可关联的需求id、product +r(count($stories1)) && p() && e('18'); // 获取需求1可关联的需求数量 +r(count($stories2)) && p() && e('18'); // 获取需求2可关联的需求数量 +r($stories1) && p('12:type,product') && e('story,1'); // 获取需求12可关联的需求id、product +r($stories2) && p('3:type,product') && e('requirement,1'); // 获取需求3可关联的需求id、product diff --git a/module/story/test/model/getstoryrelation.php b/module/story/test/model/getstoryrelation.php index 22c8ffacdf..735952053b 100755 --- a/module/story/test/model/getstoryrelation.php +++ b/module/story/test/model/getstoryrelation.php @@ -34,14 +34,14 @@ $relation->AID->range('1,11,1,2,12,2,3,13,3,4,14,4,5,15,5,6,16,6,7,17,7,8,18,8') $relation->BID->range('11,1,1,12,2,2,13,3,3,14,4,4,15,5,5,16,6,6,17,7,7,18,8,8'); $relation->AType->range('requirement,story,design'); $relation->BType->range('story,requirement,commit'); -$relation->relation->range('subdivideinto,subdividedfrom,completedin'); +$relation->relation->range('linkedto,linkedfrom,completedin'); $relation->gen(24); global $tester; $relations1 = $tester->loadModel('story')->getStoryRelation(1, 'requirement'); $relations2 = $tester->story->getStoryRelation(11, 'story'); -r(count($relations1)) && p() && e('1'); // 获取用户需求1关联的软件需求数量 -r(count($relations2)) && p() && e('1'); // 获取软件需求11关联的用户需求数量 -r($relations1[0]) && p('title,type,status') && e('用户需求11,story,closed'); // 获取用户需求1关联的软件需求详情 -r($relations2[0]) && p('title,type,status') && e('用户需求1,requirement,draft'); // 获取软件需求11关联的用户需求详情 +r(count($relations1)) && p() && e('1'); // 获取用户需求1关联的软件需求数量 +r(count($relations2)) && p() && e('1'); // 获取软件需求11关联的用户需求数量 +r($relations1['story']) && p('11:title,type,status') && e('用户需求11,story,closed'); // 获取用户需求1关联的软件需求详情 +r($relations2['requirement']) && p('1:title,type,status') && e('用户需求1,requirement,draft'); // 获取软件需求11关联的用户需求详情 diff --git a/module/story/test/model/gettoandcclist.php b/module/story/test/model/gettoandcclist.php index a4343a9d77..edb7033449 100755 --- a/module/story/test/model/gettoandcclist.php +++ b/module/story/test/model/gettoandcclist.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->getToAndCcList(); +timeout=0 cid=0 - 获取创建需求1时的通知人数量 @2 @@ -11,7 +12,7 @@ cid=0 - 获取变更需求1时的通知人数量 @2 - 获取创建需求1时的通知人详情 @admin - 获取评审需求1时的通知人详情 @admin -- 获取变更需求1时的通知人详情 @user92, +- 获取变更需求1时的通知人详情 @, */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -27,9 +28,9 @@ $dataList1 = $tester->story->getToAndCcList($story1, 'opened'); $dataList2 = $tester->story->getToAndCcList($story1, 'reviewed'); $dataList3 = $tester->story->getToAndCcList($story1, 'changed'); -r(count($dataList1)) && p() && e('2'); //获取创建需求1时的通知人数量 -r(count($dataList2)) && p() && e('2'); //获取评审需求1时的通知人数量 -r(count($dataList3)) && p() && e('2'); //获取变更需求1时的通知人数量 -r($dataList1[0]) && p() && e('admin'); //获取创建需求1时的通知人详情 -r($dataList2[0]) && p() && e('admin'); //获取评审需求1时的通知人详情 -r($dataList3[1]) && p() && e('user92,'); //获取变更需求1时的通知人详情 +r(count($dataList1)) && p() && e('2'); //获取创建需求1时的通知人数量 +r(count($dataList2)) && p() && e('2'); //获取评审需求1时的通知人数量 +r(count($dataList3)) && p() && e('2'); //获取变更需求1时的通知人数量 +r($dataList1[0]) && p() && e('admin'); //获取创建需求1时的通知人详情 +r($dataList2[0]) && p() && e('admin'); //获取评审需求1时的通知人详情 +r($dataList3[1]) && p() && e(','); //获取变更需求1时的通知人详情 \ No newline at end of file diff --git a/module/story/test/model/gettrackbyid.php b/module/story/test/model/gettrackbyid.php deleted file mode 100755 index 8f3b26905e..0000000000 --- a/module/story/test/model/gettrackbyid.php +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env php -getTrackByID(); -cid=0 - -- 获取用户需求1下面的所有任务数量 @8 -- 获取用户需求1下面的所有用例数量 @5 -- 获取用户需求1下面的所有Bug数量 @5 -- 获取用户需求1下面的任务8的名字属性name @开发任务18 -- 获取用户需求的信息属性title @用户需求11 - -*/ -include dirname(__FILE__, 5) . '/test/lib/init.php'; -su('admin'); - -zenData('product')->gen(2); -zenData('storyspec')->gen(60); -$story = zenData('story'); -$story->product->range(1); -$story->parent->range('0{18},`-1`,19'); -$story->type->range('requirement{10},story{10}'); -$story->gen(20); - -$projectstory = zenData('projectstory'); -$projectstory->project->range(1); -$projectstory->product->range(1); -$projectstory->story->range('1-18'); -$projectstory->gen(18); - -$case = zenData('case'); -$case->story->range(11); -$case->gen(5); - -$bug = zenData('bug'); -$bug->story->range(11); -$bug->gen(5); - -$task = zenData('task'); -$task->story->range('11{8},19{2}'); -$task->parent->range('0{8},`-1`,9'); -$task->gen(10); - -$design = zenData('design'); -$design->story->range(11); -$design->gen(10); - -$relation = zenData('relation'); -$relation->product->range(1); -$relation->AID->range('1,11,1,2,12,2,3,13,3,4,14,4,5,15,5,6,16,6,7,17,7,8,18,8'); -$relation->BID->range('11,1,1,12,2,2,13,3,3,14,4,4,15,5,5,16,6,6,17,7,7,18,8,8'); -$relation->AType->range('requirement,story,design'); -$relation->BType->range('story,requirement,commit'); -$relation->relation->range('subdivideinto,subdividedfrom,completedin'); -$relation->gen(24); - -zenData('repohistory')->gen(10); - -global $tester; -$story1Tracks = $tester->loadModel('story')->getTrackByID(1); - -r(count($story1Tracks[11]->tasks)) && p() && e('8'); //获取用户需求1下面的所有任务数量 -r(count($story1Tracks[11]->cases)) && p() && e('5'); //获取用户需求1下面的所有用例数量 -r(count($story1Tracks[11]->bugs)) && p() && e('5'); //获取用户需求1下面的所有Bug数量 -r($story1Tracks[11]->tasks[8]) && p('name') && e('开发任务18'); //获取用户需求1下面的任务8的名字 -r($story1Tracks[11]) && p('title') && e('用户需求11'); //获取用户需求的信息 diff --git a/module/story/test/model/gettracksbystories.php b/module/story/test/model/gettracksbystories.php new file mode 100644 index 0000000000..a4d08ce936 --- /dev/null +++ b/module/story/test/model/gettracksbystories.php @@ -0,0 +1,28 @@ +#!/usr/bin/env php +getTracksByStories(); +cid=0 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +zenData('story')->gen(10); + +su('admin'); + +global $tester; +$tester->loadModel('story'); + +$tester->story->lang->ERCommon = '业务需求'; +$tester->story->lang->URCommon = '用户需求'; +$tester->story->lang->SRCommon = '研发需求'; + +$tracks = $tester->story->getTracksByStories(array(), 'epic'); +r(empty($tracks)) && p() && e('1'); //传入空参数。 + +$stories = $tester->story->dao->select('*')->from(TABLE_STORY)->where('id')->in(array('2,3,4,5'))->fetchAll('id'); +$tracks = $tester->story->getTracksByStories($stories, 'epic'); +r(isset($tracks['lanes']) && isset($tracks['cols']) && isset($tracks['items'])) && p() && e('1'); //检查返回数据的 lanes,cols,items。 diff --git a/module/story/test/model/isclickable.php b/module/story/test/model/isclickable.php index 4d2a2bff70..5dd04dafa8 100755 --- a/module/story/test/model/isclickable.php +++ b/module/story/test/model/isclickable.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->isclickable(); +timeout=0 cid=0 - 判断需求2是否有变更按钮操作权限 @1 @@ -12,7 +13,7 @@ cid=0 - 判断需求2是否有激活按钮操作权限 @0 - 判断需求2是否有指派按钮操作权限 @1 - 判断需求2是否有创建用例按钮操作权限 @1 -- 判断需求2是否有批量创建按钮操作权限 @0 +- 判断需求2是否有批量创建按钮操作权限 @1 */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -30,4 +31,4 @@ r($storyModel::isClickable($story, 'close')) && p() && e('1'); // 判断 r($storyModel::isClickable($story, 'activate')) && p() && e('0'); // 判断需求2是否有激活按钮操作权限 r($storyModel::isClickable($story, 'assignto')) && p() && e('1'); // 判断需求2是否有指派按钮操作权限 r($storyModel::isClickable($story, 'createcase')) && p() && e('1'); // 判断需求2是否有创建用例按钮操作权限 -r($storyModel::isClickable($story, 'batchcreate')) && p() && e('0'); // 判断需求2是否有批量创建按钮操作权限 +r($storyModel::isClickable($story, 'batchcreate')) && p() && e('1'); // 判断需求2是否有批量创建按钮操作权限 \ No newline at end of file diff --git a/module/story/test/model/linkstories.php b/module/story/test/model/linkstories.php index b3547b1c85..d7980d208b 100755 --- a/module/story/test/model/linkstories.php +++ b/module/story/test/model/linkstories.php @@ -1,36 +1,41 @@ #!/usr/bin/env php product->range(1); +$story->version->range(1); +$story->parent->range('0{18},`-1`,19'); +$story->type->range('requirement{10},story{10}'); +$story->gen(20); + +zenData('storyspec')->gen(60); +$relation = zenData('relation'); +$relation->gen(0); /** title=测试 storyModel->linkStories(); -cid=0 +timeout=0 +cid=1 - 查看关联前的关联关系数量 @0 - 查看关联后的关联关系数量 @4 - 查看关联后的需求详情 @1|3|5|7 */ -include dirname(__FILE__, 5) . '/test/lib/init.php'; -include dirname(__FILE__, 2) . '/lib/story.unittest.class.php'; -su('admin'); - -zenData('story')->gen(20); -zenData('storyspec')->gen(60); -$relation = zenData('relation'); -$relation->gen(0); - global $tester; $tester->loadModel('story'); $beforeRelations = $tester->story->getRelation(1, 'requirement'); $storyIdList = array(1, 3, 5, 7); -$_POST['stories'] = $storyIdList; -$tester->story->linkStories(1); +$tester->story->linkStories(1, $storyIdList); $afterRelations = $tester->story->getRelation(1, 'requirement'); r(count($beforeRelations)) && p() && e('0'); // 查看关联前的关联关系数量 r(count($afterRelations)) && p() && e('4'); // 查看关联后的关联关系数量 -r(implode('|', $afterRelations)) && p() && e('1|3|5|7'); // 查看关联后的需求详情 +r(implode('|', $afterRelations)) && p() && e('1|3|5|7'); // 查看关联后的需求详情 \ No newline at end of file diff --git a/module/story/test/model/mergereviewer.php b/module/story/test/model/mergereviewer.php index 8e5bc94e54..4b88b2b74f 100755 --- a/module/story/test/model/mergereviewer.php +++ b/module/story/test/model/mergereviewer.php @@ -4,18 +4,15 @@ /** title=测试 storyModel->mergeReviewer(); +timeout=0 cid=0 - 获取需求2的评审人 @admin|user1|user2 - 获取需求2的待评审人 @admin|user1 - 获取需求18的评审人 @admin|user1|user2 - 获取需求18的待评审人 @admin|user1 -- 获取需求18子需求的评审人 @admin|user1|user2 -- 获取需求18子需求的待评审人 @admin|user1 - 批量获取需求18的评审人 @admin|user1|user2 - 批量获取需求18的待评审人 @admin|user1 -- 批量获取需求18子需求的评审人 @admin|user1|user2 -- 批量获取需求18子需求的待评审人 @admin|user1 */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -28,7 +25,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); @@ -46,18 +42,14 @@ global $tester; $story = $tester->loadModel('story')->getById(2); $story = $tester->story->mergeReviewer($story, true); r(implode('|', $story->reviewer)) && p() && e('admin|user1|user2'); // 获取需求2的评审人 -r(implode('|', $story->notReview)) && p() && e('admin|user1'); // 获取需求2的待评审人 +r(implode('|', $story->notReview)) && p() && e('admin|user1'); // 获取需求2的待评审人 $story = $tester->loadModel('story')->getById(18); $story = $tester->story->mergeReviewer($story, true); -r(implode('|', $story->reviewer)) && p() && e('admin|user1|user2'); // 获取需求18的评审人 -r(implode('|', $story->notReview)) && p() && e('admin|user1'); // 获取需求18的待评审人 -r(implode('|', $story->children[20]->reviewer)) && p() && e('admin|user1|user2'); // 获取需求18子需求的评审人 -r(implode('|', $story->children[20]->notReview)) && p() && e('admin|user1'); // 获取需求18子需求的待评审人 +r(implode('|', $story->reviewer)) && p() && e('admin|user1|user2'); // 获取需求18的评审人 +r(implode('|', $story->notReview)) && p() && e('admin|user1'); // 获取需求18的待评审人 $stories = $tester->story->mergeReviewer(array(18 => $story)); -r(implode('|', $stories[18]->reviewer)) && p() && e('admin|user1|user2'); // 批量获取需求18的评审人 -r(implode('|', $stories[18]->notReview)) && p() && e('admin|user1'); // 批量获取需求18的待评审人 -r(implode('|', $stories[18]->children[20]->reviewer)) && p() && e('admin|user1|user2'); // 批量获取需求18子需求的评审人 -r(implode('|', $stories[18]->children[20]->notReview)) && p() && e('admin|user1'); // 批量获取需求18子需求的待评审人 +r(implode('|', $stories[18]->reviewer)) && p() && e('admin|user1|user2'); // 批量获取需求18的评审人 +r(implode('|', $stories[18]->notReview)) && p() && e('admin|user1'); // 批量获取需求18的待评审人 \ No newline at end of file diff --git a/module/story/test/model/recallchange.php b/module/story/test/model/recallchange.php index 339f9c20d8..fb6da8f9b6 100644 --- a/module/story/test/model/recallchange.php +++ b/module/story/test/model/recallchange.php @@ -29,7 +29,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/recallreview.php b/module/story/test/model/recallreview.php index 37277c34c1..832a9d4698 100644 --- a/module/story/test/model/recallreview.php +++ b/module/story/test/model/recallreview.php @@ -21,7 +21,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/recordreviewaction.php b/module/story/test/model/recordreviewaction.php index 1b4d2e5805..e3a71b4913 100755 --- a/module/story/test/model/recordreviewaction.php +++ b/module/story/test/model/recordreviewaction.php @@ -26,7 +26,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/replaceurlang.php b/module/story/test/model/replaceurlang.php old mode 100644 new mode 100755 index 1d8408543d..932c9eea52 --- a/module/story/test/model/replaceurlang.php +++ b/module/story/test/model/replaceurlang.php @@ -4,12 +4,13 @@ /** title=测试 storyModel->replaceURLang(); +timeout=0 cid=0 - 查看更换语言之前的create属性create @提研发需求 - 查看更换语言之前的create属性create @提用户需求 - 查看更换语言之前的common属性common @研发需求 -- 查看更换语言之前的common属性common @用户需求 +- 查看更换语言之前的common属性common @研发需求 - 查看更换语言之前的title属性title @研发需求名称 - 查看更换语言之前的title属性title @用户需求名称 @@ -32,7 +33,7 @@ r($beforeLang) && p('create') && e('提研发需求'); //查看更换语言之 r($afterLang) && p('create') && e('提用户需求'); //查看更换语言之前的create r($beforeLang) && p('common') && e('研发需求'); //查看更换语言之前的common -r($afterLang) && p('common') && e('用户需求'); //查看更换语言之前的common +r($afterLang) && p('common') && e('研发需求'); //查看更换语言之前的common r($beforeLang) && p('title') && e('研发需求名称'); //查看更换语言之前的title -r($afterLang) && p('title') && e('用户需求名称'); //查看更换语言之前的title +r($afterLang) && p('title') && e('用户需求名称'); //查看更换语言之前的title \ No newline at end of file diff --git a/module/story/test/model/reportcondition.php b/module/story/test/model/reportcondition.php index 6f445894c4..a867ca3ace 100755 --- a/module/story/test/model/reportcondition.php +++ b/module/story/test/model/reportcondition.php @@ -4,11 +4,12 @@ /** title=测试 storyModel->reportCondition(); +timeout=0 cid=0 -- 执行story模块的reportCondition方法 @1=1 -- 执行story模块的reportCondition方法 @`title` like '%aa%' -- 执行story模块的reportCondition方法 @id in (SELECT t1.id FROM `zt_story` AS t1 WHERE t1.`title` like '%aa%') +- 执行story模块的reportCondition方法,参数是'story' @1=1 +- 执行story模块的reportCondition方法,参数是'story' @`title` like '%aa%' +- 执行story模块的reportCondition方法,参数是'story' @id in (SELECT t1.id FROM `zt_story` AS t1 WHERE t1.`title` like '%aa%') */ include dirname(__FILE__, 5) . '/test/lib/init.php'; @@ -16,12 +17,12 @@ su('admin'); global $tester; $tester->loadModel('story'); -r($tester->story->reportCondition()) && p() && e('1=1'); +r($tester->story->reportCondition('story')) && p() && e('1=1'); $_SESSION['storyOnlyCondition'] = true; $_SESSION['storyQueryCondition'] = "`title` like '%aa%'"; -r($tester->story->reportCondition()) && p() && e("`title` like '%aa%'"); +r($tester->story->reportCondition('story')) && p() && e("`title` like '%aa%'"); $_SESSION['storyOnlyCondition'] = false; $_SESSION['storyQueryCondition'] = "SELECT * FROM `zt_story` AS t1 WHERE t1.`title` like '%aa%'"; -r($tester->story->reportCondition()) && p() && e("id in (SELECT t1.id FROM `zt_story` AS t1 WHERE t1.`title` like '%aa%')"); +r($tester->story->reportCondition('story')) && p() && e("id in (SELECT t1.id FROM `zt_story` AS t1 WHERE t1.`title` like '%aa%')"); \ No newline at end of file diff --git a/module/story/test/model/review.php b/module/story/test/model/review.php index 84afbf3065..99b12d3f8a 100755 --- a/module/story/test/model/review.php +++ b/module/story/test/model/review.php @@ -20,7 +20,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/setstage.php b/module/story/test/model/setstage.php index f52d58031e..190bab9193 100755 --- a/module/story/test/model/setstage.php +++ b/module/story/test/model/setstage.php @@ -4,13 +4,14 @@ /** title=测试 storyModel->setStage(); +timeout=0 cid=0 - 不传入需求; @0 -- 传入已经分解任务的需求,检查需求阶段。属性stage @testing +- 传入已经分解任务的需求,检查需求阶段。属性stage @planning - 传入已关闭的需求,检查需求阶段。属性stage @closed - 传入只关联的计划的需求,检查需求阶段。属性stage @planned -- 传入只关联的项目的需求,检查需求阶段。属性stage @projected +- 传入只关联的项目的需求,检查需求阶段。属性stage @planning - 传入关联多分支,并分解成任务的需求,检查需求阶段。属性stage @testing - 传入关联多分支,并分解成任务的需求,检查分支 0 的需求阶段。属性stage @testing - 传入关联多分支,并分解成任务的需求,检查分支 1 的需求阶段。属性stage @tested @@ -47,6 +48,7 @@ $story = zenData('story'); $story->product->range('1,2,2,1,1'); $story->plan->range('1,0,1,1,1'); $story->branch->range('0,0,1,0,0'); +$story->stage->range('defining,testing,closed,planned,planning'); $story->gen(5); $task = zenData('task'); @@ -71,13 +73,13 @@ $storyModel = $tester->loadModel('story'); r($storyModel->setStage(0)) && p() && e('0'); //不传入需求; $storyTest = new storyTest(); -r($storyTest->setStageTest(1)) && p('stage') && e('testing'); //传入已经分解任务的需求,检查需求阶段。 -r($storyTest->setStageTest(3)) && p('stage') && e('closed'); //传入已关闭的需求,检查需求阶段。 -r($storyTest->setStageTest(4)) && p('stage') && e('planned'); //传入只关联的计划的需求,检查需求阶段。 -r($storyTest->setStageTest(5)) && p('stage') && e('projected'); //传入只关联的项目的需求,检查需求阶段。 +r($storyTest->setStageTest(1)) && p('stage') && e('planning'); //传入已经分解任务的需求,检查需求阶段。 +r($storyTest->setStageTest(3)) && p('stage') && e('closed'); //传入已关闭的需求,检查需求阶段。 +r($storyTest->setStageTest(4)) && p('stage') && e('planned'); //传入只关联的计划的需求,检查需求阶段。 +r($storyTest->setStageTest(5)) && p('stage') && e('planning'); //传入只关联的项目的需求,检查需求阶段。 $story = $storyTest->setStageTest(2); r($story) && p('stage') && e('testing'); //传入关联多分支,并分解成任务的需求,检查需求阶段。 r($story->stages[0]) && p('stage') && e('testing'); //传入关联多分支,并分解成任务的需求,检查分支 0 的需求阶段。 r($story->stages[1]) && p('stage') && e('tested'); //传入关联多分支,并分解成任务的需求,检查分支 1 的需求阶段。 -r($story->stages[2]) && p('stage') && e('released'); //传入关联多分支,并分解成任务的需求,检查分支 2 的需求阶段。 +r($story->stages[2]) && p('stage') && e('released'); //传入关联多分支,并分解成任务的需求,检查分支 2 的需求阶段。 \ No newline at end of file diff --git a/module/story/test/model/subdivide.php b/module/story/test/model/subdivide.php index 54db630fa8..af559ec445 100755 --- a/module/story/test/model/subdivide.php +++ b/module/story/test/model/subdivide.php @@ -4,27 +4,15 @@ /** title=测试 storyModel->subdivide(); +timeout=0 cid=0 -- 将用户需求1拆分两个软件需求,查看relation表记录的数量。 @4 - 将用户需求1拆分两个软件需求,查看relation表记录的关系。 - - 属性AID @1 - - 属性BID @2 - - 属性relation @subdivideinto -- 将用户需求1拆分两个软件需求,查看relation表记录的关系。 - - 属性AID @2 - - 属性BID @1 - - 属性relation @subdividedfrom -- 将用户需求1拆分两个软件需求,查看relation表记录的关系。 - - 属性AID @1 - - 属性BID @3 - - 属性relation @subdivideinto -- 将用户需求1拆分两个软件需求,查看relation表记录的关系。 - - 属性AID @3 - - 属性BID @1 - - 属性relation @subdividedfrom + - 属性isParent @1 + - 属性root @0 + - 属性grade @0 - 将软件需求2拆分两个子需求,查看子需求的数量。 @2 -- 将软件需求2拆分两个子需求,查看父需求的parent字段。 @-1 +- 将软件需求2拆分两个子需求,查看父需求的parent字段。 @0 - 将软件需求2拆分两个子需求,查看子需求5的parent字段。 @4 */ @@ -50,11 +38,7 @@ $story = new storyTest(); $requirementResult = $story->subdivideTest(1, array(2, 3), 'requirement'); $childrenResult = $story->subdivideTest(4, array(5, 6), 'story'); -r(count($requirementResult)) && p() && e('4'); // 将用户需求1拆分两个软件需求,查看relation表记录的数量。 -r($requirementResult[0]) && p('AID,BID,relation') && e('1,2,subdivideinto'); // 将用户需求1拆分两个软件需求,查看relation表记录的关系。 -r($requirementResult[1]) && p('AID,BID,relation') && e('2,1,subdividedfrom'); // 将用户需求1拆分两个软件需求,查看relation表记录的关系。 -r($requirementResult[2]) && p('AID,BID,relation') && e('1,3,subdivideinto'); // 将用户需求1拆分两个软件需求,查看relation表记录的关系。 -r($requirementResult[3]) && p('AID,BID,relation') && e('3,1,subdividedfrom'); // 将用户需求1拆分两个软件需求,查看relation表记录的关系。 -r(count($childrenResult->children)) && p() && e('2'); // 将软件需求2拆分两个子需求,查看子需求的数量。 -r($childrenResult->parent) && p() && e('-1'); // 将软件需求2拆分两个子需求,查看父需求的parent字段。 -r($childrenResult->children[5]->parent) && p() && e('4'); // 将软件需求2拆分两个子需求,查看子需求5的parent字段。 +r($requirementResult) && p('isParent,root,grade') && e('1,0,0'); // 将用户需求1拆分两个软件需求,查看relation表记录的关系。 +r(count($childrenResult->children)) && p() && e('2'); // 将软件需求2拆分两个子需求,查看子需求的数量。 +r($childrenResult->parent) && p() && e('0'); // 将软件需求2拆分两个子需求,查看父需求的parent字段。 +r($childrenResult->children[0]->parent) && p() && e('4'); // 将软件需求2拆分两个子需求,查看子需求5的parent字段。 \ No newline at end of file diff --git a/module/story/test/model/submitreview.php b/module/story/test/model/submitreview.php index 6298b44ec1..6db51ec03e 100644 --- a/module/story/test/model/submitreview.php +++ b/module/story/test/model/submitreview.php @@ -21,7 +21,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/model/unlinkstory.php b/module/story/test/model/unlinkstory.php index 5d50e2cff4..a46b97dd51 100755 --- a/module/story/test/model/unlinkstory.php +++ b/module/story/test/model/unlinkstory.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->unlinkStory(); +timeout=0 cid=0 - 删除用户需求1的关联关系之前,获取关联关系数量 @1 @@ -19,7 +20,7 @@ $relation->AID->range('1,11,1,2,12,2,3,13,3,4,14,4,5,15,5,6,16,6,7,17,7,8,18,8') $relation->BID->range('11,1,1,12,2,2,13,3,3,14,4,4,15,5,5,16,6,6,17,7,7,18,8,8'); $relation->AType->range('requirement,story,design'); $relation->BType->range('story,requirement,commit'); -$relation->relation->range('subdivideinto,subdividedfrom,completedin'); +$relation->relation->range('linkedto,linkedfrom,completedin'); $relation->gen(24); global $tester; @@ -30,4 +31,4 @@ $tester->story->unlinkStory(1, 11); $afterRelation = $tester->story->getRelation(1, 'requirement'); r(count($beforeRelation)) && p() && e('1'); //删除用户需求1的关联关系之前,获取关联关系数量 -r(count($afterRelation)) && p() && e('0'); //删除用户需求1的关联关系之后,获取关联关系数量 +r(count($afterRelation)) && p() && e('0'); //删除用户需求1的关联关系之后,获取关联关系数量 \ No newline at end of file diff --git a/module/story/test/model/update.php b/module/story/test/model/update.php index 13e678630d..06662b8518 100755 --- a/module/story/test/model/update.php +++ b/module/story/test/model/update.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->update(); +timeout=0 cid=0 - 编辑用户需求,判断返回的信息,stage为空 @@ -28,7 +29,7 @@ zenData('product')->gen(30); $story = new storyTest(); $story1 = $tester->loadModel('story')->fetchByID(2); -$story1->parent = 2; +$story1->parent = 1; $story1->pri = 4; $story1->plan = '0'; $story1->estimate = 1; @@ -52,4 +53,4 @@ $result1 = $story->updateTest(2, clone($story1)); $result2 = $story->updateTest(4, clone($story1)); r($result1) && p('pri,estimate,sourceNote,product') && e('4,1,测试来源备注1,2'); // 编辑用户需求,判断返回的信息,stage为空 -r($result2) && p('pri,estimate,sourceNote,product') && e('4,1,测试来源备注1,2'); // 编辑软件需求,判断返回的信息,stage为wait,parent为2 +r($result2) && p('pri,estimate,sourceNote,product') && e('4,1,测试来源备注1,2'); // 编辑软件需求,判断返回的信息,stage为wait,parent为2 \ No newline at end of file diff --git a/module/story/test/model/updateparentstatus.php b/module/story/test/model/updateparentstatus.php index 3cc0655e38..8cf404bc05 100755 --- a/module/story/test/model/updateparentstatus.php +++ b/module/story/test/model/updateparentstatus.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->updateParentStatus(); +timeout=0 cid=0 - 只有一个子需求,并且需求状态为closed,检查父需求的状态。 @closed @@ -34,7 +35,7 @@ $story4 = $storyModel->fetchByID(4); $story15 = $storyModel->fetchByID(15); $story16 = $storyModel->fetchByID(16); -r($story2->status) && p() && e('closed'); // 只有一个子需求,并且需求状态为closed,检查父需求的状态。 -r($story4->parent) && p() && e('0'); // 没有子需求的父需求,检查父需求的parent字段。 -r($story15->status) && p() && e('active'); // 已经关闭的父需求,有激活状态的子需求,检查父需求的状态。 -r($story16->parent) && p() && e('0'); // 没有父任务的子需求,检查子需求的parent字段。 +r($story2->status) && p() && e('closed'); // 只有一个子需求,并且需求状态为closed,检查父需求的状态。 +r($story4->isParent) && p() && e('0'); // 没有子需求的父需求,检查父需求的parent字段。 +r($story15->status) && p() && e('active'); // 已经关闭的父需求,有激活状态的子需求,检查父需求的状态。 +r($story16->parent) && p() && e('0'); // 没有父任务的子需求,检查子需求的parent字段。 \ No newline at end of file diff --git a/module/story/test/model/updatestorybyreview.php b/module/story/test/model/updatestorybyreview.php index 3822d2e8d7..7b9e59da02 100755 --- a/module/story/test/model/updatestorybyreview.php +++ b/module/story/test/model/updatestorybyreview.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->updateStoryByReview(); +timeout=0 cid=0 - 执行storyModel模块的updateStoryByReview方法,参数是1, $oldStory, $story 属性status @draft @@ -28,6 +29,7 @@ $storyModel = $tester->loadModel('story'); $story = new stdclass(); $story->status = 'draft'; +$story->reviewedBy = 'admin,user1'; $oldStory = new stdclass(); $oldStory->status = 'draft'; @@ -42,4 +44,4 @@ $storyModel->config->story->superReviewers = ''; $story->reviewedBy = 'admin'; r($storyModel->updateStoryByReview(1, $oldStory, $story)) && p('status') && e('draft'); $story->reviewedBy = 'admin,user1,user2'; -r($storyModel->updateStoryByReview(1, $oldStory, $story)) && p('status') && e('active'); +r($storyModel->updateStoryByReview(1, $oldStory, $story)) && p('status') && e('active'); \ No newline at end of file diff --git a/module/story/test/model/updatestoryproduct.php b/module/story/test/model/updatestoryproduct.php index 222fce26a4..1efe31a65b 100755 --- a/module/story/test/model/updatestoryproduct.php +++ b/module/story/test/model/updatestoryproduct.php @@ -4,6 +4,7 @@ /** title=测试 storyModel->updateStoryProduct(); +timeout=0 cid=0 - 判断需求变更所属产品之前的产品ID属性product @1 @@ -14,15 +15,20 @@ include dirname(__FILE__, 5) . '/test/lib/init.php'; include dirname(__FILE__, 2) . '/lib/story.unittest.class.php'; su('admin'); -zenData('story')->gen(1); +$story = zenData('story'); +$story->parent->range('0,1'); +$story->gen(2); zenData('storyspec')->gen(5); $story = new storyTest(); global $tester; -$oldStory = $tester->loadModel('story')->getByID(1); -$tester->story->updateStoryProduct(1, 2); -$newStory = $tester->story->getByID(1); +$parent = $tester->loadModel('story')->getByID(1); +$parent->product = 2; + +$oldStory = $tester->story->getByID(2); +$tester->story->updateStoryProduct(2, $parent, 1); +$newStory = $tester->story->getByID(2); r($oldStory) && p('product') && e('1'); // 判断需求变更所属产品之前的产品ID -r($newStory) && p('product') && e('2'); // 判断需求变更所属产品之后的产品ID +r($newStory) && p('product') && e('2'); // 判断需求变更所属产品之后的产品ID \ No newline at end of file diff --git a/module/story/test/model/updatestoryversion.php b/module/story/test/model/updatestoryversion.php deleted file mode 100755 index 9d5c064eb0..0000000000 --- a/module/story/test/model/updatestoryversion.php +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env php -updateStoryVersion(); -cid=0 - -- 执行story模块的updateStoryVersionTest方法,参数是12 属性AVersion @2 - -*/ -include dirname(__FILE__, 5) . '/test/lib/init.php'; -include dirname(__FILE__, 2) . '/lib/story.unittest.class.php'; -su('admin'); - -zenData('story')->gen(20); -zenData('product')->gen(1); -$story = zenData('story'); -$story->product->range(1); -$story->version->range(1); -$story->gen(20); - -$storySpec = zenData('storyspec'); -$storySpec->story->range('1-20{2}'); -$storySpec->version->range('1-2'); -$storySpec->gen(40); -zenData('storyspec')->gen(20); -$relation = zenData('relation'); -$relation->AID->range('1,10,2,11,3,12,4,13,5,14,6,15,7,16,8,17'); -$relation->AVersion->range('1'); -$relation->BID->range('10,1,11,2,12,3,13,4,14,5,15,6,16,7,17,8'); -$relation->BVersion->range('1'); -$relation->gen(16); - -$story = new storyTest(); -r($story->updateStoryVersionTest(12)) && p('AVersion') && e('2'); diff --git a/module/story/test/tao/buildtrackcol.php b/module/story/test/tao/buildtrackcol.php new file mode 100644 index 0000000000..703213210a --- /dev/null +++ b/module/story/test/tao/buildtrackcol.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +buildTrackCol(); +cid=0 + +- 执行story模块的buildTrackCol方法,参数是'epic', '业务需求' + - 属性name @epic + - 属性title @业务需求 + - 属性parent @0 +- 执行story模块的buildTrackCol方法,参数是'epic', '业务需求', '-1' + - 属性name @epic + - 属性title @业务需求 + - 属性parent @-1 +- 执行story模块的buildTrackCol方法,参数是'epic_1', 'BR1', 'epic' + - 属性name @epic_1 + - 属性title @BR1 + - 属性parent @epic + - 属性parentName @epic + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +global $tester; +$tester->loadModel('story'); + +r($tester->story->buildTrackCol('epic', '业务需求')) && p('name,title,parent') && e('epic,业务需求,0'); +r($tester->story->buildTrackCol('epic', '业务需求', '-1')) && p('name,title,parent') && e('epic,业务需求,-1'); +r($tester->story->buildTrackCol('epic_1', 'BR1', 'epic')) && p('name,title,parent,parentName') && e('epic_1,BR1,epic,epic'); diff --git a/module/story/test/tao/buildtrackcols.php b/module/story/test/tao/buildtrackcols.php new file mode 100644 index 0000000000..1ab8148e40 --- /dev/null +++ b/module/story/test/tao/buildtrackcols.php @@ -0,0 +1,136 @@ +#!/usr/bin/env php +buildTrackCols(); +timeout=0 +cid=0 + +- 执行$epicCols @16 +- 执行$epicCols[0] + - 属性name @epic + - 属性title @业务需求 + - 属性parent @-1 +- 执行$epicCols[1] + - 属性name @requirement + - 属性title @用户需求 + - 属性parent @-1 +- 执行$epicCols[2] + - 属性name @story + - 属性title @研发需求 + - 属性parent @-1 +- 执行$epicCols[10] + - 属性name @story_1 + - 属性title @SR1 + - 属性parent @story + - 属性parentName @story +- 执行$epicCols[13] + - 属性name @requirement_2 + - 属性title @UR2 + - 属性parent @requirement + - 属性parentName @requirement +- 执行$epicCols[15] + - 属性name @epic_2 + - 属性title @BR2 + - 属性parent @epic + - 属性parentName @epic +- 执行$requirementCols @13 +- 执行$requirementCols[0] + - 属性name @requirement + - 属性title @用户需求 + - 属性parent @-1 +- 执行$requirementCols[1] + - 属性name @story + - 属性title @研发需求 + - 属性parent @-1 +- 执行$requirementCols[10] + - 属性name @story_2 + - 属性title @SR2 + - 属性parent @story + - 属性parentName @story +- 执行$requirementCols[12] + - 属性name @requirement_2 + - 属性title @UR2 + - 属性parent @requirement + - 属性parentName @requirement +- 执行$storyCols @10 +- 执行$storyCols[0] + - 属性name @story + - 属性title @研发需求 + - 属性parent @-1 +- 执行$storyCols[9] + - 属性name @story_2 + - 属性title @SR2 + - 属性parent @story + - 属性parentName @story +- 执行$epicCols[0] + - 属性name @epic_1 + - 属性title @业务需求 + - 属性parent @0 +- 执行$epicCols[1] + - 属性name @requirement + - 属性title @用户需求 + - 属性parent @-1 +- 执行$epicCols[2] + - 属性name @story + - 属性title @研发需求 + - 属性parent @-1 +- 执行$epicCols[10] + - 属性name @story_1 + - 属性title @SR1 + - 属性parent @story + - 属性parentName @story +- 执行$epicCols[13] + - 属性name @requirement_2 + - 属性title @UR2 + - 属性parent @requirement + - 属性parentName @requirement + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +$storyGrade = zenData('storygrade'); +$storyGrade->type->range('epic{2},requirement{2},story{2}'); +$storyGrade->grade->range('1,2'); +$storyGrade->name->range('BR1,BR2,UR1,UR2,SR1,SR2'); +$storyGrade->status->range('enable'); +$storyGrade->gen(6); + +global $tester; +$tester->loadModel('story'); + +$tester->story->lang->ERCommon = '业务需求'; +$tester->story->lang->URCommon = '用户需求'; +$tester->story->lang->SRCommon = '研发需求'; + +$epicCols = $tester->story->buildTrackCols('epic'); +$requirementCols = $tester->story->buildTrackCols('requirement'); +$storyCols = $tester->story->buildTrackCols('story'); + +r(count($epicCols)) && p() && e(16); +r($epicCols[0]) && p('name,title,parent') && e('epic,业务需求,-1'); +r($epicCols[1]) && p('name,title,parent') && e('requirement,用户需求,-1'); +r($epicCols[2]) && p('name,title,parent') && e('story,研发需求,-1'); +r($epicCols[10]) && p('name,title,parent,parentName') && e('story_1,SR1,story,story'); +r($epicCols[13]) && p('name,title,parent,parentName') && e('requirement_2,UR2,requirement,requirement'); +r($epicCols[15]) && p('name,title,parent,parentName') && e('epic_2,BR2,epic,epic'); + +r(count($requirementCols)) && p() && e(13); +r($requirementCols[0]) && p('name,title,parent') && e('requirement,用户需求,-1'); +r($requirementCols[1]) && p('name,title,parent') && e('story,研发需求,-1'); +r($requirementCols[10]) && p('name,title,parent,parentName') && e('story_2,SR2,story,story'); +r($requirementCols[12]) && p('name,title,parent,parentName') && e('requirement_2,UR2,requirement,requirement'); + +r(count($storyCols)) && p() && e(10); +r($storyCols[0]) && p('name,title,parent') && e('story,研发需求,-1'); +r($storyCols[9]) && p('name,title,parent,parentName') && e('story_2,SR2,story,story'); + +$tester->story->dao->delete()->from(TABLE_STORYGRADE)->where('type')->eq('epic')->andWhere('grade')->gt('1')->exec(); + +$epicCols = $tester->story->buildTrackCols('epic'); +r($epicCols[0]) && p('name,title,parent') && e('epic_1,业务需求,0'); +r($epicCols[1]) && p('name,title,parent') && e('requirement,用户需求,-1'); +r($epicCols[2]) && p('name,title,parent') && e('story,研发需求,-1'); +r($epicCols[10]) && p('name,title,parent,parentName') && e('story_1,SR1,story,story'); +r($epicCols[13]) && p('name,title,parent,parentName') && e('requirement_2,UR2,requirement,requirement'); diff --git a/module/story/test/tao/buildtrackitems.php b/module/story/test/tao/buildtrackitems.php new file mode 100644 index 0000000000..d680dc496d --- /dev/null +++ b/module/story/test/tao/buildtrackitems.php @@ -0,0 +1,121 @@ +#!/usr/bin/env php +buildTrackItems(); +cid=0 + +- 执行$epicItems['lane_11']['epic_1'][0]属性id @11 +- 执行$epicItems['lane_10']['epic_1'][0]属性id @1 +- 执行$epicItems['lane_10']['requirement_2'][0]属性id @3 +- 执行$epicItems['lane_10']['project'][0]属性id @1 +- 执行$epicItems['lane_4']['task'][0]属性id @4 +- 执行$epicItems['lane_4']['bug'][0]属性id @4 +- 执行$epicItems['lane_4']['case'][0]属性id @4 +- 执行$epicItems['lane_10']['requirement_2'][0]属性id @3 +- 执行$epicItems['lane_10']['project'][0]属性id @1 +- 执行$epicItems['lane_4']['task'][0]属性id @4 +- 执行$epicItems['lane_4']['bug'][0]属性id @4 +- 执行$epicItems['lane_4']['case'][0]属性id @4 +- 执行$epicItems['lane_10']['requirement_2'][0]属性id @3 +- 执行$epicItems['lane_10']['project'][0]属性id @1 +- 执行$epicItems['lane_5']['task'][0]属性id @5 +- 执行$epicItems['lane_5']['bug'][0]属性id @5 +- 执行$epicItems['lane_5']['case'][0]属性id @5 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +$projectStory = zenData('projectstory'); +$projectStory->project->range('1-2{5}'); +$projectStory->product->range('1'); +$projectStory->story->range('6-10'); +$projectStory->gen(10); + +$project = zenData('project'); +$project->id->range('1-2'); +$project->type->range('project,scrum'); +$project->gen('2'); + +$design = zenData('design'); +$design->project->range('2{5}'); +$design->product->range('1'); +$design->story->range('6-10'); +$design->commit->range('5-1'); +$design->gen(5); + +$commit = zenData('repohistory'); +$commit->id->range('1-5'); +$commit->comment->range('1-5')->prefix('comment'); +$commit->gen('5'); + +$story = zenData('story'); +$story->product->range('1'); +$story->root->range('1{10},11'); +$story->grade->range('1,2{3},3{6},1'); +$story->parent->range('0,1{3},2{3},3{3},0'); +$story->type->range('epic,requirement{3},story{6},epic'); +$story->gen(11)->fixPath(); + +$task = zenData('task'); +$task->story->range('1-5'); +$task->gen('10'); + +$bug = zenData('bug'); +$bug->story->range('1-5'); +$bug->gen('10'); + +$case = zenData('case'); +$case->story->range('1-5'); +$case->gen('10'); + +$storyGrade = zenData('storygrade'); +$storyGrade->type->range('epic{2},requirement{2},story{2}'); +$storyGrade->grade->range('1,2'); +$storyGrade->name->range('BR1,BR2,UR1,UR2,SR1,SR2'); +$storyGrade->status->range('enable'); +$storyGrade->gen(6); + +su('admin'); + +global $tester; +$tester->loadModel('story'); + +$allStoryIdList = array(1,2,3,4,5,6,7,8,9,10,11); +$stories = $tester->story->dao->select('*')->from(TABLE_STORY)->where('id')->in('1,11')->orderBy('id_desc')->fetchAll('id'); +$leafNodes = $tester->story->getLeafNodes($stories, $allStoryIdList); +$allStories = $tester->story->dao->select('id,parent,isParent,root,path,grade,product,pri,type,status,stage,title,estimate')->from(TABLE_STORY)->where('root')->in('1,11')->andWhere('deleted')->eq(0)->orderBy('type,grade,parent')->fetchAll('id'); + +$epicItems = $tester->story->buildTrackItems($allStories, $leafNodes, 'epic'); +r((array)$epicItems['lane_11']['epic_1'][0]) && p('id') && e('11'); +r((array)$epicItems['lane_10']['epic_1'][0]) && p('id') && e('1'); +r((array)$epicItems['lane_10']['requirement_2'][0]) && p('id') && e('3'); +r((array)$epicItems['lane_10']['project'][0]) && p('id') && e('1'); +r((array)$epicItems['lane_4']['task'][0]) && p('id') && e('4'); +r((array)$epicItems['lane_4']['bug'][0]) && p('id') && e('4'); +r((array)$epicItems['lane_4']['case'][0]) && p('id') && e('4'); + +$allStoryIdList = array(2,3,4,5,6,7,8,9,10); +$stories = $tester->story->dao->select('*')->from(TABLE_STORY)->where('id')->in('2,3,4')->orderBy('id_desc')->fetchAll('id'); +$leafNodes = $tester->story->getLeafNodes($stories, $allStoryIdList); +$allStories = $tester->story->dao->select('id,parent,isParent,root,path,grade,product,pri,type,status,stage,title,estimate')->from(TABLE_STORY)->where('root')->in('1')->andWhere('deleted')->eq(0)->orderBy('type,grade,parent')->fetchAll('id'); + +$requirementItems = $tester->story->buildTrackItems($allStories, $leafNodes, 'requirement'); +r((array)$epicItems['lane_10']['requirement_2'][0]) && p('id') && e('3'); +r((array)$epicItems['lane_10']['project'][0]) && p('id') && e('1'); +r((array)$epicItems['lane_4']['task'][0]) && p('id') && e('4'); +r((array)$epicItems['lane_4']['bug'][0]) && p('id') && e('4'); +r((array)$epicItems['lane_4']['case'][0]) && p('id') && e('4'); + +$allStoryIdList = array(5,6,7,8,9,10); +$stories = $tester->story->dao->select('*')->from(TABLE_STORY)->where('id')->in('5,6,7,8,9,10')->orderBy('id_desc')->fetchAll('id'); +$leafNodes = $tester->story->getLeafNodes($stories, $allStoryIdList); +$allStories = $tester->story->dao->select('id,parent,isParent,root,path,grade,product,pri,type,status,stage,title,estimate')->from(TABLE_STORY)->where('root')->in('1')->andWhere('deleted')->eq(0)->orderBy('type,grade,parent')->fetchAll('id'); + +$storyItems = $tester->story->buildTrackItems($allStories, $leafNodes, 'story'); +r((array)$epicItems['lane_10']['requirement_2'][0]) && p('id') && e('3'); +r((array)$epicItems['lane_10']['project'][0]) && p('id') && e('1'); +r((array)$epicItems['lane_5']['task'][0]) && p('id') && e('5'); +r((array)$epicItems['lane_5']['bug'][0]) && p('id') && e('5'); +r((array)$epicItems['lane_5']['case'][0]) && p('id') && e('5'); diff --git a/module/story/test/tao/buildtracklanes.php b/module/story/test/tao/buildtracklanes.php new file mode 100644 index 0000000000..5a15a2e11e --- /dev/null +++ b/module/story/test/tao/buildtracklanes.php @@ -0,0 +1,31 @@ +#!/usr/bin/env php +buildTrackLanes(); +timeout=0 +cid=0 + +- 传入空参数。 @0 +- 检查泳道数。 @4 +- 检查第一个泳道的name参数。 @lane_1 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +global $tester; +$tester->loadModel('story'); + +$leafNodes = array(); +foreach(array(1,2,3,4) as $storyID) +{ + $story = new stdclass(); + $story->id = $storyID; + $leafNodes[$storyID] = $story; +} +r(count($tester->story->buildTrackLanes(array()))) && p() && e('0'); //传入空参数。 + +$lanes = $tester->story->buildTrackLanes($leafNodes); +r(count($lanes)) && p() && e('4'); //检查泳道数。 +r($lanes[0]['name']) && p() && e('lane_1'); //检查第一个泳道的name参数。 diff --git a/module/story/test/tao/computeparentstage.php b/module/story/test/tao/computeparentstage.php new file mode 100755 index 0000000000..08cccab4dd --- /dev/null +++ b/module/story/test/tao/computeparentstage.php @@ -0,0 +1,100 @@ +#!/usr/bin/env php +gen(1); + +$project = zdTable('project'); +$project->id->range('11-20'); +$project->type->range('project,project,sprint,kanban'); +$project->model->range('scrum,kanban'); +$project->gen(10); + +$projectStory = zdTable('projectstory'); +$projectStory->story->range('6-20'); +$projectStory->product->range('1'); +$projectStory->project->range('11-20'); +$projectStory->gen(15); + +$projectProduct = zdTable('projectproduct'); +$projectProduct->product->range('1'); +$projectProduct->project->range('11-20'); +$projectProduct->gen(10); + +$story = zdTable('story'); +$story->product->range('1'); +$story->plan->range('`1,2`,2,3,4,5'); +$story->type->range('story'); +$story->parent->range('0{5},1-5{3}'); +$story->root->range('1-5,1-5{3}'); +$story->isParent->range('1{5},0{15}'); +$story->stage->range('wait,planned,developing,tested,released'); +$story->gen(20)->fixPath(); + +$task = zdTable('task'); +$task->story->range('6-20'); +$task->execution->range('11-20'); +$task->type->range('devel,test,design'); +$task->gen(100); + +$release = zdTable('release'); +$release->stories->range('6-10'); +$release->gen(5); + +/** + +title=测试 storyModel->setStage(); +timeout=0 +cid=1 + +- 查看父需求初始阶段属性stage @wait +- 查看父需求初始阶段属性stage @planned +- 查看父需求初始阶段属性stage @developing +- 查看父需求初始阶段属性stage @tested +- 查看父需求初始阶段属性stage @released +- 通过子需求计算父需求的阶段属性stage @developing +- 通过子需求计算父需求的阶段属性stage @delivering +- 通过子需求计算父需求的阶段属性stage @developing +- 通过子需求计算父需求的阶段属性stage @delivering +- 通过子需求计算父需求的阶段属性stage @delivering + +*/ + +global $tester; +$storyModel = $tester->loadModel('story'); +$p1 = $tester->story->fetchById(1); +$p2 = $tester->story->fetchById(2); +$p3 = $tester->story->fetchById(3); +$p4 = $tester->story->fetchById(4); +$p5 = $tester->story->fetchById(5); + +r($p1) && p('stage') && e('wait'); // 查看父需求初始阶段 +r($p2) && p('stage') && e('planned'); // 查看父需求初始阶段 +r($p3) && p('stage') && e('developing'); // 查看父需求初始阶段 +r($p4) && p('stage') && e('tested'); // 查看父需求初始阶段 +r($p5) && p('stage') && e('released'); // 查看父需求初始阶段 + +$c1 = $tester->story->fetchById(6); +$c2 = $tester->story->fetchById(9); +$c3 = $tester->story->fetchById(12); +$c4 = $tester->story->fetchById(15); +$c5 = $tester->story->fetchById(18); + +$tester->story->computeParentStage($c1); +$tester->story->computeParentStage($c2); +$tester->story->computeParentStage($c3); +$tester->story->computeParentStage($c4); +$tester->story->computeParentStage($c5); + +$p1 = $tester->story->fetchById(1); +$p2 = $tester->story->fetchById(2); +$p3 = $tester->story->fetchById(3); +$p4 = $tester->story->fetchById(4); +$p5 = $tester->story->fetchById(5); + +r($p1) && p('stage') && e('developing'); // 通过子需求计算父需求的阶段 +r($p2) && p('stage') && e('delivering'); // 通过子需求计算父需求的阶段 +r($p3) && p('stage') && e('developing'); // 通过子需求计算父需求的阶段 +r($p4) && p('stage') && e('delivering'); // 通过子需求计算父需求的阶段 +r($p5) && p('stage') && e('delivering'); // 通过子需求计算父需求的阶段 \ No newline at end of file diff --git a/module/story/test/tao/dochangeparent.php b/module/story/test/tao/dochangeparent.php index b334de30e7..d444dd9203 100755 --- a/module/story/test/tao/dochangeparent.php +++ b/module/story/test/tao/dochangeparent.php @@ -18,7 +18,7 @@ $story = zenData('story'); $story->id->range('1-100'); $story->title->range('teststory'); $story->product->range('1'); -$story->parent->range('`-1`,`-1`,0,1,1'); +$story->parent->range('0,0,0,1,1'); $story->version->range('1'); $story->gen(5); @@ -33,4 +33,4 @@ $storyModel->doChangeParent(3, $story, 2); r($storyModel->dao->select('*')->from(TABLE_STORY)->where('id')->eq(2)->fetch()) && p('parent') && e('0'); $story->parent = 1; $storyModel->doChangeParent(3, $story, 0); -r($storyModel->dao->select('*')->from(TABLE_STORY)->where('id')->eq(1)->fetch()) && p('childStories', '|') && e('4,5'); +r($storyModel->dao->select('*')->from(TABLE_STORY)->where('id')->eq(1)->fetch()) && p('isParent') && e('1'); diff --git a/module/story/test/tao/getaffectedbugs.php b/module/story/test/tao/getaffectedbugs.php index f716a71981..8974f0c8a9 100644 --- a/module/story/test/tao/getaffectedbugs.php +++ b/module/story/test/tao/getaffectedbugs.php @@ -19,7 +19,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/tao/getaffectedcases.php b/module/story/test/tao/getaffectedcases.php index ae29c1128a..7647c51614 100644 --- a/module/story/test/tao/getaffectedcases.php +++ b/module/story/test/tao/getaffectedcases.php @@ -19,7 +19,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/tao/getaffectedprojects.php b/module/story/test/tao/getaffectedprojects.php index 05b88e9228..ec24ada25e 100644 --- a/module/story/test/tao/getaffectedprojects.php +++ b/module/story/test/tao/getaffectedprojects.php @@ -20,7 +20,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/tao/getaffectedtwins.php b/module/story/test/tao/getaffectedtwins.php index 5903318873..380c866294 100644 --- a/module/story/test/tao/getaffectedtwins.php +++ b/module/story/test/tao/getaffectedtwins.php @@ -19,7 +19,6 @@ $story->plan->range('0,1,0{100}'); $story->duplicateStory->range('0,4,0{100}'); $story->linkStories->range('0,6,0{100}'); $story->linkRequirements->range('3,0{100}'); -$story->childStories->range('0,8,0{100}'); $story->toBug->range('0{9},1,0{100}'); $story->parent->range('0{17},`-1`,0,18,0{100}'); $story->twins->range('``{27},30,``,28'); diff --git a/module/story/test/tao/getdesignsfortrack.php b/module/story/test/tao/getdesignsfortrack.php new file mode 100644 index 0000000000..90ba6d8cee --- /dev/null +++ b/module/story/test/tao/getdesignsfortrack.php @@ -0,0 +1,50 @@ +#!/usr/bin/env php +getDesignsForTrack(); +timeout=0 +cid=0 + +- 传入空参数。 @0 +- 执行$designs['design'] @6;7;8;9;10 +- 执行$designs['commit'] @6;7;8;9;10 +- 执行$designs['design'][6] @1 +- 执行$designs['commit'][6] @5 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +$design = zenData('design'); +$design->project->range('2{5}'); +$design->product->range('1'); +$design->story->range('6-10'); +$design->commit->range('5-1'); +$design->gen(5); + +$commit = zenData('repohistory'); +$commit->id->range('1-5'); +$commit->comment->range('1-5')->prefix('comment'); +$commit->gen('5'); + +$story = zenData('story'); +$story->product->range('1'); +$story->root->range('1{10},11'); +$story->grade->range('1,2{3},3{6},1'); +$story->parent->range('0,1{3},2{3},3{3},0'); +$story->type->range('epic,requirement{3},story{6},epic'); +$story->gen(11); + +su('admin'); + +global $tester; +$tester->loadModel('story'); + +r(count($tester->story->getDesignsForTrack(array()))) && p() && e('0'); //传入空参数。 + +$designs = $tester->story->getDesignsForTrack(array(5,6,7,8,9,10)); +r(implode(';', array_keys($designs['design']))) && p() && e('6;7;8;9;10'); +r(implode(';', array_keys($designs['commit']))) && p() && e('6;7;8;9;10'); +r(implode(';', array_keys($designs['design'][6]))) && p() && e('1'); +r(implode(';', array_keys($designs['commit'][6]))) && p() && e('5'); \ No newline at end of file diff --git a/module/story/test/tao/getlastnodes.php b/module/story/test/tao/getlastnodes.php new file mode 100644 index 0000000000..e39ba62f82 --- /dev/null +++ b/module/story/test/tao/getlastnodes.php @@ -0,0 +1,37 @@ +#!/usr/bin/env php +getLeafNodes(); +cid=0 + +- 传入空参数。 @0 +- 只传入 allStoryIdList @0 +- 只传入 stories @0 +- 执行story模块的getLeafNodes方法,参数是$stories, $allStoryIdList @11;10;9;8;7;6;5;4 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +$story = zenData('story'); +$story->product->range('1'); +$story->root->range('1{10},11'); +$story->grade->range('1,2{3},3{6},1'); +$story->parent->range('0,1{3},2{3},3{3},0'); +$story->type->range('epic,requirement{3},story{6},epic'); +$story->gen(11); + +su('admin'); + +global $tester; +$tester->loadModel('story'); + +$allStoryIdList = array(1,2,3,4,5,6,7,8,9,10,11); +$stories = $tester->story->dao->select('*')->from(TABLE_STORY)->where('id')->in('1,11')->orderBy('id_desc')->fetchAll('id'); + +r(count($tester->story->getLeafNodes(array(), array()))) && p() && e('0'); //传入空参数。 +r(count($tester->story->getLeafNodes(array(), $allStoryIdList))) && p() && e('0'); //只传入 allStoryIdList +r(count($tester->story->getLeafNodes($stories, array()))) && p() && e('0'); //只传入 stories + +r(implode(';', array_keys($tester->story->getLeafNodes($stories, $allStoryIdList)))) && p() && e('11;10;9;8;7;6;5;4'); diff --git a/module/story/test/tao/getprojectsfortrack.php b/module/story/test/tao/getprojectsfortrack.php new file mode 100644 index 0000000000..fc09a03dfc --- /dev/null +++ b/module/story/test/tao/getprojectsfortrack.php @@ -0,0 +1,48 @@ +#!/usr/bin/env php +getProjectsForTrack(); +cid=0 + +- 传入空参数。 @0 +- 执行$projects['project'] @6;7;8;9;10 +- 执行$projects['execution'] @6;7;8;9;10 +- 执行$projects['project'][6] @1 +- 执行$projects['execution'][6] @2 + +*/ +include dirname(__FILE__, 5) . '/test/lib/init.php'; + +$projectStory = zenData('projectstory'); +$projectStory->project->range('1-2{5}'); +$projectStory->product->range('1'); +$projectStory->story->range('6-10'); +$projectStory->gen(10); + +$project = zenData('project'); +$project->id->range('1-2'); +$project->type->range('project,scrum'); +$project->gen('2'); + +$story = zenData('story'); +$story->product->range('1'); +$story->root->range('1{10},11'); +$story->grade->range('1,2{3},3{6},1'); +$story->parent->range('0,1{3},2{3},3{3},0'); +$story->type->range('epic,requirement{3},story{6},epic'); +$story->gen(11); + +su('admin'); + +global $tester; +$tester->loadModel('story'); + +r(count($tester->story->getProjectsForTrack(array()))) && p() && e('0'); //传入空参数。 + +$projects = $tester->story->getProjectsForTrack(array(5,6,7,8,9,10)); +r(implode(';', array_keys($projects['project']))) && p() && e('6;7;8;9;10'); +r(implode(';', array_keys($projects['execution']))) && p() && e('6;7;8;9;10'); +r(implode(';', array_keys($projects['project'][6]))) && p() && e('1'); +r(implode(';', array_keys($projects['execution'][6]))) && p() && e('2'); diff --git a/module/story/ui/affected.html.php b/module/story/ui/affected.html.php index 5c5b171edc..213e39b61a 100644 --- a/module/story/ui/affected.html.php +++ b/module/story/ui/affected.html.php @@ -2,94 +2,117 @@ declare(strict_types=1); namespace zin; -$getAffectedTabs = function($story, $users) +if($story->type == 'story') { - global $lang, $config; - $affectedProjects = array(); - $affectedTaskCount = 0; - foreach($story->executions as $executionID => $execution) + $getAffectedTabs = function($story, $users) { - $teams = ''; - if(isset($story->teams[$executionID])) + global $lang, $config; + $affectedProjects = array(); + $affectedTaskCount = 0; + foreach($story->executions as $executionID => $execution) { - foreach($story->teams[$executionID] as $member) $teams .= zget($users, $member->account) . ' '; + $teams = ''; + if(isset($story->teams[$executionID])) + { + foreach($story->teams[$executionID] as $member) $teams .= zget($users, $member->account) . ' '; + } + + $executionTasks = array_values(zget($story->tasks, $executionID, array())); + $affectedTaskCount += count($executionTasks); + $affectedProjects[] = h6 + ( + $execution->name, + $teams ? h::small(icon('group'), $teams) : null + ); + $affectedProjects[] = empty($executionTasks) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable + ( + set::cols($config->story->affect->projects->fields), + set::data($executionTasks) + ); } - $executionTasks = array_values(zget($story->tasks, $executionID, array())); - $affectedTaskCount += count($executionTasks); - $affectedProjects[] = h6 + return formGroup ( - $execution->name, - $teams ? h::small(icon('group'), $teams) : null + setClass('w-full'), + set::label($lang->story->checkAffection), + set::required(false), + tabs + ( + setClass('w-full'), + tabPane + ( + to::suffix(label($affectedTaskCount)), + set::key('affectedProjects'), + set::title($lang->story->affectedProjects), + set::active(true), + empty($affectedProjects) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : $affectedProjects + ), + tabPane + ( + to::suffix(label(count($story->bugs))), + set::key('affectedBugs'), + set::title($lang->story->affectedBugs), + empty($story->bugs) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable + ( + set::cols($config->story->affect->bugs->fields), + set::data(array_values($story->bugs)), + set::style(array('min-width' => '100%')) + ) + ), + tabPane + ( + to::suffix(label(count($story->cases))), + set::key('affectedCases'), + set::title($lang->story->affectedCases), + empty($story->cases) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable + ( + set::cols($config->story->affect->cases->fields), + set::data(array_values($story->cases)), + set::style(array('min-width' => '100%')) + ) + ), + empty($story->twins) ? null : tabPane + ( + to::suffix(label(count($story->twins))), + set::key('affectedTwins'), + set::title($lang->story->affectedTwins), + empty($story->twins) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable + ( + set::cols($config->story->affect->twins->fields), + set::data(array_values($story->twins)), + set::style(array('min-width' => '100%')) + ) + ) + ) ); - $affectedProjects[] = empty($executionTasks) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable + }; +} +else +{ + $getAffectedTabs = function($story) + { + global $lang, $config; + return formGroup ( - set::cols($config->story->affect->projects->fields), - set::data($executionTasks) + setClass('w-full'), + set::label($lang->story->checkAffection), + set::required(false), + tabs + ( + setClass('w-full'), + tabPane + ( + set::active(true), + to::suffix(label(count($story->children))), + set::key('affectedChildren'), + set::title($lang->story->children), + $story->children ? dtable + ( + set::cols($config->story->affect->children->fields), + set::data(array_values($story->children)) + ): null + ) + ) ); - } - - $tabs = tabs - ( - setClass('w-full'), - tabPane - ( - to::suffix(label($affectedTaskCount)), - set::key('affectedProjects'), - set::title($lang->story->affectedProjects), - set::active(true), - empty($affectedProjects) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : $affectedProjects - ), - tabPane - ( - to::suffix(label(count($story->bugs))), - set::key('affectedBugs'), - set::title($lang->story->affectedBugs), - empty($story->bugs) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable - ( - set::cols($config->story->affect->bugs->fields), - set::data(array_values($story->bugs)), - set::style(array('min-width' => '100%')) - ) - ), - tabPane - ( - to::suffix(label(count($story->cases))), - set::key('affectedCases'), - set::title($lang->story->affectedCases), - empty($story->cases) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable - ( - set::cols($config->story->affect->cases->fields), - set::data(array_values($story->cases)), - set::style(array('min-width' => '100%')) - ) - ), - empty($story->twins) ? null : tabPane - ( - to::suffix(label(count($story->twins))), - set::key('affectedTwins'), - set::title($lang->story->affectedTwins), - empty($story->twins) ? div(setClass('dtable-empty-tip'), div(setClass('text-gray'), $lang->noData)) : dtable - ( - set::cols($config->story->affect->twins->fields), - set::data(array_values($story->twins)), - set::style(array('min-width' => '100%')) - ) - ) - ); - - global $app; - return $app->rawMethod == 'change' ? section - ( - set::width('full'), - set::title($lang->story->checkAffection), - set::required(false), - $tabs - ) : formGroup - ( - setClass('w-full'), - set::label($lang->story->checkAffection), - set::required(false), - $tabs - ); -}; + }; +} diff --git a/module/story/ui/batchchangeparent.html.php b/module/story/ui/batchchangeparent.html.php new file mode 100644 index 0000000000..2c2f88d26d --- /dev/null +++ b/module/story/ui/batchchangeparent.html.php @@ -0,0 +1,28 @@ + +* @package story +* @link https://www.zentao.net +*/ + +namespace zin; + +modalHeader(set::title($lang->story->batchChangeParent)); +unset($parents[0]); +formPanel +( + formGroup + ( + set::label($lang->story->parent), + picker + ( + set::name('parent'), + set::items($parents) + ) + ), +); diff --git a/module/story/ui/batchclose.html.php b/module/story/ui/batchclose.html.php index 34b772c4c2..f0a79a7215 100644 --- a/module/story/ui/batchclose.html.php +++ b/module/story/ui/batchclose.html.php @@ -10,12 +10,13 @@ declare(strict_types=1); */ namespace zin; +data('activeMenuID', $storyType); jsVar('twinsCount', $twinsCount); jsVar('langTwins', $lang->story->twins . ': '); if(!empty($errorTips)) pageJS("zui.Modal.alert({message: '{$errorTips}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'});\n"); -unset($lang->story->reasonList['subdivided']); -foreach($lang->story->reasonList as $key => $value) +unset($lang->{$storyType}->reasonList['subdivided']); +foreach($lang->{$storyType}->reasonList as $key => $value) { if($key == 'cancel') continue; $reasonList[] = array('text' => $value, 'value' => $key); diff --git a/module/story/ui/batchcreate.html.php b/module/story/ui/batchcreate.html.php index b6427dc287..862672b7f7 100644 --- a/module/story/ui/batchcreate.html.php +++ b/module/story/ui/batchcreate.html.php @@ -14,10 +14,14 @@ namespace zin; include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php'); -data('activeMenuID', $type); +if($app->tab == 'product') data('activeMenuID', $type); data('storyType', $type); jsVar('storyType', $type); jsVar('productID', $productID); +jsVar('executionID', $executionID); +jsVar('branch', $branch); +jsVar('storyID', $storyID); +data('gradeRule', $gradeRule); !isAjaxRequest() && dropmenu(); @@ -27,7 +31,7 @@ foreach(explode(',', $this->config->story->create->requiredFields) as $requiredF } /* Generate fields for the batch create form. */ -$fnGenerateFields = function() use ($lang, $fields, $stories, $customFields, $showFields) +$fnGenerateFields = function() use ($app, $lang, $type, $fields, $stories, $customFields, $showFields, $storyID, $gradeRule, $hiddenGrade) { global $config; @@ -55,22 +59,61 @@ $fnGenerateFields = function() use ($lang, $fields, $stories, $customFields, $sh foreach($cols as $index => $col) { $colName = $col['name']; - if(str_contains(",{$config->story->create->requiredFields},", ",{$colName},")) $cols[$index]['required'] = true; + if(str_contains(",{$config->{$app->rawModule}->create->requiredFields},", ",{$colName},")) $cols[$index]['required'] = true; if(isset($customFields[$colName]) && strpos(",$showFields,", ",$colName,") === false) $cols[$index]['hidden'] = true; if($colName == 'sourceNote' && strpos(",$showFields,", ",source,") === false) $cols[$index]['hidden'] = true; + if($colName == 'plan' && $type != 'story') $cols[$index]['multiple'] = true; + if($colName == 'grade' && $gradeRule == 'stepwise') $cols[$index]['disabled'] = true; + if($colName == 'grade' && $hiddenGrade) $cols[$index]['hidden'] = true; + if($colName == 'parent' && $storyID > 0 ) $cols[$index]['hidden'] = true; + if($colName == 'spec') unset($cols[$index]['control']); + if($colName == 'source') $cols[$index]['items'] = $lang->{$type}->sourceList; + if($colName == 'category') $cols[$index]['items'] = $lang->{$type}->categoryList; + if($colName == 'pri') $cols[$index]['items'] = $lang->{$type}->priList; + if($colName == 'grade') $cols[$index]['required'] = true; } return $cols; }; +$storyTypeRadio = null; +if($storyID > 0 && $story->type != 'story' && $config->{$story->type}->gradeRule != 'stepwise') +{ + $gradeRule = $this->config->{$story->type}->gradeRule; + $isLastGrade = $story->grade == $maxGradeGroup[$story->type]; + $disabled = $story->isParent == '1' || $isLastGrade || ($gradeRule == 'stepwise' && !$isLastGrade); + + $title = ''; + if($story->isParent == '1') $title = $lang->story->errorCannotSplit; + if($isLastGrade) $title = $lang->story->errorMaxGradeSubdivide; + if(!$isLastGrade && $gradeRule == 'stepwise') $title = $lang->story->errorStepwiseSubdivide; + + if($story->type == 'epic') unset($lang->story->typeList['story']); + if($story->type == 'requirement') unset($lang->story->typeList['epic']); + + $storyTypeRadio = radioList + ( + set::name('typeSwitcher'), + set::items($lang->story->typeList), + set::inline(true), + set::value($type), + set::disabled($disabled), + $title ? set::title($title) : null, + on::change('switchType') + ); +} + formBatchPanel ( setID('dataform'), on::change('[data-name="branch"]', 'setModuleAndPlanByBranch'), + on::change('[data-name="parent"]', 'setGrade'), on::change('[data-name="region"]', 'changeRegion'), $stories ? set::data($stories) : null, set::ajax(array('beforeSubmit' => jsRaw('clickSubmit'))), set::title($storyID ? $storyTitle . $lang->hyphen . $this->lang->story->subdivide : $this->lang->story->batchCreate), + $storyTypeRadio ? set::headingActionsClass('flex-auto') : null, + $storyTypeRadio ? to::headingActions($storyTypeRadio) : null, set::uploadParams('module=story¶ms=' . helper::safe64Encode("productID=$productID&branch=$branch&moduleID=$moduleID&storyID=$storyID&executionID=$executionID&plan={$planID}&type=$type")), set::pasteField('title'), set::customFields(array('list' => $customFields, 'show' => explode(',', $showFields), 'key' => 'batchCreateFields')), diff --git a/module/story/ui/batchedit.html.php b/module/story/ui/batchedit.html.php index 953643d345..4befd24822 100644 --- a/module/story/ui/batchedit.html.php +++ b/module/story/ui/batchedit.html.php @@ -21,15 +21,17 @@ jsVar('storyType', $storyType); if(!empty($twinsTip)) pageJS("zui.Modal.alert({message: '{$twinsTip}', icon: 'icon-exclamation-sign', iconClass: 'warning-pale rounded-full icon-2x'});\n"); -$fields = $config->story->form->batchEdit; - $items = array(); $items['storyIdList'] = array('name' => 'storyIdList', 'label' => '', 'control' => 'hidden', 'hidden' => true); $items['id'] = array('name' => 'id', 'label' => $lang->idAB, 'control' => 'index', 'width' => '60px'); +$fields = $config->story->form->batchEdit; foreach($fields as $fieldName => $field) { if($fieldName == 'color') continue; if(isset($field['options']) && $field['options'] == 'users') $field['options'] = $users; + if($fieldName == 'source') $field['options'] = $lang->{$storyType}->sourceList; + if($fieldName == 'pri') $field['options'] = $lang->{$storyType}->priList; + if($fieldName == 'category') $field['options'] = $lang->{$storyType}->categoryList; $items[$fieldName] = array('name' => $fieldName, 'label' => zget($lang->story, $fieldName), 'control' => $field['control'], 'width' => $field['width'], 'required' => $field['required'], 'items' => zget($field, 'options', array())); if(isset($customFields[$fieldName]) && strpos(",$showFields,", ",$fieldName,") === false) $items[$fieldName]['hidden'] = true; if($fieldName == 'sourceNote' && strpos(",$showFields,", ",source,") === false) $items['sourceNote']['hidden'] = true; @@ -42,12 +44,6 @@ $items['assignedTo']['defaultDitto'] = 'off'; $items['source']['defaultDitto'] = 'off'; $items['stage']['defaultDitto'] = 'off'; -if($storyType == 'requirement') -{ - unset($items['plan']); - unset($items['stage']); -} - if(!$branchProduct) unset($items['branch'], $customFields['branch']); /* Build form field value for batch edit. */ diff --git a/module/story/ui/batchtotask.html.php b/module/story/ui/batchtotask.html.php index a9be8cde44..c3124090b9 100644 --- a/module/story/ui/batchtotask.html.php +++ b/module/story/ui/batchtotask.html.php @@ -15,6 +15,10 @@ namespace zin; data('activeMenuID', 'story'); jsVar('executionID', $executionID); jsVar('langPreview', $lang->preview); +jsVar('hasParent', $hasParent); +jsVar('hasERUR', $hasERUR); +jsVar('errorParentSplitTask', $lang->story->errorParentSplitTask); +jsVar('errorERURSplitTask', $lang->story->errorERURSplitTask); jsVar('copyStoryTitleTip', $lang->task->copyStoryTitle); $fields = array(); diff --git a/module/story/ui/change.html.php b/module/story/ui/change.html.php index e8f6da0d78..489677c37e 100644 --- a/module/story/ui/change.html.php +++ b/module/story/ui/change.html.php @@ -197,7 +197,7 @@ detailBody )), sectionList($formItems), h::hr(), - history() + history(set::objectID($story->id)) ); render(); diff --git a/module/story/ui/close.html.php b/module/story/ui/close.html.php index 1d28b684ac..3fc272ca47 100644 --- a/module/story/ui/close.html.php +++ b/module/story/ui/close.html.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace zin; jsVar('storyType', $story->type); +jsVar('storyID', $story->id); modalHeader(); formPanel diff --git a/module/story/ui/create.field.php b/module/story/ui/create.field.php index 1415b3a48d..5a4028f7f5 100644 --- a/module/story/ui/create.field.php +++ b/module/story/ui/create.field.php @@ -5,6 +5,7 @@ global $lang, $config; $fields = defineFieldList('story.create'); $createFields = data('fields'); $type = data('type'); +$gradeRule = data('gradeRule'); $isSR = $type == 'story'; $isBranchUR = isset($createFields['branch']) && $type != 'story'; $isKanban = data('executionType') == 'kanban'; @@ -42,6 +43,17 @@ if(isset($createFields['branch']) && $type == 'story') )); } +$fields->field('parent') + ->hidden(data('hiddenParent')) + ->items($createFields['parent']['options']) + ->value($createFields['parent']['default']); + +$fields->field('grade') + ->hidden(data('hiddenGrade')) + ->disabled($gradeRule == 'stepwise') + ->required() + ->items($createFields['grade']['options']) + ->value($createFields['grade']['default']); if(isset($createFields['URS'])) { $fields->field('URS') @@ -87,7 +99,7 @@ $fields->field('assignedTo') $fields->field('category') ->required($createFields['category']['required']) - ->items($createFields['category']['options']) + ->items($lang->{$type}->categoryList) ->value($createFields['category']['default']); if($isKanban) @@ -115,7 +127,7 @@ $fields->field('pri') ->width('1/4') ->required($createFields['pri']['required']) ->control('priPicker') - ->items($createFields['pri']['options']) + ->items($lang->{$type}->priList) ->value($createFields['pri']['default']); $fields->field('estimate') @@ -139,14 +151,14 @@ $fields->field('verify') $fields->field('files')->width('full')->control('fileSelector'); -if(!isset($createFields['branch']) && $type == 'story' and isset($createFields['plan'])) +if(!(isset($createFields['branch']) && $type == 'story') && isset($createFields['plan'])) { $fields->field('plan') ->foldable() ->required($createFields['plan']['required']) ->control('inputGroup') ->items(false) - ->itemBegin('plan')->control('picker')->id('planIdBox')->items($createFields['plan']['options'])->value($createFields['plan']['default'])->itemEnd() + ->itemBegin('plan')->control('picker')->id('planIdBox')->items($createFields['plan']['options'])->value($createFields['plan']['default'])->multiple($type != 'story')->itemEnd() ->item(empty($createFields['plan']['options']) ? field()->control('btn')->icon('plus')->url(createLink('productplan', 'create', 'productID=' . data('productID') . '&branch=' . data('branch')))->set(array('data-toggle' => 'modal', 'data-size' => 'lg'))->set('title', $lang->productplan->create) : null) ->item(empty($createFields['plan']['options']) ? field()->control('btn')->icon('refresh')->id("loadProductPlans")->set('title', $lang->refresh) : null); } @@ -155,7 +167,7 @@ $fields->field('source') ->foldable() ->width('1/4') ->required($createFields['source']['required']) - ->items($createFields['source']['options']) + ->items($lang->{$type}->sourceList) ->value($createFields['source']['default']); $fields->field('sourceNote') diff --git a/module/story/ui/create.html.php b/module/story/ui/create.html.php index ea2a0e9e60..d37aa8eaff 100644 --- a/module/story/ui/create.html.php +++ b/module/story/ui/create.html.php @@ -14,9 +14,10 @@ namespace zin; include($this->app->getModuleRoot() . 'ai/ui/inputinject.html.php'); data('storyType', $type); -data('activeMenuID', $type); +data('gradeRule', $gradeRule); +if($app->tab == 'product') data('activeMenuID', $type); -$forceReview = $this->story->checkForceReview(); +$forceReview = $this->story->checkForceReview($type); $createFields = useFields('story.create'); $createFields->field('needNotReview')->value($forceReview ? 0 : 1); if(!$forceReview) $createFields->field('reviewer')->hidden(true); @@ -41,33 +42,19 @@ if(!empty($objectID)) $createFields->field('source')->className('full:w-1/2'); $createFields->field('sourceNote')->className('full:w-1/2'); - $orders = 'product,module,twinsStory,URS,assignedTo,reviewer,region,lane,title,category,pri,estimate,spec,verify,files'; - $fullModeOrders = 'product,module,twinsStory,plan,URS,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; + $orders = 'product,module,twinsStory,assignedTo,reviewer,region,lane,title,category,pri,estimate,spec,verify,files'; + $fullModeOrders = 'product,module,twinsStory,plan,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; if(!isset($fields['plan'])) { - $createFields->field('category')->width('1/6'); - $createFields->field('pri')->width('1/6'); - $createFields->field('estimate')->width('1/6'); $createFields->field('source')->width('1/2'); $createFields->field('sourceNote')->width('1/2'); - $fullModeOrders = 'product,module,twinsStory,URS,assignedTo,reviewer,region,lane,title,category,pri,estimate,spec,verify,files'; - - if(!isset($fields['URS'])) - { - $createFields->field('category')->width('1/2'); - $createFields->field('pri')->width('1/4'); - $createFields->field('estimate')->width('1/4'); - $orders = 'product,module,twinsStory,assignedTo,category,reviewer,region,lane,title,pri,estimate,spec,verify,files'; - $fullModeOrders = 'product,module,twinsStory,assignedTo,category,reviewer,region,lane,title,pri,estimate,spec,verify,files'; - } + $createFields->field('category')->width('1/2'); + $createFields->field('pri')->width('1/4'); + $createFields->field('estimate')->width('1/4'); + $orders = 'product,module,twinsStory,assignedTo,category,reviewer,region,lane,title,pri,estimate,spec,verify,files'; + $fullModeOrders = 'product,module,twinsStory,assignedTo,category,reviewer,region,lane,title,pri,estimate,spec,verify,files'; } - elseif(isset($fields['URS'])) - { - $createFields->field('category')->width('1/6')->className('full:w-1/2'); - $createFields->field('pri')->width('1/6')->className('full:w-1/4'); - $createFields->field('estimate')->width('1/6')->className('full:w-1/4'); - } - elseif(!isset($fields['URS'])) + else { $createFields->field('category')->className('full:w-1/6'); $createFields->field('pri')->className('full:w-1/6'); @@ -86,23 +73,19 @@ else $createFields->field('sourceNote')->className('full:w-1/2'); if($type == 'story') { - if(!isset($fields['branch']) || !isset($fields['URS'])) - { - $createFields->field('category')->width('1/2')->className('full:w-1/6'); - $createFields->field('pri')->width('1/4')->className('full:w-1/6'); - $createFields->field('estimate')->width('1/4')->className('full:w-1/6'); - } + $createFields->field('category')->width('1/2')->className('full:w-1/6'); + $createFields->field('pri')->width('1/4')->className('full:w-1/6'); + $createFields->field('estimate')->width('1/4')->className('full:w-1/6'); } - $fullModeOrders = 'product,module,twinsStory,plan,URS,parent,assignedTo,reviewer,region,lane,title,category,pri,estimate,spec,verify,files'; - if($type != 'story') $fullModeOrders = 'product,module,twinsStory,plan,URS,parent,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; + $fullModeOrders = 'product,module,twinsStory,plan,parent,grade,assignedTo,reviewer,region,lane,title,category,pri,estimate,spec,verify,files'; + if($type != 'story') $fullModeOrders = 'product,module,twinsStory,plan,parent,grade,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; if($type == 'story' and isset($fields['branch'])) { - $fullModeOrders = 'product,module,twinsStory,plan,URS,parent,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; - if(!isset($fields['URS'])) $fullModeOrders = 'product,module,twinsStory,reviewer,region,lane,plan,assignedTo,title,category,pri,estimate,spec,verify,files'; + $fullModeOrders = 'product,module,twinsStory,plan,parent,grade,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'; } - $createFields->orders('product,module,twinsStory,URS,parent,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'); + $createFields->orders('product,module,twinsStory,parent,grade,reviewer,region,lane,assignedTo,category,title,pri,estimate,spec,verify,files'); $createFields->fullModeOrders($fullModeOrders); } @@ -130,9 +113,9 @@ formGridPanel !isInModal() ? array('text' => $lang->goback, 'back' => true) : null )), set::fields($createFields), - on::click('#loadURS', "loadURS"), on::click('#loadProductPlans', "loadProductPlans('{$productID}')"), on::change('[name=module]', 'loadURS'), + on::change('[name=parent]', 'loadGrade'), on::change('[name=product]', 'loadProduct'), on::change('[name=source]', "toggleFeedback(e.target)"), on::change('[name=region]', 'setLane'), diff --git a/module/story/ui/edit.html.php b/module/story/ui/edit.html.php index 84da240cbd..a09ceec28a 100644 --- a/module/story/ui/edit.html.php +++ b/module/story/ui/edit.html.php @@ -12,11 +12,11 @@ declare(strict_types=1); namespace zin; $canEditContent = str_contains(',draft,changing,', ",{$story->status},"); -$forceReview = $this->story->checkForceReview(); +$forceReview = $this->story->checkForceReview($story->type); $assignedToList = $story->status == 'closed' ? array('closed' => 'Closed') : $users; $planCount = !empty($story->planTitle) ? count($story->planTitle) : 0; -$multiplePlan = ($product->type != 'normal' && empty($story->branch) && $planCount > 1); +$multiplePlan = ($product->type != 'normal' && empty($story->branch) && $planCount > 1) || ($story->type != 'story'); $minStage = $story->stage; $stageList = implode(',', array_keys($this->lang->story->stageList)); @@ -34,17 +34,24 @@ if(!empty($story->stages) && isset($fields['stage']['options'])) } } -data('activeMenuID', $story->type); +unset($fields['stage']['options']['defining']); +unset($fields['stage']['options']['planning']); +unset($fields['stage']['options']['delivering']); + +if($app->tab == 'product') data('activeMenuID', $story->type); jsVar('storyType', $story->type); jsVar('storyID', $story->id); jsVar('storyStatus', $story->status); +jsVar('isParent', $story->isParent); +jsVar('oldProductID', $story->product); +jsVar('oldGrade', $story->grade); +jsVar('oldParent', $story->parent); jsVar('lastReviewer', explode(',', $lastReviewer)); jsVar('storyReviewers', $storyReviewers); jsVar('reviewerNotEmpty', $lang->story->notice->reviewerNotEmpty); -jsVar('oldProductID', $story->product); jsVar('twins', $story->twins); jsVar('relievedTwinsTip', $lang->story->relievedTwinsTip); -jsVar('parentStory', !empty($story->children)); +jsVar('changeProductTips', $lang->story->changeProductTips); jsVar('moveChildrenTips', $lang->story->moveChildrenTips); jsVar('executionID', isset($objectID) ? $objectID : 0); jsVar('langTreeManage', $lang->tree->manage); @@ -71,6 +78,7 @@ detailBody setID('dataform'), set::isForm(true), set::ajax(array('beforeSubmit' => jsRaw('clickSubmit'))), + on::change('[name=parent]', 'loadGrade'), $canEditContent ? set::actions(array ( array('btnType' => 'submit', 'class' => 'primary', 'data-status' => 'active', 'text' => $lang->save), @@ -189,7 +197,7 @@ detailBody formGroup(editor(set::name('comment'))) ) ), - history(), + history(set::objectID($story->id)), detailSide ( set::isForm(true), @@ -197,7 +205,7 @@ detailBody ( setClass('mt-5'), set::title($lang->story->legendBasicInfo), - $story->parent <= 0 && !$product->shadow ? item + !$product->shadow ? item ( set::trClass(zget($fields['product'], 'className', '')), set::name($lang->story->product), @@ -241,13 +249,18 @@ detailBody set::manageLink(createLink('tree', 'browse', "rootID={$story->product}&view=story¤tModuleID=0&branch={$story->branch}")) ) ), - $story->parent >= 0 && $story->type == 'story' ? item + ($story->parent >= 0 && ($showGrade || $story->type != 'epic')) ? item ( set::trClass($app->tab == 'product' ? zget($fields['parent'], 'className', '') : 'hidden'), set::name($lang->story->parent), picker(setID('parent'), set::name('parent'), set::items($fields['parent']['options']), set::value($fields['parent']['default'])) - ) : formHidden('parent', $story->parent), - $story->type == 'story' && $story->parent >= 0 ? item + ) : null, + $showGrade ? item + ( + set::name($lang->story->grade), + picker(setID('grade'), set::name('grade'), set::required(true), set::items($fields['grade']['options']), set::value($story->grade), set::disabled($gradeRule == 'stepwise')) + ) : picker(setID('grade'), set::name('grade'), set::required(true), set::items($fields['grade']['options']), set::value($story->grade), set::hidden(true)), + item ( set::trClass(zget($fields['plan'], 'className', '')), set::name($lang->story->plan), @@ -261,12 +274,12 @@ detailBody empty($fields['plan']['options']) ? btn(set::url($this->createLink('productplan', 'create', "productID={$story->product}&branch={$story->branch}")), setData(array('toggle' => 'modal')), icon('plus')) : null, empty($fields['plan']['options']) ? btn(set('onclick', "loadProductPlans({$story->product}, {$story->branch})"), setClass('refresh'), icon('refresh')) : null ) - ) : null, + ), item ( set::trClass('sourceBox'), set::name($lang->story->source), - picker(setID('source'), set::name('source'), set::items($fields['source']['options']), set::value($fields['source']['default']), on::change('window.toggleFeedback(e.target)')) + picker(setID('source'), set::name('source'), set::items($lang->{$story->type}->sourceList), set::value($fields['source']['default']), on::change('window.toggleFeedback(e.target)')) ), item ( @@ -280,25 +293,25 @@ detailBody span(setClass("status-{$story->status}"), $this->processStatus('story', $story)), formHidden('status', $story->status) ), - $story->type == 'story' ? item + ($story->type == 'story' && $story->isParent == '0') ? item ( set::name($lang->story->stage), picker(setID('stage'), set::name('stage'), set::items($fields['stage']['options']), set::value($minStage)) - ) : null, + ) : formHidden('stage', $story->stage), item ( set::name($lang->story->category), - picker(setID('category'), set::name('category'), set::items($fields['category']['options']), set::value($fields['category']['default'])) + picker(setID('category'), set::name('category'), set::items($lang->{$story->type}->categoryList), set::value($fields['category']['default'])) ), item ( set::name($lang->story->pri), - priPicker(set::name('pri'), set::items($fields['pri']['options']), set::value($fields['pri']['default'])) + priPicker(set::name('pri'), set::items($lang->{$story->type}->priList), set::value($fields['pri']['default'])) ), item ( set::name($lang->story->estimate), - $story->parent >= 0 ? input(set::name('estimate'), set::value($story->estimate)) : $story->estimate + $story->isParent == '0' ? input(set::name('estimate'), set::value($story->estimate)) : $story->estimate ), item ( @@ -366,56 +379,16 @@ detailBody picker(setID('closedReason'), set::name('closedReason'), set::items($fields['closedReason']['options']), set::value($fields['closedReason']['default']), on::change('setStory')) ) : null, ), - tableData + $story->status == 'closed' ? tableData ( set::title($lang->story->legendMisc), - $story->status == 'closed' ? item + item ( set::trClass('duplicateStoryBox'), set::name($lang->story->duplicateStory), picker(setID('duplicateStory'), set::name('duplicateStory'), set::items($fields['duplicateStory']['options']), set::value($fields['duplicateStory']['default']), set::placeholder($lang->bug->placeholder->duplicate)) - ) : null, - item - ( - set::name($story->type == 'story' ? $lang->requirement->linkStory : $lang->story->linkStory), - ($story->type == 'story' && common::hasPriv('story', 'linkStories')) ? btn - ( - setClass('secondary'), - setID('linkStoriesLink'), - setData(array('toggle' => 'modal', 'size' => 'lg')), - on::click('linkStories'), - $lang->story->linkStoriesAB - ) : null, - ($story->type == 'requirement' && common::hasPriv('requirement', 'linkRequirements')) ? btn - ( - setClass('secondary'), - setID('linkStoriesLink'), - setData(array('toggle' => 'modal', 'size' => 'lg')), - on::click('linkStories'), - $lang->story->linkRequirementsAB - ) : null ), - item - ( - set::name(' '), - !empty($story->linkStoryTitles) ? div - ( - setID('linkedStories'), - array_values(array_map(function($linkStoryID, $linkStoryTitle) use($story) - { - $linkStoryField = $story->type == 'story' ? 'linkStories' : 'linkRequirements'; - return div - ( - set::title($linkStoryTitle), - checkbox(set::name($linkStoryField . '[]'), set::rootClass('inline'), set::value($linkStoryID), set::checked(true)), - label(setClass('circle size-sm'), $linkStoryID), - span(setClass('linkStoryTitle'), $linkStoryTitle) - ); - }, array_keys($story->linkStoryTitles), array_values($story->linkStoryTitles))) - ) : null, - div(setID('linkStoriesBox')) - ) - ) + ) : null ) ); diff --git a/module/story/ui/linkstories.html.php b/module/story/ui/linkstories.html.php deleted file mode 100644 index b4bdef8f2d..0000000000 --- a/module/story/ui/linkstories.html.php +++ /dev/null @@ -1,90 +0,0 @@ - -* @package story -* @link https://www.zentao.net -*/ - -namespace zin; - -$storyType = $story->type; - -$cols = array(); -$cols['id'] = $config->story->dtable->fieldList['id']; -$cols['pri'] = $config->story->dtable->fieldList['pri']; -if(!empty($product->shadow)) $cols['product'] = $config->story->dtable->fieldList['product']; -$cols['title'] = $config->story->dtable->fieldList['title']; -$cols['status'] = $config->story->dtable->fieldList['status']; -$cols['openedBy'] = $config->story->dtable->fieldList['openedBy']; -$cols['assignedTo'] = $config->story->dtable->fieldList['assignedTo']; - -$data = array(); -foreach($stories2Link as $story) $data[] = $this->story->formatStoryForList($story); - -modalHeader(set::title($story->type == 'story' ? $lang->story->linkStoriesAB : $lang->story->linkRequirementsAB)); - -searchForm -( - set::module('story'), - set::simple(true), - set::show(true) -); - -dtable -( - setID('linkStories'), - set::userMap($users), - set::cols($cols), - set::data($data), - set::checkboxLabel($lang->selectAll), - set::footPager(usePager()), - set::footer(array('checkbox', 'toolbar', 'flex', 'pager')), -); - -div -( - setClass('flex justify-center gap-4 p-4'), - btn - ( - setClass('px-6'), - set::type('primary'), - set::btnType('button'), - set::text($lang->save), - setData(array('on' => 'click', 'call' => 'fnLinkStories', 'params' => 'event')), - ), - isInModal() ? null : backBtn(setClass('px-6')) -); - -h::js -( -<< div').length; - checkedList.forEach(function(id) - { - var linkStoryField = '{$storyType}' == 'story' ? 'linkStories' : 'linkRequirements'; - var storyInfo = dtable.$.getRowInfo(id).data; - var checkbox = "
"; - var idLabel = "" + storyInfo.id + ""; - var titleSpan = "" + storyInfo.title + ""; - - itemBoxHtml += "
" + checkbox + idLabel + titleSpan + "
"; - index ++; - }); - - $('#linkStoriesBox').append(itemBoxHtml); - zui.Modal.hide('#' + $(e.target).closest('.modal').attr('id')); -}; -EOD -); - -render(); diff --git a/module/story/ui/linkstory.html.php b/module/story/ui/linkstory.html.php index 76c7f64170..a52fa5463a 100644 --- a/module/story/ui/linkstory.html.php +++ b/module/story/ui/linkstory.html.php @@ -14,14 +14,14 @@ namespace zin; $pageParams = http_build_query($app->getParams()); -$config->story->dtable->fieldList['title']['title'] = $lang->story->title; +$config->story->dtable->fieldList['title']['title'] = $lang->story->name; $cols = array(); -$cols['id'] = $config->story->dtable->fieldList['id']; -$cols['title'] = $config->story->dtable->fieldList['title']; -$cols['pri'] = $config->story->dtable->fieldList['pri']; -$cols['status'] = $config->story->dtable->fieldList['status']; -if($story->type == 'requirement') $cols['stage'] = $config->story->dtable->fieldList['stage']; +$cols['id'] = $config->story->dtable->fieldList['id']; +$cols['title'] = $config->story->dtable->fieldList['title']; +$cols['pri'] = $config->story->dtable->fieldList['pri']; +$cols['status'] = $config->story->dtable->fieldList['status']; +$cols['stage'] = $config->story->dtable->fieldList['stage']; $cols['openedBy'] = $config->story->dtable->fieldList['openedBy']; $cols['estimate'] = $config->story->dtable->fieldList['estimate']; $cols['title']['nestedToggle'] = false; @@ -29,9 +29,9 @@ $cols['title']['flex'] = 1; $cols = array_map(function($col){unset($col['sortType']); return $col;}, $cols); $data = array(); -foreach($stories2Link as $linkStory) $data[] = $this->story->formatStoryForList($linkStory); +foreach($stories2Link as $linkStory) $data[] = $this->story->formatStoryForList($linkStory, array(), $linkStory->type, $maxGradeGroup); -modalHeader(set::title($lang->story->link . ($story->type == 'story' ? $lang->story->requirement : $lang->story->story))); +modalHeader(set::title($lang->story->linkStory)); searchForm ( @@ -46,8 +46,8 @@ dtable set::userMap($users), set::cols($cols), set::data($data), + set::footPager(usePager()), set::checkboxLabel($lang->selectAll), - set::footer(array('checkbox', 'toolbar')) ); div diff --git a/module/story/ui/report.html.php b/module/story/ui/report.html.php index 3b0027d93b..fb424b73fc 100644 --- a/module/story/ui/report.html.php +++ b/module/story/ui/report.html.php @@ -10,10 +10,10 @@ declare(strict_types=1); */ namespace zin; -data('activeMenuID', $storyType); jsVar('params', "productID={$productID}&branchID={$branchID}&storyType={$storyType}&browseType={$browseType}&moduleID={$moduleID}"); jsVar('projectID', $projectID); jsVar('storyType', $storyType); +data('activeMenuID', $storyType); detailHeader ( diff --git a/module/story/ui/review.html.php b/module/story/ui/review.html.php index 318dd464d7..dddf158103 100644 --- a/module/story/ui/review.html.php +++ b/module/story/ui/review.html.php @@ -18,6 +18,8 @@ jsVar('rawModule', $this->app->rawModule); jsVar('isMultiple', count($reviewers) > 1); jsVar('isLastOne', $isLastOne); +if($story->isParent == '1') $fields['estimate']['readonly'] = true; + $formItems = array(); foreach($fields as $field => $attr) { @@ -29,8 +31,8 @@ foreach($fields as $field => $attr) $formItems[$field] = formRow ( - in_array($field, array('assignedTo', 'duplicateStory', 'pri', 'estimate', 'childStories')) ? setID($field . 'Box') : null, - in_array($field, array('closedReason', 'duplicateStory', 'pri', 'estimate', 'childStories', 'status')) ? set::hidden(true) : null, + in_array($field, array('assignedTo', 'duplicateStory', 'pri', 'estimate')) ? setID($field . 'Box') : null, + in_array($field, array('closedReason', 'duplicateStory', 'pri', 'estimate', 'status')) ? set::hidden(true) : null, $field == 'closedReason' ? setID('rejectedReasonBox') : null, $field == 'assignedTo' ? set::hidden(!$isLastOne) : null, @@ -44,7 +46,8 @@ foreach($fields as $field => $attr) set::label($attr['title']), set::control($control), set::value($attr['default']), - set::required($attr['required']) + set::required($attr['required']), + set::readonly(!empty($attr['readonly'])) ) ); } @@ -56,7 +59,7 @@ panel setClass('panel-form mx-auto'), form($formItems), h::hr(setClass('mt-6 mb-6')), - history() + history(set::objectID($story->id)) ); render(); diff --git a/module/story/ui/submitreview.html.php b/module/story/ui/submitreview.html.php index 352d392dac..7f364e137d 100644 --- a/module/story/ui/submitreview.html.php +++ b/module/story/ui/submitreview.html.php @@ -16,7 +16,7 @@ jsVar('lastReviewer', $reviewers); jsVar('storyType', $story->type); $needNotReviewBox = null; -if(!$this->story->checkForceReview()) +if($needReview) { $needNotReviewBox = span ( @@ -52,7 +52,7 @@ formPanel set::value($story->reviewer), set::multiple(true), set::items($reviewers), - set::disabled(!$this->story->checkForceReview() && $needReview) + set::disabled(!$this->story->checkForceReview($story->type) && $needReview) ), $needNotReviewBox ) diff --git a/module/story/ui/view.html.php b/module/story/ui/view.html.php index 09aae8ef35..f2294d1905 100644 --- a/module/story/ui/view.html.php +++ b/module/story/ui/view.html.php @@ -17,6 +17,7 @@ include($this->app->getModuleRoot() . 'ai/ui/promptmenu.html.php'); $isInModal = isInModal(); $isRequirement = $story->type == 'requirement'; $isStoryType = $story->type == 'story'; +if(empty($executionID)) $executionID = 0; /* 版本列表。Version list. */ $versions = array(); @@ -36,7 +37,7 @@ for($i = $story->version; $i >= 1; $i--) } /* 根据需求类型,设置要激活的导航项。Active navbar item by story type. */ -setPageData('activeMenuID', $story->type); +if($app->tab == 'product') setPageData('activeMenuID', $story->type); /* 初始化头部右上方工具栏。Init detail toolbar. */ $toolbar = array(); @@ -49,7 +50,7 @@ if(!$isInModal && hasPriv('story', 'create')) 'icon' => 'plus', 'type' => 'primary', 'text' => $lang->story->create, - 'url' => createLink('story', 'create', "productID={$story->product}&branch={$story->branch}&moduleID={$story->module}&$otherParam&bugID=0&planID=0&todoID=0&extra=&storyType=$story->type") + 'url' => createLink($story->type, 'create', "productID={$story->product}&branch={$story->branch}&moduleID={$story->module}&$otherParam&bugID=0&planID=0&todoID=0&extra=&storyType=$story->type") ); } @@ -87,9 +88,10 @@ if($story->children) $cols['estimate'] = $config->story->dtable->fieldList['estimate']; $cols['status'] = $config->story->dtable->fieldList['status']; $cols['actions'] = $config->story->dtable->fieldList['actions']; + $cols['title']['title'] = $lang->story->name; $cols['id']['checkbox'] = false; $cols['title']['nestedToggle'] = false; - $cols['actions']['minWidth'] = 190; + $cols['actions']['minWidth'] = 200; if($isInModal) { $cols['title']['data-toggle'] = 'modal'; @@ -99,11 +101,11 @@ if($story->children) foreach(array_keys($cols) as $fieldName) $cols[$fieldName]['sortType'] = false; $options = array('users' => $users); - foreach($story->children as $child) $child = $this->story->formatStoryForList($child, $options); + foreach($story->children as $child) $child = $this->story->formatStoryForList($child, $options, $child->type, $maxGradeGroup); $sections[] = array ( - 'title' => $isRequirement ? $lang->story->story : $lang->story->children, + 'title' => $lang->story->children, 'control' => 'dtable', 'id' => 'table-story-children', 'cols' => $cols, @@ -139,11 +141,11 @@ if($twins) ->items($twins); } -if($this->config->URAndSR && !$hiddenURS && !in_array($config->vision, array('lite', 'or'))) +if(!in_array($config->vision, array('lite', 'or'))) { $tabs[] = setting() ->group('relatives') - ->title($isStoryType ? $lang->story->requirement : $lang->story->story) + ->title($lang->story->linkStories) ->control('linkedStoryList') ->items($relations) ->story($story); @@ -168,7 +170,7 @@ if($config->vision != 'lite') } $parentTitle = $story->parent > 0 ? set::parentTitle($story->parentName) : null; -$parentUrl = $story->parent > 0 ? set::parentUrl(createLink('story', 'view', "storyID={$story->parent}&version=0¶m=0&storyType=$story->type")) : null; +$parentUrl = $story->parent > 0 ? set::parentUrl(createLink($story->parentType, 'view', "storyID={$story->parent}&version=0¶m=0&storyType=$story->type")) : null; $versionBtn = count($versions) > 1 ? to::title(dropdown ( @@ -199,6 +201,29 @@ foreach($actions as $key => $action) continue; } + if(isset($action['icon']) && $action['icon'] == 'split') + { + $canBatchCreateStory = $story->grade < $maxGradeGroup[$story->type] && empty($story->hasOtherTypeChild); + $objectID = $app->tab == 'project' ? $projectID : $executionID; + if($canBatchCreateStory) + { + $action['url'] = createLink($story->type, 'batchCreate', "productID=$story->product&branch=$story->branch&moduleID=$story->module&storyID=$story->id&executionID=$objectID"); + } + elseif($story->type == 'epic' && common::hasPriv('requirement', 'batchCreate') && empty($story->hasSameTypeChild) && !($this->config->epic->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['epic'])) + { + $action['url'] = createLink('requirement', 'batchCreate', "productID=$story->product&branch=$story->branch&moduleID=$story->module&storyID=$story->id&executionID=$objectID"); + } + elseif($story->type == 'requirement' && common::hasPriv('story', 'batchCreate') && empty($story->hasSameTypeChild) && !($this->config->requirement->gradeRule == 'stepwise' && $story->grade < $maxGradeGroup['requirement'])) + { + $action['url'] = createLink('story', 'batchCreate', "productID=$story->product&branch=$story->branch&moduleID=$story->module&storyID=$story->id&executionID=$objectID"); + } + else + { + unset($actions[$key]); + continue; + } + } + if($isInModal && !isset($actions[$key]['data-toggle']) && !isset($actions[$key]['data-load'])) { $actions[$key]['data-load'] = 'modal'; diff --git a/module/story/view/sendmail.html.php b/module/story/view/sendmail.html.php index 5282167564..f88fd79e5d 100644 --- a/module/story/view/sendmail.html.php +++ b/module/story/view/sendmail.html.php @@ -10,7 +10,7 @@ * @link https://www.zentao.net */ ?> -id . ' ' . $object->title;?> +type) . ' #' . $object->id . ' ' . $object->title;?> app->getModuleRoot() . 'common/view/mail.header.html.php';?>
story->common;?> storyCount;?>
@@ -18,7 +18,7 @@
color) ? '#333' : $object->color;?> - id", 'html'), $mailTitle, '', "style='color: {$color}; text-decoration: underline;'");?> + type, 'view', "storyID=$object->id", 'html'), $mailTitle, '', "style='color: {$color}; text-decoration: underline;'");?>
diff --git a/module/story/zen.php b/module/story/zen.php index d043c76842..012de6b85f 100644 --- a/module/story/zen.php +++ b/module/story/zen.php @@ -71,10 +71,11 @@ class storyZen extends story * @param string $branch * @param int $executionID * @param string $extra + * @param string $storyType * @access protected * @return void */ - protected function setMenuForBatchCreate(int $productID, string $branch = '', int $executionID = 0, string $extra = ''): void + protected function setMenuForBatchCreate(int $productID, string $branch = '', int $executionID = 0, string $extra = '', $storyType = 'story'): void { $this->view->hiddenProduct = false; $this->view->hiddenPlan = false; @@ -125,7 +126,7 @@ class storyZen extends story $output = $this->story->parseExtra($extra); $regionPairs = $this->kanban->getRegionPairs($executionID, 0, 'execution'); $regionID = !empty($output['regionID']) ? $output['regionID'] : key($regionPairs); - $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, 'story'); + $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, $storyType); $laneID = !empty($output['laneID']) ? $output['laneID'] : key($lanePairs); $this->view->regionID = $regionID; @@ -187,7 +188,7 @@ class storyZen extends story $this->loadModel('my'); if($from == 'work' || $from == 'contribute') { - $this->lang->my->menu->{$from}['subModule'] = 'story'; + $this->lang->my->menu->{$from}['subModule'] = $storyType; $this->lang->my->menu->{$from}['subMenu']->{$storyType}['subModule'] = 'story'; } return; @@ -251,7 +252,7 @@ class storyZen extends story { if($from == 'work' || $from == 'contribute') { - $this->lang->my->menu->{$from}['subModule'] = 'story'; + $this->lang->my->menu->{$from}['subModule'] = $storyType; $this->lang->my->menu->{$from}['subMenu']->{$storyType}['subModule'] = 'story'; } @@ -268,10 +269,11 @@ class storyZen extends story * * @param int $objectID * @param array $kanbanSetting + * @param string $storyType * @access protected * @return void */ - protected function setViewVarsForKanban(int $objectID, array $kanbanSetting): void + protected function setViewVarsForKanban(int $objectID, array $kanbanSetting, string $storyType = 'story'): void { if(empty($objectID)) return; @@ -281,7 +283,7 @@ class storyZen extends story /* 如果是看板执行,设置看板的view变量。 */ $regionPairs = $this->loadModel('kanban')->getRegionPairs($execution->id, 0, 'execution'); $regionID = !empty($kanbanSetting['regionID']) ? $kanbanSetting['regionID'] : key($regionPairs); - $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, 'story'); + $lanePairs = $this->kanban->getLanePairsByRegion((int)$regionID, $storyType); $laneID = !empty($kanbanSetting['laneID']) ? $kanbanSetting['laneID'] : key($lanePairs); $this->view->executionType = 'kanban'; @@ -476,10 +478,11 @@ class storyZen extends story * @param string $branch * @param int $objectID * @param object $initStory + * @param string $storyType * @access protected * @return array */ - protected function getFormFieldsForCreate(int $productID, string $branch, int $objectID, object $initStory): array + protected function getFormFieldsForCreate(int $productID, string $branch, int $objectID, object $initStory, string $storyType = 'story'): array { $account = $this->app->user->account; $fields = $this->config->story->form->create; @@ -493,22 +496,24 @@ class storyZen extends story } $branch = str_contains($branch, ',') ? current(explode(',', $branch)) : $branch; - $product = $this->product->getByID($productID); - $users = $this->user->getPairs('pdfirst|noclosed|nodeleted'); - $stories = $this->story->getParentStoryPairs($productID); - $plans = $this->loadModel('productplan')->getPairs($productID, $branch == 0 ? '' : $branch, 'unexpired|noclosed', true); - $plans = array_map(function($planName){return str_replace(FUTURE_TIME, $this->lang->story->undetermined, $planName);}, $plans); - $forceReview = $this->story->checkForceReview(); - $needReview = ($account == $product->PO || $objectID > 0 || $this->config->story->needReview == 0 || !$forceReview); - $reviewers = $this->story->getProductReviewers($productID); + $product = $this->product->getByID($productID); + $users = $this->user->getPairs('pdfirst|noclosed|nodeleted'); + $stories = $this->story->getParentStoryPairs($productID, '', $storyType); + $grades = $this->story->getGradePairs($storyType); + $plans = $this->loadModel('productplan')->getPairs($productID, $branch == 0 ? '' : $branch, 'unexpired|noclosed', true); + $plans = array_map(function($planName){return str_replace(FUTURE_TIME, $this->lang->story->undetermined, $planName);}, $plans); + $forceReview = $this->story->checkForceReview($storyType); + $needReview = ($account == $product->PO || $objectID > 0 || $this->config->{$storyType}->needReview == 0 || !$forceReview); + $reviewers = $this->story->getProductReviewers($productID); + $requiredFields = $this->config->{$storyType}->create->requiredFields; /* 追加字段的name、title属性,展开user数据。 */ foreach($fields as $field => $attr) { if(isset($attr['options']) and $attr['options'] == 'users') $fields[$field]['options'] = $users; - if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field; - if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->story, $field); - if(strpos(",{$this->config->story->create->requiredFields},", ",$field,") !== false) $fields[$field]['required'] = true; + if(!isset($fields[$field]['name'])) $fields[$field]['name'] = $field; + if(!isset($fields[$field]['title'])) $fields[$field]['title'] = zget($this->lang->story, $field); + if(str_contains($requiredFields, $field)) $fields[$field]['required'] = true; } /* 设置下拉菜单内容。 */ @@ -517,6 +522,8 @@ class storyZen extends story $fields['branches']['options'] = $branches; $fields['plan']['options'] = $plans; $fields['plans']['options'] = $plans; + $fields['grade']['options'] = $grades; + $fields['grade']['default'] = current($grades); $fields['reviewer']['options'] = $reviewers; $fields['parent']['options'] = array_filter($stories); @@ -543,6 +550,7 @@ class storyZen extends story $this->view->objectID = $objectID; $this->view->forceReview = $forceReview; $this->view->needReview = $needReview; + $this->view->gradeRule = $this->config->{$storyType}->gradeRule; return $fields; } @@ -561,9 +569,11 @@ class storyZen extends story /* 准备数据。*/ $story = $this->view->story; + $parent = $this->story->fetchByID($story->parent); + $grades = $this->story->getGradeOptions($parent, $story->type, (array)$story->grade); $product = $this->view->product; $users = $this->loadModel('user')->getPairs('pofirst|nodeleted|noclosed', "$story->assignedTo,$story->openedBy,$story->closedBy"); - $stories = $this->story->getParentStoryPairs($story->product, $story->parent); + $stories = $this->story->getParentStoryPairs($story->product, $story->parent, $story->type, $storyID); $plans = $this->loadModel('productplan')->getPairs($story->product, $story->branch == 0 ? 'all' : $story->branch, '', true); $reviewerList = $this->story->getReviewerPairs($story->id, $story->version); @@ -611,6 +621,7 @@ class storyZen extends story $fields['plan']['options'] = $plans; $fields['reviewer']['options'] = $reviewers; $fields['parent']['options'] = array_filter($stories); + $fields['grade']['options'] = $grades; $fields['duplicateStory']['options'] = $productStories; $fields['assignedTo']['options'] += array('closed' => 'Closed'); @@ -619,6 +630,7 @@ class storyZen extends story $this->view->users = $users; $this->view->storyReviewers = array_keys($reviewerList); + $this->view->gradeRule = $this->config->{$story->type}->gradeRule; return $fields; } @@ -638,6 +650,8 @@ class storyZen extends story $product = $this->loadModel('product')->getByID($productID); $fields = $this->config->story->form->batchCreate; + foreach(explode(',', trim($this->config->{$storyType}->create->requiredFields, ',')) as $field) $fields[$field]['required'] = true; + if($executionID) { $productBranches = $product->type != 'normal' ? $this->loadModel('execution')->getBranchByProduct(array($productID), $executionID, 'noclosed|withMain') : array(); @@ -661,10 +675,8 @@ class storyZen extends story $plans = $this->loadModel('productplan')->getPairs($productID, ($branch === 'all' or empty($branch)) ? '' : $branch, 'unexpired|noclosed', true); $reviewers = $this->story->getProductReviewers($productID); $users = $this->user->getPairs('pdfirst|noclosed|nodeleted'); - $stories = $this->story->getParentStoryPairs($productID); - - $storyTypes = strpos($product->vision, 'or') !== false ? 'launched' : 'active'; - $URS = $storyType != 'story' ? array() : $this->story->getProductStoryPairs($productID, $branch, 0, $storyTypes, 'id_desc', 0, '', 'requirement'); + $stories = $this->story->getParentStoryPairs($productID, '', $storyType); + $grades = $this->story->getGradePairs($storyType); /* 追加字段的label属性。 */ foreach($fields as $field => $attr) @@ -695,24 +707,26 @@ class storyZen extends story $fields = array_merge($fieldPlatform, $fields); break; } + + $fields['grade']['default'] = current($grades); $fields['module']['options'] = $modules; $fields['plan']['options'] = $plans; + $fields['grade']['options'] = $grades; $fields['reviewer']['options'] = $reviewers; $fields['assignedTo']['options'] = $users; $fields['mailto']['options'] = $users; $fields['parent']['options'] = array_filter($stories); - $fields['URS']['options'] = $URS; - if($this->story->checkForceReview()) $fields['reviewer']['required'] = true; + if($this->story->checkForceReview($storyType)) $fields['reviewer']['required'] = true; if(empty($branches)) unset($fields['branch']); - if($this->view->hiddenPlan || $storyType == 'requirement') unset($fields['plan']); - if($storyType == 'requirement') - { - unset($fields['parent']); - unset($fields['URS']); - } + if($this->view->hiddenPlan) unset($fields['plan']); - $this->view->branchID = $branch; + $hiddenGrade = $this->config->edition != 'ipd'; + if($hiddenGrade && $storyType == 'epic') $fields['parent']['hidden'] = true; + + $this->view->branchID = $branch; + $this->view->gradeRule = $this->config->{$storyType}->gradeRule; + $this->view->hiddenGrade = $hiddenGrade; return $fields; } @@ -747,11 +761,11 @@ class storyZen extends story $fields['verify']['default'] = $story->verify; $fields['status']['default'] = $story->status; - $forceReview = $this->story->checkForceReview(); + $forceReview = $this->story->checkForceReview($story->type); if($forceReview) $fields['reviewer']['required'] = true; $this->view->forceReview = $forceReview; - $this->view->needReview = ($this->app->user->account == $this->view->product->PO || $this->config->story->needReview == 0 || !$forceReview) && empty($reviewer); + $this->view->needReview = ($this->app->user->account == $this->view->product->PO || $this->config->{$story->type}->needReview == 0 || !$forceReview) && empty($reviewer); $fields['comment'] = array('type' => 'string', 'control' => 'editor', 'required' => false, 'default' => '', 'name' => 'comment', 'title' => $this->lang->comment); @@ -775,7 +789,7 @@ class storyZen extends story $story = $this->view->story; $fields = $this->config->story->form->review; $users = $this->loadModel('user')->getPairs('nodeleted|noclosed', "$story->lastEditedBy,$story->openedBy"); - $resultList = $this->lang->story->reviewResultList; + $resultList = $this->lang->{$story->type}->reviewResultList; if($story->status == 'reviewing') { if($story->version == 1) unset($resultList['revert']); @@ -1000,20 +1014,11 @@ class storyZen extends story */ protected function removeFormFieldsForCreate(array $fields, string $storyType = 'story'): array { - $productID = $this->view->productID; - $branch = $this->view->branch; - $objectID = $this->view->objectID; + $objectID = $this->view->objectID; /* Hidden some fields of projects without products. */ - $hiddenProduct = $hiddenParent = $hiddenPlan = $hiddenURS = false; - $teamUsers = $URS = array(); - - if($storyType == 'story') - { - $moduleIdList = $this->tree->getAllChildId($this->view->moduleID); - $URS = $this->story->getProductStoryPairs($productID, $branch === 'all' ? 0 : $branch, $moduleIdList, 'active,launched', 'id_desc', 0, '', 'requirement'); - } - $fields['URS']['options'] = $URS; + $hiddenProduct = $hiddenParent = $hiddenPlan = false; + $teamUsers = array(); if($this->app->tab === 'project' || $this->app->tab === 'execution') { @@ -1028,11 +1033,9 @@ class storyZen extends story $hiddenProduct = true; if($project->model !== 'scrum' or !$project->multiple) $hiddenPlan = true; - if($project->model === 'kanban') $hiddenURS = true; } } - if($storyType != 'story') unset($fields['region'], $fields['lane'], $fields['branches'], $fields['modules'], $fields['plans']); - if($storyType != 'story' || !$this->config->URAndSR || $hiddenURS) unset($fields['URS']); + if($storyType != 'story') unset($fields['branches'], $fields['modules'], $fields['plans']); if($hiddenPlan) unset($fields['plan']); if($hiddenProduct) { @@ -1041,7 +1044,11 @@ class storyZen extends story $fields['assignedTo']['options'] = $teamUsers; } + $hiddenGrade = $this->config->edition != 'ipd'; + if($hiddenGrade && $storyType == 'epic') $hiddenParent = true; + $this->view->hiddenParent = $hiddenParent; + $this->view->hiddenGrade = $hiddenGrade; return $fields; } @@ -1055,10 +1062,9 @@ class storyZen extends story */ protected function hiddenFormFieldsForEdit(array $fields): array { - $story = $this->view->story; $product = $this->view->product; - $hiddenProduct = $hiddenParent = $hiddenPlan = $hiddenURS = false; + $hiddenProduct = $hiddenParent = $hiddenPlan = false; $teamUsers = array(); if($product->shadow) { @@ -1074,7 +1080,6 @@ class storyZen extends story $hiddenPlan = true; unset($this->lang->story->stageList[''], $this->lang->story->stageList['wait'], $this->lang->story->stageList['planned']); } - if($project->model === 'kanban') $hiddenURS = true; } if($hiddenProduct) @@ -1109,7 +1114,7 @@ class storyZen extends story unset($fields['region']); unset($fields['lane']); } - if($this->app->tab == 'project' || $this->app->tab == 'execution') unset($fields['parent']); + if($this->app->tab == 'project' || $this->app->tab == 'execution') $fields['parent']['hidden'] = true; return $fields; } @@ -1137,16 +1142,17 @@ class storyZen extends story * 构建创建需求数据。 * Build story for create * - * @param int $executionID - * @param int $bugID + * @param int $executionID + * @param int $bugID + * @param string $storyType * @access protected * @return object|false */ - protected function buildStoryForCreate(int $executionID, int $bugID): object|false + protected function buildStoryForCreate(int $executionID, int $bugID, string $storyType = 'story'): object|false { $fields = $this->config->story->form->create; $editorFields = array_keys(array_filter(array_map(function($config){return $config['control'] == 'editor';}, $fields))); - foreach(explode(',', trim($this->config->story->create->requiredFields, ',')) as $field) $fields[$field]['required'] = true; + foreach(explode(',', trim($this->config->{$storyType}->create->requiredFields, ',')) as $field) $fields[$field]['required'] = true; if(!isset($_POST['plan'])) $this->config->story->create->requiredFields = str_replace(',plan,', ',', ",{$this->config->story->create->requiredFields},"); if(!empty($_POST['modules']) && !empty($fields['module']['required'])) { @@ -1162,11 +1168,12 @@ class storyZen extends story $storyData = form::data($fields) ->setIF($this->post->assignedTo, 'assignedDate', helper::now()) - ->setIF($this->post->plan > 0, 'stage', 'planned') + ->setIF($this->post->plan > 0 && $storyType == 'story', 'stage', 'planned') ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '') ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '') ->setIF($executionID > 0, 'stage', 'projected') ->setIF($bugID > 0, 'fromBug', $bugID) + ->setIF($storyType != 'story', 'stage', 'defining') ->get(); if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']); @@ -1177,7 +1184,7 @@ class storyZen extends story } /* Need and force review, then set status to reviewing. */ - if($storyData->status != 'draft' && $this->story->checkForceReview() && !$this->post->needNotReview) $storyData->status = 'reviewing'; + if($storyData->status != 'draft' and $this->story->checkForceReview($storyType) and !$this->post->needNotReview) $storyData->status = 'reviewing'; /* If in ipd mode, set requirement status = 'launched'. */ if($this->config->systemMode == 'PLM' && $storyData->type == 'requirement' && $storyData->status == 'active' && $this->config->vision == 'rnd') $storyData->status = 'launched'; @@ -1200,14 +1207,14 @@ class storyZen extends story $oldStory = $this->story->getByID($storyID); if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther; - if(strpos('draft,changing', $oldStory->status) !== false and $this->story->checkForceReview() and empty($_POST['reviewer'])) dao::$errors[] = $this->lang->story->notice->reviewerNotEmpty; + if(strpos('draft,changing', $oldStory->status) !== false and $this->story->checkForceReview($oldStory->type) and empty($_POST['reviewer'])) dao::$errors[] = $this->lang->story->notice->reviewerNotEmpty; if(!empty($_POST['plan'])) $storyPlan = is_array($_POST['plan']) ? array_filter($_POST['plan']) : array($_POST['plan']); - if(count($storyPlan) > 1) + if(count($storyPlan) > 1 && $oldStory->type == 'story') { $oldStoryPlan = !empty($oldStory->plan) ? array_filter(explode(',', $oldStory->plan)) : array(); $oldPlanDiff = array_diff($storyPlan, $oldStoryPlan); $storyPlanDiff = array_diff($oldStoryPlan, $storyPlan); - if(!empty($oldPlanDiff) or !empty($storyPlanDiff)) dao::$errors[] = $this->lang->story->notice->changePlan; + if(!empty($oldPlanDiff) or !empty($storyPlanDiff)) dao::$errors['plan'] = $this->lang->story->notice->changePlan; } if(strpos(',draft,changing,', $oldStory->status) !== false) { @@ -1238,13 +1245,13 @@ class storyZen extends story ->setDefault('estimate', $oldStory->estimate) ->setDefault('stage', $oldStory->stage) ->setDefault('stagedBy', $oldStory->stagedBy) - ->setDefault('childStories', $oldStory->childStories) ->setIF($this->post->assignedTo != $oldStory->assignedTo, 'assignedDate', $now) ->setIF($this->post->closedBy && $oldStory->closedDate == '', 'closedDate', $now) ->setIF($this->post->closedReason && $oldStory->closedDate == '', 'closedDate', $now) ->setIF($this->post->closedBy || $this->post->closedReason != false, 'status', 'closed') ->setIF($this->post->closedReason && $this->post->closedBy == false, 'closedBy', $this->app->user->account) ->setIF($this->post->stage == 'released', 'releasedDate', $now) + ->setIF(!$this->post->grade, 'grade', $oldStory->grade) ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'feedbackBy', '') ->setIF(!in_array($this->post->source, $this->config->story->feedbackSource), 'notifyEmail', '') ->setIF(!empty($_POST['plan'][0]) and $oldStory->stage == 'wait', 'stage', 'planned') @@ -1256,6 +1263,13 @@ class storyZen extends story if($this->post->linkStories) $storyData->linkStories = implode(',', array_unique($this->post->linkStories)); if($this->post->linkRequirements) $storyData->linkRequirements = implode(',', array_unique($this->post->linkRequirements)); + if($oldStory->product != $storyData->product) + { + $storyData->root = $storyID; + $storyData->parent = 0; + $storyData->module = 0; + } + return $this->loadModel('file')->processImgURL($storyData, $editorFields, $this->post->uid); } @@ -1269,11 +1283,11 @@ class storyZen extends story */ protected function buildStoryForChange(int $storyID): object|false { - $oldStory = $this->story->getByID($storyID); - if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther; - if(strpos($this->config->story->change->requiredFields, 'comment') !== false and !$this->post->comment) dao::$errors['comment'][] = sprintf($this->lang->error->notempty, $this->lang->comment); - if(strpos($this->config->story->change->requiredFields, 'spec') !== false and !$this->post->spec) dao::$errors['spec'][] = sprintf($this->lang->error->notempty, $this->lang->story->spec); - if(strpos($this->config->story->change->requiredFields, 'verify') !== false and !$this->post->spec) dao::$errors['verify'][] = sprintf($this->lang->error->notempty, $this->lang->story->verify); + $oldStory = $this->story->getByID($storyID); + if(!empty($_POST['lastEditedDate']) and $oldStory->lastEditedDate != $this->post->lastEditedDate) dao::$errors[] = $this->lang->error->editedByOther; + if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'spec') !== false and !$this->post->spec) dao::$errors['spec'][] = sprintf($this->lang->error->notempty, $this->lang->story->spec); + if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'verify') !== false and !$this->post->spec) dao::$errors['verify'][] = sprintf($this->lang->error->notempty, $this->lang->story->verify); + if(strpos($this->config->{$oldStory->type}->change->requiredFields, 'comment') !== false and !$this->post->comment) dao::$errors['comment'] = sprintf($this->lang->error->notempty, $this->lang->comment); if(isset($_POST['reviewer'])) $_POST['reviewer'] = array_filter($_POST['reviewer']); if(!$this->post->needNotReview and empty($_POST['reviewer'])) dao::$errors['reviewer'] = $this->lang->story->errorEmptyReviewedBy; @@ -1393,7 +1407,7 @@ class storyZen extends story if(isset($_POST['plan']) and is_array($_POST['plan'])) $story->plan = trim(implode(',', $_POST['plan']), ','); if(isset($_POST['branch']) and $_POST['branch'] == 0) $story->branch = 0; - if(isset($story->stage) and $oldStory->stage != $story->stage) $story->stagedBy = (strpos('tested|verified|released|closed', $story->stage) !== false) ? $this->app->user->account : ''; + if(isset($story->stage) and $oldStory->stage != $story->stage) $story->stagedBy = (strpos('tested|verified|rejected|pending|released|closed', $story->stage) !== false) ? $this->app->user->account : ''; if(isset($_POST['reviewer']) or isset($_POST['needNotReview'])) $this->story->doUpdateReviewer($storyID, $story); } @@ -1407,15 +1421,21 @@ class storyZen extends story */ protected function buildStoryForReview(int $storyID): object|false { - $now = helper::now(); - $fields = $this->config->story->form->review; - foreach(explode(',', trim($this->config->story->review->requiredFields, ',')) as $field) + $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); + $now = helper::now(); + $fields = $this->config->story->form->review; + foreach(explode(',', trim($this->config->{$oldStory->type}->review->requiredFields, ',')) as $field) { if($field == 'comment' && !$this->post->comment) { dao::$errors['comment'][] = sprintf($this->lang->error->notempty, $this->lang->comment); return false; } + if($field == 'reviewedDate' && !$this->post->reviewedDate) + { + dao::$errors['reviewedDate'] = sprintf($this->lang->error->notempty, $this->lang->story->reviewedDate); + return false; + } if(isset($fields[$field])) $fields[$field]['required'] = true; } if($this->post->result == false) @@ -1427,13 +1447,11 @@ class storyZen extends story $editorFields = array_keys(array_filter(array_map(function($config){return $config['control'] == 'editor';}, $fields))); $result = $this->post->result; $closedReason = $this->post->closedReason; - $oldStory = $this->dao->findById($storyID)->from(TABLE_STORY)->fetch(); $storyData = form::data($fields) ->setDefault('lastEditedBy', $this->app->user->account) ->setDefault('lastEditedDate', $now) - ->removeIF($result != 'reject', 'closedReason,duplicateStory,childStories') + ->removeIF($result != 'reject', 'closedReason,duplicateStory') ->removeIF($result == 'reject' && $closedReason != 'duplicate', 'duplicateStory') - ->removeIF($result == 'reject' && $closedReason != 'subdivided', 'childStories') ->get(); if($oldStory->assignedTo != $storyData->assignedTo) $storyData->assignedDate = $now; @@ -1457,7 +1475,7 @@ class storyZen extends story */ protected function buildStoriesForBatchCreate(int $productID, string $storyType): array { - $forceReview = $this->story->checkForceReview(); + $forceReview = $this->story->checkForceReview($storyType); $fields = $this->config->story->form->batchCreate; $account = $this->app->user->account; $now = helper::now(); @@ -1470,13 +1488,21 @@ class storyZen extends story $story->type = $storyType; $story->status = (empty($story->reviewer) && !$forceReview) ? 'active' : 'reviewing'; $story->status = $saveDraft ? 'draft' : $story->status; - $story->stage = ($this->app->tab == 'project' || $this->app->tab == 'execution') ? 'projected' : 'wait'; $story->product = $productID; $story->openedBy = $account; $story->vision = $this->config->vision; $story->openedDate = $now; $story->version = 1; + if(in_array($this->app->tab, array('project', 'execution'))) + { + $story->stage = $storyType == 'story' ? 'projected' : 'planning'; + } + else + { + $story->stage = $storyType == 'story' ? 'wait' : 'defining'; + } + !empty($story->assignedTo) && $story->assignedDate = $now; if($this->post->uploadImage && $this->post->uploadImage[$i]) $story->uploadImage = $this->post->uploadImage[$i]; @@ -1501,7 +1527,6 @@ class storyZen extends story $now = helper::now(); $fields['duplicateStory'] = array('type' => 'int', 'required' => false, 'default' => 0); - $fields['childStories'] = array('type' => 'string', 'required' => false, 'default' => '', 'filter' => 'trim'); $stories = form::batchData($fields)->get(); $oldStories = $this->story->getByList(array_keys($stories)); @@ -1515,7 +1540,7 @@ class storyZen extends story if($oldStory->assignedTo != $story->assignedTo) $story->assignedDate = $now; if($oldStory->parent < 0) $story->plan = ''; - if($story->stage != $oldStory->stage) $story->stagedBy = (str_contains('|tested|verified|released|closed|', "|{$story->stage}|")) ? $account : ''; + if($story->stage != $oldStory->stage) $story->stagedBy = (str_contains('|tested|verified|rejected|pending|released|closed|', "|{$story->stage}|")) ? $account : ''; if($story->closedBy && helper::isZeroDate($oldStory->closedDate)) $story->closedDate = $now; if($story->closedReason && helper::isZeroDate($oldStory->closedDate)) $story->closedDate = $now; @@ -1633,7 +1658,7 @@ class storyZen extends story */ protected function getAfterEditLocation(int $storyID, string $storyType) { - $module = 'story'; + $module = $storyType; $params = "storyID=$storyID&version=0¶m=0&storyType=$storyType"; $method = 'view'; if($this->app->tab == 'project') @@ -1701,7 +1726,11 @@ class storyZen extends story if($storyID) { $locateLink = $this->session->storyList ? $this->session->storyList : $this->createLink('projectstory', 'view', "storyID=$storyID&projectID={$this->app->project}"); - if($this->app->tab == 'product') $locateLink = $this->inlink('view', "storyID=$storyID&version=0¶m=0&storyType=$storyType"); + if($this->app->tab == 'product') + { + $story = $this->story->fetchById($storyID); + $locateLink = $this->createLink($story->type, 'view', "storyID=$storyID&version=0¶m=0&storyType=$storyType"); + } return $locateLink; } @@ -1733,7 +1762,7 @@ class storyZen extends story protected function getAfterChangeLocation(int $storyID, string $storyType = 'story'): string { if($this->app->tab == 'execution') return helper::createLink('execution', 'storyView', "storyID=$storyID"); - if($this->app->tab != 'project') return helper::createLink('story', 'view', "storyID=$storyID&version=0¶m=0&storyType=$storyType"); + if($this->app->tab != 'project') return helper::createLink($storyType, 'view', "storyID=$storyID&version=0¶m=0&storyType=$storyType"); if($this->app->tab == 'project') { @@ -1768,7 +1797,7 @@ class storyZen extends story return helper::createLink('projectstory', 'view', "storyID={$storyID}&projectID={$this->session->project}"); } - if($from != 'execution') return helper::createLink('story', 'view', "storyID={$storyID}&version=0¶m=0&storyType={$storyType}"); + if($from != 'execution') return helper::createLink($storyType, 'view', "storyID={$storyID}&version=0¶m=0&storyType={$storyType}"); $module = 'execution'; $method = 'storyView'; @@ -1842,14 +1871,6 @@ class storyZen extends story $config->story->custom->batchCreateFields = trim(sprintf($config->story->custom->batchCreateFields, ''), ','); } - /* User requirement without plan/parent/URS field. */ - if($storyType == 'requirement') - { - unset($customFields['plan']); - unset($customFields['parent']); - unset($customFields['URS']); - } - return $customFields; } @@ -1871,7 +1892,6 @@ class storyZen extends story $showFields = str_replace(array(0 => ",branch,", 1 => ",platform,"), '', ",$showFields,"); $showFields = trim($showFields, ','); } - if($storyType == 'requirement') $showFields = str_replace(array(',plan,', ',parent,', ',URS,'), ',', ",$showFields,"); return $showFields; } @@ -1930,7 +1950,7 @@ class storyZen extends story } /** - * Set hidden fields for view. like: hiddenPlan,hiddenURS. + * Set hidden fields for view. like: hiddenPlan. * * @param object $product * @access protected @@ -1939,7 +1959,6 @@ class storyZen extends story protected function setHiddenFieldsForView(object $product) { $this->view->hiddenPlan = false; - $this->view->hiddenURS = false; if(empty($product->shadow)) return; $projectInfo = $this->dao->select('t2.model, t2.multiple')->from(TABLE_PROJECTPRODUCT)->alias('t1') @@ -1949,12 +1968,8 @@ class storyZen extends story ->fetch(); if($projectInfo->model == 'waterfall') $this->view->hiddenPlan = true; - if($projectInfo->model == 'kanban') - { - $this->view->hiddenPlan = true; - $this->view->hiddenURS = true; - } - if(!$projectInfo->multiple) $this->view->hiddenPlan = true; + if($projectInfo->model == 'kanban') $this->view->hiddenPlan = true; + if(!$projectInfo->multiple) $this->view->hiddenPlan = true; } /** diff --git a/module/task/control.php b/module/task/control.php index a83220c311..d9a43ae455 100755 --- a/module/task/control.php +++ b/module/task/control.php @@ -1234,7 +1234,7 @@ class task extends control */ public function ajaxGetTestStories(int $executionID, int $taskID = 0) { - $stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', '', 'ignoreID', 'active'); + $stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', '', 'ignoreID', 'active', '', false); $testStoryIdList = $this->story->getTestStories(array_keys($stories), $executionID); $testStories = array(); foreach($stories as $testStoryID => $storyTitle) @@ -1266,7 +1266,7 @@ class task extends control */ public function ajaxGetStories(int $executionID, int $moduleID, string $zeroTaskStory = 'false') { - $stories = $this->loadModel('story')->getExecutionStoryPairs($executionID, 0, 'all', $moduleID, 'short', 'active'); + $stories = $this->loadModel('story')->getExecutionStoryPairs($executionID, 0, 'all', $moduleID, 'short', 'active', '', false); $taskCountList = $this->task->getStoryTaskCounts(array_keys($stories), $executionID); $items = array(); diff --git a/module/task/model.php b/module/task/model.php index cb277716cc..ad84a6da30 100755 --- a/module/task/model.php +++ b/module/task/model.php @@ -2228,7 +2228,7 @@ class taskModel extends model public function isNoStoryExecution(object $execution): bool { if(empty($execution)) return false; - return $execution->lifetime == 'ops' || in_array($execution->attribute, array('request', 'review')); + return $execution->lifetime == 'ops'; } /** diff --git a/module/task/ui/edit.html.php b/module/task/ui/edit.html.php index a9109232fa..bde0820d34 100644 --- a/module/task/ui/edit.html.php +++ b/module/task/ui/edit.html.php @@ -138,7 +138,7 @@ detailBody $task->desc && isHTML($task->desc) ? html($task->desc) : $task->desc ) ), - $execution->lifetime != 'ops' && !in_array($execution->attribute, array('request', 'review')) ? section + $execution->lifetime != 'ops' ? section ( set::title($lang->task->story), formGroup diff --git a/module/task/zen.php b/module/task/zen.php index 9a28b6404c..e32275a1bc 100644 --- a/module/task/zen.php +++ b/module/task/zen.php @@ -128,7 +128,7 @@ class taskZen extends task */ protected function assignStoryForCreate(int $executionID, int $moduleID): void { - $stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', $moduleID, 'full', 'active'); + $stories = $this->story->getExecutionStoryPairs($executionID, 0, 'all', $moduleID, 'full', 'active', '', false); $testStoryIdList = $this->loadModel('story')->getTestStories(array_keys($stories), $executionID); $testStories = array(); foreach($stories as $testStoryID => $storyTitle) @@ -281,7 +281,7 @@ class taskZen extends task } $this->view->title = $this->lang->task->edit . 'TASK' . $this->lang->hyphen . $this->view->task->name; - $this->view->stories = $this->story->getExecutionStoryPairs($this->view->execution->id, 0, 'all', '', 'full', 'active'); + $this->view->stories = $this->story->getExecutionStoryPairs($this->view->execution->id, 0, 'all', '', 'full', 'active', '', false); $this->view->tasks = $tasks; $this->view->taskMembers = $taskMembers; $this->view->users = $this->loadModel('user')->getPairs('nodeleted|noclosed', "{$task->openedBy},{$task->canceledBy},{$task->closedBy}"); @@ -351,7 +351,7 @@ class taskZen extends task $showAllModule = !empty($this->config->execution->task->allModule) ? 'allModule' : ''; $modules = $this->loadModel('tree')->getTaskOptionMenu($execution->id, 0, $showAllModule); $story = $this->story->getByID($storyID); - $stories = $this->story->getExecutionStoryPairs($execution->id, 0, 'all', $story ? $story->module : 0, 'short', 'active'); + $stories = $this->story->getExecutionStoryPairs($execution->id, 0, 'all', $story ? $story->module : 0, 'short', 'active', '', false); list($customFields, $checkedFields) = $this->getCustomFields($execution, 'batchCreate'); if(isset($customFields['story'])) $customFields['preview'] = $customFields['copyStory'] = ''; diff --git a/module/testcase/control.php b/module/testcase/control.php index 0e7c57eaa3..43264188b5 100644 --- a/module/testcase/control.php +++ b/module/testcase/control.php @@ -585,9 +585,11 @@ class testcase extends control $showSuhosinInfo = common::judgeSuhosinSetting($countInputVars); if($showSuhosinInfo) $this->view->suhosinInfo = extension_loaded('suhosin') ? sprintf($this->lang->suhosinInfo, $countInputVars) : sprintf($this->lang->maxVarsInfo, $countInputVars); + $stories = $this->loadModel('story')->getProductStoryPairs($productID, $branch); + /* 展示变量. */ /* Show the variables. */ - $this->view->stories = $this->loadModel('story')->getProductStoryPairs($productID, $branch); + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->caseIdList = $caseIdList; $this->view->productID = $productID; $this->view->cases = $cases; diff --git a/module/testcase/zen.php b/module/testcase/zen.php index df49213e8b..e2a37500bc 100755 --- a/module/testcase/zen.php +++ b/module/testcase/zen.php @@ -426,11 +426,11 @@ class testcaseZen extends testcase if($this->app->tab != 'qa' && $this->app->tab != 'product' && $this->app->tab != 'my') { $projectID = $this->app->tab == 'project' ? $this->session->project : $this->session->execution; - if($projectID) $stories = $this->story->getExecutionStoryPairs($projectID, $productID, $branch, $modules); + if($projectID) $stories = $this->story->getExecutionStoryPairs($projectID, $productID, $branch, $modules, 'full', 'all', 'story', false); } if($storyID && !isset($stories[$storyID])) $stories = $this->story->formatStories(array($storyID => $story)) + $stories; - $this->view->stories = $stories; + $this->view->stories = $this->story->addGradeLabel($stories); $this->view->currentModuleID = $currentModuleID; $this->view->moduleOptionMenu = $this->tree->getOptionMenu($productID, 'case', 0, $branch === 'all' || !isset($branches[$branch]) ? '0' : $branch); $this->view->sceneOptionMenu = $this->testcase->getSceneMenu($productID, $moduleID, $branch === 'all' || !isset($branches[$branch]) ? '0' : $branch); @@ -915,7 +915,7 @@ class testcaseZen extends testcase $storyStatus = $this->loadModel('story')->getStatusList('noclosed'); if($this->app->tab == 'execution') { - $stories = $this->loadModel('story')->getExecutionStoryPairs($case->execution, $case->product, $case->branch, $moduleIdList); + $stories = $this->loadModel('story')->getExecutionStoryPairs($case->execution, $case->product, $case->branch, $moduleIdList, 'full', 'all', 'story', false); } else { @@ -924,7 +924,7 @@ class testcaseZen extends testcase if(!in_array($this->app->tab, array('execution', 'project')) && empty($stories)) $stories = $this->story->getProductStoryPairs($case->product, $case->branch, 0, $storyStatus, 'id_desc', 0, 'full', 'story', false); - $this->view->stories = $stories; + $this->view->stories = $this->story->addGradeLabel($stories); } /** @@ -1362,7 +1362,7 @@ class testcaseZen extends testcase $this->view->customFields = $customFields; $this->view->showFields = $showFields; $this->view->story = $story; - $this->view->storyPairs = $storyPairs; + $this->view->storyPairs = $this->story->addGradeLabel($storyPairs); $this->view->sceneOptionMenu = $this->testcase->getSceneMenu($productID, $moduleID, $branch === 'all' || !isset($branches[$branch]) ? '0' : $branch); $this->view->currentModuleID = $moduleID; } diff --git a/module/todo/config.php b/module/todo/config.php index 5385112a93..df74c5645a 100644 --- a/module/todo/config.php +++ b/module/todo/config.php @@ -30,13 +30,15 @@ $config->todo->custom = new stdclass(); $config->todo->custom->batchCreateFields = 'type,pri,desc,beginAndEnd'; $config->todo->custom->batchEditFields = 'pri,beginAndEnd,status'; -$config->todo->moduleList = array('bug', 'task', 'story', 'testtask'); +$config->todo->moduleList = array('bug', 'task', 'epic', 'requirement', 'story', 'testtask'); $config->todo->getUserObjectsMethod = array(); -$config->todo->getUserObjectsMethod['bug'] = 'ajaxGetUserBugs'; -$config->todo->getUserObjectsMethod['task'] = 'ajaxGetUserTasks'; -$config->todo->getUserObjectsMethod['story'] = 'ajaxGetUserStories'; -$config->todo->getUserObjectsMethod['testtask'] = 'ajaxGetUserTestTasks'; +$config->todo->getUserObjectsMethod['bug'] = 'ajaxGetUserBugs'; +$config->todo->getUserObjectsMethod['task'] = 'ajaxGetUserTasks'; +$config->todo->getUserObjectsMethod['story'] = 'ajaxGetUserStories'; +$config->todo->getUserObjectsMethod['epic'] = 'ajaxGetUserStories'; +$config->todo->getUserObjectsMethod['requirement'] = 'ajaxGetUserStories'; +$config->todo->getUserObjectsMethod['testtask'] = 'ajaxGetUserTestTasks'; $config->todo->objectList = array(); $config->todo->objectList['bug'] = 'bugs'; diff --git a/module/todo/config/form.php b/module/todo/config/form.php index 14e19cacbf..a530f1c05b 100644 --- a/module/todo/config/form.php +++ b/module/todo/config/form.php @@ -5,6 +5,8 @@ $config->todo->create->form = array(); $config->todo->create->form['type'] = array('required' => true, 'type' => 'string'); $config->todo->create->form['name'] = array('required' => true, 'type' => 'string', 'default' => ''); $config->todo->create->form['story'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->create->form['epic'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->create->form['requirement'] = array('required' => false, 'type' => 'string', 'default' => 0); $config->todo->create->form['task'] = array('required' => false, 'type' => 'string', 'default' => 0); $config->todo->create->form['bug'] = array('required' => false, 'type' => 'string', 'default' => 0); $config->todo->create->form['status'] = array('required' => true, 'type' => 'string'); @@ -63,19 +65,21 @@ $config->todo->batchClose->form['todoIdList'] = array('required' => true, 'type' $config->todo->batchEdit = new stdclass(); $config->todo->batchEdit->form = array(); -$config->todo->batchEdit->form['id'] = array('required' => true, 'type' => 'int', 'base' => true); -$config->todo->batchEdit->form['date'] = array('required' => true, 'type' => 'date'); -$config->todo->batchEdit->form['type'] = array('required' => true, 'type' => 'string'); -$config->todo->batchEdit->form['pri'] = array('required' => true, 'type' => 'int'); -$config->todo->batchEdit->form['name'] = array('required' => false, 'type' => 'string', 'default' => ''); -$config->todo->batchEdit->form['assignedTo'] = array('required' => false, 'type' => 'string', 'default' => ''); -$config->todo->batchEdit->form['begin'] = array('required' => false, 'type' => 'string', 'default' => ''); -$config->todo->batchEdit->form['end'] = array('required' => false, 'type' => 'string', 'default' => ''); -$config->todo->batchEdit->form['status'] = array('required' => true, 'type' => 'string'); -$config->todo->batchEdit->form['story'] = array('required' => false, 'type' => 'string', 'default' => 0); -$config->todo->batchEdit->form['task'] = array('required' => false, 'type' => 'string', 'default' => 0); -$config->todo->batchEdit->form['bug'] = array('required' => false, 'type' => 'string', 'default' => 0); -$config->todo->batchEdit->form['testtask'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['id'] = array('required' => true, 'type' => 'int', 'base' => true); +$config->todo->batchEdit->form['date'] = array('required' => true, 'type' => 'date'); +$config->todo->batchEdit->form['type'] = array('required' => true, 'type' => 'string'); +$config->todo->batchEdit->form['pri'] = array('required' => true, 'type' => 'int'); +$config->todo->batchEdit->form['name'] = array('required' => false, 'type' => 'string', 'default' => ''); +$config->todo->batchEdit->form['assignedTo'] = array('required' => false, 'type' => 'string', 'default' => ''); +$config->todo->batchEdit->form['begin'] = array('required' => false, 'type' => 'string', 'default' => ''); +$config->todo->batchEdit->form['end'] = array('required' => false, 'type' => 'string', 'default' => ''); +$config->todo->batchEdit->form['status'] = array('required' => true, 'type' => 'string'); +$config->todo->batchEdit->form['story'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['epic'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['requirement'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['task'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['bug'] = array('required' => false, 'type' => 'string', 'default' => 0); +$config->todo->batchEdit->form['testtask'] = array('required' => false, 'type' => 'string', 'default' => 0); if($config->edition != 'open') $config->todo->batchEdit->form['feedback'] = array('required' => false, 'type' => 'string', 'default' => 0); if(in_array($config->edition, array('max', 'ipd'))) { diff --git a/module/todo/control.php b/module/todo/control.php index bf54560978..28f4477aad 100755 --- a/module/todo/control.php +++ b/module/todo/control.php @@ -499,16 +499,18 @@ class todo extends control $todos = $this->todo->getByExportList($orderBy, (string) $this->session->todoReportCondition, (string)$checkedItem); list($todos, $fields) = $this->todoZen->exportTodoInfo((array)$todos, (string)$this->config->todo->list->exportFields, $todoLang); - list($users, $bugs, $stories, $tasks, $testTasks) = $this->todoZen->exportAssociated('default', (string)$user->account); + list($users, $bugs, $stories, $epics, $requirements, $tasks, $testTasks) = $this->todoZen->exportAssociated('default', (string)$user->account); $times = date::buildTimeList((int)$configTime->begin, (int)$configTime->end, (int)$configTime->delta); $assemble = new stdclass(); - $assemble->users = $users; - $assemble->bugs = $bugs; - $assemble->stories = $stories; - $assemble->tasks = $tasks; - $assemble->testTasks = $testTasks; + $assemble->users = $users; + $assemble->bugs = $bugs; + $assemble->stories = $stories; + $assemble->epics = $epics; + $assemble->requirements = $requirements; + $assemble->tasks = $tasks; + $assemble->testTasks = $testTasks; if(in_array($this->config->edition, array('max', 'ipd'))) { $iroData = $this->todoZen->exportAssociated((string)$this->config->edition, (string)$user->account); diff --git a/module/todo/lang/de.php b/module/todo/lang/de.php index 17c26afb21..37459f2019 100644 --- a/module/todo/lang/de.php +++ b/module/todo/lang/de.php @@ -116,12 +116,14 @@ $lang->todo->priList[2] = 'Hoch'; $lang->todo->priList[3] = 'Normal'; $lang->todo->priList[4] = 'Niedrig'; -$lang->todo->typeList['custom'] = 'Eigene'; -$lang->todo->typeList['cycle'] = 'Wiederkehrend'; -$lang->todo->typeList['bug'] = 'Bug'; -$lang->todo->typeList['task'] = 'Aufgabe'; -$lang->todo->typeList['story'] = 'Story'; -$lang->todo->typeList['testtask'] = 'Testtask'; +$lang->todo->typeList['custom'] = 'Eigene'; +$lang->todo->typeList['cycle'] = 'Wiederkehrend'; +$lang->todo->typeList['bug'] = 'Bug'; +$lang->todo->typeList['task'] = 'Aufgabe'; +$lang->todo->typeList['story'] = 'Story'; +if($config->enableER) $lang->todo->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->todo->typeList['requirement'] = $lang->URCommon; +$lang->todo->typeList['testtask'] = 'Testtask'; $lang->todo->fromList['bug'] = 'Related Bug'; $lang->todo->fromList['task'] = 'Related Task'; diff --git a/module/todo/lang/en.php b/module/todo/lang/en.php index ae07cef355..b8623fa6b1 100644 --- a/module/todo/lang/en.php +++ b/module/todo/lang/en.php @@ -116,12 +116,14 @@ $lang->todo->priList[2] = 'Important'; $lang->todo->priList[3] = 'Normal'; $lang->todo->priList[4] = 'Low'; -$lang->todo->typeList['custom'] = 'Custom'; -$lang->todo->typeList['cycle'] = 'Recur'; -$lang->todo->typeList['bug'] = 'Bug'; -$lang->todo->typeList['task'] = 'Task'; -$lang->todo->typeList['story'] = 'Story'; -$lang->todo->typeList['testtask'] = 'Testtask'; +$lang->todo->typeList['custom'] = 'Custom'; +$lang->todo->typeList['cycle'] = 'Recur'; +$lang->todo->typeList['bug'] = 'Bug'; +$lang->todo->typeList['task'] = 'Task'; +$lang->todo->typeList['story'] = 'Story'; +if($config->enableER) $lang->todo->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->todo->typeList['requirement'] = $lang->URCommon; +$lang->todo->typeList['testtask'] = 'Testtask'; $lang->todo->fromList['bug'] = 'Related Bug'; $lang->todo->fromList['task'] = 'Related Task'; diff --git a/module/todo/lang/fr.php b/module/todo/lang/fr.php index ee2a0e387c..edd0abb6a0 100644 --- a/module/todo/lang/fr.php +++ b/module/todo/lang/fr.php @@ -116,12 +116,14 @@ $lang->todo->priList[2] = 'Importante'; $lang->todo->priList[3] = 'Normale'; $lang->todo->priList[4] = 'Faible'; -$lang->todo->typeList['custom'] = 'Person.'; -$lang->todo->typeList['cycle'] = 'Récur'; -$lang->todo->typeList['bug'] = 'Bug'; -$lang->todo->typeList['task'] = 'Tâche'; -$lang->todo->typeList['story'] = 'Story'; -$lang->todo->typeList['testtask'] = 'Testtask'; +$lang->todo->typeList['custom'] = 'Person.'; +$lang->todo->typeList['cycle'] = 'Récur'; +$lang->todo->typeList['bug'] = 'Bug'; +$lang->todo->typeList['task'] = 'Tâche'; +$lang->todo->typeList['story'] = 'Story'; +if($config->enableER) $lang->todo->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->todo->typeList['requirement'] = $lang->URCommon; +$lang->todo->typeList['testtask'] = 'Testtask'; $lang->todo->fromList['bug'] = 'Related Bug'; $lang->todo->fromList['task'] = 'Related Task'; diff --git a/module/todo/lang/zh-cn.php b/module/todo/lang/zh-cn.php index 86477168ae..3850af5cd9 100644 --- a/module/todo/lang/zh-cn.php +++ b/module/todo/lang/zh-cn.php @@ -116,12 +116,15 @@ $lang->todo->priList[2] = 2; $lang->todo->priList[3] = 3; $lang->todo->priList[4] = 4; -$lang->todo->typeList['custom'] = '自定义'; -$lang->todo->typeList['cycle'] = '周期'; -$lang->todo->typeList['bug'] = 'Bug'; -$lang->todo->typeList['task'] = '任务'; -$lang->todo->typeList['story'] = $lang->SRCommon; -$lang->todo->typeList['testtask'] = '测试单'; +global $config; +$lang->todo->typeList['custom'] = '自定义'; +$lang->todo->typeList['cycle'] = '周期'; +$lang->todo->typeList['bug'] = 'Bug'; +$lang->todo->typeList['task'] = '任务'; +$lang->todo->typeList['story'] = $lang->SRCommon; +if($config->enableER) $lang->todo->typeList['epic'] = $lang->ERCommon; +if($config->URAndSR) $lang->todo->typeList['requirement'] = $lang->URCommon; +$lang->todo->typeList['testtask'] = '测试单'; $lang->todo->fromList['bug'] = '相关Bug'; $lang->todo->fromList['task'] = '相关任务'; diff --git a/module/todo/tao.php b/module/todo/tao.php index b09b0a07f4..b968d812b0 100644 --- a/module/todo/tao.php +++ b/module/todo/tao.php @@ -387,7 +387,7 @@ class todoTao extends todoModel { $savedTodoName = $todo->name; - if($todo->type == 'story') + if(in_array($todo->type, array('story', 'epic', 'requirement'))) { $story = $this->dao->findByID($todo->objectID)->from(TABLE_STORY)->fetch(); $todo->object = $story; diff --git a/module/todo/zen.php b/module/todo/zen.php index dc1b688b77..90db826d93 100755 --- a/module/todo/zen.php +++ b/module/todo/zen.php @@ -174,7 +174,7 @@ class todoZen extends todo if($todo->type != 'custom' && !empty($todo->objectID)) { $type = $todo->type; - $object = $this->loadModel($type)->getByID($todo->objectID); + $object = $this->loadModel($type)->fetchByID($todo->objectID); if(isset($object->name)) $todo->name = $object->name; if(isset($object->title)) $todo->name = $object->title; } @@ -311,7 +311,7 @@ class todoZen extends todo if(in_array($todo->type, $this->config->todo->moduleList)) { $type = $todo->type; - $object = $this->loadModel($type)->getByID((int)$objectID); + $object = $this->loadModel($type)->fetchByID((int)$objectID); if(isset($object->name)) $todo->name = $object->name; if(isset($object->title)) $todo->name = $object->title; } @@ -381,11 +381,13 @@ class todoZen extends todo list($editedTodos, $objectIdList) = $this->getBatchEditInitTodos($todoIdList, $type, $account, $status); $editedTodos = array_map(function($item) { $item->begin = str_replace(':', '', $item->begin); $item->end = str_replace(':', '', $item->end); return $item;}, $editedTodos); - $bugs = $this->loadModel('bug')->getUserBugPairs($account, true, 0, array(), array(), isset($objectIdList['bug']) ? $objectIdList['bug'] : array()); - $tasks = $this->loadModel('task')->getUserTaskPairs($account, 'wait,doing', array(), isset($objectIdList['task']) ? $objectIdList['task'] : array()); - $stories = $this->loadModel('story')->getUserStoryPairs($account, 10, 'story', '', isset($objectIdList['story']) ? $objectIdList['story'] : array()); - $users = $this->loadModel('user')->getPairs('noclosed|nodeleted|noempty'); - $testtasks = $this->loadModel('testtask')->getUserTestTaskPairs($account); + $bugs = $this->loadModel('bug')->getUserBugPairs($account, true, 0, array(), array(), isset($objectIdList['bug']) ? $objectIdList['bug'] : array()); + $tasks = $this->loadModel('task')->getUserTaskPairs($account, 'wait,doing', array(), isset($objectIdList['task']) ? $objectIdList['task'] : array()); + $stories = $this->loadModel('story')->getUserStoryPairs($account, 10, 'story', '', isset($objectIdList['story']) ? $objectIdList['story'] : array()); + $epics = $this->loadModel('story')->getUserStoryPairs($account, 10, 'epic', '', isset($objectIdList['epic']) ? $objectIdList['epic'] : array()); + $requirements = $this->loadModel('story')->getUserStoryPairs($account, 10, 'requirement', '', isset($objectIdList['requirement']) ? $objectIdList['requirement'] : array()); + $users = $this->loadModel('user')->getPairs('noclosed|nodeleted|noempty'); + $testtasks = $this->loadModel('testtask')->getUserTestTaskPairs($account); if($this->config->edition != 'open') $feedbacks = $this->loadModel('feedback')->getUserFeedbackPairs($account, '', isset($objectIdList['feedback']) ? $objectIdList['feedback'] : ''); if(in_array($this->config->edition, array('max', 'ipd'))) { @@ -401,16 +403,18 @@ class todoZen extends todo $showSuhosinInfo = common::judgeSuhosinSetting($countInputVars); if($showSuhosinInfo) $this->view->suhosinInfo = extension_loaded('suhosin') ? sprintf($this->lang->suhosinInfo, $countInputVars) : sprintf($this->lang->maxVarsInfo, $countInputVars); - $this->view->bugs = $bugs; - $this->view->tasks = $tasks; - $this->view->stories = $stories; - $this->view->reviews = $reviews; - $this->view->testtasks = $testtasks; - $this->view->editedTodos = $editedTodos; - $this->view->users = $users; - $this->view->type = $type; - $this->view->userID = $userID; - $this->view->status = $status; + $this->view->bugs = $bugs; + $this->view->tasks = $tasks; + $this->view->stories = $stories; + $this->view->epics = $epics; + $this->view->requirements = $requirements; + $this->view->reviews = $reviews; + $this->view->testtasks = $testtasks; + $this->view->editedTodos = $editedTodos; + $this->view->users = $users; + $this->view->type = $type; + $this->view->userID = $userID; + $this->view->status = $status; if($this->config->edition != 'open') $this->view->feedbacks = $feedbacks; if(in_array($this->config->edition, array('max', 'ipd'))) { @@ -503,7 +507,7 @@ class todoZen extends todo { dao::$errors["name[{$todoID}]"] = sprintf($this->lang->error->notempty, $this->lang->todo->name); } - unset($todo->story, $todo->task, $todo->bug, $todo->testtask, $todo->feedback, $todo->issue, $todo->risk, $todo->opportunity, $todo->review); + unset($todo->story, $todo->epic, $todo->requirement, $todo->task, $todo->bug, $todo->testtask, $todo->feedback, $todo->issue, $todo->risk, $todo->opportunity, $todo->review); $todo->begin = empty($todo->begin) || $this->post->switchTime ? 2400 : $todo->begin; $todo->end = empty($todo->end) || $this->post->switchTime ? 2400 : $todo->end; @@ -820,6 +824,8 @@ class todoZen extends todo $this->loadModel('user')->getPairs('noletter'), $this->loadModel('bug')->getUserBugPairs($account), $this->loadModel('story')->getUserStoryPairs($account, 100, 'story'), + $this->loadModel('story')->getUserStoryPairs($account, 100, 'epic'), + $this->loadModel('story')->getUserStoryPairs($account, 100, 'requirement'), $this->loadModel('task')->getUserTaskPairs($account), $this->loadModel('testtask')->getUserTesttaskPairs($account), ); @@ -853,10 +859,12 @@ class todoZen extends todo $type = $todo->type; if(isset($assemble->users[$todo->account])) $todo->account = $assemble->users[$todo->account]; - if($type == 'bug') $todo->name = isset($assemble->bugs[$todo->objectID]) ? $assemble->bugs[$todo->objectID] . "(#$todo->objectID)" : ''; - if($type == 'task') $todo->name = isset($assemble->tasks[$todo->objectID]) ? $assemble->tasks[$todo->objectID] . "(#$todo->objectID)" : ''; - if($type == 'story') $todo->name = isset($assemble->stories[$todo->objectID]) ? $assemble->stories[$todo->objectID] . "(#$todo->objectID)" : ''; - if($type == 'testtask') $todo->name = isset($assemble->testTasks[$todo->objectID]) ? $assemble->testTasks[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'bug') $todo->name = isset($assemble->bugs[$todo->objectID]) ? $assemble->bugs[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'task') $todo->name = isset($assemble->tasks[$todo->objectID]) ? $assemble->tasks[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'story') $todo->name = isset($assemble->stories[$todo->objectID]) ? $assemble->stories[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'epic') $todo->name = isset($assemble->epics[$todo->objectID]) ? $assemble->epics[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'requirement') $todo->name = isset($assemble->requirements[$todo->objectID]) ? $assemble->requirements[$todo->objectID] . "(#$todo->objectID)" : ''; + if($type == 'testtask') $todo->name = isset($assemble->testTasks[$todo->objectID]) ? $assemble->testTasks[$todo->objectID] . "(#$todo->objectID)" : ''; if(in_array($this->config->edition, array('max', 'ipd'))) { diff --git a/module/transfer/control.php b/module/transfer/control.php index 5381b2e003..fb0c45ec0f 100755 --- a/module/transfer/control.php +++ b/module/transfer/control.php @@ -124,6 +124,30 @@ class transfer extends control return $this->send(array('result' => 'success', 'load' => $locate, 'closeModal' => true)); } + /* 项目和执行中的需求导入时应选择需求类型. */ + if($module == 'story') + { + $this->app->loadLang('story'); + + $storyType = ''; + if($this->app->tab == 'project') + { + $project = $this->loadModel('project')->fetchByID($this->session->project); + $storyType = $project->storyType; + } + + if($storyType) + { + foreach($this->lang->story->typeList as $key => $value) + { + if(strpos(",$storyType,", ",$key,") === false) unset($this->lang->story->typeList[$key]); + } + } + + $this->view->isProjectStory = $this->app->tab == 'project'; + $this->view->typeList = $this->lang->story->typeList; + } + $this->view->title = $this->lang->transfer->importCase; $this->display(); } diff --git a/module/transfer/model.php b/module/transfer/model.php index 44d88a2ed9..b150c22f3e 100644 --- a/module/transfer/model.php +++ b/module/transfer/model.php @@ -69,6 +69,8 @@ class transferModel extends model { if($module) { + if(in_array($module, array('epic', 'requirement'))) $module = 'story'; + $this->loadModel($module); $this->moduleConfig = $this->config->$module; $this->moduleLang = $this->lang->$module; @@ -620,10 +622,6 @@ class transferModel extends model /* Deal children datas and multiple tasks. */ if($moduleDatas) $moduleDatas = $this->transferTao->updateChildDatas($moduleDatas); - /* 设置导出用户需求相关相关研发需求数据。*/ - /* Deal linkStories datas. */ - if($moduleDatas and isset($fieldList['linkStories'])) $moduleDatas = $this->transferTao->processLinkStories($moduleDatas); - return $moduleDatas; } diff --git a/module/transfer/tao.php b/module/transfer/tao.php index 3def902995..d923e7308c 100644 --- a/module/transfer/tao.php +++ b/module/transfer/tao.php @@ -243,7 +243,6 @@ class transferTao extends transferModel /* 根据name或title获取父数据。*/ /* Get parent data by name or title. */ if(!empty($data->name)) $data->name = '>' . $data->name; - elseif(!empty($data->title)) $data->title = '>' . $data->title; $children[$data->parent][$id] = $data; unset($datas[$id]); } @@ -264,27 +263,4 @@ class transferTao extends transferModel return $datas; } - - /** - * 设置导出用户需求相关相关研发需求数据。 - * Process LinkStories datas. - * - * @param array $stories - * @access protected - * @return array - */ - protected function processLinkStories($stories) - { - $storyDatas = end($stories); - $lastType = $storyDatas->type; - if(empty($storyDatas->type) || $storyDatas->type == 'story') return $stories; - - /* 获取产品ID列表。*/ - /* Get product id list. */ - $productIDList = array(); - foreach($stories as $story) $productIDList[] = $story->product; - $productIDList = array_unique($productIDList); - - return $this->loadModel('story')->mergePlanTitleAndChildren($productIDList, $stories, $lastType); - } } diff --git a/module/transfer/ui/exporttemplate.html.php b/module/transfer/ui/exporttemplate.html.php index 46101fb88a..6c511f1fa6 100644 --- a/module/transfer/ui/exporttemplate.html.php +++ b/module/transfer/ui/exporttemplate.html.php @@ -24,6 +24,15 @@ form set::type('number'), set::value(10) ), + !empty($isProjectStory) ? formGroup + ( + set::label($lang->story->type), + set::name('storyType'), + set::required(true), + set::control('picker'), + set::value('story'), + set::items($typeList) + ) : null, formGroup ( set::label($lang->file->extension), diff --git a/module/transfer/ui/import.html.php b/module/transfer/ui/import.html.php index bd3f1b5c56..cbf556678e 100644 --- a/module/transfer/ui/import.html.php +++ b/module/transfer/ui/import.html.php @@ -13,6 +13,14 @@ namespace zin; formPanel ( set::title($title), + !empty($isProjectStory) ? formGroup + ( + set::name('storyType'), + set::required(true), + set::control('picker'), + set::value('story'), + set::items($typeList) + ) : null, input(set::type('file'), set::name('file')), span(setClass('label secondary'), $lang->transfer->importNotice) ); diff --git a/module/upgrade/config.php b/module/upgrade/config.php index aeac8a2c66..2d35121f0a 100644 --- a/module/upgrade/config.php +++ b/module/upgrade/config.php @@ -1225,6 +1225,7 @@ $config->delete['20_0'][] = 'extension/ipd/attend/ext/control/stat.php'; $config->delete['20_0'][] = 'extension/biz/feedback/ext/ui/admin.excel.html.hook.php'; $config->delete['20_0'][] = 'extension/max/feedback/ext/ui/admin.excel.html.hook.php'; $config->delete['20_0'][] = 'extension/ipd/feedback/ext/ui/admin.excel.html.hook.php'; +$config->delete['20_0'][] = 'module/story/ui/linkstories.html.php'; $config->delete['20_0'][] = 'extension/or/product/ext/control/batchedit.php'; $config->delete['20_1'][] = 'module/gitfox'; @@ -1254,8 +1255,7 @@ $config->delete['20_1'][] = 'execution/ipd/bug/ext/ui/view.effort.html.hook.php' $config->delete['20_1'][] = 'execution/ipd/story/ext/ui/view.effort.html.hook.php'; $config->delete['20_1'][] = 'execution/ipd/testcase/ext/ui/view.effort.html.hook.php'; -$config->upgrade->openModules = array('action', 'admin', 'ai', 'bi', 'aiapp', 'api', 'automation', 'backup', 'block', 'branch', 'budget', 'bug', 'build', 'caselib', 'chart', 'ci', 'client', 'common', 'company', 'compile', 'convert', 'cron', 'custom', 'datatable', 'dataview', 'dept', 'design', 'dev', 'dimension', 'doc', 'durationestimation', 'entry', 'execution', 'extension', 'file', 'git', 'gitlab', 'group', 'holiday', 'im', 'index', 'index.html', 'install', 'issue', 'jenkins', 'job', 'kanban', 'license', 'mail', 'message', 'metric', 'misc', 'mr', 'my', 'personnel', 'pipeline', 'product', 'productplan', 'productset', 'program', 'programplan', 'project', 'projectbuild', 'projectplan', 'projectrelease', 'projectstory', 'pivot', 'qa', 'release', 'repo', 'report', 'risk', 'score', 'screen', 'search', 'setting', 'sonarqube', 'sso', 'stage', 'stakeholder', 'story', 'subject', 'svn', 'task', 'testcase', 'testreport', 'testsuite', 'testtask', 'todo', 'tree', 'tutorial', 'upgrade', 'user', 'webhook', 'weekly', 'workestimation', 'gitea', 'gogs', 'transfer', 'zahost', 'zanode', 'editor', 'charter', 'roadmap', 'account', 'cne', 'host', 'instance', 'ops', 'serverroom', 'space', 'store', 'system', 'solution', 'demand'); - +$config->upgrade->openModules = array('action', 'admin', 'ai', 'bi', 'aiapp', 'api', 'automation', 'backup', 'block', 'branch', 'budget', 'bug', 'build', 'caselib', 'chart', 'ci', 'client', 'common', 'company', 'compile', 'convert', 'cron', 'custom', 'datatable', 'dataview', 'dept', 'design', 'dev', 'dimension', 'doc', 'durationestimation', 'entry', 'execution', 'extension', 'file', 'git', 'gitlab', 'group', 'holiday', 'im', 'index', 'index.html', 'install', 'issue', 'jenkins', 'job', 'kanban', 'license', 'mail', 'message', 'metric', 'misc', 'mr', 'my', 'personnel', 'pipeline', 'product', 'productplan', 'productset', 'program', 'programplan', 'project', 'projectbuild', 'projectplan', 'projectrelease', 'projectstory', 'pivot', 'qa', 'release', 'repo', 'report', 'risk', 'score', 'screen', 'search', 'setting', 'sonarqube', 'sso', 'stage', 'stakeholder', 'story', 'subject', 'svn', 'task', 'testcase', 'testreport', 'testsuite', 'testtask', 'todo', 'tree', 'tutorial', 'upgrade', 'user', 'webhook', 'weekly', 'workestimation', 'gitea', 'gogs', 'transfer', 'zahost', 'zanode', 'editor', 'charter', 'roadmap', 'account', 'cne', 'host', 'instance', 'ops', 'serverroom', 'space', 'store', 'system', 'solution', 'demand', 'gitfox', 'epic', 'requirement'); $config->upgrade->unsetModules = array('design', 'program', 'programplan', 'projectbuild', 'projectrelease', 'stage', 'stakeholder', 'product', 'branch', 'productplan', 'release', 'build', 'qa', 'bug', 'testcase', 'testtask', 'testreport', 'testsuite', 'caselib', 'automation', 'repo', 'ci', 'compile', 'jenkins', 'job', 'svn', 'gitlab', 'sonarqube', 'mr', 'git', 'report', 'sqlbuilder', 'feedback', 'faq', 'attend', 'holiday', 'leave', 'makeup', 'overtime', 'lieu', 'ops', 'host', 'serverroom', 'account', 'domain', 'service', 'deploy', 'conference', 'traincourse', 'pssp', 'baseline', 'classify', 'cm', 'cmcl', 'auditcl', 'reviewcl', 'process', 'activity', 'zoutput', 'auditplan', 'nc', 'subject', 'weekly', 'workestimation', 'issue', 'durationestimation', 'risk', 'opportunity', 'trainplan', 'gapanalysis', 'researchplan', 'researchreport', 'meeting', 'meetingroom', 'budget', 'reviewissue', 'reviewsetting', 'review', 'milestone', 'measurement', 'measrecord', 'assetlib', 'setting', 'im', 'client', 'ldap', 'dev', 'api', 'gitea', 'gogs', 'zanode', 'zahost'); global $lang; diff --git a/module/upgrade/config/upgradeflow.php b/module/upgrade/config/upgradeflow.php index 563691e90e..b423bf412d 100644 --- a/module/upgrade/config/upgradeflow.php +++ b/module/upgrade/config/upgradeflow.php @@ -98,7 +98,7 @@ $config->upgrade->execFlow['20_0_beta1'] = array('functions' => 'hideOA,updateM $config->upgrade->execFlow['20_0_beta2'] = array('functions' => 'updateWorkflowFieldDefaultValue,update1811,updateZeroDateToNull,updateProgramplanCustom'); $config->upgrade->execFlow['20_0'] = array('functions' => 'openCacheByAPCu'); $config->upgrade->execFlow['20_1_0'] = array('functions' => 'completeClassifyLang'); -$config->upgrade->execFlow['20_1_1'] = array('functions' => 'syncActivityAndOutput'); +$config->upgrade->execFlow['20_1_1'] = array('functions' => 'syncActivityAndOutput,changeCustomStoryStage,processStoryRelation,processLinkStories,addERName'); if(!empty($config->isINT)) { @@ -158,6 +158,7 @@ $config->upgrade->execFlow['biz5_2'] = array('functions' => 'addDefaultKan $config->upgrade->execFlow['biz5_3_1'] = array('functions' => 'processFeedbackField,addFileFields,addReportActions'); $config->upgrade->execFlow['biz6_4'] = array('functions' => 'importLiteModules'); $config->upgrade->execFlow['biz8_12'] = array('functions' => 'AddFeedbackDatasource'); +$config->upgrade->execFlow['biz10_0_0'] = array('functions' => 'importERURModules'); if(!empty($config->isINT)) { diff --git a/module/upgrade/lang/de.php b/module/upgrade/lang/de.php index 6185ab3c0f..b2cd3599a6 100644 --- a/module/upgrade/lang/de.php +++ b/module/upgrade/lang/de.php @@ -164,3 +164,22 @@ $lang->upgrade->remarkDesc = 'You can also switch the mode in the Admin-System-M $lang->upgrade->upgradingTip = 'The system is being upgraded, please wait patiently...'; $lang->upgrade->addTraincoursePrivTips = "In order to facilitate everyone's learning of project management-related knowledge, we have made the courses and practical repositories of the academy accessible to all permission groups by default. This ensures that everyone can easily access the resources. However, if you do not require this feature, you can disable it in the backend feature switch."; + +$lang->upgrade->storyStageList[''] = ''; +$lang->upgrade->storyStageList['defining'] = 'Defining'; +$lang->upgrade->storyStageList['wait'] = 'Waiting'; +$lang->upgrade->storyStageList['planning'] = 'Planning'; +$lang->upgrade->storyStageList['planned'] = 'Planned'; +$lang->upgrade->storyStageList['projected'] = 'Projected'; +$lang->upgrade->storyStageList['designing'] = 'Designing'; +$lang->upgrade->storyStageList['designed'] = 'Designed'; +$lang->upgrade->storyStageList['developing'] = 'Developing'; +$lang->upgrade->storyStageList['developed'] = 'Developed'; +$lang->upgrade->storyStageList['testing'] = 'Testing'; +$lang->upgrade->storyStageList['tested'] = 'Tested'; +$lang->upgrade->storyStageList['verified'] = 'Accepted'; +$lang->upgrade->storyStageList['rejected'] = 'Verify Rejected'; +$lang->upgrade->storyStageList['delivering'] = 'Delivering'; +$lang->upgrade->storyStageList['pending'] = 'Release Pending'; +$lang->upgrade->storyStageList['released'] = 'Released'; +$lang->upgrade->storyStageList['closed'] = 'Closed'; diff --git a/module/upgrade/lang/en.php b/module/upgrade/lang/en.php index 7dd46bbc11..cb9a7b4e50 100644 --- a/module/upgrade/lang/en.php +++ b/module/upgrade/lang/en.php @@ -164,3 +164,22 @@ $lang->upgrade->remarkDesc = 'You can also switch the mode in the Admin-System-M $lang->upgrade->upgradingTip = 'The system is being upgraded, please wait patiently...'; $lang->upgrade->addTraincoursePrivTips = "In order to facilitate everyone's learning of project management-related knowledge, we have made the courses and practical repositories of the academy accessible to all permission groups by default. This ensures that everyone can easily access the resources. However, if you do not require this feature, you can disable it in the backend feature switch."; + +$lang->upgrade->storyStageList[''] = ''; +$lang->upgrade->storyStageList['defining'] = 'Defining'; +$lang->upgrade->storyStageList['wait'] = 'Waiting'; +$lang->upgrade->storyStageList['planning'] = 'Planning'; +$lang->upgrade->storyStageList['planned'] = 'Planned'; +$lang->upgrade->storyStageList['projected'] = 'Projected'; +$lang->upgrade->storyStageList['designing'] = 'Designing'; +$lang->upgrade->storyStageList['designed'] = 'Designed'; +$lang->upgrade->storyStageList['developing'] = 'Developing'; +$lang->upgrade->storyStageList['developed'] = 'Developed'; +$lang->upgrade->storyStageList['testing'] = 'Testing'; +$lang->upgrade->storyStageList['tested'] = 'Tested'; +$lang->upgrade->storyStageList['verified'] = 'Accepted'; +$lang->upgrade->storyStageList['rejected'] = 'Verify Rejected'; +$lang->upgrade->storyStageList['delivering'] = 'Delivering'; +$lang->upgrade->storyStageList['pending'] = 'Release Pending'; +$lang->upgrade->storyStageList['released'] = 'Released'; +$lang->upgrade->storyStageList['closed'] = 'Closed'; diff --git a/module/upgrade/lang/fr.php b/module/upgrade/lang/fr.php index 487d91e730..219b26d731 100644 --- a/module/upgrade/lang/fr.php +++ b/module/upgrade/lang/fr.php @@ -164,3 +164,22 @@ $lang->upgrade->remarkDesc = 'You can also switch the mode in the Admin-System-M $lang->upgrade->upgradingTip = 'The system is being upgraded, please wait patiently...'; $lang->upgrade->addTraincoursePrivTips = "In order to facilitate everyone's learning of project management-related knowledge, we have made the courses and practical repositories of the academy accessible to all permission groups by default. This ensures that everyone can easily access the resources. However, if you do not require this feature, you can disable it in the backend feature switch."; + +$lang->upgrade->storyStageList[''] = ''; +$lang->upgrade->storyStageList['defining'] = 'Defining'; +$lang->upgrade->storyStageList['wait'] = 'Waiting'; +$lang->upgrade->storyStageList['planning'] = 'Planning'; +$lang->upgrade->storyStageList['planned'] = 'Planned'; +$lang->upgrade->storyStageList['projected'] = 'Projected'; +$lang->upgrade->storyStageList['designing'] = 'Designing'; +$lang->upgrade->storyStageList['designed'] = 'Designed'; +$lang->upgrade->storyStageList['developing'] = 'Developing'; +$lang->upgrade->storyStageList['developed'] = 'Developed'; +$lang->upgrade->storyStageList['testing'] = 'Testing'; +$lang->upgrade->storyStageList['tested'] = 'Tested'; +$lang->upgrade->storyStageList['verified'] = 'Accepted'; +$lang->upgrade->storyStageList['rejected'] = 'Verify Rejected'; +$lang->upgrade->storyStageList['delivering'] = 'Delivering'; +$lang->upgrade->storyStageList['pending'] = 'Release Pending'; +$lang->upgrade->storyStageList['released'] = 'Released'; +$lang->upgrade->storyStageList['closed'] = 'Closed'; diff --git a/module/upgrade/lang/zh-cn.php b/module/upgrade/lang/zh-cn.php index 29dd932f3b..f5c2928959 100644 --- a/module/upgrade/lang/zh-cn.php +++ b/module/upgrade/lang/zh-cn.php @@ -164,3 +164,22 @@ $lang->upgrade->remarkDesc = '后续您还可以在禅道的后台-系统设置- $lang->upgrade->upgradingTip = '系统正在升级中,请耐心等待...'; $lang->upgrade->addTraincoursePrivTips = '为了帮助大家更好的学习项目管理相关知识,默认给所有权限分组分配了学堂的课程和实践库权限,便于大家都能访问。如果您不需要该功能,可以到后台功能开关中关闭该功能。'; + +$lang->upgrade->storyStageList[''] = ''; +$lang->upgrade->storyStageList['defining'] = '定义中'; +$lang->upgrade->storyStageList['wait'] = '未开始'; +$lang->upgrade->storyStageList['planning'] = '规划中'; +$lang->upgrade->storyStageList['planned'] = "已计划"; +$lang->upgrade->storyStageList['projected'] = '已立项'; +$lang->upgrade->storyStageList['designing'] = '设计中'; +$lang->upgrade->storyStageList['designed'] = '设计完毕'; +$lang->upgrade->storyStageList['developing'] = '研发中'; +$lang->upgrade->storyStageList['developed'] = '研发完毕'; +$lang->upgrade->storyStageList['testing'] = '测试中'; +$lang->upgrade->storyStageList['tested'] = '测试完毕'; +$lang->upgrade->storyStageList['verified'] = '已验收'; +$lang->upgrade->storyStageList['rejected'] = '验收失败'; +$lang->upgrade->storyStageList['delivering'] = '交付中'; +$lang->upgrade->storyStageList['pending'] = '待发布'; +$lang->upgrade->storyStageList['released'] = '已发布'; +$lang->upgrade->storyStageList['closed'] = '已关闭'; diff --git a/module/upgrade/model.php b/module/upgrade/model.php index 297559c7b9..0ad39902cd 100644 --- a/module/upgrade/model.php +++ b/module/upgrade/model.php @@ -8784,12 +8784,220 @@ class upgradeModel extends model return true; } + /** + * 为自定义过的需求阶段增加条目。 + * Add items for custom story stage. + * + * @access public + * @return bool + */ + public function changeCustomStoryStage(): bool + { + $clientLang = $this->app->getClientLang(); + $customStagePairs = $this->dao->select('`key`, value')->from(TABLE_LANG) + ->where('module')->eq('story') + ->andWhere('section')->eq('stageList') + ->andWhere('lang')->eq($clientLang) + ->fetchPairs(); + + if(empty($customStagePairs)) + { + $clientLang = 'all'; + $customStagePairs = $this->dao->select('`key`, value')->from(TABLE_LANG) + ->where('module')->eq('story') + ->andWhere('section')->eq('stageList') + ->andWhere('lang')->eq($clientLang) + ->fetchPairs(); + } + + if(empty($customStagePairs)) return false; + + $this->dao->delete()->from(TABLE_LANG) + ->where('module')->eq('story') + ->andWhere('section')->eq('stageList') + ->andWhere('lang')->eq($clientLang) + ->exec(); + + foreach($this->lang->upgrade->storyStageList as $key => $value) + { + $item = new stdclass(); + $item->lang = $clientLang; + $item->module = 'story'; + $item->section = 'stageList'; + $item->key = $key; + $item->value = isset($customStagePairs[$key]) ? $customStagePairs[$key] : $value; + $item->system = '1'; + $item->vision = 'rnd'; + + $this->dao->insert(TABLE_LANG)->data($item)->exec(); + } + + return true; + } + + /** + * 禅道20版本之前,用需和软需是关联关系,多对多,现改为父子关系,一对多。 + * Before ZenTao 20, the relationship between use requirements and story is a many-to-many relationship, now changed to a parent-child relationship, a one-to-many relationship. + * + * @access public + * @return void + */ + public function processStoryRelation() + { + $this->loadModel('story'); + $relations = $this->dao->select('t1.*, t2.parent as BParent, t2.isParent')->from(TABLE_RELATION)->alias('t1') + ->leftJoin(TABLE_STORY)->alias('t2')->on('t1.BID = t2.id') + ->where('AType')->eq('requirement') + ->andWhere('BType')->eq('story') + ->andWhere('relation')->eq('subdivideinto') + ->orderBy('AID asc') + ->fetchGroup('AID', 'BID'); + + $processd = array(); // 记录已处理的软件需求,一个软件需求只能有一个父需求。 + foreach($relations as $AID => $storyList) + { + $hasChild = false; + $lastChildID = 0; + + foreach($storyList as $BID => $story) + { + if($story->BParent > 0) continue; // 如果已经是子需求,无需处理。 + + if(!isset($processd[$BID])) // 没有被其它用需变为子需求,才处理。 + { + $processd[$BID] = $BID; + + $hasChild = true; + $lastChildID = $BID; + + /* 当软件需求变为子需求后,需要更改这些字段。 */ + $child = new stdclass(); + $child->parent = $AID; + $child->parentVersion = $story->AVersion; + $child->path = ',' . $AID . ',' . $BID . ','; + $child->root = $AID; + + $this->dao->update(TABLE_STORY)->data($child)->where('id')->eq($BID)->exec(); + + /* 如果该软件需求本身就是父需求,则将root和path同步给它的子需求。 */ + if($story->isParent == '1') + { + $this->dao->update(TABLE_STORY) + ->set('root')->eq($AID) + ->set("path = concat(',', {$AID}, path)") + ->where('parent')->eq($BID) + ->exec(); + } + + /* 将用需软需的关联关系去掉,它们现在是父子关系了。 */ + $this->dao->delete()->from(TABLE_RELATION)->where('AID')->eq($BID)->andWhere('BID')->eq($AID)->exec(); + $this->dao->delete()->from(TABLE_RELATION)->where('AID')->eq($AID)->andWhere('BID')->eq($BID)->exec(); + } + } + + if($hasChild) + { + /* 如果用需成为了父需求,则需要通过它最后一个子需求计算它的阶段、工时、状态等信息。 */ + $this->dao->update(TABLE_STORY)->set('isParent')->eq('1')->where('id')->eq($AID)->exec(); + $this->story->setStage($lastChildID); + $this->story->updateParentStatus($lastChildID, $AID, false); + } + } + + /* 其它没被处理成父子关系的细分关系,变成关联关系。 */ + $this->dao->update(TABLE_RELATION)->set('relation')->eq('linkedto')->where('relation')->eq('subdivideinto')->exec(); + $this->dao->update(TABLE_RELATION)->set('relation')->eq('linkedfrom')->where('relation')->eq('subdividedfrom')->exec(); + } + + /** + * 禅道20版本前,软需关联软需,用需关联用需,是以字符串形式记录在story表的linkStories字段中,现迁移到relation表中。 + * Before ZenTao 20, the relationship between stories and stories, and the relationship between requirements and requirements, are recorded in the linkStories field of the story table in string form, and now migrated to the relation table. + * + * @access public + * @return void + */ + public function processLinkStories() + { + $doProcess = function($stories, $storyType) + { + foreach($stories as $storyID => $linkStories) + { + $linkStories = explode(',', trim($linkStories, ',')); + foreach($linkStories as $linkStoryID) + { + if(!$linkStoryID) continue; + + $relation = new stdclass(); + $relation->AType = $storyType; + $relation->AID = $storyID; + $relation->BType = $storyType; + $relation->BID = $linkStoryID; + $relation->relation = 'linkedto'; + + $this->dao->replace(TABLE_RELATION)->data($relation)->exec(); + + $relation->AType = $storyType; + $relation->AID = $linkStoryID; + $relation->BType = $storyType; + $relation->BID = $storyID; + $relation->relation = 'linkedfrom'; + + $this->dao->replace(TABLE_RELATION)->data($relation)->exec(); + } + } + }; + + $stories = $this->dao->select('id, linkStories')->from(TABLE_STORY) + ->where('linkStories')->ne('') + ->andWhere('deleted')->eq('0') + ->fetchPairs(); + + $requirements = $this->dao->select('id, linkRequirements')->from(TABLE_STORY) + ->where('linkRequirements')->ne('') + ->andWhere('deleted')->eq('0') + ->fetchPairs(); + + $doProcess($stories, 'story'); + $doProcess($requirements, 'requirement'); + } + + /** + * 如果用户自定义过需求概念,则追加默认的业需概念。 + * If the user has customized the concept of demand, append the default concept of business demand. + * + * @access public + * @return void + */ + public function addERName() + { + $lang = $this->app->getClientLang(); + + $URSRList = $this->dao->select('*')->from(TABLE_LANG) + ->where('lang')->eq($lang) + ->andWhere('module')->eq('custom') + ->andWhere('section')->eq('URSRList') + ->fetchAll(); + + foreach($URSRList as $URSR) + { + $value = json_decode($URSR->value); + if(isset($value->ERName)) continue; + + $value->ERName = $this->lang->defaultERName; + + $this->dao->update(TABLE_LANG) + ->set('value')->eq(json_encode($value)) + ->where('id')->eq($URSR->id) + ->exec(); + } + } + /** * 更新升级BI内置数据。 * Import BI data. * * @access public - * @return bool + * @return void */ public function upgradeBIData() { diff --git a/module/user/control.php b/module/user/control.php index 9b91987919..6c73d947c4 100755 --- a/module/user/control.php +++ b/module/user/control.php @@ -746,7 +746,7 @@ class user extends control $method = strtolower($method); $this->app->loadLang('my'); - $this->app->loadLang($module == 'requirement' ? 'story' : $module); + $this->app->loadLang($module); /* 判断禁止访问的类型。*/ /* Judge the type of deny. */ diff --git a/test/data/childstory.yaml b/test/data/childstory.yaml index d29bd61ef0..8df9e77d3d 100644 --- a/test/data/childstory.yaml +++ b/test/data/childstory.yaml @@ -239,13 +239,6 @@ fields: postfix: "" loop: 0 format: "" - - field: childStories - note: "细分需求" - range: 0 - prefix: "" - postfix: "" - loop: 0 - format: "" - field: linkStories note: "相关需求" range: 0 diff --git a/test/data/story.yaml b/test/data/story.yaml index bf66d4e8a3..79c66f8901 100644 --- a/test/data/story.yaml +++ b/test/data/story.yaml @@ -38,6 +38,13 @@ fields: postfix: "" loop: 0 format: "" + - field: grade + note: "需求层级" + range: 1-2 + prefix: "" + postfix: "" + loop: 0 + format: "" - field: source note: "需求来源" range: customer,user,po,market,service,operation,support,competitor,partner,dev,tester,bug,forum,other @@ -226,13 +233,6 @@ fields: postfix: "" loop: 0 format: "" - - field: childStories - note: "细分需求" - range: 0 - prefix: "" - postfix: "" - loop: 0 - format: "" - field: linkStories note: "相关需求" range: 0 diff --git a/test/data/storygrade.yaml b/test/data/storygrade.yaml new file mode 100644 index 0000000000..82baf926e0 --- /dev/null +++ b/test/data/storygrade.yaml @@ -0,0 +1,14 @@ +title: table zt_storygrade +desc: "需求阶段" +author: automated export +version: "1.0" +fields: + - field: type + note: "类型" + range: "epic{2},requirement{2},story{2}" + - field: grade + range: "1,2" + - field: name + range: "BR1,BR2,UR1,UR2,SR1,SR2" + - field: status + range: "enable" diff --git a/www/js/zui3/zin.js b/www/js/zui3/zin.js index 0b848efd74..d201f11ef2 100644 --- a/www/js/zui3/zin.js +++ b/www/js/zui3/zin.js @@ -497,7 +497,7 @@ if(options.modal) headers['X-Zui-Modal'] = 'true'; const requestMethod = (options.method || 'GET').toUpperCase(); if(!options.cache && options.cache !== false) options.cache = requestMethod === 'GET' ? (url + (url.includes('?') ? '&zin=' : '?zin=') + encodeURIComponent(selectors.join(','))) : false; - if(!window.config || !window.config.clientCache) options.cache = false; + options.cache = false; // Disable local cache for 20.1. const cacheKey = options.cache; let cache; const renderPageData = (data, onlyZinDebug) => diff --git a/www/js/zui3/zui.zentao.css b/www/js/zui3/zui.zentao.css index 9f83a9ad6b..c58940d792 100644 --- a/www/js/zui3/zui.zentao.css +++ b/www/js/zui3/zui.zentao.css @@ -1,2 +1,2 @@ /*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border-color:rgb(var(--color-gray-200-rgb));border-style:solid;border-width:0;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:-apple-system,Noto Sans,Helvetica Neue,Helvetica,Nimbus Sans L,Arial,Liberation Sans,PingFang SC,Hiragino Sans GB,Noto Sans CJK SC,Source Han Sans SC,Source Han Sans CN,Microsoft YaHei,Wenquanyi Micro Hei,WenQuanYi Zen Hei,ST Heiti,SimHei,WenQuanYi Zen Hei Sharp,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:rgb(var(--color-gray-400-rgb));opacity:1}input::placeholder,textarea::placeholder{color:rgb(var(--color-gray-400-rgb));opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}:root{--screen-2xl:1720px;--color-gray-50:#fcfdfe;--color-gray-100:#eff1f3;--color-gray-200:#ebedf3;--color-gray-300:#d2d6e5;--color-gray-400:#b2b9c5;--color-gray-500:#64758b;--color-gray-600:#4a577f;--color-gray-700:#3d4667;--color-gray-800:#353d57;--color-gray-900:#30394a;--color-gray-950:#0b0f18;--color-gray-50-rgb:252,253,254;--color-gray-100-rgb:239,241,243;--color-gray-200-rgb:235,237,243;--color-gray-300-rgb:210,214,229;--color-gray-400-rgb:178,185,197;--color-gray-500-rgb:100,117,139;--color-gray-600-rgb:74,87,127;--color-gray-700-rgb:61,70,103;--color-gray-800-rgb:53,61,87;--color-gray-900-rgb:48,57,74;--color-gray-950-rgb:11,15,24;--color-primary-50:#eef6ff;--color-primary-100:#d9eaff;--color-primary-200:#bcdcff;--color-primary-300:#8ec6ff;--color-primary-400:#59a5ff;--color-primary-500:#3883fa;--color-primary-600:#2b67e5;--color-primary-700:#144ce1;--color-primary-800:#173db6;--color-primary-900:#19388f;--color-primary-950:#142457;--color-primary-50-rgb:238,246,255;--color-primary-100-rgb:217,234,255;--color-primary-200-rgb:188,220,255;--color-primary-300-rgb:142,198,255;--color-primary-400-rgb:89,165,255;--color-primary-500-rgb:56,131,250;--color-primary-600-rgb:43,103,229;--color-primary-700-rgb:20,76,225;--color-primary-800-rgb:23,61,182;--color-primary-900-rgb:25,56,143;--color-primary-950-rgb:20,36,87;--color-secondary-50:#e7f6ff;--color-secondary-100:#cdecff;--color-secondary-200:#9bd9ff;--color-secondary-300:#77cbff;--color-secondary-400:#60c2ff;--color-secondary-500:#37b2fe;--color-secondary-600:#18a6fd;--color-secondary-700:#1099ed;--color-secondary-800:#078ada;--color-secondary-900:#046bab;--color-secondary-950:#062a4b;--color-secondary-50-rgb:231,246,255;--color-secondary-100-rgb:205,236,255;--color-secondary-200-rgb:155,217,255;--color-secondary-300-rgb:119,203,255;--color-secondary-400-rgb:96,194,255;--color-secondary-500-rgb:55,178,254;--color-secondary-600-rgb:24,166,253;--color-secondary-700-rgb:16,153,237;--color-secondary-800-rgb:7,138,218;--color-secondary-900-rgb:4,107,171;--color-secondary-950-rgb:6,42,75;--color-success-50:#effaf6;--color-success-100:#d6f2e7;--color-success-200:#ade5ce;--color-success-300:#83d7b6;--color-success-400:#5aca9d;--color-success-500:#31bd85;--color-success-600:#2ba675;--color-success-700:#28996c;--color-success-800:#21805a;--color-success-900:#185e3b;--color-success-950:#0e3421;--color-success-50-rgb:239,250,246;--color-success-100-rgb:214,242,231;--color-success-200-rgb:173,229,206;--color-success-300-rgb:131,215,182;--color-success-400-rgb:90,202,157;--color-success-500-rgb:49,189,133;--color-success-600-rgb:43,166,117;--color-success-700-rgb:40,153,108;--color-success-800-rgb:33,128,90;--color-success-900-rgb:24,94,59;--color-success-950-rgb:14,52,33;--color-warning-50:#fff4ea;--color-warning-100:#ffecdb;--color-warning-200:#ffd2a9;--color-warning-300:#ffbc7e;--color-warning-400:#ffaf65;--color-warning-500:#ff9f46;--color-warning-600:#f38f19;--color-warning-700:#ed8307;--color-warning-800:#d17100;--color-warning-900:#af5f01;--color-warning-50-rgb:255,244,234;--color-warning-100-rgb:255,236,219;--color-warning-200-rgb:255,210,169;--color-warning-300-rgb:255,188,126;--color-warning-400-rgb:255,175,101;--color-warning-500-rgb:255,159,70;--color-warning-600-rgb:243,143,25;--color-warning-700-rgb:237,131,7;--color-warning-800-rgb:209,113,0;--color-warning-900-rgb:175,95,1;--color-danger-50:#ffebeb;--color-danger-100:#ffd5d5;--color-danger-200:#ffb1b1;--color-danger-300:#ff9292;--color-danger-400:#ff7c7c;--color-danger-500:#fc5959;--color-danger-600:#fb2b2b;--color-danger-700:#d91b1b;--color-danger-800:#ba1313;--color-danger-900:#a20606;--color-danger-50-rgb:255,235,235;--color-danger-100-rgb:255,213,213;--color-danger-200-rgb:255,177,177;--color-danger-300-rgb:255,146,146;--color-danger-400-rgb:255,124,124;--color-danger-500-rgb:252,89,89;--color-danger-600-rgb:251,43,43;--color-danger-700-rgb:217,27,27;--color-danger-800-rgb:186,19,19;--color-danger-900-rgb:162,6,6;--color-important-50:#fff8f5;--color-important-100:#ffe2d9;--color-important-200:#ffcebf;--color-important-300:#ffb29b;--color-important-400:#ff8c68;--color-important-500:#ff8058;--color-important-600:#ff6f42;--color-important-700:#ff4912;--color-important-800:#c83509;--color-important-900:#611700;--color-important-50-rgb:255,248,245;--color-important-100-rgb:255,226,217;--color-important-200-rgb:255,206,191;--color-important-300-rgb:255,178,155;--color-important-400-rgb:255,140,104;--color-important-500-rgb:255,128,88;--color-important-600-rgb:255,111,66;--color-important-700-rgb:255,73,18;--color-important-800-rgb:200,53,9;--color-important-900-rgb:97,23,0;--color-special-50:#efecfa;--color-special-100:#e4dff7;--color-special-200:#c4b6ff;--color-special-300:#a38cff;--color-special-400:#8a6ff5;--color-special-500:#8166ee;--color-special-600:#7055df;--color-special-700:#513baa;--color-special-800:#452da5;--color-special-900:#30216e;--color-special-50-rgb:239,236,250;--color-special-100-rgb:228,223,247;--color-special-200-rgb:196,182,255;--color-special-300-rgb:163,140,255;--color-special-400-rgb:138,111,245;--color-special-500-rgb:129,102,238;--color-special-600-rgb:112,85,223;--color-special-700-rgb:81,59,170;--color-special-800-rgb:69,45,165;--color-special-900-rgb:48,33,110;--color-inherit:inherit;--color-transparent:transparent;--color-current:currentColor;--color-black:#000;--color-white:#fff;--color-canvas:#fff;--color-canvas-light:#fff;--color-canvas-dark:#fcfdfe;--color-inverse:#000;--color-surface:#eff1f3;--color-surface-light:#fcfdfe;--color-surface-strong:#ebedf3;--color-fore:#30394a;--color-fore-in-light:#30394a;--color-fore-in-dark:#fff;--color-focus:#bcdcff;--color-link:#2b47a2;--color-link-hover:#144ce1;--color-link-visited:#6328a4;--color-link-active:#144ce1;--color-placeholder:#64758b;--color-border:#ebedf3;--color-border-strong:#d2d6e5;--color-border-light:#eff1f3;--color-black-rgb:0,0,0;--color-white-rgb:255,255,255;--color-canvas-rgb:255,255,255;--color-canvas-light-rgb:255,255,255;--color-canvas-dark-rgb:252,253,254;--color-inverse-rgb:0,0,0;--color-surface-rgb:239,241,243;--color-surface-light-rgb:252,253,254;--color-surface-strong-rgb:235,237,243;--color-fore-rgb:48,57,74;--color-fore-in-light-rgb:48,57,74;--color-fore-in-dark-rgb:255,255,255;--color-focus-rgb:188,220,255;--color-link-rgb:43,71,162;--color-link-hover-rgb:20,76,225;--color-link-visited-rgb:99,40,164;--color-link-active-rgb:20,76,225;--color-placeholder-rgb:100,117,139;--color-border-rgb:235,237,243;--color-border-strong-rgb:210,214,229;--color-border-light-rgb:239,241,243;--radius-none:0px;--radius-sm:0.0625rem;--radius:0.125rem;--radius-md:0.25rem;--radius-lg:0.375rem;--radius-xl:0.5rem;--radius-2xl:0.75rem;--radius-3xl:1rem;--radius-full:9999px;--shadow-sm:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--shadow-md:0 2px 6px -1px rgba(0,0,0,.2),0 1px 4px -2px rgba(0,0,0,.15);--shadow-lg:0 5px 15px -3px rgba(0,0,0,.2),0 2px 6px -4px rgba(0,0,0,.15);--shadow-xl:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--shadow-2xl:0 10px 50px -10px rgba(0,0,0,.4),0 8px 20px -10px rgba(0,0,0,.3);--shadow-inner:inset 0 2px 4px 0 rgba(0,0,0,.05);--shadow-none:none;--space:0.25rem;--font-size-article:0.875rem;--font-size-root:16px;--font-size-page-lineHeight:1.5384615385}.dark{--color-gray-50:#0b0f18;--color-gray-100:#30394a;--color-gray-200:#353d57;--color-gray-300:#3d4667;--color-gray-400:#4a577f;--color-gray-500:#64758b;--color-gray-600:#b2b9c5;--color-gray-700:#d2d6e5;--color-gray-800:#ebedf3;--color-gray-900:#eff1f3;--color-gray-950:#fcfdfe;--color-gray-50-rgb:11,15,24;--color-gray-100-rgb:48,57,74;--color-gray-200-rgb:53,61,87;--color-gray-300-rgb:61,70,103;--color-gray-400-rgb:74,87,127;--color-gray-500-rgb:100,117,139;--color-gray-600-rgb:178,185,197;--color-gray-700-rgb:210,214,229;--color-gray-800-rgb:235,237,243;--color-gray-900-rgb:239,241,243;--color-gray-950-rgb:252,253,254;--color-primary-50:#142457;--color-primary-100:#19388f;--color-primary-200:#173db6;--color-primary-300:#144ce1;--color-primary-400:#2b67e5;--color-primary-500:#3883fa;--color-primary-600:#59a5ff;--color-primary-700:#8ec6ff;--color-primary-800:#bcdcff;--color-primary-900:#d9eaff;--color-primary-950:#eef6ff;--color-primary-50-rgb:20,36,87;--color-primary-100-rgb:25,56,143;--color-primary-200-rgb:23,61,182;--color-primary-300-rgb:20,76,225;--color-primary-400-rgb:43,103,229;--color-primary-500-rgb:56,131,250;--color-primary-600-rgb:89,165,255;--color-primary-700-rgb:142,198,255;--color-primary-800-rgb:188,220,255;--color-primary-900-rgb:217,234,255;--color-primary-950-rgb:238,246,255;--color-secondary-50:#062a4b;--color-secondary-100:#046bab;--color-secondary-200:#078ada;--color-secondary-300:#1099ed;--color-secondary-400:#18a6fd;--color-secondary-500:#37b2fe;--color-secondary-600:#60c2ff;--color-secondary-700:#77cbff;--color-secondary-800:#9bd9ff;--color-secondary-900:#cdecff;--color-secondary-950:#e7f6ff;--color-secondary-50-rgb:6,42,75;--color-secondary-100-rgb:4,107,171;--color-secondary-200-rgb:7,138,218;--color-secondary-300-rgb:16,153,237;--color-secondary-400-rgb:24,166,253;--color-secondary-500-rgb:55,178,254;--color-secondary-600-rgb:96,194,255;--color-secondary-700-rgb:119,203,255;--color-secondary-800-rgb:155,217,255;--color-secondary-900-rgb:205,236,255;--color-secondary-950-rgb:231,246,255;--color-success-50:#0e3421;--color-success-100:#185e3b;--color-success-200:#21805a;--color-success-300:#28996c;--color-success-400:#2ba675;--color-success-500:#31bd85;--color-success-600:#5aca9d;--color-success-700:#83d7b6;--color-success-800:#ade5ce;--color-success-900:#d6f2e7;--color-success-950:#effaf6;--color-success-50-rgb:14,52,33;--color-success-100-rgb:24,94,59;--color-success-200-rgb:33,128,90;--color-success-300-rgb:40,153,108;--color-success-400-rgb:43,166,117;--color-success-500-rgb:49,189,133;--color-success-600-rgb:90,202,157;--color-success-700-rgb:131,215,182;--color-success-800-rgb:173,229,206;--color-success-900-rgb:214,242,231;--color-success-950-rgb:239,250,246;--color-warning-50:#af5f01;--color-warning-100:#d17100;--color-warning-200:#ed8307;--color-warning-300:#f38f19;--color-warning-400:#ff9f46;--color-warning-500:#ffaf65;--color-warning-600:#ffbc7e;--color-warning-700:#ffd2a9;--color-warning-800:#ffecdb;--color-warning-900:#fff4ea;--color-warning-50-rgb:175,95,1;--color-warning-100-rgb:209,113,0;--color-warning-200-rgb:237,131,7;--color-warning-300-rgb:243,143,25;--color-warning-400-rgb:255,159,70;--color-warning-500-rgb:255,175,101;--color-warning-600-rgb:255,188,126;--color-warning-700-rgb:255,210,169;--color-warning-800-rgb:255,236,219;--color-warning-900-rgb:255,244,234;--color-danger-50:#a20606;--color-danger-100:#ba1313;--color-danger-200:#d91b1b;--color-danger-300:#fb2b2b;--color-danger-400:#fc5959;--color-danger-500:#ff7c7c;--color-danger-600:#ff9292;--color-danger-700:#ffb1b1;--color-danger-800:#ffd5d5;--color-danger-900:#ffebeb;--color-danger-50-rgb:162,6,6;--color-danger-100-rgb:186,19,19;--color-danger-200-rgb:217,27,27;--color-danger-300-rgb:251,43,43;--color-danger-400-rgb:252,89,89;--color-danger-500-rgb:255,124,124;--color-danger-600-rgb:255,146,146;--color-danger-700-rgb:255,177,177;--color-danger-800-rgb:255,213,213;--color-danger-900-rgb:255,235,235;--color-important-50:#611700;--color-important-100:#c83509;--color-important-200:#ff4912;--color-important-300:#ff6f42;--color-important-400:#ff8058;--color-important-500:#ff8c68;--color-important-600:#ffb29b;--color-important-700:#ffcebf;--color-important-800:#ffe2d9;--color-important-900:#fff8f5;--color-important-50-rgb:97,23,0;--color-important-100-rgb:200,53,9;--color-important-200-rgb:255,73,18;--color-important-300-rgb:255,111,66;--color-important-400-rgb:255,128,88;--color-important-500-rgb:255,140,104;--color-important-600-rgb:255,178,155;--color-important-700-rgb:255,206,191;--color-important-800-rgb:255,226,217;--color-important-900-rgb:255,248,245;--color-special-50:#30216e;--color-special-100:#452da5;--color-special-200:#513baa;--color-special-300:#7055df;--color-special-400:#8166ee;--color-special-500:#8a6ff5;--color-special-600:#a38cff;--color-special-700:#c4b6ff;--color-special-800:#e4dff7;--color-special-900:#efecfa;--color-special-50-rgb:48,33,110;--color-special-100-rgb:69,45,165;--color-special-200-rgb:81,59,170;--color-special-300-rgb:112,85,223;--color-special-400-rgb:129,102,238;--color-special-500-rgb:138,111,245;--color-special-600-rgb:163,140,255;--color-special-700-rgb:196,182,255;--color-special-800-rgb:228,223,247;--color-special-900-rgb:239,236,250;--color-canvas:#0b0f18;--color-inverse:#fff;--color-surface:#353d57;--color-surface-light:#30394a;--color-surface-strong:#3d4667;--color-fore:#ebedf3;--color-focus:#173db6;--color-link:#59a5ff;--color-link-hover:#3883fa;--color-link-visited:#bcdcff;--color-link-active:#bcdcff;--color-placeholder:#4a577f;--color-border:#353d57;--color-border-strong:#3d4667;--color-border-light:#30394a;--color-canvas-rgb:11,15,24;--color-inverse-rgb:255,255,255;--color-surface-rgb:53,61,87;--color-surface-light-rgb:48,57,74;--color-surface-strong-rgb:61,70,103;--color-fore-rgb:235,237,243;--color-focus-rgb:23,61,182;--color-link-rgb:89,165,255;--color-link-hover-rgb:56,131,250;--color-link-visited-rgb:188,220,255;--color-link-active-rgb:188,220,255;--color-placeholder-rgb:74,87,127;--color-border-rgb:53,61,87;--color-border-strong-rgb:61,70,103;--color-border-light-rgb:48,57,74}.light-in-dark{--color-gray-50:#fcfdfe;--color-gray-100:#eff1f3;--color-gray-200:#ebedf3;--color-gray-300:#d2d6e5;--color-gray-400:#b2b9c5;--color-gray-500:#64758b;--color-gray-600:#4a577f;--color-gray-700:#3d4667;--color-gray-800:#353d57;--color-gray-900:#30394a;--color-gray-950:#0b0f18;--color-gray-50-rgb:252,253,254;--color-gray-100-rgb:239,241,243;--color-gray-200-rgb:235,237,243;--color-gray-300-rgb:210,214,229;--color-gray-400-rgb:178,185,197;--color-gray-500-rgb:100,117,139;--color-gray-600-rgb:74,87,127;--color-gray-700-rgb:61,70,103;--color-gray-800-rgb:53,61,87;--color-gray-900-rgb:48,57,74;--color-gray-950-rgb:11,15,24;--color-primary-50:#eef6ff;--color-primary-100:#d9eaff;--color-primary-200:#bcdcff;--color-primary-300:#8ec6ff;--color-primary-400:#59a5ff;--color-primary-500:#3883fa;--color-primary-600:#2b67e5;--color-primary-700:#144ce1;--color-primary-800:#173db6;--color-primary-900:#19388f;--color-primary-950:#142457;--color-primary-50-rgb:238,246,255;--color-primary-100-rgb:217,234,255;--color-primary-200-rgb:188,220,255;--color-primary-300-rgb:142,198,255;--color-primary-400-rgb:89,165,255;--color-primary-500-rgb:56,131,250;--color-primary-600-rgb:43,103,229;--color-primary-700-rgb:20,76,225;--color-primary-800-rgb:23,61,182;--color-primary-900-rgb:25,56,143;--color-primary-950-rgb:20,36,87;--color-secondary-50:#e7f6ff;--color-secondary-100:#cdecff;--color-secondary-200:#9bd9ff;--color-secondary-300:#77cbff;--color-secondary-400:#60c2ff;--color-secondary-500:#37b2fe;--color-secondary-600:#18a6fd;--color-secondary-700:#1099ed;--color-secondary-800:#078ada;--color-secondary-900:#046bab;--color-secondary-950:#062a4b;--color-secondary-50-rgb:231,246,255;--color-secondary-100-rgb:205,236,255;--color-secondary-200-rgb:155,217,255;--color-secondary-300-rgb:119,203,255;--color-secondary-400-rgb:96,194,255;--color-secondary-500-rgb:55,178,254;--color-secondary-600-rgb:24,166,253;--color-secondary-700-rgb:16,153,237;--color-secondary-800-rgb:7,138,218;--color-secondary-900-rgb:4,107,171;--color-secondary-950-rgb:6,42,75;--color-success-50:#effaf6;--color-success-100:#d6f2e7;--color-success-200:#ade5ce;--color-success-300:#83d7b6;--color-success-400:#5aca9d;--color-success-500:#31bd85;--color-success-600:#2ba675;--color-success-700:#28996c;--color-success-800:#21805a;--color-success-900:#185e3b;--color-success-950:#0e3421;--color-success-50-rgb:239,250,246;--color-success-100-rgb:214,242,231;--color-success-200-rgb:173,229,206;--color-success-300-rgb:131,215,182;--color-success-400-rgb:90,202,157;--color-success-500-rgb:49,189,133;--color-success-600-rgb:43,166,117;--color-success-700-rgb:40,153,108;--color-success-800-rgb:33,128,90;--color-success-900-rgb:24,94,59;--color-success-950-rgb:14,52,33;--color-warning-50:#fff4ea;--color-warning-100:#ffecdb;--color-warning-200:#ffd2a9;--color-warning-300:#ffbc7e;--color-warning-400:#ffaf65;--color-warning-500:#ff9f46;--color-warning-600:#f38f19;--color-warning-700:#ed8307;--color-warning-800:#d17100;--color-warning-900:#af5f01;--color-warning-50-rgb:255,244,234;--color-warning-100-rgb:255,236,219;--color-warning-200-rgb:255,210,169;--color-warning-300-rgb:255,188,126;--color-warning-400-rgb:255,175,101;--color-warning-500-rgb:255,159,70;--color-warning-600-rgb:243,143,25;--color-warning-700-rgb:237,131,7;--color-warning-800-rgb:209,113,0;--color-warning-900-rgb:175,95,1;--color-danger-50:#ffebeb;--color-danger-100:#ffd5d5;--color-danger-200:#ffb1b1;--color-danger-300:#ff9292;--color-danger-400:#ff7c7c;--color-danger-500:#fc5959;--color-danger-600:#fb2b2b;--color-danger-700:#d91b1b;--color-danger-800:#ba1313;--color-danger-900:#a20606;--color-danger-50-rgb:255,235,235;--color-danger-100-rgb:255,213,213;--color-danger-200-rgb:255,177,177;--color-danger-300-rgb:255,146,146;--color-danger-400-rgb:255,124,124;--color-danger-500-rgb:252,89,89;--color-danger-600-rgb:251,43,43;--color-danger-700-rgb:217,27,27;--color-danger-800-rgb:186,19,19;--color-danger-900-rgb:162,6,6;--color-important-50:#fff8f5;--color-important-100:#ffe2d9;--color-important-200:#ffcebf;--color-important-300:#ffb29b;--color-important-400:#ff8c68;--color-important-500:#ff8058;--color-important-600:#ff6f42;--color-important-700:#ff4912;--color-important-800:#c83509;--color-important-900:#611700;--color-important-50-rgb:255,248,245;--color-important-100-rgb:255,226,217;--color-important-200-rgb:255,206,191;--color-important-300-rgb:255,178,155;--color-important-400-rgb:255,140,104;--color-important-500-rgb:255,128,88;--color-important-600-rgb:255,111,66;--color-important-700-rgb:255,73,18;--color-important-800-rgb:200,53,9;--color-important-900-rgb:97,23,0;--color-special-50:#efecfa;--color-special-100:#e4dff7;--color-special-200:#c4b6ff;--color-special-300:#a38cff;--color-special-400:#8a6ff5;--color-special-500:#8166ee;--color-special-600:#7055df;--color-special-700:#513baa;--color-special-800:#452da5;--color-special-900:#30216e;--color-special-50-rgb:239,236,250;--color-special-100-rgb:228,223,247;--color-special-200-rgb:196,182,255;--color-special-300-rgb:163,140,255;--color-special-400-rgb:138,111,245;--color-special-500-rgb:129,102,238;--color-special-600-rgb:112,85,223;--color-special-700-rgb:81,59,170;--color-special-800-rgb:69,45,165;--color-special-900-rgb:48,33,110;--color-inherit:inherit;--color-transparent:transparent;--color-current:currentColor;--color-black:#000;--color-white:#fff;--color-canvas:#fff;--color-canvas-light:#fff;--color-canvas-dark:#fcfdfe;--color-inverse:#000;--color-surface:#eff1f3;--color-surface-light:#fcfdfe;--color-surface-strong:#ebedf3;--color-fore:#30394a;--color-fore-in-light:#30394a;--color-fore-in-dark:#fff;--color-focus:#bcdcff;--color-link:#2b47a2;--color-link-hover:#144ce1;--color-link-visited:#6328a4;--color-link-active:#144ce1;--color-placeholder:#64758b;--color-border:#ebedf3;--color-border-strong:#d2d6e5;--color-border-light:#eff1f3;--color-black-rgb:0,0,0;--color-white-rgb:255,255,255;--color-canvas-rgb:255,255,255;--color-canvas-light-rgb:255,255,255;--color-canvas-dark-rgb:252,253,254;--color-inverse-rgb:0,0,0;--color-surface-rgb:239,241,243;--color-surface-light-rgb:252,253,254;--color-surface-strong-rgb:235,237,243;--color-fore-rgb:48,57,74;--color-fore-in-light-rgb:48,57,74;--color-fore-in-dark-rgb:255,255,255;--color-focus-rgb:188,220,255;--color-link-rgb:43,71,162;--color-link-hover-rgb:20,76,225;--color-link-visited-rgb:99,40,164;--color-link-active-rgb:20,76,225;--color-placeholder-rgb:100,117,139;--color-border-rgb:235,237,243;--color-border-strong-rgb:210,214,229;--color-border-light-rgb:239,241,243}body{--btn-bg:var(--color-gray-50)}@media (prefers-color-scheme:dark){.dark-auto{--color-gray-50:#0b0f18;--color-gray-100:#30394a;--color-gray-200:#353d57;--color-gray-300:#3d4667;--color-gray-400:#4a577f;--color-gray-500:#64758b;--color-gray-600:#b2b9c5;--color-gray-700:#d2d6e5;--color-gray-800:#ebedf3;--color-gray-900:#eff1f3;--color-gray-950:#fcfdfe;--color-gray-50-rgb:11,15,24;--color-gray-100-rgb:48,57,74;--color-gray-200-rgb:53,61,87;--color-gray-300-rgb:61,70,103;--color-gray-400-rgb:74,87,127;--color-gray-500-rgb:100,117,139;--color-gray-600-rgb:178,185,197;--color-gray-700-rgb:210,214,229;--color-gray-800-rgb:235,237,243;--color-gray-900-rgb:239,241,243;--color-gray-950-rgb:252,253,254;--color-primary-50:#142457;--color-primary-100:#19388f;--color-primary-200:#173db6;--color-primary-300:#144ce1;--color-primary-400:#2b67e5;--color-primary-500:#3883fa;--color-primary-600:#59a5ff;--color-primary-700:#8ec6ff;--color-primary-800:#bcdcff;--color-primary-900:#d9eaff;--color-primary-950:#eef6ff;--color-primary-50-rgb:20,36,87;--color-primary-100-rgb:25,56,143;--color-primary-200-rgb:23,61,182;--color-primary-300-rgb:20,76,225;--color-primary-400-rgb:43,103,229;--color-primary-500-rgb:56,131,250;--color-primary-600-rgb:89,165,255;--color-primary-700-rgb:142,198,255;--color-primary-800-rgb:188,220,255;--color-primary-900-rgb:217,234,255;--color-primary-950-rgb:238,246,255;--color-secondary-50:#062a4b;--color-secondary-100:#046bab;--color-secondary-200:#078ada;--color-secondary-300:#1099ed;--color-secondary-400:#18a6fd;--color-secondary-500:#37b2fe;--color-secondary-600:#60c2ff;--color-secondary-700:#77cbff;--color-secondary-800:#9bd9ff;--color-secondary-900:#cdecff;--color-secondary-950:#e7f6ff;--color-secondary-50-rgb:6,42,75;--color-secondary-100-rgb:4,107,171;--color-secondary-200-rgb:7,138,218;--color-secondary-300-rgb:16,153,237;--color-secondary-400-rgb:24,166,253;--color-secondary-500-rgb:55,178,254;--color-secondary-600-rgb:96,194,255;--color-secondary-700-rgb:119,203,255;--color-secondary-800-rgb:155,217,255;--color-secondary-900-rgb:205,236,255;--color-secondary-950-rgb:231,246,255;--color-success-50:#0e3421;--color-success-100:#185e3b;--color-success-200:#21805a;--color-success-300:#28996c;--color-success-400:#2ba675;--color-success-500:#31bd85;--color-success-600:#5aca9d;--color-success-700:#83d7b6;--color-success-800:#ade5ce;--color-success-900:#d6f2e7;--color-success-950:#effaf6;--color-success-50-rgb:14,52,33;--color-success-100-rgb:24,94,59;--color-success-200-rgb:33,128,90;--color-success-300-rgb:40,153,108;--color-success-400-rgb:43,166,117;--color-success-500-rgb:49,189,133;--color-success-600-rgb:90,202,157;--color-success-700-rgb:131,215,182;--color-success-800-rgb:173,229,206;--color-success-900-rgb:214,242,231;--color-success-950-rgb:239,250,246;--color-warning-50:#af5f01;--color-warning-100:#d17100;--color-warning-200:#ed8307;--color-warning-300:#f38f19;--color-warning-400:#ff9f46;--color-warning-500:#ffaf65;--color-warning-600:#ffbc7e;--color-warning-700:#ffd2a9;--color-warning-800:#ffecdb;--color-warning-900:#fff4ea;--color-warning-50-rgb:175,95,1;--color-warning-100-rgb:209,113,0;--color-warning-200-rgb:237,131,7;--color-warning-300-rgb:243,143,25;--color-warning-400-rgb:255,159,70;--color-warning-500-rgb:255,175,101;--color-warning-600-rgb:255,188,126;--color-warning-700-rgb:255,210,169;--color-warning-800-rgb:255,236,219;--color-warning-900-rgb:255,244,234;--color-danger-50:#a20606;--color-danger-100:#ba1313;--color-danger-200:#d91b1b;--color-danger-300:#fb2b2b;--color-danger-400:#fc5959;--color-danger-500:#ff7c7c;--color-danger-600:#ff9292;--color-danger-700:#ffb1b1;--color-danger-800:#ffd5d5;--color-danger-900:#ffebeb;--color-danger-50-rgb:162,6,6;--color-danger-100-rgb:186,19,19;--color-danger-200-rgb:217,27,27;--color-danger-300-rgb:251,43,43;--color-danger-400-rgb:252,89,89;--color-danger-500-rgb:255,124,124;--color-danger-600-rgb:255,146,146;--color-danger-700-rgb:255,177,177;--color-danger-800-rgb:255,213,213;--color-danger-900-rgb:255,235,235;--color-important-50:#611700;--color-important-100:#c83509;--color-important-200:#ff4912;--color-important-300:#ff6f42;--color-important-400:#ff8058;--color-important-500:#ff8c68;--color-important-600:#ffb29b;--color-important-700:#ffcebf;--color-important-800:#ffe2d9;--color-important-900:#fff8f5;--color-important-50-rgb:97,23,0;--color-important-100-rgb:200,53,9;--color-important-200-rgb:255,73,18;--color-important-300-rgb:255,111,66;--color-important-400-rgb:255,128,88;--color-important-500-rgb:255,140,104;--color-important-600-rgb:255,178,155;--color-important-700-rgb:255,206,191;--color-important-800-rgb:255,226,217;--color-important-900-rgb:255,248,245;--color-special-50:#30216e;--color-special-100:#452da5;--color-special-200:#513baa;--color-special-300:#7055df;--color-special-400:#8166ee;--color-special-500:#8a6ff5;--color-special-600:#a38cff;--color-special-700:#c4b6ff;--color-special-800:#e4dff7;--color-special-900:#efecfa;--color-special-50-rgb:48,33,110;--color-special-100-rgb:69,45,165;--color-special-200-rgb:81,59,170;--color-special-300-rgb:112,85,223;--color-special-400-rgb:129,102,238;--color-special-500-rgb:138,111,245;--color-special-600-rgb:163,140,255;--color-special-700-rgb:196,182,255;--color-special-800-rgb:228,223,247;--color-special-900-rgb:239,236,250;--color-canvas:#0b0f18;--color-inverse:#fff;--color-surface:#353d57;--color-surface-light:#30394a;--color-surface-strong:#3d4667;--color-fore:#ebedf3;--color-focus:#173db6;--color-link:#59a5ff;--color-link-hover:#3883fa;--color-link-visited:#bcdcff;--color-link-active:#bcdcff;--color-placeholder:#4a577f;--color-border:#353d57;--color-border-strong:#3d4667;--color-border-light:#30394a;--color-canvas-rgb:11,15,24;--color-inverse-rgb:255,255,255;--color-surface-rgb:53,61,87;--color-surface-light-rgb:48,57,74;--color-surface-strong-rgb:61,70,103;--color-fore-rgb:235,237,243;--color-focus-rgb:23,61,182;--color-link-rgb:89,165,255;--color-link-hover-rgb:56,131,250;--color-link-visited-rgb:188,220,255;--color-link-active-rgb:188,220,255;--color-placeholder-rgb:74,87,127;--color-border-rgb:53,61,87;--color-border-strong-rgb:61,70,103;--color-border-light-rgb:48,57,74}}*,:after,:before{--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity))}:focus-visible{outline-color:transparent}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }html{font-size:var(--font-size-root)}body{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));font-size:.8125rem;line-height:1.5384615385}.dark,body{--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))}:root{--font-h1-size:2.25rem;--article-p-space:0.5em;--article-heading-space:0.5em;--article-hr-space:1.5em;--article-cell-padding:0.25rem 0.75rem}a,a.disabled:hover{--tw-text-opacity:1;color:rgba(var(--color-link-rgb),var(--tw-text-opacity))}a:hover{--tw-text-opacity:1;color:rgba(var(--color-link-hover-rgb),var(--tw-text-opacity))}a:focus-visible{outline-color:rgb(var(--color-focus-rgb));outline-offset:2px;outline-width:2px}.code,code{background-color:rgba(var(--color-important-500-rgb),.1);border-radius:var(--radius);padding-left:.25rem;padding-right:.25rem;--tw-text-opacity:1;color:rgba(var(--color-important-500-rgb),var(--tw-text-opacity))}pre{font-size:.75rem;line-height:1rem;overflow:auto;padding:.5rem}pre code{background-color:transparent;color:inherit;padding:0}samp,var{font-weight:700}.blockquote,blockquote{border-left-width:4px;font-size:.875rem;line-height:1.3rem;padding-left:.75rem;text-wrap:balance}.blockquote figcaption,blockquote figcaption{font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-weight:700;line-height:1.5;text-wrap:balance}.h1,h1{font-size:1.75rem}.h2,h2{font-size:1.5rem}.h3,h3{font-size:1.25rem}.h4,h4{font-size:1rem}.h5,h5{font-size:.875rem}.h6,h6{font-size:.75rem}ul{list-style-type:disc}ol,ul{padding-left:1.5em}ol{list-style-type:decimal}dt{font-weight:700}dd{padding-left:2em}.article{color:var(--font-size-article)}.article h1,.article h2,.article h3,.article h4{margin-bottom:var(--article-heading-space);margin-top:var(--article-heading-space)}.article audio,.article blockquote,.article figure,.article img,.article ol,.article p,.article table,.article ul,.article video{margin-bottom:var(--article-p-space);margin-top:var(--article-p-space)}.article p{min-height:1em}.article p:empty:before{content:" ";display:inline-block}.article hr{margin-bottom:var(--article-hr-space);margin-top:var(--article-hr-space)}.article table{border-width:1px}.article td,.article th{border-bottom-width:1px;padding:var(--article-cell-padding)}.article thead{background-color:var(--table-head-bg,var(--color-gray-100))}.article ol *,.article table *,.article ul *{margin-bottom:0;margin-top:0}.article pre{border-radius:var(--radius);border-width:1px;--tw-bg-opacity:1;background-color:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity))}:root{--alert-bg:var(--color-surface)}.alert{align-items:center;background-color:var(--alert-bg);border-radius:var(--radius);display:flex;gap:.75rem;padding:.5rem .75rem}.alert-content{display:flex;flex:1 1 auto;flex-direction:column;gap:1rem}.alert-close{margin-bottom:-.375rem;margin-right:-.5rem;margin-top:-.375rem}.alert-link{color:inherit;font-weight:700}.alert-link:hover{color:inherit;text-decoration-line:underline}.alert-heading{font-weight:700;margin:0}.alert-text{flex:1 1 auto;white-space:pre-line}.alert-actions{align-items:center;display:flex;gap:.5rem}:root{--breadcrumb-divider:"/";--breadcrumb-divider-color:var(--color-gray-500);--breadcrumb-color-active:var(--color-gray-500)}.breadcrumb{display:flex;gap:1.25rem;list-style-type:none;padding-left:0}.breadcrumb>li{position:relative}.breadcrumb>li+li:before{color:var(--breadcrumb-divider-color);content:var(--breadcrumb-divider);display:block;left:-1.25rem;position:absolute;text-align:center;width:1.25rem}.breadcrumb>.active{color:var(--breadcrumb-color-active)}:root{--btn-radius:var(--radius);--btn-bg:var(--color-surface);--btn-border-color:var(--color-gray-300);--btn-height:2rem}.btn{align-items:center;background-color:var(--btn-bg);border-radius:var(--btn-radius);display:inline-flex;gap:.375rem;height:var(--btn-height);justify-content:center;padding-left:.75rem;padding-right:.75rem;white-space:nowrap;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--btn-border-color)}.btn,.btn-default:hover{color:inherit}.btn-link{--tw-text-opacity:1;color:rgba(var(--color-link-rgb),var(--tw-text-opacity));text-decoration-line:underline;text-underline-offset:2px;--tw-ring-color:transparent}.btn-link:visited{color:rgba(var(--color-link-visited-rgb),var(--tw-text-opacity))}.btn-link:hover{--tw-text-opacity:1;color:rgba(var(--color-link-hover-rgb),var(--tw-text-opacity))}.btn-link{--btn-bg:transparent}.btn.size-xs,.size-xs>.btn{padding-left:.25rem;padding-right:.25rem;--btn-height:1.25rem}.btn.size-sm,.btn.size-xs,.size-sm>.btn,.size-xs>.btn{font-size:.75rem;line-height:1rem}.btn.size-sm,.size-sm>.btn{--btn-height:1.5rem}.btn.size-md,.btn.size-sm,.size-md>.btn,.size-sm>.btn{padding-left:.5rem;padding-right:.5rem}.btn.size-md,.size-md>.btn{--btn-height:1.75rem}.btn.size-lg,.size-lg>.btn{font-size:1rem;line-height:1.5rem;padding-left:1rem;padding-right:1rem;--btn-height:2.5rem}.btn.size-xl,.size-xl>.btn{font-size:1.125rem;line-height:1.75rem;padding-left:1.25rem;padding-right:1.25rem;--btn-height:3rem}.btn.square{aspect-ratio:1/1;gap:.125rem;padding-left:0;padding-right:0}@supports not (aspect-ratio:1/1){.btn.square{width:var(--btn-height)}}.btn.btn-caret{padding-left:.25rem;padding-right:.25rem}.btn.btn-wide{min-width:80px}:root{--checkbox-size:12px;--checkbox-color:var(--color-primary-500)}.checkbox,.radio{align-items:center;cursor:pointer;display:flex;gap:.375rem;min-height:20px}.checkbox>input[type=checkbox],.radio>input[type=radio]{accent-color:rgb(var(--color-primary-500-rgb));border-radius:var(--radius-lg)}.checkbox-primary,.radio-primary{display:flex;gap:.375rem;min-height:20px;position:relative}.checkbox-primary>input[type=checkbox],.radio-primary>input[type=radio]{bottom:0;left:0;opacity:0;position:absolute;right:0;top:0}.checkbox-primary>label,.radio-primary>label{cursor:pointer;padding-left:calc(var(--checkbox-size) + 8px);position:relative}.checkbox-primary>label:after,.checkbox-primary>label:before,.radio-primary>label:after,.radio-primary>label:before{display:block;height:var(--checkbox-size);left:0;position:absolute;transition-duration:.15s;transition-property:transform,background-color,color,opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--checkbox-size);--tw-content:"";content:var(--tw-content);top:calc((20px - var(--checkbox-size))/2)}.checkbox-primary>label:before{border-radius:var(--radius-sm);border-width:1px;--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.checkbox-primary>label:after{--tw-scale-x:.9;--tw-scale-y:.9;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0ibTkgMjAuNDItNi4yMS02LjIxIDIuODMtMi44M0w5IDE0Ljc3bDkuODgtOS44OSAyLjgzIDIuODN6Ii8+PC9zdmc+");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))}.radio-primary>label:before{border-radius:var(--radius-full);border-width:1px;--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity))}.radio-primary>label:after{background-color:var(--checkbox-color);border-radius:var(--radius-full);opacity:0;--checkbox-radio-size:calc(var(--checkbox-size)*2/3);height:var(--checkbox-radio-size);left:calc((var(--checkbox-size) - var(--checkbox-radio-size))/2);top:calc((20px - var(--checkbox-radio-size))/2);width:var(--checkbox-radio-size)}.checkbox-primary>label:hover:before,.radio-primary>label:hover:before{border-color:var(--checkbox-color)}.checkbox-primary.focus>label:before,.checkbox-primary>input[type=checkbox]:focus+label:before,.radio-primary.focus>label:before,.radio-primary>input[type=radio]:focus+label:before{border-color:var(--checkbox-color);outline-color:rgb(var(--color-focus-rgb));outline-style:solid;outline-width:2px}.checkbox-primary.checked>label:before,.checkbox-primary>input[type=checkbox]:checked+label:before{background-color:var(--checkbox-color);border-color:var(--checkbox-color)}.radio-primary.checked>label:before,.radio-primary>input[type=radio]:checked+label:before{border-color:var(--checkbox-color)}.checkbox-primary.checked>label:after,.checkbox-primary>input[type=checkbox]:checked+label:after,.radio-primary.checked>label:after,.radio-primary>input[type=radio]:checked+label:after{opacity:1}.checkbox-primary.disabled>label,.checkbox-primary>input[type=checkbox]:disabled+label,.radio-primary.disabled>label,.radio-primary>input[type=radio]:disabled+label{cursor:not-allowed;opacity:.7;pointer-events:none;--tw-grayscale:grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.checkbox-primary.indeterminate>label:after,.checkbox-primary>input[type=checkbox]:indeterminate+label:after{--tw-border-opacity:1;background-color:var(--checkbox-color);background-image:none;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity));border-width:calc((var(--checkbox-size) - 2px)/2) 2px;opacity:1}.check-list{align-items:flex-start;flex-direction:column;gap:.5rem}.check-list,.check-list-inline{display:flex;padding-bottom:.375rem;padding-top:.375rem}.check-list-inline{align-items:center;-moz-column-gap:1rem;column-gap:1rem;flex-direction:row;flex-wrap:wrap;row-gap:.5rem}.switch{display:flex;gap:.375rem;min-height:20px;padding-left:2.5rem;position:relative}.switch>input{bottom:0;left:0;opacity:0;position:absolute;right:0;top:0}.switch>label:after,.switch>label:before{border-radius:var(--radius-full);border-width:1px;height:1rem;left:0;position:absolute;top:.125rem;width:2rem;--tw-border-opacity:1;border-color:rgba(var(--color-border-strong-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));transition-duration:.3s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);--tw-content:"";content:var(--tw-content)}.switch>label:before{cursor:pointer;--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.switch>label:hover:before{--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity))}.switch>label:after{height:.75rem;left:.125rem;top:.25rem;width:.75rem;--tw-border-opacity:1;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.switch>input:checked+label:before{background-color:var(--checkbox-color);border-color:rgba(var(--color-inverse-rgb),.2)}.switch>input:checked+label:after{color:var(--checkbox-color);left:18px}.switch.focus>label:before,.switch>input:focus+label:before{border-color:var(--checkbox-color);outline-color:rgb(var(--color-focus-rgb));outline-style:solid;outline-width:2px}.switch.disabled>label:before,.switch>input:disabled+label:before{cursor:not-allowed;opacity:.7;pointer-events:none;--tw-grayscale:grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}:root{--form-control-radius:var(--radius);--form-control-border:var(--color-border);--form-control-focus:var(--color-primary-500);--form-control-disabled:var(--color-surface);--form-control-readonly:var(--color-surface)}.dark{--form-control-border:var(--color-border-strong)}input::-moz-placeholder,textarea::-moz-placeholder{--tw-placeholder-opacity:1;color:rgba(var(--color-placeholder-rgb),var(--tw-placeholder-opacity))}input::placeholder,textarea::placeholder{--tw-placeholder-opacity:1;color:rgba(var(--color-placeholder-rgb),var(--tw-placeholder-opacity))}.form-control{border-radius:var(--form-control-radius);display:block;height:2rem;width:100%;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));outline-color:transparent;outline-style:solid;outline-width:1px;padding:.25rem .5rem;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--form-control-border);transition-duration:.15s;transition-property:outline,box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.form-control.focus,.form-control:focus{outline-color:var(--form-control-focus);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-focus-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.3}.form-control.disabled,.form-control[disabled]{background-color:var(--form-control-disabled);opacity:1}.form-control.readonly,.form-control[readonly]{background-color:var(--form-control-readonly)}select{-webkit-appearance:none;background-image:url("data:image/gif;base64,R0lGODlhBwAEAIAAAMvQ2////yH5BAEAAAEALAAAAAAHAAQAAAIIhA+BGWoNWSgAOw==");background-position:right 8px top 50%;background-repeat:no-repeat;background-size:8px auto;padding-right:1rem}select.form-control[multiple]{background-image:none}textarea.form-control{height:auto;min-height:32px}input[type=file].form-control{padding:.125rem}input[type=file].form-control::-webkit-file-upload-button{-webkit-appearance:none;appearance:none;border-radius:inherit;border-style:none;cursor:pointer;height:1.75rem;padding-left:.5rem;padding-right:.5rem}input[type=file].form-control::-webkit-file-upload-button:hover{background-color:rgba(var(--color-black-rgb),.1)}.has-error .form-control,.has-error.form-control{--tw-ring-color:rgba(var(--color-danger-500-rgb),0.6)}.has-error .form-control.focus,.has-error .form-control:focus,.has-error.form-control.focus,.has-error.form-control:focus{outline-color:rgb(var(--color-danger-500-rgb));--tw-ring-color:rgba(var(--color-danger-500-rgb),0.1)}.has-warning .form-control,.has-warning.form-control{--tw-ring-color:rgba(var(--color-warning-500-rgb),0.6)}.has-warning .form-control.focus,.has-warning .form-control:focus,.has-warning.form-control.focus,.has-warning.form-control:focus{outline-color:rgb(var(--color-warning-500-rgb));--tw-ring-color:rgba(var(--color-warning-500-rgb),0.2)}.has-success .form-control,.has-success.form-control{--tw-ring-color:rgba(var(--color-success-500-rgb),0.6)}.has-success .form-control.focus,.has-success .form-control:focus,.has-success.form-control.focus,.has-success.form-control:focus{outline-color:rgb(var(--color-success-500-rgb));--tw-ring-color:rgba(var(--color-success-500-rgb),0.2)}.form-control.size-sm,.size-sm>.form-control{font-size:.75rem;height:1.5rem;line-height:1rem;padding-left:.5rem;padding-right:.5rem}.form-control.size-md,.size-md>.form-control{height:1.75rem;padding-left:.5rem;padding-right:.5rem}.form-control.size-lg,.size-lg>.form-control{font-size:1rem;height:2.5rem;line-height:1.5rem;padding-left:.75rem;padding-right:.75rem}:root{--input-group-addon-bg:var(--color-gray-50)}.input-group{align-items:stretch;display:flex}.input-group>.form-control,.input-group>.input-control{flex:1 1 auto;z-index:1}.input-group-addon{align-items:center;background-color:var(--input-group-addon-bg);border-radius:var(--form-control-radius);display:flex;flex:none;height:2rem;padding-left:.5rem;padding-right:.5rem;z-index:0;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--form-control-border)}.size-sm>.input-group-addon{font-size:.75rem;height:1.5rem;line-height:1rem;padding-left:.5rem;padding-right:.5rem}.size-lg>.input-group-addon{font-size:1rem;height:2.5rem;line-height:1.5rem;padding-left:.75rem;padding-right:.75rem}.input-group-control{flex:1 1 auto}.input-group>*+*,.input-group>*+* .form-control{border-bottom-left-radius:var(--radius-none);border-top-left-radius:var(--radius-none)}.input-group>:not(:last-child),.input-group>:not(:last-child) .form-control{border-bottom-right-radius:var(--radius-none);border-top-right-radius:var(--radius-none)}.input-group .btn:focus-visible,.input-group .form-control:focus{z-index:2}:root{--form-tip-color:var(--color-gray-400);--form-label-color:var(--color-gray-700);--form-horz-label-width:6rem;--form-grid-gap-x:1.25rem;--form-grid-gap-y:.75rem}.form{flex-direction:column;gap:1rem}.form,.form-label{display:flex}.form-label{align-items:center;color:var(--form-label-color);flex-direction:row;gap:.25rem;height:2rem;overflow:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap}.form-label.required:after{content:"*";display:inline-block;margin-left:.25rem;--tw-translate-y:0.125rem;--tw-scale-x:1.25;--tw-scale-y:1.25;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));--tw-text-opacity:1;color:rgba(var(--color-danger-500-rgb),var(--tw-text-opacity))}.no-required .form-label.required:after{display:none}.form-label-actions{flex:1 1 auto;justify-content:flex-end}.form-label>.checkbox-primary{margin-left:.75rem}.form-tip{color:var(--form-tip-color);display:block;margin-top:.25rem}.has-error .form-tip{--tw-text-opacity:1;color:rgba(var(--color-danger-500-rgb),var(--tw-text-opacity))}.has-warning .form-tip{--tw-text-opacity:1;color:rgba(var(--color-warning-500-rgb),var(--tw-text-opacity))}.has-success .form-tip{--tw-text-opacity:1;color:rgba(var(--color-success-500-rgb),var(--tw-text-opacity))}.form-actions{display:flex;flex-direction:row;gap:1rem;justify-content:center}.form-grid{--form-grid-gap-x-half:calc(var(--form-grid-gap-x)/2);--form-grid-gap-y-half:calc(var(--form-grid-gap-y)/2);align-items:flex-start;flex-direction:row;flex-wrap:wrap;gap:0}.form-grid .form-group{padding:var(--form-grid-gap-y-half) var(--form-grid-gap-x-half)}.form-grid .form-label.required:after{margin-left:.25rem;order:-9999}.form-grid .form-label .checkbox{margin-left:.75rem;margin-right:.75rem}.form-grid .has-wrap-after:after{display:block;width:100%;--tw-content:"";content:var(--tw-content)}.form-grid-wrap{width:100%}.form-grid-row{display:flex;flex-direction:row;padding-bottom:var(--form-grid-gap-y-half);padding-top:var(--form-grid-gap-y-half);position:relative;width:100%}.form-grid-col{padding-left:var(--form-grid-gap-x-half);padding-right:var(--form-grid-gap-x-half)}.form-grid .form-actions{gap:1.5rem;justify-content:center;padding:var(--form-grid-gap-y) var(--form-grid-gap-x-half);width:100%}.form-grid .form-actions.is-pinned{border-top-width:1px}.form-horz .form-group,.form-horz .form-row{align-items:flex-start;display:flex;flex-direction:row}.form-horz .form-group{flex-grow:1;flex-wrap:wrap;min-height:32px;min-width:0;padding-left:var(--form-horz-label-width);position:relative}.form-horz .form-group[class*=" w-"],.form-horz .form-group[class^=w-]{flex:none}.form-horz .form-group>*{min-width:0}.form-horz .form-group.no-label{padding-left:0}.form-horz .form-group.no-label>.check-list-inline{padding-left:1rem;padding-right:1rem}.form-horz .form-label{justify-content:flex-end;left:0;padding-left:1rem;padding-right:.5rem;position:absolute;top:0;width:var(--form-horz-label-width)}.form-horz .form-label.required:after{margin-left:0;order:-9999}.form-horz .form-tip{width:100%}.form-horz .form-tip,.form-horz .input-control,.form-horz .input-group{flex:1 1 auto}.form .form-group-wrapper,.form-horz .form-group-wrapper{flex:1 1 auto;min-width:0;width:100%;z-index:1}.form .form-group-wrapper.flex-none,.form-horz .form-group-wrapper.flex-none{flex:none}.form fieldset>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.form fieldset{padding-bottom:1rem;padding-left:1rem}.form-grid fieldset>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.form legend{border-bottom-width:1px;display:flex;justify-content:flex-start;margin-left:-1rem;width:100%;--tw-border-opacity:1;border-bottom-color:rgba(var(--color-primary-100-rgb),var(--tw-border-opacity));width:calc(100% + 1rem)}.form-title{align-items:center;background-color:rgba(var(--color-primary-50-rgb),.5);display:flex;font-weight:700;height:1.75rem;padding-left:.75rem;padding-right:.75rem}:root{--input-control-fix-width-sm:2rem;--input-control-fix-width:4.375rem;--input-control-fix-width-lg:6.75rem}.input-control{position:relative;--input-control-prefix:8px;--input-control-suffix:8px}.input-control-prefix,.input-control-suffix{align-items:center;display:flex;height:100%;left:0;opacity:.5;padding-left:.5rem;padding-right:.5rem;position:absolute;top:0;white-space:nowrap;width:var(--input-control-prefix);z-index:1}.input-control-suffix{justify-content:flex-end;left:auto;right:0;width:var(--input-control-suffix)}.form-control:focus+.input-control-prefix,.form-control:focus+.input-control-suffix{opacity:1}.input-control>.form-control{padding-left:var(--input-control-prefix);padding-right:var(--input-control-suffix)}.input-control>select.form-control{background-position-x:calc(100% - var(--input-control-suffix) - 8px);padding-right:calc(var(--input-control-suffix) + 20px)}.has-prefix{--input-control-prefix:var(--input-control-fix-width)}.has-suffix{--input-control-suffix:var(--input-control-fix-width)}.has-prefix-sm{--input-control-prefix:var(--input-control-fix-width-sm)}.has-suffix-sm{--input-control-suffix:var(--input-control-fix-width-sm)}.has-prefix-lg{--input-control-prefix:var(--input-control-fix-width-lg)}.has-suffix-lg{--input-control-suffix:var(--input-control-fix-width-lg)}.has-prefix-icon{--input-control-prefix:32px}.has-suffix-icon{--input-control-suffix:32px}.input-control.has-prefix-icon{--input-control-prefix:32px}.input-control.has-suffix-icon{--input-control-suffix:32px}.input-control.has-prefix-icon>.input-control-prefix,.input-control.has-suffix-icon>.input-control-suffix{justify-content:center;padding:0}.input-control.size-sm>.form-control,.input-control.size-sm>.input-control-prefix,.input-control.size-sm>.input-control-suffix{font-size:.75rem;line-height:1rem}.input-control.size-lg>.form-control,.input-control.size-lg>.input-control-prefix,.input-control.size-lg>.input-control-suffix{font-size:1rem;line-height:1.5rem}:root{--label-bg:rgba(var(--color-canvas-rgb),.1);--label-border-color:var(--color-gray-300);--label-color:var(--color-gray-500);--label-radius:var(--radius)}.label{align-items:center;background-color:var(--label-bg);border-radius:var(--label-radius);color:var(--label-color);display:inline-flex;font-size:.75rem;height:1.25rem;justify-content:center;line-height:1rem;min-width:1.25rem;padding-left:.25rem;padding-right:.25rem;white-space:pre-line;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:var(--label-border-color)}.label.size-lg{font-size:.8125rem;height:1.5rem;line-height:1.25rem;min-width:1.5rem;padding-left:.5rem;padding-right:.5rem}.label.size-sm{height:1rem;min-width:1rem}.label-dot{aspect-ratio:1/1;border-radius:var(--radius-full);height:.5rem;min-width:0;padding-left:0;padding-right:0}.caret,.caret-down,.caret-left,.caret-right,.caret-up{align-items:center;display:inline-flex;height:.75rem;justify-content:center;position:relative;width:.75rem}.caret-down:before,.caret-left:before,.caret-right:before,.caret-up:before,.caret:before{display:block;height:.35rem;transform-origin:center;width:.35rem;--tw-translate-y:-1px;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.caret-left:before{--tw-translate-x:0.125rem;--tw-translate-y:0px;--tw-rotate:135deg}.caret-left:before,.caret-right:before{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))}.caret-right:before{--tw-translate-x:-0.125rem;--tw-translate-y:0px;--tw-rotate:-45deg}.caret-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}:focus>.caret,:hover>.caret{opacity:inherit}.disabled .caret,.readonly .caret{opacity:var(--caret-opacity,.5)}.listitem{align-items:center;border-radius:var(--radius);display:flex;flex-direction:row;flex-wrap:nowrap;gap:.5rem;padding:.25rem}.listitem.selected{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.listitem.active{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.item-actions,.item-avatar,.item-icon,.item-trailing,.item-trailing-icon{flex:none}.item-icon.icon-EMPTY{display:none}.item-subtitle{font-size:.75rem;line-height:1rem;opacity:.8}.item-content,.item-text{flex:1 1 auto;min-width:0}.item-leading,.item-trailing{align-items:center;display:flex;flex:none;gap:.5rem}.item-checkbox{margin-right:-.5rem}.item-content{align-items:center;display:flex;gap:.5rem}.multiline>.item-content{align-items:flex-start;flex-direction:column;gap:.25rem}.list{list-style-type:none;padding-left:0}.list-heading-inner .item-title{font-weight:700;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.list.is-nested-sub{padding-left:var(--list-indent)}.is-nested-collapsed{display:none}.nested-toggle-icon{align-items:center;border-radius:var(--radius);display:flex;height:1rem;justify-content:center;margin-right:-.25rem;width:1rem}.magnifier{display:inline-block;height:1em;position:relative;width:1em}.magnifier:after,.magnifier:before{border-radius:var(--radius-full);content:"";display:block;position:absolute}.magnifier:before{border:1px solid;height:.75em;width:.75em}.magnifier:after{height:1px;left:.55em;top:.7em;width:.5em;--tw-rotate:45deg;background-color:currentColor;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))}.close{aspect-ratio:1/1;display:block;position:relative;width:1em}.close:after,.close:before{content:"";display:block;height:1px;left:0;position:absolute;width:100%;--tw-rotate:45deg;background-color:currentColor;border-radius:var(--radius-full);top:calc(50% - .5px)}.close:after,.close:before{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))}.close:after{--tw-rotate:-45deg}.search-box.compact.has-prefix-icon{--input-control-prefix:24px}.search-box.compact.has-suffix-icon{--input-control-suffix:24px}:root{--menu-bg:var(--color-canvas);--menu-active-bg:var(--color-primary-500);--menu-active-color:var(--color-canvas);--menu-selected-bg:var(--color-primary-50);--menu-selected-color:var(--color-primary-500);--menu-icon-opacity:.5;--menu-icon-margin:1.75rem;--menu-min-width:3rem;--menu-heading-color:var(--color-gray-500)}.menu{background:var(--menu-bg);max-width:var(--menu-max-width);min-width:var(--menu-min-width);padding:.5rem}.menu-wrapper{display:flex;flex-direction:column;min-height:0}.menu-wrapper>.menu{flex:1 1 auto;min-height:0;overflow:auto}.menu-item>.item-inner,.menu-item>a{align-items:center;border-radius:var(--radius);color:inherit;cursor:pointer;display:flex;gap:.5rem;justify-content:space-between;overflow:hidden;padding:.25rem .5rem;position:relative;text-overflow:ellipsis;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);white-space:nowrap}.menu-item>.selected{background:var(--menu-selected-bg);color:var(--menu-selected-color)}.menu-item>.active,.menu-item>a.hover,.menu-item>a:hover{background:var(--menu-active-bg);color:var(--menu-active-color)}.menu-item.disabled>a:hover,.menu-item>a.disabled:hover{background-color:inherit;color:inherit}.menu-item>a>.item-title{flex:1 1 auto}.menu>.divider{margin:.5rem .25rem}.menu-heading{align-items:center;color:var(--menu-heading-color);display:flex;font-size:.75rem;font-weight:700;height:2rem;line-height:1rem;padding-left:.25rem;padding-right:.25rem}.has-icons>.menu-item>a>.item-icon{align-items:center;display:flex;height:1rem;justify-content:center;opacity:var(--menu-icon-opacity);width:1rem}.menu-nested{--menu-indent:10px}.menu-nested .menu{background-color:transparent;margin-bottom:-.5rem;margin-left:var(--menu-indent);margin-top:-.5rem;padding-right:0}.menu-nested .menu-item>.item-inner{padding-left:0}.menu.compact .menu-item>a{padding-bottom:.125rem;padding-top:.125rem}.menu.compact .menu-heading{height:1.75rem}.search-menu-header{padding:.5rem .5rem .125rem}.search-menu-footer{padding:.125rem .5rem .5rem}.search-menu .item.is-not-match,.search-menu-footer:empty,.search-menu-header:empty{display:none}.search-menu .item.is-not-match.has-match-child{display:list-item}.search-menu .item.is-not-match.has-match-child>.item-inner{opacity:.5;pointer-events:none}.search-menu.no-toggle-on-search .nested-toggle-icon{pointer-events:none}.search-menu .is-match-keys{font-weight:700;text-decoration-line:underline}.search-menu-no-match-hint{display:none;font-size:.75rem;line-height:1rem;padding:.5rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.no-match-child .search-menu-no-match-hint{display:block}.no-match-child>.menu{display:none}:root{--alert-bg:var(--color-surface)}.alert{align-items:center;background-color:var(--alert-bg);border-radius:var(--radius);display:flex;gap:.75rem;padding:.5rem .75rem}.alert-content{display:flex;flex:1 1 auto;flex-direction:column;gap:1rem}.alert-close{margin-bottom:-.375rem;margin-right:-.5rem;margin-top:-.375rem}.alert-link{color:inherit;font-weight:700}.alert-link:hover{color:inherit;text-decoration-line:underline}.alert-heading{font-weight:700;margin:0}.alert-text{flex:1 1 auto;white-space:pre-line}.alert-actions{align-items:center;display:flex;gap:.5rem}.close{aspect-ratio:1/1;display:block;position:relative;width:1em}.close:after,.close:before{content:"";display:block;height:1px;left:0;position:absolute;width:100%;--tw-rotate:45deg;background-color:currentColor;border-radius:var(--radius-full);top:calc(50% - .5px)}.close:after,.close:before{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))}.close:after{--tw-rotate:-45deg}.messagers{align-items:flex-end;bottom:0;display:flex;flex-direction:column;gap:.5rem;justify-content:flex-end;left:0;padding:1rem;pointer-events:none;position:fixed;right:0;top:0;z-index:1900}.messagers-top-left,.messagers-top-right{justify-content:flex-start}.messagers-bottom-left,.messagers-top-left{align-items:flex-start}.messagers-bottom,.messagers-center,.messagers-top{align-items:center}.messagers-top,.messagers-top-left,.messagers-top-right{justify-content:flex-start}.messagers-center{justify-content:center}.messager{background-color:rgba(var(--color-inverse-rgb),.8);pointer-events:auto;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity));--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);transition-duration:.3s}:root{--progress-radius:var(--radius);--progress-striped-size:40px;--progress-bg:var(--color-surface);--progress-bar-color:var(--color-primary-500)}.progress{background:var(--progress-bg);border-radius:var(--progress-radius);display:flex;height:1.25rem}.progress-bar{background:var(--progress-bar-color);height:100%}.progress-striped>.progress-bar{background-image:linear-gradient(45deg,rgba(var(--color-canvas-rgb),.15) 25%,transparent 25%,transparent 50%,rgba(var(--color-canvas-rgb),.15) 50%,rgba(var(--color-canvas-rgb),.15) 75%,transparent 75%,transparent);background-size:var(--progress-striped-size) var(--progress-striped-size)}.progress>.progress-bar:first-child{border-bottom-left-radius:inherit;border-top-left-radius:inherit}.progress>.progress-bar:last-child{border-bottom-right-radius:inherit;border-top-right-radius:inherit}.progress.active .progress-bar{animation:progress-bar-stripes 2s linear infinite}@keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}:root{--scrollbar-size:10px;--scrollbar-opacity:.6;--scrollbar-bg:rgba(var(--color-inverse-rgb),.15);--scrollbar-inset:inset 0 0 0 1px rgba(var(--color-inverse-rgb),.05);--scrollbar-bar-bg:rgba(var(--color-inverse-rgb),.3);--scrollbar-hover-bg:rgba(var(--color-inverse-rgb),.4);--scrollbar-drag-bg:rgba(var(--color-inverse-rgb),.5);--scrollbar-radius:var(--radius-sm);--scrollbar-duration:.7s}::-webkit-scrollbar{height:var(--scrollbar-size);width:var(--scrollbar-size)}::-webkit-scrollbar-track{box-shadow:var(--scrollbar-shadow);-webkit-transition:background-color var(--scrollbar-duration);transition:background-color var(--scrollbar-duration)}::-webkit-scrollbar-track:hover,:hover::-webkit-scrollbar-track{background:var(--scrollbar-bg)}::-webkit-scrollbar-thumb{background:var(--scrollbar-bar-bg);border-radius:var(--scrollbar-radius);min-height:var(--scrollbar-size);-webkit-transition:var(--scrollbar-duration);transition:var(--scrollbar-duration);-webkit-transition-property:background-color;transition-property:background-color}::-webkit-scrollbar-thumb:hover{background:var(--scrollbar-hover-bg)}::-webkit-scrollbar-thumb:active{background:var(--scrollbar-drag-bg)}@-moz-document url-prefix(){.scrollbar-thin{scrollbar-width:thin}}.scrollbar-thin::-webkit-scrollbar{height:calc(var(--scrollbar-size)/2);width:calc(var(--scrollbar-size)/2)}.scrollbar-hover::-webkit-scrollbar,.scrollbar-hover::-webkit-scrollbar-thumb,.scrollbar-hover::-webkit-scrollbar-track{visibility:hidden}.scrollbar-hover:hover::-webkit-scrollbar,.scrollbar-hover:hover::-webkit-scrollbar-thumb,.scrollbar-hover:hover::-webkit-scrollbar-track{visibility:visible}@font-face{font-family:ZentaoIcon;font-style:normal;font-weight:400;src:url(./icons/ZentaoIcon.eot?v=1.65);src:url(./icons/ZentaoIcon.woff?v=1.65) format("woff"),url(./icons/ZentaoIcon.ttf?v=1.65) format("truetype"),url(./icons/ZentaoIcon.svg?v=1.65#regular) format("svg")}.icon,[class*=" icon-"],[class^=icon-]{display:inline-block;font-display:block;font-family:ZentaoIcon;font-size:14px;font-style:normal;font-variant:normal;font-weight:400;line-height:1;text-transform:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon:before{display:inline-block;min-width:14px;text-align:center}.icon-xs:before{font-size:.7em}.icon-sm:before{font-size:.8em}.icon-lg:before{font-size:1.25em;vertical-align:-10%}.icon-2x{font-size:28px}.icon-3x{font-size:42px}.icon-4x{font-size:56px}.icon-5x{font-size:70px}.icon-run{position:relative}.icon-run:before{opacity:.5}.icon-run:after{bottom:0;content:"\ea07";display:block;left:0;position:absolute;right:0;text-align:center;top:0}.icon-guihua:before{content:"\ea02"}.icon-memory:before{content:"\e9fd"}.icon-cpu:before{content:"\e9fe"}.icon-pause-pure:before{content:"\e9ff"}.icon-exclamation-pure:before{content:"\ea00"}.icon-zentao:before{content:"\e901"}.icon-zentao-alt:before{content:"\e900"}.icon-safe:before{content:"\ea06"}.icon-ai:before{content:"\e983"}.icon-kanban:before{content:"\e997"}.icon-college:before{content:"\e9c8"}.icon-home:before,.icon-menu-my:before{content:"\e97a"}.icon-program:before{content:"\e9aa"}.icon-lightbulb-alt:before,.icon-product:before{content:"\e98f"}.icon-project:before,.icon-rocket:before{content:"\e99c"}.icon-run:before{content:"\e9a9"}.icon-run-alt:before{content:"\ea07"}.icon-test:before{content:"\e956"}.icon-devops:before,.icon-infinite:before{content:"\e9a3"}.icon-doc:before,.icon-menu-doc:before{content:"\e99b"}.icon-statistic:before{content:"\e999"}.icon-group:before,.icon-menu-users:before,.icon-persons:before,.icon-team:before{content:"\e97e"}.icon-assets:before,.icon-diamond:before{content:"\e9ae"}.icon-flow:before{content:"\e994"}.icon-backend:before,.icon-cog-outline:before{content:"\ea05"}.icon-help:before{content:"\e968"}.icon-download:before,.icon-import:before{content:"\e904"}.icon-export:before{content:"\e905"}.icon-lightbulb:before{content:"\e91c"}.icon-close:before{content:"\e936"}.icon-check:before{content:"\e5ca"}.icon-minus:before{content:"\e926"}.icon-expand-alt:before{content:"\e6f1"}.icon-collapse-alt:before{content:"\e6f2"}.icon-fullscreen:before{content:"\e96b"}.icon-star-empty:before{content:"\e94a"}.icon-star:before{content:"\e94b"}.icon-exclamation-sign:before{content:"\e930"}.icon-info-sign:before{content:"\e9d5"}.icon-flag:before{content:"\e937"}.icon-check-circle:before{content:"\e92f"}.icon-check-sign:before{content:"\e938"}.icon-chart-pie:before{content:"\e95b"}.icon-history:before{content:"\e95f"}.icon-pencil:before{content:"\e254"}.icon-search:before{content:"\e928"}.icon-restart:before{content:"\e95e"}.icon-cog:before{content:"\e93b"}.icon-chart-line:before{content:"\e95c"}.icon-bar-chart:before,.icon-chart-bar:before{content:"\e95d"}.icon-exchange:before{content:"\e927"}.icon-severity:before{content:"\e973"}.icon-book:before{content:"\f02d"}.icon-treemap-alt:before{content:"\e971"}.icon-severity-solid:before{content:"\e902"}.icon-chat-line:before{content:"\e998"}.icon-stack:before{content:"\e943"}.icon-cube:before{content:"\e967"}.icon-minus-sign:before{content:"\e939"}.icon-bars-sign:before{content:"\e93a"}.icon-chat:before,.icon-message:before{content:"\e940"}.icon-more:before{content:"\e744"}.icon-certificate:before{content:"\f0a3"}.icon-bell:before{content:"\e7f5"}.icon-columns:before{content:"\f0db"}.icon-envelope-o:before{content:"\e92a"}.icon-unfold-all:before{content:"\e931"}.icon-fold-all:before{content:"\e932"}.icon-bars:before{content:"\e948"}.icon-cards-view:before{content:"\e949"}.icon-ellipsis-v:before{content:"\e5d4"}.icon-spinner-indicator:before{content:"\e982"}.icon-up-circle:before{content:"\e92b"}.icon-right-circle:before{content:"\e92c"}.icon-down-circle:before{content:"\e92d"}.icon-left-circle:before{content:"\e92e"}.icon-angle-double-right:before{content:"\f101"}.icon-angle-down:before{content:"\e313"}.icon-angle-left:before{content:"\e314"}.icon-angle-right:before{content:"\e315"}.icon-angle-top:before{content:"\e316"}.icon-first-page:before{content:"\e5dc"}.icon-last-page:before{content:"\e5dd"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-arrow-up:before{content:"\e923"}.icon-arrow-down:before{content:"\e924"}.icon-arrow-left:before{content:"\e952"}.icon-arrow-right:before{content:"\e93e"}.icon-chevron-left:before{content:"\e934"}.icon-chevron-right:before{content:"\e935"}.icon-chevron-double-up:before{content:"\e959"}.icon-chevron-double-down:before{content:"\e95a"}.icon-folder-account:before{content:"\e942"}.icon-folder-move:before{content:"\e960"}.icon-folder-plus:before{content:"\e961"}.icon-folder-upload:before{content:"\e962"}.icon-folder-star:before{content:"\e963"}.icon-folder-edit:before{content:"\e964"}.icon-folder-download:before{content:"\e965"}.icon-folder-outline:before{content:"\e966"}.icon-folder:before{content:"\e944"}.icon-folder-o:before{content:"\e945"}.icon-folder-open-o:before{content:"\e946"}.icon-folder-open:before{content:"\e947"}.icon-color:before{content:"\e93c"}.icon-paper-clip:before{content:"\e93d"}.icon-text:before{content:"\e929"}.icon-share:before{content:"\f064"}.icon-format-list-bulleted:before{content:"\e9a8"}.icon-format-bold:before{content:"\e953"}.icon-format-header-pound:before{content:"\e954"}.icon-format-italic:before{content:"\e955"}.icon-format-list-numbers:before{content:"\e969"}.icon-format-quote-close:before{content:"\e96a"}.icon-image:before{content:"\e96c"}.icon-table-large:before{content:"\e96d"}.icon-productplan:before{content:"\e9fc"}.icon-aiux:before{content:"\e99e"}.icon-qc:before{content:"\e986"}.icon-qc-q:before{content:"\e985"}.icon-qc-c:before{content:"\e987"}.icon-sonarqube:before{content:"\e9ba"}.icon-board:before{content:"\ea15"}.icon-market:before{content:"\ea01"}.icon-ztool:before{content:"\e9c1"}.icon-contacts:before{content:"\e9c3"}.icon-chats:before{content:"\e9c4"}.icon-ops:before{content:"\e903"}.icon-menu-backend:before{content:"\e993"}.icon-feedback:before{content:"\e991"}.icon-oa:before{content:"\e9a1"}.icon-more-circle:before{content:"\e988"}.icon-controls:before{content:"\e995"}.icon-account:before{content:"\e992"}.icon-about:before,.icon-info:before{content:"\e996"}.icon-exclamation:before{content:"\e9fa"}.icon-exit:before{content:"\e99a"}.icon-theme:before{content:"\e9a0"}.icon-globe:before,.icon-lang:before{content:"\f0ac"}.icon-template:before{content:"\e9fb"}.icon-seal:before{content:"\e9f8"}.icon-menu-arrow-left:before{content:"\e9f0"}.icon-menu-arrow-right:before{content:"\e9f1"}.icon-add-directory:before{content:"\e9ef"}.icon-design:before{content:"\e9ec"}.icon-download-alt:before{content:"\e9ea"}.icon-blame:before{content:"\e9e0"}.icon-draft-edit:before{content:"\e9dc"}.icon-sub-review-user:before{content:"\e9db"}.icon-sub-review:before{content:"\e9df"}.icon-ztf:before{content:"\e9dd"}.icon-save:before{content:"\e9d8"}.icon-list-box:before{content:"\e9b4"}.icon-usecase:before{content:"\e99d"}.icon-code:before{content:"\e990"}.icon-summary:before{content:"\e9ad"}.icon-more-alt:before{content:"\e9a7"}.icon-customer:before{content:"\e9d9"}.icon-alarm:before{content:"\ea16"}.icon-treeview:before{content:"\ea0d"}.icon-or:before{content:"\ea0b"}.icon-manager:before{content:"\ea0c"}.icon-target:before{content:"\ea0a"}.icon-ipd:before{content:"\e9f9"}.icon-wiki-lib:before{content:"\e9f5"}.icon-annex-lib:before{content:"\e9f6"}.icon-interface-lib:before{content:"\e9f7"}.icon-has-authority-pack:before{content:"\e9f3"}.icon-without-authority-pack:before{content:"\e9f4"}.icon-data-structure:before{content:"\e9f2"}.icon-waterfallplus:before{content:"\e9ed"}.icon-agileplus:before{content:"\e9ee"}.icon-horn:before{content:"\e9eb"}.icon-resume:before{content:"\e9e9"}.icon-mirror:before{content:"\e9e6"}.icon-remote:before{content:"\e9e7"}.icon-moon:before{content:"\e9e8"}.icon-ticket:before{content:"\e9e1"}.icon-gantt-alt:before{content:"\e9e2"}.icon-tree:before{content:"\e9c9"}.icon-list:before{content:"\e9cb"}.icon-gantt:before{content:"\e9cc"}.icon-group-view:before{content:"\e9cd"}.icon-inherit-space:before{content:"\e9c2"}.icon-card-archive:before{content:"\e9b8"}.icon-col-archive:before{content:"\e9b9"}.icon-col-add-right:before{content:"\e9bb"}.icon-col-add-left:before{content:"\e9bc"}.icon-col-split:before{content:"\e9bd"}.icon-waterfall:before{content:"\e9a4"}.icon-manual:before{content:"\e98d"}.icon-lane:before{content:"\e9b1"}.icon-unpinned:before{content:"\ea08"}.icon-pinned:before{content:"\ea09"}.icon-support-ticket:before{content:"\ea04"}.icon-snap-house:before{content:"\e9e3"}.icon-wrench:before{content:"\e9e4"}.icon-desktop:before{content:"\e9e5"}.icon-back-circle:before{content:"\e9da"}.icon-back:before{content:"\e9d3"}.icon-shield:before{content:"\e9ca"}.icon-meh:before{content:"\e9ce"}.icon-frown:before{content:"\e9cf"}.icon-smile:before{content:"\e9d0"}.icon-unlock-solid:before{content:"\e9d1"}.icon-lock-solid:before{content:"\e9d2"}.icon-ver:before{content:"\e9c6"}.icon-publish:before,.icon-send:before{content:"\e9c7"}.icon-tag:before{content:"\e9be"}.icon-tag-lock:before{content:"\e9bf"}.icon-code-fork:before{content:"\f126"}.icon-branch-lock:before{content:"\e9c0"}.icon-groups:before{content:"\e9af"}.icon-thumbs-up:before{content:"\f087"}.icon-thumbs-down:before{content:"\f088"}.icon-thumbs-up-solid:before{content:"\e9d6"}.icon-thumbs-down-solid:before{content:"\e9d7"}.icon-hash:before,.icon-version:before{content:"\e9ab"}.icon-p-square:before{content:"\e97b"}.icon-video-play:before{content:"\e97f"}.icon-plus-solid-circle:before{content:"\e974"}.icon-minuse-solid-circle:before{content:"\e9b6"}.icon-s:before{content:"\e975"}.icon-c:before{content:"\e976"}.icon-t:before{content:"\e977"}.icon-guide:before{content:"\e978"}.icon-todo:before{content:"\e979"}.icon-plus:before{content:"\e925"}.icon-side-left:before{content:"\e9b3"}.icon-side-right:before{content:"\e9b2"}.icon-fullscreen-exit:before{content:"\e972"}.icon-alert:before{content:"\e99f"}.icon-undo:before{content:"\e93f"}.icon-redo:before{content:"\e9d4"}.icon-swap:before{content:"\e9b0"}.icon-chat-solid:before{content:"\e9b5"}.icon-clock:before{content:"\e97c"}.icon-cost:before{content:"\e97d"}.icon-pencil-alt:before{content:"\e984"}.icon-size-height:before{content:"\e9c5"}.icon-upload-file:before{content:"\ea03"}.icon-file-log:before{content:"\e9de"}.icon-rich-text:before{content:"\e913"}.icon-markdown:before{content:"\e916"}.icon-excel:before{content:"\e933"}.icon-text-link:before{content:"\e94d"}.icon-ppt:before{content:"\e957"}.icon-word:before{content:"\e958"}.icon-doc-lib:before{content:"\e96f"}.icon-file-empty:before,.icon-file:before{content:"\f016"}.icon-file-text:before{content:"\f0f6"}.icon-file-alt:before{content:"\f15b"}.icon-file-text-alt:before{content:"\f15c"}.icon-file-pdf:before{content:"\f1c1"}.icon-file-word:before{content:"\f1c2"}.icon-file-excel:before{content:"\f1c3"}.icon-file-powerpoint:before{content:"\f1c4"}.icon-file-image:before{content:"\f1c5"}.icon-file-archive:before{content:"\f1c6"}.icon-file-audio:before{content:"\f1c7"}.icon-file-video:before{content:"\f1c8"}.icon-file-code:before{content:"\f1c9"}.icon-cell-input:before{content:"\ea0e"}.icon-input:before{content:"\ea0f"}.icon-checkbox:before{content:"\ea10"}.icon-radio:before{content:"\ea11"}.icon-transition:before{content:"\ea12"}.icon-add-sub-chapter:before{content:"\ea13"}.icon-add-chapter:before{content:"\ea14"}.icon-menu-collapse:before{content:"\e980"}.icon-menu-expand:before{content:"\e981"}.icon-estimate:before{content:"\e9ac"}.icon-sprint:before{content:"\e9a2"}.icon-shield-check:before{content:"\e9a5"}.icon-ok:before{content:"\e9a6"}.icon-printer:before{content:"\e906"}.icon-bullhorn:before{content:"\e910"}.icon-person:before{content:"\e941"}.icon-fields:before{content:"\e989"}.icon-trigger:before{content:"\e98a"}.icon-layout:before{content:"\e98b"}.icon-audit:before{content:"\e98c"}.icon-ban-circle:before,.icon-cancel:before{content:"\e951"}.icon-eye:before{content:"\e94e"}.icon-eye-off:before{content:"\e96e"}.icon-unlock:before{content:"\e94f"}.icon-lock:before,.icon-private:before{content:"\e950"}.icon-move:before{content:"\e94c"}.icon-hand-right:before{content:"\e907"}.icon-checked:before{content:"\e908"}.icon-off:before{content:"\e909"}.icon-play:before,.icon-start:before{content:"\e90a"}.icon-time:before{content:"\e90b"}.icon-edit:before{content:"\e90c"}.icon-trash:before{content:"\e90d"}.icon-link:before{content:"\e90e"}.icon-unlink:before{content:"\e90f"}.icon-bug:before{content:"\e911"}.icon-list-alt:before{content:"\e912"}.icon-alter:before,.icon-change:before{content:"\e970"}.icon-glasses:before,.icon-review:before{content:"\e914"}.icon-sitemap:before,.icon-testcase:before{content:"\e915"}.icon-pluses:before{content:"\e917"}.icon-report-list:before{content:"\e918"}.icon-active:before,.icon-magic:before{content:"\e919"}.icon-treemap:before{content:"\e91a"}.icon-confirm:before{content:"\e91b"}.icon-split:before{content:"\e98e"}.icon-calendar:before,.icon-delay:before{content:"\e91d"}.icon-pause:before{content:"\e91e"}.icon-ban:before{content:"\e91f"}.icon-plus-bold:before{content:"\e920"}.icon-copy:before{content:"\e921"}.icon-refresh:before{content:"\e922"}.icon-diff:before{content:"\e9b7"}.status-active,.status-asked,.status-normal,.status-unconfirmed,.status-wait{color:#313c52}.status-active.status-issue,.status-checking,.status-commenting,.status-confirmed,.status-doing{color:#ff6f42}.status-blocked,.status-hangup,.status-pause,.status-suspended{color:#b89664}.status-clarify,.status-draft{color:#8166ee}.status-noreview,.status-reviewing,.status-wait.status-testcase{color:#18a6fd}.status-active.status-risk,.status-changed,.status-changing,.status-fail{color:#fb2b2b}.status-checked,.status-done,.status-pass,.status-replied,.status-resolved,.status-success{color:#0dbb7d}.status-cancel,.status-canceled,.status-investigate{color:#838a9d}.status-canceled,.status-closed,.status-testtask.status-done{color:#9ea3b0}.pri,[class*=" pri-"],[class^=pri-]{border-color:currentColor;border-radius:var(--radius-full);border-width:1px;display:inline-block;font-size:.75rem;height:1rem;line-height:1rem;line-height:14px;max-width:54px;min-width:16px;overflow:hidden;padding-left:.125rem;padding-right:.125rem;text-align:center;white-space:nowrap;--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.pri-1{color:rgb(255 73 18/var(--tw-text-opacity))}.pri-1,.pri-2{--tw-text-opacity:1}.pri-2{color:rgba(var(--color-warning-600-rgb),var(--tw-text-opacity))}.pri-3{color:rgb(59 130 246/var(--tw-text-opacity))}.pri-3,.pri-4{--tw-text-opacity:1}.pri-4{color:rgb(149 211 224/var(--tw-text-opacity))}.severity{display:inline-block;font-size:.75rem;height:1rem;line-height:1rem;line-height:14px;max-width:54px;min-width:16px;overflow:hidden;padding-left:.125rem;padding-right:.125rem;position:relative;text-align:center;white-space:nowrap;--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.severity:before{background-color:currentColor;background-repeat:no-repeat;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;--tw-content:attr(data-severity);content:var(--tw-content);-webkit-mask:url('data:image/svg+xml;charset=utf-8,');mask:url('data:image/svg+xml;charset=utf-8,');-webkit-mask-size:100% 100%;mask-size:100% 100%}.severity:after,.severity:before{display:inline-block}.severity:after{--tw-scale-x:.75;--tw-scale-y:.75;content:attr(data-severity);line-height:20px;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))}.severity[data-severity="1"]{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.severity[data-severity="2"]{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity))}.severity[data-severity="3"]{--tw-text-opacity:1;color:rgb(250 174 26/var(--tw-text-opacity))}.severity[data-severity="4"]{--tw-text-opacity:1;color:rgb(184 150 100/var(--tw-text-opacity))}.severity[data-severity="5"]{--tw-text-opacity:1;color:rgb(158 163 176/var(--tw-text-opacity))}.severity-label{background-color:transparent}.severity-label:after,.severity-label:before{--tw-content:none;content:var(--tw-content)}.risk-high{color:rgb(220 38 38/var(--tw-text-opacity))}.risk-high,.risk-middle{--tw-text-opacity:1}.risk-middle{color:rgb(234 179 8/var(--tw-text-opacity))}.nav-feature{--nav-active-bg:var(--color-primary-100);--nav-active-color:var(--color-fore)}.nav-feature .nav-checkbox>.checkbox,.nav-feature .nav-checkbox>.checkbox-primary,.nav-feature .nav-item>a{align-items:center;border-radius:var(--radius);height:2rem;padding-left:.625rem;padding-right:.625rem}.nav-feature .nav-item>a>.icon{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.sortable-list .listitem{cursor:move;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding-left:.5rem;padding-right:.5rem}.sortable-list .sortable-ghost>.listitem{--tw-bg-opacity:1;background-color:rgba(var(--color-warning-50-rgb),var(--tw-bg-opacity))}.sortable-empty-shadow{height:1px!important;left:0!important;opacity:0!important;pointer-events:none!important;position:absolute!important;top:0!important;width:1px!important}:root{--avatar-radius:12.5%;--avatar-bg:var(--color-surface)}.avatar{align-items:center;aspect-ratio:1/1;background:var(--avatar-bg);border-radius:var(--avatar-radius);display:inline-flex;justify-content:center;overflow:hidden;width:2rem}.avatar>img{height:100%;margin:0;-o-object-fit:cover;object-fit:cover;width:100%}.avatar.size-xs{width:1.25rem}.avatar.size-sm,.avatar.size-xs{font-size:.75rem;line-height:1rem}.avatar.size-sm{width:1.5rem}.avatar.size-lg{font-size:1.5rem;line-height:2rem;width:3rem}.avatar.size-xl{font-size:2.25rem;line-height:2.5rem;width:5rem}.avatar-group{display:flex;gap:.625rem}.avatar-group>.avatar{box-shadow:0 0 0 1px var(--color-canvas)}.avatar-group>*+*{margin-left:-1rem}.avatar-group.size-xs>*+*{margin-left:-.75rem}.avatar-group.size-sm>*+*{margin-left:-.875rem}.avatar-group.size-lg>*+*{margin-left:-1.25rem}.avatar-group.size-xl>*+*{margin-left:-1.5rem}.btn-group{display:flex;gap:1px}.btn-group>.btn.active{z-index:1}.btn-group>.btn:focus-within,.btn-group>.btn:hover{z-index:2}.btn-group>.btn.disabled,.btn-group>.btn:disabled{z-index:0}.btn-group>.btn:not(:first-child){border-bottom-left-radius:var(--radius-none);border-left-color:rgba(var(--color-canvas-rgb),.2);border-left-width:1px;border-top-left-radius:var(--radius-none)}.btn-group>.btn:not(:first-child):before{left:-1px}.btn-group>.btn:not(:last-child){border-bottom-right-radius:var(--radius-none);border-top-right-radius:var(--radius-none)}.color-picker-heading{align-items:center;font-weight:700;justify-content:space-between;margin-bottom:-.25rem;padding:.5rem}.color-picker-heading,.color-picker-row{display:flex;flex-direction:row}.color-picker-row{flex-wrap:wrap;gap:.5rem;padding:.75rem}.color-picker-item{align-items:center;border-radius:var(--radius-full);display:flex;height:1.25rem;justify-content:center;padding-left:.125rem;padding-right:.125rem;width:1.25rem;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.pick-container{left:0;pointer-events:none;position:absolute;top:0;z-index:1600}.pick-pop{display:flex;flex-direction:column;opacity:0;pointer-events:auto;position:absolute;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);z-index:1200}.pick-pop.in{opacity:1}.date-picker-menu{--menu-bg:none}.date-picker-menu .mini-calendar{margin-top:-.75rem}.date-pick-menu-month,.date-pick-menu-years{display:flex;flex-direction:row;flex-wrap:wrap;gap:1px;max-height:237px;padding-bottom:.5rem;padding-left:.5rem;padding-right:.5rem}.date-pick-menu-years>.btn{width:2.5rem}.date-pick-menu-years>.btn.is-current{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.date-pick-menu-month>.btn.active,.date-pick-menu-years>.btn.active{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.date-pick-menu-month>.btn{width:3rem}.date-picker-menu-footer{border-top-width:1px;display:flex;flex-direction:row;padding:.25rem}.date-picker-menu-footer>.toolbar{flex:1 1 auto;justify-content:center}.date-picker-menu>.menu{border-right-width:1px}.date-picker-menu .menu-item{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.time-picker-menu{max-height:inherit}.time-picker-menu .menu-item>a{justify-content:center}.datetime-picker-menu{max-height:inherit}.mini-calendar{padding:.5rem}.mini-calendar>.row>.col{align-items:center;justify-content:center;width:14.2857%}.mini-calendar-day{height:2rem}.mini-calendar-day.disabled{pointer-events:none;--tw-bg-opacity:1;background-color:rgba(var(--color-gray-200-rgb),var(--tw-bg-opacity));opacity:.5}.mini-calendar-day>.btn{width:2rem}.mini-calendar-day.is-weekend>.btn{--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.mini-calendar-day.active>.btn,.mini-calendar-day.is-today>.btn{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.mini-calendar-day.selected>.btn{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.mini-calendar-day.is-out-month{opacity:.5}.i-time{border-color:currentColor;border-radius:50%;border-width:.08em;display:inline-block;height:1em;position:relative;width:1em}.i-time:after{border-color:currentColor;border-width:0 0 .08em .08em;height:.385em;left:.3846em;position:absolute;top:.08em;width:.385em;--tw-content:"";content:var(--tw-content)}.chevron-down,.chevron-left,.chevron-right,.chevron-up{align-items:center;display:inline-flex;height:1em;justify-content:center;position:relative;width:1em}.chevron-down:before,.chevron-left:before,.chevron-right:before,.chevron-up:before{display:block;height:.6923077em;transform-origin:center;width:.6923077em;--tw-translate-y:-0.125rem;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.chevron-left:before{--tw-translate-y:0px;--tw-translate-x:0.125rem;--tw-rotate:135deg}.chevron-left:before,.chevron-right:before{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))}.chevron-right:before{--tw-translate-y:0px;--tw-translate-x:-0.125rem;--tw-rotate:-45deg}.chevron-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}.i-calendar{border-radius:2px;display:inline-block;height:.79em;margin-top:.16em;position:relative;width:1em}.i-calendar,.i-calendar:after,.i-calendar:before{border-color:currentColor;border-width:.08em}.i-calendar:after,.i-calendar:before{position:absolute;--tw-content:"";content:var(--tw-content)}.i-calendar:before{border-bottom-width:0;left:.15em;top:.307em;width:.6em}.i-calendar:after{border-bottom-width:0;border-top-width:0;height:.3077em;left:.17em;top:-.154em;width:.538em}:root{--modal-radius:var(--radius-lg);--modal-bg:rgba(var(--color-inverse-rgb),.4);--modal-sm:37.5rem;--modal-base:50rem;--modal-lg:75rem;--modal-xl:90rem}.modal{align-items:center;background-color:var(--modal-bg);bottom:0;display:none;justify-content:center;left:0;overflow:hidden;position:fixed;right:0;top:0;z-index:1500}.modal.load-indicator:before{background-color:transparent;transition-property:none}.modal.loading:after{--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.modal.loading .modal-dialog{opacity:0!important}.modal.show{display:flex}.modal-no-backdrop{background-color:transparent;pointer-events:none}.modal-hide{opacity:0!important}.modal-dialog{background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));pointer-events:auto;position:relative;--tw-bg-opacity:0.9;--tw-shadow:0 10px 50px -10px rgba(0,0,0,.4),0 8px 20px -10px rgba(0,0,0,.3);--tw-shadow-colored:0 10px 50px -10px var(--tw-shadow-color),0 8px 20px -10px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);border-radius:var(--modal-radius);width:var(--modal-base)}.modal-content,.modal-dialog{max-height:100vh}.modal-content{display:flex;flex-direction:column;height:100%}.loading>.modal-dialog>.modal-content{opacity:0;visibility:hidden}.close{aspect-ratio:1/1;display:block;position:relative;width:1em}.close:after,.close:before{content:"";display:block;height:1px;left:0;position:absolute;width:100%;--tw-rotate:45deg;background-color:currentColor;border-radius:var(--radius-full);top:calc(50% - .5px)}.close:after,.close:before{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))}.close:after{--tw-rotate:-45deg}.modal-header{align-items:center;display:flex;flex:none;flex-wrap:nowrap;gap:1rem;padding:1rem 1.25rem;position:relative}.modal-title{flex:1 1 auto;font-size:1rem;font-weight:700;line-height:1.5rem}.modal-actions{position:absolute;right:.75rem;top:.75rem;z-index:15}.modal-body{flex:1 1 auto;overflow:auto;padding:1rem 1.25rem}.modal-footer{align-items:center;display:flex;flex:none;gap:.75rem;padding:1.25rem}.modal-dialog.size-full,.modal-dialog[data-size=full]{height:100vh;width:100vw}.modal-dialog.size-sm,.modal-dialog[data-size=sm]{width:var(--modal-sm)}.modal-dialog.size-lg,.modal-dialog[data-size=lg]{width:var(--modal-lg)}.modal-dialog.size-xl,.modal-dialog[data-size=xl]{width:var(--modal-xl)}.modal-iframe{flex:1 1 auto;width:100%}.modal-trans{opacity:0;transition-duration:.2s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.modal-trans.in{opacity:1}.modal-trans>.modal-dialog{transition-property:transform,opacity;--tw-scale-x:.95;--tw-scale-y:.95;opacity:0;transition-duration:.3s}.modal-trans.in>.modal-dialog,.modal-trans>.modal-dialog{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))}.modal-trans.in>.modal-dialog{--tw-scale-x:1;--tw-scale-y:1;opacity:1}.modal-alert .modal-footer{justify-content:flex-end}.modal-alert .modal-header{margin-bottom:-1rem}.modal-alert .modal-body{overflow-wrap:break-word;white-space:pre-line}.modal-alert .modal-actions+.modal-body{padding-right:3rem}.modal-alert .modal-header+.modal-actions+.modal-body{padding-right:1.25rem}.file-selector{gap:.5rem;width:100%}.file-selector,.file-selector-box{display:flex;flex-direction:column}.file-selector-box{border-style:dashed;border-width:1px;gap:0;height:auto;justify-content:center;position:relative;--tw-border-opacity:1;background-color:rgba(var(--color-primary-50-rgb),.3);border-color:rgba(var(--color-primary-400-rgb),var(--tw-border-opacity));padding:.5rem .75rem;--tw-ring-color:transparent;--state-color:rgba(var(--color-primary-500-rgb),.05)}.file-selector-box .text{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.is-dragging .file-selector-box{border-style:solid}.file-selector-btn{align-items:center;display:flex;gap:.5rem;position:relative}.is-dragging .file-selector-btn{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.file-selector-input{-webkit-appearance:none;-moz-appearance:none;appearance:none;bottom:0;cursor:pointer;left:0;opacity:.1;position:absolute;right:0;top:0}.file-selector-tip{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.file-selector.is-mode-button{flex-direction:column-reverse}.file-selector-list.is-empty{display:none}.file-selector-item .item-avatar{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.file-selector-item .item-subtitle{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.file-selector-item .item-content{flex:none;margin-right:.25rem;max-width:100%}.file-selector-item>.listitem:hover{background-color:rgba(var(--color-gray-500-rgb),.05)}.file-selector-item.is-renaming .item-avatar+.item-content{max-width:calc(100% - 32px)}.file-selector-rename-text{max-width:100%;min-width:100px;position:relative}.file-selector-rename-text>div{overflow:hidden;pointer-events:none;visibility:hidden;white-space:nowrap}.file-selector-rename-input{bottom:0;left:0;position:absolute;right:0;top:0}.file-selector-grid{align-items:flex-start;border-color:transparent;border-radius:var(--radius);border-width:1px;display:flex;flex-direction:row;flex-wrap:wrap;gap:var(--file-selector-grid-gap);--tw-bg-opacity:1;background-color:rgba(var(--color-surface-light-rgb),var(--tw-bg-opacity));padding:.5rem}.file-selector-grid-item{height:var(--file-selector-grid-height)}.file-selector-grid-btn,.file-selector-grid-item{width:var(--file-selector-grid-width)}.file-selector-grid-btn{aspect-ratio:1/1;border-style:dashed;border-width:1px;display:flex;flex-direction:column;gap:0;height:auto;justify-content:center;--tw-border-opacity:1;background-color:rgba(var(--color-primary-50-rgb),.3);border-color:rgba(var(--color-primary-400-rgb),var(--tw-border-opacity));padding:.5rem;--tw-ring-color:transparent;--state-color:rgba(var(--color-primary-500-rgb),.05)}.file-selector-grid-btn .text{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.file-selector-grid-item .listitem{flex-direction:column;height:100%;padding:0;position:relative;width:100%}.file-selector-grid-item .item-avatar{border-radius:var(--radius);border-width:1px;width:100%;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.file-selector-grid-item .item-avatar.has-icon>.icon{font-size:calc(var(--file-selector-grid-width)/2)}.file-selector-grid-item .item-content,.file-selector-grid-item .toolbar{aspect-ratio:1/1;left:0;position:absolute;right:0;top:0;width:100%}.file-selector-grid-item .item-title{bottom:-1.25rem;font-size:.75rem;left:0;line-height:1rem;overflow:hidden;position:absolute;right:1.5rem;white-space:nowrap}.file-selector-grid-item .item-subtitle{border-radius:var(--radius-full);bottom:.25rem;left:.25rem;position:absolute;--tw-bg-opacity:1;background-color:rgba(var(--color-fore-rgb),var(--tw-bg-opacity));font-size:.75rem;line-height:1rem;padding-left:.25rem;padding-right:.25rem;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity));opacity:0;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.file-selector-grid-item:hover .item-subtitle{opacity:1}.file-selector-grid-item .toolbar .btn[data-remove-file]{background-color:rgba(var(--color-inverse-rgb),.5);padding:.375rem;position:absolute;right:1px;top:1px;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.file-selector-grid-item .toolbar .btn[data-rename-file]{bottom:-1.5rem;padding:0;position:absolute;right:0}.file-selector-grid .file-selector-rename{bottom:0;height:1.5rem;position:absolute;top:auto}.file-selector-grid .file-selector-rename-text{width:100%}.is-dragging .file-selector-grid{border-width:1px;--tw-border-opacity:1;border-color:rgba(var(--color-primary-400-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}:root{--nav-radius:var(--radius);--nav-active-color:var(--color-primary-500);--nav-active-bg:var(--color-inherit);--nav-heading-color:var(--color-gray-500);--nav-gap:0.25rem;--nav-item-color:inherit;--nav-item-bg:transparent;--nav-item-padding:1rem;--nav-item-height:2rem;--nav-item-gap:0.75rem;--nav-divider-height:calc(var(--nav-item-height)/2);--nav-divider-margin:0.5rem}.nav{list-style-type:none;padding-left:0}.nav,.nav-item{align-items:center;display:flex;position:relative}.nav-item{gap:var(--nav-item-gap)}.nav-item>a{align-items:center;background-color:var(--nav-item-bg);color:var(--nav-item-color);cursor:pointer;display:flex;gap:var(--nav-gap);height:var(--nav-item-height);justify-content:center;padding-left:var(--nav-item-padding);padding-right:var(--nav-item-padding)}.nav-item>a>.text{white-space:nowrap}.nav-item>.active{font-weight:700;--nav-item-color:var(--nav-active-color);--nav-item-bg:var(--nav-active-bg)}.nav-item>.selected{--nav-item-color:var(--nav-selected-color);--nav-item-bg:var(--nav-selected-bg)}.nav-item>.disabled{--nav-item-color:var(--nav-disabled-color);--nav-item-bg:var(--nav-disabled-bg)}.nav-item>.disabled>*{opacity:var(--opacity-disabled)}.nav-divider,.nav>.divider{align-self:center;height:var(--nav-divider-height);margin-left:var(--nav-divider-margin);margin-right:var(--nav-divider-margin)}.nav-heading{align-items:center;color:var(--nav-heading-color);display:flex;font-weight:700;gap:.25rem;height:2rem;justify-content:center;padding-left:1rem;padding-right:1rem}.nav-space{flex:1 1 auto;width:1rem}.nav-compact{--nav-gap:0.25rem;--nav-item-gap:0.5rem;--nav-item-height:1.75rem;--nav-item-padding:0.5rem;--nav-divider-margin:0.25rem}.nav-primary>.nav-item{position:relative}.nav-primary>.nav-item:hover{z-index:10}.nav-primary>.nav-item+.nav-item{margin-left:-1px}.nav-primary>.nav-item>a{border-radius:var(--radius-none);border-width:1px}.nav-primary>.nav-item:first-child>a,.nav-primary>.nav-item:has(.nav-heading)+.nav-item>a{border-bottom-left-radius:var(--nav-radius);border-top-left-radius:var(--nav-radius)}.nav-primary>.nav-item:last-child>a{border-bottom-right-radius:var(--nav-radius);border-top-right-radius:var(--nav-radius)}.nav-primary>.nav-item>.active{--tw-text-opacity:1;background:var(--nav-active-color);border-color:var(--nav-active-color);color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.nav-primary>.nav-divider{display:none}.nav-secondary>.nav-item>a{border-radius:var(--radius-none);position:relative}.nav-secondary>.nav-item>a:after{bottom:-2px;display:block;height:2px;left:0;position:absolute;right:0;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}.nav-secondary>.nav-item>.active:after{background-color:currentColor}.nav-secondary>.nav-divider{margin:0 0 0 -1px}.nav-tabs>.nav-item>a{border-bottom-left-radius:var(--radius-none);border-bottom-right-radius:var(--radius-none);position:relative}.nav-tabs>.nav-item>a:after{border-color:transparent;border-width:1px;bottom:-1px;display:block;left:-1px;pointer-events:none;position:absolute;right:-1px;top:-1px;--tw-border-opacity:1;border-bottom-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));--tw-content:"";border-top-left-radius:inherit;border-top-right-radius:inherit;content:var(--tw-content)}.nav-tabs>.nav-item>.active:after{--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));border-bottom-color:transparent}.nav-tabs>.nav-divider{margin:0 0 0 -1px}.nav-pills>.nav-item>a{border-radius:var(--radius-full)}.nav-pills>.nav-item>.active{--tw-text-opacity:1;background:var(--nav-active-color);border-color:var(--nav-active-color);color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.nav-steps{--nav-item-bg:var(--color-canvas);--nav-active-bg:var(--color-primary-500);--nav-active-color:var(--color-canvas);--nav-selected-bg:var(--color-secondary-500);--nav-selected-color:var(--color-canvas)}.nav-steps>.nav-item+.nav-item>a{padding-left:calc(var(--nav-item-padding) + var(--nav-item-height)/3)}.nav-steps>.nav-item>a:after,.nav-steps>.nav-item>a:before{border-style:solid;height:0;left:100%;position:absolute;width:0;z-index:2;--tw-content:"";border-color:transparent;border-left-color:var(--nav-item-bg,var(--color-canvas));border-width:calc(var(--nav-item-height)/2);border-left-width:calc(var(--nav-item-height)/3);border-right-width:0;content:var(--tw-content)}.nav-steps>.nav-item>a:before{background-color:transparent!important;border-left-color:var(--color-border);bottom:auto;left:calc(100% + 5.5px);opacity:1!important;right:auto;top:.4rem;transform:none;z-index:1;--nav-item-height:1.2rem}.nav-steps>.nav-item:has(+.nav-item>.active)>.selected:before,.nav-steps>.nav-item:has(+.nav-item>.selected)>.active:before,.nav-steps>.nav-item:last-child>a:after,.nav-steps>.nav-item:last-child>a:before{display:none}.nav-steps>.nav-item>a:hover{--nav-item-bg:var(--color-gray-100)}.nav-steps>.nav-item>.active:hover{--nav-item-bg:var(--color-primary-600)}.nav-steps>.nav-item>.selected:hover{--nav-item-bg:var(--color-secondary-600)}.nav-stacked{flex-direction:column}.nav-stacked>.nav-item{width:100%}.nav-stacked>.nav-heading,.nav-stacked>.nav-item>a{justify-content:flex-start;width:100%}.nav-stacked>.nav-divider{height:1px;margin:.5rem 0;width:100%}.nav-primary.nav-stacked>.nav-item>a{height:2.5rem}.nav-primary.nav-stacked>.nav-item+.nav-item{margin-left:0;margin-top:-1px}.nav-primary.nav-stacked>.nav-item:first-child>a{border-bottom-left-radius:0;border-top-left-radius:var(--nav-radius);border-top-right-radius:var(--nav-radius)}.nav-primary.nav-stacked>.nav-item:last-child>a{border-bottom-left-radius:var(--nav-radius);border-bottom-right-radius:var(--nav-radius);border-top-right-radius:0}.nav-secondary.nav-stacked>.nav-item>a:after{bottom:0;height:100%;left:auto;right:0;top:0;width:2px;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity))}.nav-secondary.nav-stacked>.nav-item>.active:after{background-color:currentColor}.nav-secondary.nav-stacked>.nav-divider{margin:0}.nav-tabs.nav-stacked>.nav-item>a{border-bottom-left-radius:var(--nav-radius);border-bottom-right-radius:var(--radius-none);border-top-right-radius:var(--radius-none);position:relative}.nav-tabs.nav-stacked>.nav-item>a:after{--tw-border-opacity:1;border-bottom-color:transparent;border-bottom-left-radius:inherit;border-right-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));border-top-left-radius:inherit}.nav-tabs.nav-stacked>.nav-item>a.active:after{--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));border-right-color:transparent}.nav-tabs.nav-stacked>.nav-divider{margin:0}.nav-pills.nav-stacked>.nav-item+.nav-item{margin-top:.25rem}.nav-justified>.nav-item:not(.flex-none,.nav-divider){flex:1 1 auto}.nav-justified>.nav-item>a{width:100%}.arrow,.arrow:before{background:inherit;height:calc(var(--arrow-size)*1.4142);position:absolute;width:calc(var(--arrow-size)*1.4142)}.arrow{visibility:hidden;--arrow-size:5px}.arrow:before{border:inherit;content:"";visibility:visible;--tw-rotate:45deg;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))}.arrow-top,.arrow-up{border-bottom:inherit;border-right:inherit;border-width:1px 0 0 1px;top:calc(1px - var(--arrow-size))}.white>.arrow-top,.white>.arrow-up{top:calc(0px - var(--arrow-size))}.arrow-bottom,.arrow-down{border-left:inherit;border-top:inherit;border-width:0 1px 1px 0;bottom:calc(1px - var(--arrow-size))}.white>.arrow-bottom,.white>.arrow-down{bottom:calc(0px - var(--arrow-size))}.arrow-left{border-right:inherit;border-top:inherit;border-width:0 0 1px 1px;left:calc(1px - var(--arrow-size))}.white>.arrow-left{left:calc(0px - var(--arrow-size))}.arrow-right{border-bottom:inherit;border-left:inherit;border-width:1px 1px 0 0;right:calc(1px - var(--arrow-size))}.white>.arrow-right{right:calc(0px - var(--arrow-size))}.popover{display:none;position:absolute;width:-moz-max-content;width:max-content}.popover.show{display:block}.popover-heading{align-items:center;border-top-left-radius:inherit;border-top-right-radius:inherit;display:flex;gap:1rem;height:2.5rem;justify-content:space-between;position:relative;z-index:1;--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));padding-left:.75rem;padding-right:.5rem}.popover-title{flex:1 1 auto;font-weight:700}.popover-content{padding:.75rem;position:relative;z-index:1}.popover-arrow{z-index:0}.has-heading>.popover-arrow.arrow-top,.has-heading>.popover-arrow.arrow-up{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity))}.popover>.btn-close{position:absolute;right:.5rem;top:.625rem;z-index:2}.btn-close+.popover-content{padding-right:3rem}.btn.with-popover-show{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.with-popover-show[data-pop-placement=top]>.caret{--tw-rotate:180deg}.with-popover-show[data-pop-placement=left]>.caret,.with-popover-show[data-pop-placement=top]>.caret{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))}.with-popover-show[data-pop-placement=left]>.caret{--tw-rotate:90deg}.with-popover-show[data-pop-placement=right]>.caret{--tw-rotate:-90deg;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))}.pager{align-items:center;display:flex;gap:.25rem;white-space:nowrap}.menu.pager-size-menu{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));text-align:center}.menu.pager-size-menu>.menu-item>a{margin:.125rem}.pager .pager-goto-group>.form-control{width:3rem}.pager .pager-goto-group>.input-group-addon{border-width:1px;--tw-border-opacity:1;border-color:rgba(var(--color-gray-300-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.pager>.pager-nav.active{--tw-text-opacity:1;box-shadow:0 0 0 1px var(--color-primary-500);color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.panel{border-radius:var(--radius);overflow:hidden;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05}.panel-heading{background-color:var(--panel-heading-bg);flex-wrap:nowrap;gap:1rem;justify-content:space-between;padding:.5rem 1rem}.panel-heading,.panel-title{align-items:center;display:flex}.panel-title{font-weight:700;gap:.5rem;margin:0}.panel-actions{align-items:center;display:flex;margin-right:-.5rem}.panel-body{padding:.75rem 1rem}.panel-footer{align-items:center;background-color:var(--panel-footer-bg);display:flex;gap:.5rem;padding:.5rem 1rem}.size-sm .panel-body,.size-sm .panel-footer,.size-sm .panel-heading{padding:.375rem .75rem}.size-lg .panel-body,.size-lg .panel-footer,.size-lg .panel-heading{padding:1rem 1.25rem}.picker{position:relative}.picker-select{align-items:center;gap:.25rem;justify-content:space-between}.picker-select,.picker-select.form-control{display:flex}.picker-select-placeholder{flex:1 1 auto;--tw-text-opacity:1;color:rgba(var(--color-placeholder-rgb),var(--tw-text-opacity))}.picker-select-multi .picker-select-placeholder{padding-left:.25rem;padding-right:.25rem}.picker-single-selection{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap}.picker-deselect-btn{border-radius:var(--radius-full);padding:.25rem}.picker-deselect-btn:not(:hover){opacity:.7}.picker-select-multi.form-control{height:auto;min-height:32px;padding-left:.25rem}.picker-multi-selections{display:flex;flex-wrap:wrap;gap:.25rem;min-width:0}.picker-multi-selection{align-items:center;border-radius:var(--radius-full);border-width:1px;display:flex;max-width:100%;--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));padding-left:.5rem}.readonly .picker-multi-selection{padding-right:.5rem}.picker-multi-selection>.text{max-width:180px;min-width:0;overflow:clip;white-space:nowrap}.picker-no-search{cursor:default}.picker-menu-list{border-style:none;flex:1 1 auto;min-height:0;overflow-y:auto;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.picker-menu-search+.picker-menu-list.menu{margin-top:-.125rem;padding-top:0}.picker-menu-list.tree{padding:.5rem}.picker-menu .search-menu-footer{border-top-width:1px;padding-bottom:.25rem;padding-top:.25rem}.picker-search-input{border-color:transparent;height:1.75rem;outline-width:0!important;padding-left:0;padding-right:2rem;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)!important;--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)!important;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)!important}.picker-search{flex:none;position:relative;width:100%}.picker-search>.magnifier{opacity:.5;position:absolute;right:0;top:.5rem}.picker-search-clear.btn{cursor:pointer;height:1.25rem;position:absolute;right:0;top:.25rem;width:1.25rem}.picker-search.is-inline{min-width:64px;width:4rem}.is-inline>.picker-search-input{height:1.25rem;min-width:64px;padding-left:.25rem;padding-right:.25rem}.picker-search-measure{height:0;left:0;overflow:hidden;position:absolute;top:0;white-space:nowrap}.split{display:flex;flex-direction:row}.split-vert{flex-direction:column}.split-cell{min-height:0;min-width:0}.has-animation>.split-cell{transition-duration:.15s;transition-property:width,height;transition-timing-function:cubic-bezier(.4,0,.2,1)}.gutter{align-items:stretch;border-radius:var(--radius);cursor:col-resize;display:flex;flex:none;flex-direction:row;justify-content:center;position:relative}.gutter-vert{cursor:row-resize;flex-direction:column}.gutter:before{display:block;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity));transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);--tw-content:"";content:var(--tw-content)}.gutter-horz:before{width:1px}.gutter-vert:before{height:1px}.gutter-horz:hover:before{width:75%}.gutter-vert:hover:before{height:75%}.gutter-resize-handler,.gutter-toggle{align-items:center;border-radius:var(--radius-lg);border-width:1px;display:flex;height:1.75rem;justify-content:center;margin-top:-.875rem;position:absolute;top:50%;width:.75rem;--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity));transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.gutter-toggle>span{--tw-scale-x:.75;--tw-scale-y:.75;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));transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.gutter:hover .gutter-toggle{--tw-border-opacity:1;border-color:rgba(var(--color-gray-500-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.gutter .gutter-toggle:hover{--tw-border-opacity:1;border-color:rgba(var(--color-primary-500-rgb),var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.is-last>.gutter-toggle{--tw-scale-x:-1}.is-first>.gutter-toggle,.is-last>.gutter-toggle{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))}.is-first>.gutter-toggle{--tw-scale-x:1}.gutter-vert .gutter-toggle{left:50%;margin-left:-.375rem;margin-top:0;top:auto;--tw-rotate:90deg}.gutter-vert .gutter-toggle,.gutter.is-next-collapsed>.gutter-toggle{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))}.gutter.is-next-collapsed>.gutter-toggle{--tw-scale-x:1}.gutter.is-prev-collapsed>.gutter-toggle{--tw-scale-x:-1;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))}.gutter-horz.is-first.is-prev-collapsed{transform:translateX(calc(-1px - var(--split-gutter-size)/2))}.gutter-horz.is-last.is-next-collapsed{transform:translateX(calc(1px + var(--split-gutter-size)/2))}.gutter-vert.is-first.is-prev-collapsed{transform:translateY(calc(-1px - var(--split-gutter-size)/2))}.gutter-vert.is-last.is-next-collapsed{transform:translateY(calc(1px + var(--split-gutter-size)/2))}.gutter-resize-handler{background-color:transparent;border-width:0}.gutter-resize-handler:before{border-left-width:1px;border-right-width:1px;bottom:0;left:.25rem;position:absolute;right:.25rem;top:0;--tw-content:"";content:var(--tw-content)}.sidebar{flex:none!important;position:relative;--gutter-width:1px;--sidebar-duration:200ms}.has-sidebar-animation .sidebar{transition-duration:.15s;transition-duration:var(--sidebar-duration);transition-property:width,margin;transition-timing-function:cubic-bezier(.4,0,.2,1)}.sidebar-left.is-collapsed{margin-left:calc(var(--gutter-width)*-1)}.sidebar-right.is-collapsed{margin-right:calc(var(--gutter-width)*-1)}.sidebar.is-collapsed>*,.sidebar.is-expanded.is-animating>*{display:none}.sidebar.is-collapsed>.sidebar-gutter{display:flex!important}.sidebar-gutter{bottom:0;left:100%;position:absolute;top:0;width:var(--gutter-width)}.sidebar-right>.sidebar-gutter{left:auto;right:100%}.is-collapsed>.gutter>.gutter-toggle>span{--tw-rotate:-180deg;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))}.sidebar-gutter:before{border-radius:var(--radius-full);opacity:0}.sidebar-gutter:hover:before{opacity:1;width:.5rem}.is-expanded .sidebar-gutter.gutter-border:before{opacity:1}.is-sidebar-resizing{cursor:col-resize}:root{--table-head-bg:var(--color-surface);--table-striped-color:var(--color-gray-50);--table-hover-color:rgba(var(--color-primary-500-rgb),.05);--table-border-color:var(--color-border)}.table{border-color:var(--table-border-color);width:100%}.table>thead{background-color:var(--table-head-bg)}.table>*>tr{border-bottom-width:1px}.table>*>tr>*{padding:.5rem 1rem}.table-striped>tbody>tr:nth-child(2n){background:var(--table-striped-color)}.table-hover>tbody>tr:hover>*{background:var(--table-hover-color)}.table.bordered>*>tr>*{border-width:1px}.table.borderless>*>tr,.table.borderless>*>tr>*{border-style:none}.condensed>*>tr>*{padding:.375rem .75rem}.table-fixed{table-layout:fixed}.table-fixed>*>tr>*{overflow:hidden;white-space:nowrap}.toolbar{align-items:center;display:flex;position:relative}.toolbar>.divider{margin:.5rem;width:1px;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity))}.toolbar-space{flex:1 1 auto;height:1px;width:1px}.toolbar>.dropdown.show{align-items:center;display:flex;position:relative}:root{--tooltip-bg:var(--skin-color,var(--color-inverse));--tooltip-text:var(--skin-text,var(--color-canvas));--tooltip-arrow:6px;--tooltip-delay:0.5s}.hint,[class*=" hint-"],[class^=hint-]{position:relative}.hint:before,[class*=" hint-"]:before,[class^=hint-]:before{border-style:solid;height:0;position:absolute;width:0;--tw-content:"";border-color:var(--tooltip-bg) transparent transparent transparent;border-width:var(--tooltip-arrow) var(--tooltip-arrow) 0 var(--tooltip-arrow);content:var(--tw-content);margin-bottom:calc(0px - var(--tooltip-arrow))}.hint:after,[class*=" hint-"]:after,[class^=hint-]:after{background-color:var(--tooltip-bg);border-color:var(--tooltip-color);border-radius:var(--radius);color:var(--tooltip-text);font-size:.75rem;line-height:1rem;max-width:28rem;overflow:hidden;padding:.25rem .5rem;position:absolute;white-space:nowrap;--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-content:attr(data-hint);content:var(--tw-content);text-shadow:0 1px 0 var(--tooltip-bg)}.hint:after,.hint:before,[class*=" hint-"]:after,[class*=" hint-"]:before,[class^=hint-]:after,[class^=hint-]:before{bottom:100%;left:50%;opacity:0;pointer-events:none;transition-delay:0s;transition-duration:.15s;transition-property:opacity,visibility;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(.4,0,1,1);visibility:hidden;z-index:1800;--tw-translate-x:-50%;--tw-translate-y:calc(var(--tooltip-arrow)*-1);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))}.hint:hover:after,.hint:hover:before,[class*=" hint-"]:hover:after,[class*=" hint-"]:hover:before,[class^=hint-]:hover:after,[class^=hint-]:hover:before{opacity:1;transition-delay:var(--tooltip-delay);visibility:visible}.hint-bottom:before{border-color:transparent transparent var(--tooltip-bg) transparent;border-width:0 var(--tooltip-arrow) var(--tooltip-arrow) var(--tooltip-arrow);margin-bottom:auto;margin-top:calc(0px - var(--tooltip-arrow))}.hint-bottom:after,.hint-bottom:before{bottom:auto;top:100%;--tw-translate-y:var(--tooltip-arrow);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))}.hint-left:before{border-color:transparent transparent transparent var(--tooltip-bg);border-width:var(--tooltip-arrow) 0 var(--tooltip-arrow) var(--tooltip-arrow);margin-bottom:auto;margin-right:calc(0px - var(--tooltip-arrow))}.hint-left:after,.hint-left:before{bottom:50%;left:auto;right:100%;--tw-translate-x:calc(var(--tooltip-arrow)*-1);--tw-translate-y:50%;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))}.hint-right:before{border-color:transparent var(--tooltip-bg) transparent;border-width:var(--tooltip-arrow) var(--tooltip-arrow) var(--tooltip-arrow) 0;margin-bottom:auto;margin-left:calc(0px - var(--tooltip-arrow))}.hint-right:after,.hint-right:before{bottom:50%;left:100%;--tw-translate-x:var(--tooltip-arrow);--tw-translate-y:50%;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))}.hint-lg:after,.hint-sm -.hint-md:after:after,.hint-xl:after{white-space:normal;word-break:break-all}.hint-sm:after{width:4rem}.hint-md:after{width:8rem}.hint-lg:after{width:16rem}.hint-xl:after{width:24rem}.hint-primary{--tooltip-bg:var(--color-primary-500)}.hint-success{--tooltip-bg:var(--color-success-500)}.hint-danger{--tooltip-bg:var(--color-danger-500)}.hint-warning{--tooltip-bg:var(--color-warning-500)}.hint-important{--tooltip-bg:var(--color-important-500)}.hint-special{--tooltip-bg:var(--color-special-500)}.tooltip{background-color:var(--tooltip-bg);border-color:var(--tooltip-color);border-radius:var(--radius);color:var(--tooltip-text);font-size:.75rem;line-height:1rem;max-width:28rem;padding:.25rem .5rem;z-index:1800}.tooltip:not(.white)>.arrow{border-width:0}.tooltip.white>.arrow{border-color:inherit}.tooltip-has-title{--tw-bg-opacity:0.8;padding:.5rem .75rem .75rem;--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.tooltip-has-title>.arrow{opacity:.75}.tooltip-heading{align-items:center;display:flex;font-size:.8125rem;font-weight:700;gap:1rem;justify-content:space-between;line-height:1.25rem;padding:.25rem}.tooltip-heading>.btn-close{margin-right:-.25rem}.tooltip-content{padding:.25rem}.tree-item-inner{flex-direction:row;min-height:24px;padding-left:var(--list-nested-indent)}.tree-item-inner,.tree-toggle{align-items:center;display:flex}.tree-toggle{border-radius:var(--radius);flex:none;height:1rem;justify-content:center;margin-right:-.375rem;width:1rem;--caret-size:5px;--caret-opacity:.3}.tree-item-inner>.item-icon{opacity:.5;width:1rem}.tree-item-inner:hover>.item-icon{opacity:.8}.tree-item-inner.has-hover{border-radius:var(--radius)}.tree-item-inner .item-title:not(:hover),.tree-item-inner>a.item-title:not(:hover){color:inherit}.tree-item-inner>.toolbar{margin-left:1rem;opacity:.6}.tree-item-inner:hover>.toolbar{opacity:1}.tree-item>.active{background:var(--menu-active-bg);color:var(--menu-active-color)}.tree-item>.selected{background:var(--menu-selected-bg)}.tree-item>.selected .item-content{color:var(--menu-selected-color);font-weight:700}.tree.search-menu-on-top>.search-box{left:0;right:0}.tree-lines .tree{position:relative}.tree-lines .tree-item{--tree-line-indent:calc(var(--list-nested-indent) + 8px)}.tree-lines .tree:before{border-left-width:1px;bottom:.5rem;display:block;left:var(--tree-line-indent);position:absolute;top:-.5rem;--tw-content:"";content:var(--tw-content)}.upload .upload-tip{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.upload .draggable-area{background-color:rgba(var(--color-primary-50-rgb),.2);border:1px dashed var(--color-primary-300)}.upload .draggable-area.dragover{background-color:rgba(var(--color-primary-500-rgb),.1);border-style:solid}.upload .upload-tip{color:var(--color-slate-500)}.file-item>.img{height:120px;width:120px}.upload .file-item:hover .file-size{display:flex!important}.form-batch-container{overflow-y:auto}.form-batch-table{min-width:100%;table-layout:fixed;--table-head-bg:var(--color-canvas)}.form-batch-table>*>tr{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.form-batch-table>*>tr>*{padding-left:.375rem;padding-right:.375rem;vertical-align:top}.form-batch-table>*>tr>th{text-align:left;vertical-align:middle}.form-batch-head[data-name=ACTIONS]{width:5rem}.form-batch-head[data-name=ACTIONS][data-action-count="3"]{width:6rem}.form-batch-label{display:inline}.form-batch-control>textarea.form-control{height:2rem}.form-batch-btn+.btn{margin-left:.25rem}.form-batch-table>tbody[data-count="1"] .form-batch-btn[data-type=delete]{display:none}.form-batch-ditto{position:relative}.form-batch-ditto-btn{bottom:0;height:auto;opacity:.5;padding-left:.5rem;padding-right:.5rem;position:absolute!important;right:0;top:0}.form-batch-ditto-btn:after{bottom:.5rem;left:-1px;position:absolute;top:.5rem;width:1px;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}[data-ditto=on] .form-batch-ditto-btn{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity));opacity:1}.form-batch-row-actions{padding-bottom:.75rem!important;padding-top:.75rem!important}.form-batch-ditto-wrapper{position:relative}.form-batch-ditto-control .form-control{padding-right:calc(var(--form-batch-ditto-width) + 8px)}.form-batch-ditto-control .input-control>.input-control-suffix{right:var(--form-batch-ditto-width)}.form-batch-ditto-control .input-control>.form-control{padding-right:calc(var(--form-batch-ditto-width) + var(--input-control-suffix))}.form-batch-ditto-control .pick-pri{padding-right:calc(var(--form-batch-ditto-width) + 8px)}.dropmenu-btn{gap:.25rem;padding-left:.5rem;padding-right:.5rem}.dropmenu-btn>.text{max-width:180px;overflow:hidden}.dropmenu-btn>.is-leading{left:-.625rem;opacity:.6;position:absolute}.dropmenu-btn>.is-caret{align-items:center;background-color:rgba(var(--color-canvas-rgb),.2);border-radius:var(--radius);display:flex;height:1rem;justify-content:center;width:1rem}.dropmenu-nav{border-bottom-width:1px;margin-left:.75rem;margin-right:.75rem;margin-top:.25rem}.has-search .dropmenu-nav{margin-top:-.5rem}.dropmenu-nav>.nav-secondary{gap:.5rem}.dropmenu-nav>.nav-secondary>.nav-item>a{border-radius:var(--radius);padding-left:.5rem;padding-right:.5rem}.dropmenu-nav>.nav-secondary>.nav-item>a:after{bottom:-1px;left:1rem;opacity:0;right:1rem}.dropmenu-nav>.nav-secondary>.nav-item>.active:after{left:.5rem;opacity:1;right:.5rem;transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dropmenu-title{font-size:.75rem;font-weight:700;line-height:1rem;margin-bottom:-.5rem;padding:.75rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.dropmenu-list{min-height:0;overflow-x:hidden;overflow-y:auto;padding:.5rem .75rem}.dropmenu.loading>*{opacity:0}.dropmenu-item .tree-item-inner{border-radius:var(--radius);cursor:pointer;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dropmenu-item.is-item.active>.tree-item-inner{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.tree-item-inner.is-link:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.tree-item-inner.is-link:hover .item-content,.tree-item-inner.is-link:hover .item-trailing-icon,.tree-item-inner.is-link:hover .label{color:inherit}.tree-item-inner.is-toggle:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity))}.dropmenu-item+.tree-item{margin-top:.125rem}.dropmenu-item .is-match{font-weight:700;text-decoration-line:underline}.dropmenu-item .item-title,.dropmenu-item .label{overflow:hidden;white-space:nowrap}.dropmenu-item .item-content{padding-left:.375rem;padding-right:.375rem}.dropmenu-item *+.item-content{padding-left:0}.tree-item-inner.is-toggle .item-title{opacity:.6}.tree-item-inner.is-toggle:hover .item-title{opacity:1}.dropmenu-item-check{align-items:center;display:flex;flex:1 1 auto;justify-content:flex-end;padding-right:.5rem}.dropmenu-item .nested-toggle-icon.is-empty{width:.25rem}.dropmenu-item .item-icon{margin-right:-.25rem}:root{--form-control-border:var(--color-border-strong)}.checkbox-primary.disabled,.checkbox-primary.disabled>label,.checkbox-primary>input[type=checkbox]:disabled,.checkbox-primary>input[type=checkbox]:disabled+label,.radio-primary.disabled,.radio-primary.disabled>label,.radio-primary>input[type=radio],.radio-primary>input[type=radio]:disabled+label{filter:none!important}.checkbox-primary.disabled>label,.radio-primary.disabled>label{opacity:1}.form-actions{gap:1rem;justify-content:center}.form-actions .btn{min-width:96px}.form-control-static{min-height:32px;padding-bottom:.375rem;padding-top:.375rem}:root{--zt-panel-form-max-width:1000px}.size-sm{--zt-panel-form-max-width:800px}.size-lg{--zt-panel-form-max-width:1200px}.panel-body>.container,.panel-heading>.container{max-width:var(--panel-container-width);min-width:0;padding-left:var(--panel-container-padding);padding-right:var(--panel-container-padding)}.panel-heading>.container{align-items:center;display:flex;flex-direction:row;flex-wrap:nowrap;gap:1rem;justify-content:space-between;min-height:48px}.panel-form{border-radius:var(--radius-md);margin-bottom:1rem;margin-left:auto;margin-right:auto;max-width:var(--zt-panel-form-max-width);min-width:var(--zt-panel-form-min-width);overflow:visible;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding-bottom:1rem;padding-left:1rem;padding-right:1rem;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.panel-form .form-actions{background-color:rgba(var(--color-canvas-rgb),.8);bottom:0;padding-bottom:1rem;padding-top:1rem;position:sticky;z-index:3;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.panel-form .form-grid{margin-left:calc(0px - var(--form-grid-gap-y-half));margin-right:calc(0px - var(--form-grid-gap-y-half));padding-bottom:0}.panel-form-toolbar{padding-left:var(--form-grid-gap-x-half);padding-right:var(--form-grid-gap-x-half);width:100%}.panel-form-toolbar>.toolbar{gap:1rem;padding-bottom:.75rem;padding-top:.75rem}.panel-form-divider{margin-bottom:.75rem;margin-top:1rem;padding-left:var(--form-grid-gap-x-half);padding-right:var(--form-grid-gap-x-half);width:100%}.panel-form-divider:before{background-color:rgba(var(--color-border-rgb),.7);display:block;height:1px;--tw-content:"";content:var(--tw-content)}.is-full .btn-full-form,.is-lite .btn-lite-form{z-index:1;--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-300-rgb),var(--tw-ring-opacity))}.btn-toggle-fold:after{content:attr(data-expand-text)}.show-fold-items .btn-toggle-fold:after{content:attr(data-collapse-text)}.is-foldable,.is-full .full\:hidden,.is-full .lite\:visible,.is-full .panel-form-divider,.is-full .panel-form-toolbar,.is-lite .full\:visible,.is-lite .lite\:hidden,.no-foldable-items .btn-toggle-fold{display:none}.is-foldable.is-pinned,.is-full .full\:visible,.is-full .is-foldable,.is-full .lite\:hidden,.is-lite .full\:hidden,.is-lite .lite\:visible,.show-fold-items .is-foldable{display:block}.is-full .full\:w-1\/2,.is-lite .lite\:w-1\/2{width:50%}.is-full .full\:w-1\/3,.is-lite .lite\:w-1\/3{width:33.333333%}.is-full .full\:w-2\/3,.is-lite .lite\:w-2\/3{width:66.666667%}.is-full .full\:w-1\/4,.is-lite .lite\:w-1\/4{width:25%}.is-full .full\:w-2\/4,.is-lite .lite\:w-2\/4{width:50%}.is-full .full\:w-3\/4,.is-lite .lite\:w-3\/4{width:75%}.is-full .full\:w-1\/5,.is-lite .lite\:w-1\/5{width:20%}.is-full .full\:w-2\/5,.is-lite .lite\:w-2\/5{width:40%}.is-full .full\:w-3\/5,.is-lite .lite\:w-3\/5{width:60%}.is-full .full\:w-4\/5,.is-lite .lite\:w-4\/5{width:80%}.is-full .full\:w-1\/6,.is-lite .lite\:w-1\/6{width:16.666667%}.is-full .full\:w-2\/6,.is-lite .lite\:w-2\/6{width:33.333333%}.is-full .full\:w-3\/6,.is-lite .lite\:w-3\/6{width:50%}.is-full .full\:w-4\/6,.is-lite .lite\:w-4\/6{width:66.666667%}.is-full .full\:w-5\/6,.is-lite .lite\:w-5\/6{width:83.333333%}.is-full .full\:w-full,.is-lite .lite\:w-full{width:100%}.page-form{--panel-container-width:960px;--panel-container-padding:20px;--zt-panel-form-max-width:1200px;--zt-panel-form-min-width:1040px;padding-left:0;padding-right:0;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.page-form .panel-heading{border-bottom-width:1px;--tw-border-opacity:0.7;padding:0}.page-form .panel-title{font-size:1rem;line-height:1.5rem}@media (max-width:1240px){.page-form{--zt-panel-form-max-width:1100px}}@media (max-width:1110px){.page-form{--zt-panel-form-max-width:1040px}}.modal-body>.page-form{margin:-1.5rem -2rem!important;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.modal-body>.page-form>.panel-heading{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);position:sticky;top:-1.25rem;z-index:2;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding-bottom:.375rem;padding-top:.375rem}.modal-body>.panel-form .form-actions{bottom:-1.5rem!important;padding-bottom:1.5rem;padding-top:1.5rem}.dropdown.popover{display:flex;flex-direction:column;position:absolute;z-index:1200}.dropdown.show{display:flex}.dropdown-menu{display:none;--menu-min-width:7rem;--menu-max-width:20rem}.dropdown-menu.show,.menu-wrapper>.dropdown-menu,.show>.dropdown-menu{display:block}.dropdown>.dropdown-menu,.menu-wrapper .dropdown-menu{position:relative}.show>.menu-wrapper{display:flex}.dropdown>.menu-wrapper{height:100%}.dropdown>.menu-wrapper .menu-wrapper{position:absolute}.dropdown>.menu-wrapper.is-tree .menu-wrapper{position:relative}.is-contextmenu .is-nested-show>a:not(:hover){--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity))}.is-contextmenu .menu-item.is-nested>.item-inner>.nested-toggle-icon{position:absolute;right:.25rem;top:.375rem}.is-contextmenu .menu-nested .menu-item>.item-inner{padding-left:.5rem;padding-right:1.25rem}.is-contextmenu .menu-nested .menu-item>.item-inner>.item-content{overflow:hidden}.dropdown>.arrow{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.global-search-panel li:not(.order-first):hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.global-search-panel li a{width:100%}.global-search-panel li{cursor:pointer}.history-panel>.panel-heading+.panel-body{margin-top:-.75rem}.history-panel-action{position:relative}.history-panel-action:before{border-left-width:1px;bottom:-.375rem;left:.5rem;position:absolute;top:22px;--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));--tw-content:"";content:var(--tw-content)}.col-reverse>.history-panel-action:first-child:before,.col>.history-panel-action:last-child:before{display:none}.history-panel-action>.item-inner{padding-left:1.5rem}.history-panel-action>.item-inner>.avatar{left:0;position:absolute;top:.375rem}.img-cutter{background:var(--color-gray-100);border:1px solid #ddd;margin-bottom:20px;padding:10px}.img-cutter .canvas{margin:0 auto;max-width:100%;min-width:50px;position:relative}.img-cutter .cover{bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.img-cutter .controller{background:none;border:1px dashed hsla(0,0%,100%,.7);cursor:move;position:absolute;z-index:5}.img-cutter .control{background:#000;background:rgba(0,0,0,.6);border:1px solid hsla(0,0%,100%,.6);height:6px;position:absolute;width:6px}.img-cutter .control[data-type=left]{cursor:w-resize;left:-4px;margin-top:-3px;top:50%}.img-cutter .control[data-type=top]{cursor:n-resize;left:50%;margin-left:-3px;top:-4px}.img-cutter .control[data-type=right]{cursor:e-resize;margin-top:-3px;right:-4px;top:50%}.img-cutter .control[data-type=bottom]{bottom:-4px;cursor:s-resize;left:50%;margin-left:-3px}.img-cutter .control[data-type=top-left]{cursor:nw-resize;left:-4px;top:-4px}.img-cutter .control[data-type=top-right]{cursor:ne-resize;right:-4px;top:-4px}.img-cutter .control[data-type=bottom-left]{bottom:-4px;cursor:sw-resize;left:-4px}.img-cutter .control[data-type=bottom-right]{bottom:-4px;cursor:se-resize;right:-4px}.img-cutter .cliper{bottom:0;left:0;position:absolute;right:0;top:0;z-index:2}.messager-lite{border-radius:var(--radius-lg);min-width:160px;--tw-bg-opacity:1!important;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))!important;padding:1rem 1.5rem;--tw-text-opacity:1!important;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))!important;--skin-ring:transparent!important;--skin-text:inherit!important;--skin-color:var(--color-gray-500)}.messager-lite .alert-icon{color:var(--skin-color);font-size:1.5rem;line-height:2rem}.messager-lite .alert-text{font-size:.875rem;font-weight:700;line-height:1.3rem}.messager-lite .toolbar{gap:.5rem;margin-left:2rem}.messager-lite .alert-content+.alert-close{margin-left:1.5rem}.messager-lite .toolbar .btn.ghost{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.modal-reconfirm .modal-body{font-size:14px;font-weight:700;padding:24px 32px}.modal-reconfirm .modal-footer{justify-content:center;padding:24px}.picker-deselect-btn{opacity:.6;transition-duration:.15s;transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.picker-multi-selection{cursor:pointer;padding-right:.5rem;position:relative}.picker-multi-selection .picker-deselect-btn{bottom:0;left:0;position:absolute;right:0;top:0;--tw-scale-x:0;--tw-scale-y:0;background-color:rgba(var(--color-surface-rgb),.8);opacity:0}.picker-deselect-btn:hover,.picker-multi-selection .picker-deselect-btn,.picker-multi-selection:hover .picker-deselect-btn{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))}.picker-deselect-btn:hover,.picker-multi-selection:hover .picker-deselect-btn{--tw-scale-x:1;--tw-scale-y:1;opacity:1;--state-color:rgba(var(--color-surface-rgb),.9)}.pick-pri{flex-direction:row;justify-content:flex-start;position:relative}.pick-pri,.pick-pri>.text{align-items:center;display:flex}.pick-pri>.text{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap}.pick-pri .placeholder{--tw-text-opacity:1;color:rgba(var(--color-placeholder-rgb),var(--tw-text-opacity))}.pick-pri-list>.btn{justify-content:flex-start}.disabled.pick-pri,.disabled.pick-pri>*{filter:none}.search-form{--gutter-width:12px;padding-bottom:.5rem}.search-form-container{border-radius:var(--radius);min-height:116px;position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.is-simple .search-form-container{min-height:56px}.search-form-field td{padding:.375rem}.search-form-field td.is-andOr,.search-form-field td.is-operator{width:5rem}.search-form-field td.is-condition{width:7rem}.search-form-field td.is-operator{width:5rem}[lang=en] .search-form-field td.is-operator{width:6rem}.search-form .sidebar-right.is-collapsed{margin-right:-.375rem}.is-simple .search-form-wrapper{display:flex}.is-simple .search-form-wrapper>.row{flex:1 1 auto}.is-simple .search-form-wrapper>.toolbar{flex:none;margin-top:0}.zinbar [data-hint]{position:relative}.zinbar [data-hint]:after{background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity));border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg);bottom:100%;display:block;pointer-events:none;position:absolute;right:0;white-space:nowrap;z-index:50;--tw-bg-opacity:0.9;font-size:.75rem;line-height:1rem;padding:.125rem .25rem;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity));opacity:0;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);--tw-content:attr(data-hint);content:var(--tw-content)}.zinbar [data-hint]:hover:after{opacity:1}.zin-perf-btn-list .text-danger{color:var(--color-danger-400)}.zinbar .state{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.zinbar .state:active{--tw-scale-x:1.05;--tw-scale-y:1.05}.zinbar .state:active,.zinbar.collapse{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))}.zinbar.collapse{--tw-translate-x:100%}.zinbar.collapse>div{pointer-events:none}.zinbar{--tw-translate-x:0px;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));transition-duration:.7s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}body[z-perf=danger]:before,body[z-perf=warning]:before{bottom:0;left:0;pointer-events:none;position:fixed;right:0;top:0;z-index:9990;--tw-border-opacity:1;border-color:rgba(var(--color-warning-600-rgb),var(--tw-border-opacity));font-size:.75rem;line-height:1rem;--tw-content:"";border-style:inset;border-width:12px 8px 8px;content:var(--tw-content)}body[z-perf=danger]:before{--tw-border-opacity:1;border-color:rgba(var(--color-danger-600-rgb),var(--tw-border-opacity))}body[z-perf=danger]:after,body[z-perf=warning]:after{font-size:.75rem;font-weight:700;left:0;line-height:1rem;line-height:1;pointer-events:none;position:fixed;right:0;text-align:center;top:0;z-index:9991;--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity));--tw-content:attr(z-perf-message);content:var(--tw-content)}.card{border-radius:var(--radius);min-height:28px;position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05;transition-duration:.15s;transition-property:box-shadow,border,color,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.card:hover{--tw-shadow:0 2px 6px -1px rgba(0,0,0,.2),0 1px 4px -2px rgba(0,0,0,.15);--tw-shadow-colored:0 2px 6px -1px var(--tw-shadow-color),0 1px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card-content,.card-footer,.card-header,.card-heading{padding:.25rem .5rem}.card-heading{align-items:center;display:flex;gap:.5rem}.card-icon{opacity:.5}.card-header{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity))}.card-content{display:flex;flex-direction:column;gap:.25rem}.card-actions{position:absolute;right:.125rem;top:.125rem}.card-actions+.card-heading{padding-right:1.75rem}.card-subtitle{font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.card.selected{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-400-rgb),var(--tw-ring-opacity))}.card-list{--list-gap:.625rem;--list-count-per-row:1}.card-list,.card-list-item{padding:calc(var(--list-gap)/2)}.card-list-item{width:calc(100%/var(--list-count-per-row))}.card-grid{display:flex;flex-wrap:wrap}.card-grid .card{height:100%}.contextmenu{display:none}.contextmenu.show{display:block}.contextmenu .popup .popup,.contextmenu.popup{position:absolute}.more,.more-vert{margin:.5rem;position:relative}.more,.more-vert,.more-vert:after,.more-vert:before,.more:after,.more:before{background-color:currentColor;border-radius:var(--radius-full);height:.125rem;width:.125rem}.more-vert:after,.more-vert:before,.more:after,.more:before{display:block;left:-5px;position:absolute;top:0;--tw-content:"";content:var(--tw-content)}.more-vert:after,.more:after{left:5px}.more-vert{--tw-rotate:90deg;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))}.dashboard-blocks{position:relative}.dashboard-block-cell{position:absolute;transition-duration:.15s;transition-property:left,top;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-block{border-radius:var(--radius);bottom:.5rem;left:.5rem;position:absolute;right:.5rem;top:.5rem;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-block.is-dragging{opacity:0;z-index:1600}.dashboard-block-header{align-items:center;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);display:flex;flex-direction:row;justify-content:space-between;left:0;padding:.75rem;pointer-events:none;position:absolute;right:0;top:0;z-index:10}.dashboard-block-header>*{pointer-events:auto}.dashboard-block-title{font-size:.875rem;font-weight:700;line-height:1.3rem}.dashboard-block-body{bottom:0;left:0;position:absolute;right:0;top:0}.dashboard-block-body>.panel{display:flex;flex-direction:column;height:100%;width:100%;--tw-shadow:0px 6px 16px rgba(202,215,235,.25)}.dashboard-block-body .panel-heading{cursor:move;flex:none}.dashboard-block-body .panel-body{flex:1 1 auto;overflow-y:auto}.has-more-menu>.dashboard-block-body .panel-heading{padding-right:3rem}.dashboard-block-body .panel-title{font-size:.875rem;height:2rem;line-height:1.3rem}.dashboard-block-actions .btn{opacity:.6}.dashboard-block-actions .btn:hover{opacity:1}.dashboard-drop-shadow{position:absolute;transition-duration:.15s;transition-property:left,top;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-drop-shadow:before{background-color:rgba(var(--color-inverse-rgb),.05);border-radius:var(--radius);bottom:.5rem;left:.5rem;position:absolute;right:.5rem;top:.5rem;--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-content:"";content:var(--tw-content)}:root{--scrollbar-size:10px;--scrollbar-opacity:.6;--scrollbar-bg:rgba(var(--color-inverse-rgb),.15);--scrollbar-inset:inset 0 0 0 1px rgba(var(--color-inverse-rgb),.05);--scrollbar-bar-bg:rgba(var(--color-inverse-rgb),.3);--scrollbar-hover-bg:rgba(var(--color-inverse-rgb),.4);--scrollbar-drag-bg:rgba(var(--color-inverse-rgb),.5);--scrollbar-radius:var(--radius-sm);--scrollbar-duration:.7s}.scrollbar{background:var(--scrollbar-bg);box-shadow:var(--scrollbar-shadow);opacity:var(--scrollbar-opacity);position:absolute;transition-duration:.15s;transition-duration:var(--scrollbar-duration);transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.scrollbar-hover .scrollbar{opacity:0}.scrollbar-hover:hover .scrollbar{opacity:var(--scrollbar-opacity)}.scrollbar.is-dragging,.scrollbar:hover{opacity:1!important}.scrollbar-bar{background:var(--scrollbar-bar-bg);border-radius:var(--scrollbar-radius);position:absolute}.scrollbar-bar:hover{background:var(--scrollbar-hover-bg)}.is-dragging>.scrollbar-bar{background:var(--scrollbar-drag-bg)}.is-horz>.scrollbar-bar{height:100%;z-index:20}.is-vert>.scrollbar-bar{width:100%;z-index:20}:root{--dtable-checked-bg:var(--color-warning-50);--dtable-checked-bg-hover:var(--color-warning-100);--dtable-checked-border:var(--color-border-strong)}.dtable-cell.is-checked,.dtable-striped .dtable-cell.is-odd-row.is-checked{background-color:var(--dtable-checked-bg)}.checkbox-primary.dtable-checkbox{margin-right:-.375rem}.dtable-checkbox{order:-5}.dtable-hover-col .dtable-cell.is-hover-col.is-checked,.dtable-hover-row .dtable-cell.is-hover-row.is-checked{background-color:var(--dtable-checked-bg-hover)}.dtable-cell.is-checked{--dtable-border-color:var(--dtable-checked-border)}.dtable-check-info{flex-shrink:1;margin-right:.5rem;overflow:hidden;white-space:nowrap}:root{--toggle-icon-size:calc(var(--font-size-root)*3/4 + 1px)}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-color:currentColor;border-width:1px;display:block;opacity:.5;position:relative}.toggle-icon-collapse:hover,.toggle-icon-expand:hover,.toggle-icon:hover{opacity:.9}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-radius:inherit;height:var(--toggle-icon-size);width:var(--toggle-icon-size)}.is-collapsed .toggle-icon:after,.is-collapsed .toggle-icon:before,.is-expanded .toggle-icon:before,.toggle-icon-collapse:after,.toggle-icon-collapse:before,.toggle-icon-expand:before{background-color:currentColor;content:" ";display:block;height:1px;left:2px;position:absolute;right:2px;top:calc((var(--toggle-icon-size) - 3px)/2)}.is-collapsed .toggle-icon:after,.toggle-icon-collapse:after{--tw-rotate:90deg;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))}:root{--dtable-nt-size:calc(var(--font-size-root)*3/4 + 1px)}.dtable-nested-indent{flex:none;margin-right:-.25rem;order:-100}.dtable-nested-toggle{align-items:center;border-radius:var(--radius-sm);display:flex;flex:none;height:1.25rem;justify-content:center;margin-left:-.25rem;order:-6;width:1.25rem}.dtable-nested-toggle.is-no-child{margin-right:-.25rem;opacity:0;pointer-events:none}.dtable-child-label{order:-5}.dtable-header-group{align-items:center;background:var(--dtable-header-bg);cursor:default;display:flex;justify-content:center;left:0;position:absolute;top:1px;z-index:30}.dtable-bordered .dtable-header-group{border-bottom:1px solid var(--dtable-border-color);border-left:1px solid var(--dtable-border-color)}.dtable-header-as-group{contain:style;content-visibility:visible;overflow:visible!important}.dtable-sorting{cursor:move}.dtable-cell.is-sorting-from{background-color:var(--dtable-bg);z-index:20}.dtable-cell.is-sorting-from:before{border-top-width:1px}.dtable-sorting .dtable-cell.is-sorting-after{--tw-translate-y:-100%}.dtable-sorting .dtable-cell.is-sorting-after,.dtable-sorting .dtable-cell.is-sorting-before{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))}.dtable-sorting .dtable-cell.is-sorting-before{--tw-translate-y:100%}.dtable-cell.is-sorting-before:before,.dtable-cell.is-sorting-from:before{border-bottom-width:1px;border-top-width:1px}:root{--dtable-bg:var(--color-canvas);--dtable-striped-bg:var(--color-gray-50);--dtable-hover-bg:rgba(var(--color-primary-500-rgb),.05);--dtable-header-bg:var(--color-surface);--dtable-footer-bg:var(--color-surface);--dtable-border-color:var(--color-border);--dtable-sorter-size:0.3125rem}.dtable{position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.dtable.no-animation *{transition-property:none!important}.dtable-block{overflow:hidden;position:absolute;width:100%}.dtable-header{background-color:var(--dtable-header-bg);border-top-left-radius:inherit;border-top-right-radius:inherit}.dtable-body{overflow:hidden}.dtable-cells{background-color:var(--dtable-bg);height:100%;position:absolute;z-index:0}.dtable-cells-container{height:100%;position:absolute;width:100%}.dtable-header .dtable-cells{background-color:var(--dtable-header-bg)}.dtable-cells.dtable-fixed-left,.dtable-cells.dtable-fixed-right{z-index:10}.dtable-cell{position:absolute;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-cell:after,.dtable-cell:before{bottom:0;content:"";left:0;opacity:0;pointer-events:none;position:absolute;right:0;top:-1px;z-index:2}.dtable-cell:before{border-bottom-width:1px;border-color:var(--dtable-border-color);opacity:1}.dtable-header .dtable-cell{align-items:center;display:flex;font-weight:700}.dtable-header .dtable-cell:after,.dtable-header .dtable-cell:before{top:0}.dtable-cell-content{align-items:center;display:flex;gap:.25rem;height:100%;overflow:hidden;padding-left:.75rem;padding-right:.75rem;position:relative;white-space:nowrap;width:100%;z-index:5}.dtable-body .dtable-cell.is-last-row:before{border-bottom-width:0}.dtable-footer{align-items:center;background-color:var(--dtable-footer-bg);border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;border-color:var(--dtable-border-color);border-top-width:1px;display:flex;padding-left:.75rem;padding-right:.75rem;position:absolute;width:100%;z-index:10}.dtable-empty-tip{align-items:center;display:flex;height:100%;justify-content:center}.dtable .scrollbar{z-index:20}.dtable-header{transition-duration:1s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-scrolled-down .dtable-header{border-bottom-color:rgba(var(--color-inverse-rgb),.15);border-bottom-width:1px}.dtable-has-scroll-y:not(.dtable-scrolled-bottom) .dtable-footer{border-top-color:rgba(var(--color-inverse-rgb),.15);border-top-width:1px}.dtable-scroll-shadow{background-image:linear-gradient(90deg,rgba(0,0,0,.03),transparent),linear-gradient(90deg,rgba(0,0,0,.03),transparent 50%);opacity:0;pointer-events:none;position:absolute;top:0;transition-duration:1s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);width:8px;z-index:20}.dtable:hover .dtable-scroll-shadow{opacity:1}.dtable-scroll-shadow.is-right{margin-left:-7px;--tw-rotate:180deg;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))}.dtable-scroll-shadow.is-left,.dtable-scrolled-end .dtable-scroll-shadow.is-right{visibility:hidden}.dtable-scroll-shadow.is-right,.dtable-scrolled-right .dtable-scroll-shadow.is-left{visibility:visible}.scrollbar-hover:hover .scrollbar{opacity:0}.scrollbar-hover .dtable-body:hover~.scrollbar{opacity:var(--scrollbar-opacity)}.dtable-sort{display:inline-flex;height:calc(var(--dtable-sorter-size)*12/5);margin-left:.25rem;opacity:.8;position:relative}.dtable-cell:hover .dtable-sort{opacity:1}.dtable-sort:after,.dtable-sort:before{border-color:transparent;border-style:solid;height:0;left:0;opacity:.4;position:absolute;width:0;--tw-content:"";border-width:var(--dtable-sorter-size);content:var(--tw-content)}.dtable-sort:before{border-bottom-color:currentColor;border-top-width:0;top:0}.dtable-sort:after{border-bottom-width:0;border-top-color:currentColor;bottom:0}.dtable-sort.dtable-sort-asc:before,.dtable-sort.dtable-sort-desc:after{opacity:.9}.dtable-sort-link{align-items:center;cursor:pointer;display:inline-flex}.dtable-cells.dtable-fixed-left:before,.dtable-cells.dtable-fixed-right:before{border-color:var(--dtable-border-color);content:"";height:100%;pointer-events:none;position:absolute;width:100%;z-index:5}.dtable-cells.dtable-fixed-left:before{border-right-width:1px;right:-1px}.dtable-cells.dtable-fixed-right:before{border-left-width:1px;left:0}.dtable-bordered:before{border-color:var(--dtable-border-color);border-radius:inherit;border-width:1px;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:15}.dtable-bordered .dtable-cell:before{border-width:1px;right:-1px}.dtable-cell.has-border-left:before{border-left-width:1px}.dtable-cell.has-border-right:before{border-right-width:1px;right:-1px}.dtable-striped .dtable-cell.is-odd-row{background-color:var(--dtable-striped-bg)}.dtable-hover-cell .dtable-cell:hover:after,.dtable-hover-col .dtable-cell.is-hover-col:before,.dtable-hover-row .dtable-body .dtable-cell.is-hover-row:before{background-color:var(--dtable-hover-bg)}:root{--kanban-lane-name-width:20px;--kanban-col-width:280px;--kanban-cols-gap:8px;--kanban-lanes-gap:8px;--kanban-header-height:32px;--kanban-lane-height:auto;--kanban-lane-color:var(--color-primary-500);--kanban-link-color:var(--color-secondary-600)}.kanban{align-items:flex-start;display:flex;flex-direction:column;min-width:-moz-fit-content;min-width:fit-content;position:relative}.kanban-header{display:flex;flex-direction:row;height:var(--kanban-header-height)}.kanban.has-sub-cols .kanban-header{height:calc(var(--kanban-header-height)*2)}.kanban-header-lane-name{background-color:rgba(var(--color-fore-rgb),.05);flex:none;width:var(--kanban-lane-name-width)}.kanban-header-cols,.kanban-header-sub-cols{display:flex;flex:1 1 0%;flex-direction:row;gap:var(--kanban-cols-gap)}.kanban-header-sub-cols{border-top-width:2px;--tw-border-opacity:1;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity))}.kanban-header-col{align-items:center;border-bottom-width:1px;display:flex;flex:1 1 0%;flex-direction:column;height:100%;width:var(--kanban-col-width)}.kanban-header-col.has-subs{width:auto}.kanban-header-col-wrapper{align-items:center;display:flex;flex:1 1 0%;flex-direction:row;position:relative;width:100%;--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));padding-left:3.5rem;padding-right:3.5rem}.kanban-header-col-wrapper.is-align-left{justify-content:flex-start;padding-left:.5rem}.kanban-header-col-wrapper.is-align-left>.kanban-header-title{justify-content:flex-start}.kanban-header-title{align-items:center;display:flex;flex:1 1 auto;flex-direction:row;justify-content:center;overflow:hidden}.kanban-header-title>.as-title{color:var(--kanban-col-color);font-weight:700;overflow:hidden;white-space:nowrap}.kanban-header-col-actions{bottom:.25rem;position:absolute;right:.25rem;top:.25rem}.kanban-header-col-actions>.btn{opacity:.6}.kanban-header-col-actions>.btn:hover{opacity:1}.kanban-body{display:flex;flex-direction:column;gap:var(--kanban-lanes-gap)}.kanban-lane{display:flex;flex-direction:row;height:var(--kanban-lane-height);min-height:96px}.kanban-lane-name{align-items:center;background-color:var(--kanban-lane-color);display:flex;justify-content:center;width:var(--kanban-lane-name-width);--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity));writing-mode:vertical-rl}.kanban-lane-actions,.kanban-lane-name{flex:none;position:relative}.kanban-lane.is-first .kanban-lane-actions{margin-top:calc(4px - var(--kanban-header-height))}.has-sub-cols .kanban-lane.is-first .kanban-lane-actions{margin-top:calc(4px - var(--kanban-header-height)*2)}.kanban-lane-actions>.btn.square{width:var(--kanban-lane-name-width)}.kanban-lane-title{align-items:center;display:flex;flex:1 1 auto;justify-content:center;overflow:hidden;white-space:nowrap}.kanban-lane-cols{display:flex;flex-direction:row;gap:var(--kanban-cols-gap)}.kanban-lane-col{background-color:rgba(var(--color-inverse-rgb),.05);display:flex;flex-direction:column;height:100%;width:var(--kanban-col-width)}.kanban-header-col,.kanban-lane-col{margin-left:calc(var(--kanban-col-gap-left) - var(--kanban-cols-gap));margin-right:calc(var(--kanban-col-gap-right) - var(--kanban-cols-gap))}.kanban-col-content{position:relative;z-index:5}.kanban-lane.is-first>.kanban-lane-name:before{background-color:var(--kanban-lane-color);bottom:100%;display:block;height:var(--kanban-header-height);left:0;position:absolute;right:0;--tw-content:"";content:var(--tw-content)}.kanban.has-sub-cols .kanban-lane.is-first>.kanban-lane-name:before{height:calc(var(--kanban-header-height)*2)}.kanban-lane{margin-bottom:calc(var(--kanban-col-gap-bottom) - var(--kanban-lanes-gap));margin-top:calc(var(--kanban-col-gap-top) - var(--kanban-lanes-gap))}.kanban-items{flex:1 1 auto;overflow:auto;scrollbar-gutter:both-edges stable}.kanban-lane.is-auto-height .kanban-items{scrollbar-gutter:auto}.kanban-item{cursor:default}.kanban-item .card-actions{z-index:5}.kanban-heading{width:100%}.kanban-heading .listitem{height:2.25rem}.kanban-heading .item-title{font-weight:700}.kanban.has-dragging{cursor:move}.kanban-item.is-dragging{cursor:move;opacity:.1}.kanban-sticky .kanban-header{position:sticky;top:0;z-index:10;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.kanban-sticky .kanban-header.is-pinned{border-bottom-color:rgba(var(--color-inverse-rgb),.15);border-bottom-width:1px;z-index:20;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.kanban-sticky .kanban-lane-name{left:0;position:sticky;z-index:21}.kanban-region{border-radius:var(--radius);min-width:-moz-fit-content;min-width:fit-content;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding:.75rem;--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.kanban-region>.kanban+.kanban{margin-top:.75rem}.kanban-region.has-heading{padding-top:0}.kanban-heading>.listitem{left:.75rem;position:sticky;width:calc(var(--kanban-list-width) - 24px)}.kanban-list{display:flex;flex-direction:column;gap:.75rem;height:100vh;overflow:auto;position:relative;scrollbar-gutter:stable}.kanban-list.is-moving{cursor:grabbing}.kanban-items.is-dropping{position:relative}.kanban-item.is-dropping{position:relative;transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.kanban-item.is-dropping:after,.kanban-items.is-dropping>.card-list:after{background-color:rgba(var(--color-warning-500-rgb),.5);border-radius:var(--radius-full);display:block;height:.25rem;--tw-content:"";content:var(--tw-content)}.kanban-item.is-dropping:after{left:.25rem;position:absolute;right:.25rem;top:-.25rem}.kanban-item.is-dropping[z-drop-side=before]{--tw-translate-y:0.25rem}.kanban-item.is-dropping[z-drop-side=after],.kanban-item.is-dropping[z-drop-side=before]{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))}.kanban-item.is-dropping[z-drop-side=after]{--tw-translate-y:-0.25rem}.kanban-item.is-dropping[z-drop-side=after]:after{bottom:-.25rem;top:auto}.kanban-links{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:5}.kanban-list>.kanban-links{overflow:visible;z-index:11}.kanban-link{align-items:center;color:var(--kanban-link-color);display:flex;justify-content:center;position:absolute}.kanban-link>svg{position:absolute}.kanban-link-text{border-radius:var(--radius);height:1em;line-height:1;padding-left:.125rem;padding-right:.125rem;pointer-events:auto;position:relative;text-align:center;white-space:nowrap}.kanban-link-start-point{height:.5rem;width:.5rem;--tw-translate-x:-0.25rem;--tw-translate-y:-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))}.kanban-link-start-point,.kanban-link-start-point:before{background-color:currentColor;border-radius:var(--radius-full);position:absolute}.kanban-link-start-point:before{bottom:-.25rem;display:block;left:-.25rem;opacity:.2;right:-.25rem;top:-.25rem;--tw-content:"";content:var(--tw-content)}.kanban-link path{pointer-events:auto}.kanban-link path+path{opacity:.6}.kanban-link.is-hover path+path{opacity:1;stroke-width:calc(var(--kanban-link-weight)*1.5)}.kanban-link-delete-btn{pointer-events:auto;--tw-scale-x:.75;--tw-scale-y:.75;opacity:0;transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.is-hover .kanban-link-delete-btn,.kanban-link-delete-btn{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))}.is-hover .kanban-link-delete-btn{--tw-scale-x:1;--tw-scale-y:1;opacity:1}.is-adding-link{cursor:crosshair;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kanban-link-editor{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:20}.kanban-link-editor-from,.kanban-link-editor-to{border-radius:var(--radius);--tw-shadow-color:rgb(var(--color-primary-500-rgb));--tw-shadow:var(--tw-shadow-colored);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-500-rgb),var(--tw-ring-opacity))}.kanban-link-editor-from,.kanban-link-editor-point,.kanban-link-editor-to{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);position:absolute}.kanban-link-editor-point{height:.5rem;pointer-events:auto;width:.5rem;--tw-translate-x:-0.25rem;--tw-translate-y:-0.25rem;border-radius:var(--radius-full);cursor:move;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));--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));opacity:.5;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:rgba(var(--color-primary-500-rgb),0.1);transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.kanban-link-editor-point.is-left{left:0;top:50%}.kanban-link-editor-point.is-right{right:0;top:50%;--tw-translate-x: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))}.kanban-link-editor-point.is-top{left:50%;top:0}.kanban-link-editor-point.is-bottom{bottom:0;left:50%;--tw-translate-y: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))}.kanban-link-editor-point:hover{opacity:1;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-primary-500-rgb),0.5)}.tab-pane{display:none}.tab-pane.active{display:block}:root{--dtable-header-bg:var(--color-surface-light);--dtable-footer-bg:var(--color-surface-light);--dtable-border-color:var(--color-border-light);--dtable-checked-border:var(--color-border);--dtable-checked-bg:var(--color-primary-50);--dtable-checked-bg-hover:var(--color-primary-100);--dtable-hover-bg:rgba(var(--color-gray-500-rgb),.05);--dtable-link-color:var(--color-link);--dtable-link-hover-color:var(--color-link-hover);--dtable-link-visited-color:var(--color-link-visited)}.dtable-cell-content[style*="--color-link"]{--dtable-link-hover-color:var(--color-link);--dtable-link-visited-color:var(--color-link)}.dtable-body .dtable-cell a{--dtable-link-color:var(--color-link)}.dtable-body .dtable-cell a:not(.btn){color:var(--dtable-link-color)}.dtable-body .dtable-cell a:not(.btn):visited{color:var(--dtable-link-visited-color)}.dtable-body .dtable-cell a:not(.btn):hover{color:var(--dtable-link-hover-color)}.dtable .btn-avatar{border-radius:var(--radius-full);gap:.25rem;height:1.25rem;--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity));padding-left:0;padding-right:.5rem;--tw-ring-color:transparent}.dtable .btn-avatar:hover{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dtable-nested-toggle{width:1rem}.dtable .toggle-icon{--toggle-icon-size:11px;border-radius:var(--radius);--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity))}.dtable .toggle-icon:after,.dtable .toggle-icon:before{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-700-rgb),var(--tw-bg-opacity))}.dtable-footer>.toolbar{margin-right:.5rem}.dtable-header .dtable-cell[data-sort]>.dtable-cell-content>a:not(:hover){color:inherit}.dtable-assign-btn{align-items:center;border-radius:var(--radius-full);display:flex;flex-direction:row;height:1.25rem;padding-left:.375rem;padding-right:.375rem;--dtable-link-color:var(--color-fore)}.dtable-assign-btn:not(:hover){color:inherit}.dtable-assign-btn.is-me{--dtable-link-color:var(--color-warning-500)!important}.dtable-assign-btn:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.dtable-assign-btn>span{border-radius:var(--radius-full);line-height:20px;padding-left:.375rem;padding-right:.375rem}.dtable-assign-btn.is-unassigned>span{--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.dtable-assign-btn.is-unassigned:hover>span{background-color:inherit;color:inherit;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.dtable-assign-btn>.icon{opacity:0;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-assign-btn:not(.disabled):hover>.icon{opacity:1}.dtable-header .dtable-cell[data-type=assign]{padding-left:1.5rem}.dtable-cell-icon{order:-5}.dtable-custom-cell[data-type=control]{align-items:center;display:flex;justify-content:center;padding-left:.125rem;padding-right:.125rem;z-index:1}.dtable-empty-tip{min-height:212px}.dtable-cell[data-type=actions] .btn>.icon{font-size:1rem;line-height:1.5rem}.menu.menu-dtable-actions.has-icons{align-items:center;display:flex;gap:.25rem;min-width:auto;--menu-icon-opacity:1}.menu.menu-dtable-actions.has-icons>.menu-item>a{padding-left:.25rem;padding-right:.25rem}.menu.menu-dtable-actions.has-icons>.menu-item>a>.icon-EMPTY,.menu.menu-dtable-actions.has-icons>.menu-item>a>.item-content{display:none}.menu.menu-dtable-actions.has-icons>.menu-item>a>.icon-EMPTY+.item-content,.menu.menu-dtable-actions.has-icons>.menu-item>a>.item-content:first-child{display:block}:root{--state-color:rgba(0,0,0,.03);--zt-page-bg:#f8f9fa;--zt-header-height:3rem;--zt-header-bg:var(--color-canvas);--zt-header-color:var(--color-fore);--zt-sidebar-width:160px;--zt-navbar-active-color:var(--color-primary-500)}body{background-color:var(--zt-page-bg)}.page-app{overflow:overlay}.container{min-width:1200px}#header{height:var(--zt-header-height);--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);background:var(--zt-header-bg);border-bottom:1px solid var(--zt-header-border,rgba(var(--color-inverse-rgb),.05));box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:var(--zt-header-color)}#header>.container{height:100%;position:relative}#heading{align-items:center;bottom:0;display:flex;gap:.25rem;left:1rem;position:absolute;top:0}#heading>.toolbar{margin-left:-.75rem}#heading .dropmenu-btn>.text{max-width:142px}#heading>[data-zui-dropmenu]+div .dropmenu-btn>.text,#heading>[data-zui-dropmenu]+div+div .dropmenu-btn>.text{max-width:78px}#navbar{align-items:center;display:flex;font-size:.875rem;height:100%;justify-content:center;line-height:1.3rem;--state-color:rgba(var(--color-primary-500-rgb),.05);--state-active-opacity:1;--state-active-color:rgba(var(--color-primary-500-rgb),.08)}#navbar>.nav{--nav-active-color:var(--zt-navbar-active-color,var(--color-current));--nav-item-padding:12px;--nav-divider-margin:8px;margin-left:var(--nav-offset-left)}#navbar>.nav>.nav-item>a{height:3rem;padding-left:var(--nav-item-padding);padding-right:var(--nav-item-padding)}#navbar>.nav>.nav-item>a:before{--tw-scale-x:1!important;--tw-scale-y:1!important;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))!important;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)!important;--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)!important;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)!important}#navbar>.nav>.nav-item>a.active:before{background:var(--state-active-color)!important;opacity:1!important}#navbar>.nav>.divider{align-self:center;height:1.25rem;margin-left:var(--nav-divider-margin);margin-right:var(--nav-divider-margin);opacity:.2}#navbar>.nav.compact{font-size:.8125rem;line-height:1.25rem}#navbar>.nav.compact-extra{font-size:.75rem;line-height:1rem}#toolbar{align-items:center;bottom:0;display:flex;gap:.25rem;position:absolute;right:1rem;top:0}#main{min-height:100vh}#header+#main{min-height:calc(100vh - 3rem)}#main>.container{padding-left:1rem;padding-right:1rem;position:relative}@media (min-width:1400px){#header>.container,#main>.container,#mainNavbar>.container{padding-left:2.5rem;padding-right:2.5rem}#heading{left:2.5rem}#toolbar{right:2.5rem}}#mainNavbar{height:2.75rem;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}#mainNavbar>.container{position:relative}#mainNavbar .nav{bottom:0;height:2.75rem;justify-content:center;left:0;position:absolute;right:0;top:0}#mainNavbar .nav-item{font-size:.875rem;line-height:1.3rem}#mainNavbar .nav-item>a{border-radius:var(--radius)}#mainMenu{gap:1rem;justify-content:space-between;padding-bottom:.75rem;padding-top:.75rem}#featureBar,#mainMenu{align-items:center;display:flex}#featureBar{flex:1 1 auto;gap:.5rem}#actionBar{gap:.5rem}#actionBar>.nav-divider{margin-left:-.25rem;margin-right:-.25rem}#mainContent{padding-top:.75rem}#mainMenu+#mainContent{padding-top:0}#mainContent.row{align-items:stretch;display:flex;gap:1rem;justify-content:space-between}#mainContent.row>*{flex:1 1 auto;min-width:0}#mainContainer.has-sidebar-left{--gutter-left-size:16px}#mainContainer.is-sidebar-left-collapsed{--gutter-left-size:0}.has-sidebar-animation.has-sidebar-left #mainMenu{transition-duration:.15s;transition-property:padding-left;transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--modal-radius:var(--radius);--avatar-bg:var(--color-gray-200)}.modal-alert .modal-footer{justify-content:center}.modal-dialog{--tw-bg-opacity:1}.modal-content,.modal-dialog{max-height:calc(100vh - 4rem)}.modal-body,.modal-footer,.modal-header{padding:1.5rem 2rem}.modal-header{padding-right:3rem}.modal-body+.modal-footer,.modal-header+.modal-actions+.modal-body,.modal-header+.modal-body{padding-top:1px}.modal-condensed .modal-body,.modal-condensed .modal-header{padding:1rem 1.25rem}.modal-condensed .modal-actions{top:.75rem}:root{--label-bg:var(--color-gray-100);--label-color:var(--color-gray-800)}.label[class$=-pale],.label[class*="-pale "]{--tw-bg-opacity:0.4;--tw-ring-opacity:0.4}.body-modal .panel-form,.modal-body>.panel-form{margin:0;padding:0}.modal-body>.panel-form>.panel-heading{padding-left:1px;padding-right:1px;padding-top:1px}.modal-body>.panel-form>.panel-body{padding:1px}.modal-body>.form{padding-top:1px}.modal .history,.modal .panel-body>.form{padding:0}:root{--article-p-space:0}.lane-col-join-with-above{position:relative}.lane-col-join-with-above:before{background-color:inherit;left:0;position:absolute;right:0;--tw-content:"";content:var(--tw-content);height:var(--kanban-col-gap-top,var(--kanban-lanes-gap));top:calc(0px - var(--kanban-col-gap-top, var(--kanban-lanes-gap)))}.arrow,.arrow:before{background:inherit;height:calc(var(--arrow-size)*1.4142);position:absolute;width:calc(var(--arrow-size)*1.4142)}.arrow{visibility:hidden;--arrow-size:5px}.arrow:before{border:inherit;content:"";visibility:visible;--tw-rotate:45deg;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))}.arrow-top,.arrow-up{border-bottom:inherit;border-right:inherit;border-width:1px 0 0 1px;top:calc(1px - var(--arrow-size))}.white>.arrow-top,.white>.arrow-up{top:calc(0px - var(--arrow-size))}.arrow-bottom,.arrow-down{border-left:inherit;border-top:inherit;border-width:0 1px 1px 0;bottom:calc(1px - var(--arrow-size))}.white>.arrow-bottom,.white>.arrow-down{bottom:calc(0px - var(--arrow-size))}.arrow-left{border-right:inherit;border-top:inherit;border-width:0 0 1px 1px;left:calc(1px - var(--arrow-size))}.white>.arrow-left{left:calc(0px - var(--arrow-size))}.arrow-right{border-bottom:inherit;border-left:inherit;border-width:1px 1px 0 0;right:calc(1px - var(--arrow-size))}.white>.arrow-right{right:calc(0px - var(--arrow-size))}.caret,.caret-down,.caret-left,.caret-right,.caret-up{align-items:center;display:inline-flex;height:.75rem;justify-content:center;position:relative;width:.75rem}.caret-down:before,.caret-left:before,.caret-right:before,.caret-up:before,.caret:before{display:block;height:.35rem;transform-origin:center;width:.35rem;--tw-translate-y:-1px;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.caret-left:before{--tw-translate-x:0.125rem;--tw-translate-y:0px;--tw-rotate:135deg}.caret-left:before,.caret-right:before{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))}.caret-right:before{--tw-translate-x:-0.125rem;--tw-translate-y:0px;--tw-rotate:-45deg}.caret-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}:focus>.caret,:hover>.caret{opacity:inherit}.disabled .caret,.readonly .caret{opacity:var(--caret-opacity,.5)}:root{--toggle-icon-size:calc(var(--font-size-root)*3/4 + 1px)}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-color:currentColor;border-width:1px;display:block;opacity:.5;position:relative}.toggle-icon-collapse:hover,.toggle-icon-expand:hover,.toggle-icon:hover{opacity:.9}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-radius:inherit;height:var(--toggle-icon-size);width:var(--toggle-icon-size)}.is-collapsed .toggle-icon:after,.is-collapsed .toggle-icon:before,.is-expanded .toggle-icon:before,.toggle-icon-collapse:after,.toggle-icon-collapse:before,.toggle-icon-expand:before{background-color:currentColor;content:" ";display:block;height:1px;left:2px;position:absolute;right:2px;top:calc((var(--toggle-icon-size) - 3px)/2)}.is-collapsed .toggle-icon:after,.toggle-icon-collapse:after{--tw-rotate:90deg;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))}.close{aspect-ratio:1/1;display:block;position:relative;width:1em}.close:after,.close:before{content:"";display:block;height:1px;left:0;position:absolute;width:100%;--tw-rotate:45deg;background-color:currentColor;border-radius:var(--radius-full);top:calc(50% - .5px)}.close:after,.close:before{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))}.close:after{--tw-rotate:-45deg}.spinner,.spinner:after,.spinner:before{display:block;height:1em;position:relative;width:1em}.spinner:after,.spinner:before{border-radius:var(--radius-full);content:" ";position:absolute}.spinner:before{animation:-spin 1s cubic-bezier(.6,0,.4,1) infinite;border:.16667em solid transparent;border-top-color:currentcolor}.spinner:after{border:.16667em solid;opacity:.2}.magnifier{display:inline-block;height:1em;position:relative;width:1em}.magnifier:after,.magnifier:before{border-radius:var(--radius-full);content:"";display:block;position:absolute}.magnifier:before{border:1px solid;height:.75em;width:.75em}.magnifier:after{height:1px;left:.55em;top:.7em;width:.5em;--tw-rotate:45deg;background-color:currentColor;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))}.more,.more-vert{margin:.5rem;position:relative}.more,.more-vert,.more-vert:after,.more-vert:before,.more:after,.more:before{background-color:currentColor;border-radius:var(--radius-full);height:.125rem;width:.125rem}.more-vert:after,.more-vert:before,.more:after,.more:before{display:block;left:-5px;position:absolute;top:0;--tw-content:"";content:var(--tw-content)}.more-vert:after,.more:after{left:5px}.more-vert{--tw-rotate:90deg;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))}.chevron-down,.chevron-left,.chevron-right,.chevron-up{align-items:center;display:inline-flex;height:1em;justify-content:center;position:relative;width:1em}.chevron-down:before,.chevron-left:before,.chevron-right:before,.chevron-up:before{display:block;height:.6923077em;transform-origin:center;width:.6923077em;--tw-translate-y:-0.125rem;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.chevron-left:before{--tw-translate-y:0px;--tw-translate-x:0.125rem;--tw-rotate:135deg}.chevron-left:before,.chevron-right:before{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))}.chevron-right:before{--tw-translate-y:0px;--tw-translate-x:-0.125rem;--tw-rotate:-45deg}.chevron-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}.i-time{border-color:currentColor;border-radius:50%;border-width:.08em;display:inline-block;height:1em;position:relative;width:1em}.i-time:after{border-color:currentColor;border-width:0 0 .08em .08em;height:.385em;left:.3846em;position:absolute;top:.08em;width:.385em;--tw-content:"";content:var(--tw-content)}.i-calendar{border-color:currentColor;border-radius:2px;border-width:.08em;display:inline-block;height:.79em;margin-top:.16em;position:relative;width:1em}.i-calendar:after,.i-calendar:before{border-color:currentColor;border-width:.08em;position:absolute;--tw-content:"";content:var(--tw-content)}.i-calendar:before{border-bottom-width:0;left:.15em;top:.307em;width:.6em}.i-calendar:after{border-bottom-width:0;border-top-width:0;height:.3077em;left:.17em;top:-.154em;width:.538em}:root{--state-color:rgba(0,0,0,.05);--state-scale:.9;--opacity-disabled:.7;--opacity-muted:.5;--load-indicator-duration:300ms;--load-indicator-delay:.5s}.btn,.item>a,.state{cursor:pointer;position:relative}.btn:before,.item>a:before,.state:before{background:var(--state-color);border-radius:inherit;bottom:0;content:" ";display:block;left:0;opacity:0;pointer-events:none;position:absolute;right:0;top:0;transform:scale(var(--state-scale));transition-duration:.2s;transition-property:opacity,transform,box-shadow,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn.active:not(:hover,:focus-visible,:focus-within,:active):before,.item>a.active:not(:hover,:focus-visible,:focus-within,:active):before,.state.active:not(:hover,:focus-visible,:focus-within,:active):before{background:var(--state-active-color,var(--state-color));opacity:var(--state-active-opacity,0);--tw-scale-x:1;--tw-scale-y:1;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));--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-duration:.7s}.btn.selected:not(:hover,:focus-visible,:focus-within,:active):before,.item>a.selected:not(:hover,:focus-visible,:focus-within,:active):before,.state.selected:not(:hover,:focus-visible,:focus-within,:active):before{background:var(--state-selected-color,var(--state-scale));opacity:var(--state-selected-opacity,.1);--tw-scale-x:1;--tw-scale-y:1;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))}.btn:focus-visible,.item>a:focus-visible,.state:focus-visible{outline:2px solid transparent;outline-offset:2px}.btn:active:before,.btn:focus-visible:before,.btn:hover:before,.item>a:active:before,.item>a:focus-visible:before,.item>a:hover:before,.state:active:before,.state:focus-visible:before,.state:hover:before{--tw-scale-x:1;--tw-scale-y:1;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))}.btn:hover:before,.item>a:hover:before,.state:hover:before{opacity:var(--state-hover-opacity,1)}.btn:active:before,.item>a:active:before,.state:active:before{background:var(--state-pressed-color,var(--state-color));opacity:var(--state-pressed-opacity,.6);--tw-ring-color:var(--state-ring-color,currentColor);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);transition-duration:.7s}.btn:focus-visible:before,.item>a:focus-visible:before,.state:focus-visible:before{background:var(--state-focus-color,var(--state-color));opacity:var(--state-focus-opacity,1);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-focus-rgb),var(--tw-ring-opacity))}.btn.disabled:before,.btn[disabled]:before,.item>a.disabled:before,.item>a[disabled]:before,.state.disabled:before,.state[disabled]:before{display:none}.btn{--state-active-opacity:1}@-moz-document url-prefix(){.btn,:root{--state-active-opacity:0;--state-focus-opacity:0}}.disabled,.disabled *,[disabled],[disabled] *{cursor:not-allowed!important;opacity:var(--opacity-disabled);--tw-grayscale:grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.load-indicator:after,.load-indicator:before{display:block;opacity:0;position:absolute;transition-delay:0s;transition-duration:.15s;transition-duration:var(--load-indicator-duration);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden}.load-indicator:before{align-items:center;background-color:rgba(var(--color-canvas-rgb),.7);border-radius:inherit;bottom:0;display:flex;font-size:.75rem;justify-content:center;left:0;line-height:1rem;right:0;top:0;z-index:99;--tw-content:attr(data-loading);content:var(--tw-content)}.load-indicator[data-loading]:before{padding-top:3.5rem}.load-indicator:after{border-color:transparent currentcolor currentcolor;border-radius:var(--radius-full);border-style:dotted;border-width:4px 3px 4px 4px;height:2rem;left:50%;margin-left:-1rem;margin-top:-1rem;top:50%;width:2rem;z-index:100;--tw-content:"";content:var(--tw-content)}.load-indicator.loading:after,.load-indicator.loading:before{opacity:1;transition-delay:var(--load-indicator-delay);visibility:visible}@keyframes -spin{to{transform:rotate(1turn)}}.load-indicator.loading:after{animation:-spin 1s linear infinite;opacity:.5}.events-none,.pointer-events-none{pointer-events:none}.events-auto,.pointer-events-auto{pointer-events:auto}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-text{-webkit-user-select:text;-moz-user-select:text;user-select:text}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;-moz-user-select:auto;user-select:auto}.cursor-auto{cursor:auto}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.cursor-text{cursor:text}.cursor-move{cursor:move}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.active\:opacity-100.active,.active\:opacity-100:active,.group.active .group-active\:opacity-100,.group:active .group-active\:opacity-100,.group:hover .group-hover\:opacity-100,.hover\:opacity-100:hover{opacity:1}.active\:text-primary.active,.active\:text-primary:active,.group.active .group-active\:text-primary,.group:active .group-active\:text-primary,.group:hover .group-hover\:text-primary,.hover\:text-primary:hover{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.active\:border-primary.active,.active\:border-primary:active,.group.active .group-active\:border-primary,.group:active .group-active\:border-primary,.group:hover .group-hover\:border-primary,.hover\:border-primary:hover{--tw-border-opacity:1;border-color:rgba(var(--color-primary-500-rgb),var(--tw-border-opacity))}.active\:primary-pale.active,.active\:primary-pale:active,.group.active .group-active\:primary-pale,.group:active .group-active\:primary-pale,.group:hover .group-hover\:primary-pale,.hover\:primary-pale:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.active\:primary.active,.active\:primary:active,.group.active .group-active\:primary,.group:active .group-active\:primary,.group:hover .group-hover\:primary,.hover\:primary:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.active\:canvas.active,.active\:canvas:active,.group.active .group-active\:canvas,.group:active .group-active\:canvas,.group:hover .group-hover\:canvas,.hover\:canvas:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.active\:font-bold.active,.active\:font-bold:active,.group.active .group-active\:font-bold,.group:active .group-active\:font-bold,.group:hover .group-hover\:font-bold,.hover\:font-bold:hover{font-weight:700}.active\:shadow.active,.active\:shadow:active,.group.active .group-active\:shadow,.group:active .group-active\:shadow,.group:hover .group-hover\:shadow,.hover\:shadow:hover{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.container{width:100%}@media (min-width:1720px){.container{max-width:1720px}}.container{margin-left:auto;margin-right:auto}.popup{background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));border-radius:var(--radius);--tw-bg-opacity:0.85;--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05;--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.hidden{display:none!important}.pull-right{float:right}.pull-left{float:left}.clearfix:after{clear:both;content:"";display:block}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-fill{-o-object-fit:fill;object-fit:fill}.object-none{-o-object-fit:none;object-fit:none}.object-scale-down{-o-object-fit:scale-down;object-fit:scale-down}.object-bottom{-o-object-position:bottom;object-position:bottom}.object-center{-o-object-position:center;object-position:center}.object-left{-o-object-position:left;object-position:left}.object-left-bottom{-o-object-position:left bottom;object-position:left bottom}.object-left-top{-o-object-position:left top;object-position:left top}.object-right{-o-object-position:right;object-position:right}.object-right-bottom{-o-object-position:right bottom;object-position:right bottom}.object-right-top{-o-object-position:right top;object-position:right top}.object-top{-o-object-position:top;object-position:top}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-clip{overflow:clip}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-x-clip{overflow-x:clip}.overflow-y-clip{overflow-y:clip}.overflow-x-visible{overflow-x:visible}.overflow-y-visible{overflow-y:visible}.overflow-x-scroll{overflow-x:scroll}.overflow-y-scroll{overflow-y:scroll}.overflow-overlay{overflow:overlay}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{bottom:0;left:0;right:0;top:0}.inset-auto{bottom:auto;left:auto;right:auto;top:auto}.inset-x-0{left:0;right:0}.inset-y-0{bottom:0;top:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.top-full{top:100%}.right-full{right:100%}.bottom-full{bottom:100%}.left-full{left:100%}.top-auto{top:auto}.right-auto{right:auto}.bottom-auto{bottom:auto}.left-auto{left:auto}.top-px{top:1px}.right-px{right:1px}.bottom-px{bottom:1px}.left-px{left:1px}.top-0\.5{top:.125rem}.right-0\.5{right:.125rem}.bottom-0\.5{bottom:.125rem}.left-0\.5{left:.125rem}.top-1{top:.25rem}.right-1{right:.25rem}.bottom-1{bottom:.25rem}.left-1{left:.25rem}.top-1\.5{top:.375rem}.right-1\.5{right:.375rem}.bottom-1\.5{bottom:.375rem}.left-1\.5{left:.375rem}.top-2{top:.5rem}.right-2{right:.5rem}.bottom-2{bottom:.5rem}.left-2{left:.5rem}.top-2\.5{top:.625rem}.right-2\.5{right:.625rem}.bottom-2\.5{bottom:.625rem}.left-2\.5{left:.625rem}.top-3{top:.75rem}.right-3{right:.75rem}.bottom-3{bottom:.75rem}.left-3{left:.75rem}.top-3\.5{top:.875rem}.right-3\.5{right:.875rem}.bottom-3\.5{bottom:.875rem}.left-3\.5{left:.875rem}.top-4{top:1rem}.right-4{right:1rem}.bottom-4{bottom:1rem}.left-4{left:1rem}.top-5{top:1.25rem}.right-5{right:1.25rem}.bottom-5{bottom:1.25rem}.left-5{left:1.25rem}.top-6{top:1.5rem}.right-6{right:1.5rem}.bottom-6{bottom:1.5rem}.left-6{left:1.5rem}.top-7{top:1.75rem}.right-7{right:1.75rem}.bottom-7{bottom:1.75rem}.left-7{left:1.75rem}.top-8{top:2rem}.right-8{right:2rem}.bottom-8{bottom:2rem}.left-8{left:2rem}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-0{z-index:0}.z-1{z-index:1}.z-5{z-index:5}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-auto{z-index:auto}.z-base{z-index:1100}.z-above{z-index:50}.z-below{z-index:-50}.z-dropdown{z-index:1200}.z-sticky{z-index:1300}.z-fixed{z-index:1400}.z-modal{z-index:1500}.z-overlay{z-index:1600}.z-popover{z-index:1700}.z-tooltip{z-index:1800}.z-messager{z-index:1900}.m-0{margin:0}.m-auto{margin:auto}.m-px{margin:1px}.m-0\.5{margin:.125rem}.m-1{margin:.25rem}.m-1\.5{margin:.375rem}.m-2{margin:.5rem}.m-2\.5{margin:.625rem}.m-3{margin:.75rem}.m-3\.5{margin:.875rem}.m-4{margin:1rem}.m-5{margin:1.25rem}.m-6{margin:1.5rem}.m-7{margin:1.75rem}.m-8{margin:2rem}.m-9{margin:2.25rem}.m-10{margin:2.5rem}.mx-0{margin-left:0;margin-right:0}.my-0{margin-bottom:0}.mt-0,.my-0{margin-top:0}.mr-0{margin-right:0}.mb-0{margin-bottom:0}.ml-0{margin-left:0}.mx-auto{margin-left:auto;margin-right:auto}.my-auto{margin-bottom:auto}.mt-auto,.my-auto{margin-top:auto}.mr-auto{margin-right:auto}.mb-auto{margin-bottom:auto}.ml-auto{margin-left:auto}.mx-px{margin-left:1px;margin-right:1px}.my-px{margin-bottom:1px}.mt-px,.my-px{margin-top:1px}.mr-px{margin-right:1px}.mb-px{margin-bottom:1px}.ml-px{margin-left:1px}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.my-0\.5{margin-bottom:.125rem}.mt-0\.5,.my-0\.5{margin-top:.125rem}.mr-0\.5{margin-right:.125rem}.mb-0\.5{margin-bottom:.125rem}.ml-0\.5{margin-left:.125rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-1{margin-bottom:.25rem}.mt-1,.my-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.mx-1\.5{margin-left:.375rem;margin-right:.375rem}.my-1\.5{margin-bottom:.375rem}.mt-1\.5,.my-1\.5{margin-top:.375rem}.mr-1\.5{margin-right:.375rem}.mb-1\.5{margin-bottom:.375rem}.ml-1\.5{margin-left:.375rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem}.mt-2,.my-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mx-2\.5{margin-left:.625rem;margin-right:.625rem}.my-2\.5{margin-bottom:.625rem}.mt-2\.5,.my-2\.5{margin-top:.625rem}.mr-2\.5{margin-right:.625rem}.mb-2\.5{margin-bottom:.625rem}.ml-2\.5{margin-left:.625rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.my-3{margin-bottom:.75rem}.mt-3,.my-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mx-3\.5{margin-left:.875rem;margin-right:.875rem}.my-3\.5{margin-bottom:.875rem}.mt-3\.5,.my-3\.5{margin-top:.875rem}.mr-3\.5{margin-right:.875rem}.mb-3\.5{margin-bottom:.875rem}.ml-3\.5{margin-left:.875rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-4{margin-bottom:1rem}.mt-4,.my-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.my-5{margin-bottom:1.25rem}.mt-5,.my-5{margin-top:1.25rem}.mr-5{margin-right:1.25rem}.mb-5{margin-bottom:1.25rem}.ml-5{margin-left:1.25rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.my-6{margin-bottom:1.5rem}.mt-6,.my-6{margin-top:1.5rem}.mr-6{margin-right:1.5rem}.mb-6{margin-bottom:1.5rem}.ml-6{margin-left:1.5rem}.mx-7{margin-left:1.75rem;margin-right:1.75rem}.my-7{margin-bottom:1.75rem}.mt-7,.my-7{margin-top:1.75rem}.mr-7{margin-right:1.75rem}.mb-7{margin-bottom:1.75rem}.ml-7{margin-left:1.75rem}.mx-8{margin-left:2rem;margin-right:2rem}.my-8{margin-bottom:2rem}.mt-8,.my-8{margin-top:2rem}.mr-8{margin-right:2rem}.mb-8{margin-bottom:2rem}.ml-8{margin-left:2rem}.mx-9{margin-left:2.25rem;margin-right:2.25rem}.my-9{margin-bottom:2.25rem}.mt-9,.my-9{margin-top:2.25rem}.mr-9{margin-right:2.25rem}.mb-9{margin-bottom:2.25rem}.ml-9{margin-left:2.25rem}.mx-10{margin-left:2.5rem;margin-right:2.5rem}.my-10{margin-bottom:2.5rem}.mt-10,.my-10{margin-top:2.5rem}.mr-10{margin-right:2.5rem}.mb-10{margin-bottom:2.5rem}.ml-10{margin-left:2.5rem}.p-0{padding:0}.p-px{padding:1px}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.p-8{padding:2rem}.p-9{padding:2.25rem}.p-10{padding:2.5rem}.px-0{padding-left:0;padding-right:0}.py-0{padding-bottom:0}.pt-0,.py-0{padding-top:0}.pr-0{padding-right:0}.pb-0{padding-bottom:0}.pl-0{padding-left:0}.px-px{padding-left:1px;padding-right:1px}.py-px{padding-bottom:1px}.pt-px,.py-px{padding-top:1px}.pr-px{padding-right:1px}.pb-px{padding-bottom:1px}.pl-px{padding-left:1px}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.py-0\.5{padding-bottom:.125rem}.pt-0\.5,.py-0\.5{padding-top:.125rem}.pr-0\.5{padding-right:.125rem}.pb-0\.5{padding-bottom:.125rem}.pl-0\.5{padding-left:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-1{padding-bottom:.25rem}.pt-1,.py-1{padding-top:.25rem}.pr-1{padding-right:.25rem}.pb-1{padding-bottom:.25rem}.pl-1{padding-left:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-1\.5{padding-bottom:.375rem}.pt-1\.5,.py-1\.5{padding-top:.375rem}.pr-1\.5{padding-right:.375rem}.pb-1\.5{padding-bottom:.375rem}.pl-1\.5{padding-left:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-2{padding-bottom:.5rem}.pt-2,.py-2{padding-top:.5rem}.pr-2{padding-right:.5rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.py-2\.5{padding-bottom:.625rem}.pt-2\.5,.py-2\.5{padding-top:.625rem}.pr-2\.5{padding-right:.625rem}.pb-2\.5{padding-bottom:.625rem}.pl-2\.5{padding-left:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-3{padding-bottom:.75rem}.pt-3,.py-3{padding-top:.75rem}.pr-3{padding-right:.75rem}.pb-3{padding-bottom:.75rem}.pl-3{padding-left:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.py-3\.5{padding-bottom:.875rem}.pt-3\.5,.py-3\.5{padding-top:.875rem}.pr-3\.5{padding-right:.875rem}.pb-3\.5{padding-bottom:.875rem}.pl-3\.5{padding-left:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-bottom:1rem}.pt-4,.py-4{padding-top:1rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pl-4{padding-left:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-5{padding-bottom:1.25rem}.pt-5,.py-5{padding-top:1.25rem}.pr-5{padding-right:1.25rem}.pb-5{padding-bottom:1.25rem}.pl-5{padding-left:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-6{padding-bottom:1.5rem}.pt-6,.py-6{padding-top:1.5rem}.pr-6{padding-right:1.5rem}.pb-6{padding-bottom:1.5rem}.pl-6{padding-left:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-7{padding-bottom:1.75rem}.pt-7,.py-7{padding-top:1.75rem}.pr-7{padding-right:1.75rem}.pb-7{padding-bottom:1.75rem}.pl-7{padding-left:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-8{padding-bottom:2rem}.pt-8,.py-8{padding-top:2rem}.pr-8{padding-right:2rem}.pb-8{padding-bottom:2rem}.pl-8{padding-left:2rem}.px-9{padding-left:2.25rem;padding-right:2.25rem}.py-9{padding-bottom:2.25rem}.pt-9,.py-9{padding-top:2.25rem}.pr-9{padding-right:2.25rem}.pb-9{padding-bottom:2.25rem}.pl-9{padding-left:2.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-10{padding-bottom:2.5rem}.pt-10,.py-10{padding-top:2.5rem}.pr-10{padding-right:2.5rem}.pb-10{padding-bottom:2.5rem}.pl-10{padding-left:2.5rem}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(0px*(1 - var(--tw-space-x-reverse)));margin-right:calc(0px*var(--tw-space-x-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.space-x-0\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.125rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.125rem*var(--tw-space-x-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.375rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.375rem*var(--tw-space-x-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-2\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.625rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.625rem*var(--tw-space-x-reverse))}.space-y-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.625rem*var(--tw-space-y-reverse));margin-top:calc(.625rem*(1 - var(--tw-space-y-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-x-3\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.875rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.875rem*var(--tw-space-x-reverse))}.space-y-3\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.875rem*var(--tw-space-y-reverse));margin-top:calc(.875rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.25rem*var(--tw-space-x-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-7>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.75rem*var(--tw-space-x-reverse))}.space-y-7>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.75rem*var(--tw-space-y-reverse));margin-top:calc(1.75rem*(1 - var(--tw-space-y-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2rem*var(--tw-space-x-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-9>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2.25rem*var(--tw-space-x-reverse))}.space-y-9>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.25rem*var(--tw-space-y-reverse));margin-top:calc(2.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-10>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2.5rem*var(--tw-space-x-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.5rem*var(--tw-space-y-reverse));margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(1px*var(--tw-space-x-reverse))}.space-y-px>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1px*var(--tw-space-y-reverse));margin-top:calc(1px*(1 - var(--tw-space-y-reverse)))}.w-0{width:0}.w-px{width:1px}.w-0\.5{width:.125rem}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-32{width:8rem}.w-36{width:9rem}.w-40{width:10rem}.w-44{width:11rem}.w-48{width:12rem}.w-52{width:13rem}.w-56{width:14rem}.w-60{width:15rem}.w-64{width:16rem}.w-72{width:18rem}.w-80{width:20rem}.w-96{width:24rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-1\/4{width:25%}.w-2\/4{width:50%}.w-3\/4{width:75%}.w-1\/5{width:20%}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-1\/6{width:16.666667%}.w-2\/6{width:33.333333%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-5\/6{width:83.333333%}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.w-min{width:-moz-min-content;width:min-content}.w-max{width:-moz-max-content;width:max-content}.w-fit{width:-moz-fit-content;width:fit-content}.min-w-0{min-width:0}.min-w-full{min-width:100%}.min-w-min{min-width:-moz-min-content;min-width:min-content}.min-w-max{min-width:-moz-max-content;min-width:max-content}.min-w-fit{min-width:-moz-fit-content;min-width:fit-content}.max-w-0{max-width:0}.max-w-none{max-width:none}.max-w-xs{max-width:20rem}.max-w-sm{max-width:24rem}.max-w-md{max-width:28rem}.max-w-lg{max-width:32rem}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-min{max-width:-moz-min-content;max-width:min-content}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-fit{max-width:-moz-fit-content;max-width:fit-content}.h-0{height:0}.h-px{height:1px}.h-0\.5{height:.125rem}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-32{height:8rem}.h-36{height:9rem}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-52{height:13rem}.h-56{height:14rem}.h-60{height:15rem}.h-64{height:16rem}.h-72{height:18rem}.h-80{height:20rem}.h-96{height:24rem}.h-1\/2{height:50%}.h-1\/3{height:33.333333%}.h-2\/3{height:66.666667%}.h-1\/4{height:25%}.h-2\/4{height:50%}.h-3\/4{height:75%}.h-1\/5{height:20%}.h-2\/5{height:40%}.h-3\/5{height:60%}.h-4\/5{height:80%}.h-1\/6{height:16.666667%}.h-2\/6{height:33.333333%}.h-3\/6{height:50%}.h-4\/6{height:66.666667%}.h-5\/6{height:83.333333%}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.h-min{height:-moz-min-content;height:min-content}.h-max{height:-moz-max-content;height:max-content}.h-fit{height:-moz-fit-content;height:fit-content}.min-h-0{min-height:0}.min-h-full{min-height:100%}.min-h-min{min-height:-moz-min-content;min-height:min-content}.min-h-max{min-height:-moz-max-content;min-height:max-content}.min-h-fit{min-height:-moz-fit-content;min-height:fit-content}.max-h-0{max-height:0}.max-h-px{max-height:1px}.max-h-none{max-height:none}.max-h-full{max-height:100%}.max-h-min{max-height:-moz-min-content;max-height:min-content}.max-h-screen{max-height:100vh}.max-h-max{max-height:-moz-max-content;max-height:max-content}.max-h-fit{max-height:-moz-fit-content;max-height:fit-content}.canvas,.surface,.surface-light,.surface-strong{--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))}.canvas{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.surface-light{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-light-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-light-rgb),var(--tw-ring-opacity))}.surface{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-rgb),var(--tw-ring-opacity))}.surface-strong{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-strong-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-strong-rgb),var(--tw-ring-opacity))}.inverse{background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity));color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.inverse,.white{--tw-bg-opacity:1;--tw-text-opacity:1}.white{background-color:rgba(var(--color-white-rgb),var(--tw-bg-opacity));color:rgba(var(--color-fore-in-light-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-white-rgb),var(--tw-ring-opacity))}.black{--tw-bg-opacity:1;background-color:rgba(var(--color-black-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-black-rgb),var(--tw-ring-opacity))}.transparent{background-color:transparent;--tw-ring-color:transparent}.skin{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.skin:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger,.darken,.darker,.darkest,.gray,.gray-100,.gray-200,.gray-300,.gray-400,.gray-50,.gray-500,.gray-600,.gray-700,.gray-800,.gray-900,.gray-950,.important,.lighten,.lighter,.lightest,.primary,.secondary,.special,.success,.warning{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger:hover,.darken:hover,.darker:hover,.darkest:hover,.gray-100:hover,.gray-200:hover,.gray-300:hover,.gray-400:hover,.gray-500:hover,.gray-50:hover,.gray-600:hover,.gray-700:hover,.gray-800:hover,.gray-900:hover,.gray-950:hover,.gray:hover,.important:hover,.lighten:hover,.lighter:hover,.lightest:hover,.primary:hover,.secondary:hover,.special:hover,.success:hover,.warning:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger,.darken,.darker,.darkest,.gray,.gray-100,.gray-200,.gray-300,.gray-400,.gray-50,.gray-500,.gray-600,.gray-700,.gray-800,.gray-900,.gray-950,.important,.lighten,.lighter,.lightest,.primary,.secondary,.special,.success,.warning{--skin-text:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity));--skin-color:rgba(var(--skin-solid-rgb),1);--state-ring-color:var(--skin-color);--state-active-color:rgba(0,0,0,.1);--state-pressed-color:rgba(0,0,0,.1);--state-color:hsla(0,0%,100%,.1);--state-focus-color:rgba(0,0,0,.1)}.primary{--skin-solid-rgb:var(--color-primary-500-rgb)}.secondary{--skin-solid-rgb:var(--color-secondary-500-rgb)}.success{--skin-solid-rgb:var(--color-success-500-rgb)}.warning{--skin-solid-rgb:var(--color-warning-500-rgb)}.danger{--skin-solid-rgb:var(--color-danger-500-rgb)}.important{--skin-solid-rgb:var(--color-important-500-rgb)}.special{--skin-solid-rgb:var(--color-special-500-rgb)}.gray-50{--skin-bg:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity));--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-100{--skin-solid-rgb:var(--color-gray-100-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-200,.lightest{--skin-solid-rgb:var(--color-gray-200-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-300,.lighter{--skin-solid-rgb:var(--color-gray-300-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-400,.lighten{--skin-solid-rgb:var(--color-gray-400-rgb)}.gray,.gray-500{--skin-solid-rgb:var(--color-gray-500-rgb)}.darken,.gray-600{--skin-solid-rgb:var(--color-gray-600-rgb)}.darker,.gray-700{--skin-solid-rgb:var(--color-gray-700-rgb)}.darkest,.gray-800{--skin-solid-rgb:var(--color-gray-800-rgb)}.gray-900{--skin-solid-rgb:var(--color-gray-900-rgb)}.gray-950{--skin-solid-rgb:var(--color-gray-950-rgb)}.danger-pale,.gray-100-pale,.gray-200-pale,.gray-300-pale,.gray-400-pale,.gray-50-pale,.gray-500-pale,.gray-600-pale,.gray-700-pale,.gray-800-pale,.gray-900-pale,.gray-950-pale,.gray-pale,.important-pale,.pale,.primary-pale,.secondary-pale,.special-pale,.success-pale,.warning-pale{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-pale:hover,.gray-100-pale:hover,.gray-200-pale:hover,.gray-300-pale:hover,.gray-400-pale:hover,.gray-50-pale:hover,.gray-500-pale:hover,.gray-600-pale:hover,.gray-700-pale:hover,.gray-800-pale:hover,.gray-900-pale:hover,.gray-950-pale:hover,.gray-pale:hover,.important-pale:hover,.pale:hover,.primary-pale:hover,.secondary-pale:hover,.special-pale:hover,.success-pale:hover,.warning-pale:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-pale,.gray-100-pale,.gray-200-pale,.gray-300-pale,.gray-400-pale,.gray-50-pale,.gray-500-pale,.gray-600-pale,.gray-700-pale,.gray-800-pale,.gray-900-pale,.gray-950-pale,.gray-pale,.important-pale,.pale,.primary-pale,.secondary-pale,.special-pale,.success-pale,.warning-pale{--skin-color:rgba(var(--skin-pale-rgb),.1);--skin-text:rgba(var(--skin-pale-rgb),1);--state-color:rgba(var(--skin-pale-rgb),.1)}.pale{--skin-pale-rgb:var(--color-gray-500-rgb)}.primary-pale{--skin-pale-rgb:var(--color-primary-500-rgb)}.secondary-pale{--skin-pale-rgb:var(--color-secondary-500-rgb)}.success-pale{--skin-pale-rgb:var(--color-success-500-rgb)}.warning-pale{--skin-pale-rgb:var(--color-warning-500-rgb)}.danger-pale{--skin-pale-rgb:var(--color-danger-500-rgb)}.important-pale{--skin-pale-rgb:var(--color-important-500-rgb)}.special-pale{--skin-pale-rgb:var(--color-special-500-rgb)}.gray-50-pale{--skin-pale-rgb:var(--color-gray-50-rgb);--skin-text:var(--color-gray-400)}.gray-100-pale{--skin-pale-rgb:var(--color-gray-100-rgb);--skin-text:var(--color-gray-400)}.gray-200-pale{--skin-pale-rgb:var(--color-gray-200-rgb);--skin-text:var(--color-gray-400)}.gray-300-pale{--skin-pale-rgb:var(--color-gray-300-rgb);--skin-text:var(--color-gray-500)}.gray-400-pale{--skin-pale-rgb:var(--color-gray-400-rgb);--skin-text:var(--color-gray-500)}.gray-500-pale,.gray-pale{--skin-pale-rgb:var(--color-gray-500-rgb)}.gray-600-pale{--skin-pale-rgb:var(--color-gray-600-rgb)}.gray-700-pale{--skin-pale-rgb:var(--color-gray-700-rgb)}.gray-800-pale{--skin-pale-rgb:var(--color-gray-800-rgb)}.gray-900-pale{--skin-pale-rgb:var(--color-gray-900-rgb)}.gray-950-pale{--skin-pale-rgb:var(--color-gray-950-rgb)}.danger-outline,.dark-outline,.gray-300-outline,.gray-400-outline,.gray-600-outline,.gray-outline,.important-outline,.light-outline,.outline,.primary-outline,.secondary-outline,.special-outline,.success-outline,.warning-outline{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-outline:hover,.dark-outline:hover,.gray-300-outline:hover,.gray-400-outline:hover,.gray-600-outline:hover,.gray-outline:hover,.important-outline:hover,.light-outline:hover,.outline:hover,.primary-outline:hover,.secondary-outline:hover,.special-outline:hover,.success-outline:hover,.warning-outline:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-outline,.dark-outline,.gray-300-outline,.gray-400-outline,.gray-600-outline,.gray-outline,.important-outline,.light-outline,.outline,.primary-outline,.secondary-outline,.special-outline,.success-outline,.warning-outline{--skin-text:var(--skin-color,inherit);--skin-bg:transparent;--skin-color:rgba(var(--skin-outline-rgb),1);--state-color:rgba(var(--skin-outline-rgb),.1)}.outline{--skin-outline-rgb:var(--color-fore-rgb)}.primary-outline{--skin-outline-rgb:var(--color-primary-500-rgb)}.secondary-outline{--skin-outline-rgb:var(--color-secondary-500-rgb)}.success-outline{--skin-outline-rgb:var(--color-success-500-rgb)}.warning-outline{--skin-outline-rgb:var(--color-warning-500-rgb)}.danger-outline{--skin-outline-rgb:var(--color-danger-500-rgb)}.important-outline{--skin-outline-rgb:var(--color-important-500-rgb)}.special-outline{--skin-outline-rgb:var(--color-special-500-rgb)}.gray-300-outline{--skin-text:var(--color-gray-500);--skin-outline-rgb:var(--color-gray-300-rgb)}.gray-400-outline{--skin-text:var(--color-gray-500);--skin-outline-rgb:var(--color-gray-400-rgb)}.gray-outline{--skin-outline-rgb:var(--color-gray-500-rgb)}.gray-600-outline{--skin-outline-rgb:var(--color-gray-600-rgb)}.dark-outline{--skin-outline-rgb:var(--color-inverse-rgb)}.light-outline{--skin-outline-rgb:var(--color-canvas-rgb)}.danger-ghost,.ghost,.important-ghost,.primary-ghost,.secondary-ghost,.special-ghost,.success-ghost,.warning-ghost{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-ghost:hover,.ghost:hover,.important-ghost:hover,.primary-ghost:hover,.secondary-ghost:hover,.special-ghost:hover,.success-ghost:hover,.warning-ghost:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-ghost,.ghost,.important-ghost,.primary-ghost,.secondary-ghost,.special-ghost,.success-ghost,.warning-ghost{--tw-ring-opacity:0;--tw-bg-opacity:0;--skin-text:var(--skin-color,inherit);--skin-bg:transparent;--skin-ring:transparent;--skin-color:rgba(var(--skin-ghost-rgb),1);--state-color:rgba(var(--skin-ghost-rgb),.1)}.ghost{--skin-ghost-rgb:var(--color-fore-rgb);--skin-text:inherit}.primary-ghost{--skin-ghost-rgb:var(--color-primary-500-rgb)}.secondary-ghost{--skin-ghost-rgb:var(--color-secondary-500-rgb)}.success-ghost{--skin-ghost-rgb:var(--color-success-500-rgb)}.warning-ghost{--skin-ghost-rgb:var(--color-warning-500-rgb)}.danger-ghost{--skin-ghost-rgb:var(--color-danger-500-rgb)}.important-ghost{--skin-ghost-rgb:var(--color-important-500-rgb)}.special-ghost{--skin-ghost-rgb:var(--color-special-500-rgb)}.font-sans{font-family:-apple-system,Noto Sans,Helvetica Neue,Helvetica,Nimbus Sans L,Arial,Liberation Sans,PingFang SC,Hiragino Sans GB,Noto Sans CJK SC,Source Han Sans SC,Source Han Sans CN,Microsoft YaHei,Wenquanyi Micro Hei,WenQuanYi Zen Hei,ST Heiti,SimHei,WenQuanYi Zen Hei Sharp,sans-serif}.font-serif{font-family:Nimbus Roman No9 L,Songti SC,Noto Serif CJK SC,Source Han Serif SC,Source Han Serif CN,STSong,AR PL New Sung,AR PL SungtiL GB,NSimSun,SimSun,TW-Sung,WenQuanYi Bitmap Song,AR PL UMing CN,AR PL UMing HK,AR PL UMing TW,AR PL UMing TW MBE,PMingLiU,MingLiU,serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-root{font-size:16px}.text-sm{font-size:.75rem;line-height:1rem}.text-base{font-size:.8125rem;line-height:1.25rem}.text-md{font-size:.875rem;line-height:1.3rem}.text-lg{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.125rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-thin{font-weight:100}.font-light{font-weight:300}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-black{font-weight:900}.leading-3{line-height:.75rem}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.leading-9{line-height:2.25rem}.leading-10{line-height:2.5rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.leading-snug{line-height:1.375}.leading-normal{line-height:1.5}.leading-relaxed{line-height:1.625}.leading-loose{line-height:2}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-3,.line-clamp-4{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-4{-webkit-line-clamp:4}.line-clamp-5{-webkit-line-clamp:5}.line-clamp-5,.line-clamp-6{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-6{-webkit-line-clamp:6}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-justify{text-align:justify}.align-middle{vertical-align:middle}.align-top{vertical-align:top}.align-bottom{vertical-align:bottom}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.text-primary{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.text-secondary{--tw-text-opacity:1;color:rgba(var(--color-secondary-500-rgb),var(--tw-text-opacity))}.text-success{--tw-text-opacity:1;color:rgba(var(--color-success-500-rgb),var(--tw-text-opacity))}.text-warning{--tw-text-opacity:1;color:rgba(var(--color-warning-500-rgb),var(--tw-text-opacity))}.text-danger{--tw-text-opacity:1;color:rgba(var(--color-danger-500-rgb),var(--tw-text-opacity))}.text-important{--tw-text-opacity:1;color:rgba(var(--color-important-500-rgb),var(--tw-text-opacity))}.text-special{--tw-text-opacity:1;color:rgba(var(--color-special-500-rgb),var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgba(var(--color-primary-50-rgb),var(--tw-text-opacity))}.text-primary-100{--tw-text-opacity:1;color:rgba(var(--color-primary-100-rgb),var(--tw-text-opacity))}.text-primary-200{--tw-text-opacity:1;color:rgba(var(--color-primary-200-rgb),var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgba(var(--color-primary-300-rgb),var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgba(var(--color-primary-400-rgb),var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgba(var(--color-primary-600-rgb),var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity:1;color:rgba(var(--color-primary-700-rgb),var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgba(var(--color-primary-800-rgb),var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgba(var(--color-primary-900-rgb),var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgba(var(--color-primary-950-rgb),var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgba(var(--color-secondary-50-rgb),var(--tw-text-opacity))}.text-secondary-100{--tw-text-opacity:1;color:rgba(var(--color-secondary-100-rgb),var(--tw-text-opacity))}.text-secondary-200{--tw-text-opacity:1;color:rgba(var(--color-secondary-200-rgb),var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgba(var(--color-secondary-300-rgb),var(--tw-text-opacity))}.text-secondary-400{--tw-text-opacity:1;color:rgba(var(--color-secondary-400-rgb),var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgba(var(--color-secondary-500-rgb),var(--tw-text-opacity))}.text-secondary-600{--tw-text-opacity:1;color:rgba(var(--color-secondary-600-rgb),var(--tw-text-opacity))}.text-secondary-700{--tw-text-opacity:1;color:rgba(var(--color-secondary-700-rgb),var(--tw-text-opacity))}.text-secondary-800{--tw-text-opacity:1;color:rgba(var(--color-secondary-800-rgb),var(--tw-text-opacity))}.text-secondary-900{--tw-text-opacity:1;color:rgba(var(--color-secondary-900-rgb),var(--tw-text-opacity))}.text-secondary-950{--tw-text-opacity:1;color:rgba(var(--color-secondary-950-rgb),var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgba(var(--color-gray-50-rgb),var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity:1;color:rgba(var(--color-gray-100-rgb),var(--tw-text-opacity))}.text-gray-200,.text-lightest{--tw-text-opacity:1;color:rgba(var(--color-gray-200-rgb),var(--tw-text-opacity))}.text-gray-300,.text-lighter{--tw-text-opacity:1;color:rgba(var(--color-gray-300-rgb),var(--tw-text-opacity))}.text-gray-400,.text-lighten{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.text-gray,.text-gray-500{--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.text-darken,.text-gray-600{--tw-text-opacity:1;color:rgba(var(--color-gray-600-rgb),var(--tw-text-opacity))}.text-darker,.text-gray-700{--tw-text-opacity:1;color:rgba(var(--color-gray-700-rgb),var(--tw-text-opacity))}.text-darkest,.text-gray-800{--tw-text-opacity:1;color:rgba(var(--color-gray-800-rgb),var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(var(--color-gray-900-rgb),var(--tw-text-opacity))}.text-gray-950{--tw-text-opacity:1;color:rgba(var(--color-gray-950-rgb),var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgba(var(--color-black-rgb),var(--tw-text-opacity))}.text-transparent{color:transparent}.text-canvas{--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.text-surface-light{--tw-text-opacity:1;color:rgba(var(--color-surface-light-rgb),var(--tw-text-opacity))}.text-surface{--tw-text-opacity:1;color:rgba(var(--color-surface-rgb),var(--tw-text-opacity))}.text-surface-strong{--tw-text-opacity:1;color:rgba(var(--color-surface-strong-rgb),var(--tw-text-opacity))}.text-inverse{--tw-text-opacity:1;color:rgba(var(--color-inverse-rgb),var(--tw-text-opacity))}.text-fore-in-light{--tw-text-opacity:1;color:rgba(var(--color-fore-in-light-rgb),var(--tw-text-opacity))}.text-fore-in-dark{--tw-text-opacity:1;color:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity))}.text-fore{--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))}.text-focus{--tw-text-opacity:1;color:rgba(var(--color-focus-rgb),var(--tw-text-opacity))}.text-link{--tw-text-opacity:1;color:rgba(var(--color-link-rgb),var(--tw-text-opacity))}.text-link-hover{--tw-text-opacity:1;color:rgba(var(--color-link-hover-rgb),var(--tw-text-opacity))}.text-current{color:currentColor}.text-inherit{color:inherit}.clip,.ellipsis,.text-clip,.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.clip,.text-clip{text-overflow:clip}.whitespace-normal{white-space:normal}.nowrap,.whitespace-nowrap{white-space:nowrap}.pre,.whitespace-pre{white-space:pre}.pre-line,.whitespace-pre-line{white-space:pre-line}.pre-wrap,.whitespace-pre-wrap{white-space:pre-wrap}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-opacity-0{--tw-text-opacity:0}.text-opacity-5{--tw-text-opacity:.05}.text-opacity-10{--tw-text-opacity:.1}.text-opacity-20{--tw-text-opacity:.2}.text-opacity-25{--tw-text-opacity:.25}.text-opacity-30{--tw-text-opacity:.3}.text-opacity-40{--tw-text-opacity:.4}.text-opacity-50{--tw-text-opacity:.5}.text-opacity-60{--tw-text-opacity:.6}.text-opacity-70{--tw-text-opacity:.7}.text-opacity-80{--tw-text-opacity:.8}.text-opacity-90{--tw-text-opacity:.9}.text-opacity-95{--tw-text-opacity:.95}.text-opacity-100{--tw-text-opacity:1}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.list-decimal{list-style-type:decimal}.list-unstyled{list-style-type:none;padding-left:0}.underline{text-decoration-line:underline}.overline{text-decoration-line:overline}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.bg-primary{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity))}.bg-secondary{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-500-rgb),var(--tw-bg-opacity))}.bg-success{--tw-bg-opacity:1;background-color:rgba(var(--color-success-500-rgb),var(--tw-bg-opacity))}.bg-warning{--tw-bg-opacity:1;background-color:rgba(var(--color-warning-500-rgb),var(--tw-bg-opacity))}.bg-danger{--tw-bg-opacity:1;background-color:rgba(var(--color-danger-500-rgb),var(--tw-bg-opacity))}.bg-important{--tw-bg-opacity:1;background-color:rgba(var(--color-important-500-rgb),var(--tw-bg-opacity))}.bg-special{--tw-bg-opacity:1;background-color:rgba(var(--color-special-500-rgb),var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.bg-primary-100{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-200-rgb),var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-300-rgb),var(--tw-bg-opacity))}.bg-primary-400{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-400-rgb),var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity))}.bg-primary-600{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-600-rgb),var(--tw-bg-opacity))}.bg-primary-700{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-700-rgb),var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-800-rgb),var(--tw-bg-opacity))}.bg-primary-900{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-900-rgb),var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-950-rgb),var(--tw-bg-opacity))}.bg-secondary-50{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-50-rgb),var(--tw-bg-opacity))}.bg-secondary-100{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-100-rgb),var(--tw-bg-opacity))}.bg-secondary-200{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-200-rgb),var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-300-rgb),var(--tw-bg-opacity))}.bg-secondary-400{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-400-rgb),var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-500-rgb),var(--tw-bg-opacity))}.bg-secondary-600{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-600-rgb),var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-700-rgb),var(--tw-bg-opacity))}.bg-secondary-800{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-800-rgb),var(--tw-bg-opacity))}.bg-secondary-900{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-900-rgb),var(--tw-bg-opacity))}.bg-secondary-950{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-950-rgb),var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity))}.bg-gray-200,.bg-lightest{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-200-rgb),var(--tw-bg-opacity))}.bg-gray-300,.bg-lighter{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-300-rgb),var(--tw-bg-opacity))}.bg-gray-400,.bg-light{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-400-rgb),var(--tw-bg-opacity))}.bg-gray,.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-500-rgb),var(--tw-bg-opacity))}.bg-darken,.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-600-rgb),var(--tw-bg-opacity))}.bg-darker,.bg-gray-700{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-700-rgb),var(--tw-bg-opacity))}.bg-darkest,.bg-gray-800{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-800-rgb),var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-900-rgb),var(--tw-bg-opacity))}.bg-gray-950{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-950-rgb),var(--tw-bg-opacity))}.bg-canvas{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.bg-surface-light{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-light-rgb),var(--tw-bg-opacity))}.bg-surface{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity))}.bg-surface-strong{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-strong-rgb),var(--tw-bg-opacity))}.bg-inverse{--tw-bg-opacity:1;background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity))}.bg-fore-in-light{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-in-light-rgb),var(--tw-bg-opacity))}.bg-fore-in-dark{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-in-dark-rgb),var(--tw-bg-opacity))}.bg-fore{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-rgb),var(--tw-bg-opacity))}.bg-focus{--tw-bg-opacity:1;background-color:rgba(var(--color-focus-rgb),var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(var(--color-white-rgb),var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgba(var(--color-black-rgb),var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-current{background-color:currentColor}.bg-inherit{background-color:inherit}.bg-none{background-image:none}.bg-opacity-0{--tw-bg-opacity:0}.bg-opacity-5{--tw-bg-opacity:.05}.bg-opacity-10{--tw-bg-opacity:.1}.bg-opacity-20{--tw-bg-opacity:.2}.bg-opacity-30{--tw-bg-opacity:.3}.bg-opacity-40{--tw-bg-opacity:.4}.bg-opacity-50{--tw-bg-opacity:.5}.bg-opacity-60{--tw-bg-opacity:.6}.bg-opacity-70{--tw-bg-opacity:.7}.bg-opacity-80{--tw-bg-opacity:.8}.bg-opacity-90{--tw-bg-opacity:.9}.bg-opacity-100{--tw-bg-opacity:1}.bg-auto{background-size:auto}.bg-cover{background-size:cover}.bg-contain{background-size:contain}.bg-repeat{background-repeat:repeat}.bg-no-repeat{background-repeat:no-repeat}.bg-repeat-x{background-repeat:repeat-x}.bg-repeat-y{background-repeat:repeat-y}.bg-repeat-round{background-repeat:round}.bg-repeat-space{background-repeat:space}.bg-bottom{background-position:bottom}.bg-center{background-position:50%}.bg-left{background-position:0}.bg-left-bottom{background-position:0 100%}.bg-left-top{background-position:0 0}.bg-right{background-position:100%}.bg-right-bottom{background-position:100% 100%}.bg-right-top{background-position:100% 0}.bg-top{background-position:top}.bg-fixed{background-attachment:fixed}.bg-local{background-attachment:local}.bg-scroll{background-attachment:scroll}.divider{align-self:stretch;border-color:currentColor;border-left-width:1px;border-top-width:1px;opacity:.1}.border-0{border-width:0}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-t{border-top-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.border-t-0{border-top-width:0}.border-r-0{border-right-width:0}.border-b-0{border-bottom-width:0}.border-l-0{border-left-width:0}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-r-2{border-right-width:2px}.border-b-2{border-bottom-width:2px}.border-t-4{border-top-width:4px}.border-l-4{border-left-width:4px}.border-r-4{border-right-width:4px}.border-b-4{border-bottom-width:4px}.border-light{--tw-border-opacity:1;border-color:rgba(var(--color-border-light-rgb),var(--tw-border-opacity))}.border-strong{--tw-border-opacity:1;border-color:rgba(var(--color-border-strong-rgb),var(--tw-border-opacity))}.border-primary{--tw-border-opacity:1;border-color:rgba(var(--color-primary-500-rgb),var(--tw-border-opacity))}.border-secondary{--tw-border-opacity:1;border-color:rgba(var(--color-secondary-500-rgb),var(--tw-border-opacity))}.border-success{--tw-border-opacity:1;border-color:rgba(var(--color-success-500-rgb),var(--tw-border-opacity))}.border-warning{--tw-border-opacity:1;border-color:rgba(var(--color-warning-500-rgb),var(--tw-border-opacity))}.border-danger{--tw-border-opacity:1;border-color:rgba(var(--color-danger-500-rgb),var(--tw-border-opacity))}.border-important{--tw-border-opacity:1;border-color:rgba(var(--color-important-500-rgb),var(--tw-border-opacity))}.border-special{--tw-border-opacity:1;border-color:rgba(var(--color-special-500-rgb),var(--tw-border-opacity))}.border-gray-50{--tw-border-opacity:1;border-color:rgba(var(--color-gray-50-rgb),var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgba(var(--color-gray-100-rgb),var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(var(--color-gray-200-rgb),var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(var(--color-gray-300-rgb),var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity))}.border-gray,.border-gray-500{--tw-border-opacity:1;border-color:rgba(var(--color-gray-500-rgb),var(--tw-border-opacity))}.border-gray-600{--tw-border-opacity:1;border-color:rgba(var(--color-gray-600-rgb),var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgba(var(--color-gray-700-rgb),var(--tw-border-opacity))}.border-gray-800{--tw-border-opacity:1;border-color:rgba(var(--color-gray-800-rgb),var(--tw-border-opacity))}.border-gray-900{--tw-border-opacity:1;border-color:rgba(var(--color-gray-900-rgb),var(--tw-border-opacity))}.border-gray-950{--tw-border-opacity:1;border-color:rgba(var(--color-gray-950-rgb),var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgba(var(--color-white-rgb),var(--tw-border-opacity))}.border-black{--tw-border-opacity:1;border-color:rgba(var(--color-black-rgb),var(--tw-border-opacity))}.border-canvas{--tw-border-opacity:1;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity))}.border-surface-light{--tw-border-opacity:1;border-color:rgba(var(--color-surface-light-rgb),var(--tw-border-opacity))}.border-surface{--tw-border-opacity:1;border-color:rgba(var(--color-surface-rgb),var(--tw-border-opacity))}.border-surface-strong{--tw-border-opacity:1;border-color:rgba(var(--color-surface-strong-rgb),var(--tw-border-opacity))}.border-inverse{--tw-border-opacity:1;border-color:rgba(var(--color-inverse-rgb),var(--tw-border-opacity))}.border-fore-in-light{--tw-border-opacity:1;border-color:rgba(var(--color-fore-in-light-rgb),var(--tw-border-opacity))}.border-fore-in-dark{--tw-border-opacity:1;border-color:rgba(var(--color-fore-in-dark-rgb),var(--tw-border-opacity))}.border-fore{--tw-border-opacity:1;border-color:rgba(var(--color-fore-rgb),var(--tw-border-opacity))}.border-focus{--tw-border-opacity:1;border-color:rgba(var(--color-focus-rgb),var(--tw-border-opacity))}.border-link{--tw-border-opacity:1;border-color:rgba(var(--color-link-rgb),var(--tw-border-opacity))}.border-link-hover{--tw-border-opacity:1;border-color:rgba(var(--color-link-hover-rgb),var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-current{border-color:currentColor}.border-inherit{border-color:inherit}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-double{border-style:double}.border-hidden{border-style:hidden}.border-none{border-style:none}.border-opacity-0{--tw-border-opacity:0}.border-opacity-5{--tw-border-opacity:.05}.border-opacity-10{--tw-border-opacity:.1}.border-opacity-20{--tw-border-opacity:.2}.border-opacity-25{--tw-border-opacity:.25}.border-opacity-30{--tw-border-opacity:.3}.border-opacity-40{--tw-border-opacity:.4}.border-opacity-50{--tw-border-opacity:.5}.border-opacity-60{--tw-border-opacity:.6}.border-opacity-70{--tw-border-opacity:.7}.border-opacity-80{--tw-border-opacity:.8}.border-opacity-90{--tw-border-opacity:.9}.border-opacity-95{--tw-border-opacity:.95}.border-opacity-100{--tw-border-opacity:1}.rounded-none{border-radius:var(--radius-none)}.rounded-sm{border-radius:var(--radius-sm)}.rounded{border-radius:var(--radius)}.rounded-md{border-radius:var(--radius-md)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.circle,.rounded-full{border-radius:var(--radius-full)}.rounded-l-none{border-bottom-left-radius:var(--radius-none);border-top-left-radius:var(--radius-none)}.rounded-t-none{border-top-left-radius:var(--radius-none)}.rounded-r-none,.rounded-t-none{border-top-right-radius:var(--radius-none)}.rounded-b-none,.rounded-r-none{border-bottom-right-radius:var(--radius-none)}.rounded-b-none{border-bottom-left-radius:var(--radius-none)}.rounded-br-none{border-bottom-right-radius:var(--radius-none)}.rounded-bl-none{border-bottom-left-radius:var(--radius-none)}.rounded-tr-none{border-top-right-radius:var(--radius-none)}.rounded-tl-none{border-top-left-radius:var(--radius-none)}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.ring-border-light{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-light-rgb),var(--tw-ring-opacity))}.ring-border{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.ring-border-strong{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.ring-light{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05}.ring-gray{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.1}.ring-dark{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.2}.ring-darker{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.3}.ring-darkest{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.5}.ring-primary{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-500-rgb),var(--tw-ring-opacity))}.ring-secondary{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-secondary-500-rgb),var(--tw-ring-opacity))}.ring-warning{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-warning-500-rgb),var(--tw-ring-opacity))}.ring-success{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-success-500-rgb),var(--tw-ring-opacity))}.ring-danger{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-danger-500-rgb),var(--tw-ring-opacity))}.ring-important{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-important-500-rgb),var(--tw-ring-opacity))}.ring-special{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-special-500-rgb),var(--tw-ring-opacity))}.ring-gray-50{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-50-rgb),var(--tw-ring-opacity))}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-100-rgb),var(--tw-ring-opacity))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-200-rgb),var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-300-rgb),var(--tw-ring-opacity))}.ring-gray-400{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-400-rgb),var(--tw-ring-opacity))}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-500-rgb),var(--tw-ring-opacity))}.ring-gray-600{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-600-rgb),var(--tw-ring-opacity))}.ring-gray-700{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-700-rgb),var(--tw-ring-opacity))}.ring-gray-800{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-800-rgb),var(--tw-ring-opacity))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-900-rgb),var(--tw-ring-opacity))}.ring-gray-950{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-950-rgb),var(--tw-ring-opacity))}.ring-inverse{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity))}.ring-canvas{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-canvas-rgb),var(--tw-ring-opacity))}.ring-surface{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-rgb),var(--tw-ring-opacity))}.ring-fore{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-fore-rgb),var(--tw-ring-opacity))}.ring-transparent{--tw-ring-color:transparent}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-black-rgb),var(--tw-ring-opacity))}.ring-white{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-white-rgb),var(--tw-ring-opacity))}.ring-inherit{--tw-ring-color:inherit}.ring-current{--tw-ring-color:currentColor}.ring-opacity-0{--tw-ring-opacity:0}.ring-opacity-5{--tw-ring-opacity:.05}.ring-opacity-10{--tw-ring-opacity:.1}.ring-opacity-20{--tw-ring-opacity:.2}.ring-opacity-30{--tw-ring-opacity:.3}.ring-opacity-40{--tw-ring-opacity:.4}.ring-opacity-50{--tw-ring-opacity:.5}.ring-opacity-60{--tw-ring-opacity:.6}.ring-opacity-70{--tw-ring-opacity:.7}.ring-opacity-80{--tw-ring-opacity:.8}.ring-opacity-90{--tw-ring-opacity:.9}.ring-opacity-100{--tw-ring-opacity:1}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-2{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-3{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-3,.ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-8{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-offset-0{--tw-ring-offset-width:0px}.ring-offset-1{--tw-ring-offset-width:1px}.ring-offset-2{--tw-ring-offset-width:2px}.ring-offset-4{--tw-ring-offset-width:4px}.ring-offset-8{--tw-ring-offset-width:8px}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 2px 6px -1px rgba(0,0,0,.2),0 1px 4px -2px rgba(0,0,0,.15);--tw-shadow-colored:0 2px 6px -1px var(--tw-shadow-color),0 1px 4px -2px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 5px 15px -3px rgba(0,0,0,.2),0 2px 6px -4px rgba(0,0,0,.15);--tw-shadow-colored:0 5px 15px -3px var(--tw-shadow-color),0 2px 6px -4px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color)}.shadow-2xl,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 10px 50px -10px rgba(0,0,0,.4),0 8px 20px -10px rgba(0,0,0,.3);--tw-shadow-colored:0 10px 50px -10px var(--tw-shadow-color),0 8px 20px -10px var(--tw-shadow-color)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-none{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.muted{opacity:var(--opacity-muted)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.blur-none{--tw-blur:blur(0)}.blur-none,.blur-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-sm{--tw-blur:blur(4px)}.blur{--tw-blur:blur(8px)}.blur,.blur-md{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-md{--tw-blur:blur(12px)}.blur-lg{--tw-blur:blur(16px)}.blur-lg,.blur-xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-xl{--tw-blur:blur(24px)}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter-none{filter:none!important}.drop-shadow-none{--tw-drop-shadow:drop-shadow(0 0 #0000)}.drop-shadow-none,.drop-shadow-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px rgba(0,0,0,.05))}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.drop-shadow,.drop-shadow-md{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-md{--tw-drop-shadow:drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06))}.drop-shadow-lg{--tw-drop-shadow:drop-shadow(0 10px 8px rgba(0,0,0,.04)) drop-shadow(0 4px 3px rgba(0,0,0,.1))}.drop-shadow-lg,.drop-shadow-xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-xl{--tw-drop-shadow:drop-shadow(0 20px 13px rgba(0,0,0,.03)) drop-shadow(0 8px 5px rgba(0,0,0,.08))}.drop-shadow-2xl{--tw-drop-shadow:drop-shadow(0 25px 25px rgba(0,0,0,.15));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-none{--tw-backdrop-blur:blur(0)}.backdrop-blur-none,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-md{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-lg,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(40px)}.backdrop-blur-2xl,.backdrop-blur-3xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(64px)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-none{transition-property:none}.fade,.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{opacity:0;transition-duration:.15s;transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{--tw-scale-x:.9;--tw-scale-y:.9}.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{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))}.fade-from-bottom{--tw-translate-y:25%}.fade-from-top{--tw-translate-y:-25%}.fade-from-left,.fade-from-top{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))}.fade-from-left{--tw-translate-x:-25%}.fade-from-right{--tw-translate-x:25%}.fade-from-bottom.in,.fade-from-center.in,.fade-from-left.in,.fade-from-right,.fade-from-right.in,.fade-from-top.in,.fade.in{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))}.fade-from-bottom.in,.fade-from-center.in,.fade-from-left.in,.fade-from-right.in,.fade-from-top.in,.fade.in{--tw-translate-x:0px;--tw-translate-y:0px;--tw-scale-x:1;--tw-scale-y:1;opacity:1}.duration-75{transition-duration:75ms}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-1000{transition-duration:1s}@keyframes -spin{to{transform:rotate(1turn)}}.animate-spin,.spin{animation:-spin 1s linear infinite}@keyframes -ping{75%,to{opacity:0;transform:scale(2)}}.animate-ping,.ping{animation:-ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes -pulse{50%{opacity:.5}}.animate-pulse,.pulse{animation:-pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes -bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-bounce,.bounce{animation:-bounce 1s infinite}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-50{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))}.scale-50{--tw-scale-x:.5;--tw-scale-y:.5}.scale-75{--tw-scale-x:.75;--tw-scale-y:.75}.scale-75,.scale-90{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))}.scale-90{--tw-scale-x:.9;--tw-scale-y:.9}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100,.scale-95{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))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.scale-105,.scale-110{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))}.scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.scale-125{--tw-scale-x:1.25;--tw-scale-y:1.25}.scale-125,.scale-150{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))}.scale-150{--tw-scale-x:1.5;--tw-scale-y:1.5}.flip-x{--tw-scale-x:-1}.flip-x,.flip-y{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))}.flip-y{--tw-scale-y:-1}.rotate-0{--tw-rotate:0deg}.rotate-0,.rotate-1{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))}.rotate-1{--tw-rotate:1deg}.rotate-2{--tw-rotate:2deg}.rotate-2,.rotate-3{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))}.rotate-3{--tw-rotate:3deg}.rotate-6{--tw-rotate:6deg}.rotate-12,.rotate-6{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))}.rotate-12{--tw-rotate:12deg}.rotate-45{--tw-rotate:45deg}.rotate-45,.rotate-90{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))}.rotate-90{--tw-rotate:90deg}.rotate-135{--tw-rotate:135deg}.rotate-135,.rotate-180{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))}.rotate-180{--tw-rotate:180deg}.rotate-270{--tw-rotate:270deg;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))}.basis-0{flex-basis:0px}.basis-1{flex-basis:0.25rem}.basis-2{flex-basis:0.5rem}.basis-3{flex-basis:0.75rem}.basis-4{flex-basis:1rem}.basis-5{flex-basis:1.25rem}.basis-6{flex-basis:1.5rem}.basis-7{flex-basis:1.75rem}.basis-8{flex-basis:2rem}.basis-9{flex-basis:2.25rem}.basis-10{flex-basis:2.5rem}.basis-11{flex-basis:2.75rem}.basis-12{flex-basis:3rem}.basis-14{flex-basis:3.5rem}.basis-16{flex-basis:4rem}.basis-20{flex-basis:5rem}.basis-24{flex-basis:6rem}.basis-28{flex-basis:7rem}.basis-32{flex-basis:8rem}.basis-36{flex-basis:9rem}.basis-40{flex-basis:10rem}.basis-44{flex-basis:11rem}.basis-48{flex-basis:12rem}.basis-52{flex-basis:13rem}.basis-56{flex-basis:14rem}.basis-60{flex-basis:15rem}.basis-64{flex-basis:16rem}.basis-72{flex-basis:18rem}.basis-80{flex-basis:20rem}.basis-96{flex-basis:24rem}.basis-auto{flex-basis:auto}.basis-px{flex-basis:1px}.basis-0\.5{flex-basis:0.125rem}.basis-1\.5{flex-basis:0.375rem}.basis-2\.5{flex-basis:0.625rem}.basis-3\.5{flex-basis:0.875rem}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.333333%}.basis-2\/3{flex-basis:66.666667%}.basis-1\/4{flex-basis:25%}.basis-2\/4{flex-basis:50%}.basis-3\/4{flex-basis:75%}.basis-1\/5{flex-basis:20%}.basis-2\/5{flex-basis:40%}.basis-3\/5{flex-basis:60%}.basis-4\/5{flex-basis:80%}.basis-1\/6{flex-basis:16.666667%}.basis-2\/6{flex-basis:33.333333%}.basis-3\/6{flex-basis:50%}.basis-4\/6{flex-basis:66.666667%}.basis-5\/6{flex-basis:83.333333%}.basis-1\/12{flex-basis:8.333333%}.basis-2\/12{flex-basis:16.666667%}.basis-3\/12{flex-basis:25%}.basis-4\/12{flex-basis:33.333333%}.basis-5\/12{flex-basis:41.666667%}.basis-6\/12{flex-basis:50%}.basis-7\/12{flex-basis:58.333333%}.basis-8\/12{flex-basis:66.666667%}.basis-9\/12{flex-basis:75%}.basis-10\/12{flex-basis:83.333333%}.basis-11\/12{flex-basis:91.666667%}.basis-full{flex-basis:100%}.row{display:flex;flex-direction:row}.center,.col{display:flex;flex-direction:column}.center,.center-row{align-items:center;justify-content:center}.center-row{display:flex;flex-direction:row}.center-x,.center-y{align-items:center;display:flex}.center-y{flex-direction:column}.row-reverse{display:flex;flex-direction:row-reverse}.col-reverse{display:flex;flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-initial{flex:0 1 auto}.flex-none{flex:none}.grow{flex-grow:1!important}.grow-0{flex-grow:0!important}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.gap-0{gap:0}.gap-x-0{-moz-column-gap:0;column-gap:0}.gap-y-0{row-gap:0}.gap-px{gap:1px}.gap-x-px{-moz-column-gap:1px;column-gap:1px}.gap-y-px{row-gap:1px}.gap-0\.5{gap:.125rem}.gap-x-0\.5{-moz-column-gap:.125rem;column-gap:.125rem}.gap-y-0\.5{row-gap:.125rem}.gap-3px{gap:3px}.gap-1{gap:.25rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-y-1{row-gap:.25rem}.gap-1\.5{gap:.375rem}.gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-y-1\.5{row-gap:.375rem}.gap-2{gap:.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-2{row-gap:.5rem}.gap-2\.5{gap:.625rem}.gap-x-2\.5{-moz-column-gap:.625rem;column-gap:.625rem}.gap-y-2\.5{row-gap:.625rem}.gap-3{gap:.75rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-3{row-gap:.75rem}.gap-3\.5{gap:.875rem}.gap-x-3\.5{-moz-column-gap:.875rem;column-gap:.875rem}.gap-y-3\.5{row-gap:.875rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-4{row-gap:1rem}.gap-5{gap:1.25rem}.gap-x-5{-moz-column-gap:1.25rem;column-gap:1.25rem}.gap-y-5{row-gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-6{row-gap:1.5rem}.gap-7{gap:1.75rem}.gap-x-7{-moz-column-gap:1.75rem;column-gap:1.75rem}.gap-y-7{row-gap:1.75rem}.gap-8{gap:2rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-8{row-gap:2rem}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.justify-evenly{justify-content:space-evenly}.self-auto{align-self:auto}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.self-stretch{align-self:stretch}.self-baseline{align-self:baseline}.content-center{align-content:center}.content-start{align-content:flex-start}.content-end{align-content:flex-end}.content-between{align-content:space-between}.content-around{align-content:space-around}.content-evenly{align-content:space-evenly}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}@font-face{font-display:fallback;font-family:D-DIN-PRO;font-weight:400;src:url(./fonts/D-DIN-PRO-400-Regular.otf)}@font-face{font-display:fallback;font-family:D-DIN-PRO;font-weight:500;src:url(./fonts/D-DIN-PRO-700-Bold.otf)}.num{font-family:D-DIN-PRO}@font-face{font-display:fallback;font-family:Nunito;font-weight:400;src:url(./fonts/Nunito-Regular.ttf)}@font-face{font-display:fallback;font-family:Nunito;font-weight:700;src:url(./fonts/Nunito-bold.ttf)}@font-face{font-display:fallback;font-family:Nunito;font-weight:300;src:url(./fonts/Nunito-Light.ttf)}.font-brand{font-family:Nunito;font-weight:400}@supports not (gap:4px){.gap-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(1px*var(--tw-space-x-reverse))}.gap-0\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2px*(1 - var(--tw-space-x-reverse)));margin-right:calc(2px*var(--tw-space-x-reverse))}.gap-3px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(3px*(1 - var(--tw-space-x-reverse)));margin-right:calc(3px*var(--tw-space-x-reverse))}.gap-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.gap-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(6px*(1 - var(--tw-space-x-reverse)));margin-right:calc(6px*var(--tw-space-x-reverse))}.gap-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.gap-2\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(10px*(1 - var(--tw-space-x-reverse)));margin-right:calc(10px*var(--tw-space-x-reverse))}.gap-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.gap-3\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(14px*(1 - var(--tw-space-x-reverse)));margin-right:calc(14px*var(--tw-space-x-reverse))}.gap-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.gap-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.25rem*var(--tw-space-x-reverse))}.gap-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.gap-7>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.75rem*var(--tw-space-x-reverse))}.gap-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2rem*var(--tw-space-x-reverse))}.col.gap-px>:not([hidden])~:not([hidden]),.gap-px.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1px*var(--tw-space-y-reverse));margin-top:calc(1px*(1 - var(--tw-space-y-reverse)))}.col.gap-0\.5>:not([hidden])~:not([hidden]),.gap-0\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2px*var(--tw-space-y-reverse));margin-top:calc(2px*(1 - var(--tw-space-y-reverse)))}.col.gap-3px>:not([hidden])~:not([hidden]),.gap-3px.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(3px*var(--tw-space-y-reverse));margin-top:calc(3px*(1 - var(--tw-space-y-reverse)))}.col.gap-1>:not([hidden])~:not([hidden]),.gap-1.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.col.gap-1\.5>:not([hidden])~:not([hidden]),.gap-1\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(6px*var(--tw-space-y-reverse));margin-top:calc(6px*(1 - var(--tw-space-y-reverse)))}.col.gap-2>:not([hidden])~:not([hidden]),.gap-2.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.col.gap-2\.5>:not([hidden])~:not([hidden]),.gap-2\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(10px*var(--tw-space-y-reverse));margin-top:calc(10px*(1 - var(--tw-space-y-reverse)))}.col.gap-3>:not([hidden])~:not([hidden]),.gap-3.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.col.gap-3\.5>:not([hidden])~:not([hidden]),.gap-3\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(14px*var(--tw-space-y-reverse));margin-top:calc(14px*(1 - var(--tw-space-y-reverse)))}.col.gap-4>:not([hidden])~:not([hidden]),.gap-4.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.col.gap-5>:not([hidden])~:not([hidden]),.gap-5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.col.gap-6>:not([hidden])~:not([hidden]),.gap-6.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.col.gap-7>:not([hidden])~:not([hidden]),.gap-7.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.75rem*var(--tw-space-y-reverse));margin-top:calc(1.75rem*(1 - var(--tw-space-y-reverse)))}.col.gap-8>:not([hidden])~:not([hidden]),.gap-8.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.nav-item>a>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}} \ No newline at end of file +.hint-md:after:after,.hint-xl:after{white-space:normal;word-break:break-all}.hint-sm:after{width:4rem}.hint-md:after{width:8rem}.hint-lg:after{width:16rem}.hint-xl:after{width:24rem}.hint-primary{--tooltip-bg:var(--color-primary-500)}.hint-success{--tooltip-bg:var(--color-success-500)}.hint-danger{--tooltip-bg:var(--color-danger-500)}.hint-warning{--tooltip-bg:var(--color-warning-500)}.hint-important{--tooltip-bg:var(--color-important-500)}.hint-special{--tooltip-bg:var(--color-special-500)}.tooltip{background-color:var(--tooltip-bg);border-color:var(--tooltip-color);border-radius:var(--radius);color:var(--tooltip-text);font-size:.75rem;line-height:1rem;max-width:28rem;padding:.25rem .5rem;z-index:1800}.tooltip:not(.white)>.arrow{border-width:0}.tooltip.white>.arrow{border-color:inherit}.tooltip-has-title{--tw-bg-opacity:0.8;padding:.5rem .75rem .75rem;--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.tooltip-has-title>.arrow{opacity:.75}.tooltip-heading{align-items:center;display:flex;font-size:.8125rem;font-weight:700;gap:1rem;justify-content:space-between;line-height:1.25rem;padding:.25rem}.tooltip-heading>.btn-close{margin-right:-.25rem}.tooltip-content{padding:.25rem}.tree-item-inner{flex-direction:row;min-height:24px;padding-left:var(--list-nested-indent)}.tree-item-inner,.tree-toggle{align-items:center;display:flex}.tree-toggle{border-radius:var(--radius);flex:none;height:1rem;justify-content:center;margin-right:-.375rem;width:1rem;--caret-size:5px;--caret-opacity:.3}.tree-item-inner>.item-icon{opacity:.5;width:1rem}.tree-item-inner:hover>.item-icon{opacity:.8}.tree-item-inner.has-hover{border-radius:var(--radius)}.tree-item-inner .item-title:not(:hover),.tree-item-inner>a.item-title:not(:hover){color:inherit}.tree-item-inner>.toolbar{margin-left:1rem;opacity:.6}.tree-item-inner:hover>.toolbar{opacity:1}.tree-item>.active{background:var(--menu-active-bg);color:var(--menu-active-color)}.tree-item>.selected{background:var(--menu-selected-bg)}.tree-item>.selected .item-content{color:var(--menu-selected-color);font-weight:700}.tree.search-menu-on-top>.search-box{left:0;right:0}.tree-lines .tree{position:relative}.tree-lines .tree-item{--tree-line-indent:calc(var(--list-nested-indent) + 8px)}.tree-lines .tree:before{border-left-width:1px;bottom:.5rem;display:block;left:var(--tree-line-indent);position:absolute;top:-.5rem;--tw-content:"";content:var(--tw-content)}.upload .upload-tip{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.upload .draggable-area{background-color:rgba(var(--color-primary-50-rgb),.2);border:1px dashed var(--color-primary-300)}.upload .draggable-area.dragover{background-color:rgba(var(--color-primary-500-rgb),.1);border-style:solid}.upload .upload-tip{color:var(--color-slate-500)}.file-item>.img{height:120px;width:120px}.upload .file-item:hover .file-size{display:flex!important}.form-batch-container{overflow-y:auto}.form-batch-table{min-width:100%;table-layout:fixed;--table-head-bg:var(--color-canvas)}.form-batch-table>*>tr{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.form-batch-table>*>tr>*{padding-left:.375rem;padding-right:.375rem;vertical-align:top}.form-batch-table>*>tr>th{text-align:left;vertical-align:middle}.form-batch-head[data-name=ACTIONS]{width:5rem}.form-batch-head[data-name=ACTIONS][data-action-count="3"]{width:6rem}.form-batch-label{display:inline}.form-batch-control>textarea.form-control{height:2rem}.form-batch-btn+.btn{margin-left:.25rem}.form-batch-table>tbody[data-count="1"] .form-batch-btn[data-type=delete]{display:none}.form-batch-ditto{position:relative}.form-batch-ditto-btn{bottom:0;height:auto;opacity:.5;padding-left:.5rem;padding-right:.5rem;position:absolute!important;right:0;top:0}.form-batch-ditto-btn:after{bottom:.5rem;left:-1px;position:absolute;top:.5rem;width:1px;--tw-bg-opacity:1;background-color:rgba(var(--color-border-rgb),var(--tw-bg-opacity));--tw-content:"";content:var(--tw-content)}[data-ditto=on] .form-batch-ditto-btn{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity));opacity:1}.form-batch-row-actions{padding-bottom:.75rem!important;padding-top:.75rem!important}.form-batch-ditto-wrapper{position:relative}.form-batch-ditto-control .form-control{padding-right:calc(var(--form-batch-ditto-width) + 8px)}.form-batch-ditto-control .input-control>.input-control-suffix{right:var(--form-batch-ditto-width)}.form-batch-ditto-control .input-control>.form-control{padding-right:calc(var(--form-batch-ditto-width) + var(--input-control-suffix))}.form-batch-ditto-control .pick-pri{padding-right:calc(var(--form-batch-ditto-width) + 8px)}.dropmenu-btn{gap:.25rem;padding-left:.5rem;padding-right:.5rem}.dropmenu-btn>.text{max-width:180px;overflow:hidden}.dropmenu-btn>.is-leading{left:-.625rem;opacity:.6;position:absolute}.dropmenu-btn>.is-caret{align-items:center;background-color:rgba(var(--color-canvas-rgb),.2);border-radius:var(--radius);display:flex;height:1rem;justify-content:center;width:1rem}.dropmenu-nav{border-bottom-width:1px;margin-left:.75rem;margin-right:.75rem;margin-top:.25rem}.has-search .dropmenu-nav{margin-top:-.5rem}.dropmenu-nav>.nav-secondary{gap:.5rem}.dropmenu-nav>.nav-secondary>.nav-item>a{border-radius:var(--radius);padding-left:.5rem;padding-right:.5rem}.dropmenu-nav>.nav-secondary>.nav-item>a:after{bottom:-1px;left:1rem;opacity:0;right:1rem}.dropmenu-nav>.nav-secondary>.nav-item>.active:after{left:.5rem;opacity:1;right:.5rem;transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dropmenu-title{font-size:.75rem;font-weight:700;line-height:1rem;margin-bottom:-.5rem;padding:.75rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.dropmenu-list{min-height:0;overflow-x:hidden;overflow-y:auto;padding:.5rem .75rem}.dropmenu.loading>*{opacity:0}.dropmenu-item .tree-item-inner{border-radius:var(--radius);cursor:pointer;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dropmenu-item.is-item.active>.tree-item-inner{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.tree-item-inner.is-link:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.tree-item-inner.is-link:hover .item-content,.tree-item-inner.is-link:hover .item-trailing-icon,.tree-item-inner.is-link:hover .label{color:inherit}.tree-item-inner.is-toggle:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity))}.dropmenu-item+.tree-item{margin-top:.125rem}.dropmenu-item .is-match{font-weight:700;text-decoration-line:underline}.dropmenu-item .item-title,.dropmenu-item .label{overflow:hidden;white-space:nowrap}.dropmenu-item .item-content{padding-left:.375rem;padding-right:.375rem}.dropmenu-item *+.item-content{padding-left:0}.tree-item-inner.is-toggle .item-title{opacity:.6}.tree-item-inner.is-toggle:hover .item-title{opacity:1}.dropmenu-item-check{align-items:center;display:flex;flex:1 1 auto;justify-content:flex-end;padding-right:.5rem}.dropmenu-item .nested-toggle-icon.is-empty{width:.25rem}.dropmenu-item .item-icon{margin-right:-.25rem}:root{--form-control-border:var(--color-border-strong)}.checkbox-primary.disabled,.checkbox-primary.disabled>label,.checkbox-primary>input[type=checkbox]:disabled,.checkbox-primary>input[type=checkbox]:disabled+label,.radio-primary.disabled,.radio-primary.disabled>label,.radio-primary>input[type=radio],.radio-primary>input[type=radio]:disabled+label{filter:none!important}.checkbox-primary.disabled>label,.radio-primary.disabled>label{opacity:1}.form-actions{gap:1rem;justify-content:center}.form-actions .btn{min-width:96px}.form-control-static{min-height:32px;padding-bottom:.375rem;padding-top:.375rem}:root{--zt-panel-form-max-width:1000px}.size-sm{--zt-panel-form-max-width:800px}.size-lg{--zt-panel-form-max-width:1200px}.panel-body>.container,.panel-heading>.container{max-width:var(--panel-container-width);min-width:0;padding-left:var(--panel-container-padding);padding-right:var(--panel-container-padding)}.panel-heading>.container{align-items:center;display:flex;flex-direction:row;flex-wrap:nowrap;gap:1rem;justify-content:space-between;min-height:48px}.panel-form{border-radius:var(--radius-md);margin-bottom:1rem;margin-left:auto;margin-right:auto;max-width:var(--zt-panel-form-max-width);min-width:var(--zt-panel-form-min-width);overflow:visible;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding-bottom:1rem;padding-left:1rem;padding-right:1rem;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.panel-form .form-actions{background-color:rgba(var(--color-canvas-rgb),.8);bottom:0;padding-bottom:1rem;padding-top:1rem;position:sticky;z-index:3;--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.panel-form .form-grid{margin-left:calc(0px - var(--form-grid-gap-y-half));margin-right:calc(0px - var(--form-grid-gap-y-half));padding-bottom:0}.panel-form-toolbar{padding-left:var(--form-grid-gap-x-half);padding-right:var(--form-grid-gap-x-half);width:100%}.panel-form-toolbar>.toolbar{gap:1rem;padding-bottom:.75rem;padding-top:.75rem}.panel-form-divider{margin-bottom:.75rem;margin-top:1rem;padding-left:var(--form-grid-gap-x-half);padding-right:var(--form-grid-gap-x-half);width:100%}.panel-form-divider:before{background-color:rgba(var(--color-border-rgb),.7);display:block;height:1px;--tw-content:"";content:var(--tw-content)}.is-full .btn-full-form,.is-lite .btn-lite-form{z-index:1;--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-300-rgb),var(--tw-ring-opacity))}.btn-toggle-fold:after{content:attr(data-expand-text)}.show-fold-items .btn-toggle-fold:after{content:attr(data-collapse-text)}.is-foldable,.is-full .full\:hidden,.is-full .lite\:visible,.is-full .panel-form-divider,.is-full .panel-form-toolbar,.is-lite .full\:visible,.is-lite .lite\:hidden,.no-foldable-items .btn-toggle-fold{display:none}.is-foldable.is-pinned,.is-full .full\:visible,.is-full .is-foldable,.is-full .lite\:hidden,.is-lite .full\:hidden,.is-lite .lite\:visible,.show-fold-items .is-foldable{display:block}.is-full .full\:w-1\/2,.is-lite .lite\:w-1\/2{width:50%}.is-full .full\:w-1\/3,.is-lite .lite\:w-1\/3{width:33.333333%}.is-full .full\:w-2\/3,.is-lite .lite\:w-2\/3{width:66.666667%}.is-full .full\:w-1\/4,.is-lite .lite\:w-1\/4{width:25%}.is-full .full\:w-2\/4,.is-lite .lite\:w-2\/4{width:50%}.is-full .full\:w-3\/4,.is-lite .lite\:w-3\/4{width:75%}.is-full .full\:w-1\/5,.is-lite .lite\:w-1\/5{width:20%}.is-full .full\:w-2\/5,.is-lite .lite\:w-2\/5{width:40%}.is-full .full\:w-3\/5,.is-lite .lite\:w-3\/5{width:60%}.is-full .full\:w-4\/5,.is-lite .lite\:w-4\/5{width:80%}.is-full .full\:w-1\/6,.is-lite .lite\:w-1\/6{width:16.666667%}.is-full .full\:w-2\/6,.is-lite .lite\:w-2\/6{width:33.333333%}.is-full .full\:w-3\/6,.is-lite .lite\:w-3\/6{width:50%}.is-full .full\:w-4\/6,.is-lite .lite\:w-4\/6{width:66.666667%}.is-full .full\:w-5\/6,.is-lite .lite\:w-5\/6{width:83.333333%}.is-full .full\:w-full,.is-lite .lite\:w-full{width:100%}.page-form{--panel-container-width:960px;--panel-container-padding:20px;--zt-panel-form-max-width:1200px;--zt-panel-form-min-width:1040px;padding-left:0;padding-right:0;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.page-form .panel-heading{border-bottom-width:1px;--tw-border-opacity:0.7;padding:0}.page-form .panel-title{font-size:1rem;line-height:1.5rem}@media (max-width:1240px){.page-form{--zt-panel-form-max-width:1100px}}@media (max-width:1110px){.page-form{--zt-panel-form-max-width:1040px}}.modal-body>.page-form{margin:-1.5rem -2rem!important;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.modal-body>.page-form>.panel-heading{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);position:sticky;top:-1.25rem;z-index:2;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding-bottom:.375rem;padding-top:.375rem}.modal-body>.panel-form .form-actions{bottom:-1.5rem!important;padding-bottom:1.5rem;padding-top:1.5rem}.dropdown.popover{display:flex;flex-direction:column;position:absolute;z-index:1200}.dropdown.show{display:flex}.dropdown-menu{display:none;--menu-min-width:7rem;--menu-max-width:20rem}.dropdown-menu.show,.menu-wrapper>.dropdown-menu,.show>.dropdown-menu{display:block}.dropdown>.dropdown-menu,.menu-wrapper .dropdown-menu{position:relative}.show>.menu-wrapper{display:flex}.dropdown>.menu-wrapper{height:100%}.dropdown>.menu-wrapper .menu-wrapper{position:absolute}.dropdown>.menu-wrapper.is-tree .menu-wrapper{position:relative}.is-contextmenu .is-nested-show>a:not(:hover){--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity))}.is-contextmenu .menu-item.is-nested>.item-inner>.nested-toggle-icon{position:absolute;right:.25rem;top:.375rem}.is-contextmenu .menu-nested .menu-item>.item-inner{padding-left:.5rem;padding-right:1.25rem}.is-contextmenu .menu-nested .menu-item>.item-inner>.item-content{overflow:hidden}.dropdown>.arrow{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.global-search-panel li:not(.order-first):hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.global-search-panel li a{width:100%}.global-search-panel li{cursor:pointer}.history-panel>.panel-heading+.panel-body{margin-top:-.75rem}.history-panel-action{position:relative}.history-panel-action:before{border-left-width:1px;bottom:-.375rem;left:.5rem;position:absolute;top:22px;--tw-border-opacity:1;border-color:rgba(var(--color-border-rgb),var(--tw-border-opacity));--tw-content:"";content:var(--tw-content)}.col-reverse>.history-panel-action:first-child:before,.col>.history-panel-action:last-child:before{display:none}.history-panel-action>.item-inner{padding-left:1.5rem}.history-panel-action>.item-inner>.avatar{left:0;position:absolute;top:.375rem}.img-cutter{background:var(--color-gray-100);border:1px solid #ddd;margin-bottom:20px;padding:10px}.img-cutter .canvas{margin:0 auto;max-width:100%;min-width:50px;position:relative}.img-cutter .cover{bottom:0;left:0;position:absolute;right:0;top:0;z-index:1}.img-cutter .controller{background:none;border:1px dashed hsla(0,0%,100%,.7);cursor:move;position:absolute;z-index:5}.img-cutter .control{background:#000;background:rgba(0,0,0,.6);border:1px solid hsla(0,0%,100%,.6);height:6px;position:absolute;width:6px}.img-cutter .control[data-type=left]{cursor:w-resize;left:-4px;margin-top:-3px;top:50%}.img-cutter .control[data-type=top]{cursor:n-resize;left:50%;margin-left:-3px;top:-4px}.img-cutter .control[data-type=right]{cursor:e-resize;margin-top:-3px;right:-4px;top:50%}.img-cutter .control[data-type=bottom]{bottom:-4px;cursor:s-resize;left:50%;margin-left:-3px}.img-cutter .control[data-type=top-left]{cursor:nw-resize;left:-4px;top:-4px}.img-cutter .control[data-type=top-right]{cursor:ne-resize;right:-4px;top:-4px}.img-cutter .control[data-type=bottom-left]{bottom:-4px;cursor:sw-resize;left:-4px}.img-cutter .control[data-type=bottom-right]{bottom:-4px;cursor:se-resize;right:-4px}.img-cutter .cliper{bottom:0;left:0;position:absolute;right:0;top:0;z-index:2}.messager-lite{border-radius:var(--radius-lg);min-width:160px;--tw-bg-opacity:1!important;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))!important;padding:1rem 1.5rem;--tw-text-opacity:1!important;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))!important;--skin-ring:transparent!important;--skin-text:inherit!important;--skin-color:var(--color-gray-500)}.messager-lite .alert-icon{color:var(--skin-color);font-size:1.5rem;line-height:2rem}.messager-lite .alert-text{font-size:.875rem;font-weight:700;line-height:1.3rem}.messager-lite .toolbar{gap:.5rem;margin-left:2rem}.messager-lite .alert-content+.alert-close{margin-left:1.5rem}.messager-lite .toolbar .btn.ghost{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.modal-reconfirm .modal-body{font-size:14px;font-weight:700;padding:24px 32px}.modal-reconfirm .modal-footer{justify-content:center;padding:24px}.picker-deselect-btn{opacity:.6;transition-duration:.15s;transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.picker-multi-selection{cursor:pointer;padding-right:.5rem;position:relative}.picker-multi-selection .picker-deselect-btn{bottom:0;left:0;position:absolute;right:0;top:0;--tw-scale-x:0;--tw-scale-y:0;background-color:rgba(var(--color-surface-rgb),.8);opacity:0}.picker-deselect-btn:hover,.picker-multi-selection .picker-deselect-btn,.picker-multi-selection:hover .picker-deselect-btn{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))}.picker-deselect-btn:hover,.picker-multi-selection:hover .picker-deselect-btn{--tw-scale-x:1;--tw-scale-y:1;opacity:1;--state-color:rgba(var(--color-surface-rgb),.9)}.pick-pri{flex-direction:row;justify-content:flex-start;position:relative}.pick-pri,.pick-pri>.text{align-items:center;display:flex}.pick-pri>.text{flex:1 1 auto;min-width:0;overflow:hidden;white-space:nowrap}.pick-pri .placeholder{--tw-text-opacity:1;color:rgba(var(--color-placeholder-rgb),var(--tw-text-opacity))}.pick-pri-list>.btn{justify-content:flex-start}.disabled.pick-pri,.disabled.pick-pri>*{filter:none}.search-form{--gutter-width:12px;padding-bottom:.5rem}.search-form-container{border-radius:var(--radius);min-height:116px;position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.is-simple .search-form-container{min-height:56px}.search-form-field td{padding:.375rem}.search-form-field td.is-andOr,.search-form-field td.is-operator{width:5rem}.search-form-field td.is-condition{width:7rem}.search-form-field td.is-operator{width:5rem}[lang=en] .search-form-field td.is-operator{width:6rem}.search-form .sidebar-right.is-collapsed{margin-right:-.375rem}.is-simple .search-form-wrapper{display:flex}.is-simple .search-form-wrapper>.row{flex:1 1 auto}.is-simple .search-form-wrapper>.toolbar{flex:none;margin-top:0}.zinbar [data-hint]{position:relative}.zinbar [data-hint]:after{background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity));border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg);bottom:100%;display:block;pointer-events:none;position:absolute;right:0;white-space:nowrap;z-index:50;--tw-bg-opacity:0.9;font-size:.75rem;line-height:1rem;padding:.125rem .25rem;--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity));opacity:0;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);--tw-content:attr(data-hint);content:var(--tw-content)}.zinbar [data-hint]:hover:after{opacity:1}.zin-perf-btn-list .text-danger{color:var(--color-danger-400)}.zinbar .state{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.zinbar .state:active{--tw-scale-x:1.05;--tw-scale-y:1.05}.zinbar .state:active,.zinbar.collapse{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))}.zinbar.collapse{--tw-translate-x:100%}.zinbar.collapse>div{pointer-events:none}.zinbar{--tw-translate-x:0px;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));transition-duration:.7s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}body[z-perf=danger]:before,body[z-perf=warning]:before{bottom:0;left:0;pointer-events:none;position:fixed;right:0;top:0;z-index:9990;--tw-border-opacity:1;border-color:rgba(var(--color-warning-600-rgb),var(--tw-border-opacity));font-size:.75rem;line-height:1rem;--tw-content:"";border-style:inset;border-width:12px 8px 8px;content:var(--tw-content)}body[z-perf=danger]:before{--tw-border-opacity:1;border-color:rgba(var(--color-danger-600-rgb),var(--tw-border-opacity))}body[z-perf=danger]:after,body[z-perf=warning]:after{font-size:.75rem;font-weight:700;left:0;line-height:1rem;line-height:1;pointer-events:none;position:fixed;right:0;text-align:center;top:0;z-index:9991;--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity));--tw-content:attr(z-perf-message);content:var(--tw-content)}.card{border-radius:var(--radius);min-height:28px;position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05;transition-duration:.15s;transition-property:box-shadow,border,color,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.card:hover{--tw-shadow:0 2px 6px -1px rgba(0,0,0,.2),0 1px 4px -2px rgba(0,0,0,.15);--tw-shadow-colored:0 2px 6px -1px var(--tw-shadow-color),0 1px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.card-content,.card-footer,.card-header,.card-heading{padding:.25rem .5rem}.card-heading{align-items:center;display:flex;gap:.5rem}.card-icon{opacity:.5}.card-header{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity))}.card-content{display:flex;flex-direction:column;gap:.25rem}.card-actions{position:absolute;right:.125rem;top:.125rem}.card-actions+.card-heading{padding-right:1.75rem}.card-subtitle{font-size:.75rem;line-height:1rem;--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.card.selected{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-400-rgb),var(--tw-ring-opacity))}.card-list{--list-gap:.625rem;--list-count-per-row:1}.card-list,.card-list-item{padding:calc(var(--list-gap)/2)}.card-list-item{width:calc(100%/var(--list-count-per-row))}.card-grid{display:flex;flex-wrap:wrap}.card-grid .card{height:100%}.contextmenu{display:none}.contextmenu.show{display:block}.contextmenu .popup .popup,.contextmenu.popup{position:absolute}.more,.more-vert{margin:.5rem;position:relative}.more,.more-vert,.more-vert:after,.more-vert:before,.more:after,.more:before{background-color:currentColor;border-radius:var(--radius-full);height:.125rem;width:.125rem}.more-vert:after,.more-vert:before,.more:after,.more:before{display:block;left:-5px;position:absolute;top:0;--tw-content:"";content:var(--tw-content)}.more-vert:after,.more:after{left:5px}.more-vert{--tw-rotate:90deg;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))}.dashboard-blocks{position:relative}.dashboard-block-cell{position:absolute;transition-duration:.15s;transition-property:left,top;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-block{border-radius:var(--radius);bottom:.5rem;left:.5rem;position:absolute;right:.5rem;top:.5rem;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-block.is-dragging{opacity:0;z-index:1600}.dashboard-block-header{align-items:center;border-top-left-radius:var(--radius);border-top-right-radius:var(--radius);display:flex;flex-direction:row;justify-content:space-between;left:0;padding:.75rem;pointer-events:none;position:absolute;right:0;top:0;z-index:10}.dashboard-block-header>*{pointer-events:auto}.dashboard-block-title{font-size:.875rem;font-weight:700;line-height:1.3rem}.dashboard-block-body{bottom:0;left:0;position:absolute;right:0;top:0}.dashboard-block-body>.panel{display:flex;flex-direction:column;height:100%;width:100%;--tw-shadow:0px 6px 16px rgba(202,215,235,.25)}.dashboard-block-body .panel-heading{cursor:move;flex:none}.dashboard-block-body .panel-body{flex:1 1 auto;overflow-y:auto}.has-more-menu>.dashboard-block-body .panel-heading{padding-right:3rem}.dashboard-block-body .panel-title{font-size:.875rem;height:2rem;line-height:1.3rem}.dashboard-block-actions .btn{opacity:.6}.dashboard-block-actions .btn:hover{opacity:1}.dashboard-drop-shadow{position:absolute;transition-duration:.15s;transition-property:left,top;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dashboard-drop-shadow:before{background-color:rgba(var(--color-inverse-rgb),.05);border-radius:var(--radius);bottom:.5rem;left:.5rem;position:absolute;right:.5rem;top:.5rem;--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-content:"";content:var(--tw-content)}:root{--scrollbar-size:10px;--scrollbar-opacity:.6;--scrollbar-bg:rgba(var(--color-inverse-rgb),.15);--scrollbar-inset:inset 0 0 0 1px rgba(var(--color-inverse-rgb),.05);--scrollbar-bar-bg:rgba(var(--color-inverse-rgb),.3);--scrollbar-hover-bg:rgba(var(--color-inverse-rgb),.4);--scrollbar-drag-bg:rgba(var(--color-inverse-rgb),.5);--scrollbar-radius:var(--radius-sm);--scrollbar-duration:.7s}.scrollbar{background:var(--scrollbar-bg);box-shadow:var(--scrollbar-shadow);opacity:var(--scrollbar-opacity);position:absolute;transition-duration:.15s;transition-duration:var(--scrollbar-duration);transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.scrollbar-hover .scrollbar{opacity:0}.scrollbar-hover:hover .scrollbar{opacity:var(--scrollbar-opacity)}.scrollbar.is-dragging,.scrollbar:hover{opacity:1!important}.scrollbar-bar{background:var(--scrollbar-bar-bg);border-radius:var(--scrollbar-radius);position:absolute}.scrollbar-bar:hover{background:var(--scrollbar-hover-bg)}.is-dragging>.scrollbar-bar{background:var(--scrollbar-drag-bg)}.is-horz>.scrollbar-bar{height:100%;z-index:20}.is-vert>.scrollbar-bar{width:100%;z-index:20}:root{--dtable-checked-bg:var(--color-warning-50);--dtable-checked-bg-hover:var(--color-warning-100);--dtable-checked-border:var(--color-border-strong)}.dtable-cell.is-checked,.dtable-striped .dtable-cell.is-odd-row.is-checked{background-color:var(--dtable-checked-bg)}.checkbox-primary.dtable-checkbox{margin-right:-.375rem}.dtable-checkbox{order:-5}.dtable-hover-col .dtable-cell.is-hover-col.is-checked,.dtable-hover-row .dtable-cell.is-hover-row.is-checked{background-color:var(--dtable-checked-bg-hover)}.dtable-cell.is-checked{--dtable-border-color:var(--dtable-checked-border)}.dtable-check-info{flex-shrink:1;margin-right:.5rem;overflow:hidden;white-space:nowrap}:root{--toggle-icon-size:calc(var(--font-size-root)*3/4 + 1px)}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-color:currentColor;border-width:1px;display:block;opacity:.5;position:relative}.toggle-icon-collapse:hover,.toggle-icon-expand:hover,.toggle-icon:hover{opacity:.9}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-radius:inherit;height:var(--toggle-icon-size);width:var(--toggle-icon-size)}.is-collapsed .toggle-icon:after,.is-collapsed .toggle-icon:before,.is-expanded .toggle-icon:before,.toggle-icon-collapse:after,.toggle-icon-collapse:before,.toggle-icon-expand:before{background-color:currentColor;content:" ";display:block;height:1px;left:2px;position:absolute;right:2px;top:calc((var(--toggle-icon-size) - 3px)/2)}.is-collapsed .toggle-icon:after,.toggle-icon-collapse:after{--tw-rotate:90deg;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))}:root{--dtable-nt-size:calc(var(--font-size-root)*3/4 + 1px)}.dtable-nested-indent{flex:none;margin-right:-.25rem;order:-100}.dtable-nested-toggle{align-items:center;border-radius:var(--radius-sm);display:flex;flex:none;height:1.25rem;justify-content:center;margin-left:-.25rem;order:-6;width:1.25rem}.dtable-nested-toggle.is-no-child{margin-right:-.25rem;opacity:0;pointer-events:none}.dtable-child-label{order:-5}.dtable-header-group{align-items:center;background:var(--dtable-header-bg);cursor:default;display:flex;justify-content:center;left:0;position:absolute;top:1px;z-index:30}.dtable-bordered .dtable-header-group{border-bottom:1px solid var(--dtable-border-color);border-left:1px solid var(--dtable-border-color)}.dtable-header-as-group{contain:style;content-visibility:visible;overflow:visible!important}.dtable-sorting{cursor:move}.dtable-cell.is-sorting-from{background-color:var(--dtable-bg);z-index:20}.dtable-cell.is-sorting-from:before{border-top-width:1px}.dtable-sorting .dtable-cell.is-sorting-after{--tw-translate-y:-100%}.dtable-sorting .dtable-cell.is-sorting-after,.dtable-sorting .dtable-cell.is-sorting-before{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))}.dtable-sorting .dtable-cell.is-sorting-before{--tw-translate-y:100%}.dtable-cell.is-sorting-before:before,.dtable-cell.is-sorting-from:before{border-bottom-width:1px;border-top-width:1px}:root{--dtable-bg:var(--color-canvas);--dtable-striped-bg:var(--color-gray-50);--dtable-hover-bg:rgba(var(--color-primary-500-rgb),.05);--dtable-header-bg:var(--color-surface);--dtable-footer-bg:var(--color-surface);--dtable-border-color:var(--color-border);--dtable-sorter-size:0.3125rem}.dtable{position:relative;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.dtable.no-animation *{transition-property:none!important}.dtable-block{overflow:hidden;position:absolute;width:100%}.dtable-header{background-color:var(--dtable-header-bg);border-top-left-radius:inherit;border-top-right-radius:inherit}.dtable-body{overflow:hidden}.dtable-cells{background-color:var(--dtable-bg);height:100%;position:absolute;z-index:0}.dtable-cells-container{height:100%;position:absolute;width:100%}.dtable-header .dtable-cells{background-color:var(--dtable-header-bg)}.dtable-cells.dtable-fixed-left,.dtable-cells.dtable-fixed-right{z-index:10}.dtable-cell{position:absolute;transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-cell:after,.dtable-cell:before{bottom:0;content:"";left:0;opacity:0;pointer-events:none;position:absolute;right:0;top:-1px;z-index:2}.dtable-cell:before{border-bottom-width:1px;border-color:var(--dtable-border-color);opacity:1}.dtable-header .dtable-cell{align-items:center;display:flex;font-weight:700}.dtable-header .dtable-cell:after,.dtable-header .dtable-cell:before{top:0}.dtable-cell-content{align-items:center;display:flex;gap:.25rem;height:100%;overflow:hidden;padding-left:.75rem;padding-right:.75rem;position:relative;white-space:nowrap;width:100%;z-index:5}.dtable-body .dtable-cell.is-last-row:before{border-bottom-width:0}.dtable-footer{align-items:center;background-color:var(--dtable-footer-bg);border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;border-color:var(--dtable-border-color);border-top-width:1px;display:flex;padding-left:.75rem;padding-right:.75rem;position:absolute;width:100%;z-index:10}.dtable-empty-tip{align-items:center;display:flex;height:100%;justify-content:center}.dtable .scrollbar{z-index:20}.dtable-header{transition-duration:1s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-scrolled-down .dtable-header{border-bottom-color:rgba(var(--color-inverse-rgb),.15);border-bottom-width:1px}.dtable-has-scroll-y:not(.dtable-scrolled-bottom) .dtable-footer{border-top-color:rgba(var(--color-inverse-rgb),.15);border-top-width:1px}.dtable-scroll-shadow{background-image:linear-gradient(90deg,rgba(0,0,0,.03),transparent),linear-gradient(90deg,rgba(0,0,0,.03),transparent 50%);opacity:0;pointer-events:none;position:absolute;top:0;transition-duration:1s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);width:8px;z-index:20}.dtable:hover .dtable-scroll-shadow{opacity:1}.dtable-scroll-shadow.is-right{margin-left:-7px;--tw-rotate:180deg;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))}.dtable-scroll-shadow.is-left,.dtable-scrolled-end .dtable-scroll-shadow.is-right{visibility:hidden}.dtable-scroll-shadow.is-right,.dtable-scrolled-right .dtable-scroll-shadow.is-left{visibility:visible}.scrollbar-hover:hover .scrollbar{opacity:0}.scrollbar-hover .dtable-body:hover~.scrollbar{opacity:var(--scrollbar-opacity)}.dtable-sort{display:inline-flex;height:calc(var(--dtable-sorter-size)*12/5);margin-left:.25rem;opacity:.8;position:relative}.dtable-cell:hover .dtable-sort{opacity:1}.dtable-sort:after,.dtable-sort:before{border-color:transparent;border-style:solid;height:0;left:0;opacity:.4;position:absolute;width:0;--tw-content:"";border-width:var(--dtable-sorter-size);content:var(--tw-content)}.dtable-sort:before{border-bottom-color:currentColor;border-top-width:0;top:0}.dtable-sort:after{border-bottom-width:0;border-top-color:currentColor;bottom:0}.dtable-sort.dtable-sort-asc:before,.dtable-sort.dtable-sort-desc:after{opacity:.9}.dtable-sort-link{align-items:center;cursor:pointer;display:inline-flex}.dtable-cells.dtable-fixed-left:before,.dtable-cells.dtable-fixed-right:before{border-color:var(--dtable-border-color);content:"";height:100%;pointer-events:none;position:absolute;width:100%;z-index:5}.dtable-cells.dtable-fixed-left:before{border-right-width:1px;right:-1px}.dtable-cells.dtable-fixed-right:before{border-left-width:1px;left:0}.dtable-bordered:before{border-color:var(--dtable-border-color);border-radius:inherit;border-width:1px;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:15}.dtable-bordered .dtable-cell:before{border-width:1px;right:-1px}.dtable-cell.has-border-left:before{border-left-width:1px}.dtable-cell.has-border-right:before{border-right-width:1px;right:-1px}.dtable-striped .dtable-cell.is-odd-row{background-color:var(--dtable-striped-bg)}.dtable-hover-cell .dtable-cell:hover:after,.dtable-hover-col .dtable-cell.is-hover-col:before,.dtable-hover-row .dtable-body .dtable-cell.is-hover-row:before{background-color:var(--dtable-hover-bg)}:root{--kanban-lane-name-width:20px;--kanban-col-width:280px;--kanban-cols-gap:8px;--kanban-lanes-gap:8px;--kanban-header-height:32px;--kanban-lane-height:auto;--kanban-lane-color:var(--color-primary-500);--kanban-link-color:var(--color-secondary-600)}.kanban{align-items:flex-start;display:flex;flex-direction:column;min-width:-moz-fit-content;min-width:fit-content;position:relative}.kanban-header{display:flex;flex-direction:row;height:var(--kanban-header-height)}.kanban.has-sub-cols .kanban-header{height:calc(var(--kanban-header-height)*2)}.kanban-header-lane-name{background-color:rgba(var(--color-fore-rgb),.05);flex:none;width:var(--kanban-lane-name-width)}.kanban-header-cols,.kanban-header-sub-cols{display:flex;flex:1 1 0%;flex-direction:row;gap:var(--kanban-cols-gap)}.kanban-header-sub-cols{border-top-width:2px;--tw-border-opacity:1;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity))}.kanban-header-col{align-items:center;border-bottom-width:1px;display:flex;flex:1 1 0%;flex-direction:column;height:100%;width:var(--kanban-col-width)}.kanban-header-col.has-subs{width:auto}.kanban-header-col-wrapper{align-items:center;display:flex;flex:1 1 0%;flex-direction:row;position:relative;width:100%;--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));padding-left:3.5rem;padding-right:3.5rem}.kanban-header-col-wrapper.is-align-left{justify-content:flex-start;padding-left:.5rem}.kanban-header-col-wrapper.is-align-left>.kanban-header-title{justify-content:flex-start}.kanban-header-title{align-items:center;display:flex;flex:1 1 auto;flex-direction:row;justify-content:center;overflow:hidden}.kanban-header-title>.as-title{color:var(--kanban-col-color);font-weight:700;overflow:hidden;white-space:nowrap}.kanban-header-col-actions{bottom:.25rem;position:absolute;right:.25rem;top:.25rem}.kanban-header-col-actions>.btn{opacity:.6}.kanban-header-col-actions>.btn:hover{opacity:1}.kanban-body{display:flex;flex-direction:column;gap:var(--kanban-lanes-gap)}.kanban-lane{display:flex;flex-direction:row;height:var(--kanban-lane-height);min-height:96px}.kanban-lane-name{align-items:center;background-color:var(--kanban-lane-color);display:flex;justify-content:center;width:var(--kanban-lane-name-width);--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity));writing-mode:vertical-rl}.kanban-lane-actions,.kanban-lane-name{flex:none;position:relative}.kanban-lane.is-first .kanban-lane-actions{margin-top:calc(4px - var(--kanban-header-height))}.has-sub-cols .kanban-lane.is-first .kanban-lane-actions{margin-top:calc(4px - var(--kanban-header-height)*2)}.kanban-lane-actions>.btn.square{width:var(--kanban-lane-name-width)}.kanban-lane-title{align-items:center;display:flex;flex:1 1 auto;justify-content:center;overflow:hidden;white-space:nowrap}.kanban-lane-cols{display:flex;flex-direction:row;gap:var(--kanban-cols-gap)}.kanban-lane-col{background-color:rgba(var(--color-inverse-rgb),.05);display:flex;flex-direction:column;height:100%;width:var(--kanban-col-width)}.kanban-header-col,.kanban-lane-col{margin-left:calc(var(--kanban-col-gap-left) - var(--kanban-cols-gap));margin-right:calc(var(--kanban-col-gap-right) - var(--kanban-cols-gap))}.kanban-col-content{position:relative;z-index:5}.kanban-lane.is-first>.kanban-lane-name:before{background-color:var(--kanban-lane-color);bottom:100%;display:block;height:var(--kanban-header-height);left:0;position:absolute;right:0;--tw-content:"";content:var(--tw-content)}.kanban.has-sub-cols .kanban-lane.is-first>.kanban-lane-name:before{height:calc(var(--kanban-header-height)*2)}.kanban-lane{margin-bottom:calc(var(--kanban-col-gap-bottom) - var(--kanban-lanes-gap));margin-top:calc(var(--kanban-col-gap-top) - var(--kanban-lanes-gap))}.kanban-items{flex:1 1 auto;overflow:auto;scrollbar-gutter:both-edges stable}.kanban-lane.is-auto-height .kanban-items{scrollbar-gutter:auto}.kanban-item{cursor:default}.kanban-item .card-actions{z-index:5}.kanban-heading{width:100%}.kanban-heading .listitem{height:2.25rem}.kanban-heading .item-title{font-weight:700}.kanban.has-dragging{cursor:move}.kanban-item.is-dragging{cursor:move;opacity:.1}.kanban-sticky .kanban-header{position:sticky;top:0;z-index:10;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.kanban-sticky .kanban-header.is-pinned{border-bottom-color:rgba(var(--color-inverse-rgb),.15);border-bottom-width:1px;z-index:20;--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.kanban-sticky .kanban-lane-name{left:0;position:sticky;z-index:21}.kanban-region{border-radius:var(--radius);min-width:-moz-fit-content;min-width:fit-content;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));padding:.75rem;--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.kanban-region>.kanban+.kanban{margin-top:.75rem}.kanban-region.has-heading{padding-top:0}.kanban-heading>.listitem{left:.75rem;position:sticky;width:calc(var(--kanban-list-width) - 24px)}.kanban-list{display:flex;flex-direction:column;gap:.75rem;height:100vh;overflow:auto;position:relative;scrollbar-gutter:stable}.kanban-list.is-moving{cursor:grabbing}.kanban-items.is-dropping{position:relative}.kanban-item.is-dropping{position:relative;transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.kanban-item.is-dropping:after,.kanban-items.is-dropping>.card-list:after{background-color:rgba(var(--color-warning-500-rgb),.5);border-radius:var(--radius-full);display:block;height:.25rem;--tw-content:"";content:var(--tw-content)}.kanban-item.is-dropping:after{left:.25rem;position:absolute;right:.25rem;top:-.25rem}.kanban-item.is-dropping[z-drop-side=before]{--tw-translate-y:0.25rem}.kanban-item.is-dropping[z-drop-side=after],.kanban-item.is-dropping[z-drop-side=before]{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))}.kanban-item.is-dropping[z-drop-side=after]{--tw-translate-y:-0.25rem}.kanban-item.is-dropping[z-drop-side=after]:after{bottom:-.25rem;top:auto}.kanban-links{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:5}.kanban-list>.kanban-links{overflow:visible;z-index:11}.kanban-link{align-items:center;color:var(--kanban-link-color);display:flex;justify-content:center;position:absolute}.kanban-link>svg{position:absolute}.kanban-link-text{border-radius:var(--radius);height:1em;line-height:1;padding-left:.125rem;padding-right:.125rem;pointer-events:auto;position:relative;text-align:center;white-space:nowrap}.kanban-link-start-point{height:.5rem;width:.5rem;--tw-translate-x:-0.25rem;--tw-translate-y:-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))}.kanban-link-start-point,.kanban-link-start-point:before{background-color:currentColor;border-radius:var(--radius-full);position:absolute}.kanban-link-start-point:before{bottom:-.25rem;display:block;left:-.25rem;opacity:.2;right:-.25rem;top:-.25rem;--tw-content:"";content:var(--tw-content)}.kanban-link path{pointer-events:auto}.kanban-link path+path{opacity:.6}.kanban-link.is-hover path+path{opacity:1;stroke-width:calc(var(--kanban-link-weight)*1.5)}.kanban-link-delete-btn{pointer-events:auto;--tw-scale-x:.75;--tw-scale-y:.75;opacity:0;transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.is-hover .kanban-link-delete-btn,.kanban-link-delete-btn{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))}.is-hover .kanban-link-delete-btn{--tw-scale-x:1;--tw-scale-y:1;opacity:1}.is-adding-link{cursor:crosshair;-webkit-user-select:none;-moz-user-select:none;user-select:none}.kanban-link-editor{bottom:0;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:20}.kanban-link-editor-from,.kanban-link-editor-to{border-radius:var(--radius);--tw-shadow-color:rgb(var(--color-primary-500-rgb));--tw-shadow:var(--tw-shadow-colored);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-500-rgb),var(--tw-ring-opacity))}.kanban-link-editor-from,.kanban-link-editor-point,.kanban-link-editor-to{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);position:absolute}.kanban-link-editor-point{height:.5rem;pointer-events:auto;width:.5rem;--tw-translate-x:-0.25rem;--tw-translate-y:-0.25rem;border-radius:var(--radius-full);cursor:move;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));--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));opacity:.5;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:rgba(var(--color-primary-500-rgb),0.1);transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.kanban-link-editor-point.is-left{left:0;top:50%}.kanban-link-editor-point.is-right{right:0;top:50%;--tw-translate-x: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))}.kanban-link-editor-point.is-top{left:50%;top:0}.kanban-link-editor-point.is-bottom{bottom:0;left:50%;--tw-translate-y: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))}.kanban-link-editor-point:hover{opacity:1;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-primary-500-rgb),0.5)}.tab-pane{display:none}.tab-pane.active{display:block}:root{--dtable-header-bg:var(--color-surface-light);--dtable-footer-bg:var(--color-surface-light);--dtable-border-color:var(--color-border-light);--dtable-checked-border:var(--color-border);--dtable-checked-bg:var(--color-primary-50);--dtable-checked-bg-hover:var(--color-primary-100);--dtable-hover-bg:rgba(var(--color-gray-500-rgb),.05);--dtable-link-color:var(--color-link);--dtable-link-hover-color:var(--color-link-hover);--dtable-link-visited-color:var(--color-link-visited)}.dtable-cell-content[style*="--color-link"]{--dtable-link-hover-color:var(--color-link);--dtable-link-visited-color:var(--color-link)}.dtable-body .dtable-cell a{--dtable-link-color:var(--color-link)}.dtable-body .dtable-cell a:not(.btn){color:var(--dtable-link-color)}.dtable-body .dtable-cell a:not(.btn):visited{color:var(--dtable-link-visited-color)}.dtable-body .dtable-cell a:not(.btn):hover{color:var(--dtable-link-hover-color)}.dtable .btn-avatar{border-radius:var(--radius-full);gap:.25rem;height:1.25rem;--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity));padding-left:0;padding-right:.5rem;--tw-ring-color:transparent}.dtable .btn-avatar:hover{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.dtable-nested-toggle{width:1rem}.dtable .toggle-icon{--toggle-icon-size:11px;border-radius:var(--radius);--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity))}.dtable .toggle-icon:after,.dtable .toggle-icon:before{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-700-rgb),var(--tw-bg-opacity))}.dtable-footer>.toolbar{margin-right:.5rem}.dtable-header .dtable-cell[data-sort]>.dtable-cell-content>a:not(:hover){color:inherit}.dtable-assign-btn{align-items:center;border-radius:var(--radius-full);display:flex;flex-direction:row;height:1.25rem;padding-left:.375rem;padding-right:.375rem;--dtable-link-color:var(--color-fore)}.dtable-assign-btn:not(:hover){color:inherit}.dtable-assign-btn.is-me{--dtable-link-color:var(--color-warning-500)!important}.dtable-assign-btn:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.dtable-assign-btn>span{border-radius:var(--radius-full);line-height:20px;padding-left:.375rem;padding-right:.375rem}.dtable-assign-btn.is-unassigned>span{--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity));--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.dtable-assign-btn.is-unassigned:hover>span{background-color:inherit;color:inherit;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.dtable-assign-btn>.icon{opacity:0;transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.dtable-assign-btn:not(.disabled):hover>.icon{opacity:1}.dtable-header .dtable-cell[data-type=assign]{padding-left:1.5rem}.dtable-cell-icon{order:-5}.dtable-custom-cell[data-type=control]{align-items:center;display:flex;justify-content:center;padding-left:.125rem;padding-right:.125rem;z-index:1}.dtable-empty-tip{min-height:212px}.dtable-cell[data-type=actions] .btn>.icon{font-size:1rem;line-height:1.5rem}.menu.menu-dtable-actions.has-icons{align-items:center;display:flex;gap:.25rem;min-width:auto;--menu-icon-opacity:1}.menu.menu-dtable-actions.has-icons>.menu-item>a{padding-left:.25rem;padding-right:.25rem}.menu.menu-dtable-actions.has-icons>.menu-item>a>.icon-EMPTY,.menu.menu-dtable-actions.has-icons>.menu-item>a>.item-content{display:none}.menu.menu-dtable-actions.has-icons>.menu-item>a>.icon-EMPTY+.item-content,.menu.menu-dtable-actions.has-icons>.menu-item>a>.item-content:first-child{display:block}:root{--state-color:rgba(0,0,0,.03);--zt-page-bg:#f8f9fa;--zt-header-height:3rem;--zt-header-bg:var(--color-canvas);--zt-header-color:var(--color-fore);--zt-sidebar-width:160px;--zt-navbar-active-color:var(--color-primary-500)}body{background-color:var(--zt-page-bg)}.page-app{overflow:overlay}.container{min-width:1200px}#header{height:var(--zt-header-height);--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);background:var(--zt-header-bg);border-bottom:1px solid var(--zt-header-border,rgba(var(--color-inverse-rgb),.05));box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:var(--zt-header-color)}#header>.container{height:100%;position:relative}#heading{align-items:center;bottom:0;display:flex;gap:.25rem;left:1rem;position:absolute;top:0}#heading>.toolbar{margin-left:-.75rem}#heading .dropmenu-btn>.text{max-width:142px}#heading>[data-zui-dropmenu]+div .dropmenu-btn>.text,#heading>[data-zui-dropmenu]+div+div .dropmenu-btn>.text{max-width:78px}#navbar{align-items:center;display:flex;font-size:.875rem;height:100%;justify-content:center;line-height:1.3rem;--state-color:rgba(var(--color-primary-500-rgb),.05);--state-active-opacity:1;--state-active-color:rgba(var(--color-primary-500-rgb),.08)}#navbar>.nav{--nav-active-color:var(--zt-navbar-active-color,var(--color-current));--nav-item-padding:12px;--nav-divider-margin:8px;margin-left:var(--nav-offset-left)}#navbar>.nav>.nav-item>a{height:3rem;padding-left:var(--nav-item-padding);padding-right:var(--nav-item-padding)}#navbar>.nav>.nav-item>a:before{--tw-scale-x:1!important;--tw-scale-y:1!important;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))!important;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)!important;--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)!important;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)!important}#navbar>.nav>.nav-item>a.active:before{background:var(--state-active-color)!important;opacity:1!important}#navbar>.nav>.divider{align-self:center;height:1.25rem;margin-left:var(--nav-divider-margin);margin-right:var(--nav-divider-margin);opacity:.2}#navbar>.nav.compact{font-size:.8125rem;line-height:1.25rem}#navbar>.nav.compact-extra{font-size:.75rem;line-height:1rem}#toolbar{align-items:center;bottom:0;display:flex;gap:.25rem;position:absolute;right:1rem;top:0}#main{min-height:100vh}#header+#main{min-height:calc(100vh - 3rem)}#main>.container{padding-left:1rem;padding-right:1rem;position:relative}@media (min-width:1400px){#header>.container,#main>.container,#mainNavbar>.container{padding-left:2.5rem;padding-right:2.5rem}#heading{left:2.5rem}#toolbar{right:2.5rem}}#mainNavbar{height:2.75rem;--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}#mainNavbar>.container{position:relative}#mainNavbar .nav{bottom:0;height:2.75rem;justify-content:center;left:0;position:absolute;right:0;top:0}#mainNavbar .nav-item{font-size:.875rem;line-height:1.3rem}#mainNavbar .nav-item>a{border-radius:var(--radius)}#mainMenu{gap:1rem;justify-content:space-between;padding-bottom:.75rem;padding-top:.75rem}#featureBar,#mainMenu{align-items:center;display:flex}#featureBar{flex:1 1 auto;gap:.5rem}#actionBar{gap:.5rem}#actionBar>.nav-divider{margin-left:-.25rem;margin-right:-.25rem}#mainContent{padding-top:.75rem}#mainMenu+#mainContent{padding-top:0}#mainContent.row{align-items:stretch;display:flex;gap:1rem;justify-content:space-between}#mainContent.row>*{flex:1 1 auto;min-width:0}#mainContainer.has-sidebar-left{--gutter-left-size:16px}#mainContainer.is-sidebar-left-collapsed{--gutter-left-size:0}.has-sidebar-animation.has-sidebar-left #mainMenu{transition-duration:.15s;transition-property:padding-left;transition-timing-function:cubic-bezier(.4,0,.2,1)}:root{--modal-radius:var(--radius);--avatar-bg:var(--color-gray-200)}.modal-alert .modal-footer{justify-content:center}.modal-dialog{--tw-bg-opacity:1}.modal-content,.modal-dialog{max-height:calc(100vh - 4rem)}.modal-body,.modal-footer,.modal-header{padding:1.5rem 2rem}.modal-header{padding-right:3rem}.modal-body+.modal-footer,.modal-header+.modal-actions+.modal-body,.modal-header+.modal-body{padding-top:1px}.modal-condensed .modal-body,.modal-condensed .modal-header{padding:1rem 1.25rem}.modal-condensed .modal-actions{top:.75rem}:root{--label-bg:var(--color-gray-100);--label-color:var(--color-gray-800)}.label[class$=-pale],.label[class*="-pale "]{--tw-bg-opacity:0.4;--tw-ring-opacity:0.4}.body-modal .panel-form,.modal-body>.panel-form{margin:0;padding:0}.modal-body>.panel-form>.panel-heading{padding-left:1px;padding-right:1px;padding-top:1px}.modal-body>.panel-form>.panel-body{padding:1px}.modal-body>.form{padding-top:1px}.modal .history,.modal .panel-body>.form{padding:0}:root{--article-p-space:0}.lane-col-join-with-above{position:relative}.lane-col-join-with-above:before{background-color:inherit;left:0;position:absolute;right:0;--tw-content:"";content:var(--tw-content);height:var(--kanban-col-gap-top,var(--kanban-lanes-gap));top:calc(0px - var(--kanban-col-gap-top, var(--kanban-lanes-gap)))}.arrow,.arrow:before{background:inherit;height:calc(var(--arrow-size)*1.4142);position:absolute;width:calc(var(--arrow-size)*1.4142)}.arrow{visibility:hidden;--arrow-size:5px}.arrow:before{border:inherit;content:"";visibility:visible;--tw-rotate:45deg;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))}.arrow-top,.arrow-up{border-bottom:inherit;border-right:inherit;border-width:1px 0 0 1px;top:calc(1px - var(--arrow-size))}.white>.arrow-top,.white>.arrow-up{top:calc(0px - var(--arrow-size))}.arrow-bottom,.arrow-down{border-left:inherit;border-top:inherit;border-width:0 1px 1px 0;bottom:calc(1px - var(--arrow-size))}.white>.arrow-bottom,.white>.arrow-down{bottom:calc(0px - var(--arrow-size))}.arrow-left{border-right:inherit;border-top:inherit;border-width:0 0 1px 1px;left:calc(1px - var(--arrow-size))}.white>.arrow-left{left:calc(0px - var(--arrow-size))}.arrow-right{border-bottom:inherit;border-left:inherit;border-width:1px 1px 0 0;right:calc(1px - var(--arrow-size))}.white>.arrow-right{right:calc(0px - var(--arrow-size))}.caret,.caret-down,.caret-left,.caret-right,.caret-up{align-items:center;display:inline-flex;height:.75rem;justify-content:center;position:relative;width:.75rem}.caret-down:before,.caret-left:before,.caret-right:before,.caret-up:before,.caret:before{display:block;height:.35rem;transform-origin:center;width:.35rem;--tw-translate-y:-1px;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.caret-left:before{--tw-translate-x:0.125rem;--tw-translate-y:0px;--tw-rotate:135deg}.caret-left:before,.caret-right:before{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))}.caret-right:before{--tw-translate-x:-0.125rem;--tw-translate-y:0px;--tw-rotate:-45deg}.caret-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}:focus>.caret,:hover>.caret{opacity:inherit}.disabled .caret,.readonly .caret{opacity:var(--caret-opacity,.5)}:root{--toggle-icon-size:calc(var(--font-size-root)*3/4 + 1px)}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-color:currentColor;border-width:1px;display:block;opacity:.5;position:relative}.toggle-icon-collapse:hover,.toggle-icon-expand:hover,.toggle-icon:hover{opacity:.9}.toggle-icon,.toggle-icon-collapse,.toggle-icon-expand{border-radius:inherit;height:var(--toggle-icon-size);width:var(--toggle-icon-size)}.is-collapsed .toggle-icon:after,.is-collapsed .toggle-icon:before,.is-expanded .toggle-icon:before,.toggle-icon-collapse:after,.toggle-icon-collapse:before,.toggle-icon-expand:before{background-color:currentColor;content:" ";display:block;height:1px;left:2px;position:absolute;right:2px;top:calc((var(--toggle-icon-size) - 3px)/2)}.is-collapsed .toggle-icon:after,.toggle-icon-collapse:after{--tw-rotate:90deg;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))}.close{aspect-ratio:1/1;display:block;position:relative;width:1em}.close:after,.close:before{content:"";display:block;height:1px;left:0;position:absolute;width:100%;--tw-rotate:45deg;background-color:currentColor;border-radius:var(--radius-full);top:calc(50% - .5px)}.close:after,.close:before{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))}.close:after{--tw-rotate:-45deg}.spinner,.spinner:after,.spinner:before{display:block;height:1em;position:relative;width:1em}.spinner:after,.spinner:before{border-radius:var(--radius-full);content:" ";position:absolute}.spinner:before{animation:-spin 1s cubic-bezier(.6,0,.4,1) infinite;border:.16667em solid transparent;border-top-color:currentcolor}.spinner:after{border:.16667em solid;opacity:.2}.magnifier{display:inline-block;height:1em;position:relative;width:1em}.magnifier:after,.magnifier:before{border-radius:var(--radius-full);content:"";display:block;position:absolute}.magnifier:before{border:1px solid;height:.75em;width:.75em}.magnifier:after{height:1px;left:.55em;top:.7em;width:.5em;--tw-rotate:45deg;background-color:currentColor;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))}.more,.more-vert{margin:.5rem;position:relative}.more,.more-vert,.more-vert:after,.more-vert:before,.more:after,.more:before{background-color:currentColor;border-radius:var(--radius-full);height:.125rem;width:.125rem}.more-vert:after,.more-vert:before,.more:after,.more:before{display:block;left:-5px;position:absolute;top:0;--tw-content:"";content:var(--tw-content)}.more-vert:after,.more:after{left:5px}.more-vert{--tw-rotate:90deg;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))}.chevron-down,.chevron-left,.chevron-right,.chevron-up{align-items:center;display:inline-flex;height:1em;justify-content:center;position:relative;width:1em}.chevron-down:before,.chevron-left:before,.chevron-right:before,.chevron-up:before{display:block;height:.6923077em;transform-origin:center;width:.6923077em;--tw-translate-y:-0.125rem;--tw-rotate:45deg;border-color:currentColor;border-width:0 1px 1px 0;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));--tw-content:"";content:var(--tw-content)}.chevron-left:before{--tw-translate-y:0px;--tw-translate-x:0.125rem;--tw-rotate:135deg}.chevron-left:before,.chevron-right:before{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))}.chevron-right:before{--tw-translate-y:0px;--tw-translate-x:-0.125rem;--tw-rotate:-45deg}.chevron-up:before{--tw-translate-y:0.125rem;--tw-rotate:225deg;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))}.i-time{border-color:currentColor;border-radius:50%;border-width:.08em;display:inline-block;height:1em;position:relative;width:1em}.i-time:after{border-color:currentColor;border-width:0 0 .08em .08em;height:.385em;left:.3846em;position:absolute;top:.08em;width:.385em;--tw-content:"";content:var(--tw-content)}.i-calendar{border-color:currentColor;border-radius:2px;border-width:.08em;display:inline-block;height:.79em;margin-top:.16em;position:relative;width:1em}.i-calendar:after,.i-calendar:before{border-color:currentColor;border-width:.08em;position:absolute;--tw-content:"";content:var(--tw-content)}.i-calendar:before{border-bottom-width:0;left:.15em;top:.307em;width:.6em}.i-calendar:after{border-bottom-width:0;border-top-width:0;height:.3077em;left:.17em;top:-.154em;width:.538em}:root{--state-color:rgba(0,0,0,.05);--state-scale:.9;--opacity-disabled:.7;--opacity-muted:.5;--load-indicator-duration:300ms;--load-indicator-delay:.5s}.btn,.item>a,.state{cursor:pointer;position:relative}.btn:before,.item>a:before,.state:before{background:var(--state-color);border-radius:inherit;bottom:0;content:" ";display:block;left:0;opacity:0;pointer-events:none;position:absolute;right:0;top:0;transform:scale(var(--state-scale));transition-duration:.2s;transition-property:opacity,transform,box-shadow,background-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn.active:not(:hover,:focus-visible,:focus-within,:active):before,.item>a.active:not(:hover,:focus-visible,:focus-within,:active):before,.state.active:not(:hover,:focus-visible,:focus-within,:active):before{background:var(--state-active-color,var(--state-color));opacity:var(--state-active-opacity,0);--tw-scale-x:1;--tw-scale-y:1;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));--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);transition-duration:.7s}.btn.selected:not(:hover,:focus-visible,:focus-within,:active):before,.item>a.selected:not(:hover,:focus-visible,:focus-within,:active):before,.state.selected:not(:hover,:focus-visible,:focus-within,:active):before{background:var(--state-selected-color,var(--state-scale));opacity:var(--state-selected-opacity,.1);--tw-scale-x:1;--tw-scale-y:1;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))}.btn:focus-visible,.item>a:focus-visible,.state:focus-visible{outline:2px solid transparent;outline-offset:2px}.btn:active:before,.btn:focus-visible:before,.btn:hover:before,.item>a:active:before,.item>a:focus-visible:before,.item>a:hover:before,.state:active:before,.state:focus-visible:before,.state:hover:before{--tw-scale-x:1;--tw-scale-y:1;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))}.btn:hover:before,.item>a:hover:before,.state:hover:before{opacity:var(--state-hover-opacity,1)}.btn:active:before,.item>a:active:before,.state:active:before{background:var(--state-pressed-color,var(--state-color));opacity:var(--state-pressed-opacity,.6);--tw-ring-color:var(--state-ring-color,currentColor);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);transition-duration:.7s}.btn:focus-visible:before,.item>a:focus-visible:before,.state:focus-visible:before{background:var(--state-focus-color,var(--state-color));opacity:var(--state-focus-opacity,1);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-focus-rgb),var(--tw-ring-opacity))}.btn.disabled:before,.btn[disabled]:before,.item>a.disabled:before,.item>a[disabled]:before,.state.disabled:before,.state[disabled]:before{display:none}.btn{--state-active-opacity:1}@-moz-document url-prefix(){.btn,:root{--state-active-opacity:0;--state-focus-opacity:0}}.disabled,.disabled *,[disabled],[disabled] *{cursor:not-allowed!important;opacity:var(--opacity-disabled);--tw-grayscale:grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.load-indicator:after,.load-indicator:before{display:block;opacity:0;position:absolute;transition-delay:0s;transition-duration:.15s;transition-duration:var(--load-indicator-duration);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden}.load-indicator:before{align-items:center;background-color:rgba(var(--color-canvas-rgb),.7);border-radius:inherit;bottom:0;display:flex;font-size:.75rem;justify-content:center;left:0;line-height:1rem;right:0;top:0;z-index:99;--tw-content:attr(data-loading);content:var(--tw-content)}.load-indicator[data-loading]:before{padding-top:3.5rem}.load-indicator:after{border-color:transparent currentcolor currentcolor;border-radius:var(--radius-full);border-style:dotted;border-width:4px 3px 4px 4px;height:2rem;left:50%;margin-left:-1rem;margin-top:-1rem;top:50%;width:2rem;z-index:100;--tw-content:"";content:var(--tw-content)}.load-indicator.loading:after,.load-indicator.loading:before{opacity:1;transition-delay:var(--load-indicator-delay);visibility:visible}@keyframes -spin{to{transform:rotate(1turn)}}.load-indicator.loading:after{animation:-spin 1s linear infinite;opacity:.5}.events-none,.pointer-events-none{pointer-events:none}.events-auto,.pointer-events-auto{pointer-events:auto}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.select-text{-webkit-user-select:text;-moz-user-select:text;user-select:text}.select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;-moz-user-select:auto;user-select:auto}.cursor-auto{cursor:auto}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-wait{cursor:wait}.cursor-text{cursor:text}.cursor-move{cursor:move}.cursor-help{cursor:help}.cursor-not-allowed{cursor:not-allowed}.active\:opacity-100.active,.active\:opacity-100:active,.group.active .group-active\:opacity-100,.group:active .group-active\:opacity-100,.group:hover .group-hover\:opacity-100,.hover\:opacity-100:hover{opacity:1}.active\:text-primary.active,.active\:text-primary:active,.group.active .group-active\:text-primary,.group:active .group-active\:text-primary,.group:hover .group-hover\:text-primary,.hover\:text-primary:hover{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.active\:border-primary.active,.active\:border-primary:active,.group.active .group-active\:border-primary,.group:active .group-active\:border-primary,.group:hover .group-hover\:border-primary,.hover\:border-primary:hover{--tw-border-opacity:1;border-color:rgba(var(--color-primary-500-rgb),var(--tw-border-opacity))}.active\:primary-pale.active,.active\:primary-pale:active,.group.active .group-active\:primary-pale,.group:active .group-active\:primary-pale,.group:hover .group-hover\:primary-pale,.hover\:primary-pale:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.active\:primary.active,.active\:primary:active,.group.active .group-active\:primary,.group:active .group-active\:primary,.group:hover .group-hover\:primary,.hover\:primary:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.active\:canvas.active,.active\:canvas:active,.group.active .group-active\:canvas,.group:active .group-active\:canvas,.group:hover .group-hover\:canvas,.hover\:canvas:hover{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.active\:font-bold.active,.active\:font-bold:active,.group.active .group-active\:font-bold,.group:active .group-active\:font-bold,.group:hover .group-hover\:font-bold,.hover\:font-bold:hover{font-weight:700}.active\:shadow.active,.active\:shadow:active,.group.active .group-active\:shadow,.group:active .group-active\:shadow,.group:hover .group-hover\:shadow,.hover\:shadow:hover{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.container{width:100%}@media (min-width:1720px){.container{max-width:1720px}}.container{margin-left:auto;margin-right:auto}.popup{background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity));border-radius:var(--radius);--tw-bg-opacity:0.85;--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05;--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.hidden{display:none!important}.pull-right{float:right}.pull-left{float:left}.clearfix:after{clear:both;content:"";display:block}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.object-fill{-o-object-fit:fill;object-fit:fill}.object-none{-o-object-fit:none;object-fit:none}.object-scale-down{-o-object-fit:scale-down;object-fit:scale-down}.object-bottom{-o-object-position:bottom;object-position:bottom}.object-center{-o-object-position:center;object-position:center}.object-left{-o-object-position:left;object-position:left}.object-left-bottom{-o-object-position:left bottom;object-position:left bottom}.object-left-top{-o-object-position:left top;object-position:left top}.object-right{-o-object-position:right;object-position:right}.object-right-bottom{-o-object-position:right bottom;object-position:right bottom}.object-right-top{-o-object-position:right top;object-position:right top}.object-top{-o-object-position:top;object-position:top}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-clip{overflow:clip}.overflow-visible{overflow:visible}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-x-clip{overflow-x:clip}.overflow-y-clip{overflow-y:clip}.overflow-x-visible{overflow-x:visible}.overflow-y-visible{overflow-y:visible}.overflow-x-scroll{overflow-x:scroll}.overflow-y-scroll{overflow-y:scroll}.overflow-overlay{overflow:overlay}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{bottom:0;left:0;right:0;top:0}.inset-auto{bottom:auto;left:auto;right:auto;top:auto}.inset-x-0{left:0;right:0}.inset-y-0{bottom:0;top:0}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.top-full{top:100%}.right-full{right:100%}.bottom-full{bottom:100%}.left-full{left:100%}.top-auto{top:auto}.right-auto{right:auto}.bottom-auto{bottom:auto}.left-auto{left:auto}.top-px{top:1px}.right-px{right:1px}.bottom-px{bottom:1px}.left-px{left:1px}.top-0\.5{top:.125rem}.right-0\.5{right:.125rem}.bottom-0\.5{bottom:.125rem}.left-0\.5{left:.125rem}.top-1{top:.25rem}.right-1{right:.25rem}.bottom-1{bottom:.25rem}.left-1{left:.25rem}.top-1\.5{top:.375rem}.right-1\.5{right:.375rem}.bottom-1\.5{bottom:.375rem}.left-1\.5{left:.375rem}.top-2{top:.5rem}.right-2{right:.5rem}.bottom-2{bottom:.5rem}.left-2{left:.5rem}.top-2\.5{top:.625rem}.right-2\.5{right:.625rem}.bottom-2\.5{bottom:.625rem}.left-2\.5{left:.625rem}.top-3{top:.75rem}.right-3{right:.75rem}.bottom-3{bottom:.75rem}.left-3{left:.75rem}.top-3\.5{top:.875rem}.right-3\.5{right:.875rem}.bottom-3\.5{bottom:.875rem}.left-3\.5{left:.875rem}.top-4{top:1rem}.right-4{right:1rem}.bottom-4{bottom:1rem}.left-4{left:1rem}.top-5{top:1.25rem}.right-5{right:1.25rem}.bottom-5{bottom:1.25rem}.left-5{left:1.25rem}.top-6{top:1.5rem}.right-6{right:1.5rem}.bottom-6{bottom:1.5rem}.left-6{left:1.5rem}.top-7{top:1.75rem}.right-7{right:1.75rem}.bottom-7{bottom:1.75rem}.left-7{left:1.75rem}.top-8{top:2rem}.right-8{right:2rem}.bottom-8{bottom:2rem}.left-8{left:2rem}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-0{z-index:0}.z-1{z-index:1}.z-5{z-index:5}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-auto{z-index:auto}.z-base{z-index:1100}.z-above{z-index:50}.z-below{z-index:-50}.z-dropdown{z-index:1200}.z-sticky{z-index:1300}.z-fixed{z-index:1400}.z-modal{z-index:1500}.z-overlay{z-index:1600}.z-popover{z-index:1700}.z-tooltip{z-index:1800}.z-messager{z-index:1900}.m-0{margin:0}.m-auto{margin:auto}.m-px{margin:1px}.m-0\.5{margin:.125rem}.m-1{margin:.25rem}.m-1\.5{margin:.375rem}.m-2{margin:.5rem}.m-2\.5{margin:.625rem}.m-3{margin:.75rem}.m-3\.5{margin:.875rem}.m-4{margin:1rem}.m-5{margin:1.25rem}.m-6{margin:1.5rem}.m-7{margin:1.75rem}.m-8{margin:2rem}.m-9{margin:2.25rem}.m-10{margin:2.5rem}.mx-0{margin-left:0;margin-right:0}.my-0{margin-bottom:0}.mt-0,.my-0{margin-top:0}.mr-0{margin-right:0}.mb-0{margin-bottom:0}.ml-0{margin-left:0}.mx-auto{margin-left:auto;margin-right:auto}.my-auto{margin-bottom:auto}.mt-auto,.my-auto{margin-top:auto}.mr-auto{margin-right:auto}.mb-auto{margin-bottom:auto}.ml-auto{margin-left:auto}.mx-px{margin-left:1px;margin-right:1px}.my-px{margin-bottom:1px}.mt-px,.my-px{margin-top:1px}.mr-px{margin-right:1px}.mb-px{margin-bottom:1px}.ml-px{margin-left:1px}.mx-0\.5{margin-left:.125rem;margin-right:.125rem}.my-0\.5{margin-bottom:.125rem}.mt-0\.5,.my-0\.5{margin-top:.125rem}.mr-0\.5{margin-right:.125rem}.mb-0\.5{margin-bottom:.125rem}.ml-0\.5{margin-left:.125rem}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-1{margin-bottom:.25rem}.mt-1,.my-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.mx-1\.5{margin-left:.375rem;margin-right:.375rem}.my-1\.5{margin-bottom:.375rem}.mt-1\.5,.my-1\.5{margin-top:.375rem}.mr-1\.5{margin-right:.375rem}.mb-1\.5{margin-bottom:.375rem}.ml-1\.5{margin-left:.375rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.my-2{margin-bottom:.5rem}.mt-2,.my-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mx-2\.5{margin-left:.625rem;margin-right:.625rem}.my-2\.5{margin-bottom:.625rem}.mt-2\.5,.my-2\.5{margin-top:.625rem}.mr-2\.5{margin-right:.625rem}.mb-2\.5{margin-bottom:.625rem}.ml-2\.5{margin-left:.625rem}.mx-3{margin-left:.75rem;margin-right:.75rem}.my-3{margin-bottom:.75rem}.mt-3,.my-3{margin-top:.75rem}.mr-3{margin-right:.75rem}.mb-3{margin-bottom:.75rem}.ml-3{margin-left:.75rem}.mx-3\.5{margin-left:.875rem;margin-right:.875rem}.my-3\.5{margin-bottom:.875rem}.mt-3\.5,.my-3\.5{margin-top:.875rem}.mr-3\.5{margin-right:.875rem}.mb-3\.5{margin-bottom:.875rem}.ml-3\.5{margin-left:.875rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-4{margin-bottom:1rem}.mt-4,.my-4{margin-top:1rem}.mr-4{margin-right:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.my-5{margin-bottom:1.25rem}.mt-5,.my-5{margin-top:1.25rem}.mr-5{margin-right:1.25rem}.mb-5{margin-bottom:1.25rem}.ml-5{margin-left:1.25rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.my-6{margin-bottom:1.5rem}.mt-6,.my-6{margin-top:1.5rem}.mr-6{margin-right:1.5rem}.mb-6{margin-bottom:1.5rem}.ml-6{margin-left:1.5rem}.mx-7{margin-left:1.75rem;margin-right:1.75rem}.my-7{margin-bottom:1.75rem}.mt-7,.my-7{margin-top:1.75rem}.mr-7{margin-right:1.75rem}.mb-7{margin-bottom:1.75rem}.ml-7{margin-left:1.75rem}.mx-8{margin-left:2rem;margin-right:2rem}.my-8{margin-bottom:2rem}.mt-8,.my-8{margin-top:2rem}.mr-8{margin-right:2rem}.mb-8{margin-bottom:2rem}.ml-8{margin-left:2rem}.mx-9{margin-left:2.25rem;margin-right:2.25rem}.my-9{margin-bottom:2.25rem}.mt-9,.my-9{margin-top:2.25rem}.mr-9{margin-right:2.25rem}.mb-9{margin-bottom:2.25rem}.ml-9{margin-left:2.25rem}.mx-10{margin-left:2.5rem;margin-right:2.5rem}.my-10{margin-bottom:2.5rem}.mt-10,.my-10{margin-top:2.5rem}.mr-10{margin-right:2.5rem}.mb-10{margin-bottom:2.5rem}.ml-10{margin-left:2.5rem}.p-0{padding:0}.p-px{padding:1px}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-3\.5{padding:.875rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.p-8{padding:2rem}.p-9{padding:2.25rem}.p-10{padding:2.5rem}.px-0{padding-left:0;padding-right:0}.py-0{padding-bottom:0}.pt-0,.py-0{padding-top:0}.pr-0{padding-right:0}.pb-0{padding-bottom:0}.pl-0{padding-left:0}.px-px{padding-left:1px;padding-right:1px}.py-px{padding-bottom:1px}.pt-px,.py-px{padding-top:1px}.pr-px{padding-right:1px}.pb-px{padding-bottom:1px}.pl-px{padding-left:1px}.px-0\.5{padding-left:.125rem;padding-right:.125rem}.py-0\.5{padding-bottom:.125rem}.pt-0\.5,.py-0\.5{padding-top:.125rem}.pr-0\.5{padding-right:.125rem}.pb-0\.5{padding-bottom:.125rem}.pl-0\.5{padding-left:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-1{padding-bottom:.25rem}.pt-1,.py-1{padding-top:.25rem}.pr-1{padding-right:.25rem}.pb-1{padding-bottom:.25rem}.pl-1{padding-left:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-1\.5{padding-bottom:.375rem}.pt-1\.5,.py-1\.5{padding-top:.375rem}.pr-1\.5{padding-right:.375rem}.pb-1\.5{padding-bottom:.375rem}.pl-1\.5{padding-left:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-2{padding-bottom:.5rem}.pt-2,.py-2{padding-top:.5rem}.pr-2{padding-right:.5rem}.pb-2{padding-bottom:.5rem}.pl-2{padding-left:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.py-2\.5{padding-bottom:.625rem}.pt-2\.5,.py-2\.5{padding-top:.625rem}.pr-2\.5{padding-right:.625rem}.pb-2\.5{padding-bottom:.625rem}.pl-2\.5{padding-left:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-3{padding-bottom:.75rem}.pt-3,.py-3{padding-top:.75rem}.pr-3{padding-right:.75rem}.pb-3{padding-bottom:.75rem}.pl-3{padding-left:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.py-3\.5{padding-bottom:.875rem}.pt-3\.5,.py-3\.5{padding-top:.875rem}.pr-3\.5{padding-right:.875rem}.pb-3\.5{padding-bottom:.875rem}.pl-3\.5{padding-left:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.py-4{padding-bottom:1rem}.pt-4,.py-4{padding-top:1rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pl-4{padding-left:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-5{padding-bottom:1.25rem}.pt-5,.py-5{padding-top:1.25rem}.pr-5{padding-right:1.25rem}.pb-5{padding-bottom:1.25rem}.pl-5{padding-left:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-6{padding-bottom:1.5rem}.pt-6,.py-6{padding-top:1.5rem}.pr-6{padding-right:1.5rem}.pb-6{padding-bottom:1.5rem}.pl-6{padding-left:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.py-7{padding-bottom:1.75rem}.pt-7,.py-7{padding-top:1.75rem}.pr-7{padding-right:1.75rem}.pb-7{padding-bottom:1.75rem}.pl-7{padding-left:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.py-8{padding-bottom:2rem}.pt-8,.py-8{padding-top:2rem}.pr-8{padding-right:2rem}.pb-8{padding-bottom:2rem}.pl-8{padding-left:2rem}.px-9{padding-left:2.25rem;padding-right:2.25rem}.py-9{padding-bottom:2.25rem}.pt-9,.py-9{padding-top:2.25rem}.pr-9{padding-right:2.25rem}.pb-9{padding-bottom:2.25rem}.pl-9{padding-left:2.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-10{padding-bottom:2.5rem}.pt-10,.py-10{padding-top:2.5rem}.pr-10{padding-right:2.5rem}.pb-10{padding-bottom:2.5rem}.pl-10{padding-left:2.5rem}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(0px*(1 - var(--tw-space-x-reverse)));margin-right:calc(0px*var(--tw-space-x-reverse))}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(0px*var(--tw-space-y-reverse));margin-top:calc(0px*(1 - var(--tw-space-y-reverse)))}.space-x-0\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.125rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.125rem*var(--tw-space-x-reverse))}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.375rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.375rem*var(--tw-space-x-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-2\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.625rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.625rem*var(--tw-space-x-reverse))}.space-y-2\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.625rem*var(--tw-space-y-reverse));margin-top:calc(.625rem*(1 - var(--tw-space-y-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-x-3\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.875rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.875rem*var(--tw-space-x-reverse))}.space-y-3\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.875rem*var(--tw-space-y-reverse));margin-top:calc(.875rem*(1 - var(--tw-space-y-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-x-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.25rem*var(--tw-space-x-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-7>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.75rem*var(--tw-space-x-reverse))}.space-y-7>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.75rem*var(--tw-space-y-reverse));margin-top:calc(1.75rem*(1 - var(--tw-space-y-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2rem*var(--tw-space-x-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.space-x-9>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2.25rem*var(--tw-space-x-reverse))}.space-y-9>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.25rem*var(--tw-space-y-reverse));margin-top:calc(2.25rem*(1 - var(--tw-space-y-reverse)))}.space-x-10>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2.5rem*var(--tw-space-x-reverse))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2.5rem*var(--tw-space-y-reverse));margin-top:calc(2.5rem*(1 - var(--tw-space-y-reverse)))}.space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(1px*var(--tw-space-x-reverse))}.space-y-px>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1px*var(--tw-space-y-reverse));margin-top:calc(1px*(1 - var(--tw-space-y-reverse)))}.w-0{width:0}.w-px{width:1px}.w-0\.5{width:.125rem}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-9{width:2.25rem}.w-10{width:2.5rem}.w-11{width:2.75rem}.w-12{width:3rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-32{width:8rem}.w-36{width:9rem}.w-40{width:10rem}.w-44{width:11rem}.w-48{width:12rem}.w-52{width:13rem}.w-56{width:14rem}.w-60{width:15rem}.w-64{width:16rem}.w-72{width:18rem}.w-80{width:20rem}.w-96{width:24rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-1\/4{width:25%}.w-2\/4{width:50%}.w-3\/4{width:75%}.w-1\/5{width:20%}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-4\/5{width:80%}.w-1\/6{width:16.666667%}.w-2\/6{width:33.333333%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-5\/6{width:83.333333%}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.w-min{width:-moz-min-content;width:min-content}.w-max{width:-moz-max-content;width:max-content}.w-fit{width:-moz-fit-content;width:fit-content}.min-w-0{min-width:0}.min-w-full{min-width:100%}.min-w-min{min-width:-moz-min-content;min-width:min-content}.min-w-max{min-width:-moz-max-content;min-width:max-content}.min-w-fit{min-width:-moz-fit-content;min-width:fit-content}.max-w-0{max-width:0}.max-w-none{max-width:none}.max-w-xs{max-width:20rem}.max-w-sm{max-width:24rem}.max-w-md{max-width:28rem}.max-w-lg{max-width:32rem}.max-w-xl{max-width:36rem}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-full{max-width:100%}.max-w-min{max-width:-moz-min-content;max-width:min-content}.max-w-max{max-width:-moz-max-content;max-width:max-content}.max-w-fit{max-width:-moz-fit-content;max-width:fit-content}.h-0{height:0}.h-px{height:1px}.h-0\.5{height:.125rem}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-28{height:7rem}.h-32{height:8rem}.h-36{height:9rem}.h-40{height:10rem}.h-44{height:11rem}.h-48{height:12rem}.h-52{height:13rem}.h-56{height:14rem}.h-60{height:15rem}.h-64{height:16rem}.h-72{height:18rem}.h-80{height:20rem}.h-96{height:24rem}.h-1\/2{height:50%}.h-1\/3{height:33.333333%}.h-2\/3{height:66.666667%}.h-1\/4{height:25%}.h-2\/4{height:50%}.h-3\/4{height:75%}.h-1\/5{height:20%}.h-2\/5{height:40%}.h-3\/5{height:60%}.h-4\/5{height:80%}.h-1\/6{height:16.666667%}.h-2\/6{height:33.333333%}.h-3\/6{height:50%}.h-4\/6{height:66.666667%}.h-5\/6{height:83.333333%}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.h-min{height:-moz-min-content;height:min-content}.h-max{height:-moz-max-content;height:max-content}.h-fit{height:-moz-fit-content;height:fit-content}.min-h-0{min-height:0}.min-h-full{min-height:100%}.min-h-min{min-height:-moz-min-content;min-height:min-content}.min-h-max{min-height:-moz-max-content;min-height:max-content}.min-h-fit{min-height:-moz-fit-content;min-height:fit-content}.max-h-0{max-height:0}.max-h-px{max-height:1px}.max-h-none{max-height:none}.max-h-full{max-height:100%}.max-h-min{max-height:-moz-min-content;max-height:min-content}.max-h-screen{max-height:100vh}.max-h-max{max-height:-moz-max-content;max-height:max-content}.max-h-fit{max-height:-moz-fit-content;max-height:fit-content}.canvas,.surface,.surface-light,.surface-strong{--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))}.canvas{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.surface-light{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-light-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-light-rgb),var(--tw-ring-opacity))}.surface{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-rgb),var(--tw-ring-opacity))}.surface-strong{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-strong-rgb),var(--tw-bg-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-strong-rgb),var(--tw-ring-opacity))}.inverse{background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity));color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.inverse,.white{--tw-bg-opacity:1;--tw-text-opacity:1}.white{background-color:rgba(var(--color-white-rgb),var(--tw-bg-opacity));color:rgba(var(--color-fore-in-light-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-white-rgb),var(--tw-ring-opacity))}.black{--tw-bg-opacity:1;background-color:rgba(var(--color-black-rgb),var(--tw-bg-opacity));--tw-text-opacity:1;color:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity));--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-black-rgb),var(--tw-ring-opacity))}.transparent{background-color:transparent;--tw-ring-color:transparent}.skin{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.skin:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger,.darken,.darker,.darkest,.gray,.gray-100,.gray-200,.gray-300,.gray-400,.gray-50,.gray-500,.gray-600,.gray-700,.gray-800,.gray-900,.gray-950,.important,.lighten,.lighter,.lightest,.primary,.secondary,.special,.success,.warning{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger:hover,.darken:hover,.darker:hover,.darkest:hover,.gray-100:hover,.gray-200:hover,.gray-300:hover,.gray-400:hover,.gray-500:hover,.gray-50:hover,.gray-600:hover,.gray-700:hover,.gray-800:hover,.gray-900:hover,.gray-950:hover,.gray:hover,.important:hover,.lighten:hover,.lighter:hover,.lightest:hover,.primary:hover,.secondary:hover,.special:hover,.success:hover,.warning:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger,.darken,.darker,.darkest,.gray,.gray-100,.gray-200,.gray-300,.gray-400,.gray-50,.gray-500,.gray-600,.gray-700,.gray-800,.gray-900,.gray-950,.important,.lighten,.lighter,.lightest,.primary,.secondary,.special,.success,.warning{--skin-text:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity));--skin-color:rgba(var(--skin-solid-rgb),1);--state-ring-color:var(--skin-color);--state-active-color:rgba(0,0,0,.1);--state-pressed-color:rgba(0,0,0,.1);--state-color:hsla(0,0%,100%,.1);--state-focus-color:rgba(0,0,0,.1)}.primary{--skin-solid-rgb:var(--color-primary-500-rgb)}.secondary{--skin-solid-rgb:var(--color-secondary-500-rgb)}.success{--skin-solid-rgb:var(--color-success-500-rgb)}.warning{--skin-solid-rgb:var(--color-warning-500-rgb)}.danger{--skin-solid-rgb:var(--color-danger-500-rgb)}.important{--skin-solid-rgb:var(--color-important-500-rgb)}.special{--skin-solid-rgb:var(--color-special-500-rgb)}.gray-50{--skin-bg:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity));--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-100{--skin-solid-rgb:var(--color-gray-100-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-200,.lightest{--skin-solid-rgb:var(--color-gray-200-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-300,.lighter{--skin-solid-rgb:var(--color-gray-300-rgb);--skin-text:rgba(var(--color-fore),var(--tw-text-opacity))}.gray-400,.lighten{--skin-solid-rgb:var(--color-gray-400-rgb)}.gray,.gray-500{--skin-solid-rgb:var(--color-gray-500-rgb)}.darken,.gray-600{--skin-solid-rgb:var(--color-gray-600-rgb)}.darker,.gray-700{--skin-solid-rgb:var(--color-gray-700-rgb)}.darkest,.gray-800{--skin-solid-rgb:var(--color-gray-800-rgb)}.gray-900{--skin-solid-rgb:var(--color-gray-900-rgb)}.gray-950{--skin-solid-rgb:var(--color-gray-950-rgb)}.danger-pale,.gray-100-pale,.gray-200-pale,.gray-300-pale,.gray-400-pale,.gray-50-pale,.gray-500-pale,.gray-600-pale,.gray-700-pale,.gray-800-pale,.gray-900-pale,.gray-950-pale,.gray-pale,.important-pale,.pale,.primary-pale,.secondary-pale,.special-pale,.success-pale,.warning-pale{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-pale:hover,.gray-100-pale:hover,.gray-200-pale:hover,.gray-300-pale:hover,.gray-400-pale:hover,.gray-50-pale:hover,.gray-500-pale:hover,.gray-600-pale:hover,.gray-700-pale:hover,.gray-800-pale:hover,.gray-900-pale:hover,.gray-950-pale:hover,.gray-pale:hover,.important-pale:hover,.pale:hover,.primary-pale:hover,.secondary-pale:hover,.special-pale:hover,.success-pale:hover,.warning-pale:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-pale,.gray-100-pale,.gray-200-pale,.gray-300-pale,.gray-400-pale,.gray-50-pale,.gray-500-pale,.gray-600-pale,.gray-700-pale,.gray-800-pale,.gray-900-pale,.gray-950-pale,.gray-pale,.important-pale,.pale,.primary-pale,.secondary-pale,.special-pale,.success-pale,.warning-pale{--skin-color:rgba(var(--skin-pale-rgb),.1);--skin-text:rgba(var(--skin-pale-rgb),1);--state-color:rgba(var(--skin-pale-rgb),.1)}.pale{--skin-pale-rgb:var(--color-gray-500-rgb)}.primary-pale{--skin-pale-rgb:var(--color-primary-500-rgb)}.secondary-pale{--skin-pale-rgb:var(--color-secondary-500-rgb)}.success-pale{--skin-pale-rgb:var(--color-success-500-rgb)}.warning-pale{--skin-pale-rgb:var(--color-warning-500-rgb)}.danger-pale{--skin-pale-rgb:var(--color-danger-500-rgb)}.important-pale{--skin-pale-rgb:var(--color-important-500-rgb)}.special-pale{--skin-pale-rgb:var(--color-special-500-rgb)}.gray-50-pale{--skin-pale-rgb:var(--color-gray-50-rgb);--skin-text:var(--color-gray-400)}.gray-100-pale{--skin-pale-rgb:var(--color-gray-100-rgb);--skin-text:var(--color-gray-400)}.gray-200-pale{--skin-pale-rgb:var(--color-gray-200-rgb);--skin-text:var(--color-gray-400)}.gray-300-pale{--skin-pale-rgb:var(--color-gray-300-rgb);--skin-text:var(--color-gray-500)}.gray-400-pale{--skin-pale-rgb:var(--color-gray-400-rgb);--skin-text:var(--color-gray-500)}.gray-500-pale,.gray-pale{--skin-pale-rgb:var(--color-gray-500-rgb)}.gray-600-pale{--skin-pale-rgb:var(--color-gray-600-rgb)}.gray-700-pale{--skin-pale-rgb:var(--color-gray-700-rgb)}.gray-800-pale{--skin-pale-rgb:var(--color-gray-800-rgb)}.gray-900-pale{--skin-pale-rgb:var(--color-gray-900-rgb)}.gray-950-pale{--skin-pale-rgb:var(--color-gray-950-rgb)}.danger-outline,.dark-outline,.gray-300-outline,.gray-400-outline,.gray-600-outline,.gray-outline,.important-outline,.light-outline,.outline,.primary-outline,.secondary-outline,.special-outline,.success-outline,.warning-outline{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-outline:hover,.dark-outline:hover,.gray-300-outline:hover,.gray-400-outline:hover,.gray-600-outline:hover,.gray-outline:hover,.important-outline:hover,.light-outline:hover,.outline:hover,.primary-outline:hover,.secondary-outline:hover,.special-outline:hover,.success-outline:hover,.warning-outline:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-outline,.dark-outline,.gray-300-outline,.gray-400-outline,.gray-600-outline,.gray-outline,.important-outline,.light-outline,.outline,.primary-outline,.secondary-outline,.special-outline,.success-outline,.warning-outline{--skin-text:var(--skin-color,inherit);--skin-bg:transparent;--skin-color:rgba(var(--skin-outline-rgb),1);--state-color:rgba(var(--skin-outline-rgb),.1)}.outline{--skin-outline-rgb:var(--color-fore-rgb)}.primary-outline{--skin-outline-rgb:var(--color-primary-500-rgb)}.secondary-outline{--skin-outline-rgb:var(--color-secondary-500-rgb)}.success-outline{--skin-outline-rgb:var(--color-success-500-rgb)}.warning-outline{--skin-outline-rgb:var(--color-warning-500-rgb)}.danger-outline{--skin-outline-rgb:var(--color-danger-500-rgb)}.important-outline{--skin-outline-rgb:var(--color-important-500-rgb)}.special-outline{--skin-outline-rgb:var(--color-special-500-rgb)}.gray-300-outline{--skin-text:var(--color-gray-500);--skin-outline-rgb:var(--color-gray-300-rgb)}.gray-400-outline{--skin-text:var(--color-gray-500);--skin-outline-rgb:var(--color-gray-400-rgb)}.gray-outline{--skin-outline-rgb:var(--color-gray-500-rgb)}.gray-600-outline{--skin-outline-rgb:var(--color-gray-600-rgb)}.dark-outline{--skin-outline-rgb:var(--color-inverse-rgb)}.light-outline{--skin-outline-rgb:var(--color-canvas-rgb)}.danger-ghost,.ghost,.important-ghost,.primary-ghost,.secondary-ghost,.special-ghost,.success-ghost,.warning-ghost{--skin-bg:var(--skin-color);--skin-ring:var(--skin-color);--skin-border:var(--skin-color);--skin-hover-text:var(--skin-text);--skin-hover-bg:var(--skin-bg);--tw-ring-color:var(--skin-ring);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);--color-link-hover:var(--skin-text);background-color:var(--skin-bg);border-color:var(--skin-border);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);color:var(--skin-text)}.danger-ghost:hover,.ghost:hover,.important-ghost:hover,.primary-ghost:hover,.secondary-ghost:hover,.special-ghost:hover,.success-ghost:hover,.warning-ghost:hover{background-color:var(--skin-hover-bg);color:var(--skin-hover-text)}.danger-ghost,.ghost,.important-ghost,.primary-ghost,.secondary-ghost,.special-ghost,.success-ghost,.warning-ghost{--tw-ring-opacity:0;--tw-bg-opacity:0;--skin-text:var(--skin-color,inherit);--skin-bg:transparent;--skin-ring:transparent;--skin-color:rgba(var(--skin-ghost-rgb),1);--state-color:rgba(var(--skin-ghost-rgb),.1)}.ghost{--skin-ghost-rgb:var(--color-fore-rgb);--skin-text:inherit}.primary-ghost{--skin-ghost-rgb:var(--color-primary-500-rgb)}.secondary-ghost{--skin-ghost-rgb:var(--color-secondary-500-rgb)}.success-ghost{--skin-ghost-rgb:var(--color-success-500-rgb)}.warning-ghost{--skin-ghost-rgb:var(--color-warning-500-rgb)}.danger-ghost{--skin-ghost-rgb:var(--color-danger-500-rgb)}.important-ghost{--skin-ghost-rgb:var(--color-important-500-rgb)}.special-ghost{--skin-ghost-rgb:var(--color-special-500-rgb)}.font-sans{font-family:-apple-system,Noto Sans,Helvetica Neue,Helvetica,Nimbus Sans L,Arial,Liberation Sans,PingFang SC,Hiragino Sans GB,Noto Sans CJK SC,Source Han Sans SC,Source Han Sans CN,Microsoft YaHei,Wenquanyi Micro Hei,WenQuanYi Zen Hei,ST Heiti,SimHei,WenQuanYi Zen Hei Sharp,sans-serif}.font-serif{font-family:Nimbus Roman No9 L,Songti SC,Noto Serif CJK SC,Source Han Serif SC,Source Han Serif CN,STSong,AR PL New Sung,AR PL SungtiL GB,NSimSun,SimSun,TW-Sung,WenQuanYi Bitmap Song,AR PL UMing CN,AR PL UMing HK,AR PL UMing TW,AR PL UMing TW MBE,PMingLiU,MingLiU,serif}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-root{font-size:16px}.text-sm{font-size:.75rem;line-height:1rem}.text-base{font-size:.8125rem;line-height:1.25rem}.text-md{font-size:.875rem;line-height:1.3rem}.text-lg{font-size:1rem;line-height:1.5rem}.text-xl{font-size:1.125rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.font-thin{font-weight:100}.font-light{font-weight:300}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-black{font-weight:900}.leading-3{line-height:.75rem}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.leading-6{line-height:1.5rem}.leading-7{line-height:1.75rem}.leading-8{line-height:2rem}.leading-9{line-height:2.25rem}.leading-10{line-height:2.5rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.leading-snug{line-height:1.375}.leading-normal{line-height:1.5}.leading-relaxed{line-height:1.625}.leading-loose{line-height:2}.line-clamp-1{-webkit-line-clamp:1}.line-clamp-1,.line-clamp-2{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-2{-webkit-line-clamp:2}.line-clamp-3{-webkit-line-clamp:3}.line-clamp-3,.line-clamp-4{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-4{-webkit-line-clamp:4}.line-clamp-5{-webkit-line-clamp:5}.line-clamp-5,.line-clamp-6{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical}.line-clamp-6{-webkit-line-clamp:6}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.text-justify{text-align:justify}.align-middle{vertical-align:middle}.align-top{vertical-align:top}.align-bottom{vertical-align:bottom}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.text-primary{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.text-secondary{--tw-text-opacity:1;color:rgba(var(--color-secondary-500-rgb),var(--tw-text-opacity))}.text-success{--tw-text-opacity:1;color:rgba(var(--color-success-500-rgb),var(--tw-text-opacity))}.text-warning{--tw-text-opacity:1;color:rgba(var(--color-warning-500-rgb),var(--tw-text-opacity))}.text-danger{--tw-text-opacity:1;color:rgba(var(--color-danger-500-rgb),var(--tw-text-opacity))}.text-important{--tw-text-opacity:1;color:rgba(var(--color-important-500-rgb),var(--tw-text-opacity))}.text-special{--tw-text-opacity:1;color:rgba(var(--color-special-500-rgb),var(--tw-text-opacity))}.text-primary-50{--tw-text-opacity:1;color:rgba(var(--color-primary-50-rgb),var(--tw-text-opacity))}.text-primary-100{--tw-text-opacity:1;color:rgba(var(--color-primary-100-rgb),var(--tw-text-opacity))}.text-primary-200{--tw-text-opacity:1;color:rgba(var(--color-primary-200-rgb),var(--tw-text-opacity))}.text-primary-300{--tw-text-opacity:1;color:rgba(var(--color-primary-300-rgb),var(--tw-text-opacity))}.text-primary-400{--tw-text-opacity:1;color:rgba(var(--color-primary-400-rgb),var(--tw-text-opacity))}.text-primary-500{--tw-text-opacity:1;color:rgba(var(--color-primary-500-rgb),var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgba(var(--color-primary-600-rgb),var(--tw-text-opacity))}.text-primary-700{--tw-text-opacity:1;color:rgba(var(--color-primary-700-rgb),var(--tw-text-opacity))}.text-primary-800{--tw-text-opacity:1;color:rgba(var(--color-primary-800-rgb),var(--tw-text-opacity))}.text-primary-900{--tw-text-opacity:1;color:rgba(var(--color-primary-900-rgb),var(--tw-text-opacity))}.text-primary-950{--tw-text-opacity:1;color:rgba(var(--color-primary-950-rgb),var(--tw-text-opacity))}.text-secondary-50{--tw-text-opacity:1;color:rgba(var(--color-secondary-50-rgb),var(--tw-text-opacity))}.text-secondary-100{--tw-text-opacity:1;color:rgba(var(--color-secondary-100-rgb),var(--tw-text-opacity))}.text-secondary-200{--tw-text-opacity:1;color:rgba(var(--color-secondary-200-rgb),var(--tw-text-opacity))}.text-secondary-300{--tw-text-opacity:1;color:rgba(var(--color-secondary-300-rgb),var(--tw-text-opacity))}.text-secondary-400{--tw-text-opacity:1;color:rgba(var(--color-secondary-400-rgb),var(--tw-text-opacity))}.text-secondary-500{--tw-text-opacity:1;color:rgba(var(--color-secondary-500-rgb),var(--tw-text-opacity))}.text-secondary-600{--tw-text-opacity:1;color:rgba(var(--color-secondary-600-rgb),var(--tw-text-opacity))}.text-secondary-700{--tw-text-opacity:1;color:rgba(var(--color-secondary-700-rgb),var(--tw-text-opacity))}.text-secondary-800{--tw-text-opacity:1;color:rgba(var(--color-secondary-800-rgb),var(--tw-text-opacity))}.text-secondary-900{--tw-text-opacity:1;color:rgba(var(--color-secondary-900-rgb),var(--tw-text-opacity))}.text-secondary-950{--tw-text-opacity:1;color:rgba(var(--color-secondary-950-rgb),var(--tw-text-opacity))}.text-gray-50{--tw-text-opacity:1;color:rgba(var(--color-gray-50-rgb),var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity:1;color:rgba(var(--color-gray-100-rgb),var(--tw-text-opacity))}.text-gray-200,.text-lightest{--tw-text-opacity:1;color:rgba(var(--color-gray-200-rgb),var(--tw-text-opacity))}.text-gray-300,.text-lighter{--tw-text-opacity:1;color:rgba(var(--color-gray-300-rgb),var(--tw-text-opacity))}.text-gray-400,.text-lighten{--tw-text-opacity:1;color:rgba(var(--color-gray-400-rgb),var(--tw-text-opacity))}.text-gray,.text-gray-500{--tw-text-opacity:1;color:rgba(var(--color-gray-500-rgb),var(--tw-text-opacity))}.text-darken,.text-gray-600{--tw-text-opacity:1;color:rgba(var(--color-gray-600-rgb),var(--tw-text-opacity))}.text-darker,.text-gray-700{--tw-text-opacity:1;color:rgba(var(--color-gray-700-rgb),var(--tw-text-opacity))}.text-darkest,.text-gray-800{--tw-text-opacity:1;color:rgba(var(--color-gray-800-rgb),var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(var(--color-gray-900-rgb),var(--tw-text-opacity))}.text-gray-950{--tw-text-opacity:1;color:rgba(var(--color-gray-950-rgb),var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(var(--color-white-rgb),var(--tw-text-opacity))}.text-black{--tw-text-opacity:1;color:rgba(var(--color-black-rgb),var(--tw-text-opacity))}.text-transparent{color:transparent}.text-canvas{--tw-text-opacity:1;color:rgba(var(--color-canvas-rgb),var(--tw-text-opacity))}.text-surface-light{--tw-text-opacity:1;color:rgba(var(--color-surface-light-rgb),var(--tw-text-opacity))}.text-surface{--tw-text-opacity:1;color:rgba(var(--color-surface-rgb),var(--tw-text-opacity))}.text-surface-strong{--tw-text-opacity:1;color:rgba(var(--color-surface-strong-rgb),var(--tw-text-opacity))}.text-inverse{--tw-text-opacity:1;color:rgba(var(--color-inverse-rgb),var(--tw-text-opacity))}.text-fore-in-light{--tw-text-opacity:1;color:rgba(var(--color-fore-in-light-rgb),var(--tw-text-opacity))}.text-fore-in-dark{--tw-text-opacity:1;color:rgba(var(--color-fore-in-dark-rgb),var(--tw-text-opacity))}.text-fore{--tw-text-opacity:1;color:rgba(var(--color-fore-rgb),var(--tw-text-opacity))}.text-focus{--tw-text-opacity:1;color:rgba(var(--color-focus-rgb),var(--tw-text-opacity))}.text-link{--tw-text-opacity:1;color:rgba(var(--color-link-rgb),var(--tw-text-opacity))}.text-link-hover{--tw-text-opacity:1;color:rgba(var(--color-link-hover-rgb),var(--tw-text-opacity))}.text-current{color:currentColor}.text-inherit{color:inherit}.clip,.ellipsis,.text-clip,.text-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.clip,.text-clip{text-overflow:clip}.whitespace-normal{white-space:normal}.nowrap,.whitespace-nowrap{white-space:nowrap}.pre,.whitespace-pre{white-space:pre}.pre-line,.whitespace-pre-line{white-space:pre-line}.pre-wrap,.whitespace-pre-wrap{white-space:pre-wrap}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.text-opacity-0{--tw-text-opacity:0}.text-opacity-5{--tw-text-opacity:.05}.text-opacity-10{--tw-text-opacity:.1}.text-opacity-20{--tw-text-opacity:.2}.text-opacity-25{--tw-text-opacity:.25}.text-opacity-30{--tw-text-opacity:.3}.text-opacity-40{--tw-text-opacity:.4}.text-opacity-50{--tw-text-opacity:.5}.text-opacity-60{--tw-text-opacity:.6}.text-opacity-70{--tw-text-opacity:.7}.text-opacity-80{--tw-text-opacity:.8}.text-opacity-90{--tw-text-opacity:.9}.text-opacity-95{--tw-text-opacity:.95}.text-opacity-100{--tw-text-opacity:1}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.normal-case{text-transform:none}.list-none{list-style-type:none}.list-disc{list-style-type:disc}.list-decimal{list-style-type:decimal}.list-unstyled{list-style-type:none;padding-left:0}.underline{text-decoration-line:underline}.overline{text-decoration-line:overline}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.bg-primary{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity))}.bg-secondary{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-500-rgb),var(--tw-bg-opacity))}.bg-success{--tw-bg-opacity:1;background-color:rgba(var(--color-success-500-rgb),var(--tw-bg-opacity))}.bg-warning{--tw-bg-opacity:1;background-color:rgba(var(--color-warning-500-rgb),var(--tw-bg-opacity))}.bg-danger{--tw-bg-opacity:1;background-color:rgba(var(--color-danger-500-rgb),var(--tw-bg-opacity))}.bg-important{--tw-bg-opacity:1;background-color:rgba(var(--color-important-500-rgb),var(--tw-bg-opacity))}.bg-special{--tw-bg-opacity:1;background-color:rgba(var(--color-special-500-rgb),var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-50-rgb),var(--tw-bg-opacity))}.bg-primary-100{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-100-rgb),var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-200-rgb),var(--tw-bg-opacity))}.bg-primary-300{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-300-rgb),var(--tw-bg-opacity))}.bg-primary-400{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-400-rgb),var(--tw-bg-opacity))}.bg-primary-500{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-500-rgb),var(--tw-bg-opacity))}.bg-primary-600{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-600-rgb),var(--tw-bg-opacity))}.bg-primary-700{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-700-rgb),var(--tw-bg-opacity))}.bg-primary-800{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-800-rgb),var(--tw-bg-opacity))}.bg-primary-900{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-900-rgb),var(--tw-bg-opacity))}.bg-primary-950{--tw-bg-opacity:1;background-color:rgba(var(--color-primary-950-rgb),var(--tw-bg-opacity))}.bg-secondary-50{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-50-rgb),var(--tw-bg-opacity))}.bg-secondary-100{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-100-rgb),var(--tw-bg-opacity))}.bg-secondary-200{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-200-rgb),var(--tw-bg-opacity))}.bg-secondary-300{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-300-rgb),var(--tw-bg-opacity))}.bg-secondary-400{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-400-rgb),var(--tw-bg-opacity))}.bg-secondary-500{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-500-rgb),var(--tw-bg-opacity))}.bg-secondary-600{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-600-rgb),var(--tw-bg-opacity))}.bg-secondary-700{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-700-rgb),var(--tw-bg-opacity))}.bg-secondary-800{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-800-rgb),var(--tw-bg-opacity))}.bg-secondary-900{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-900-rgb),var(--tw-bg-opacity))}.bg-secondary-950{--tw-bg-opacity:1;background-color:rgba(var(--color-secondary-950-rgb),var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-50-rgb),var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-100-rgb),var(--tw-bg-opacity))}.bg-gray-200,.bg-lightest{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-200-rgb),var(--tw-bg-opacity))}.bg-gray-300,.bg-lighter{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-300-rgb),var(--tw-bg-opacity))}.bg-gray-400,.bg-light{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-400-rgb),var(--tw-bg-opacity))}.bg-gray,.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-500-rgb),var(--tw-bg-opacity))}.bg-darken,.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-600-rgb),var(--tw-bg-opacity))}.bg-darker,.bg-gray-700{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-700-rgb),var(--tw-bg-opacity))}.bg-darkest,.bg-gray-800{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-800-rgb),var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-900-rgb),var(--tw-bg-opacity))}.bg-gray-950{--tw-bg-opacity:1;background-color:rgba(var(--color-gray-950-rgb),var(--tw-bg-opacity))}.bg-canvas{--tw-bg-opacity:1;background-color:rgba(var(--color-canvas-rgb),var(--tw-bg-opacity))}.bg-surface-light{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-light-rgb),var(--tw-bg-opacity))}.bg-surface{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-rgb),var(--tw-bg-opacity))}.bg-surface-strong{--tw-bg-opacity:1;background-color:rgba(var(--color-surface-strong-rgb),var(--tw-bg-opacity))}.bg-inverse{--tw-bg-opacity:1;background-color:rgba(var(--color-inverse-rgb),var(--tw-bg-opacity))}.bg-fore-in-light{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-in-light-rgb),var(--tw-bg-opacity))}.bg-fore-in-dark{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-in-dark-rgb),var(--tw-bg-opacity))}.bg-fore{--tw-bg-opacity:1;background-color:rgba(var(--color-fore-rgb),var(--tw-bg-opacity))}.bg-focus{--tw-bg-opacity:1;background-color:rgba(var(--color-focus-rgb),var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(var(--color-white-rgb),var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgba(var(--color-black-rgb),var(--tw-bg-opacity))}.bg-transparent{background-color:transparent}.bg-current{background-color:currentColor}.bg-inherit{background-color:inherit}.bg-none{background-image:none}.bg-opacity-0{--tw-bg-opacity:0}.bg-opacity-5{--tw-bg-opacity:.05}.bg-opacity-10{--tw-bg-opacity:.1}.bg-opacity-20{--tw-bg-opacity:.2}.bg-opacity-30{--tw-bg-opacity:.3}.bg-opacity-40{--tw-bg-opacity:.4}.bg-opacity-50{--tw-bg-opacity:.5}.bg-opacity-60{--tw-bg-opacity:.6}.bg-opacity-70{--tw-bg-opacity:.7}.bg-opacity-80{--tw-bg-opacity:.8}.bg-opacity-90{--tw-bg-opacity:.9}.bg-opacity-100{--tw-bg-opacity:1}.bg-auto{background-size:auto}.bg-cover{background-size:cover}.bg-contain{background-size:contain}.bg-repeat{background-repeat:repeat}.bg-no-repeat{background-repeat:no-repeat}.bg-repeat-x{background-repeat:repeat-x}.bg-repeat-y{background-repeat:repeat-y}.bg-repeat-round{background-repeat:round}.bg-repeat-space{background-repeat:space}.bg-bottom{background-position:bottom}.bg-center{background-position:50%}.bg-left{background-position:0}.bg-left-bottom{background-position:0 100%}.bg-left-top{background-position:0 0}.bg-right{background-position:100%}.bg-right-bottom{background-position:100% 100%}.bg-right-top{background-position:100% 0}.bg-top{background-position:top}.bg-fixed{background-attachment:fixed}.bg-local{background-attachment:local}.bg-scroll{background-attachment:scroll}.divider{align-self:stretch;border-color:currentColor;border-left-width:1px;border-top-width:1px;opacity:.1}.border-0{border-width:0}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-t{border-top-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.border-t-0{border-top-width:0}.border-r-0{border-right-width:0}.border-b-0{border-bottom-width:0}.border-l-0{border-left-width:0}.border-t-2{border-top-width:2px}.border-l-2{border-left-width:2px}.border-r-2{border-right-width:2px}.border-b-2{border-bottom-width:2px}.border-t-4{border-top-width:4px}.border-l-4{border-left-width:4px}.border-r-4{border-right-width:4px}.border-b-4{border-bottom-width:4px}.border-light{--tw-border-opacity:1;border-color:rgba(var(--color-border-light-rgb),var(--tw-border-opacity))}.border-strong{--tw-border-opacity:1;border-color:rgba(var(--color-border-strong-rgb),var(--tw-border-opacity))}.border-primary{--tw-border-opacity:1;border-color:rgba(var(--color-primary-500-rgb),var(--tw-border-opacity))}.border-secondary{--tw-border-opacity:1;border-color:rgba(var(--color-secondary-500-rgb),var(--tw-border-opacity))}.border-success{--tw-border-opacity:1;border-color:rgba(var(--color-success-500-rgb),var(--tw-border-opacity))}.border-warning{--tw-border-opacity:1;border-color:rgba(var(--color-warning-500-rgb),var(--tw-border-opacity))}.border-danger{--tw-border-opacity:1;border-color:rgba(var(--color-danger-500-rgb),var(--tw-border-opacity))}.border-important{--tw-border-opacity:1;border-color:rgba(var(--color-important-500-rgb),var(--tw-border-opacity))}.border-special{--tw-border-opacity:1;border-color:rgba(var(--color-special-500-rgb),var(--tw-border-opacity))}.border-gray-50{--tw-border-opacity:1;border-color:rgba(var(--color-gray-50-rgb),var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgba(var(--color-gray-100-rgb),var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(var(--color-gray-200-rgb),var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(var(--color-gray-300-rgb),var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(var(--color-gray-400-rgb),var(--tw-border-opacity))}.border-gray,.border-gray-500{--tw-border-opacity:1;border-color:rgba(var(--color-gray-500-rgb),var(--tw-border-opacity))}.border-gray-600{--tw-border-opacity:1;border-color:rgba(var(--color-gray-600-rgb),var(--tw-border-opacity))}.border-gray-700{--tw-border-opacity:1;border-color:rgba(var(--color-gray-700-rgb),var(--tw-border-opacity))}.border-gray-800{--tw-border-opacity:1;border-color:rgba(var(--color-gray-800-rgb),var(--tw-border-opacity))}.border-gray-900{--tw-border-opacity:1;border-color:rgba(var(--color-gray-900-rgb),var(--tw-border-opacity))}.border-gray-950{--tw-border-opacity:1;border-color:rgba(var(--color-gray-950-rgb),var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgba(var(--color-white-rgb),var(--tw-border-opacity))}.border-black{--tw-border-opacity:1;border-color:rgba(var(--color-black-rgb),var(--tw-border-opacity))}.border-canvas{--tw-border-opacity:1;border-color:rgba(var(--color-canvas-rgb),var(--tw-border-opacity))}.border-surface-light{--tw-border-opacity:1;border-color:rgba(var(--color-surface-light-rgb),var(--tw-border-opacity))}.border-surface{--tw-border-opacity:1;border-color:rgba(var(--color-surface-rgb),var(--tw-border-opacity))}.border-surface-strong{--tw-border-opacity:1;border-color:rgba(var(--color-surface-strong-rgb),var(--tw-border-opacity))}.border-inverse{--tw-border-opacity:1;border-color:rgba(var(--color-inverse-rgb),var(--tw-border-opacity))}.border-fore-in-light{--tw-border-opacity:1;border-color:rgba(var(--color-fore-in-light-rgb),var(--tw-border-opacity))}.border-fore-in-dark{--tw-border-opacity:1;border-color:rgba(var(--color-fore-in-dark-rgb),var(--tw-border-opacity))}.border-fore{--tw-border-opacity:1;border-color:rgba(var(--color-fore-rgb),var(--tw-border-opacity))}.border-focus{--tw-border-opacity:1;border-color:rgba(var(--color-focus-rgb),var(--tw-border-opacity))}.border-link{--tw-border-opacity:1;border-color:rgba(var(--color-link-rgb),var(--tw-border-opacity))}.border-link-hover{--tw-border-opacity:1;border-color:rgba(var(--color-link-hover-rgb),var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-current{border-color:currentColor}.border-inherit{border-color:inherit}.border-solid{border-style:solid}.border-dashed{border-style:dashed}.border-dotted{border-style:dotted}.border-double{border-style:double}.border-hidden{border-style:hidden}.border-none{border-style:none}.border-opacity-0{--tw-border-opacity:0}.border-opacity-5{--tw-border-opacity:.05}.border-opacity-10{--tw-border-opacity:.1}.border-opacity-20{--tw-border-opacity:.2}.border-opacity-25{--tw-border-opacity:.25}.border-opacity-30{--tw-border-opacity:.3}.border-opacity-40{--tw-border-opacity:.4}.border-opacity-50{--tw-border-opacity:.5}.border-opacity-60{--tw-border-opacity:.6}.border-opacity-70{--tw-border-opacity:.7}.border-opacity-80{--tw-border-opacity:.8}.border-opacity-90{--tw-border-opacity:.9}.border-opacity-95{--tw-border-opacity:.95}.border-opacity-100{--tw-border-opacity:1}.rounded-none{border-radius:var(--radius-none)}.rounded-sm{border-radius:var(--radius-sm)}.rounded{border-radius:var(--radius)}.rounded-md{border-radius:var(--radius-md)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.circle,.rounded-full{border-radius:var(--radius-full)}.rounded-l-none{border-bottom-left-radius:var(--radius-none);border-top-left-radius:var(--radius-none)}.rounded-t-none{border-top-left-radius:var(--radius-none)}.rounded-r-none,.rounded-t-none{border-top-right-radius:var(--radius-none)}.rounded-b-none,.rounded-r-none{border-bottom-right-radius:var(--radius-none)}.rounded-b-none{border-bottom-left-radius:var(--radius-none)}.rounded-br-none{border-bottom-right-radius:var(--radius-none)}.rounded-bl-none{border-bottom-left-radius:var(--radius-none)}.rounded-tr-none{border-top-right-radius:var(--radius-none)}.rounded-tl-none{border-top-left-radius:var(--radius-none)}.ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.ring-border-light{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-light-rgb),var(--tw-ring-opacity))}.ring-border{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-rgb),var(--tw-ring-opacity))}.ring-border-strong{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-border-strong-rgb),var(--tw-ring-opacity))}.ring-light{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.05}.ring-gray{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.1}.ring-dark{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.2}.ring-darker{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.3}.ring-darkest{--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity));--tw-ring-opacity:0.5}.ring-primary{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-primary-500-rgb),var(--tw-ring-opacity))}.ring-secondary{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-secondary-500-rgb),var(--tw-ring-opacity))}.ring-warning{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-warning-500-rgb),var(--tw-ring-opacity))}.ring-success{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-success-500-rgb),var(--tw-ring-opacity))}.ring-danger{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-danger-500-rgb),var(--tw-ring-opacity))}.ring-important{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-important-500-rgb),var(--tw-ring-opacity))}.ring-special{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-special-500-rgb),var(--tw-ring-opacity))}.ring-gray-50{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-50-rgb),var(--tw-ring-opacity))}.ring-gray-100{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-100-rgb),var(--tw-ring-opacity))}.ring-gray-200{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-200-rgb),var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-300-rgb),var(--tw-ring-opacity))}.ring-gray-400{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-400-rgb),var(--tw-ring-opacity))}.ring-gray-500{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-500-rgb),var(--tw-ring-opacity))}.ring-gray-600{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-600-rgb),var(--tw-ring-opacity))}.ring-gray-700{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-700-rgb),var(--tw-ring-opacity))}.ring-gray-800{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-800-rgb),var(--tw-ring-opacity))}.ring-gray-900{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-900-rgb),var(--tw-ring-opacity))}.ring-gray-950{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-gray-950-rgb),var(--tw-ring-opacity))}.ring-inverse{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-inverse-rgb),var(--tw-ring-opacity))}.ring-canvas{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-canvas-rgb),var(--tw-ring-opacity))}.ring-surface{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-surface-rgb),var(--tw-ring-opacity))}.ring-fore{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-fore-rgb),var(--tw-ring-opacity))}.ring-transparent{--tw-ring-color:transparent}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-black-rgb),var(--tw-ring-opacity))}.ring-white{--tw-ring-opacity:1;--tw-ring-color:rgba(var(--color-white-rgb),var(--tw-ring-opacity))}.ring-inherit{--tw-ring-color:inherit}.ring-current{--tw-ring-color:currentColor}.ring-opacity-0{--tw-ring-opacity:0}.ring-opacity-5{--tw-ring-opacity:.05}.ring-opacity-10{--tw-ring-opacity:.1}.ring-opacity-20{--tw-ring-opacity:.2}.ring-opacity-30{--tw-ring-opacity:.3}.ring-opacity-40{--tw-ring-opacity:.4}.ring-opacity-50{--tw-ring-opacity:.5}.ring-opacity-60{--tw-ring-opacity:.6}.ring-opacity-70{--tw-ring-opacity:.7}.ring-opacity-80{--tw-ring-opacity:.8}.ring-opacity-90{--tw-ring-opacity:.9}.ring-opacity-100{--tw-ring-opacity:1}.ring-0{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-0,.ring-2{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-3{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-3,.ring-4{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-4{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-8{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-inset{--tw-ring-inset:inset}.ring-offset-0{--tw-ring-offset-width:0px}.ring-offset-1{--tw-ring-offset-width:1px}.ring-offset-2{--tw-ring-offset-width:2px}.ring-offset-4{--tw-ring-offset-width:4px}.ring-offset-8{--tw-ring-offset-width:8px}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.04),0 0 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 0 2px -1px var(--tw-shadow-color)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow:0 3px 8px 0 rgba(0,0,0,.04),0 1px 2px -1px rgba(0,0,0,.01);--tw-shadow-colored:0 3px 8px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 2px 6px -1px rgba(0,0,0,.2),0 1px 4px -2px rgba(0,0,0,.15);--tw-shadow-colored:0 2px 6px -1px var(--tw-shadow-color),0 1px 4px -2px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 5px 15px -3px rgba(0,0,0,.2),0 2px 6px -4px rgba(0,0,0,.15);--tw-shadow-colored:0 5px 15px -3px var(--tw-shadow-color),0 2px 6px -4px var(--tw-shadow-color)}.shadow-xl{--tw-shadow:0 10px 25px -5px rgba(0,0,0,.2),0 4px 10px -6px rgba(0,0,0,.15);--tw-shadow-colored:0 10px 25px -5px var(--tw-shadow-color),0 4px 10px -6px var(--tw-shadow-color)}.shadow-2xl,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 10px 50px -10px rgba(0,0,0,.4),0 8px 20px -10px rgba(0,0,0,.3);--tw-shadow-colored:0 10px 50px -10px var(--tw-shadow-color),0 8px 20px -10px var(--tw-shadow-color)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 rgba(0,0,0,.05);--tw-shadow-colored:inset 0 2px 4px 0 var(--tw-shadow-color)}.shadow-inner,.shadow-none{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000}.muted{opacity:var(--opacity-muted)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.blur-none{--tw-blur:blur(0)}.blur-none,.blur-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-sm{--tw-blur:blur(4px)}.blur{--tw-blur:blur(8px)}.blur,.blur-md{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-md{--tw-blur:blur(12px)}.blur-lg{--tw-blur:blur(16px)}.blur-lg,.blur-xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.blur-xl{--tw-blur:blur(24px)}.grayscale{--tw-grayscale:grayscale(100%)}.grayscale,.invert{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert:invert(100%)}.filter-none{filter:none!important}.drop-shadow-none{--tw-drop-shadow:drop-shadow(0 0 #0000)}.drop-shadow-none,.drop-shadow-sm{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-sm{--tw-drop-shadow:drop-shadow(0 1px 1px rgba(0,0,0,.05))}.drop-shadow{--tw-drop-shadow:drop-shadow(0 1px 2px rgba(0,0,0,.1)) drop-shadow(0 1px 1px rgba(0,0,0,.06))}.drop-shadow,.drop-shadow-md{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-md{--tw-drop-shadow:drop-shadow(0 4px 3px rgba(0,0,0,.07)) drop-shadow(0 2px 2px rgba(0,0,0,.06))}.drop-shadow-lg{--tw-drop-shadow:drop-shadow(0 10px 8px rgba(0,0,0,.04)) drop-shadow(0 4px 3px rgba(0,0,0,.1))}.drop-shadow-lg,.drop-shadow-xl{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow-xl{--tw-drop-shadow:drop-shadow(0 20px 13px rgba(0,0,0,.03)) drop-shadow(0 8px 5px rgba(0,0,0,.08))}.drop-shadow-2xl{--tw-drop-shadow:drop-shadow(0 25px 25px rgba(0,0,0,.15));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-none{--tw-backdrop-blur:blur(0)}.backdrop-blur-none,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.backdrop-blur{--tw-backdrop-blur:blur(8px)}.backdrop-blur,.backdrop-blur-md{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px)}.backdrop-blur-lg,.backdrop-blur-xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-xl{--tw-backdrop-blur:blur(24px)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(40px)}.backdrop-blur-2xl,.backdrop-blur-3xl{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(64px)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-shadow{transition-duration:.15s;transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-none{transition-property:none}.fade,.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{opacity:0;transition-duration:.15s;transition-property:opacity,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{--tw-scale-x:.9;--tw-scale-y:.9}.fade-from-bottom,.fade-from-center,.fade-from-left,.fade-from-right,.fade-from-top{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))}.fade-from-bottom{--tw-translate-y:25%}.fade-from-top{--tw-translate-y:-25%}.fade-from-left,.fade-from-top{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))}.fade-from-left{--tw-translate-x:-25%}.fade-from-right{--tw-translate-x:25%}.fade-from-bottom.in,.fade-from-center.in,.fade-from-left.in,.fade-from-right,.fade-from-right.in,.fade-from-top.in,.fade.in{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))}.fade-from-bottom.in,.fade-from-center.in,.fade-from-left.in,.fade-from-right.in,.fade-from-top.in,.fade.in{--tw-translate-x:0px;--tw-translate-y:0px;--tw-scale-x:1;--tw-scale-y:1;opacity:1}.duration-75{transition-duration:75ms}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-1000{transition-duration:1s}@keyframes -spin{to{transform:rotate(1turn)}}.animate-spin,.spin{animation:-spin 1s linear infinite}@keyframes -ping{75%,to{opacity:0;transform:scale(2)}}.animate-ping,.ping{animation:-ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes -pulse{50%{opacity:.5}}.animate-pulse,.pulse{animation:-pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes -bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.animate-bounce,.bounce{animation:-bounce 1s infinite}.scale-0{--tw-scale-x:0;--tw-scale-y:0}.scale-0,.scale-50{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))}.scale-50{--tw-scale-x:.5;--tw-scale-y:.5}.scale-75{--tw-scale-x:.75;--tw-scale-y:.75}.scale-75,.scale-90{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))}.scale-90{--tw-scale-x:.9;--tw-scale-y:.9}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100,.scale-95{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))}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.scale-105,.scale-110{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))}.scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.scale-125{--tw-scale-x:1.25;--tw-scale-y:1.25}.scale-125,.scale-150{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))}.scale-150{--tw-scale-x:1.5;--tw-scale-y:1.5}.flip-x{--tw-scale-x:-1}.flip-x,.flip-y{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))}.flip-y{--tw-scale-y:-1}.rotate-0{--tw-rotate:0deg}.rotate-0,.rotate-1{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))}.rotate-1{--tw-rotate:1deg}.rotate-2{--tw-rotate:2deg}.rotate-2,.rotate-3{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))}.rotate-3{--tw-rotate:3deg}.rotate-6{--tw-rotate:6deg}.rotate-12,.rotate-6{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))}.rotate-12{--tw-rotate:12deg}.rotate-45{--tw-rotate:45deg}.rotate-45,.rotate-90{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))}.rotate-90{--tw-rotate:90deg}.rotate-135{--tw-rotate:135deg}.rotate-135,.rotate-180{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))}.rotate-180{--tw-rotate:180deg}.rotate-270{--tw-rotate:270deg;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))}.basis-0{flex-basis:0px}.basis-1{flex-basis:0.25rem}.basis-2{flex-basis:0.5rem}.basis-3{flex-basis:0.75rem}.basis-4{flex-basis:1rem}.basis-5{flex-basis:1.25rem}.basis-6{flex-basis:1.5rem}.basis-7{flex-basis:1.75rem}.basis-8{flex-basis:2rem}.basis-9{flex-basis:2.25rem}.basis-10{flex-basis:2.5rem}.basis-11{flex-basis:2.75rem}.basis-12{flex-basis:3rem}.basis-14{flex-basis:3.5rem}.basis-16{flex-basis:4rem}.basis-20{flex-basis:5rem}.basis-24{flex-basis:6rem}.basis-28{flex-basis:7rem}.basis-32{flex-basis:8rem}.basis-36{flex-basis:9rem}.basis-40{flex-basis:10rem}.basis-44{flex-basis:11rem}.basis-48{flex-basis:12rem}.basis-52{flex-basis:13rem}.basis-56{flex-basis:14rem}.basis-60{flex-basis:15rem}.basis-64{flex-basis:16rem}.basis-72{flex-basis:18rem}.basis-80{flex-basis:20rem}.basis-96{flex-basis:24rem}.basis-auto{flex-basis:auto}.basis-px{flex-basis:1px}.basis-0\.5{flex-basis:0.125rem}.basis-1\.5{flex-basis:0.375rem}.basis-2\.5{flex-basis:0.625rem}.basis-3\.5{flex-basis:0.875rem}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.333333%}.basis-2\/3{flex-basis:66.666667%}.basis-1\/4{flex-basis:25%}.basis-2\/4{flex-basis:50%}.basis-3\/4{flex-basis:75%}.basis-1\/5{flex-basis:20%}.basis-2\/5{flex-basis:40%}.basis-3\/5{flex-basis:60%}.basis-4\/5{flex-basis:80%}.basis-1\/6{flex-basis:16.666667%}.basis-2\/6{flex-basis:33.333333%}.basis-3\/6{flex-basis:50%}.basis-4\/6{flex-basis:66.666667%}.basis-5\/6{flex-basis:83.333333%}.basis-1\/12{flex-basis:8.333333%}.basis-2\/12{flex-basis:16.666667%}.basis-3\/12{flex-basis:25%}.basis-4\/12{flex-basis:33.333333%}.basis-5\/12{flex-basis:41.666667%}.basis-6\/12{flex-basis:50%}.basis-7\/12{flex-basis:58.333333%}.basis-8\/12{flex-basis:66.666667%}.basis-9\/12{flex-basis:75%}.basis-10\/12{flex-basis:83.333333%}.basis-11\/12{flex-basis:91.666667%}.basis-full{flex-basis:100%}.row{display:flex;flex-direction:row}.center,.col{display:flex;flex-direction:column}.center,.center-row{align-items:center;justify-content:center}.center-row{display:flex;flex-direction:row}.center-x,.center-y{align-items:center;display:flex}.center-y{flex-direction:column}.row-reverse{display:flex;flex-direction:row-reverse}.col-reverse{display:flex;flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-initial{flex:0 1 auto}.flex-none{flex:none}.grow{flex-grow:1!important}.grow-0{flex-grow:0!important}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-7{order:7}.order-8{order:8}.order-9{order:9}.order-10{order:10}.order-11{order:11}.order-12{order:12}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.gap-0{gap:0}.gap-x-0{-moz-column-gap:0;column-gap:0}.gap-y-0{row-gap:0}.gap-px{gap:1px}.gap-x-px{-moz-column-gap:1px;column-gap:1px}.gap-y-px{row-gap:1px}.gap-0\.5{gap:.125rem}.gap-x-0\.5{-moz-column-gap:.125rem;column-gap:.125rem}.gap-y-0\.5{row-gap:.125rem}.gap-3px{gap:3px}.gap-1{gap:.25rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-y-1{row-gap:.25rem}.gap-1\.5{gap:.375rem}.gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-y-1\.5{row-gap:.375rem}.gap-2{gap:.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-2{row-gap:.5rem}.gap-2\.5{gap:.625rem}.gap-x-2\.5{-moz-column-gap:.625rem;column-gap:.625rem}.gap-y-2\.5{row-gap:.625rem}.gap-3{gap:.75rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-3{row-gap:.75rem}.gap-3\.5{gap:.875rem}.gap-x-3\.5{-moz-column-gap:.875rem;column-gap:.875rem}.gap-y-3\.5{row-gap:.875rem}.gap-4{gap:1rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-4{row-gap:1rem}.gap-5{gap:1.25rem}.gap-x-5{-moz-column-gap:1.25rem;column-gap:1.25rem}.gap-y-5{row-gap:1.25rem}.gap-6{gap:1.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-6{row-gap:1.5rem}.gap-7{gap:1.75rem}.gap-x-7{-moz-column-gap:1.75rem;column-gap:1.75rem}.gap-y-7{row-gap:1.75rem}.gap-8{gap:2rem}.gap-x-8{-moz-column-gap:2rem;column-gap:2rem}.gap-y-8{row-gap:2rem}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.justify-evenly{justify-content:space-evenly}.self-auto{align-self:auto}.self-start{align-self:flex-start}.self-end{align-self:flex-end}.self-center{align-self:center}.self-stretch{align-self:stretch}.self-baseline{align-self:baseline}.content-center{align-content:center}.content-start{align-content:flex-start}.content-end{align-content:flex-end}.content-between{align-content:space-between}.content-around{align-content:space-around}.content-evenly{align-content:space-evenly}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}@font-face{font-display:fallback;font-family:D-DIN-PRO;font-weight:400;src:url(./fonts/D-DIN-PRO-400-Regular.otf)}@font-face{font-display:fallback;font-family:D-DIN-PRO;font-weight:500;src:url(./fonts/D-DIN-PRO-700-Bold.otf)}.num{font-family:D-DIN-PRO}@font-face{font-display:fallback;font-family:Nunito;font-weight:400;src:url(./fonts/Nunito-Regular.ttf)}@font-face{font-display:fallback;font-family:Nunito;font-weight:700;src:url(./fonts/Nunito-bold.ttf)}@font-face{font-display:fallback;font-family:Nunito;font-weight:300;src:url(./fonts/Nunito-Light.ttf)}.font-brand{font-family:Nunito;font-weight:400}@supports not (gap:4px){.gap-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1px*(1 - var(--tw-space-x-reverse)));margin-right:calc(1px*var(--tw-space-x-reverse))}.gap-0\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2px*(1 - var(--tw-space-x-reverse)));margin-right:calc(2px*var(--tw-space-x-reverse))}.gap-3px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(3px*(1 - var(--tw-space-x-reverse)));margin-right:calc(3px*var(--tw-space-x-reverse))}.gap-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}.gap-1\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(6px*(1 - var(--tw-space-x-reverse)));margin-right:calc(6px*var(--tw-space-x-reverse))}.gap-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.gap-2\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(10px*(1 - var(--tw-space-x-reverse)));margin-right:calc(10px*var(--tw-space-x-reverse))}.gap-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.75rem*var(--tw-space-x-reverse))}.gap-3\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(14px*(1 - var(--tw-space-x-reverse)));margin-right:calc(14px*var(--tw-space-x-reverse))}.gap-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1rem*var(--tw-space-x-reverse))}.gap-5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.25rem*var(--tw-space-x-reverse))}.gap-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.gap-7>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.75rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.75rem*var(--tw-space-x-reverse))}.gap-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(2rem*var(--tw-space-x-reverse))}.col.gap-px>:not([hidden])~:not([hidden]),.gap-px.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1px*var(--tw-space-y-reverse));margin-top:calc(1px*(1 - var(--tw-space-y-reverse)))}.col.gap-0\.5>:not([hidden])~:not([hidden]),.gap-0\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2px*var(--tw-space-y-reverse));margin-top:calc(2px*(1 - var(--tw-space-y-reverse)))}.col.gap-3px>:not([hidden])~:not([hidden]),.gap-3px.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(3px*var(--tw-space-y-reverse));margin-top:calc(3px*(1 - var(--tw-space-y-reverse)))}.col.gap-1>:not([hidden])~:not([hidden]),.gap-1.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.col.gap-1\.5>:not([hidden])~:not([hidden]),.gap-1\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(6px*var(--tw-space-y-reverse));margin-top:calc(6px*(1 - var(--tw-space-y-reverse)))}.col.gap-2>:not([hidden])~:not([hidden]),.gap-2.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.col.gap-2\.5>:not([hidden])~:not([hidden]),.gap-2\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(10px*var(--tw-space-y-reverse));margin-top:calc(10px*(1 - var(--tw-space-y-reverse)))}.col.gap-3>:not([hidden])~:not([hidden]),.gap-3.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.col.gap-3\.5>:not([hidden])~:not([hidden]),.gap-3\.5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(14px*var(--tw-space-y-reverse));margin-top:calc(14px*(1 - var(--tw-space-y-reverse)))}.col.gap-4>:not([hidden])~:not([hidden]),.gap-4.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.col.gap-5>:not([hidden])~:not([hidden]),.gap-5.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.col.gap-6>:not([hidden])~:not([hidden]),.gap-6.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.5rem*var(--tw-space-y-reverse));margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)))}.col.gap-7>:not([hidden])~:not([hidden]),.gap-7.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.75rem*var(--tw-space-y-reverse));margin-top:calc(1.75rem*(1 - var(--tw-space-y-reverse)))}.col.gap-8>:not([hidden])~:not([hidden]),.gap-8.flex-row>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.nav-item>a>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.25rem*var(--tw-space-x-reverse))}}